k6를 이용해, AuthServer를 사내에 도입하기 전에 얼마나 많은 부하를 감당할 수 있는지 확인한다.
- 대상 엔드포인트:
POST /user/login - 실제 AWS KMS 호출은 프로세스 내부 mock 서명기(
scripts/run_mock_kms.py)로 대체했다. KMS 네트워크 지연을 배제하고 서버/DB 성능만 순수하게 측정하기 위함이며, 반복 실행 시 비용/rate-limit 노이즈도 피할 수 있다. 테스트 전용이며, dev/live 환경에는 절대 사용하지 않는다. - 부하 패턴: VU를 100 → 300 → 600 → 1000 단계로 올리며 각 단계를 유지하는 계단식 램프(
k6/login_stress.js). 단일 VU 값에서 성공/실패만 보는 대신, 무너지는 지점을 정확히 찾기 위한 설계다.
아래 수치는 모두 mock KMS 서버를 대상으로 한 동일한 계단식 테스트 결과다.
| 설정 | req/s | 에러율 | 중간값 지연 | p95 지연 |
|---|---|---|---|---|
| worker 1개, 기본 풀 설정 | ~180-190 | 0% | 335-760ms | 5.8-7.0s |
| worker 1개, DB 풀 확대 (아래 참고) | ~180-190 | 0% | 거의 동일 | 거의 동일 |
| worker 4개, 워커당 풀 과대 설정(20/30) | 190 | 36.2% | 8.7ms (성공 건만) | 3.5s |
| worker 4개, 워커당 풀 적정 설정(10/10) | 379.5 | 0.87% | 14.5ms | 1.52s |
- 가설: DB 커넥션 풀이 작다.
main_app.py가asyncpg라이브러리 기본값(min_size=10, max_size=10)으로 raw 풀을 만들고 있었다. 50으로 늘려봤지만 효과가 없었다 — 알고 보니 이 풀은 어디서도 읽히지 않는 죽은 코드였다. 실제 ORM 쿼리는 별도의 SQLAlchemy 엔진(src/core/database.py)을 통해 나간다. - 가설: SQLAlchemy 풀이 작다. 기본값(
pool_size=5, max_overflow=10, 총 15개)을 50으로 늘렸지만 처리량에 변화가 없었다 — 이 가설도 배제됐다. - 진짜 원인: 단일 프로세스의 GIL 병목. worker가 1개일 때
htop에서 프로세스가 (24코어 중) 코어 1개의 약 72%에 고정되어 있었고, 풀 크기와 무관하게 처리량이 평평했다. FastAPI/asyncio의 동시성은 요청 처리 경로에서 I/O 대기 시간만 제거해줄 뿐, 각 요청의 CPU 연산 구간(Pydantic 검증, SQLAlchemy 쿼리 빌딩/결과 매핑, JSON 직렬화, 미들웨어 오버헤드)은 병렬화하지 못한다. 이 구간은 Python의 GIL 때문에 스레드 하나에서만 실행되며, 단일 프로세스의 처리량 상한을1 / (요청당 CPU 시간)≈ 188 req/s로 만든다 — 즉 로그인 요청당 약 5.3ms의 CPU 연산이 있다는 뜻이다. (참고: JWT 서명 자체는 이 병목의 일부가 아니었다 — 이미run_in_executor로 스레드풀에 위임되어 있었고, 내부 crypto 연산이 GIL을 놓아주기 때문이다.) - 멀티 프로세스 확장으로 확인. worker 프로세스 4개(
scripts/run_mock_kms.py <workers>,uvicorn.run("scripts.run_mock_kms:app", workers=N)사용)를 띄우자 각 프로세스가 자기만의 인터프리터/GIL을 가지게 되어, CPU 연산 구간이 코어별로 진짜 병렬 실행됐다. - 새로 드러난 병목: Postgres
max_connections. worker 4개 × 워커당 확대된 풀(20+30=50)이 최대 200개 커넥션을 요청했는데, Postgres 기본max_connections은 100이라 36.2%가 실패했다(타임아웃이 아니라 빠른 거부). 워커당 풀을pool_size=10, max_overflow=10(4워커 합계 80개, 100 미만)으로 줄이자 해결됐다 — 처리량은 약 2배(188 → 379.5 req/s)로 늘고 에러율은 0.87%로 떨어졌다.
src/core/database.py:echo=False,pool_size=10,max_overflow=10으로 변경 (기존:echo=True, 라이브러리 기본값5/10).scripts/run_mock_kms.py(신규, 테스트 전용):JwtLogic.initialize를 프로세스 내부 RSA 서명기로 대체하며, 멀티 프로세스 실행을 위한workers인자를 지원.k6/*.js,k6/users.json(신규): smoke, 로그인, 다중 사용자, 계단식 stress 테스트 스크립트.
- 튜닝된 4-worker 설정에서 실제로 감당 가능한 최대 처리량(목표 SLA 기준 p95)을 찾는다 — VU 1000에서는 아직
p95<300ms기준이 깨진다. - 튜닝된 설정을 실제 AWS KMS로 재검증해 운영 환경에 가까운 수치를 얻는다(mock은 실제 KMS 네트워크 지연을 제거한 상태였다).
- worker 개수(2/4/8)를 늘려가며 확장성 곡선을 그려서, 처리량이 어디까지 선형적으로 늘고 어디서(Postgres/Redis 같은 단일 공유 인스턴스) 다시 꺾이는지 확인한다.
- 짧은 stress test로는 못 보는 커넥션/메모리 누수를 잡기 위한 soak test(중간 부하로 10-20분 유지)를 진행한다.