티스토리 뷰
이길저길의 실시간 양방향 위치 공유 시스템 설계 과정에 수행한 성능테스트 결과를 공유하고자 한다.
성능테스트 결과 뿐만 아니라 이전 글의 분석까지 종합하여 기술을 선정하였다.
시스템의 요구사항은 간략하게 다음과 같다:
- 각 클라이언트는 연결이 된 시점부터 3초에 한 번 자신의 위치를 공유한다.
- 위치 공유 대상은 사용자가 생성한 그룹의 그룹원들이며, 설계 단계의 예상 평균 그룹 인원수는 4명이다.
실시간 통신 기술들을 성능테스트 사전에 비교했던 흐름은 다음과 같다:
- SSE vs Short Polling
- SSE로 구현시 API를 총 2개를 구현하여 사용해야한다.
- SSE 커넥션 API
- 자신의 위치를 전송하여 그룹원들에게 알리는 API
- Short Polling 에 비해 커넥션을 하나 더 가지고 있게 되어 SSE는 비효율적이라 판단했다.
- SSE로 구현시 API를 총 2개를 구현하여 사용해야한다.
- Short Polling vs Long Polling
- Long Polling로 구현시 그룹원의 위치 공유시 연결을 끊게 되며 다음과 같은 상황이 발생한다.
- 그룹원 4명 기준으로 1.3초마다 연결을 끊게 되어 커넥션을 생성하고 끊는 주기가 매우 짧아진다.
- 클라이언트당 3초에 한 번 요청 * 4명 => (1/3)s * 4 = 약 1.3s
- 주기가 짧은 만큼 굳이 Long Polling으로 구현할만한 요구사항이 아니라고 보여졌다.
- 하지만, 예상과 다를 수 있기 때문에 성능테스트 결과를 확인하기로 했다.
- Long Polling로 구현시 그룹원의 위치 공유시 연결을 끊게 되며 다음과 같은 상황이 발생한다.
- Short Polling vs Websocket
- Short Polling 구현시 고려할점이 따로 있었다.
- Short Polling을 3초에 한 번 요청시 다른 기술들보다 실시간성이 떨어질 수 밖에 없었다.
- 타 기술들은 그룹원들이 각 다른 시점에 공유를 시작하기 때문에 평균 1.3s 마다 새로운 데이터를 받아보지만, Short Polling의 경우 API 요청시마다 데이터를 받을 수 밖에 없기 때문에 3초에 한 번 요청한다는 점에서 실시간성이 떨어진다.
- Short Polling의 주기를 3초에서 줄여보는 것을 고려는 했지만 Short Polling 특성상 HTTP 커넥션을 계속해서 만들었다 끊어야 하기에 CPU intensive 한 작업으로 예상 됐다.
- 굳이 Short Polling을 더 짧은 주기로 구현하기보다는 우선 3초로 설정해 성능테스트 결과를 확인하기로 했다.
- Short Polling 구현시 고려할점이 따로 있었다.
- Websocket vs STOMP
- Websocket과 STOMP의 구현적 특징을 제외한 기술적 차이점을 생각해봤다.
- STOMP의 경우 따로 Message Broker를 사용한다는 특징이 있다.
- Websocket의 경우 직접 해당하는 그룹원의 WebsocketSession을 찾아 공유된 데이터를 알려줘야한다.
- 여기서 Map과 같은 자료구조를 사용해야 한다.
- 파악한 부분으로는 명확하지 않기에 성능테스트 결과를 확인하기로 했다.
- Websocket과 STOMP의 구현적 특징을 제외한 기술적 차이점을 생각해봤다.
성능테스트로 다음 기술들을 비교 하고자 한다.
- Short Polling vs Long Polling
- Short Polling vs Websocket
- Websocket vs STOMP
성능 테스트 시나리오
성능테스트 툴은 WebSocket, STOMP 등의 테스트가 용이했기에 K6를 사용했다.
- 원활한 테스트를 위해 추상화한 부분
- 그룹의 개수를 성능 테스트의 VUS에 맞게 비율을 고려하여 정하였다. 이후 VUS 마다 랜덤하게 그룹에 들어가도록 했다.
- 평균 그룹의 그룹원 인원수는 요구사항을 따라가도록 하였다.
- 그룹 생성과 합류는 실제 서비스에서는 다른 CRUD API에서 수행하는데, 테스트를 간편히 하기 위해 이렇게 구성했다.
- 그룹의 개수를 성능 테스트의 VUS에 맞게 비율을 고려하여 정하였다. 이후 VUS 마다 랜덤하게 그룹에 들어가도록 했다.
- 테스트 흐름
- 공통
- 성능테스트 코드에서 유저는 랜덤한 그룹으로 설정된다.
- 이후 단계는 기술별로 아래 단계를 따른다.
- 성능테스트 코드에서 유저는 랜덤한 그룹으로 설정된다.
- Short Polling
- 3초에 한 번 자신의 위치를 전송하고 나머지 그룹원들의 위치를 조회해 받아온다.
- STOMP, WebSocket
- 커넥션을 생성한다.
- 3초에 한 번 자신의 위치를 모든 그룹원에게 공유하기 위해 요청한다.
- SSE, Long Polling
- 커넥션을 생성한다.
- 3초에 한 번 그룹원에게 자신의 위치를 알리는 API를 호출한다.
- 그룹원들의 위치 공유마다 데이터를 받으며, SSE의 경우 커넥션이 지속되고 Long Polling은 다시 커넥션 생성을 요청한다.
- 공통
- 요청 시나리오
- 1000, 5000, 10000 vus 가 있을 때를 가정하고 매 기술을 3 단계에 나누어 테스트하였다.
- 요청은 다음 stages를 따른다:
- 20초간 300명의 유저가 될때까지 늘려간다.
- 유저 및 요청이 점차 증가함
- 30초간 [1000, 5000, 10000] 를 순서대로 해당 유저수가 될때까지 늘려 실제 테스트하고자 하는 유저수를 테스트한다.
- 유저 및 요청이 급격히 증가함
- 10초간 유저를 0까지 점차 줄인다.
- 유저 및 요청이 줄어들게 하여 서버의 회복성을 확인한다.
- 20초간 300명의 유저가 될때까지 늘려간다.
성능테스트 결과 및 분석
성능테스트 결과는 1000, 5000도 어느정도 의미 있었지만 가장 결과 차이가 큰 10000으로 비교하기로 한다.
1. Short Polling vs Long Polling [10000 vus]
Short Polling 결과
data_received..............: 19 MB 237 kB/s
data_sent..................: 5.5 MB 67 kB/s
http_req_blocked...........: avg=64.85µs min=0s med=4µs max=162.66ms p(90)=185µs p(95)=288µs
http_req_connecting........: avg=36.13µs min=0s med=0s max=16.3ms p(90)=142µs p(95)=214µs
http_req_duration..........: avg=422.63ms min=392µs med=43.5ms max=3.6s p(90)=1.47s p(95)=1.94s
http_req_failed............: 100.00% ✓ 63081 ✗ 0
http_req_receiving.........: avg=504.35µs min=4µs med=28µs max=574.14ms p(90)=182µs p(95)=652µs
http_req_sending...........: avg=133.4µs min=1µs med=14µs max=183.77ms p(90)=64µs p(95)=134µs
http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...........: avg=421.99ms min=370µs med=42.99ms max=3.6s p(90)=1.46s p(95)=1.94s
http_reqs..................: 63081 770.071737/s
iteration_duration.........: avg=3.42s min=3s med=3.04s max=6.6s p(90)=4.47s p(95)=4.94s
iterations.................: 63081 770.071737/s
vus........................: 252 min=9 max=9999
vus_max....................: 10000 min=10000 max=10000
Long Polling 결과
data_received..................: 6.9 MB 78 kB/s
data_sent......................: 6.7 MB 75 kB/s
http_req_blocked...............: avg=75.59µs min=0s med=2µs max=99.86ms p(90)=257µs p(95)=366µs
http_req_connecting............: avg=56.52µs min=0s med=0s max=97.54ms p(90)=203µs p(95)=283µs
http_req_duration..............: avg=3.32s min=402µs med=3.08s max=13.36s p(90)=5.67s p(95)=8.06s
{ expected_response:true }...: avg=2.54s min=402µs med=2.4s max=12.65s p(90)=4.76s p(95)=5.37s
http_req_failed................: 33.03% ✓ 18949 ✗ 38419
http_req_receiving.............: avg=1.15ms min=3µs med=39µs max=2.05s p(90)=217µs p(95)=643.64µs
http_req_sending...............: avg=53.32µs min=1µs med=11µs max=64.86ms p(90)=52µs p(95)=89µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=3.32s min=367µs med=3.08s max=13.36s p(90)=5.67s p(95)=8.06s
http_reqs......................: 57368 643.211303/s
iteration_duration.............: avg=12.96s min=3.01s med=13.08s max=25.48s p(90)=17.07s p(95)=19.92s
iterations.....................: 19122 214.396293/s
vus............................: 1 min=1 max=10000
vus_max........................: 10000 min=10000 max=10000
분석
- iteration_duration(함수를 1회 실행 소요 시간), interations(함수 호출이 반복된 총 횟수)
- iteration_duration
- Short Polling -> p(95) = 4.94s, avg = 3.42s
- Long Polling -> p(95) = 19.92s, avg = 12.96s
- 3초보다 길어진 부분은 요청이 많아지며 처리가 지연된듯하다.
- iterations
- Short Polling = 63081
- Long Polling = 19122
- Short Polling의 처리량이 보다 높음을 알 수 있다.
- iteration_duration
- Heap Used
- Short Polling = 29.2 / Long Polling = 62.9
- Long Polling의 경우 DeferredResult를 통해 커넥션을 유지하기 때문에 힙 사용량이 보다 많다.
처리량이나 힙 사용량 등에서 Long Polling의 사용할 이유가 더더욱 없어졌다.
CPU 사용률도 비슷했지만 Long Polling이 오히려 CPU도 더 많이 사용했다.
2. Short Polling vs Websocket [10000 vus]
굳이 Short Polling 결과를 다시 복사 붙여넣기 하지는 않겠다.
Websocket 결과
data_received.........: 65 MB 717 kB/s
data_sent.............: 2.6 MB 28 kB/s
iteration_duration....: avg=18.16s min=3.03s med=14.36s max=58.08s p(90)=38.85s p(95)=44.15s
iterations............: 136 1.509431/s
vus...................: 3007 min=9 max=10000
vus_max...............: 10000 min=10000 max=10000
ws_connecting.........: avg=11.86s min=590.66µs med=3.78s max=43.12s p(90)=25.55s p(95)=25.77s
ws_msgs_received......: 1336749 14836.253751/s
ws_msgs_sent..........: 8705 96.614689/s
ws_session_duration...: avg=18.31s min=1.71ms med=14.66s max=54.13s p(90)=44.02s p(95)=48.3s
ws_sessions...........: 8771 97.347207/s
분석
- System CPU Usage / Process CPU Usage
- System CPU Usage
- Short Polling = 최대 0.970, 평균 0.193
- Websocket = 최대 0.318, 평균 0.121
- Process CPU Usage
- Short Polling = 최대 0.364, 평균 0.0343
- Websocket = 최대 0.0965, 평균 0.0384
- WebSocket이 CPU를 보다 적게 사용하면서 통신한다.
- System CPU Usage
- Heap Used
- Short Polling = 29.2%
- Websocket = 43.0%
- WebSocket Session을 계속 유지하며 지속적으로 Heap에 존재하게 되므로 커넥션을 유지하지 않는 ShortPolling 보다 WebSocket이 Heap 사용량이 더 많다.
Short Polling의 CPU 사용률이 눈에 띄게 매우 높았다. 하드웨어 자원을 더 많이 사용하면 해결될 수 있지만 CPU 자원은 메모리 자원보다 비용이 더 들어간다.
Heap Used가 더 높은 Websocket의 Heap 사용량이 43%인 것을 보면 무조건 메모리를 늘려야하는 수치는 아니라고 판단했다.
3. Websocket vs STOMP [10000 vus]
굳이 Websocket 결과를 다시 복사 붙여넣기 하지는 않겠다.
STOMP 결과
data_received.........: 823 MB 9.1 MB/s
data_sent.............: 3.7 MB 42 kB/s
iteration_duration....: avg=32.37s min=12.01s med=33s max=33.02s p(90)=33s p(95)=33s
iterations............: 878 9.754204/s
vus...................: 329 min=12 max=10000
vus_max...............: 10000 min=10000 max=10000
ws_connecting.........: avg=5.54s min=434.79µs med=354.49ms max=43.78s p(90)=30s p(95)=30s
ws_msgs_received......: 3409118 37873.839154/s
ws_msgs_sent..........: 24696 274.361
ws_session_duration...: avg=29.65s min=9.01s med=30s max=30.05s p(90)=30s p(95)=30s
ws_sessions...........: 9844 109.362619/s
분석
- ws_msgs_sent(전송된 Websocket 메시지수), ws_msgs_received(받은 Websocket 메시지수),
iterations(함수 호출이 반복된 총 횟수)- ws_msgs_sent
- Websocket = 8,705
- STOMP = 24,696
- ws_msgs_received
- Websocket = 1,336,749
- STOMP = 3,409,118
- iterations
- Websocket = 136
- STOMP = 878
- 처리량 관점에서 STOMP가 확실한 우위를 보였다.
- ws_msgs_sent
- ws_sessions (connection 연결 총 개수)
- Websocket = 8,771
- STOMP = 9,844
- 커넥션 수용 관점에서도 STOMP가 더 원활했다.
- Websocket의 경우 톰캣 NIO가 스레드를 할당한 이후 요청 처리가 끝날 때까지 스레드 하나가 할당된 채로 계속 물고 있지만, STOMP의 경우 요청과 응답에 비동기 I/O가 적용되어 훨씬 높은 처리량을 보였다고 판단했다.
처리량이나 커넥션을 수용하는 관점에서 STOMP가 Websocket에 비해 뛰어났다. 하드웨어 자원 사용률이 미세하게 STOMP가 더 높았으나, 처리량 차이가 컸기에 같은 처리량을 기준으로 한다면 더 효율적으로 사용하는 것이라 볼 수 있을 것 같다.
기술 선택 결과
테스트 진행 결과를 확인하며 실시간 통신 기술 중에 자원사용률과 처리량을 고려해 STOMP를 선택하게 되었다.
MQ를 사용해야 하기에 관리 포인트를 늘린다는 단점은 있지만, 그 만큼 Websocket보다 높은 처리량을 보이며 하드웨어 자원 사용률이 안정적이라 보여졌기에, trade-off를 통해 STOMP를 선택했다.
이길저길 서비스 요구사항에 맞게 시나리오를 구성하여 나온 결과이므로 다른 환경과 요구사항에서는 또 다른 결과가 충분히 나올 수 있다고 생각한다.
테스트 수행 스테이징 서버 코드 저장소:
https://github.com/ohksj77/realtime-communication
'프로젝트 탐구 > 이길저길' 카테고리의 다른 글
전략 패턴 기반 테스트 더블을 이용한 단위 테스트 (0) | 2024.03.25 |
---|---|
Resilience4j 적용과 모니터링까지 (0) | 2024.03.25 |
Redis Cache 적용과 만료 전략 수립 (0) | 2024.03.25 |
RabbitMQ 비동기 처리와 데드레터 처리 적용기 (0) | 2024.03.25 |
FULL Text Index 적용기 (0) | 2024.03.25 |