Tech

gunicorn 설정의 A to Z

2021. 06. 09

안녕하세요, 화해의 데브옵스 엔지니어 민윤홍입니다. gunicorn 설정의 A to Z

 

화해 서비스를 만들어나가고 있는 개발팀에서는 Django Framework 기반의 백엔드 어플리케이션을 위한 WSGI 서버로 gunicorn을 채택하여 사용해오고 있습니다. gunicorn을 활용해 다양한 기술적 문제를 경험하고 해결해나가는 과정에서 gunicorn에 대한 내용들을 정리하다보니 유용한 정보가 많은 것 같아 이 내용들을 공유해보려고 합니다.

 

 

 

 

기본 개념 설명

 

gunicorn은 WSGI(Web Server Gateway Interface) 서버입니다. WSGI란 python으로 작성된 웹 어플리케이션과 python으로 작성된 서버 사이의 약속된 인터페이스 또는 규칙이라 보시면 됩니다. 간단히 말하면 WSGI 서버와 웹 어플리케이션이 WSGI의 규칙에 따라 작성되면, 웹 어플리케이션 입장에서는 내부 구현과 상관 없이 자유롭게 WSGI 서버를 골라서 사용할 수 있는 유연성을 제공합니다.

 

예를 들어 Django는 wsgi 를 위한 module인 wsgi.py를 제공하며, 내부 구현 변경 없이 gunicorn, uwsgi 등 다양한 WSGI 서버를 자유롭게 선택하여 사용할 수 있습니다. WSGI 규칙에 맞는 웹 서버와 웹 어플리케이션을 어떻게 구현해야 하는지는 PEP 3333에 자세히 나와 있습니다.

 

gunicorn의 프로세스는 프로세스 기반의 처리 방식을 채택하고 있으며, 이는 내부적으로 크게 master process와 worker process로 나뉘어 집니다. gunicorn이 실행되면, 그 프로세스 자체가 master process이며, fork를 사용하여 설정에 부여된 worker 수대로 worker process가 생성 됩니다. master process는 worker process를 관리하는 역할을 하고, worker process는 웹어플리케이션을 임포트하며, 요청을 받아 웹어플리케이션 코드로 전달하여 처리하도록 하는 역할을 합니다.

 

 

 

 

주요 설정 및 의미

 

  • gunicorn version: 20.1.0
  • gunicorn의 document, 코드 분석, 실험, 운영 경험을 통해 얻어진 지식을 바탕으로 작성하였습니다.
  • worker class의 경우 현재 개발팀에서 주로 사용하고 있는 syncgevent 위주로 포스트를 작성하였습니다. 이외에도 eventlet, tornado, gthread의 정보가 궁금하시면 gunicorn.org 사이트에서 확인 가능합니다.

 

 

 

 

Config

 

config

gunicorn 실행 시 참조하는 config 파일의 위치입니다. 기본 값은 gunicorn.config.py입니다. config 파일이 존재하지 않는 경우, command line 명령어의 인자만으로 실행이 됩니다. config 파일의 설정과 command line 명령어의 설정이 중복되는 경우 command line의 설정을 최종적으로 참조하여 실행하게 됩니다.

 

config 파일은 python 코드로 작성이 가능하고, command line 명령어로는 제공되지 않는 server hook 등을 넣을 수 있는 장점이 있습니다. 예를 들어 workers 설정 값을 고정 정수 값이 아닌 CPU의 개수에 상대적인 값이 들어가도록 사용하고 싶은 경우 아래와 같이 사용이 가능합니다.

 

import multiprocessing
workers = multiprocessing.cpu_count() * 2 + 1

 

wsgi_app

$(MODULE_NAME):$(VARIABLE_NAME)로 구성됩니다. 이 문서를 보면 쉽게 이해할 수 있습니다.

 

command line 명령어 사용 시, gunicorn 명령어의 바로 뒤에 오는 positional 인자가 wsgi app 입니다.

gunicorn 설정_wagi app

 

 

