본문 바로가기

app/python

wsgi를 왜 쓰나요

"uwsgi를 왜 쓰나요?”라는 질문을 “파이썬에서 통신을 하기 위해 지정한 인터페이스예요”라고 대답을 했지만 … 개인적으로 충분한 대답이 되지 않은 것 같아 이 글을 작성합니다. 

 

해당 글은 “왜”, “어떻게” 그리고 "정말 쓰면 좋아요?”를 주제로 정하고 작성하였습니다. 

 

 

1. 왜 django의 runserver로 배포 하면 안돼요?

django에서는 runserver를 통해 개발 및 테스트를 합니다. 네. runserver는  “개발 및 테스트”가 목적입니다. 

django의 공식 문서에의 runserver의 글입니다. 

(https://docs.djangoproject.com/en/2.2/ref/django-admin/#runserver)

DO NOT USE THIS SERVER IN A PRODUCTION SETTING. It has not gone through security audits or performance tests. (And that’s how it’s gonna stay. We’re in the business of making Web frameworks, not Web servers, so improving this server to be able to handle a production environment is outside the scope of Django.)

"프로덕션 세팅으로 서버를 사용하지 마세요. 보안과 성능 테스트를 거치지 않았습니다. …. 프로덕션 환경은 django의 영역을 벗어난 것입니다.”

네. 공식적으로 프로덕션으로 사용하지 말라고 되어 있습니다. runserver의 성능은 매우 느리며, 보안에 대한 문제가 내포되어 있습니다. (여기서의 보안은 통신상의 보안입니다.) 프로덕션일 경우엔 wsgi를 통해서 서비스하도록 권장합니다. 

 

 

2. wsgi를 써서 django를 실행했으면 끝 아닌가요? 왜 nginx를 앞에 붙이나요?

wsgi를 쓴다면 django 등의 웹 프레임워크 기능을 할 수 있게 됩니다. 하지만 여기에 nginx를 앞에 붙이면 더 좋은 성능을 낼 수 있습니다. 

예를 들어 몇몇 wsgi는 ssl과 정적 파일을  지원 하지 않습니다. ssl과 정적파일을  django까지 요청이 도착한 다음에 처리해야 하므로 성능이 저하됩니다. 또한 DOS 공격과 같은 많은 요청을 nignx에서 처리 및 분산시킴으로써 서버 기능을 보장할 수 있게 됩니다. 

( uwsgi는 ssl과 정적 파일 지원이 가능합니다. 하지만 다른 wsgi보다 메모리와 cpu를 더 많이 소비합니다. )

nginx를 쓴다면 아래와 같은 기능을 앞단에서 수행할 수 있습니다. 

 

  • 도메인 라우팅을 관리합니다 (리버스 프록시).
  • 정적 파일 제공
  • 한 번에 들어오는 많은 요청을 처리
  • 느린 클라이언트 처리
  • 동적 요청을 wsgi에 전달
  • SSL (https)
  • Python 코드와 비교하여 컴퓨팅 리소스 (CPU 및 메모리) 절약
  • 로드 밸런싱, 캐싱 등

 

wsgi는 아래와 같은 기능을 수행하도록 합니다. 

 

  • 작업자 프로세스 / 스레드 풀 실행 (코드 실행)
  • Nginx에서 들어오는 요청을 WSGI와 호환되도록 번역
  • 앱의 WSGI 응답을 적절한 http 응답으로 변환
  • 요청이 들어오면 실제로 파이썬 코드를 호출합니다.

 

각자의 주어진 역할을 수행함으로써 더 좋은 성능을 낼 수 있습니다. 

 

3. wsgi는 무엇인가요?

기존의 파이선 웹 애플리케이션 프레임워크는 웹서버를 선택하는 데 있어서 제약이 있었습니다. 보통 CGI, FastCGI, mod_python과 같은 커스텀 API 중에 하나만 사용할 수 있도록 디자인되었는데, WSGI는 그에 반하여 low-level로 만들어져서 웹서버와 웹 애플리케이션, 프레임워크 간의 벽을 허물었습니다. WSGI라는 인터페이스로 통일을 시켜 확장성을 높였습니다. 

 

WSGI 서버는 많은 요청을 동시에 처리하도록 설계되었습니다

WSGI는 응용 프로그램을 변경하지 않고 웹 스택 구성 요소를 변경과 유연성을 제공합니다. 

- 인증 / 권한 부여, 세션 항목, 캐싱, 필터링 등과 같은 기존 미들웨어 구성 요소를 통해 WSGI의 기능을 향상할 수 있습니다. 

자세한 스펙과 설명은 pep-333, pep-3333을 참고하시면 됩니다.

 

WSGI는 “server” 혹은 “gateway” (uwsgi,  gunicorn, gevent 등) 측과 “application”, “framework” (django, flask 등) 측 두 가지로 나누어집니다. server side에서는 application side에서 제공된 object를 호출합니다.

 

- appliation/framework 구현  ( https://www.python.org/dev/peps/pep-3333/ )

HELLO_WORLD = b"Hello world!\n"


def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [HELLO_WORLD]

######################################################################3

class AppClass:
    """Produce the same output, but using a class


    (Note: 'AppClass' is the "application" here, so calling it
    returns an instance of 'AppClass', which is then the iterable
    return value of the "application callable" as required by
    the spec.


    If we wanted to use *instances* of 'AppClass' as application
    objects instead, we would have to implement a '__call__'
    method, which would be invoked to execute the application,
    and we would need to create an instance for use by the
    server or gateway.
    """


    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response


    def __iter__(self):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        self.start(status, response_headers)
        yield HELLO_WORLD

application은 두 개의 매개변수를 가지고 있습니다. environ과 start_response를 사용합니다. 

environ은 CGI 환경에 대한 변수와 WSGI에서 필요로 하는 변수를 갖고 있는 dictinary입니다.

start_response는 두 개의 위치성 매개변수와 하나의 optional 매개변수를 필요로 하는 callable이며 application은 이 callable을 무조건 호출해야 합니다.

 

-server/gateway 측의 구현. 

파이썬 2.5부터 지원하는 wsgiref를 통해 간단하게 구현하였습니다. 

from wsgiref.simple_server import make_server

httpd = make_server('', 8000, simple_app)
httpd.serve_forever()

 

위의 코드를 실행하여 0.0.0.0:8000으로 접속하면 결과가 출력되는 것을 확인할 수 있습니다. 

 

위의 코드를 시각화를 하면 다음과 같습니다. 

 

 

 

위의 소스를 하나식 따라 가보면 의사 코드는 다음과 같습니다. 

 

httpd = make_server('', 8000, simple_app)

1)  WSGIServer    ->  HTTPServer   ->  TCPServer init() 호출

2)  TCPServer      ->  socket.socket()에서 객체 생성

