앞선 포스팅 [Spring Boot] Jmeter로 STOMP 부하 테스트 하기
에서 모두의 마당 채팅 서비스의 성능 및 부하 테스트를 진행했습니다. 이 과정을 통해, 서비스의 성능 최적화가 필요함을 명확히 인지하게 되었습니다.
성능 향상을 위해 여러 방안을 고려하던 중, 결국 스케일 아웃을 통한 성능 향상이 최적의 해결책이라는 결론에 도달했습니다. 이에 따라, 효율적인 트래픽 분산 및 관리를 위해 nginx 로드밸런서를 도입하기로 결정했다.
목표인 아키텍쳐 구조입니다.
3개의 인스턴스
(WAS 서버 2개, nginx 서버 1개)
웹소켓 관련 test frame은 이전 포스팅에서 확인하실 수 있습니다.
로드밸런싱에도 여러 알고리즘이 있는데 맨 처음에는 일반 적인 라운드 로빈
알고리즘을 적용하였다
라운드로빈 방식(Round Robin Method)이란?
서버에 들어온 요청을 순서대로 돌아가며 배정하는 방식입니다. 클라이언트의 요청을 순서대로 분배하기 때문에 여러 대의 서버가 동일한 스펙을 갖고 있고, 서버와의 연결(세션)이 오래 지속되지 않는 경우에
활용하기 적합합니다.
따로 가중치를 두지 않으면 트래픽이 균등하게 분배된다!
sudo vi /etc/nginx/nginx.conf
nginx 설정파일이나, 따로 지정한 설정파일로 들어가줍니다
upstream 블록에 upstream 서버 즉, origin 서버의 정보를 정의하고 프록시 설정으로 요청이 왔을 때 origin 서버에 요청을 전달하도록 설정한다.
upstream origin {
# 로드밸런싱 알고리즘 선택(default는 라운드 로빈)
server xxxxxxxxxxx:8080;
server xxxxxxxxxxx:8080;
}
server {
...
location / {
proxy_pass http://origin;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
...
}
해당 설정으로 바꾸고 nginx를 재시작하면 두둥
로드밸런싱이 적용된걸 아래 사진을 통해 확인 할 수 있다.
라운드 로빈을 적용하다가 추가적인 정보를 찾아보는 도중
https://nooptoday.com/why-websockets-are-hard-to-scale/
해당 글을 읽게 되었다.
stateful
load balancing
일반적인 http 서버에서는 round robin 같은 간단한 알고리즘으로 구현할 수 있다. 하지만 웹소켓 서버에는 적절하지 않다.
http 요청들은 수명이 짧기 때문에(응답 주면 끝) 서버가 추가/제거되더라도 모든 인스턴스에 고르게 부하가 부산 된다.
websocket 서버는 persistent connection이기 때문에 새로운 서버가 추가되더라도 기존 websocket 서버가 처리하고 있던 작업들을 나눠서 처리할 수가 없다.
기존 연결을 끊고 새롭게 부하를 분산하기 위해 기존 서버들을 재시작 한다고 하더라도, 거의 동시에 재시작된다면 로드벨런서는 모든 커넥션을 새롭게 추가되는 서버에 향하도록 할 것이다.
이 문제에 대한 most elegant solution은 consistent hashing
이다.
이 방법을 사용하면 모든 connection은 끊을 필요 없이 일부 connection만 끊을 수 있다.
/gateway (get) 요청을 보내 사용 가능한 websocket server url를 받고 해당 websocket server로 연결한다.
→ 서버 1에서 서버 2로 500개의 connection을 옮기고자 한다면, 간단하게 500 connection을 끊고 /gateway를 통해 서버 2의 주소를 알려주면 된다.
/gateway는 모든 서버에 대한 load distribution을 알아야 하고 이 정보를 토대로 결정을 내려야 한다.
이 방법은 consistent hashing과 비교하여 훨씬 더 간단하다. 하지만 consistent hashing 모든 서버에 대한 load distribuion을 알 필요가 없고 작업 전에 http 요청(/gateway)을 필요로 하지도 않는다.
수평적 규모 확장성을 달성하기 위해서는 요청 또는 데이터를 서버에 균등하게 나누는 것이 중요하다.
해시 키 재배치 문제
n개의 캐시 서버가 있다고 할 때, 서버들에 부하를 균등하게 나누는 방법은 아래의 해시 함수를 적용하는 것이다.
serverIndex = hash(key) & N
서버 풀이 고정되어 있을때 이 방법은 데이터를 균등하게 각 서버에 분배하지만, 서버가 추가되거나 기존 서버가 삭제되면 문제가 생긴다.
4개의 서버에 균등하게 데이터가 분배된 상태에서 1번 서버가 죽게 되면 1번 서버에 보관되어 있는 키 뿐만 아니라 대부분의 키가 재분배되어 대부분 캐시 클라이언트가 데이터가 없는 엉뚱한 서버에 접속하게 된다. 그 결과 대규모 캐시 미스가 발생하게 될 것이다.
이를 효과적으로 해결해 줄 기술이 안정해시이다.
안정해시
안정해시는 해시 테이블 크기가 조정될 때 평균적으로 k(키의 개수)/n(슬롯의 개수)개의 키만 재배치 되는 기술이다.
전통적 해시 테이블은 슬롯의 수가 바뀌면 거의 대부분 키를 재배치한다.
우선 nginx에 Consistent Hashing 적용하기 위해서, 일반적으로 우리가
sudo yum install nginx
이렇게 nginx를 다운받으면 Consistent Hashing 알고리즘을 적용할수 없다.
🤖 그럼 어떻게 적용할수있냐?
타사 모듈과 함꼐 NGINX를 해당 모듈과 함께 소스에서 직접 컴파일해야 합니다!
위 사이트의 nginx 공식 문서를 읽어보면, 참고 nginx.conf와 모듈 오픈 소스코드가 있습니다. 저 자료들을 바탕으로, 직접 nginx를 다운받아 모듈과 같이 컴파일 하는 방법을 알아보겠습니다.
wget http://nginx.org/download/nginx-1.xx.x.tar.gz
tar -xzvf nginx-1.xx.x.tar.gz
cd nginx-1.xx.x/
sudo yum install gcc-c++ pcre-devel zlib-devel make openssl-devel
sudo apt-get install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev
NGINX를 컴파일할 때, 컴파일 옵션으로 모듈의 소스 코드 경로를 포함시킵니다.
압축 해제 경로로 이동후
./configure --with-http_ssl_module --add-module=/path/to/ngx_http_upstream_consistent_hash_module.c
make
sudo make install
옵션 | 설명 |
---|---|
--with-http_ssl_module | Https 지원 여부 |
--with-http_v2_module | http2 지원 여부 |
--with-http_mp4_module | mp4 스트리밍 지원 여부 |
--with-file-aio | Async I/O 지원 여부 |
--with-http_image_filter_module | 이미지 필터 지원 여부 |
--with-http_gzip_static_module | HTTP gzip 압축 지원 여부 |
--with-pcre= | PCRE 소스코드 경로 지정 |
--add-module= | Third-Party 라이브러리 추가 |
cd /usr/local/nginx/sbin
./nginx or sudo ./nginx
sudo vim /usr/lib/systemd/system/nginx.service
아래 내용 붙여넣기!!
[Unit]
Description=nginx - high performance web server
Documentation=http://nginx.org/en/docs/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
[Install]
WantedBy=multi-user.target
sudo pkill -9 nginx
sudo systemctl daemon-reload
sudo systemctl enable nginx.service
http {
upstream myapp {
consistent_hash $request_uri;
server backend1.example.com;
server backend2.example.com;
}
server {
listen 80;
location / {
proxy_pass http://myapp;
# 기타 필요한 설정 ...
}
}
}
sudo systemctl start nginx
$request_uri 변수를 기반으로 일관된 해시를 계산하여 요청을 서버에 분산합니다.
테스트 프레임
websocket open -> connect -> subscribe -> enter -> message
평균 응답 시간(Average Response Time): 최적화 전에는 319ms 였던 반면, 최적화 후에는 108ms로 줄어들었습니다. 이는 약 66.1%의 개선
처리량(Throughput): 초당 처리량이 최적화 전 145.4개에서 최적화 후 309.3개로 증가했습니다. 이는 약 112.7% 증가
오류(Error %): 두 경우 모두 0%
표준 편차(STD.DEV): 최적화 전에는 921.10이었던 표준 편차가 최적화 후에는 255.11로 줄어들었습니다. 이는 약 72.3% 감소
평균 응답 시간(Average Response Time): 최적화 전에 평균 1666ms 였던 반면, 최적화 후에는 1097ms로 감소하였습니다. 대략 34.2%의 개선
처리량(Throughput): 최적화 전에는 처리량이 0으로 나타났으나, 최적화 후에는 초당 658.8건으로 크게 증가했습니다. 처리량이 0에서 측정 가능한 수치로 변화한 것은 상당한 개선
오류(Error %): 오류율은 최적화 전 28.62%에서 최적화 후 9.09%로 감소했습니다. 이는 오류율이 68.25% 감소
표준 편차(STD.DEV): 표준 편차는 최적화 전 4341.75에서 최적화 후 1832.75로 감소하여, 약 57.8% 줄어듬
하지만 로드밸런싱을 적용하여 트래픽을 분산 처리했음에도 불구하고, 9%로 Read Time Out의 오류가 발생했습니다.
1000명의 유저가 채팅을 보냈을때 , ec2 인스턴스를 모니터링 해보면
CPU 사용률이 100%에 가까운걸 확인 할 수있습니다.
AWS 프리티어를 사용중이라 제한된 CPU와 메모리 자원을 가지고 있기 때문에 높은 부하 상황에서는 자원 부족이 발생한것같습니다...
인스턴스를 추가하여 스케일 아웃을 하면 성능을 추가로 향상 시킬수있겠지만, 과금 소요가 있어 저는 인스턴스 총 3대로만 진행하고 마무리하겠습니다ㅜㅜㅜ
로드 밸런싱을 적용하면서, 모두의 마당 프로젝트 채팅 서비스의 성능을 향상 시킬 수 있게 됬습니다. 이 작업을 하면서, stomp 채팅서비스 로드밸런싱에 관한 자료들이 부족해서 시행착오를 정말 많이 겪었습니다......
하지만 시행착오를 단지 시간의 소비로만 생각하지 않았고, 이를 통해서 다른 대안을 모색하고 배울 수 있는 기회로 삼고 시스템 성능에 관해 좀 더 고민함으로서 성장 할수있던 기회였던것 같습니다!!
https://www.nginx.com/resources/wiki/modules/consistent_hash/
https://github.com/replay/ngx_http_consistent_hash/blob/master/ngx_http_upstream_consistent_hash_module.c
https://semtax.tistory.com/41
https://nooptoday.com/why-websockets-are-hard-to-scale/
https://yo0on.github.io/posts/ConsistentHashing/