설정 파일에 이 설정이 추가 된 경우, command line에 wsgi app인자는 생략할 수 있습니다.

gunicorn 설정_추가


wsgi_app = 'myapp:app'

config.py

 

 

 

 

Debugging

 

reload

code가 변경될 때 마다 worker가 재시작됩니다. production 환경에서는 권장하지 않는 설정이지만 development 환경에서 변경된 코드에 대한 빠른 결과 확인을 위해 유용한 설정입니다.

 

현재는 이 설정을 사용하지 않지만 과거에 이 설정을 활성화 했을 때 경험했던 에피소드를 하나 말씀드리겠습니다.

  • 개발 환경 서버에는 reload 설정이 활성화되어 있었다.
  • 어플리케이션이 의존하는 python package가 requirements.txt에 추가되었다.
  • 개발 환경 서버에 접속해 배포를 위해 코드를 git pull하는 경우, reload 설정이 활성화 되었기 때문에 package가 설치되지 않은 상태에서 worker가 재시작되었다.
  • 추가된 package의 모듈을 임포트하지 못해 ModuleNotFoundError가 발생하고, gunicorn 자체가 중단되었다.
  • 개발 환경을 사용하는 모든 사용자가 불편을 겪었다.

따라서 개발환경에서 사용할 때에는 위의 상황을 이해하고 사용할 필요가 있을 것 같습니다.

 

 

spew

이 설정이 활성화 될 시 실행되는 모든 코드를 console에 보여줍니다. 디버깅 용도로 이 설정이 존재하는 것 같지만, 활성화하지 않기를 추천드립니다. 디버깅을 원한다면 IDE에서 제공하는 툴을 사용하는게 더 좋습니다.

 

 

check_config

configuration 체크를 위한 설정입니다. 일반적으로 명령어에서 자주 볼 수 있는 dry-run과 비슷하다고 생각하면 됩니다. 코드 상으로는 실제로 어플리케이션을 임포트하는 과정까지 거칩니다. 따라서 어플리케이션의 로그 파일 위치 등의 설정도 문제가 없는지 확인할 수 있습니다.


def run(self):
    if self.cfg.print_config:
        print(self.cfg)
    ...
    if self.cfg.print_config or self.cfg.check_config:
  try:
            self.load()
        except Exception:
            msg = "\nError while loading the application:\n"
            print(msg, file=sys.stderr)
            traceback.print_exc()
            sys.stderr.flush()
            sys.exit(1)
        sys.exit(0)
    ...

gunicorn/base.py의 <a class="notion-link-token notion-enable-hover" style="color: #da615c;" href="http://application.run/" target="_blank" rel="noopener noreferrer" data-token-index="1" data-reactroot="">Application.run</a>  method

 

 

이 설정의 활성화 시 주의할 점은, 의도한 것인지는 모르겠으나 raw_env 설정을 사용할 경우, 환경 변수가 정상적으로 적용되지 않은 채로 실행이 됩니다. raw_env 설정 대신 실행 명령어 제일 앞에 환경변수 값을 직접 선언하는 방식으로 실행시키면 문제 없이 작동합니다.

 

예를 들어 다음과 같이 --check-config flag를 사용할 때 --env flag를 사용하면 환경 변수 값이 정상적으로 적용되지 않습니다.

gunicorn 설정_flag

 

환경 변수를 적용하고 싶으면, 다음과 같이 실행해야 합니다.

 

print_config

check_config와 동일하나, 설정값을 print하여 console에 보여줍니다. logging 모듈이 아닌 python의 print 함수를 사용합니다.


def run(self):
    if self.cfg.print_config:
        print(self.cfg)
    ...
    

gunicorn/base.py의  <a class="notion-link-token notion-enable-hover" style="color: #da615c;" href="http://application.run/" target="_blank" rel="noopener noreferrer" data-token-index="1" data-reactroot="">Application.run</a>  method

 

 

 

 

Logging

 

accesslog

