Golang Mutex Contention High: 무엇부터 확인할까
마지막 업데이트

Golang Mutex Contention High: 무엇부터 확인할까


Go 서비스에서 mutex contention 이 높게 보일 때 문제는 대개 mutex primitive 자체가 아닙니다. 더 흔한 경우는 hot shared path, lock 을 너무 오래 잡는 critical section, 혹은 하나의 state boundary 를 두고 너무 많은 goroutine 이 몰리는 상황입니다.

짧게 말하면 핵심은 이것입니다. 어떤 lock 이 가장 뜨겁고, critical section 안에 얼마나 오래 머무는지 먼저 찾아야 합니다. 많은 mutex incident 는 사실 shared-state 설계 문제가 locking symptom 으로 보이는 경우입니다.


hot lock 과 hold time 부터 본다

lock 이 바빠졌다는 것은 보통 보호하는 경로가 너무 중심적이거나 너무 느리다는 뜻입니다.

그래서 아래 질문이 “mutex 를 바꿔야 하나?” 보다 훨씬 중요합니다.

  • lock 을 얼마나 자주 잡는가
  • 얼마나 오래 잡는가
  • 잡고 있는 동안 무슨 일을 하는가
  • 몇 개의 goroutine 이 동시에 경쟁하는가

이 답이 없으면 primitive 교체는 대부분 추측에 가깝습니다.


high contention 은 실전에서 어떻게 보이나

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

  • 동시성이 올라갈수록 latency spike
  • 하나의 shared path 뒤에 많은 goroutine 이 줄을 섬
  • CPU 는 완전히 차지 않았는데 throughput 이 멈춘다
  • 특정 cache, map, coordinator 가 bottleneck 이 된다
  • profile 에서 useful work 보다 wait 시간이 많아진다

즉 contention 은 단순히 unlucky 한 lock call 하나보다 더 넓은 문제인 경우가 많습니다.


흔한 원인

1. critical section 이 너무 길다

lock 안의 I/O, allocation, heavy computation 이 contention 을 빠르게 키울 수 있습니다.

mu.Lock()
defer mu.Unlock()

data, err := http.Get(url) // lock 을 잡은 채 느린 호출

network 나 disk 작업이 critical section 안에 있으면 hot lock 하나가 시스템 전반의 contention 으로 번질 수 있습니다.

2. 하나의 shared structure 가 너무 뜨겁다

많은 goroutine 이 아래 하나를 두고 싸울 수 있습니다.

  • map
  • cache
  • coordinator object
  • metrics 또는 state aggregator

lock 이 짧아도 많은 request path 가 거기에 의존하면 충분히 문제가 됩니다.

3. lock scope 가 필요 이상으로 넓다

공유 mutation 에 꼭 필요한 범위를 넘어서 너무 많은 일을 lock 아래서 보호하고 있을 수 있습니다.

예를 들면:

  • read 와 write 가 같은 큰 lock 을 쓴다
  • validation, transformation 도 lock 안에서 한다
  • 편의 코드가 시간이 지나며 critical section 을 키운다

4. downstream wait 를 lock 안에서 한다

가장 나쁜 contention 패턴 중 하나입니다.

예를 들면:

  • HTTP call
  • DB call
  • file access
  • channel wait

5. goroutine 수가 같은 bottleneck 을 더 키운다

모든 goroutine 이 같은 locked path 로 모인다면 concurrency 를 더 늘려도 도움이 되지 않습니다.

오히려 같은 contention 만 더 시끄럽게 만들 수 있습니다.


실전 점검 순서

1. 어떤 lock 또는 path 가 가장 뜨거운지 찾는다

가장 많은 waiting 을 모으는 shared state 부터 찾아야 합니다.

2. critical section 안에 얼마나 오래 머무는지 측정한다

짧지만 자주 잡는 lock 과, 드물지만 오래 잡는 lock 은 실패 모양이 다릅니다.

지금 어떤 유형인지 알아야 합니다.

3. lock 을 잡은 상태에서 blocking call 이 있는지 본다

Go mutex incident 에서 가장 가치가 큰 체크 중 하나입니다.

lock 안에서 외부 시스템을 기다리면, 진짜 bottleneck 은 프로세스 바깥에 있을 수도 있습니다.

4. shared-state 설계와 실제 access pattern 을 비교한다

너무 많은 트래픽이 하나의 object 또는 coordinator 를 통과하도록 짜여 있지 않은지 보세요.

5. primitive 를 바꾸기 전에 lock scope 를 줄인다

대부분의 첫 fix 는 sync.Mutex 를 버리는 것이 아니라, 보호하는 작업량을 줄이는 것입니다.


hot path 를 찾은 뒤 무엇을 바꾸면 좋나

critical section 이 너무 길다면

느린 작업을 lock 밖으로 빼야 합니다.

하나의 shared structure 가 너무 중심적이라면

shard 하거나 ownership 을 나누거나, 그 object 에 의존하는 트래픽을 줄여야 합니다.

lock scope 가 너무 넓다면

공유 mutation 에 꼭 필요한 부분만 보호해야 합니다.

goroutine 수가 contention 을 키운다면

worker 를 더 늘리기보다 concurrency 또는 설계를 먼저 조정해야 합니다.

실제 문제가 blocked coordination 이라면

mutex 만의 문제가 아니라 더 넓은 concurrency incident 로 봐야 합니다.


장애 중에 던져볼 질문

이 질문이 꽤 유용합니다.

이 lock 이 사라져도 lock 안의 작업 자체가 느려서 여전히 느릴까, 아니면 lock 이 병목의 핵심일까?

이 질문이 bad critical-section design 과 primitive suspicion 을 잘 가릅니다.


FAQ

Q. mutex 를 바로 바꿔야 하나

primitive 보다 lock scope 나 shared-state 설계가 원인인 경우가 훨씬 많아서 보통은 아닙니다.

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

가장 뜨거운 lock 과 critical section 길이를 찾는 것입니다.

Q. goroutine 을 더 늘리면 해결되나

같은 shared path 뒤에 더 많이 몰린다면 오히려 악화될 수 있습니다.

Q. mutex contention 은 항상 CPU 문제인가

아닙니다. CPU 를 태우기보다 대기 시간이 늘어나면서도 충분히 심각할 수 있습니다.


Sources:

먼저 읽어볼 가이드

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