이전 글에서 Django Channels를 배포하는 방법에 대해 알아보았습니다. ( 링크 )

이번에는 Nginx worker, uWSGI worker를 조정하고 또한 Daphne 인스턴스를 늘리고 load balancing을 통해 대용량 트래픽에 대한 해결을 하려고 합니다.


여기서 다루는 내용들은 검색과 Channels, Daphne 깃헙의 issue들을 살펴보면서 언급된 내용들을 정리한 것이니 제가 잘못 이해한 부분이 있을 수 있고, 또한 이 분야의 전문가가 아니기 때문에 이 방식대로 하면 대용량 트래픽을 해결할 수 있다고 확신하지는 못하겠습니다.

그래서 stress tool( Apache JMeter )을 써서 테스트를 해보았고, 결과가 예상한 대로 나와서 어느 정도 신뢰할 만한 참고자료가 될 것 같아 이를 공유하려 합니다.




먼저 worker에 대한 개념과 Django Channels, Daphne이 어떻게 동작하는지 대략적으로 살펴보겠습니다.


Worker

Nginx와 uWSGI의 설정 파일을 보면 worker라는 것이 있습니다.

worker는 클라이언트와의 연결을 효율적으로 처리하기 위한 프로세스입니다.


1) Nginx worker

아래의 코드는 Nginx 설정 파일의 일부입니다.

worker_processes 1; events { worker_connections 1024; }

요청을 받을 수 있는 클라이언트는 총 worker_process * worker_connections 입니다. ( 위 예에서는 1024개 )

worker_connections는 하나의 worker_processes가 받을 수 있는 클라이언트 개수를 의미하고,

worker_proecess는 CPU의 총 개수와 맞추는 것이 좋습니다.


따라서 CPU 개수에 따라 worker_processes를 늘리면 많은 요청을 받을 수 있고, 또한 worker_connections를 늘려도 많은 요청을 받을 수 있습니다.

단, worker_processes는 한정적이기 때문에, worker_connections를 늘리는 것이 트래픽을 해결 하는데에 있어 현실적인 방안일 것입니다.




2) uWSGI worker

다음으로는 uWSGI 설정 파일을 보도록 하겠습니다.

[uwsgi] ... processes = 5 ...

uwsgi 설정 파일에서는 worker를 processes로 명시합니다.

uwsgi를 실행한 후, log를 찍어보면 아래와 같이 worker가 5개 생성되는 것을 확인할 수 있습니다.


저는 왜 uWSGI가 쓰레드를 생성하지 않고 프로세스를 생성할까 고민을 했었는데, 이에 대한 좋은 답변이 있었습니다. ( 링크 )

위 링크에 따르면 Python 쓰레드의 증가는 동시성을 향상 시키지만, 성능은 조금밖에 향상 되지 않아서 성능을 개선하고자 한다면 process를 늘리는 것이 좋다고 합니다.




3) Daphne worker

Daphne의 wokrer를 여러 개 생성하는 명령어는 Daphne을 실행할 때 -v 옵션으로 -v2와 같이 worker의 수를 작성해주면 됩니다.

그러나 CPU core의 개수만큼 worker를 생성하는 것을 추천하기 때문에, 1개만 생성하는 것을 추천합니다.
( Daphne worker의 개수는 기본 값이 1개 입니다. )


정리하면 worker는 클라이언트와의 요청을 받아들이고 처리하는 프로세스이고,
스케일 업을 하기 위해서는 Nginx, uWSGI의 worker를 늘려 가면서 테스트를 해봐야 합니다.




Channels 멀티쓰레드 및 스케일링
Channels는 멀티 프로세스로 실행되며, 프로세스 당 2~4개 이상의 쓰레드를 실행하는 것이 좋은데, 
쓰레드는 ThreadPoolExecutor의 기본 값인(CPUs * 5)개가 실행됩니다.
물론 Thread safe 하게 말이죠.

쓰레드는 실행할 수 있는 동기화 작업의 최대 수를 의미하는 ASGI_THREADS 환경 변수를 사용하여 수를 조정할 수도 있지만,
클라이언트의 동시 접속 허용과는 직접적인 관련이 없을 뿐더러, context swtiching에 의해 많은 쓰레드가 더 빠르다는 보장도 없습니다.

또한 Channels는 여러 인스턴스를 실행할 수 없습니다.

그렇다면 쓰레드를 늘려봤자 의미가 없고, 여러 인스턴스를 늘릴수도 없는데 어떻게 확장 시킬 수 있을까요?
Channels 개발자는 이에 대해 현재로썬 reverse proxy를 통해 load balancing을 하는 방법 밖에 없다고 말합니다.



지금까지 대용량 처리를 위해 알아야 할 개념들을 살펴보았습니다.
위의 내용들을 그림으로 표현하면 다음과 같습니다.


따라서 저는 Nginx worker , uWSGI worker를 조정 하면서 HTTP 요청에 대한 성능 테스트를 진행했고,
Daphne 인스턴스를 3개로 늘리면서 load balancing을 통해 WS 요청에 대한 성능 테스트를 진행해보았습니다.
( 테스트를 위한 툴은 Apache JMeter를 사용했고, 사용 방법에 대한 글은 여기를 참고해주세요 )

결과부터 말씀 드리면,
[ 1번. HTTP 요청 10000건 , 동시 접속자 수 100명  /  2번. HTTP 요청 20000건 , 동시 접속자 수 200명 ] 에 대해
[ nginx worker : 1024, 4000 / uwsgi worker : 5, 10 ] 를 조합해서 총 8가지 경우에 대한 테스트 결과 
1번, 2번 두 경우에 대해 nginx worker : 4000 / uwsgi worker : 10이 가장 좋은 응답 시간을 보였습니다.