gunicorn access log 파일 위치를 가리킵니다. -이면 stdout으로 기록이 됩니다. 주의할 점은 Django를 사용할 경우, Django 설정 파일의 Logging 설정에서 'disable_existing_loggers': False로 설정해 주어야 access log가 기록 됩니다.

 

 

access_log_format

access log에 대한 formatting을 할 수 있습니다. 자세한 사항은 gunicorn 설정 문서에 잘 나와 있으니 확인해 보시기 바랍니다.

 

 

errorlog

gunicorn error log 파일 위치를 가리킵니다. 기본값이 -이고, stderr를 의미합니다. 설정 이름은 errorlog이지만 worker process의 exception에 의한 에러 로그 뿐만 아니라 gunicorn 의 실행, worker process의 spawn, termination 등 master process 레벨의 로그도 보여줍니다.

 

 

loglevel

errorlog의 level 입니다. 경험상 info 레벨로 두면 worker의 실행 및 종료까지 파악할 수 있어 문제 해결에 도움이 되었던 적이 많았습니다.

 

 

syslog

gunicorn의 access log와 error log를 syslog로 보내는 설정입니다.

 

이 설정은 Django와 함께 사용 시 문제가 있습니다. Python 3.6.7 버전 이후에는 이 설정을 Django 사용과 함께 활성화 하면, logging 시 파이썬 레벨에서 OSError: [Errno 9] Bad file descriptor 에러가 발생합니다. 이 버그와 관련하여 gunicorn의 github 페이지에 2년째 Open 중인 이슈가 있으며, python의 이슈 리스트에서도 Open 상태인 이슈가 있습니다.


gunicorn[5957]: OSError: [Errno 9] Bad file descriptor
gunicorn[5957]: Call stack:
gunicorn[5957]:   File "/home/ubuntu/.local/bin/gunicorn", line 8, in <module>
gunicorn[5957]:     sys.exit(run())
gunicorn[5957]:   File "/home/ubuntu/.local/lib/python3.6/site-packages/gunicorn/app/wsgiapp.py", line 67, in run
gunicorn[5957]:     WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]").run()
gunicorn[5957]:   File "/home/ubuntu/.local/lib/python3.6/site-packages/gunicorn/app/base.py", line 231, in run
gunicorn[5957]:     super().run()
gunicorn[5957]:   File "/home/ubuntu/.local/lib/python3.6/site-packages/gunicorn/app/base.py", line 72, in run
gunicorn[5957]:     Arbiter(self).run()
gunicorn[5957]:   File "/home/ubuntu/.local/lib/python3.6/site-packages/gunicorn/arbiter.py", line 202, in run
gunicorn[5957]:     self.manage_workers()
gunicorn[5957]:   File "/home/ubuntu/.local/lib/python3.6/site-packages/gunicorn/arbiter.py", line 551, in manage_workers
gunicorn[5957]:     self.spawn_workers()
gunicorn[5957]:   File "/home/ubuntu/.local/lib/python3.6/site-packages/gunicorn/arbiter.py", line 622, in spawn_workers
gunicorn[5957]:     self.spawn_worker()
gunicorn[5957]:   File "/home/ubuntu/.local/lib/python3.6/site-packages/gunicorn/arbiter.py", line 605, in spawn_worker
gunicorn[5957]:     self.log.info("Worker exiting (pid: %s)", worker.pid)
gunicorn[5957]:   File "/home/ubuntu/.local/lib/python3.6/site-packages/gunicorn/glogging.py", line 264, in info
gunicorn[5957]:     self.error_log.info(msg, *args, **kwargs)
gunicorn[5957]: Message: 'Worker exiting (pid: %s)'
gunicorn[5957]: Arguments: (5957,)

</module>

   syslog에 찍히는 에러 예시

 

 

