Go 1.27에 고루틴 누수 프로파일러 추가, /debug/pprof/goroutineleak로 프로덕션 누수까지 탐지
- Go 1.27이 goroutine leak profiler를 도입함. runtime/pprof의
goroutineleak프로파일 타입으로, 또는 net/http/pprof 핸들러를 이미 붙여둔 서비스라면/debug/pprof/goroutineleak엔드포인트로 실행 중인 프로그램에서 바로 수집 가능함 - 사람이 프로파일을 읽고 판단하던 기존 방식과 달리 정확하고 오탐이 거의 없지만, 채널이나 sync 패키지 기본 요소에 영구히 블록된 고루틴만 잡는다는 제약이 있음. 작성자는 이것만으로 실제 누수의 상당 부분을 덮는다고 봄
- 누수 고루틴은 해제 조건이 영원히 충족되지 않는 블록 상태이며, 쌓이면 고루틴 자체와 참조 메모리 때문에 메모리 사용량이 늘고 GC의 CPU 사용도 증가함. 특히 GOMEMLIMIT을 쓰는 서비스에서 문제가 커짐
- 테스트 단계에서는 우버의 goleak 라이브러리와 Go 1.25의 synctest 패키지가 누수를 잡아주지만, 둘 다 테스트가 예상하지 못한 방식으로 동작하는 대규모 프로덕션 시스템은 검사하지 못함
- 본문 예제는 에러 조기 반환으로 언버퍼드 채널의 송신 고루틴이 영원히 블록되는 코드이며, 우버 프로덕션 서비스를 포함한 실제 Go 프로그램에서 발견된 흔한 실수라고 밝힘
Hacker News opinions
다른 언어에서 주로 넘어와서 버그가 뭔지 한참 봤는데, Go 채널은 기본이 언버퍼드라서 송신하려면 받는 쪽이 기다리고 있어야 하고 그때까지 블록됨. 버퍼를 넣으면 블록이 풀려서 고루틴이 진행하고 결국 종료되니 누수도 안 생기는데, 버퍼 크기가 충분해야 함.
채널은 받는 쪽이 사라진 걸 인식하지 못하는 것 같더라. 수신자가 없어도 무한정 블록되고, 송신자와 수신자가 같은 객체라 그 채널은 GC도 안 됨.
고루틴은 GC 대상이 아니고, 코드에서 고루틴을 참조하거나 핸들로 잡을 방법도 없음. join 같은 기본 요소도 없어서 그냥 띄우면 추적이 안 됨.
두 번째 항목은 좀 더 미묘한데, 채널을 닫을 수는 있지만 단일 송신자일 때만 괜찮음. 수신자는 닫힌 채널을 우아하게 처리하지만(반복은 그냥 멈추고, 2값 수신으로 상태 확인 가능), 송신자가 닫힌 채널에 보내면 그대로 패닉임. 팬아웃용으로 설계된 셈이고, 안 닫는 것도 관용적으로 봄. Tour of Go에도 채널은 파일이 아니라서 보통 닫을 필요가 없다고 나옴.
<-chan TYPE이 수신 쪽, chan<- TYPE이 송신 쪽, 화살표 없는 chan TYPE은 둘 다로 쓸 수 있음. close()는 전송 끝을 알리는 거라 송신 쪽에서만 써야 하고, 수신 쪽을 명시적으로 닫는 방법은 없음. select로 논블로킹 송수신은 되지만 일반 송수신은 블로킹임.
일반 규칙 하나만 기억하면 됨. 고루틴이 어떻게 끝날지 모르면 절대 띄우지 마라.