|
| 1 | +# WebSocket Thread Pool 크기 계산 근거 |
| 2 | + |
| 3 | +## 1. 핵심 공식: Little's Law |
| 4 | + |
| 5 | +``` |
| 6 | +필요 스레드 수 = 동시 요청 수 × 요청당 처리 시간 |
| 7 | + = (요청 도착률) × (평균 응답 시간) |
| 8 | +``` |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## 2. 테스트 데이터 기반 계산 |
| 13 | + |
| 14 | +### 측정된 값 (k6-results.json 기준) |
| 15 | + |
| 16 | +| 지표 | 값 | 출처 | |
| 17 | +| ----------------------- | ---------- | ------------------------- | |
| 18 | +| 최대 VUs (동시 연결) | 400 | `vus_max.max = 400` | |
| 19 | +| WS 연결 시간 (avg) | 162ms | `ws_connecting.avg` | |
| 20 | +| WS 연결 시간 (p95) | 877ms | `ws_connecting.p(95)` | |
| 21 | +| WS 세션 지속 시간 (avg) | 1,609ms | `ws_session_duration.avg` | |
| 22 | +| 메시지 송신율 | 76.3 msg/s | `ws_msgs_sent.rate` | |
| 23 | +| 메시지 수신율 | 76.3 msg/s | `ws_msgs_received.rate` | |
| 24 | + |
| 25 | +### 계산: Outbound Thread Pool |
| 26 | + |
| 27 | +**시나리오**: 50개 방에서 5초마다 동시 메시지 브로드캐스팅 |
| 28 | + |
| 29 | +``` |
| 30 | +메시지 생성률 = 50개 방 / 5초 = 10 msg/s (서버 → 클라이언트) |
| 31 | +
|
| 32 | +각 메시지당 브로드캐스팅 대상 = 8명 (방당 플레이어) |
| 33 | +
|
| 34 | +실제 전송률 = 10 msg/s × 8명 = 80 msg/s |
| 35 | +``` |
| 36 | + |
| 37 | +**메시지 전송 처리 시간 추정**: |
| 38 | + |
| 39 | +- JSON 직렬화: ~1ms |
| 40 | +- 네트워크 I/O (로컬): ~2ms |
| 41 | +- 총 처리 시간: **~3ms/메시지** |
| 42 | + |
| 43 | +**필요 스레드 수 (Little's Law)**: |
| 44 | + |
| 45 | +``` |
| 46 | +필요 스레드 = 전송률 × 처리시간 |
| 47 | + = 80 msg/s × 0.003s |
| 48 | + = 0.24 스레드 (이론적 최소) |
| 49 | +``` |
| 50 | + |
| 51 | +**하지만 현실은 다름**: |
| 52 | + |
| 53 | +- 버스트 트래픽: 50개 방이 **동시에** 5초마다 전송 |
| 54 | +- 버스트 시 순간 전송률: 50개 방 × 8명 = **400 msg/순간** |
| 55 | + |
| 56 | +``` |
| 57 | +버스트 시 필요 스레드 = 400msg × 0.003s = 1.2 스레드 (이론) |
| 58 | +안전 계수 (p95 고려) = 1.2 × 3 = 3.6 스레드 |
| 59 | +여유분 (50%) = 3.6 × 1.5 ≈ 6 스레드 (최소) |
| 60 | +``` |
| 61 | + |
| 62 | +--- |
| 63 | + |
| 64 | +## 3. CPU 100% 발생 원인 분석 |
| 65 | + |
| 66 | +### 현재 설정 |
| 67 | + |
| 68 | +```java |
| 69 | +// WebSocketConfig.java |
| 70 | +configureClientOutboundChannel: |
| 71 | + corePoolSize = 2 |
| 72 | + maxPoolSize = 4 |
| 73 | +``` |
| 74 | + |
| 75 | +### 문제 발생 메커니즘 |
| 76 | + |
| 77 | +``` |
| 78 | +시간 T=0: 50개 방에서 동시에 메시지 전송 시작 |
| 79 | + → 400개 메시지가 Outbound Queue에 적재 |
| 80 | +
|
| 81 | +Queue 상태: |
| 82 | +┌─────────────────────────────────────────────┐ |
| 83 | +│ Outbound Queue: [msg1, msg2, ... msg400] │ |
| 84 | +└─────────────────────────────────────────────┘ |
| 85 | + ↓ |
| 86 | + 4개 스레드가 순차 처리 |
| 87 | + ↓ |
| 88 | +┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ |
| 89 | +│ T-1 │ │ T-2 │ │ T-3 │ │ T-4 │ ← 4개만 동시 처리 |
| 90 | +└─────┘ └─────┘ └─────┘ └─────┘ |
| 91 | +
|
| 92 | +처리 시간 계산: |
| 93 | +- 400 메시지 ÷ 4 스레드 = 100 메시지/스레드 |
| 94 | +- 100 메시지 × 3ms = 300ms (최소 처리 시간) |
| 95 | +- 실제로는 CPU 경쟁으로 2~3배 증가 = 600~900ms |
| 96 | +``` |
| 97 | + |
| 98 | +**CPU 100% 원인**: |
| 99 | + |
| 100 | +- 4개 스레드가 300ms 동안 **100% CPU 사용** |
| 101 | +- 스레드 간 문맥 교환 오버헤드 |
| 102 | +- GC 발생 가능성 |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## 4. 최적 Thread Pool 크기 도출 |
| 107 | + |
| 108 | +### 공식: Brian Goetz의 스레드 풀 크기 공식 |
| 109 | + |
| 110 | +``` |
| 111 | +최적 스레드 수 = CPU 코어 수 × (1 + 대기시간/처리시간) |
| 112 | +``` |
| 113 | + |
| 114 | +**EC2 프리티어 (1 vCPU) 기준**: |
| 115 | + |
| 116 | +``` |
| 117 | +I/O 대기 비율(W/C) = 네트워크 I/O 시간 / CPU 처리 시간 |
| 118 | + ≈ 2ms / 1ms = 2 |
| 119 | +
|
| 120 | +최적 스레드 수 = 1 × (1 + 2) = 3 스레드 (I/O 바운드 작업 기준) |
| 121 | +``` |
| 122 | + |
| 123 | +**하지만 버스트 트래픽 고려**: |
| 124 | + |
| 125 | +``` |
| 126 | +피크 처리량 = 400 msg/5s = 80 msg/s |
| 127 | +안전 계수 = 2배 (지연 시 누적 방지) |
| 128 | +권장 스레드 = 3 × 2 = 6 스레드 (최소) |
| 129 | +``` |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## 5. 권장 설정 (근거 포함) |
| 134 | + |
| 135 | +### WebSocketConfig.java 변경안 |
| 136 | + |
| 137 | +```java |
| 138 | +@Override |
| 139 | +public void configureClientOutboundChannel(ChannelRegistration registration) { |
| 140 | + registration.taskExecutor() |
| 141 | + .corePoolSize(8) // 이유: 6(최소) + 여유 2 |
| 142 | + .maxPoolSize(16) // 이유: 버스트 시 2배 확장 |
| 143 | + .queueCapacity(500) // 이유: 50방×8명×1.25 = 500 |
| 144 | + .keepAliveSeconds(60); |
| 145 | +} |
| 146 | + |
| 147 | +@Override |
| 148 | +public void configureClientInboundChannel(ChannelRegistration registration) { |
| 149 | + registration.interceptors(webSocketChannelInterceptor); |
| 150 | + registration.taskExecutor() |
| 151 | + .corePoolSize(4) // 이유: 연결/구독 처리 (상대적 저빈도) |
| 152 | + .maxPoolSize(8) // 이유: 동시 연결 버스트 대비 |
| 153 | + .keepAliveSeconds(60); |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +### 계산 근거 요약 |
| 158 | + |
| 159 | +| 설정 | 현재 | 권장 | 계산 근거 | |
| 160 | +| -------------- | ---- | ------- | ------------------------- | |
| 161 | +| Outbound Core | 2 | **8** | 6(Little's Law) + 2(여유) | |
| 162 | +| Outbound Max | 4 | **16** | Core의 2배 (버스트 대비) | |
| 163 | +| Outbound Queue | 기본 | **500** | 50방 × 8명 × 1.25(버퍼) | |
| 164 | +| Inbound Core | 2 | **4** | 연결 처리 2배 개선 | |
| 165 | +| Inbound Max | 4 | **8** | Core의 2배 | |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +## 6. 예상 효과 |
| 170 | + |
| 171 | +| 지표 | 현재 | 개선 후 예상 | |
| 172 | +| ------------------ | --------- | ------------ | |
| 173 | +| CPU 100% 도달 시점 | 200 VUs | 400+ VUs | |
| 174 | +| 메시지 처리 지연 | 600-900ms | 100-200ms | |
| 175 | +| 동시 처리 메시지 | 4개 | 16개 | |
| 176 | +| 처리량 | 76 msg/s | 300+ msg/s | |
0 commit comments