또한 WS 요청은 Daphne 서버를 1개 실행하는 것보다, Daphne 서버 3개를 실행하여 load balancing 하는 것이 시스템 자원을 적게 소모했습니다.




테스트는 서버 상황에 맞게 직접 테스트 해보시면 좋을 것 같고,

그렇다면 이제 남은 것은 어떻게 구현을 하느냐는 것입니다.


worker를 설정하는 방법은 Nginx 설정 파일, uwsgi 설정 파일에서 숫자만 바꾸면 되므로 어렵지 않습니다.

문제는 Daphne 서버를 여러 개 생성하여 3개의 인스턴스를 load balancing 하는 것인데, 이것도 어렵지 않습니다.



Daphne load balancing

먼저, Daphne을 데몬으로 실행시킬 수 있어야 합니다.

저는 Daphne를 service로 실행했었는데, 이와 관련된 내용은 여기를 참고해주세요.


1) 인스턴스 생성

Daphne.service를 등록했다면 여러 개의 인스턴스를 생성하는 방법은 간단합니다.

3개의 service 파일을 만들고, ExecStart 부분만 수정하면 됩니다.

수정할 부분은 포트 번호, access-log 경로 두 가지입니다.

# vi /etc/systemd/system/daphne1.service
# vi /etc/systemd/system/daphne2.service
# vi /etc/systemd/system/daphne3.service
기존의 daphne.service에서 ExecStart의 포트 번호와 로그 경로만 바꾼다.
daphne1.service		=> 8443 , --access-log /var/log/daphne/daphne1.log
daphne2.service		=> 8444 , --access-log /var/log/daphne/daphne2.log
daphne3.service		=> 8445 , --access-log /var/log/daphne/daphne3.log


다음으로 서비스를 등록하고, 실행합니다. ( 물론 Nginx, uWSGI 서비스도 같이 실행을 해야 할 것입니다. )

# systemctl enable daphne1.service # systemctl enable daphne2.service # systemctl enable daphne3.service # systemctl start daphne1.service # systemctl start daphne2.service # systemctl start daphne3.service

3개의 daphne service를 동시에 실행하면 가끔 몇 개는 실행이 안되는 경우가 있으므로 journalctl -u 서비스이름 명령어로 서비스 실행이 잘 되었나 확인하는 것이 좋습니다.




2) load balancing

이제 3개의 Daphne 인스턴스를 생성했으니 Nginx에서 Load balancing 설정을 하겠습니다.


Nginx는 라운드 로빈, least_conn , ip_hash , hash 4개의 로드밸런싱 메서드를 제공하는데,

여기서는 기본 값으로서, 모든 서버에 동등하게 요청을 분산하는 라운드 로빈 방식을 사용하도록 하겠습니다.



아래의 사진은 이전 글에서 작성한 proxy 설정 일부인데, /ws 경로에 대해 8443 포트로 실행되고 있는 하나의 Daphne 서버가 처리하도록 되어 있습니다.


지금은 여러 개의 인스턴스가 있는 상황이므로 포트 번호를 명시하는 것이 아니라, upstream을 작성해야 합니다.

즉 proxy_pass에 load balancing 이름( daphne-server )을 작성한 후, upstream에 daphne 서버들을 나열하면 됩니다.

# vi /etc/nginx/conf.d/myproject.conf

server{ listen 443 ssl; server_name 192.168.203.139; ssl_certificate /etc/letsencrypt/live/3ab7cf4c.ngrok.io/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/3ab7cf4c.ngrok.io/privkey.pem; location / { include uwsgi_params; uwsgi_pass unix:/run/uwsgi/myproject.sock; } location /static/ { root /usr/local/victolee/channels_app/; access_log off; } location /ws { proxy_pass https://daphne-server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } # load balancing - round robin upstream daphne-server{ server 192.168.203.139:8443; server 192.168.203.139:8444; server 192.168.203.139:8445; }

따라서 /ws 경로의 요청에 대해, daphne-server upstream이 실행되고, 라운드 로빈 방식으로 8443, 8444, 8445 포트가 실행되고 있는 Daphne 서버로 load balancing이 될 것입니다.




3) html 수정

마지막으로 해야 할 것은, html 파일에서 WebSocket을 생성하는 부분을 수정해야 합니다.

# vi /usr/local/victolee/Channels_test/channels_app/templates/chat/room.html var chatSocket = new WebSocket( 'wss://' + window.location.hostname + '/ws/chat/' + roomName + '/');

원래는 WebSocket을 생성할 때, :8443/ws/chat 이였지만, 이제는 여러 개의 Daphne 인스턴스가 있으므로 포트 번호를 생략합니다.




이제 설정은 끝났습니다.

WS 요청이 Daphne1, Daphne2, Daphne3으로 잘 분배되는지 확인하기 위해, 메시지를 보내보고 각각의 access log를 확인해보면 됩니다.

그리고 load balancing 성능 테스트는 top 명령어를 통해 Daphne이 1개 있을 때와 비교하여 CPU와 Memory를 확인하시면 됩니다.



이상으로 Django Channels 애플리케이션을 배포할 때, 대용량 트래픽을 처리하는 방법에 대해 알아보았습니다.


[ 참고 ]

http://lists.unbit.it/pipermail/uwsgi/2011-April/001883.html

https://avilpage.com/2018/05/deploying-scaling-django-channels.html

https://github.com/django/channels/issues/215

https://github.com/django/channels/issues/960

https://gist.github.com/agronick/692d9a7bc41b75449f8f5f7cad93a924