[개발] Load Balance with Socket.io

Tamm
6 min readMar 11, 2016

이번 글은 지난 글에 이어 로드 밸런싱, 특히 WebSocket을 보다 쉽게 사용할 수 있게 만들어진 Socket.io를 로드 밸런싱 하는 방법에 대해 알아보려 한다.

발단

일반적으로 실시간 채팅, 혹은 실시간 알림 서비스를 구현하기 위해 Socket.io를 많이 사용하고 있을텐데 나도 비슷한 서비스를 개발해야해서 처음에는 2core 4GB 짜리 클라우드 인스턴스를 생성해서 이를 운영하고 있었다. 처음 테스트 시에는 모든 것이 잘 될것 같았지만 실제 서비스에 붙여보니 소켓 수가 어느정도 증가하니 신나게 에러를 내뿜고 있었다.

그래서 처음에는 아무 생각 없이 PM2에서 지원하는 클러스터 모드로 이를 해결하려 했지만.. 이게 웬걸? PM2 클러스터 모드는 Socket.io 1.x 버전과 궁합이 좋지 못하다.(즉 동작하지 않는다.) 그래서 어쩔 수 없이 앞 단에서 로드 밸런서를 사용해야 하는 일이 벌어졌다.

Nginx

처음에는 Nginx로 설정을 진행했다. 이전에 어느 글에서 Socket.io 로드 밸런싱을 지원하는 내용을 보기도 했고 Node.js와 Nginx의 궁합이 나쁘지 않았기 때문이다. Nginx로 Socket.io 로드 밸런싱을 하기 위해서는 아래와 같은 설정이 필요하다.

location /socket {
proxy_pass http://sockets;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

여기서 각각 중요한 것을 살펴보면 첫 번째로 proxy_http_version 프로퍼티를 볼 수 있다. HTTP/1.1 버전에서 지원하는 프로토콜 전환 메커니즘을 사용하기 위해서라고 한다. 두 번째로 proxy_set_header 부분이다. 여기서 Upgrade, Connection이 뜻하는건 hop-by-hop 헤더를 사용한다고 하는 것이다.

Nginx는 기본적으로 리버스 프록시를 지원하고 있는데 WebSocket을 지원하고자 하는 경우에는 클라이언트를 포워드 프록시 해야한다. 또한 클라이언트를 직접적으로 Socket.io 노드를 바라볼 수 있도록 프로토콜을 WebSocket으로 변경하고 헤더 정보를 온전히 전달할 수 있도록 설정을 변경하는 것이다.(제대로 이해한게 맞는지 모르겠다.)

위 설정을 마친다면 proxy_pass 부분에 정의된 노드들로 부하를 분산하게 된다.

upstream sockets {
ip_hash;
server localhost:8080 weight=3;
server localhost:8081;
server localhost:8082;
}

다만 개인적으로 로드 밸런서는 HAProxy를 주로 사용하고 있고(Nginx가 나쁘다는 것은 아니다.) 어차피 [HAProxy(전용 로드 밸런서) -> Nginx(부하 처리) -> Socket.io 노드들] 형태로 서버를 구성할 예정이었으므로 HAProxy 설정도 진행하였다.

HAProxy

HAProxy 역시 Nginx와 마찬가지로 설정이 매우 쉬운편이다. 다음 설정을 살펴보자.

frontend https_frontend
...
acl is_websocket path_beg /
acl is_websocket hdr(Upgrade) -i WebSocket
acl is_websocket hdr_beg(Host) -i ws
use_backend websockets if is_websocket

여기서 acl(Access Control List)은 특정 요청에 대해 데이터를 추출하거나 필터링, 혹은 헤더를 변경하는 등의 역할을 한다.

첫 번째 path_beg 부분은 요청 URI를 필터링 하는 부분이다. 지금 나같은 경우에는 전용으로 사용하기 때문에 /가 될 것이고 만약 Path를 지정했다면 /socket 같은 형태가 될 것이다. 두 번째 hdr(Upgrade) 부분은 해당 요청에 Upgrade 헤더가 포함되어 있다면 WebSocket 오퍼레이터를 지정한다는 내용이다.(-i는 대소문자를 구분하지 않겠다는 Flag) 세 번째 hdr_beg(Host)는 요청 Host가 ws로 시작할 경우를 뜻한다.

마지막 use_backend 부분은 만약 위에서 검사한 acl 조건이(is_websocket) 맞는다면 설정한 백엔드 부분으로(websockets) 전달한다는 내용이다.

websockets 백엔드는 다음과 같이 구성할 수 있을 것이다.

— 3월 19일 수정: 여러 서버를 사용하는 경우 balance 옵션을 leastconn으로 설정해서 Stickey Session을 사용하는 것이 여러모로 바람직해 보인다.

backend websockets
mode http
balance leastconn
cookie SERVERID insert indirect nocache
server s1 127.0.0.1:8080 cookie s1 check inter 5000 fastinter 1000 rise 1 fall 1 weight 1
server s2 another-ip:8080 cookie s2 check inter 5000 fastinter 1000 rise 1 fall 1 weight 1

여기까지 잘 구성했다면 별 문제없이 패킷이 오고가는 것을 확인할 수 있을 것이다.

현재 상태

매번 접속량이 변하다보니 서버 1대당 정확히 얼마나 많은 소켓을 유지할 수 있는지는 측정 중에 있지만 평균적으로 서버 2대로(Cent OS 6.5, TCP 튜닝 기준) 약 1500~2000개 정도 유지하는 것을 확인할 수 있었다.(2000개가 넘어가면 연결이 되지 않거나 지연된다.) 다만 뭔가 효율이 안나오는 것 같아서 조금 더 커널 쪽 세팅을 공부해 볼예정.

Reference

https://www.haproxy.com/doc/aloha/7.0/haproxy/acls.html

http://nodeqa.com/nodejs_ref/16 (한글)

네트워크는 어렵다.. 끝.

--

--