최근 여러 대규모 머신러닝 프로젝트에서는 기존 WSGI가 아닌 ASGI WAS를 이용해 비동기 처리를 하고 있다.
어떤 장단점이 ASGI에 있는지 알아보고, 특히 기존 응답시간이 많이 걸리는 Machine Learning의 serving 부분에서 어떤 패턴으로 적용되며, 어떻게 사용 가능한지 리뷰.
대규모 머신러닝 프로젝트 Serving에 사용되는 Python WAS, ASGI - uvicorn
ASGI는 Python에서 지원하는 WAS(Web Application Server)인 WSGI를 대체하는, 비동기 대용량 트래픽 처리를 위한 현대적인 서비스를 의미한다. uvicorn는 2021년 현재 ASGI를 구축할 수 있는 python 웹 어플리케이션 서버이다.
- Web 서버와 WAS
- WSGI와 gunicorn
- ASGI를 사용하는 이유
- ASGI - uvicorn 코드 리뷰
- ASGI interface 정리
- 다른 ASGI 서버
Web 서버와 WAS
웹서버의 용도는 우리가 잘 아는 것처럼, client의 HTTP요청을 받아 리턴하는 역할을 수행한다. WAS는 동적으로 결과를 리턴할 수 있도록 웹 어플리케이션을 수행하는 미들웨어이다. 주로 데이터베이스에 접근해 그 결과를 HTML로 만들어 리턴하거나, 많은 동적 콘텐트를 제공하는 역할을 수행한다.
WSGI와 gunicorn
WSGI는 Python으로 만든 어플리케이션이 웹서버와 통신할 수 있도록 인터페이스를 수행하며, 이 처리를 WSGI가 수행한다. Python에서는 그간 gunicorn이 이 역할을 수행했다. 이 처리는 동기(synchronous)로 처리되며 잘 동작하지만, 대규모 트래픽 처리에는 한계가 있다. node에서는 이러한 비동기 처리 기능을 잘 지원하나, Python에서는 async 패키지 라이브러리가 한동안 제공되지 않았다.
ASGI를 사용하는 이유
WSGI의 한계인 요청에 대해 단일 동기적으로 처리하는 루틴을 비동기로 처리해 처리량을 늘리고 높은 성능을 제공하는 WAS를 구현하기 위해서 ASGI(Asynchronous Server Gateway Interface) 명세를 구현해 나오기 시작했고, 특히 Django에서 적용되면서 빠르게 확산되었다.
Django의 기존 WSGI 처리 패턴
Django의 ASGI 패턴
이미지 출처: Async Tasks with Django Channels
Python의 비동기 처리를 활용해 성능적인 측면에서 많은 향상이 이루어졌고, 특히 기존 응답시간이 많이 걸리는 Machine Learning의 serving 부분에서 적용되어, 현재는 많은 대규모 ML 모델 inference에서 사용되고 있다.
ASGI - uvicorn 코드 리뷰
간략히 공식 repo에서 제공하는 코드를 리뷰.
WSGI와 비슷한 부분이 많고, async 부분을 제외하면 대부분 비슷한 처리가 가능할 듯.
기본 요청(Request)과 응답(Response)
uvicorn을 설치하려면 아래 명령을 수행한다. WSL의 ubuntu bash에서 테스트했다.
pip install uvicorn
이어서, 아래 기본 코드를 수행한다.
import uvicorn async def app(scope, receive, send): assert scope['type'] == 'http' await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], }) await send({ 'type': 'http.response.body', 'body': b'Hello, world!', }) if __name__ == "__main__": uvicorn.run("example:app", host="127.0.0.1", port=5000, log_level="info")
py 파일로 저장하고 실행하면 uvicorn이 잘 동작한다.
ASGI interface 정리
uvicorn은 세가지의 ASGI interface가 있다.
scope: A dictionary containing information about the incoming connection.
receive: A channel on which to receive incoming messages from the server.
send: A channel on which to send outgoing messages to the server.
Scope
Scope에 따르는 동작을 통해 루틴을 제한하거나, 필요하다면 exception 처리를 한다.
... assert scope['type'] == 'http' ...
Receive & Response
인입되는 요청을 받아 처리하는 루틴이다. 이렇게 요청을 받아 처리한 다음 body 변수에 저장 후 response의 body에 넣어 응답 처리한다.
... body = f'Received {scope["method"]} request to {scope["path"]}' ... await send({ 'type': 'http.response.body', 'body': body.encode('utf-8'), }) ...
Streaming response
아래의 send패턴으로, 여러 http.response.body 메세지를 스트리밍 전달 가능하다. List인 [b'Hello', b', ', b'world!']의 각 item들을 하나씩 1초 간격으로 전달한다.
import asyncio async def app(scope, receive, send): """ Send a slowly streaming HTTP response back to the client. """ await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ] }) for chunk in [b'Hello', b', ', b'world!']: await send({ 'type': 'http.response.body', 'body': chunk, 'more_body': True }) await asyncio.sleep(1) await send({ 'type': 'http.response.body', 'body': b'', })
다른 ASGI 서버들
uvicorn 외에도 Daphne와 Hypercorn이 있다. 아래 링크 참조.
django/daphne: Django Channels HTTP/WebSocket server (github.com)
참고링크
encode/uvicorn: The lightning-fast ASGI server. 🦄 (github.com)
jordaneremieff/asgi-examples: A collection of example ASGI applications (github.com)
Introduction — ASGI 3.0 documentation