Skip to content

Commit ce3434c

Browse files
authored
Merge pull request #194 from pirogramming/sihyun
[Fix] HikariCP 커넥션 풀 타임아웃 및 슬로우 쿼리 로그 설정 추가
2 parents 1d6e19f + 33db1e7 commit ce3434c

9 files changed

Lines changed: 976 additions & 0 deletions

File tree

‎backend/k6/config.js‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// 테스트 환경: http://localhost:8080 (dev) / https://api.piroin.com (prod)
2+
// 작성 기준: application.yml, SecurityConfig.java, *Controller.java
3+
// SSE 구현: SseEmitter (Spring WebMVC, Tomcat 스레드 점유 방식)
4+
// 대상 유저 수: 약 40명
5+
6+
import http from 'k6/http';
7+
import { check } from 'k6';
8+
import encoding from 'k6/encoding';
9+
10+
export const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
11+
export const SESSION_ID = __ENV.SESSION_ID || '1';
12+
13+
export const CREDENTIALS = {
14+
name: __ENV.USER_NAME || 'test_user',
15+
password: __ENV.USER_PASSWORD || 'test_password',
16+
};
17+
18+
export const AUTH = {
19+
type: 'bearer',
20+
token: __ENV.API_TOKEN || '',
21+
};
22+
23+
export function getHeaders(withAuth = true) {
24+
const headers = { 'Content-Type': 'application/json' };
25+
if (withAuth && AUTH.token) {
26+
headers['Authorization'] = `Bearer ${AUTH.token}`;
27+
}
28+
return headers;
29+
}
30+
31+
export function getHeadersWithToken(token) {
32+
return {
33+
'Content-Type': 'application/json',
34+
'Authorization': `Bearer ${token}`,
35+
};
36+
}
37+
38+
export function login() {
39+
const res = http.post(
40+
`${BASE_URL}/api/auth/login`,
41+
JSON.stringify({ name: CREDENTIALS.name, password: CREDENTIALS.password }),
42+
{ headers: { 'Content-Type': 'application/json' } }
43+
);
44+
check(res, { '로그인 성공': (r) => r.status === 200 });
45+
try {
46+
return res.json('token') || '';
47+
} catch {
48+
return '';
49+
}
50+
}
51+
52+
// 테스트용 1x1 JPEG (base64 인라인 — 외부 파일 불필요)
53+
const TEST_IMAGE_B64 =
54+
'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkS' +
55+
'Ew8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAAR' +
56+
'CAABAAEDASIAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAA' +
57+
'AAAAAAAAAAAAAP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAA' +
58+
'AAAAAAAA/9oADAMBAAIRAxEAPwCwAB//2Q==';
59+
60+
// QnA 이미지 업로드: POST /api/images (multipart/form-data)
61+
// Authorization 헤더만 포함 — Content-Type은 k6가 multipart로 자동 설정
62+
export function uploadTestImage(token) {
63+
const imageBytes = encoding.b64decode(TEST_IMAGE_B64, 'std', 'b');
64+
const data = {
65+
file: http.file(imageBytes, 'test.jpg', 'image/jpeg'),
66+
};
67+
return http.post(`${BASE_URL}/api/images`, data, {
68+
headers: { Authorization: `Bearer ${token}` },
69+
});
70+
}
71+
72+
// ⚠️ HikariCP pool-size=10, Tomcat threads.max=200
73+
// 40명 운영에는 pool-size=10 적절
74+
// Stress(120 VU) 시 DB 커넥션 병목 관측 예상 — 의도된 설정
75+
export const THRESHOLDS = {
76+
smoke: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<500'] },
77+
load: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<1000', 'p(99)<2000'] },
78+
stress: { http_req_failed: ['rate<0.05'], http_req_duration: ['p(95)<3000'] },
79+
soak: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<1000'] },
80+
sse: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<2000'] },
81+
};

