Golang Context Cancelled Too Early: 무엇부터 확인할까
마지막 업데이트

Golang Context Cancelled Too Early: 무엇부터 확인할까


Go 에서 context canceled 가 너무 빨리 보인다면 문제는 보통 cancel 자체가 아니라 scope 와 ownership 입니다. parent context 가 실제 작업보다 먼저 끝나거나, timeout 이 실제 경로보다 너무 짧게 잡힌 경우가 많습니다.

짧게 말하면 핵심은 이것입니다. 누가 context 를 소유하고 있고, cancellation 이 어디서 시작되는지 먼저 봐야 합니다. context cancellation 은 runtime 입장에선 보통 정상 동작입니다. 진짜 질문은 올바른 context 가 올바른 작업을 소유하고 있는가입니다.


parent lifetime 과 ownership 부터 본다

많은 context canceled incident 는 사실 lifecycle bug 입니다.

알아야 할 것은 이렇습니다.

  • 누가 context 를 만든다
  • 누가 cancel 을 호출한다
  • 어떤 작업이 그 context 에 묶여 있다
  • 그 작업이 정말 parent 와 함께 멈춰야 하는가

이 ownership map 이 없으면 context bug 는 랜덤한 실패처럼 보이기 쉽습니다.


early cancellation 은 실전에서 어떻게 보이나

운영 환경에서는 보통 이렇게 보입니다.

  • request 가 끝나면서 background work 가 같이 죽는다
  • retry 나 fan-out call 이 너무 많은 작업을 일찍 취소한다
  • dependency 는 멀쩡해 보이는데 handler 에서 context canceled 가 나온다
  • shutdown logic 이 끝나도 되는 작업까지 너무 빨리 멈춘다

이런 incident 가 놀랍게 느껴지는 이유는 코드에서 context lifetime 이 충분히 드러나지 않기 때문입니다.


흔한 원인

1. parent context 의 수명이 너무 짧다

request-scoped context 는 팀이 생각하는 것보다 훨씬 짧은 범위만 제어하는 경우가 많습니다.

request 보다 오래 살아야 하는 작업에 r.Context() 를 그대로 쓰는 건 자주 틀립니다.

2. timeout 설정이 너무 공격적이다

configured deadline 이 실제 dependency 또는 worker path 보다 짧을 수 있습니다.

이 경우 cancellation 은 미스터리가 아니라 예측 가능한 결과입니다.

3. background work 가 잘못된 context 를 쓴다

request 하나를 넘어 살아야 하는 task 가 실수로 request cancellation 을 상속받을 수 있습니다.

ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

go runBackgroundJob(ctx) // request 가 끝나면 같이 멈출 수 있음

background work 가 request 보다 오래 살아야 한다면 r.Context() 를 그대로 물리면 안 됩니다.

4. retry 와 fan-out path 가 cancellation pressure 를 키운다

병렬 호출이 많아지면 짧은 parent deadline 하나가 너무 많은 child work 를 한꺼번에 취소할 수 있습니다.

특히 서로 다른 latency profile 을 가진 downstream call 들을 하나의 parent context 가 묶을 때 자주 아픕니다.

5. shutdown flow 의 cancellation boundary 가 불분명하다

graceful drain 되어야 할 worker 가 더 넓은 shutdown path 의 abrupt cancellation 을 상속받을 수 있습니다.


실전 점검 순서

1. 누가 context 를 만들고 누가 cancel 하는지 찾는다

가장 중요한 첫 단계입니다.

owner 를 모르면 모든 cancellation 이 임의적으로 보입니다.

2. 실제 작업 시간과 timeout / deadline 설정을 비교한다

deadline 이 정상 경로보다 짧다면 early cancellation 은 예상된 동작입니다.

3. background task 가 request-scoped context 를 상속받는지 본다

가장 아픈 Go lifecycle bug 들이 여기서 많이 드러납니다.

4. fan-out 과 retry path 에서 parent cancellation 이 너무 이른지 확인한다

짧은 parent deadline 하나가 여러 child 를 함께 과도하게 취소할 수 있습니다.

5. 멈춰야 하는 작업에만 cancellation 이 도달하는지 확인한다

마지막 질문은 cancellation boundary 가 실제 ownership 과 맞는가입니다.


ownership bug 를 찾은 뒤 무엇을 바꾸면 좋나

parent 수명이 너무 짧다면

작업을 올바른 lifecycle 을 가진 context 에 다시 묶어야 합니다.

timeout 이 너무 공격적이라면

현실적인 latency 에 맞게 조정하거나 단계별 deadline 으로 나눠야 합니다.

background work 가 request context 를 상속받는다면

request 바깥의 explicit owner 를 줘야 합니다.

fan-out 이 과도하게 취소된다면

하나의 parent deadline 이 여러 child 를 어떻게 제어하는지 다시 봐야 합니다.

shutdown 이 너무 넓게 cancel 한다면

graceful drain context 와 hard-stop context 를 분리해야 합니다.


장애 중에 던져볼 질문

이 질문이 꽤 유용합니다.

이 작업은 정말 이 parent context 가 끝날 때 같이 멈춰야 하나, 아니면 실제 job 보다 더 짧은 lifetime 을 잘못 상속받은 건가?

이 질문이 error text 만 보는 것보다 훨씬 빨리 real bug 를 드러냅니다.


FAQ

Q. background work 가 request context 를 써도 되나

그 작업이 request 와 함께 멈춰야 할 때만 그렇습니다.

Q. 가장 빠른 첫 단계는 무엇인가

context creator 를 찾고, 그 lifetime 과 제어하는 작업의 길이를 비교하는 것입니다.

Q. early cancellation 은 항상 timeout 문제인가

아닙니다. ownership 과 scope 실수도 매우 흔합니다.

Q. cancellation 이 “정상” 인데 왜 버그인가

runtime 은 정상인데 코드가 잘못된 작업을 잘못된 lifetime 에 묶었을 수 있기 때문입니다.


Sources:

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.