웹소켓 서버 수평 확장

hyng·2023년 2월 6일
1

smilegate-winter-dev-camp

목록 보기
14/15

현재 진행 중인 프로젝트에서 웹소켓 서버의 수평 확장을 구현하기 위해 찾아보고 결정하게 된 내용을 기록한다.

websocket 서버 수평 확장 어려움

https://nooptoday.com/why-websockets-are-hard-to-scale/

  • stateful

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

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

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

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

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

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

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

      서버 1이 1000개의 connection 가지고 있음.
      서버 2가 추가됨.
      서버 2가 추가되자마자 서버 1에 rebalancing algorithm이 동작.
      rebalacing algorithm은 대략 500개의 connection이 서버 2로 전달되어야 한다고 감지.
      서버 1이 500 클라이언트들에게 재연결 메시지를 보내고 클라이언트들은 서버 2로 연결됨.

    • 수많은 웹소켓 연결을 관리하는 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

가상 면접 사례로 배우는 대규모 시스템 설계 기초 책 참고

  • 수평적 규모 확장성을 달성하기 위해서는 요청 또는 데이터를 서버에 균등하게 나누는 것이 중요하다.
  • 해시 키 재배치 문제
    • n개의 캐시 서버가 있다고 할 때, 서버들에 부하를 균등하게 나누는 방법은 아래의 해시 함수를 적용하는 것이다.
    • serverIndex = hash(key) & N
    • 서버 풀이 고정되어 있을때 이 방법은 데이터를 균등하게 각 서버에 분배하지만, 서버가 추가되거나 기존 서버가 삭제되면 문제가 생긴다.
    • 4개의 서버에 균등하게 데이터가 분배된 상태에서 1번 서버가 죽게 되면 1번 서버에 보관되어 있는 키 뿐만 아니라 대부분의 키가 재분배되어 대부분 캐시 클라이언트가 데이터가 없는 엉뚱한 서버에 접속하게 된다. 그 결과 대규모 캐시 미스가 발생하게 될 것이다.
    • 이를 효과적으로 해결해 줄 기술이 안정해시이다.
  • 안정해시
    • 안정해시는 해시 테이블 크기가 조정될 때 평균적으로 k(키의 개수)/n(슬롯의 개수)개의 키만 재배치 되는 기술이다 (위키피디아)
    • 전통적 해시 테이블은 슬롯의 수가 바뀌면 거의 대부분 키를 재배치한다.
    • 자세한 동작 원리는 책 참고

load balancing algorithms

https://ably.com/topic/the-challenge-of-scaling-websockets

  • round-robin: 각각 서버들이 동일한 양의 트래픽을 처리. 서버가 두 개 있다고 할 때 첫 번째 요청이 서버 1에 가면 다음 요청은 서버 2 그리고 다음 요청은 다시 서버 1 이런식으로 처리함.
  • weighted round-robin: 각각 서버들이 성능에 따라 서로 다른 양의 트래픽을 처리.
  • least-connected: 각각의 서버들이 현재 얼마나 많은 커넥션을 맺고 있는지에 따라 다르게 처리.
  • least-loaded: 부하량에 따라 다르게 처리.
  • least response time: health monitoring request에 응답하는데 시간이 가장 적게 거리는 서버로 라우팅 됨.
  • hashing methods: 포트, 도메인 이름, ip 주소 같은 다양한 정보의 해시값을 통해 결정됨.
  • random two choices: 랜덤으로 두 개의 서버를 고르고 가장 적은 활성 커넥션 수를 가진 서버로 결정.
  • custom load: SNMP(Simple Network Management Protocol)을 이용해 개인 서버에 부하량를 쿼리하고 best load metrics를 가진 서버로 결정.
    cpu 사용률, 메모리, 응답시간 같은 다양한 metrics를 정의해야 한다.

gateway

  • blocking(zuul) vs non-blocking(spring gateway)
    blocking → 요청 수만큼의 스레드가 필요, 처리할 스레드가 없다면 큐에서 처리될 때까지 대기

  • non-blocking → 메인 스레드는 항상 요청을 처리할 수 있고 다른 여러 스레드가 백그라운드에서 비동기적으로 요청을 처리하기 때문에 blocking과 비교했을 때 동일한 양의 요청을 처리하는데 더 적은 리소스를 필요로한다.

  • zuul2는 non-blocking으로 동작하지만 spring clound가 zuul2 integration을 지원하지 않는다.


찾아본 내용을 토대로 websocket 서버 수평 확장을 위해서는 다음 부분들을 고려해야할 것으로 생각했다.
consistent hashing, 서버 다운시 해당 서버와 연결된 커넥션 모두 끊기, 새로운 서버 추가 시 다른 서버가 처리하는 커넥션을 일부 가지고 오기 위해 해당 커넥션들 끊기등을 구현해야 하는데 시간이 많이 부족할 것 같아 일단은 round-robin으로 구현하기로 결정하였다.


참고

가상 면접 사례로 배우는 대규모 시스템 설계 기초
https://nooptoday.com/why-websockets-are-hard-to-scale/
https://nooptoday.com/scalable-websockets-by-chatgpt/
https://www.zibtek.com/blog/websockets-and-load-balancers-how-to-use-them/amp/
https://www.springcloud.io/post/2022-03/load-balanced-websockets-with-spring-cloud-gateway/#gsc.tab=0
https://tsh.io/blog/how-to-scale-websocket/
https://www.devglan.com/spring-cloud/spring-cloud-gateway
https://www.devglan.com/spring-cloud/spring-cloud-gateway-websockets
https://www.devglan.com/spring-cloud/spring-cloud-netflix-eureka
https://ably.com/topic/the-challenge-of-scaling-websockets

profile
공부하고 알게 된 내용을 기록하는 블로그

0개의 댓글