원인을 살펴보면 gunicorn에서 SysLogHandler로 로깅 핸들러를 설정 한 후, 어플리케이션에서 dictConfig()를 호출할 때 생기는 이슈입니다. Django에서는 항상 dictConfig()를 호출하므로, Django + gunicorn 조합을 사용할때는 오류 메세지가 syslog에 기록되기 때문에 사용하지 않는 것이 좋을 것 같습니다. 발생 원인을 해결하기 위해서는 python에서 dictConfig에 대한 내부 실행 로직을 변경하거나, Django에서 로깅 방식을 바꿔줘야 하는데, 해결될 가능성이 적습니다. 해결되더라도 현재 사용하는 python 버전이나 Django의 버전의 patch 업데이트로 나올 가능성은 적어 보입니다.

 

 

disable_redirect_access_to_syslog

gunicorn의 access log가 syslog로 넘어가지 않도록 하는 설정입니다. 이 값은 아래 코드에서 볼 수 있듯이 syslog 설정값이 활성화 되어 있지 않으면 동작하지 않는 설정입니다.


if cfg.syslog:
    self._set_syslog_handler(
        self.error_log, cfg, self.syslog_fmt, "error"
    )
    if not cfg.disable_redirect_access_to_syslog:
        self._set_syslog_handler(
            self.access_log, cfg, self.syslog_fmt, "access"
        )

   gunicorn/glogging.py

 

 

 

 

Security

 

limit_request_line

HTTP의 request-line의 사이즈에 대한 제한값(bytes)입니다. 기본값은 4092이고 최대 8190까지 값을 부여할 수 있고, 0을 사용하면 무한의 길이를 허용하게 됩니다. GET 호출에 대한 query parameter도 이 제한 값에 들어가기 때문에 클라이언트에서 최대로 호출할 수 있는 query parameter를 파악하여 값을 지정하는게 좋습니다. 그리고 이 값을 결정할 때는 gunicorn 구간 뿐만 아니라 nginx 등 앞단의 proxy 서버 설정도 고려해야 합니다.

 

이 값을 무한대로 설정하는 것은 DDOS 공격에 취약할 가능성이 높고, 설정에 대한 제어 권한이 없는 proxy 등에서 막힐 가능성이 있기 때문에 최대 값인 8190 (약 8kb)를 넘어가게 된다면 드롭박스 처럼 POST 사용을 고려해 보거나 다른 방식을 시도해 보는 것을 추천 드립니다.

 

 

 

 

Server Mechanics

 

preload_app

gunicorn은 기본적으로 master process에서 worker process를 fork한 후, 각 worker process에서 어플리케이션을 임포트 합니다. 이 설정을 활성화하면 worker process를 fork하기 전에 어플리케이션 코드를 임포트합니다.

 

 

raw_env

실행 환경의 environment variable을 추가할 수 있습니다. 복수개의 명령어도 가능합니다.

 

만약 command 실행 시 shell 레벨 정의된 환경 변수와 raw_env 설정값을 사용한 환경변수가 중복이 된다면, raw_env 설정값을 사용한 환경변수를 최종적으로 참조하여 사용하게 됩니다.

 

예를 들어 다음과 같은 명령어를 실행했다면 실행 환경 내에서 NAME 환경 변수의 값은 argument가 됩니다.

 

 

 

pidfile

master process의 pid 값이 저장된 파일의 위치를 지정합니다. 이 설정을 사용하지 않으면 pid 파일을 생성하지 않습니다.

 

pid 파일은 다양한 용도로 사용될 수 있습니다. 예를 들어 pid 파일의 값을 이용하여 worker process의 개수를 조회한 다음 workers 설정값과 동일하게 생성이 되었는지 확인하는 validaiton 용도로도 사용할 수 있습니다.

 

 

user, group

worker process를 실행하는 uid, gid 값입니다. 기본값은 os.geteuid()os.getegid() 실행 결과입니다. 즉, 해당 프로세스를 실행시키는 user와 group의 id 값입니다. 일반적으로 프로그램 실행 시 systemd 등을 사용하여 user와 group을 지정하였기 때문에 이 설정을 사용한 적은 없습니다.


def validate_user(val):
    if val is None:
        return os.geteuid()
    ...


