티스토리 뷰
발단
최근 회사의 한 프로젝트를 크게 리팩토링 할 일이 있었는데, 해당 내용을 배포한 이후 메모리 누수 현상이 발생하기 시작했다.
메모리 사용량이 계속 오르기만 하다 결국 인스턴스가 죽어버린다.
추정 중인 원인은, gunicorn이 복잡한 request, 즉 응답에 시간이 오래 걸리는 request에 대한 메모리 해제를 제대로 해주지 못하는 것이다. 구글링을 좀 해보면, 여기저기서 gunicorn의 메모리 누수 현상 때문에 곡소리를 내고 있다는 것을 알 수 있다.
gunicorn은 자신들의 메모리 누수 현상을 인지하고 있으며, 이를 대비한 조금 과격한 해결책을 구비해 놓았다. 바로 max_requests 옵션이다.
max_requests란?
gunicorn엔 max_requests와 max_requests_jitter란 옵션이 존재한다.
max_requests는 워커가 처리할 수 있는 최대 요청 수이며, max_requests_jitter는 max_requests에 시차를 두어 모든 worker가 동시에 종료되는 것을 방지하기 위해 존재하는 값이다. 즉, 워커는 max_requests 언저리만큼의 request를 처리하면 재시작된다.
jitter의 역할과 max_requests 값이 최종적으로 어떻게 결정되는지는 아래 코드를 통해 할 수 있다.
# gunicorn.workers.base.Worker.__init__
if cfg.max_requests > 0:
jitter = randint(0, cfg.max_requests_jitter)
self.max_requests = cfg.max_requests + jitter
else:
self.max_requests = sys.maxsize
각 워커의 max_requests의 값에 randint(0, cfg.max_requests_jitter) 값이 더해진다.
조금 떨떠름하다만, max_requests_jitter를 넉넉하게 준다면 워커가 동시에 꺼지는 일은 거의 없을 것 같다.
걱정되는 부분
적용 전에 걱정되는 부분 몇 가지가 있어 확인을 해봤다.
우선, worker가 재구동되는 데에 시간이 오래 걸리면 모든 worker가 다 꺼지는 순간이 발생할 수도 있지 않을까? 하는 생각이 들어, 재구동에 시간이 얼마나 걸리는지 테스트 해봤다.
결과적으로 worker의 종료와 재시작은 거의 동시에 이루어지는 것을 확인할 수 있었다. 이유인 즉슨, gunicorn은 pre-fork worker model을 차용하며, master process의 정신만 멀쩡하다면 fork를 통해 바로 worker process를 만들어낼 수 있다.
그래도 그래도, 확률이 매우 희박하긴 하겠다만, 한 워커가 재시동을 완료하기 전에 나머지 워커도 모두 꺼지는 상황에 대한 대비책이 존재하는지 궁금해져 코드를 좀 더 확인해봤다.
# gunicorn.workers.base_async.AsyncWorker.handle_request
self.nr += 1
if self.nr >= self.max_requests:
if self.alive:
self.log.info("Autorestarting worker after current request.")
self.alive = False
if not self.alive or not self.cfg.keepalive:
resp.force_close()
nr은 now_requests의 줄임말인 듯 하며, nr이 max_requests 이상이 되면 force_close() 함수가 호출되는 듯 하다. 더 자세히 타고 타고 들어가보면.. 현재 처리 중이던 요청이 마무리된 후 워커가 종료된다.
즉, 모든 워커가 종료되는 것에 대한 예방책은 딱히 존재하지 않는다.
그러나, 모든 워커가 같은 max_requests를 가질 확률은 1 / jitter^num_workers 의 확률이며, 모든 워커의 nr이 같고, 마스터 프로세스가 맛이 가서 모든 워커가 꺼질 동안 모든 워커가 다시 켜지지 않은 상태여야 하고... 발생하지 않는다고 봐도 된다. 안심하고 이 옵션을 사용하도록 하자.
적용 후기
해당 옵션을 적용하고 난 뒤의 메모리 사용량이다. 메모리 사용량이 급증할 때가 있긴 하다만, 어느 정도 억제되는 모습을 보여준다.
메모리를 좀 더 빡빡하게 관리하고 싶다면, max_requests의 수를 더 줄이면 될 것 같다. 좀 더 지켜보고 값을 적절하게 조절해볼 생각이다.
참조
- Total
- Today
- Yesterday
- mlops
- Python
- 메모리 누수
- S3+CloudFront
- s3
- 개발자회고
- 넷플릭스
- 유난한도전
- 백엔드
- 조직문화
- 토스
- uvicorn
- Gunicorn
- ddd
- 사이드프로젝트
- AWS
- 규칙없음
- 개발자동아리
- 모델 추론 최적화
- memory leak
- CloudFront
- Triton Inference Server
- 모델 추론
- 회고
- 정적웹사이트
- Ai
- 웹사이트배포
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |