Repository navigation
fix(realtime): 멈춘 SSE 구독자가 전체 fan-out 을 세우던 문제 - #270
Merged
Merged
Conversation
RealTime(Go) 을 9/8 이후 처음 다시 봤다. 서로 물린 결함 둘을 찾았다. ## 1. SSE 쓰기에 데드라인이 없었다 WS 는 처음부터 WSWriteTimeout(10s)이 있었는데 SSE 는 없다. 멈춘 클라이언트 (면접 중 노트북 덮개를 닫는 경우)는 TCP 를 열어둔 채 수신 창을 막으므로 Fprintf 가 무기한 블록된다 — OS 가 포기할 때까지 수 분이 걸릴 수 있다. 그 고루틴이 sub.Ch 를 비우지 못하면 버퍼(16)가 차고, 그 채널로 가는 모든 Dispatch 가 slow-consumer 타임아웃을 문다. ## 2. slowTimeout 이 구독자마다 리셋됐다 Dispatch 가 구독자마다 타이머를 새로 세서, 멈춘 탭 3개면 5초 × 3 = 15초가 걸렸다. **AMQP 컨슈머는 단일 스레드 + prefetch=1** 이라(messaging.Consumer) 여기서 막히는 시간이 곧 다른 세션들의 이벤트 지연이다. realtime/CLAUDE.md §9 는 "그 구독자만 drop, 다른 구독자는 영향 없음" 이라고 적고 있었는데 사실이 아니었다. 같은 채널의 뒷 구독자도, 다른 채널의 세션도 같이 밀렸다. ## 고친 방법 - SSE: http.NewResponseController 로 쓰기마다 데드라인(REALTIME_SSE_WRITE_TIMEOUT, 기본 10s). 데드라인을 지원하지 않는 ResponseWriter(테스트 recorder 등)에서는 기존 동작을 유지한다 — 스트림을 실패시키지 않는다. - Dispatch: slowTimeout 을 **호출 전체의 예산**으로. 멈춘 구독자가 몇이든 한 메시지당 최악 한 번의 slowTimeout 이다. - 예산을 다 써도 남은 구독자에게 논블로킹 전송은 시도한다. 앞사람이 멈췄다는 이유로 건강한 구독자가 이벤트를 잃으면 안 된다. ## 테스트 예산이 구독자 수만큼 곱해지지 않는다 (멈춘 구독자 3명 × 150ms 예산) 앞이 멈춰도 뒤의 건강한 구독자는 이벤트를 받는다 구독자 없는 채널은 기다리지 않는다 가드가 실제로 회귀를 잡는지 확인했다 — 예산을 구독자마다 리셋하도록 되돌리니 첫 테스트가 실패했다. go test -race ./... 전부 통과. 컨슈머 동시성이 본업인 서버라 -race 는 필수다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RealTime(Go)을 9/8 이후 처음 다시 봤다. 서로 물린 결함 둘을 찾았다.
1. SSE 쓰기에 데드라인이 없었다
WS 는 처음부터
WSWriteTimeout(10s)이 있었는데 SSE 는 0건이었다.멈춘 클라이언트(면접 중 노트북 덮개를 닫는 경우)는 TCP 를 열어둔 채 수신 창을 막으므로
Fprintf가 무기한 블록된다 — OS 가 포기할 때까지 수 분이 걸릴 수 있다. 그 고루틴이sub.Ch를 비우지 못하면 버퍼(16)가 차고, 그 채널로 가는 모든Dispatch가 slow-consumer타임아웃을 문다.
2.
slowTimeout이 구독자마다 리셋됐다멈춘 탭 3개면 5초 × 3 = 15초. 그리고 이게 중요한 이유:
AMQP 컨슈머는 단일 스레드 + prefetch=1 이다. 여기서 막히는 시간이 곧 다른 세션들의
이벤트 지연이다.
realtime/CLAUDE.md §9는 이렇게 적고 있었다:사실이 아니었다. 같은 채널의 뒷 구독자도, 다른 채널의 세션도 같이 밀렸다.
고친 방법
http.NewResponseController로 쓰기마다 데드라인 (REALTIME_SSE_WRITE_TIMEOUT, 기본 10s)slowTimeout을 호출 전체의 예산으로 — 멈춘 구독자가 몇이든 메시지당 최악 한 번마지막 항목이 중요하다: 앞사람이 멈췄다는 이유로 건강한 구독자가 이벤트를 잃으면 안 된다.
데드라인을 지원하지 않는
ResponseWriter(테스트 recorder, 일부 미들웨어)에서는 기존 동작을유지한다 — 관측 기능 때문에 스트림을 실패시키지 않는다.
테스트
가드가 실제로 회귀를 잡는지 확인했다 — 예산을 구독자마다 리셋하도록 되돌리니 첫 테스트가
실패했다.
go test -race ./...전부 통과. 컨슈머 동시성이 본업인 서버라-race는 필수다.함께 확인한 것 (문제 없음)
sub.Ch를 닫는 코드가 없다 →Unsubscribe와Dispatch가 겹쳐도 send-on-closed 패닉이 없다Dispatch가 락 밖에서 전송한다 (복사 후 RUnlock)time.After였다면 (구독자 × 이벤트)마다 타이머 고루틴 누수)