ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Application 튜닝해서 성능 최적화하기
    커널(Kernel) 2022. 12. 26. 01:03

     

    이번 포스팅은 redis, flask로 이루어진 애플리케이션을 system level, app level에서 어떻게 튜닝하고

    최적화할 수 있는지에 대해서 알아본다. 

     

    환경 정보

    - OS: ubuntu 20.04

    - IP: 172.30.1.40

    - redis가 설치된 환경

    - 부하 테스트를 위한 siege가 설치된 환경 

    - net.ipv4.tcp_max_tw_buckets=30000(time wait socket 개수를 30000개로 설정)

    - net.ipv4.tcp_tw_reuse = 0 (time wait 소켓을 재사용하지 않음)

    - net.ipv4.ip_local_port_range = 30000  60000 (약 3만 개의 포트를 사용할 수 있도록 설정)

    - redis, flask, siege는 모두 동일한 환경에 설치된다. 

     

     

    Flask Application 만들기 

    간단한 애플리케이션을 만들기 위해 test.py를 작성한다.

    import redis
    import time
    from flask import Flask
    app =Flask(__name__)
    
    
    @app.route("/test/<key>")
    def testApp(key):
        r = redis.StrictRedis(host="127.0.0.1", port=6379, db=0)
        r.set(key, time.time())
    
        return r.get(key)
    
    if __name__=="__main__":
        app.run(host="0.0.0.0")

    위 코드는 요청이 들어오면 같은 환경에 설치되어 있는 redis와 커넥션을 맺는다. 

     

    위 스크립트를 통해 서버를 실행시킨 후,

     

    siege를 통해서 flask server에 부하 테스트를 한다. 

    siege -c 100 -b -t10s  http://172.30.1.40:5000/test/1
    # -c 는 동시 사용자수 
    # -b 벤치마킹 모드로 동작
    # -t는 시간

     

    위 결과를 통해서 초당 약 761번의 트랜잭션을 성공시켰고, 응답시간이 0.13s라는 것을 알 수 있다.  

     

     

    CPU 효율 높이기  

    현재의 시스템에서는 CPU가 2 core이고,

    flask 프로세스는 싱글 스레드로 동작할 것이기 때문에 core 하나가 놀 것이다.  

     

    따라서 멀티스레드를 사용하거나 프로세스를 여러 개로 두면 성능을 높일 수 있다. 

    나의 경우에는 gunicorn을 이용해서 4개의 프로세스를 생성했다. 

     

    다음과 같이 gunicorn을 설치한다.

    pip install gunicorn==20.1.0 eventlet==0.30.2

     

    gunicorn을 통해서 서버를 실행시킨다. 

    gunicorn -w 4 -b 0.0.0.0:5000 test:app

     

    그리고 다시 siege를 통해서 부하를 측정해보면, 프로세스를 4개로 늘렸을 뿐인데

    초당 트랜잭션이 약 1800으로 엄청나게 상승한 것을 알 수 있다. 

     

    그리고 그 만큼 time wait 소켓 또한 약 3만 개가 생성이 되었다.  

     

     

    네트워크 소켓 최적화하기 

    현재 사용할 수 있는 포트의 개수를 약 3만 개로 설정했기 때문에

    로컬 포트 고갈이 발생할 수 있는 아슬아슬한 상태이다. 

     

    그리고 flask 서버 => redis에 대한 timewait 소켓의 개수를 확인해 보면

    약 17000개로 엄청나게 많은 timewait 소켓이 생기고, 이는 로컬 포트고갈로 이어질 수 있다. 

    flask => redis로 가는 time wait 소켓을 줄이려면 connection pool 방식을 사용하면 된다.  

     

    기존의 방식은 요청이 올 때마다 소켓을 만들고 제거하는 방식이었고,  

    connection pool 방식은 소켓을 미리 할당해서 해당 소켓을 계속 유지하는 방법이다. 

     

    다음은 connection pool을 이용한 pool.py 스크립트다.  

    import redis
    import time
    from flask import Flask
    app =Flask(__name__)
    pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0)
    
    @app.route("/test/<key>")
    def testApp(key):
        r = redis.Redis(connection_pool=pool)
        r.set(key, time.time())
    
        return r.get(key)
    
    if __name__=="__main__":
        app.run(host="0.0.0.0")

     

    이후 다시 서버를 실행시킨 후, 부하를 측정해 본다. 

     

    부하 테스트를 실시한다. 

     

    요청을 보낼 때마다 발생하는 tcp 커넥션 맺고/끊음 과정이 줄었기 때문에 초당 트랜잭션 수가 약 2300으로 늘었다.  

     

    또한 flask => redis는 커넥션은 ESTABLISHED를 계속 유지하기 때문에 time wait의 개수가 17000개에서 0개로 줄었다. 

     

    하지만 여전히 bash 클라이언트 <=> flask 서버에 대한 time wait 소켓은 약 14000개로 많이 존재한다.

     

    nestat를 통해서 자세히 확인해 보면,

    bash 클라이언트 쪽에서 끊는 경우와 flask 서버 쪽에서 먼저 연결을 끊는 경우를 확인할 수 있었다.

     

    telnet을 통해서 flask로 직접 요청을 보내본 결과,

    header에 [connection: close]라는 헤더값을 넣어 보내주면서 flask 쪽에서 연결을 먼저 끊어 time wait 소켓을 생성한다. 

     

    이렇게 요청을 하나하나 보낼 때마다 커넥션을 새로 만들고, 없애는 방식은 비효율적이다. 

    또 time wait socket도 너무 많이 생긴다.  

     

    이럴 때는 keepalive를 이용해 세션을 만들고, 세션을 더 이상 사용하지 않을 때 끊는 것이 효율적이다. 

     

    그래서  이번에는 gunicorn으로 띄워둔 flask 프로세스들에 keepalive를 적용한다. 

    gunicorn -w 4 -b 0.0.0.0:5000 pool:app --keep-alive 10 -k eventlet

     

    이후 다시 telnet을 통해 요청을 보냈을 때 keepalive 헤더가 내려오는 것을 확인할 수 있다. 

     

    flask 쪽 keepalive가 잘 동작하는 것을 확인했으니 bash 클라이언트에서 siege에 keep alive 헤더를 담아 요청을 보내면 된다. 

    siege -c 100 -b -t30s  http://172.30.1.40:5000/test/1 -H "Connnection: Keep-Alive"

     

    여기서 문제가 조금 있긴 한데...

    현재 내가 사용하고 있는 siege 버전에서는 [connection: close] 헤더를 디폴트로 보내며, 

    keepalive 헤더를 함께 넣어주었을 때 오버라이드 되지 않고, connection 헤더가 다음과 같이 두 개씩 찍힌다...

    즉, keepalive가 원하는 대로 동작하지 않는다. 

     

     

    그래서 이 부분은 정상적으로 테스트하지 못했고, 만약 클라이언트를 새로 만들게 된다면

    flask로 요청을 보낼 때 keepalive를 헤더에 넣어서 보내면 될 것이다. 

     

    만약 keepalive가 정상적으로 동작했다면 불필요한 TCP handshake를 줄여 초당 트랜잭션도 높아질 것이고, 

    flask => client로 보내는 timewait 소켓을 많이 줄일 수 있을 것이다. 

     

    client => flask일 때도 time wait 소켓이 많이 생길 수 있는데, 

    이렇게 많은 time wait 소켓이 시스템을 차지하는 경우 로컬 포트 고갈 문제가 발생할 수 있다.   

     

    이렇게 클라이언트 측에서 포트 고갈 문제가 있을 때는 

    다음 커널 파라미터를 1로 설정하여 time wait 소켓을 재사용할 수 있다.  

    sysctl -w net.ipv4.tcp_tw_reuse=1

     

    다시 siege를 통해서 부하를 측정한 결과, 성능이 어느 정도 유지되면서 time wait 소켓이 일부 줄었다. 

     

    참고로 reuse옵션은 time wait 소켓을 일부 줄이는 기능도 있겠지만, 

    다음과 같이 로컬 포트 고갈 문제가 발생할 수 있는 상황을 위해서 보통은 1로 설정하는 것이 좋다고 한다. 

     

    참고로 나도 잘 모르기 때문에 틀린 내용이 있을 수 있다.  

     

    Reference

    Devops와 SE를 위한 리눅스 커널 이야기 

    반응형

    댓글

Designed by Tistory.