def validate_group(val):
    if val is None:
        return os.getegid()
    ...

   gunicorn/config.py

 

 

 

 

Server Socket

 

bind

bind 할 소켓을 지정합니다. 기본값 127.0.0.1:8000이고, 복수개를 지정할 수 있습니다. unix:PATH를 사용하면 소켓 파일을 생성합니다. 예를 들어 unix:/tmp/gunicorn.sock이라 지정하면 /tmp/gunicorn.sock/ 파일이 생성됩니다. 이 소켓 파일은 nginx에서 gunicorn으로의 reverse proxy를 구성할 때 path로 지정할 수 있습니다.


http {
    server {
        ...

        location / {
            ...
            proxy_pass http://unix:/tmp/gunicorn.sock;
            ...
        }
    }
}


 

 

 

 

backlog

 

listen system call의 backlog 인자입니다. gunicorn이 실행되면 master process에서 socket.listen을 호출하게 되고, 이때 이 설정값이 인자로 들어갑니다. 이 설정값은 실제 어플리케이션에서 처리하고 있는 커넥션을 제외하고 ESTABLISHED 상태로 유지가 될 수 있는 커넥션의 개수라 보면 됩니다.

 

만약 nginx와 같은 웹서버가 앞단에 위치한다면  nginx가 허용할 수 있는 전체 커넥션 수gunicorn의 backlog 설정 값설정 값 사이의 관계를 고려해야 합니다.

 

만약 gunicorn의 backlog 설정 값 > nginx가 허용할 수 있는 전체 커넥션 수라면

  • 클라이언트에서 웹어플리케이션까지의 모든 구간이 ESTABLISHED로 유지되는 커넥션의 개수는 기껏해야 nginx가 허용할 수 있는 커넥션 수일 것입니다.
  • gunicorn의 backlog 설정값만큼 queue가 채워지는 일은 발생하지 않습니다. 이 경우 이 설정값을 무의미하게 크게 설정했다고 볼 수 있습니다.

 

 

 

 

Worker Processes

 

workers

master 프로세스가 fork하는 worker 프로세스의 수입니다. 공식 문서에서는 2-4 X CPU 수를 추천하고 있습니다.

 

 

worker_class

worker process의 종류입니다. 기본값은 sync(동기 worker)이며, 이 경우 worker process는 하나의 커넥션의 요청만 처리할 수 있습니다.

 

비동기 worker에는 다양한 종류가 있지만, 그 중에 gevent를 예를 들어 보겠습니다. gevent를 사용하는 경우, coroutine 을 사용하여 실행되기 때문에, 하나의 worker process에서 다수의 커넥션 요청을 동시에 처리할 수 있습니다. 실제로 CPU intensive job을 worker process 개수(CPU 수보다 값이 큼)만큼 실행한 경우에도, health check 용도 같은 간단한 API를 호출 했을 시, 응답을 문제 없이 내려주는 것을 확인하였습니다.

 

 

worker_connections

worker_class 값이 eventlet 또는 gevent일때만 의미가 있는 값입니다. worker process 당 허용할 수 있는 커넥션의 개수입니다.

 

 

max_requests

worker process가 정해진 수치만큼 요청을 받으면 재시작하도록 하는 설정입니다. 0이 기본값으로, 이 값일 때는 재시작을 하지 않습니다. 0 보다 큰 값을 넣으면, 해당 수만큼 worker로 요청이 들어왔을 때 재시작 됩니다. 이 옵션은 memory leak의 원인 파악이 어려울 때 사용하면 메모리 부족현상을 해결 할 수 있어서 유용합니다.

 

화해 서비스를 운영하면서도 특정 레포지터리가 오랫동안 배포가 안되어 worker instance가 재시작 되지 않는 경우 memory leak 때문에 OOM 장애가 발생한 적이 있습니다. gunicorn의 memery leak의 원인은 아니었고, 원인을 단기간내에 파악하기는 어렵다고 판단되어 메모리 증가량과 requests의 상관관계를 계산하여 값을 산출해 이 설정을 적용하여 서비스 품질을 유지해나갔습니다.

 

 

