본문 바로가기

CS ﹒ Algorithm/Network Infra

Toss SLASH 22 - 토스 증권 실시간 시세 적용기 정리

금주의 사내 스터디 주제로 "Toss SLASH 22 - 토스 증권 실시간 시세 적용기"를 시청하게 되었다.

application부터 Infra까지 대용량 처리를 위한 여정을 22분이라는 짧은 영상에 알차게 담은 영상이라 굉장히 유익하기 때문에 관심이 있으면 직접 보도록 하자. (https://toss.im/slash-22/sessions/2-6)

 

지난번에 시청한 영상은 오직 카프카 하나만을 다루고 있었기 때문에 영상과 관련된 레퍼런스를 자세하게 조사할 수 있었지만, 이번에는 너무 광범위한 내용을 다루고 있어서.. 영상 내용 요약 및 부가적인 설명 정도로 글을 작성해볼까 한다.

 

코틀린은 언제 공부하나..

 

 

1. 주문 호가 및 시세를 전달하기 위한 WebSocket 채택

  (1) 왜 WebSocket을 채택했는가? 

 

주식 어플리케이션에서 가장 중요한 것은 종목에 대한 최신 시세와 주문 호가를 유저에게 전달하는 것이다.

처음 토스는 유명 미국 주식 플랫폼인 로빈후드 MTS를 벤치마킹하여 API Polling 방식을 채택하였으나, 출시 직전 타겟층이 전문 증권 투자자가 아닌, 일반 유저일지라도 최대한 정확한 정보를 빠르게 전달하는 것이 중요하다는 내부 논의가 있었고, 이에 따라 최종적으로 WebSocket 방식을 채택하게 되었다.

 

* 추가 설명

쉬운 내용이라 간단하게만 소개한다. 자세한 건 아래 양질의 자료를 링크해놨으므로 직접 읽자.

HTTP Polling : 클라이언트가 주기적으로 서버에 데이터를 요청하는 방식으로, 클라이언트가 서버에게 지속적인 요청을 보내는 단방향 통신.

SSE (Server Sent Event) : HTTP 프로토콜 기반 기술로, 서버와 클라이언트의 지속적인 연결을 유지하여 서버 부하가 상대적으로 적다. (handshake가 계속해서 발생하지 않기 때문에)

WebSocket : 양방향 통신을 지원하며 독자적인 프로토콜을 사용한다. SSE와 달리 데이터 포멧에 제약이 없다. 또한 처음 접속을 맺은 이후에는 더이상 헤더 정보를 보내지 않아 비용 측면에서 유리하다.

(참고 자료, https://www.itfind.or.kr/streamdocs/view/sd;streamdocsId=wE3Q0IySvmU__gkeMGXh_uFHIDrjw3ye-FnJ0vNoRlQ)

 

  (2) 왜 SSE가 아닌 WebSocket을 채택하였는가?

 

SSE는 단방향 통신만 지원하기 때문에, 추후 양방향 통신에 대한 기능 확대 니즈를 고려하여 WebSocket을 채택하였다.

그러나 막상 WebSocket을 채택하고보니 WebSocket의 방화벽 이슈로 인해 단방향 통신인 SSE를 선택하는 것도 괜찮을 것 같다는 것이 박성우 개발자님의 말씀이다.

 

* 추가 설명

WebSocket은 HTTP 프로토콜 위에 구현된 Protocol이므로 Enterprise 환경에서 WebSocket 연결 시에는 방화벽 설정 변경 혹은 WebSocket Proxy(NginX, Apache..)를 사용해야 한다.

반면, SSE는 애초에 HTTP 2.0을 기반으로 두고 있는 기술이므로 별도의 복잡한 설정이 필요하지 않음.

아주 세부적인 이유를 다루고 있는 참고 자료를 찾지 못했다. 다만 위의 참고 자료를 읽어보면 WebSocket은 첫 연결 이후 Header를 포함하지 않는다는 특징을 가지고 있는데, 이것과 관계가 있지 않을까?라고 유추해본다.. 누가 알려주삼.

이거랑은 다른 이야기지만 재미있는 포스팅이 있으므로 관심 있으면 읽어보삼.

(https://www.timeplus.com/post/websocket-vs-sse, WebSocket vs. Server-sent Events : A Perforance Comparison)

 

  (3) WebSocket을 어떻게 활용하였는가?

 

WebSocket 연결을 기본적으로 사용하되, 연결이 실패하거나 지연이 발생하면 Polling을 활용했다. (Fallback system)

또한, 양방향 통신의 이점을 살려 동일 종목을 보고 있는 동시 사용자 수를 노출하는 기능을 개발하였으나, A/B Test를 통해 이러한 시스템이 매매 전환에 도움이 되지 않는 결과가 도출되어 현재는 Fade out 되었다고 한다.

 

* 혹시나 A/B Test를 처음 들어본다면.. (https://www.oracle.com/kr/cx/marketing/what-is-ab-testing/)

 

 

2. 인프라 구조

 

 (1) 망 분리 요건을 고려한 인프라 구조

 

WebSocket Server를 DMZ에 배치하고, N개의 서버가 각각 고유한 호스트명을 가지도록 지정했다.

(이 때, N개의 서버에 대한 Host name을 지정한 이유는 scale out으로 인해 각 서버마다 다른 유저가 접속해 있고, 요청에 대해 해당 응답을 해당 서버에 전달해주기 위함)

또한, 로드 밸런싱을 위한 라우팅 서버를 구성하여 Server - Client간 연결 정보를 Redis를 통해 관리하였다.

 

첫 번째 이유는, 전자 금융 감독 규정의 망분리 원칙 (클라이언트가 증권사 내부 서버에 직접 접속하면 안됨) 때문이다.

두 번째 이유는, 쿠버네티스 같은 네트워크 홉을 줄이기 위함이라는데 이 부분은 이해하지 못했다. 누가 설명좀.

 

WebSocket은 Spring WebSocket과 Stomp Protocol 두 가지를 사용하였다고 한다.

 

* 추가 설명

Stomp protocol은 WebSocket을 기반으로 하지만 보다 추상화된 레이어에서 동작하는 message framework로 Pub/Sub 기능을 제공한다. 라고 어렵게 설명할 수 있지만 쉽게 설명하면 그냥 Message Broker 역할을 수행하고 있다.

쉬운 설명 : https://stackoverflow.com/questions/40988030/what-is-the-difference-between-websocket-and-stomp-protocols (요약 : STOMP와 WebSocket의 관계는 HTTP - TCP의 관계와 유사하다.)

 

 (2) 실제 처리 과정

 

- 종목별 시세 수신 과정

  > 클라이언트가 WebSocket으로 요청 시도, 실패 시 Polling API 호출

  > WebSocket 연결 성공 시 커넥션 정보를 라우팅 서버에 전달, 이후 로드밸런싱을 위한 커넥션 정보를 Redis에 저장

  > 클라이언트는 화면 노출 종목에 필요한 토픽을 구독

  > WebSocket에서 수신된 정보를 Kafka를 통해 전달 받음

 

 - 사용자별 자산 정보 갱신 과정

  > WebSocket 접속 시 이루어지는 Handshake에서 사용자 id 획득

  > WebSocket Server가 Routing Server에 커넥션 정보 전달하고, 이를 레디스에 저장

  > 자산 정보 변경 이벤트 발생 시, Routing Server에 해당 이벤트 전달

  > Routing Server는 사용자별 접속 정보를 기준으로 실제 사용자가 연결된 WebSocket에 정보를 포워딩

  > WebSocket은 해당 클라이언트에 정보 갱신 요청

 

  (3) Kafka Latency 최적화

 

Compression Type, Acks 두가지 설정으로 Kafka Latency를 최적화할 수 있었다고 한다.

Compression Type은 압축 알고리즘을 의미하며, 토스에서 테스트 결과 lz4가 producer, cunsumer 모두를 고려했을 때 더 좋은 성능을 보여줬다고 한다.

Acks의 경우 Leader에만 저장되면 응답하는 방식이, Leader와 Replica 모두 저장에 성공했을 때 응답하는 방식보다 1kb 메세지를 100만개 송수신하여 테스트했을 때 5.7배의 latency 차이를 보여줬다고 한다.

 

* 추가 설명

Acks ? Producer가 전송이 정상적으로 완료되었음을 판단하는 기준으로, 설정이 all인 경우 Leader, Replica에 모든 데이터가 저장된 것이 확인되었을 때 전송이 정상적으로 완료되었다고 판단하며, 0은 전송 즉시 성공한 것으로 판단한다, 1은 Leader Broker에 저장되었을 때 성공한 것으로 판단한다.

즉, 가용성과 내구성 사이에서 트레이드 오프를 결정하기 위한 설정이다.

(참고 자료, https://kafka.apache.org/documentation/)

 

Compression Type ? 단순히 압축 알고리즘이다. 우리가 컴퓨터에서 7zip으로 압축할거냐, rar로 압축할거냐 이런 거랑 별반 차이 없다. 마찬가지로 각 압축 방식마다 장단점이 있으므로 상황에 맞게 택하는 게.. 맞겠지만, 우선 ElasticSearch도 Toss와 같은 Lz4 압축 알고리즘을 사용한다고 한다.

(압축률이 제일 낮은 대신 빠른 속도를 보장하므로, 네트워크 대역폭에 많은 돈을 투자할 수 있으면 Lz4가 좋은 선택이 될 수 있는듯..)

https://developer.ibm.com/articles/benefits-compression-kafka-messaging/

 

  (4) 토스에서 겪은 런칭 이후 이슈

 

1. Load Balancing 방식에 다른 Connection 지연

주식 어플리케이션 특성 상, 장이 시작한 직후에 최대 트래픽이 발생하는 것이 일반적이다.

처음에는 Least Connection Algorithm을 사용했으나, 이로 인해 특정 서버에 다수의 커넥션이 몰리는 현상이 발생하여 Connection 지연이 발생했다고 한다.

결국 이후 Round Robin 방식과 Least Connection을 혼용하는 방식으로 이를 해결했다.

 

* 추가 설명

Least Connection : 현재 가장 Connection이 적은 서버에 클라이언트의 요청을 연결한다.

Round Robin : 기초적인 LoadBalancing Algorithm으로, 요청을 순차적으로 각 서버에 분배한다.

(아래는 부가적인 설명이 없고, 정확한 레퍼런스를 찾지 못해 내가 추측한 것이다.)

순간적으로 트래픽이 몰리면서 가장 연결이 적은 서버에 한 번에 다수의 클라이언트를 연결시켜버리기 때문에 문제가 발생한다. 이는 악순환으로 이어지는데, "실제로 연결이 성공한 클라이언트 수"를 기준으로 Connection을 판단하는 것이기 때문에 순간적으로 하나의 서버에 요청이 몰리며 클라이언트의 요청이 지연되면 해당 서버에 계속해서 클라이언트를 할당하려는 시도가 이어질 수 있..지 않을까? 정확한 정보를 알고 있는 분이 있다면 알려주십쇼.

 

2. WebSocket Connection Leak

WebSocket 서버의 재시작이 없는 경우 시간이 지나다보면 Connection이 점진적으로 증가하는 문제가 발생했다.

로컬 테스트 결과 Stomp는 문제가 없었으나, Abnormal Event 발생 시 WebSocket이 정상적으로 종료되지 않는 것을 확인하였다.

WebSocket Handler Decorator Factory를 이용해 afterConnectionClose 시 WebSocket의 onClose() 메서드를 실행되도록 하여 문제를 해결하였다고 한다.

 

딱히 더 설명할 건 없지만, 나도 WebSocket을 이용해 간단한 채팅 기능을 구현했을 때 비슷한 문제를 경험한 적이 있어 공감이 되었다.. 얼마나 WebSocket을 욕했었던지 ㅎ..

 

3. Outbound 트래픽 증가로 인한 서비스 지연

예상보다 빠른 성공으로 인해, 장 초반에 종목 상세 등 주요 화면 로딩 속도가 느려지고, 시세 연결에 실패하는 문제가 발생하였는데, 가장 큰 문제는 주식 서비스 뿐 아니라 전체 시스템에 걸쳐서 문제가 발생한 것이다.

원인은 놀랍게도 방화벽 및 보안 장비 처리량의 이슈였다고 한다.

서버 자체는 다른 서버와 따로 사용하지만, 보안 장비와 방화벽은 공유하고 있었던 것이 문제로 인터넷 회선을 분리하고 방화벽을 보다 높은 처리량을 가진 장비로 교체하여 문제를 해결했다.

 

4. Scale out의 어려움

현재 개선 중인 문제로, On-Premise 환경에서 서버를 운영하다보니 scale out에 한계가 있어서 aws cloud로 이전 중이라고 한다.

 

 

 

 

마무리

 

단순히 서버 인프라 뿐 아니라 인터넷 회선 같은 물리 계층의 문제까지 고려해야 한다는 것은 전혀 생각하지 못했는데, 어찌 보면 당연한 일이긴 하다..

또한, RoundRobin Algorithm 같이 단순한 알고리즘은 어쩐지 별로 좋지 않은 알고리즘처럼 느껴졌는데(..) 오히려 트래픽이 급증하는 상황에서는 CPU와 Memory를 절감하고, 순차적으로 Connection을 분배하는 방식이 오히려 효율적일 수 있다는 것이 흥미로웠다. 역시 The simple is best.. 아인슈타인좌는 늘 옳다.

여러모로 생각할 거리가 많았던 유익한 시간이였다.

 

- 끗 -