WebSocket 메시지 처리량 최적화

강동현·2025년 4월 28일
0

Spring Websocket

목록 보기
9/9

이번에는 websocket을 통해서 오가는 메시지를 얼마나 빠르고 효율적으로 처리할 수 있는지에 초점을 맞춰서 공부해보려고 한다. 아무리 많은 연결을 수용할 수 있어도, 메시지 처리량이 낮다면 실시간 서비스의 진정한 가치를 제공하기 어렵다. 그렇다면 메시지 처리량을 극대화하기 위해서는 어떠한 기법들이 있는지 살펴보자.

메시지 압축 : 대역폭 절약과 리소스 사용량

네트워크를 통해 전송되는 데이터, 특히 텍스트 기반 JSON 같은 형식은 반복되는 패턴이 많아 압축 효율이 좋다. WebSocket에서는 permessage-deflate 라는 표준 압축 확장(RFC 7692)를 사용하여 메시지 페이로드를 압축함으로써 네트워크 대역폭 사용량을 크게 줄일 수 있다.

permessage-deflate 작동 원리

이 확장은 DEFLATE알고리즘을 기반으로 메시지를 압축하여, 사용 여부와 세부 설정은 핸드셰이크 과정에서 Sec-WebSocket-Extensions 헤더를 통해서 클라이언트와 서버 간에 협상이 된다.

  1. 클라이언트의 제안 : 클라이언트는 압축 사용 의사를 밝히며 관련 파라미터를 제안할 수 있다.
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=10
  1. 서버 응답 : 서버가 수락하면 응답 헤더에 permessage-deflate와 합의된 파라미터를 포함해서 회신한다. 서버가 이 헤더를 보내지 않으면 압축은 사용되지 않는다.
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=10

압축 파라미터 이해하기

협상 가능한 파라미터들은 압축 효율성과 리소스 사용량 사이의 균형을 조절하는데 중요하다.

  1. 컨텍스트 제어:

    • server_no_context_takeover : 서버가 메시지 간 압축 상태를 재사용하지 않도록 요청
    • client_no_context_takeover : 클라이언트가 메시지 간 압축 상태를 재사용하지 않도록 요청을 지시한다.
      -> 서버의 메모리는 절약하지만 압축률이 저하될 수 있다.
    • 컨텍스트 재사용 : 압축률이 높지만 메모리 사용량을 늘린다.
  2. 윈도우 비트:

    • server_max_window_bits : 서버가 사용할 압축 윈도우 크기 상한을 클라이언트가 제안한다. 값이 클수록 압축률이 좋지만, 서버 압축 메모리 및 클라이언트 압축 해제 메모리 요구량이 증가한다.
    • client_max_window_bits : 클라이언트가 사용할 최대 윈도우 크기를 서버에 알리거나, 서버가 클라이언트에게 제한을 지시한다. 서버 압축 해제 메모리 요구량을 줄이는 데 사용된다.
  3. 압축/메모리 레벨 :

    • 압축 레벨 : 압축률과 CPU 사용량 간의 트레이드오프(9가 최고 압축률/최고 CPU 사용)
    • 메모리 레벨 : 압축 상태 메모리 사용량과 압축률 간의 트레이드오프(9가 최고 압축률/최고 메모리 사용)

성능 트레이드오프 및 고려사항

  • CPU/메모리 vs 대역폭 : 압축은 명백히 CPU와 메모리 사용량을 증가시킨다. 네트워크 비용이 높거나 제한적인 환경에서는 유리할 수 있지만 CPU/메모리 자원이 부족하면 오히려 성능 저하를 일으킬 수 있다.
  • 작은 메시지 효율 : 컨텍스트 재사용 시 작은 메시지도 비교적 잘 압축되지만, 컨텍스트 재사용은 비활성화하면 효율이 떨어질 수 있다. 아주 작은 메시지는 압축에서 제외하는 옵션(ws의 whreshold)도 고려해볼 수 있다.

