티스토리 뷰

728x90
반응형

안녕하세요. Teus입니다.

 

이번 포스팅은 FastAPI에서 기본적으로 사용하는 WebServer인 Uvicorn의 workers가 어떤 의미를 갖는지 알아봅니다.

 

그리고 일반적으로 알기 어려운 WebServer의 역할을 Python코드를 통해서 알아봅니다.

0. Gunicorn

일반적으로 Web서버의 구성은 아래처럼 구성 됩니다.

client <----> WebServer <----> WebApplicationServer(WAS) <----> DB

Springboot같은 경우 Webserver의 역할을 Tomcat이나 Apache 서버가 해줍니다.

 

하지만 Python같은 경우 Gunicorn이라는 패키지를 통해서 Client의 Requests를 받고, 이를 WAS(=Django, FastAPI Application)로 넘겨줍니다.

 

그래서 이 Gunicorn의 위치는 WebServer와 WAS사이에서 위치하게 됩니다.

client <----> WebServer <----> Middleware(Gunicorn) <----> WebApplicationServer(WAS) <----> DB

WebServer, Middleware는 왜 쓰는건가요🤔?(Python기준)
WebServer : Nginx...
Middleware : Gunicorn, Uvicorn...
WAS : Flask, Django, FastAPI...
client가 보내는 reques message는 NIC를 거쳐서 윈도우 socket으로 넘어오게 됩니다. 이때 도착한 메세지를 python이 이해할 수 있는 형태로 바꿔주는 것을 WSGI(혹은 ASGI)가 담당해 줍니다.

이때 의문이 드는데, Flask와 같은 WAS친구들도 자체적으로 WSGI가 구현 되어 있어서 python run.py 형태로 서버를 실행할 수가 있습니다. 

문제는 python run.py로 실행할 경우 Python은 GIL의 때문에 Thread를 사용해서 한순간에 여러 요청을 처리할 수 없습니다. 그러기 때문에 Multiprocessing을 사용 해야합니다.

이때 WAS마다 알아서 Multiprocessing을 적용하는 것이 아니라 middleware에서 해당 기능을 구현하여 WAS의 개발을 쉽게 해 준다 정도로 생각하면 되겠습니다.

webServer의 경우 NIC로 전달된 사용자의 요청을 보다 효율적으로 처리하기 위해서 사용된다고 보시면 됩니다.

Gunicorn같은 경우 Gevent는 비동기 라이브러리를 사용해서 해당 기능을 구현합니다.

 

대신 Gunicorn의 경우 WSGI기반의 Application을 호스팅하기 때문에 Worker(=Process) 내에서 Concurrency를 구현하기 위해서 greenlet Thread를 사용합니다.(Gunicorn 설정할때 이를 gThread라고 부릅니다)

 

덕분에 Gunicorn을 사용할 경우 비동기 함수 없이 Worker개수 이상의 Requests를 Concurrency로 처리할 수가 있습니다

1. Uvicorn

Uvicorn의 경우 FastAPI를 실행할때 필수적으로 사용됩니다.

 

Gunicorn과는 다르게 Uvicorn은 ASGI 기반으로 동작합니다.

 

이말즉 Uvicorn 내부에서는 Aysncio 기반으로 Concurrency를 구현합니다.

 

실제 Uvicorn 내부에서는 asyncio.run을 통해서 비동기함수를 실행시킵니다.

#https://github.com/encode/uvicorn/blob/master/uvicorn/server.py#L51
class Server:
    ...
    def run(self, sockets: list[socket.socket] | None = None) -> None:
        self.config.setup_event_loop()
        return asyncio.run(self.serve(sockets=sockets))

그렇기 때문에 Uvicorn같은 경우 Gunicorn과 다르게 thread를 설정하는 항목이 없죠.

 

Gunicorn과 Uvicorn을 그림으로 비교해보면 아래처럼 이해하면 좋습니다.

image.png


때문에 Gunicorn의 경우 Concurrency에 한계가 있지만, Uvicorn의 경우 비동기함수를 사용해서 별도의 Thread생성 없이 Concurrency를 구현할 수 있습니다.

2. Uvicorn의 동작

복잡하지 않게, 간단히 몇개의 코드만 보고 가겠습니다.

 

먼저 Uvicorn에서 제공하는 기초 코드입니다.

import uvicorn

async def app(scope, receive, send):
    ...

if __name__ == "__main__":
    uvicorn.run("main:app", port=5000, log_level="info")

Uvicorn같은 경우 uvicorn에서 제공해주는 run이라는 함수로 돌아갑니다.

해당 run()함수를 조금만 보고가면 Uvicorn이 어떤식으로 동작하는지 유추해볼 수 있습니다.

