[Nginx] 로드밸런싱 적용을 통한 채팅 서비스 개선기 - Consistent Hashing

이민우·2023년 12월 9일
2
post-thumbnail

도입

앞선 포스팅 [Spring Boot] Jmeter로 STOMP 부하 테스트 하기
에서 모두의 마당 채팅 서비스의 성능 및 부하 테스트를 진행했습니다. 이 과정을 통해, 서비스의 성능 최적화가 필요함을 명확히 인지하게 되었습니다.

성능 향상을 위해 여러 방안을 고려하던 중, 결국 스케일 아웃을 통한 성능 향상이 최적의 해결책이라는 결론에 도달했습니다. 이에 따라, 효율적인 트래픽 분산 및 관리를 위해 nginx 로드밸런서를 도입하기로 결정했다.

목표인 아키텍쳐 구조입니다.
3개의 인스턴스 (WAS 서버 2개, nginx 서버 1개)

웹소켓 관련 test frame은 이전 포스팅에서 확인하실 수 있습니다.

라운드 로빈 적용

로드밸런싱에도 여러 알고리즘이 있는데 맨 처음에는 일반 적인 라운드 로빈 알고리즘을 적용하였다

라운드로빈 방식(Round Robin Method)이란?

서버에 들어온 요청을 순서대로 돌아가며 배정하는 방식입니다. 클라이언트의 요청을 순서대로 분배하기 때문에 여러 대의 서버가 동일한 스펙을 갖고 있고, 서버와의 연결(세션)이 오래 지속되지 않는 경우에 활용하기 적합합니다.

따로 가중치를 두지 않으면 트래픽이 균등하게 분배된다!

👉 Nginx 서버에 로드밸런싱 설정

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를 재시작하면 두둥
로드밸런싱이 적용된걸 아래 사진을 통해 확인 할 수 있다.

👆왼쪽이 A인스턴스, 오른쪽이 B인스턴스(타이틀이 다른걸 확인 가능)

💻 라운드 로빈 변경

라운드 로빈을 적용하다가 추가적인 정보를 찾아보는 도중
https://nooptoday.com/why-websockets-are-hard-to-scale/
해당 글을 읽게 되었다.

  • stateful

    • 클라이언트가 처음 연결한 서버와 지속 연결됨
      그래서 서버 간 메시지를 주고받을 방법이 필요함
  • load balancing

    일반적인 http 서버에서는 round robin 같은 간단한 알고리즘으로 구현할 수 있다. 하지만 웹소켓 서버에는 적절하지 않다.

    http 요청들은 수명이 짧기 때문에(응답 주면 끝) 서버가 추가/제거되더라도 모든 인스턴스에 고르게 부하가 부산 된다.

    websocket 서버는 persistent connection이기 때문에 새로운 서버가 추가되더라도 기존 websocket 서버가 처리하고 있던 작업들을 나눠서 처리할 수가 없다.

    기존 연결을 끊고 새롭게 부하를 분산하기 위해 기존 서버들을 재시작 한다고 하더라도, 거의 동시에 재시작된다면 로드벨런서는 모든 커넥션을 새롭게 추가되는 서버에 향하도록 할 것이다.

    • 서버에 재연결 요청이 급증하기 때문에 서버 부하가 한순간에 크게 증가할 수있다.
    • 서버 수의 변동이 잦으면 클라이언트는 매번 reconnect를 해야 하고 이는 사용자 경험에 안 좋은 영향을 끼친다.
      → elegant solution 아니다.

이 문제에 대한 most elegant solution은 consistent hashing이다.

이 방법을 사용하면 모든 connection은 끊을 필요 없이 일부 connection만 끊을 수 있다.


수많은 웹소켓 연결을 관리하는 discord는 아래 방식으로 문제를 해결

  • /gateway (get) 요청을 보내 사용 가능한 websocket server url를 받고 해당 websocket server로 연결한다.
    → 서버 1에서 서버 2로 500개의 connection을 옮기고자 한다면, 간단하게 500 connection을 끊고 /gateway를 통해 서버 2의 주소를 알려주면 된다.

  • /gateway는 모든 서버에 대한 load distribution을 알아야 하고 이 정보를 토대로 결정을 내려야 한다.

    • 간단하게 minimum load를 가진 서버의 url을 반환할 수있다.
  • 이 방법은 consistent hashing과 비교하여 훨씬 더 간단하다. 하지만 consistent hashing 모든 서버에 대한 load distribuion을 알 필요가 없고 작업 전에 http 요청(/gateway)을 필요로 하지도 않는다.

결론

웹소켓 서버의 추후에나 확장성 문제를 해결하기 위해 Consistent Hashing을 적용
  1. 지속적 연결 관리:
    웹소켓은 지속적인 연결을 유지하므로, 서버 추가/제거 시 기존의 라운드 로빈 방식으로는 효과적인 부하 분산이 어렵습니다.
  2. 부분적 재분배:
    Consistent Hashing을 사용하면 서버 구성 변경 시 모든 연결을 재설정할 필요 없이, 최소한의 연결만 재분배할 수 있습니다.
  3. 균형있는 부하 분산:
    새로운 서버 추가 시에도 기존 연결의 대부분을 유지하면서 효과적으로 부하를 분산시킬 수 있습니다.
  4. 확장성 개선:
    서버의 추가/제거가 전체 시스템에 미치는 영향을 최소화하여 안정적인 확장이 가능합니다.
  5. 사용자 경험 향상:
    잦은 재연결을 피함으로써 서비스의 안정성과 사용자 경험을 개선할 수 있습니다.

