Skip to content

Latest commit

 

History

History
43 lines (30 loc) · 4.9 KB

File metadata and controls

43 lines (30 loc) · 4.9 KB

부하 테스트 보고서: AuthServer 용량 분석

목적

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

원인 분석 과정

  1. 가설: DB 커넥션 풀이 작다. main_app.py가 asyncpg 라이브러리 기본값(min_size=10, max_size=10)으로 raw 풀을 만들고 있었다. 50으로 늘려봤지만 효과가 없었다 — 알고 보니 이 풀은 어디서도 읽히지 않는 죽은 코드였다. 실제 ORM 쿼리는 별도의 SQLAlchemy 엔진(src/core/database.py)을 통해 나간다.
  2. 가설: SQLAlchemy 풀이 작다. 기본값(pool_size=5, max_overflow=10, 총 15개)을 50으로 늘렸지만 처리량에 변화가 없었다 — 이 가설도 배제됐다.
  3. 진짜 원인: 단일 프로세스의 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을 놓아주기 때문이다.)
  4. 멀티 프로세스 확장으로 확인. worker 프로세스 4개(scripts/run_mock_kms.py <workers>, uvicorn.run("scripts.run_mock_kms:app", workers=N) 사용)를 띄우자 각 프로세스가 자기만의 인터프리터/GIL을 가지게 되어, CPU 연산 구간이 코어별로 진짜 병렬 실행됐다.
  5. 새로 드러난 병목: 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분 유지)를 진행한다.