[Loki] 컨텍스트 취소 시 downstreamer goroutine 누수 방지
PR 링크: grafana/loki#21062 상태: Merged | 변경: +77 / -1
들어가며
Go에서 unbuffered 채널에 수신자 없이 데이터를 보내려 하면 goroutine이 영원히 블로킹됩니다. Loki의 sharded 쿼리에서 컨텍스트가 취소되면 For 함수의 메인 루프는 즉시 반환하지만, 백그라운드 goroutine이 에러를 unbuffered 채널에 보내려다 영영 멈추게 됩니다.
핵심 코드 분석
Before (downstreamer.go):
if err != nil {
ch <- logql.Resp{
I: -1,
Err: err,
}
}
close(ch)
After:
if err != nil {
select {
case ch <- logql.Resp{
I: -1,
Err: err,
}:
case <-ctx.Done():
}
}
close(ch)
select 문이 두 가지 경우를 동시에 감시합니다:
- 채널 전송이 성공하면 에러가 정상적으로 전달됩니다
- 컨텍스트가 취소되어 수신자가 없으면
ctx.Done()채널이 닫히면서 goroutine이 안전하게 종료됩니다
테스트: 100번 반복으로 누수 검증
func TestCancelDoesNotLeakGoroutines(t *testing.T) {
baseline := runtime.NumGoroutine()
for i := 0; i < 100; i++ {
ctx, cancel := context.WithCancel(context.Background())
// ... 쿼리 시작 후 즉시 취소
cancel()
<-done
}
require.Eventually(t, func() bool {
return runtime.NumGoroutine() <= baseline+10
}, 10*time.Second, 100*time.Millisecond)
}
왜 이게 좋은가
- 노드 업그레이드, 롤링 드레인 등으로 취소된 쿼리가 많아지면 goroutine이 수십만 개까지 누적되어 메모리와 레이턴시가 악화됩니다
- 수정은 단일
select문 추가(6줄)로 매우 간결합니다 close(ch)는select문 바깥에서 항상 실행되므로 채널 정리가 보장됩니다- 100회 반복 테스트로 누수가 확실히 해결되었음을 검증합니다
- Go에서 unbuffered 채널을 사용할 때 항상
select와ctx.Done()을 함께 사용해야 하는 좋은 예시입니다
참고 자료
관련 포스트
PR Analysis 의 다른글
- 이전글 [Axolotl] MXFP4 양자화 지원 추가
- 현재글 : [Loki] 컨텍스트 취소 시 downstreamer goroutine 누수 방지
- 다음글 [feast] Feast 성능 최적화: Timestamp 변환 비용 절감으로 온라인 피처 서빙 가속화
댓글