‎backend/k6/k6-result.md‎

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# k6 부하 테스트 결과
2+
3+
> 테스트 일시: 2026-06-07
4+
> 대상 서버: https://api.piroin.com
5+
> 기준 유저 수: 40명
6+
> 계정: test / 12345678 (MEMBER), SESSION_ID=23 (IN_SESSION)
7+
8+
---
9+
10+
## 결과 요약
11+
12+
| 테스트 | 결과 | 에러율 | p(95) | 비고 |
13+
|--------|------|--------|-------|------|
14+
| Smoke | ✅ PASS | 0.00% | 123ms | 전 항목 정상 |
15+
| Load | ✅ PASS | 0.00% | 96ms | 40 VU 전 항목 정상 |
16+
| SSE | ⚠️ 부분 해결 | - | 110ms (TTFB) / 116ms (일반 API) | sse_connect_time 해결, sse_error k6 한계 |
17+
| Stress | ⚠️ FAIL | 0.00% | 614ms | 3s 초과 응답 300건 |
18+
| Soak | ⚠️ FAIL | 0.07% | 130ms | 간헐적 DB 오류 98건, 최대 응답 24분 |
19+
20+
---
21+
22+
## 1. Smoke Test ✅ PASS
23+
24+
| 항목 | 값 | 기준 |
25+
|------|-----|------|
26+
| 에러율 | 0.00% | < 1% |
27+
| p(95) | 123ms | < 500ms |
28+
| checks | 100% (280/280) | |
29+
| 총 요청 | 187건 | |
30+
31+
**체크 항목 전부 통과**
32+
- 헬스 체크, 커리큘럼, 세션 목록, 질문 목록, 출석, 이미지 업로드 모두 정상
33+
34+
---
35+
36+
## 2. Load Test ✅ PASS
37+
38+
| 항목 | 값 | 기준 |
39+
|------|-----|------|
40+
| 에러율 | 0.00% | < 1% |
41+
| p(95) | 96ms | < 1000ms |
42+
| p(99) | 115ms | < 2000ms |
43+
| 평균 응답 | 29ms | |
44+
| 최대 응답 | 3.41s | |
45+
| 총 요청 | 46,461건 | |
46+
| checks | 100% (47,981/47,981) | |
47+
48+
**40 VU 전체 구간에서 에러 0건.** 이미지 업로드(20% 확률) 포함 모두 정상.
49+
최대 응답 3.41s는 이미지 업로드 요청으로 추정.
50+
51+
---
52+
53+
## 3. SSE Test (재테스트)
54+
55+
| 항목 | 1차 | 재테스트 | 기준 | 판정 |
56+
|------|-----|----------|------|------|
57+
| `sse_connect_time` p(95) | 35003ms | **110ms** | < 2000ms | ✅ 해결 |
58+
| `sse_error` count | 510 | 596 | < 10 | ✗ |
59+
| `normal_api` p(95) | 86.9ms | 116.92ms | < 1000ms | ✅ |
60+
| SSE connected 이벤트 수신 | - | ✅ 100% | | |
61+
62+
**sse_connect_time 완전 해결**: TTFB 측정으로 전환 후 35003ms → 110ms.
63+
64+
**sse_error 596 원인 (k6 한계)**:
65+
timeout=10s 설정 시, k6가 연결을 강제 종료하는 시점에 body 버퍼링이 완료되지 않아
66+
`res.body`에 `connected` 문자열이 없는 경우 에러로 카운트됨.
67+
`SSE connected 이벤트 수신 ✓`와 모순되는 것처럼 보이지만,
68+
체크는 버퍼링 성공한 연결에서만 실행되기 때문. 서버 문제 아님.
69+
70+
**normal_api 4% 실패 (130건)**:
71+
SSE 30개 연결 중 일반 API 배치 요청 4% 실패. HikariCP 간헐적 병목으로 추정.
72+
p(95)=116ms로 대부분 정상이며 스레드 고갈은 아님.
73+
74+
**핵심 확인 사항 ✅**:
75+
- SSE 30개 연결 중에도 일반 API p(95)=116ms → **스레드 고갈 없음**
76+
- connected 이벤트 정상 수신 확인
77+
- `sse_error` 임계값은 k6의 body 버퍼링 한계로 인한 것 — 추가 도구로 검증 필요
78+
79+
---
80+
81+
## 4. Stress Test ⚠️ FAIL
82+
83+
| 항목 | 값 | 기준 |
84+
|------|-----|------|
85+
| 에러율 (`http_req_failed`) | **0.00%** | < 5% ✅ |
86+
| p(95) | 614ms | < 3000ms ✅ |
87+
| 평균 응답 | 191ms | |
88+
| 최대 응답 | 40.53s | |
89+
| 3s 초과 응답 | **300건** | 0건 기대 |
90+
| `custom_errors` | 300 | < 100 ✗ |
91+
| 총 요청 | 173,458건 | |
92+
93+
**에러율 0%, p(95) 614ms로 기준치는 통과했으나**, 3초를 초과한 응답이 300건 발생해 `custom_errors` 임계값(100건) 초과.
94+
최대 응답 40.53s는 120 VU 구간(한계 탐색)에서 HikariCP 커넥션 대기가 쌓인 것으로 추정.
95+
96+
**주요 발견**:
97+
- 40 VU(정상) → 80 VU(2배) 구간까지는 안정적
98+
- 120 VU(3배) 구간에서 일부 요청이 3s 초과 — **병목 지점 확인**
99+
- 회복 구간(마지막 5분)에서 에러율 0% 복귀 → 서버 자가 회복 능력 정상
100+
101+
**개선 방향**: HikariCP `maximum-pool-size` 상향 또는 쿼리 최적화로 커넥션 점유 시간 단축.
102+
103+
---
104+
105+
## 5. Soak Test ⚠️ FAIL
106+
107+
| 항목 | 값 | 기준 |
108+
|------|-----|------|
109+
| 에러율 (`http_req_failed`) | 0.07% | < 1% ✅ |
110+
| p(95) | 130ms | < 1000ms ✅ |
111+
| `db_error_count` | **98** | < 20 ✗ |
112+
| 최대 응답 | **24분 2초** | |
113+
| slow_response p(95) | 814s | |
114+
| 총 요청 | 84,593건 / 2시간 | |
115+
116+
**p(95) 130ms로 대부분 요청은 정상**이나, 간헐적으로 수 분~24분짜리 극단적 지연이 발생.
117+
118+
**실패 항목별 건수**
119+
- 커리큘럼 조회 실패: 15건
120+
- 1s 초과 응답: 71건
121+
- 출석 조회 실패: 18건
122+
- 질문 목록 실패: 23건
123+
- 이미지 업로드 실패: 4건
124+
125+
**분석**:
126+
- 2시간 중 99% 구간은 정상 동작 (p(95)=130ms)
127+
- 극단적 지연(max=24분)은 HikariCP 커넥션 대기가 누적되어 일부 요청이 timeout까지 대기한 것으로 추정
128+
- `db_error_count` 98건은 연속 실패가 아닌 간헐적 발생 → 서버가 완전히 멈추진 않음
129+
- 실제 40명 환경(20 VU)에서도 장시간 운영 시 간헐적 DB 병목 발생 확인
130+
131+
**개선 방향**:
132+
- 장시간 운영 중 커넥션 점유 시간이 긴 쿼리 식별 (슬로우 쿼리 로그 확인)
133+
- HikariCP `idle-timeout` 설정 검토
134+
135+
---
136+
137+
## 전체 총평
138+
139+
| 구분 | 내용 |
140+
|------|------|
141+
| **안전 운영 범위** | 40 VU (실제 40명 동시 사용) — 에러 0%, p(95) 96ms |
142+
| **병목 시작 지점** | 80~120 VU — 3s 초과 응답 발생, HikariCP 커넥션 대기 |
143+
| **SSE 영향도** | 없음 — SSE 30개 연결 중에도 일반 API p(95)=86ms 유지 |
144+
| **주요 개선 과제** | Soak 간헐적 DB 오류 원인 파악 (최대 응답 24분 발생) |

0 commit comments

Comments
 (0)