3)  TCPServer      ->  server_bind()에서 서버 기본값 설정 및 socket에 정보 설정

4)  TCPServer      ->  server_activate()에서 socket.listen() 실행

 

httpd.serve_forever()

5)  HTTPServer   ->  BaseServer server_forever() 호출

6)  BaseServer    ->  selectors.PollSelector 호출

7)  Selectors        ->  select.poll의 기본 세팅 

8)  BaseServer    ->  3번에서 종료 인터럽트가 오기 전까지 while True:

9)  BaseServer    ->  selector.select() 호출

10) selectors       ->  fd_event_list = self._poll.poll()를 통해 이벤트가 발행했다면 해당 이벤트 반환

11)  BaseServer    ->  12번의 이벤트가 있다면 _handle_request_noblock() 호출 

12) 10번으로 돌아감 무한반복

 

기본 서버의 시작점에서 WSGIServer로 시작하는 것만 다를 뿐 기본적인 내부 로직은 앞서 글에서 설명했던 서버 내부 동작과 같습니다.

https://uiandwe.tistory.com/1240

 

 

  4. 성능 분석

-> gunicorn으로 테스트하였습니다.

 

api의 로직은 DB를 검색하여 리턴하는 api였습니다. 

테스트는 로컬에서 진행하였으며, django runserver / uwsgi  두 가지 조건으로 테스트하였습니다. 

 

Django Runserver로 실행 시

100명의 유저가 0.5초마다 1000번의 요청을 견디는 테스트였습니다. 

 

5000번을 넘길 때쯤 에러율이 10%가 넘어갔습니다. 

에러는 커넥션 에러로 더 이상 요청을 받을 수 없는 상태에 이르러 중단하였습니다. 

Type

Name

# requests

# failures

Median response time

Average response time

Min response time

Max response time

Average Content Size

Requests/s

Requests Failed/s

GET

/animal/

5778

626

54

184

2

8113

73

347.11

37.61

None

Aggregated

5778

626

54

184

2

8113

73

347.11

37.61

파란색이 성공, 빨간색이 실패율로 시간이 갈수록 실패율이 올라가는 것을 볼 수 있습니다. 

 

 

wsig(gunicorn) + django로 실행 시 

위와 동일 조건으로 실행하였습니다.

모든 요청이 끝날 때까지 에러가 발생하지 않았습니다.

Type

Name

# requests

# failures

Median response time

Average response time

Min response time

Max response time

Average Content Size

Requests/s

Requests Failed/s

GET

/animal/

981342

0

240

347

3

925

82

471.59

0.00

None

Aggregated

981342

0

240

347

3

925

82

471.59

0.00

 

wsgi를 사용하는 것 만으로 리퀘스트의 성능이 크게 향상되는 것을 볼 수 있습니다.

 

 

 

참고 사항

 

https://www.fullstackpython.com/wsgi-servers.html

 

https://b.luavis.kr/python/python-pep-3333

 

https://www.cabotsolutions.com/2017/11/a-detailed-study-of-wsgi-web-server-gateway-interface-of-python

 

https://www.appdynamics.com/blog/engineering/an-introduction-to-python-wsgi-servers-part-1/

 

https://rahmonov.me/posts/what-the-hell-is-wsgi-anyway-and-what-do-you-eat-it-with/

 

https://stackoverflow.com/questions/38601440/what-is-the-point-of-uwsgi

 

https://vsupalov.com/django-runserver-in-production/

 

https://timewizhan.tistory.com/entry/%EB%B2%88%EC%97%AD-Speed-Up-Your-Python-Program-With-Concurrency

 

https://www.slideshare.net/JueunSeo1/django-64975491

 

https://brownbears.tistory.com/215

 

https://spoqa.github.io/2012/01/16/wsgi-and-flask.html

'app > python' 카테고리의 다른 글

Peephole optimization  (0) 2020.04.09
The internals of Python string interning  (0) 2020.04.01
wsgi를 왜 쓰나요  (1) 2020.03.31
Python dictionary implementation  (1) 2020.03.21
python list implementation  (0) 2020.03.20
Python string objects implementation  (0) 2020.03.19