Consistent Hashing이란?

  • 수평적 규모 확장성을 달성하기 위해서는 요청 또는 데이터를 서버에 균등하게 나누는 것이 중요하다.

  • 해시 키 재배치 문제
    n개의 캐시 서버가 있다고 할 때, 서버들에 부하를 균등하게 나누는 방법은 아래의 해시 함수를 적용하는 것이다.
    serverIndex = hash(key) & N

  • 서버 풀이 고정되어 있을때 이 방법은 데이터를 균등하게 각 서버에 분배하지만, 서버가 추가되거나 기존 서버가 삭제되면 문제가 생긴다.
    4개의 서버에 균등하게 데이터가 분배된 상태에서 1번 서버가 죽게 되면 1번 서버에 보관되어 있는 키 뿐만 아니라 대부분의 키가 재분배되어 대부분 캐시 클라이언트가 데이터가 없는 엉뚱한 서버에 접속하게 된다. 그 결과 대규모 캐시 미스가 발생하게 될 것이다.

  • 이를 효과적으로 해결해 줄 기술이 안정해시이다.

  • 안정해시
    안정해시는 해시 테이블 크기가 조정될 때 평균적으로 k(키의 개수)/n(슬롯의 개수)개의 키만 재배치 되는 기술이다.
    전통적 해시 테이블은 슬롯의 수가 바뀌면 거의 대부분 키를 재배치한다.

채팅서비스에 Consistent Hashing 적용

우선 nginx에 Consistent Hashing 적용하기 위해서, 일반적으로 우리가

sudo yum install nginx

이렇게 nginx를 다운받으면 Consistent Hashing 알고리즘을 적용할수 없다.

🤖 그럼 어떻게 적용할수있냐?

타사 모듈과 함꼐 NGINX를 해당 모듈과 함께 소스에서 직접 컴파일해야 합니다!

nginx consistent hashing 적용 방법
모듈 github-도메인 사용할 경우

위 사이트의 nginx 공식 문서를 읽어보면, 참고 nginx.conf와 모듈 오픈 소스코드가 있습니다. 저 자료들을 바탕으로, 직접 nginx를 다운받아 모듈과 같이 컴파일 하는 방법을 알아보겠습니다.

모듈 소스 코드 준비

  • 공통
    아래 github url에서 ngx_http_upstream_consistent_hash_module.c 다운
  1. 도메인 사용할 경우
    https://github.com/replay/ngx_http_consistent_hash/tree/dns
  2. 사용 x
    https://github.com/replay/ngx_http_consistent_hash

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/

필요한 라이브러리 설치

  • Red Hat/CentOS 기반 시스템의 경우
sudo yum install gcc-c++ pcre-devel zlib-devel make openssl-devel
  • Debian/Ubuntu 기반 시스템의 경우
sudo apt-get install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev

NGINX 컴파일 및 설치

NGINX를 컴파일할 때, 컴파일 옵션으로 모듈의 소스 코드 경로를 포함시킵니다.

압축 해제 경로로 이동후

./configure --with-http_ssl_module --add-module=/path/to/ngx_http_upstream_consistent_hash_module.c
make
sudo make install
옵션설명
--with-http_ssl_moduleHttps 지원 여부
--with-http_v2_modulehttp2 지원 여부
--with-http_mp4_modulemp4 스트리밍 지원 여부
--with-file-aioAsync I/O 지원 여부
--with-http_image_filter_module이미지 필터 지원 여부
--with-http_gzip_static_moduleHTTP gzip 압축 지원 여부
--with-pcre=PCRE 소스코드 경로 지정
--add-module=Third-Party 라이브러리 추가

설치된 경로로 이동

cd /usr/local/nginx/sbin

nginx 시작

./nginx or sudo ./nginx

nginx systemctl에 등록하여 서비스 시작시키기

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
  • 기존의 nginx 서비스를 종료
sudo pkill -9 nginx
  • systemctl 데몬을 설정 정보를 갱신합니다.
sudo systemctl daemon-reload
sudo systemctl enable nginx.service

nginx.conf 설정

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 변수를 기반으로 일관된 해시를 계산하여 요청을 서버에 분산합니다.

Consistent Hashing 적용 후 채팅 서비스 성능 테스트

테스트 프레임
websocket open -> connect -> subscribe -> enter -> message

User 100명

로드밸런싱 적용 전

로드밸런싱 적용 후

비교 분석

  • 평균 응답 시간(Average Response Time): 최적화 전에는 319ms 였던 반면, 최적화 후에는 108ms로 줄어들었습니다. 이는 약 66.1%의 개선

  • 처리량(Throughput): 초당 처리량이 최적화 전 145.4개에서 최적화 후 309.3개로 증가했습니다. 이는 약 112.7% 증가

  • 오류(Error %): 두 경우 모두 0%

  • 표준 편차(STD.DEV): 최적화 전에는 921.10이었던 표준 편차가 최적화 후에는 255.11로 줄어들었습니다. 이는 약 72.3% 감소

User 1000명

로드밸런싱 적용 전

로드밸런싱 적용 후

비교 분석

  • 평균 응답 시간(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 채팅서비스 로드밸런싱에 관한 자료들이 부족해서 시행착오를 정말 많이 겪었습니다......

하지만 시행착오를 단지 시간의 소비로만 생각하지 않았고, 이를 통해서 다른 대안을 모색하고 배울 수 있는 기회로 삼고 시스템 성능에 관해 좀 더 고민함으로서 성장 할수있던 기회였던것 같습니다!!

profile
백엔드 공부중입니다!

0개의 댓글