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 를 태우기보다 대기 시간이 늘어나면서도 충분히 심각할 수 있습니다.
Read Next
- lock pressure 가 stuck worker coordination 으로 번진다면 Golang WaitGroup Stuck 을 이어서 보세요.
- blocked goroutine 이 shared state 주변에 계속 쌓인다면 Golang Goroutine Leak 과 비교해 보세요.
- 같은 workload 에서 queueing pressure 도 커진다면 Golang Worker Pool Backpressure 와 같이 보세요.
- 전체 Go 분기 지도는 Golang Troubleshooting Guide 에서 이어 볼 수 있습니다.
Related Posts
- Golang WaitGroup Stuck
- Golang Goroutine Leak
- Golang Worker Pool Backpressure
- Golang Troubleshooting Guide
Sources:
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.