티스토리 뷰

Concurrent한 HTTP Requests가 python과 go는 어떻게 다른지 알아봅니다.

 

multiple requets를 간단하게 go로 작성하여 직접 확인해 봤습니다. python으로는 httpx를 사용해볼 수 있겠습니다.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"
)

type response struct {
    Hello  string `json: "hello"`
    Client string `json: "client"`
}

func main() {
    defer timeTrack(time.Now(), "main")
    server := []string{"A", "B", "C", "D", "E"}

    var wg sync.WaitGroup
    wg.Add(len(server))
    for _, s := range server {
        go func(s string) {
            defer wg.Done()
            res, err := http.Get("http://localhost:8000/?client=" + s)
            if err != nil {
                panic(err)
            }
            defer res.Body.Close()
            r := new(response)
            json.NewDecoder(res.Body).Decode(&r)
            log.Println(r)
        }(s)
    }
    wg.Wait()
}

// Track time past
// reference: https://coderwall.com/p/cp5fya/measuring-execution-time-in-go
func timeTrack(start time.Time, name string) {
    elapsed := time.Since(start)
    fmt.Printf("!! %s took %s", name, elapsed)
}

Go

도움됐던 stackoverflow를 소개합니다.

Question("Process Management for the go Webserver")

Python, Ruby 등에 익숙한 저는 어플리케이션 앞단에 Nginx, Apache 등과 같은 웹 서버를 위치합니다. worker의 thread나 proccess를 조정하여 각각의 thread가 처리 하는 각각의 HTTP 연결 갯수를 설정합니다.

Go의 웹 서버에서는 어떻게 다루고 있고 scale하는 방법을 알고싶다.

만약 아래와 같은 HTTP request를 다루는 함수가 있다하자.

func main() {
   http.HandleFunc("/", processRequest)
   http.ListenAndServe(":8000", nil)    
}

한번에 HandleFunc이 다루는 connection이 몇개 인지, connection이 생기면 blocking이 되는지 궁금합니다.

go routine으로 따로 함수를 처리해야 할까요? 근데 그러면 많은 thread로 인해 시스템이 죽는 경우를 어떻게 막아야 할까요?

정리하면

  1. go의 웹 서버의 proccess를 이해하고 싶습니다.
  2. go가 이러한 문제를 내장 기능으로 어떻게 다루고 있는지 아니면 사용할만한 표준 패키지가 있는지 궁금합니다.

Answer(Tweaking/configuration HTTP server)

HTTP server의 구현체로 http.Server가 있습니다. http.Server를 만들지 않는다면. http.ListenAndserve() 함수를 호출하여 http.Server를 만들 수 있습니다.

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

HTTP server를 커스텀하고 싶다면 직접 Server.ListenAndServe() 메소드를 호출 하면 됩니다.

Server.Serve()문서를 보면 아래와 같습니다.

Serve는 Listner의 l에서 들어오는 연결을 허용하여 각각에 대해 새로은 서비스 go routine을 만듭니다. 서비스 go routine은 requests를 read하여 reply하기 위해 srv.Handler를 호출합니다. Serve는 항상 non-nil 에러를 반환합니다.

따라서 들어오는 각각의 HTTP requests는 새로운 go routine에서 처리 됩니다. 즉 concurrent하게 serve 된다는 말이죠. 하지만 문서에는 그 이상으로 나와 있지 않고 작동되는 방법을 바꾸는 내용은 나와 있지 않습니다.

(stackoverflow는 1.6.2버전이 나와 있습니다.)

현재 구현되어있는 (Go 1.17.4 )의 serve.go 소스를 보면 코드(https://github.com/golang/go/blob/go1.17.4/src/net/http/server.go#L3034)를 보면 아래와 같습니다.

func (srv *Server) Serve(l net.Listener) error {
    if fn := testHookServerServe; fn != nil {
        fn(srv, l) // call hook with unwrapped listener
    }

    origListener := l
    l = &onceCloseListener{Listener: l}
    defer l.Close()

    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }

    if !srv.trackListener(&l, true) {
        return ErrServerClosed
    }
    defer srv.trackListener(&l, false)

    baseCtx := context.Background()
    if srv.BaseContext != nil {
        baseCtx = srv.BaseContext(origListener)
        if baseCtx == nil {
            panic("BaseContext returned a nil context")
        }
    }

    var tempDelay time.Duration // how long to sleep on accept failure

    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()
        if err != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := err.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return err
        }
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks) // before Serve can return
        go c.serve(connCtx)
    }
}

Go에서의 웹 어플리케이션는 따로 process를 컨트롤할 서버가 필요 없다. Go 자체의 웹서버에서 request들을 concurrent하게 다루기 때문이다.

즉 Go로 작성된 서버를 실행 시키면 이미 Production 상태 인 것이다.

추가적으로 HTTPS, 인증/인가, 라우팅, 로드밸런싱등 작업도 추가할 수가 있다.

Python

python에서의 concurrent한 requests를 처리하는 것은 asyncio보다 web server와 관련이 있다.

비동기 함수

python에서는 비동기 프로그래밍을 할 때 async, await를 사용한다. 함수 앞에 async를 붙여주기만 하면 coroutine이 된다.

# 동기
def func_sync():
    pass

# 비동기
async def func_async():
    pass

FastAPI

fastAPI는 async을 안붙이면 external threadpool를 사용하여 비동기적으로 request를 처리한다.

When you declare a path operation function with normal def instead of async def, it is run in an external threadpool that is then awaited, instead of being called directly (as it would block the server).
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}

Django

multiple requests를 concurent하게 처리하는 방법이 django 코드를 수정해야 한다고 생각 했다. 결론은 아니었고 server와 관련이 있었다.

What happens when Django receives multiple requests?

Your question has nothing to do with Django. It's the responsibility of a WSGI server that runs your Django application, and the server may employ various concurrency/parallelism models. Usually it's multiple workers that run in separate processes to allow parallel processing of requests.

runserver(default server)

django의 기본 server로 실행하게되면 매 request마다 thread로 처리가 되기 때문에 def로 작성해도 concurrent 하다.

$ python manage.py runserver

# single thread로 실행하는 방법
$ python manage.py runserver --nothreading

gunicorn

gunicorn은 기본적으로 하나의 worker마다 하나의 thread를 사용하기 때문에 worker의 갯수 만큼 concurrent하게 request처리가 된다. concurrent하게 처리하기 위해서는 worker의 갯수(-w)와 theread(--threads)를 따로 신경써서 설정해줘야 한다.

$ gunicorn --bind 0.0.0.0:8000 -w 1 --threads 5 

$ gunicorn --bind 0.0.0.0:8000 -w 5

Reference

Go

Python async/await

FastAPI

Django

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함