압축 외에도 메시지 자체를 처리하고 전송하는 방식을 최적화할 수 있다.

메시지 배치

여러 개의 작은 논리적인 메시지를 하나의 WebSocket 메시지로 묶어서 보내는 기법이다. 각 메시지 전송 시 발생하는 헤더 오버헤드(네트워크 패킷, WebSocket 프레임)을 줄일 수 있다.

  • 네트워크 오버헤드 감소, 전체 처리량이 향상될 수 있다.

작은 메시지를 매우 빈번하게 주고받는 경우에는 효과적일 수 있으나 실시간성이 매우 중요한 환경이면 지연 시간 증가가 오버헤드가 될 수 있다.

직렬화 형식 : JSON 만이 답은 아니다.

WebSocket으로 객체를 주고받으려면 바이트 스트림으로 직렬화하고 받은 이후에는 다시 객체로 역직렬화해야한다. 이 과정의 효율성이 성능에 영향을 미친다.

  • JSON :
    장점 : 가독성이 좋고 웹 표준이다
    단점 : 텍스트 기반이라 데이터가 크고 직렬화/역직렬화 속도도 상대적으로 느리다.

  • Protocol Buffers :
    장점 : 매우 효율적인 바이너리 형식, 고성능 직렬화, 강력한 타입 검사
    단점 : 가독성이 낮고 스키마 정의/컴파일 필요, 학습 곡선

  • MessagePack :
    장점 : JSON보다 효율적인 바이너리 형식, 빠른 직렬화/역직렬화, 동적 타이핑 지원
    단점 : 가독성 낮음, Protobuf만큼의 압축률 속도는 아닐 수 있음

Json 대신 Protobuf나 MessagePack 같은 바이너리 형식을 사용하면 페이로드 크기와 CPU 시간을 많이 절약할 수 있다. 이는 네트워크 대역폭 절약과 서버 처리 용량 증가로 이어진다.

Spring WebSocket 스레드

Spring에서 websocket을 사용하고 있다면 내부 메시지 처리 스레드 풀을 이해해야 된다.

주요 메시지 채널과 스레드 풀

메시지는 비동기적으로 여러 채널을 통해서 흐르며 각 채널은 스레드 풀에 의해 지원된다.

  • clientInboundChannel : 클라이언트 -> 서버 메시지 처리
  • clientOutboundChannel : 서버 -> 클라이언트 메시지 전송
  • brokerChannel : 애플리케이션 -> 메시지 브로커 메시지 전달

clientInboundChannel

  • CPU 바운드 작업 : 스레드 수를 cpu 코어 정도 수로 유지
  • I/O 바운드 작업(DB/외부 API) : 스레드 수를 늘리는 것을 고려할 순 있지만 근본적으로는 논블로킹 방식으로 개선하는게 좋음
  • queueCapacity 주의 : 큐 용량을 적절히 설정하지 않으면 코어 스레드가 꽉 찼을 때 새 스레드 대신 큐에 작업이 계속 쌓여 maxPoolSize 설정이 무의미해질 수 있다.

clientOutboundChannel

  • 이 채널의 성능은 클라이언트 네트워크 상태에 크게 좌우된다.
  • 느린 클라이언트 관리 : 느린 클라이언트가 서버 리소스를 고갈시키는 것을 막기 위해서 WebSocketTransportRegistration 설정이 필수다.
    • setSendTimeLimit(int timeLimit) : 메시지 전송 최대 시간 제한, 초과 시 연결 종료 시도
    • setSendBufferSizeLimit(int sizeLimit) : 세션당 최대 전송 버퍼 크기 제한, 초과 시 연결 종료 시도

마무리

지금까지 메시지 처리량에 대해서 최적화 하는 기법들에 대해서 알아보았는데 압축이라는 게 cpu 사용량이 많은 작업이기 때문에 각 환경에서 상황에 따라 적절히 압축을 사용해야될 것 같다.

profile
스스로에게 질문하고 답을 할 줄 아는 개발자

0개의 댓글