gunicorn과 메모리 누수

sihwan_e·2022년 7월 25일
1

배경

최근 회사에서 aws 비용 절감을 위해 사용중인 인스턴스의 크기를 줄이고 있다.
그러던 중 서비스 중 하나의 인스턴스를 줄였을 때 CPU는 괜찮았지만, 메모리가 95에서 99를 왔다갔다하면서 결국 서버가 내려가게 됬다.
그래서 현재 서비스의 서버 구성에 맞게 메모리 사용을 최적화 시킬 필요가 있다.

구조

먼저 해당 서비스의 서버 구성을 살펴봐야겠다.

Language = Python 3.7
Framework = django 1.11.17
Nginx 
gunicorn
(gevent)

Nginx

"전달자 역할만 하는 동시 접속 처리(비동기 처리)에 특화된 웹서버"
1. 정적 파일을 처리(HTTP 서버 역할)
2. 리버스 프록시(클라이언트가 내부 서버에 request를 보내면, 프록시 서버(Nginx)가 reverse server로 부터 데이터를 가져오는 역할을 함) -> 요청을 배분(클라이언트가 어디에 접근하는지는 관계가 없고 서버측에서 제공해주는 것에 따라 받게되므로 보안이 뛰어나며 로드 밸런싱을 통해 부하가 적다.)

upstream django{
(proxy를 설정해주는 부분)
    server 127.0.0.1:8000;
    keepalive 2048;
}

server {
	(외부에서 어떤 port를 listening 할지 정하는 부분)
    listen 80;

    set $redirect 0;

    if ($http_x_forwarded_proto != 'https') {
      set $redirect 1;
    }
    if ($request_uri = '/eb-health/') {
      set $redirect 0;
    }

    if ($redirect = 1) {
        return 301 https://서비스 도메인$request_uri;
    }

    charset utf-8;
    client_max_body_size 30M;

	Nginx 기본 빌드시 프록시 모듈 설정
	(버퍼링, 제한시간, 에러처리 등등)
    location / {
        proxy_buffer_size   128k;
		(백엔드 서버 응답의 첫 부분을 읽기 위한 버퍼 크기를 설정)
        proxy_buffers   4 256k;
		(백엔드 서버로 부터의 응답 데이터를 읽는데 사용할 버퍼의 수와 크기 설정)
        proxy_busy_buffers_size   256k;
        (백엔드에서 수신되는 데이터가 버퍼에 쌓이다가 해당 지정값을 초과하면 데이터를 클라이언트에 보내고 버퍼를 비움)
        proxy_pass_header Server;
        (추가 요청 헤더를 각각 백엔드 서버에 전달할지 여부)
        proxy_set_header Host $http_host;
        (넘겨 받을 때 프록시 헤더 정보)
        proxy_redirect off;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_http_version 1.1;
        proxy_set_header Connection “”;
        proxy_pass http://django;
        set $pscheme $http_x_forwarded_proto;
        if ($pscheme = “”) {
            set $pscheme $scheme;
        }
        proxy_set_header X-Forwarded-Proto $pscheme;
    }
}

이렇게 conf 파일을 설정해주게 되는데,
nginx 구성을 좀 바꾸고, 다른 방식으로 프록시 모듈 설정을 바꾸면 memory부하를 줄일 수 있지 않을까?

gunicorn

django 기반에서 어플리케이션을 위한 WSGI 서버
프로세스 기반의 처리방식

gunicorn이 실행 되면 그 프로세스가 master process가 되고, fork를 통해 설정에 부여된 worker 수대로 worker process가 생성된다.(현재 세팅은 5)
마스터 프로세스는 워커 프로세스를 관리하며, 워커 프로세스는 웹어프리케이션을 임포트하여, request를 받아 코드로 전달하여 처리하는 역할을 한다.

(워커 사이에서 메모리를 공유하지 않고, 각각의 워커 프로세스 생성 시 따로 리소스를 메모리에 올려놓는 방식이다.)
-> worker 프로세스 수 만큼 대용량 리소스를 메모리에 올려놓는다 -> 웹서버의 메모리를 많이 사용하게 된다.

gunicorn 공식문서 -> 워커 프로세스의 갯수는 보통 서버의 코어당 2-4개 사이에서 결정합니다. faq 를 체크하여서 이 파라미터에 대한 설계들을 확인해 보세요.

CPU bound 작업은 물리적 코어 수, IO bound 작업은 논리적 코어 수에 의존한다. 만약 물리적 코어가 2개인 머신에서 CPU bound 작업을 처리할 때 -w를 2로주나 4로주나 유의미한 차이가 없으며 오히려 오버헤드가 발생한다. 공식 문서에서 권장하는 워커와 스레드의 개수는 2 * $NUM_CPU + 1이다.

해결책

  1. gunicorn의 --preload 옵션을 통해 워커들 간에 메모리를 공유하게 설정한다.
  2. 처리가 오래 걸리는 작업은 timeout을 길게 주기 보다는 mq나 celery를 이용하자(하지만 해당 서비스에 이 정도 리소스 투자는 필요 없을 것 같다)
    https://blog.winterjung.dev/2018/04/08/flask-concurrency-test
  3. gunicorn 옵션 중 max_requests를 사용해 max_requests에 도달하면 워커들을 재시작 하는 옵션
    (참고: https://yujuwon.tistory.com/entry/gunicorn-%EB%A9%94%EB%AA%A8%EB%A6%AC-leak-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0)
    max_requests_jitter 옵션을 같이 써주어 동시에 재시작 되는 것을 막아 주도록 하자.
    일반적으로 gunicorn 사용 중에 메모리 누수에 대한 원인 규명이 힘들면 위의 옵션들로 메모리 부족 현상을 해결 할 수 있다고 한다.

마치며

주말에 다시 좀 개선해야겠습니다.

profile
Sometimes you gotta run before you can walk.

0개의 댓글