max_requests_jitter

max_requests설정과 함께 사용하는 설정입니다. worker process의 재시작에 시간차를 두어 요청을 일시적으로 받지 못하는 상황을 방지합니다.

 

다음 코드와 같이 worker process의 max_requests 수에 randint(0, max_requests_jitter) 만큼 변경값을 추가해줍니다.


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

gunicorn/workers/base.py

 

 

 

timeout

worker process로 요청이 들어왔는데, timeout (초) 내에 응답을 못받을 경우 worker process를 강제로 종료한 후 다시 생성합니다. 0 일 경우 timeout이 비활성화됩니다.

 

강제 종료이기 때문에, 웹어플리케이션에서 이 값의 영향을 받을 만한 API가 있다면, API 중간에 process가 종료되더라도 DB transaction 등을 적용해 문제 없도록 구성되어야 합니다.

 

또 주의해야 할 점은 nginx 등 앞단에 웹서버가 있다면, 웹서버의 timeout도 고려해야 합니다. 예를 들어 nginx가 reverse proxy로 구성되어 있다면 nginx의 proxy_read_timeout 설정값도 고려해줘야 합니다. 만약 AWS의 load balancer를 사용한다면 idle timeout 값도 고려를 해줘야 합니다.

 

sync worker가 아닌 경우 이 값은 큰 의미가 없습니다. geventworker_class로 설정하여 실험했을 때 timeout은 제대로 작동하지 않았습니다.

 

 

graceful_timeout

timeout 값과는 다르게 특정 signal을 master process나 worker process가 받으면 graceful_timeout 값 만큼 요청을 처리할 시간이 worker에게 주어지게 됩니다. 그러나 실질적으로 graceful_timeout 적용되는 signal은 매우 한정적입니다.

 

sync worker로 구동되는 gunicorn에 master process로 signal을 날렸을 때 graceful_timeout이 적용되는 경우는   TERM  밖에 없습니다. worker process의 restart를 위한 HUP signal을 날린 경우 workers의 설정값 수만큼 바로 worker process가 생성되며 기존의 worker process는 graceful_timeout과는 상관 없이 처리중인 요청 처리가 끝난 후에 종료되는 것을 확인할 수 있었습니다.

 

worker_classgevent인 경우 HUP signal에 대한 반응을 살펴보면 새로운 worker process가 즉시 생성되는 점은 sync worker와 동일했지만 기존 worker가 요청에 대한 처리가 끝날때 까지 기다리지 않고 graceful timeout 만큼 기다린 다음 강제 종료된 점이 달랐습니다.

 

개인적인 생각으로는 worker class의 종류, signal의 종류에 따라 예측할 수 없는 방향으로 반응하므로 어떻게 보면 애매한 설정값이라 볼 수 있을 것 같습니다.

 

 

 

 

마치며

 

지금까지 gunicorn의 주요 설정값에 대해 알아 보았습니다. 경험적 지식을 기반으로 작성하다보니 주요 설정만 살펴볼 수 있었던 아쉬움이 있네요. 하지만 제가 제공해드린 지식이 여러분이 gunicorn에 조금더 깊게 다가가기 위한 시작점이 되기를 바라며, 이만 글을 마치고자 합니다. 읽어 주셔서 감사합니다.

 

 


 

이 콘텐츠가 마음에 드셨다면 다음 콘텐츠도 확인해보세요!

AWS EventBridge를 활용한 업무 자동화

표준 서비스 환경 자동 구축을 위한 서버 템플릿 도구 도입 사례

 

gunicorn 설정의 A to Z gunicorn 설정의 A to Z gunicorn 설정의 A to Z 

  • 성능개선
  • 데브옵스
  • gunicorn
  • 미들웨어
avatar image

민윤홍 | Devops Engineer

화해 서비스의 무사안일을 기원하는 Devops Engineer입니다🙂

연관 아티클