Skip to content

Commit 2f1f077

Browse files
authored
Merge pull request #152 from hamlsy/test/151
[Test/151] WebSocket 타이머 브로드캐스팅 확장성 검증 및 I/O 병목 테스트 및 방장 로직 수정
2 parents e0bb981 + dc190db commit 2f1f077

18 files changed

Lines changed: 2003 additions & 1108 deletions

File tree

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ dependencies {
6161
implementation "com.vladsch.flexmark:flexmark-all:0.64.8"
6262
implementation "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1"
6363

64+
6465
//flyway
6566
// implementation 'org.flywaydb:flyway-core'
6667
// runtimeOnly 'mysql:mysql-connector-j'
@@ -69,6 +70,7 @@ dependencies {
6970
testCompileOnly 'org.projectlombok:lombok'
7071
runtimeOnly 'com.mysql:mysql-connector-j'
7172
annotationProcessor 'org.projectlombok:lombok'
73+
testAnnotationProcessor 'org.projectlombok:lombok'
7274
testImplementation 'org.springframework.boot:spring-boot-starter-test'
7375
testImplementation 'org.springframework.security:spring-security-test'
7476
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

cpu-usage-log-v3-improved.csv

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"Time","CPU"
2+
"15:06:10","28.89"
3+
"15:06:16","18.22"
4+
"15:06:22","11.25"
5+
"15:06:28","23.38"
6+
"15:06:34","80.4"
7+
"15:06:40","41.69"
8+
"15:06:46","29.34"
9+
"15:06:52","20.54"
10+
"15:06:58","96.93"
11+
"15:07:04","23.93"
12+
"15:07:10","44.68"
13+
"15:07:16","34.8"
14+
"15:07:22","17.5"
15+
"15:07:28","91.83"
16+
"15:07:34","59.85"
17+
"15:07:40","41.29"
18+
"15:07:46","67.37"
19+
"15:07:52","42.1"
20+
"15:07:58","63.95"
21+
"15:08:04","41.33"
22+
"15:08:10","49.32"
23+
"15:08:16","33.57"
24+
"15:08:22","33.68"
25+
"15:08:28","36.18"
26+
"15:08:34","40.66"
27+
"15:08:40","25.44"
28+
"15:08:46","21.93"
29+
"15:08:52","19.34"
30+
"15:08:58","32.07"
31+
"15:09:04","19.62"
32+
"15:09:10","5.54"
33+
"15:09:16","25.1"
34+
"15:09:22","14.72"
35+
"15:09:28","21.79"
36+
"15:09:34","14.26"
37+
"15:09:40","14.93"
38+
"15:09:46","31.18"
39+
"15:09:52","7.58"
40+
"15:09:58","31.97"
41+
"15:10:04","17.65"
42+
"15:10:10","23.33"
43+
"15:10:16","15.61"
44+
"15:10:22","66.75"
45+
"15:10:28","56.6"
46+
"15:10:35","77.68"
47+
"15:10:41","59.39"
48+
"15:10:47","72.98"
49+
"15:10:53","60.13"
50+
"15:10:59","59.62"
51+
"15:11:05","65.96"
52+
"15:11:11","54.3"
53+
"15:11:17","54.95"
54+
"15:11:23","49.08"
55+
"15:11:29","34.02"
56+
"15:11:35","29.51"
57+
"15:11:41","31.47"
58+
"15:11:47","27.31"
59+
"15:11:53","18.88"
60+
"15:11:59","5.72"
61+
"15:12:05","28.11"
62+
"15:12:11","18.76"
63+
"15:12:17","34.52"
64+
"15:12:23","23.62"
65+
"15:12:29","40.52"
66+
"15:12:35","12.98"
67+
"15:12:41","39.93"
68+
"15:12:47","71.56"
69+
"15:12:53","25.37"
70+
"15:12:59","27.06"
71+
"15:13:05","16.48"
72+
"15:13:11","25.83"
73+
"15:13:17","29.59"
74+
"15:13:23","40.07"
75+
"15:13:29","34.53"
76+
"15:13:35","22.92"
77+
"15:13:41","17.54"
78+
"15:13:47","24.35"
79+
"15:13:53","17.02"
80+
"15:13:59","4.92"
81+
"15:14:05","16.84"

cpu-usage-log-v4-refactored.csv

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"Time","CPU"
2+
"17:56:07","38.38"
3+
"17:56:13","62.35"
4+
"17:56:19","51.62"
5+
"17:56:25","35.74"
6+
"17:56:31","44.76"
7+
"17:56:37","60.96"
8+
"17:56:43","39.05"
9+
"17:56:49","41.01"
10+
"17:56:55","33.38"
11+
"17:57:01","40.25"
12+
"17:57:07","49.7"
13+
"17:57:13","34.12"
14+
"17:57:19","39.91"
15+
"17:57:25","38.39"
16+
"17:57:31","44.29"
17+
"17:57:37","45.29"
18+
"17:57:43","51.23"
19+
"17:57:49","35.83"
20+
"17:57:56","42.33"
21+
"17:58:02","52.11"
22+
"17:58:08","41.32"
23+
"17:58:14","43.95"
24+
"17:58:20","75.8"
25+
"17:58:26","58.85"
26+
"17:58:32","37.27"
27+
"17:58:38","39.73"
28+
"17:58:44","31.77"
29+
"17:58:50","57.93"
30+
"17:58:56","59.76"
31+
"17:59:02","60.86"
32+
"17:59:08","38.43"
33+
"17:59:14","23.05"
34+
"17:59:20","29.23"
35+
"17:59:26","31.23"
36+
"17:59:32","36.51"
37+
"17:59:38","26.25"
38+
"17:59:44","41.29"
39+
"17:59:50","39.82"
40+
"17:59:56","56.89"
41+
"18:00:02","56.8"
42+
"18:00:08","34.92"
43+
"18:00:14","31.18"
44+
"18:00:20","49.63"
45+
"18:00:26","39.31"
46+
"18:00:32","43.82"
47+
"18:00:38","42.43"
48+
"18:00:44","32.2"
49+
"18:00:50","57.7"
50+
"18:00:56","77.55"
51+
"18:01:02","97.31"
52+
"18:01:09","77.43"
53+
"18:01:15","38.87"
54+
"18:01:21","53.48"
55+
"18:01:27","42.52"
56+
"18:01:33","40.91"
57+
"18:01:39","44.7"
58+
"18:01:45","26.54"
59+
"18:01:51","32.76"
60+
"18:01:57","39.02"
61+
"18:02:03","36.89"
62+
"18:02:09","67.29"
63+
"18:02:15","42.59"
64+
"18:02:21","38.7"
65+
"18:02:27","32.77"
66+
"18:02:33","38.15"
67+
"18:02:39","51.94"
68+
"18:02:45","49.38"
69+
"18:02:51","20.36"
70+
"18:02:57","18.79"
71+
"18:03:03","32.21"
72+
"18:03:09","24.65"
73+
"18:03:15","15.53"
74+
"18:03:21","24.58"
75+
"18:03:27","21.5"
76+
"18:03:33","24.81"
77+
"18:03:39","17.45"
78+
"18:03:45","20.87"
79+
"18:03:51","32.51"
80+
"18:03:57","25.89"
81+
"18:04:03","17.59"
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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

Comments
 (0)