#https://github.com/encode/uvicorn/blob/master/uvicorn/main.py#L461
def run(
    app: ASGIApplication | Callable[..., Any] | str,
    *,
    host: str = "127.0.0.1",
    port: int = 8000,
    ...
    workers: int | None = None,
    ...
)
    config = Config(...)
    server = Server(config=config)
    ...
    elif config.workers > 1:
        #입력받은 host와 port가 binding된 socket을 반환
        sock = config.bind_socket()
        #만들어진 Socket을 가지고 Process에 전달해서 Process마다 Socket을 사용
        Multiprocess(config, target=server.run, sockets=[sock]).run()
    ...

보면 Uvicorn은 하나의 Socket을 만들고, 이 Socket을 가지고 workers의 개수만큼 Process를 생성합니다.

TIDK
Port번호가 Process의 구별자 라고 생각했는데, Parent Process와 Child Process가 동일한 Port번호 상태에서 동작할 수 있다는걸 이번에 알았네요.

import socket
import multiprocessing as mp
import time
def foo(s, i):    
    print(s, i)    
    while 1:
        client, address = s.accept()    
        data = client.recv(2048*2)    
        if data:
            ret = data.decode()
            for i in ret.split(r"\r\n"):
                print(i)      
            temp_msg = """HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
Content-Type: text/html; charset=UTF-8
Content-Length: 131
Accept-Ranges: bytes

<html>
<head>
  <title>An Example Page</title>
</head>
<body>
  Hello World, this is a very simple HTML document.
</body>
</html>"""      
            client.send(temp_msg.encode())              

        time.sleep(3)
        print(i)
        print("End Of HTTP\r\n")        
        client.close()

if __name__ == "__main__":
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('',22220))
    s.listen(10)
    pool = mp.Pool(8)
    pool.starmap(foo, [[s, i] for i in range(8)])

덕분에 Process개수만큼 사용자의 요청을 받아들일 수가 있게 되었습니다.

 

그리고, 각 Process 내부에서는 입력받은 config를 기반으로 asynio.create_server를 통해서 server가 만들어져서

 

유저가 보낸 Requests를 처리하는 ASGI Appliation이 돌아가게 됩니다.

#https://github.com/encode/uvicorn/blob/master/uvicorn/server.py#L51
class Server:
    def __init__(self, config: Config) -> None:
        self.config = config
        self.server_state = ServerState()
    ...
    def run(self, sockets: list[socket.socket] | None = None) -> None:
        self.config.setup_event_loop()
        return asyncio.run(self.serve(sockets=sockets))
    ....
    async def _serve(self, sockets: list[socket.socket] | None = None) -> None:
        ...
        config = self.config
        ...
        await self.startup(sockets=sockets)
        ...
    async def startup(self, sockets: list[socket.socket] | None = None) -> None:
        ...
        loop = asyncio.get_running_loop()
        listeners: Sequence[socket.SocketType]
        ...
        server = await loop.create_server(
                    create_protocol,
                    host=config.host,
                    port=config.port,
                    ssl=config.ssl,
                    backlog=config.backlog,
                )

정리.
Uvicorn은

  1. Python으로 돌아가는 프로그램이다.
  2. Process를 만들 때 spawn방식을 사용해서 window 에서도 사용할 수 있다.
  3. Socket을 공유하는 Process를 통해서 N개의 Request를 동시에 처리할 수 있다.
  4. async함수를 사용해야지만 asyncio를 통해서 비동기처리가 가능하다.(sync함수도 가능하지만, blocking이됨)

번외. Uvicorn은 Single Process인가?

구글링을 통해서 Uvicorn을 검색하면, 많은 블로그에서 Single Process기반으로 Uvicorn이 돌아가서, 모든 코어를 사용하려면 Gunicorn과 Uvicorn을 같이 써야한다. 라고 언급하고 있습니다.

 

하지만 위에 소스코드를 보면 알겠지만, workers 매개변수를 설정해서 Uvicorn자체적으로 Multiprocessing를 사용해서 다수의 requests를 처리하는것을 볼 수 있습니다.

 

그 이유를 찾아보니
https://fastapi.tiangolo.com/deployment/server-workers/

image.png


다들 아마도 해당 문서를 본것 같습니다.

하지만 이 문장은 기존에 Tutorial에서 Uvicorn을 Single Process로 썼다는 것을 의미합니다.

실제 해당 문서 아래부분에서 보면

image.png


Uvicorn도 workers를 조정해서 다수의 Process를 사용해서 requests를 처리할 수 있다고 기술하고 있습니다.

gunicorn과 uvicorn을 같이 사용하는 이유는 성능때문지, uvicorn이 single Process이기 때문이 아닙니다.

(성능의 경우 gunicorn gevent의 효율성 & uvicorn의 process생성이 spawn인 부분 때문으로 추정됨)

728x90
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함