Back-end/FastAPI

[gunicorn] 메모리 누수 현상, max_requests로 해결하자.

권기준 2023. 5. 29. 21:56

발단

 최근 회사의 한 프로젝트를 크게 리팩토링 할 일이 있었는데, 해당 내용을 배포한 이후 메모리 누수 현상이 발생하기 시작했다.

 

오직 직진.

 메모리 사용량이 계속 오르기만 하다 결국 인스턴스가 죽어버린다.

 

 추정 중인 원인은, gunicorn이 복잡한 request, 즉 응답에 시간이 오래 걸리는 request에 대한 메모리 해제를 제대로 해주지 못하는 것이다. 구글링을 좀 해보면, 여기저기서 gunicorn의 메모리 누수 현상 때문에 곡소리를 내고 있다는 것을 알 수 있다.

 

 gunicorn은 자신들의 메모리 누수 현상을 인지하고 있으며, 이를 대비한 조금 과격한 해결책을 구비해 놓았다. 바로 max_requests 옵션이다.

 

max_requests란?

 gunicorn엔 max_requestsmax_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의 수를 더 줄이면 될 것 같다. 좀 더 지켜보고 값을 적절하게 조절해볼 생각이다.

 

참조

  1. https://docs.gunicorn.org/en/stable/settings.html
  2. https://docs.gunicorn.org/en/stable/design.html
  3. https://github.com/benoitc/gunicorn