Python worker 하나만 보면 메모리가 버틸 만한데 worker 수를 늘리자마자 node 전체 메모리가 급격히 커진다면, 한 프로세스의 전형적인 leak보다 worker 간 duplication이 핵심인 경우가 많습니다.
짧게 말하면, 먼저 프로세스 하나가 계속 커지는 문제인지 여러 worker가 같은 큰 데이터를 반복해서 들고 있는 문제인지 구분한 뒤, preload 동작과 process model, 큰 shared object를 확인해야 합니다.
먼저 한 프로세스가 큰 건지, 많은 프로세스가 같은 데이터를 반복하는 건지 구분하세요
이 구분이 조사 방향을 크게 바꿉니다.
worker 하나가 계속 커진다면 memory leak, cache growth, retained object를 의심해야 합니다. 반대로 worker 각각은 안정적인데 worker 수만큼 총 메모리가 거의 선형으로 증가한다면 duplication 쪽일 가능성이 큽니다.
이 패턴은 Gunicorn, Celery prefork pool, multi-process Python 배포에서 특히 흔합니다.
worker memory duplication이 생기는 흔한 이유
1. 각 worker가 같은 큰 데이터를 따로 로드함
모델, lookup table, in-memory cache, 큰 config, precomputed data가 worker마다 독립적으로 올라오는 경우가 많습니다.
worker 하나만 보면 괜찮아 보여도 4개, 8개로 늘리면 전체 footprint는 빠르게 커집니다.
2. preload 동작을 잘못 이해하고 있음
preload를 쓰면 duplication이 자동으로 사라질 거라고 기대하기 쉽지만, 실제로는 fork 전에 로드되고 이후 거의 read-only로 유지되는 객체에서만 도움이 큽니다. 이후에 mutation이 일어나거나 worker별로 새 상태가 붙으면 per-worker memory는 다시 커질 수 있습니다.
즉 preload는 만능 해결책이 아니라 메모리 모양을 바꾸는 도구에 가깝습니다.
3. worker 수가 memory budget보다 빨리 늘어남
애플리케이션이 2 worker에서는 멀쩡하다가 8 worker에서 node memory incident를 만드는 것은 전혀 이상한 일이 아닙니다.
worker 증설은 throughput knob이기도 하지만, 동시에 memory budget을 직접 소모하는 변화입니다.
4. cache와 connection-local state가 worker마다 따로 생김
database client, request cache, LRU store, framework cache는 process-local 상태로 worker마다 따로 존재하는 경우가 많습니다.
어느 정도는 정상 동작이지만 운영 관점에서는 여전히 중요한 비용입니다.
5. duplication과 일반 memory growth가 함께 존재함
각 worker가 큰 기본 footprint를 가진 상태에서 cache churn이나 retained object 때문에 시간이 지나며 더 커지는 혼합 패턴도 자주 나옵니다.
그래서 duplication을 찾았다고 해서 조사를 바로 끝내면 안 됩니다.
실전 점검 순서
1. worker별 메모리와 host 전체 메모리를 같이 보세요
worker 하나가 400MB이고 worker가 8개라면, node cost는 400MB가 아닙니다. 곱해진 footprint와 추가 overhead를 같이 봐야 합니다.
많은 incident가 per-process 숫자만 보다가 node-level 총량에서 갑자기 크게 느껴집니다.
2. fork 전과 worker 시작 후 무엇이 로드되는지 확인하세요
아래 질문이 핵심입니다.
- module import 단계에서 어떤 객체가 생성되는가
- worker startup hook에서 무엇이 초기화되는가
- 첫 요청이나 첫 task 이후에 lazy 하게 무엇이 만들어지는가
이 지점에서 preload의 의미가 생깁니다. fork 전에 로드된 read-only 객체는 더 효율적으로 공유될 수 있지만, worker 시작 후 생성되거나 자주 변경되는 객체는 그렇지 않습니다.
3. worker 수 증가와 memory incident 시점을 비교하세요
concurrency 증가 직후 memory pressure가 시작됐다면, leak보다 multiplication이 더 큰 원인일 가능성이 높습니다.
생각보다 자주 놓치는 부분입니다.
4. 가장 큰 shared object와 cache를 찾으세요
특히 아래 항목을 확인하세요.
- 큰 ML model
- lookup table
- in-memory result cache
- ORM metadata나 큰 config tree
- 큰 request 또는 task payload retention
핵심 질문은 “무엇이 큰가”뿐 아니라 “그것을 모든 worker가 따로 들고 있는가”입니다.
5. 해법이 worker 감축인지, 객체 축소인지, 다른 concurrency 모델인지 결정하세요
어떤 경우엔 worker 수를 줄이는 게 가장 낫고, 어떤 경우엔 큰 데이터를 per-worker memory 밖으로 빼는 게 더 낫습니다. 또 preload 방식이나 process 전략을 바꾸는 편이 좋은 경우도 있습니다.
정답은 throughput과 memory efficiency 중 무엇이 더 중요한지에 따라 달라집니다.
패턴을 찾은 뒤 어떻게 바꿀지
모든 worker가 같은 큰 데이터를 로드하는 경우
per-worker footprint를 줄이고, 큰 optional feature를 분리하고, 공유 상태를 매 worker 메모리 밖의 서비스나 저장소로 옮기는 방법을 검토하세요.
preload가 일부만 도움이 되는 경우
가능하면 객체를 read-only에 가깝게 유지하고, fork 이후 큰 shared structure를 자주 mutation 하지 않도록 설계하는 편이 copy-on-write 효과를 더 오래 유지하는 데 도움이 됩니다.
worker 수가 node budget에 비해 너무 높은 경우
worker 수를 줄이거나 더 큰 node로 옮겨야 합니다. memory budget을 넘긴 상태에서 worker를 계속 늘리는 것은 지속 가능한 해결책이 아닙니다.
각 프로세스 내부 memory growth도 함께 큰 경우
이때는 duplication만이 아니라 일반 메모리 증가도 함께 보는 편이 맞습니다. Python memory usage high 글과 같이 보세요.
빠른 체크리스트
worker memory duplication이 의심될 때는 아래 순서가 가장 실용적입니다.
- worker별 메모리와 host 전체 메모리를 같이 본다
- 큰 객체가 모든 worker에 존재하는지 확인한다
- preload와 post-fork initialization 동작을 점검한다
- worker 수 증가와 incident 시점을 비교한다
- per-worker state 축소와 worker 감축 중 무엇이 맞는지 결정한다
FAQ
Q. 이건 항상 leak인가요?
아닙니다. process-based concurrency에서 생기는 예상 가능한 duplication인 경우가 많습니다.
Q. preload가 항상 해결해주나요?
아닙니다. 이후 mutation과 process-local state가 여전히 중요합니다.
Q. 가장 빠른 첫 단계는 무엇인가요?
worker 하나의 메모리와 현재 worker 수를 함께 봐서 node 전체 비용을 계산하는 것입니다.
Q. 코드 변경이 없었는데 worker를 늘리자마자 메모리가 튄 이유는 뭔가요?
중복된 in-memory state가 worker 수만큼 거의 선형으로 늘어났기 때문일 가능성이 큽니다.
Read Next
- 각 프로세스 내부 메모리 증가도 크다면 Python memory usage high를 보세요.
- worker platform 자체 이슈도 의심된다면 Python gunicorn workers restarting를 확인하세요.
- 더 넓은 흐름은 Python troubleshooting guide에서 이어서 볼 수 있습니다.
Related Posts
- Python memory usage high
- Python gunicorn workers restarting
- Python Celery worker concurrency too low
- Python 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를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.