이번에는 websocket을 통해서 오가는 메시지를 얼마나 빠르고 효율적으로 처리할 수 있는지에 초점을 맞춰서 공부해보려고 한다. 아무리 많은 연결을 수용할 수 있어도, 메시지 처리량이 낮다면 실시간 서비스의 진정한 가치를 제공하기 어렵다. 그렇다면 메시지 처리량을 극대화하기 위해서는 어떠한 기법들이 있는지 살펴보자.
네트워크를 통해 전송되는 데이터, 특히 텍스트 기반 JSON 같은 형식은 반복되는 패턴이 많아 압축 효율이 좋다. WebSocket에서는 permessage-deflate 라는 표준 압축 확장(RFC 7692)를 사용하여 메시지 페이로드를 압축함으로써 네트워크 대역폭 사용량을 크게 줄일 수 있다.
이 확장은 DEFLATE알고리즘을 기반으로 메시지를 압축하여, 사용 여부와 세부 설정은 핸드셰이크 과정에서 Sec-WebSocket-Extensions 헤더를 통해서 클라이언트와 서버 간에 협상이 된다.
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=10
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=10
협상 가능한 파라미터들은 압축 효율성과 리소스 사용량 사이의 균형을 조절하는데 중요하다.
컨텍스트 제어:
server_no_context_takeover
: 서버가 메시지 간 압축 상태를 재사용하지 않도록 요청client_no_context_takeover
: 클라이언트가 메시지 간 압축 상태를 재사용하지 않도록 요청을 지시한다.윈도우 비트:
server_max_window_bits
: 서버가 사용할 압축 윈도우 크기 상한을 클라이언트가 제안한다. 값이 클수록 압축률이 좋지만, 서버 압축 메모리 및 클라이언트 압축 해제 메모리 요구량이 증가한다.client_max_window_bits
: 클라이언트가 사용할 최대 윈도우 크기를 서버에 알리거나, 서버가 클라이언트에게 제한을 지시한다. 서버 압축 해제 메모리 요구량을 줄이는 데 사용된다.압축/메모리 레벨 :
압축 외에도 메시지 자체를 처리하고 전송하는 방식을 최적화할 수 있다.
여러 개의 작은 논리적인 메시지를 하나의 WebSocket 메시지로 묶어서 보내는 기법이다. 각 메시지 전송 시 발생하는 헤더 오버헤드(네트워크 패킷, WebSocket 프레임)을 줄일 수 있다.
작은 메시지를 매우 빈번하게 주고받는 경우에는 효과적일 수 있으나 실시간성이 매우 중요한 환경이면 지연 시간 증가가 오버헤드가 될 수 있다.
WebSocket으로 객체를 주고받으려면 바이트 스트림으로 직렬화하고 받은 이후에는 다시 객체로 역직렬화해야한다. 이 과정의 효율성이 성능에 영향을 미친다.
JSON :
장점 : 가독성이 좋고 웹 표준이다
단점 : 텍스트 기반이라 데이터가 크고 직렬화/역직렬화 속도도 상대적으로 느리다.
Protocol Buffers :
장점 : 매우 효율적인 바이너리 형식, 고성능 직렬화, 강력한 타입 검사
단점 : 가독성이 낮고 스키마 정의/컴파일 필요, 학습 곡선
MessagePack :
장점 : JSON보다 효율적인 바이너리 형식, 빠른 직렬화/역직렬화, 동적 타이핑 지원
단점 : 가독성 낮음, Protobuf만큼의 압축률 속도는 아닐 수 있음
Json 대신 Protobuf나 MessagePack 같은 바이너리 형식을 사용하면 페이로드 크기와 CPU 시간을 많이 절약할 수 있다. 이는 네트워크 대역폭 절약과 서버 처리 용량 증가로 이어진다.
Spring에서 websocket을 사용하고 있다면 내부 메시지 처리 스레드 풀을 이해해야 된다.
메시지는 비동기적으로 여러 채널을 통해서 흐르며 각 채널은 스레드 풀에 의해 지원된다.
clientInboundChannel
: 클라이언트 -> 서버 메시지 처리clientOutboundChannel
: 서버 -> 클라이언트 메시지 전송brokerChannel
: 애플리케이션 -> 메시지 브로커 메시지 전달WebSocketTransportRegistration
설정이 필수다.setSendTimeLimit(int timeLimit)
: 메시지 전송 최대 시간 제한, 초과 시 연결 종료 시도setSendBufferSizeLimit(int sizeLimit)
: 세션당 최대 전송 버퍼 크기 제한, 초과 시 연결 종료 시도지금까지 메시지 처리량에 대해서 최적화 하는 기법들에 대해서 알아보았는데 압축이라는 게 cpu 사용량이 많은 작업이기 때문에 각 환경에서 상황에 따라 적절히 압축을 사용해야될 것 같다.