배경

저번 세미나 리마인드 Bad logging

uvicorn이 request를 처리하는 방식(비동기) 때문에 해당 문제가 발생한게 아닌가여?

@app.get("/sync")
def sync_endpoint():
    """동기 방식으로 로그 출력"""
    sync_logger.info(f"Sync log: {random.randint(0, 100)}")
    return {"message": "Synchronous logging"}
 
@app.get("/async")
async def async_endpoint():
    """비동기 방식으로 로그 출력"""
    async_logger.info(f"Async log: {random.randint(0, 100)}")
    return {"message": "Asynchronous logging"}
 
  • sync , async endpoint를 어떤식으로 처리하길래?
  • 파이썬 비동기 프레임워크에서 http request를 어떻게 handling 할까?

Uvicorn

starlette

ASGI server를 구현하기 위한 프레임워크

# starlette.routing
def request_response(
    func: typing.Callable[[Request], typing.Awaitable[Response] | Response],
) -> ASGIApp:
    """
    Takes a function or coroutine `func(request) -> response`,
    and returns an ASGI application.
    """
    f: typing.Callable[[Request], typing.Awaitable[Response]] = (
        func if is_async_callable(func) else functools.partial(run_in_threadpool, func)  # type:ignore
    )
 
    async def app(scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope, receive, send)
 
        async def app(scope: Scope, receive: Receive, send: Send) -> None:
            response = await f(request)
            await response(scope, receive, send)
 
        await wrap_app_handling_exceptions(app, request)(scope, receive, send)
 
    return app
 
# 
 
async def run_in_threadpool(func: typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
    func = functools.partial(func, *args, **kwargs)
    return await anyio.to_thread.run_sync(func) # sync네?
  • async def가 아닌 function def는 thread connection pool에서 처리함
    • 이는 일반적인 WSGI app 처리 방식과 동일
    • tomcat(thread pool) or gunicorn(pre-fork)

  • 왜 gunicorn(python was)s thread pool을 쓰지 않고 여러 worker를 띄울까?
    • GIL 때문에 결국 병렬처리에서 손해
  • multi thread가 안되면, 비동기 처리로 해볼까?

uvloop

  • AsyncIO 와 event loop을 통해 비동기 처리를 구현한 것은 동일
    • 다만 Python으로 구현된 AsyncIO와 달리 Cython으로 구현됌 성능 이점
  • fs, socket IO에 대한 작업들을 이벤트 루프로 처리함

libuv

https://docs.libuv.org/en/v1.x/design.html

  • Full-featured event loop backed by epoll, kqueue, IOCP, event ports.

  • file I/O 같은 경우 각 OS 별 async IO interface가 달라서 결국 동기로 처리 (multi-threading)

  • 요놈이 nodejs 에서 event loop로 사용되는 c 라이브러리

multiplexing I/O

  • 비동기처리는 1 process, 1 thread에서 socket IO를 감지

epoll (kqueue, IOCP)

epoll server

static int event_loop(struct tcp_state *state)
{
    int err;
    int ret = 0;
    int timeout = 3000; /* in milliseconds */
    int maxevents = 32;
    int epoll_ret;
    int epoll_fd = state->epoll_fd;
    struct epoll_event events[32];
 
    printf("Entering event loop...\n");
 
    while (!state->stop) {
 
        /*
         * I sleep on `epoll_wait` and the kernel will wake me up
         * when event comes to my monitored file descriptors, or
         * when the timeout reached.
         */
        epoll_ret = epoll_wait(epoll_fd, events, maxevents, timeout);
 
 
        if (epoll_ret == 0) {
            /*
             *`epoll_wait` reached its timeout
             */
            printf("I don't see any event within %d milliseconds\n", timeout);
            continue;
        }
 
 
        if (epoll_ret == -1) {
            err = errno;
            if (err == EINTR) {
                printf("Something interrupted me!\n");
                continue;
            }
 
            /* Error */
            ret = -1;
            printf("epoll_wait(): " PRERF, PREAR(err));
            break;
        }
 
 
        for (int i = 0; i < epoll_ret; i++) {
            int fd = events[i].data.fd;
 
            if (fd == state->tcp_fd) {
                /*
                 * A new client is connecting to us...
                 */
                if (accept_new_client(fd, state) < 0) {
                    ret = -1;
                    goto out;
                }
                continue;
            }
 
            /*
             * We have event(s) from client, let's call `recv()` to read it.
             */
            handle_client_event(fd, events[i].events, state);
        }
    }
 
out:
    return ret;
}

usage

  • tomcat

  • NIO 구현체를 확인하면 epoll을 JNI로 래핑해서 사용
    • NIO Selector는 플랫폼에 따라 (epoll, kqueue, IOCP)를 사용

tomcat version 9부터 NIO만 지원