제목, 태그, 카테고리로 검색

모든 글
약 24분 분량 프로젝트/위키엔진

분산 안정성 검증 — stress 테스트 + 한계점 분석

목차

이전 글

Redis 샤딩 — Consistent Hashing으로 워크로드 격리에서 KEYS 블로킹 안티패턴 제거, 3노드 Consistent Hashing, 블랙리스트 전용 인스턴스 격리를 완료했습니다. 이 글은 분산 아키텍처 전체를 stress 테스트로 검증하는 과정입니다.


이전 글 요약

핵심 수치
Redis L2 캐시 + Stateless 전환L1 73% + L2 9% = 82% 히트, Lettuce P95 2.5ms
MySQL ReplicationGTID 비동기, Primary 5 + Replica 15, Lag 0~1초
App 스케일아웃482ms → 37ms (92%↓), 에러 13.25% → 0%, CPU 100% → 50%
CDC — 이벤트 기반 동기화PostService dual-write 제거, OCP 준수
Redis 샤딩KEYS→SCAN, 3노드 분리, 워크로드 격리

Redis 샤딩 이후 부하 테스트 (100 VU, 20분):

지표결과
평균 응답시간42.8ms
P95190ms
에러율0.00%
처리량 (피크)~58 req/s

각 컴포넌트가 단독으로 검증된 상태다. 하지만 전체가 함께 높은 부하에서 동작했을 때의 한계는 확인하지 않았다.


1. 왜 지금인가

stress 테스트와의 차이

stress 테스트단일 서버의 한계점을 찾았다:

  • 단일 서버 (1 App + 1 MySQL + 0 Redis) → 200 VU, 25분 → CPU 100%, P95 1.4초
  • “스케일 아웃이 필요하다”는 근거 확보

이번 테스트는 분산 아키텍처의 한계점을 찾는다:

  • 분산 아키텍처 (2 App + MySQL Primary/Replica + Redis 3샤드 + Kafka + Debezium) → stress (한계점 재탐색)
  • “프로덕션에 올려도 되나?”에 대한 답

미뤄둔 soak 테스트

stress 테스트에서 명시적으로 미뤘던 soak 테스트(50 VU, 4시간)는 분산 인프라가 갖춰진 후에 의미가 있다. 단일 서버에서 50 VU × 4시간은 CPU 100%로 금방 폭사하지만, 분산 아키텍처에서는 CPU가 여유 있는 상태에서 시간이 지남에 따라 나타나는 문제를 찾는다.

soak 테스트로 찾는 “시간 기반” 문제들:

  • 메모리 누수: Heap 사용량이 GC 후에도 점진적 상승 → OOM
  • GC Pause 누적: G1 GC Mixed/Full GC 빈도가 시간에 따라 증가
  • DB 커넥션 풀 Drift: HikariCP Active 커넥션이 점진적 상승 → 풀 고갈
  • Redis 메모리 Drift: used_memory 점진적 상승 → eviction → 블랙리스트 키 유실 위험
  • Kafka Consumer Lag 축적: 초기엔 0이지만 시간에 따라 lag 증가
  • Replication Lag 축적: 부하 시 MySQL Replica가 점차 뒤처짐

현업 사례:

  • Netflix: soak 테스트에서 48시간 후 Zuul 프록시의 DirectByteBuffer 누수 발견 — 짧은 테스트에서는 GC가 처리했지만 장시간 시 OOM
  • LinkedIn: 4시간 soak에서 Kafka Consumer의 max.poll.records 설정 오류로 rebalancing 반복 발견
  • Stripe: soak 테스트에서 Ruby GC의 heap fragmentation이 시간에 비례해 증가하여 P99 레이턴시가 2시간 후 3배 악화

2. 테스트 환경

인프라 구성

현재 분산 아키텍처

서버역할스펙
서버 1App + DB Primary + Redis + NginxARM 2코어, 12GB
서버 2App + DB Replica + Redis Shard + KafkaARM 2코어, 12GB
서버 3Grafana + Loki + k6 + InfluxDBAMD 1코어, 6GB
서버 4Prometheus + Node ExporterAMD 1코어, 6GB

데이터 상태

항목
posts 테이블~1,215만 건
Lucene 인덱스~20GB (Primary + Replica)
Redis 자동완성 KV수만~수십만 키 (3 샤드 분산)
Redis 캐시 L2핫 게시글 수천 키 (3 샤드 분산)
Redis 토큰 블랙리스트수백~수천 키 (전용 인스턴스)

3. stress 테스트 계획

VU 수치 산출 근거

stress 테스트에서 ARM 2코어 단일 서버의 한계가 100~150 VU(CPU 100%)였다. App 스케일아웃에서 App 2대로 확장한 결과, 100 VU에서 CPU가 50%로 안정화되었다. 단순 선형 확장이면 200~300 VU가 한계이지만, Amdahl’s Law에 의해 공유 자원(MySQL Primary, Nginx, Redis) 경합이 발생하므로 선형 확장은 불가능하다.

프로필 (25분)

0~3분: 0 → 100 VU (워밍업)
3~8분: 100 VU 유지 (정상 부하 — Baseline 비교)
8~12분: 100 → 200 VU (과부하 진입)
12~22분: 200 VU 유지 (한계점 탐색)
22~25분: 200 → 0 VU (회복)

트래픽 비율

시나리오비율설명
검색30%한국어/영문 혼합 15개 키워드
자동완성25%12개 prefix
상세 조회20%핫 게시글 80% + 랜덤 20%
목록 조회15%페이지 0~9
쓰기5%게시글 생성
좋아요5%핫 게시글 대상

성공 기준

지표stress 테스트 (단일)목표 (분산)
최대 VU (에러율 < 1%)~100-150 VU200+ VU
P95 (100 VU)1,400ms< 200ms
에러율 (200 VU)13.25% (100 VU)< 1%

4. stress 테스트 결과

k6 콘솔 결과

k6 stress 콘솔 결과

시나리오평균P95
전체897.69ms1,910.67ms
검색 (전체)955.03ms1,985.55ms
검색 (희귀 10%)785.60ms1,861.00ms
검색 (중빈도 60%)940.54ms1,926.67ms
검색 (고빈도 30%)1,042.18ms1,999.72ms
자동완성693.41ms1,797.56ms
최신 게시글672.73ms1,714.21ms
상세 조회1,305.00ms2,299.64ms
쓰기 (생성+좋아요)519.76ms1,607.26ms
에러율0.09%
총 요청 수54,165

k6 Overview 대시보드

k6 Overview — 응답시간, 처리량, 에러율, VU

지표분석
평균 응답시간1.08s100 VU 구간(42.8ms) 대비 200 VU에서 급등
P952.20sRedis 샤딩 P95(190ms) 대비 11.5배 악화
P995.73s극단적 지연 — CPU 포화 시 대기열 급증
에러율0.105%목표(< 1%) 달성
피크 처리량109 req/s100 VU 구간에서 피크, 200 VU에서 오히려 하락

응답시간 추이:

  • 0~8분 (0→100 VU): 평균 ~50ms, P95 ~200ms — Baseline과 일치
  • 8~12분 (100→200 VU): P95가 분 단위로 급등 — CPU 포화 진입
  • 12~22분 (200 VU 유지): P95/P99가 초 단위까지 폭등 — 대기열 눈사태(queueing avalanche)
  • 22~25분 (200→0 VU): 부하 감소 후 ~2분 내 정상 복귀

5. 원인 분석 — App CPU 포화가 근본 병목

100 VU vs 200 VU 비교

지표100 VU 구간200 VU 구간악화율
평균~50ms897ms18배
P95~200ms1,911ms9.5배
P99~400ms5,730ms14배
처리량~60 req/s~25 req/s (하락)-58%
App CPU~50%80-100%포화

처리량이 오히려 하락한 것은 queueing avalanche — 앱이 요청을 처리하는 속도보다 새 요청이 들어오는 속도가 빨라, 대기열이 눈덩이처럼 커지는 현상이다.

소거법 — CPU가 병목인 근거

컴포넌트200 VU 상태병목?
App CPU80-100%, Load Average ~15 (2코어의 7.5배)YES
MySQLInnoDB 히트율 100%, Slow Query 0 (Primary), Row Lock 12ms 1회No
RedisOPS 50 ops/s (한계 10만), 메모리 0.6%, Eviction 0, Slowlog 0No
KafkaConsumer Lag 일시 3K → 0 수렴, CDC Lag ~40msNo (일시적)
NginxActive 200+, 자체 CPU 미미No (앱 대기)
네트워크TTFB 3.95s — 서버 응답 대기가 대부분No (CPU의 결과)
ReplicationLag 0~1초, IO/SQL Thread RunningNo

stress 테스트(단일 서버)와 동일한 결론: 병목은 App CPU (Lucene BM25 스코어링 + Nori 형태소 분석)이다. App 스케일아웃에서 App 2대로 확장하여 100 VU에서 CPU 50%로 안정화했지만, 200 VU에서 다시 CPU 포화에 도달한다.

200+ VU를 지원하려면

대안효과실현 가능성
App 3대로 확장CPU 4→6코어, ρ ≈ 0.67 (200 VU)OCI Free Tier 한도 초과
캐시 히트율 개선Origin 조회 감소 → CPU 절감L2 65% → 80%+ 가능 (TTL 조정)
상세 조회 캐시 강화가장 느린 시나리오(P95 2.3s) 개선postDetail L1/L2 히트율 개선 여지

6. Grafana 대시보드

시나리오별 응답시간

시나리오별 평균/P95 + 빈도별 비교

  • 200 VU 구간에서 모든 시나리오가 동시에 악화 — 특정 API 병목이 아닌 시스템 전체 CPU 포화
  • 상세 조회(P95 9.83s)가 가장 느림 — origin hit(Lucene + MySQL) 비율이 높기 때문
  • 희귀 토큰(10%) 검색이 고빈도(30%) 대비 빠름 — 고빈도 토큰은 매칭 문서가 많아 BM25 스코어링 비용이 큼

Spring Boot HTTP + JVM

HTTP 평균/P95/P99, 처리량, 에러율, JVM Heap/GC

  • Heap: 두 인스턴스 모두 ~200-300MB 사용, 1G 한도 내 안정
  • GC Pause: 100 VU에서 ~0.5ms, 200 VU 진입 시 6ms까지 상승 — G1 Evacuation Pause 증가

Tiered Cache + Lettuce

L1/L2/Origin 히트율, Lettuce P95/P99

  • Origin 도달률: 200 VU에서 10 ops/s까지 상승 — 캐시 미스 증가로 Lucene/MySQL 부하 집중
  • Lettuce P95: 100 VU에서 ~2ms → 200 VU에서 10ms까지 스파이크 (CPU 경합으로 이벤트 루프 지연)

HikariCP + CPU

HikariCP 커넥션 풀, 커넥션 획득 시간, CPU, 스레드 수

  • Replica 풀: Active 최대 14~15까지 상승(200 VU) — 읽기 부하 집중
  • 커넥션 획득 시간: 200 VU 진입 직후 200ms 스파이크 — CPU 포화의 증상
  • App CPU: 100 VU에서 ~40-60%, 200 VU에서 80-100%병목 확인

컨테이너별 리소스

컨테이너별 CPU, 메모리, 네트워크, 디스크 I/O

  • wiki-app-prod CPU: 피크 180% (2코어 기준)
  • wiki-mysql-prod CPU: 거의 0% — DB는 병목이 아님
  • 메모리: App ~1G, MySQL ~3.5G — 안정적, OOM 위험 없음

Redis 샤드별

샤드별 메모리, OPS, 키 수, 네트워크, 연결 수

  • OPS: 200 VU에서 50 ops/s — 여유 (Redis 한계 10만 ops/s)
  • 키 수: shard-1 916개, shard-2 436개, shard-3 527개
  • 연결 수: 각 샤드 3개

Redis 통합

메모리 사용률, L2 히트율, OPS, Eviction, Slowlog

  • 메모리 사용률: 0.597% — 극도로 여유
  • L2 캐시 히트율: 65.0% — stress 부하로 cold query 비율 증가
  • Eviction: 0, Slowlog: 0

Redis 네트워크 I/O, Uptime

Nginx

Nginx 상태, 커넥션, 요청 수

  • Active Connections: 200 VU에서 200+까지 상승
  • Waiting 커넥션: 200 VU에서 급증 — 앱 응답 대기 중인 커넥션

MySQL

MySQL QPS, 커넥션, InnoDB 버퍼 풀, Slow Query, Row Lock

  • InnoDB 버퍼 풀 히트율: Primary/Replica 모두 100% — DB I/O 병목 없음
  • Table Locks: 0

InnoDB Row Lock 대기시간, Row 연산별 처리량

MySQL Replication

Replication Lag, Thread 상태, Primary vs Replica 명령 비교

  • Replication Lag: 0~1초 진동 — 정상 범위
  • R/W 분리: Primary SELECT ~10 ops/s, Replica SELECT ~70 ops/s

Kafka + CDC

Kafka 메시지 수신/분, Consumer Lag

  • Consumer Lag: 100 VU에서 0, 200 VU에서 최대 ~3K까지 상승 → 이후 0으로 수렴
  • 피크 메시지: ~28 msg/s

Debezium

Debezium 연결 상태, CDC Lag, Events/sec

  • Connected: CONNECTED (전체 구간), Erroneous Events: 0
  • CDC Lag: 피크 ~40ms — 거의 실시간

네트워크 상세

HTTP 요청 단계별 소요시간, 네트워크 트래픽

  • TTFB: 3.95s — 200 VU 구간에서 서버 응답 대기가 대부분
  • 총 수신 데이터: 242 MiB

Host

호스트 CPU, 메모리, Swap, 디스크, Load Average

  • 서버 1 메모리: 63.3%, Swap: 12.2%
  • 서버 1 Load Average 1m: 피크 ~15 (2코어 기준 7.5배)

Kafka Topics

Kafka 토픽별 파티션 수


7. 성능 종합 비교

stress 테스트(단일) vs 분산

지표stress 테스트 (단일)stress (분산)개선
최대 VU (에러 < 1%)~100-150 VU~150-200 VU~1.3-2배
P95 (100 VU)1,400ms~200ms7배 개선
에러율 (200 VU)13.25% (100 VU에서)0.09% (200 VU에서)에러 거의 제거
처리량 피크~30 req/s~109 req/s3.6배
회복측정 안 함~2분 내 정상 복귀
병목App CPU (단일 2코어)App CPU (분산 4코어)동일 병목, 한계점 상승

100 VU 구간 비교 (정상 부하)

지표Redis 샤딩 (Baseline)stress 100 VU 구간
평균42.8ms~50ms
P95190ms~200ms
에러율0.00%0.00%

100 VU에서는 Baseline과 동일. SLA(P95 < 300ms) 충족.

핵심 결론

  1. 100 VU에서 P95 200ms — 정상 부하에서 SLA 충족
  2. 200 VU에서 에러율 0.09%stress 테스트(단일, 100 VU에서 13.25%)보다 압도적 개선
  3. App CPU가 여전히 근본 병목 — 3대 이상 확장 없이는 200 VU가 실질적 한계
  4. MySQL, Redis, Kafka, Nginx 모두 여유 — 앱 CPU만 해결하면 더 확장 가능

다음 글

카테고리 검색 필터링 + Facet 집계에서 Lucene Occur.FILTER 절로 카테고리 필터링을 구현하고, DB GROUP BY 간이 Facet으로 카테고리 분포를 제공합니다.


향후 계획

soak 테스트 (50 VU, 4시간)

검색 파이프라인 고도화(동의어 확장, 쿼리 이해, LTR 등) 완료 후 최종 아키텍처에서 실행 예정. 메모리 누수, GC Pause 누적, 커넥션 풀 drift, Redis 메모리 drift, Kafka Consumer Lag 축적을 30분 간격으로 관찰한다.

Chaos Engineering (장애 주입)

최종 아키텍처에서 8개 시나리오를 계획:

#시나리오기대 결과
1aApp graceful 중지 (SIGTERM)Nginx → 다른 App 전환, 에러 0
1bApp crash (SIGKILL)진행 중 요청 유실, 이후 전환
2Redis 샤드 crash해당 샤드만 캐시 미스
3토큰 BL Redis crash보수적 정책 (모든 토큰 거부)
4MySQL Replica crash읽기 → Primary 라우팅
5Kafka crashCDC 중단, fallback 동작
6Debezium crashCDC 중단, 복구 후 catch-up
7네트워크 지연 100ms응답시간 증가, 기능 정상
8Redis+Kafka 동시 crash캐시 미스 + CDC 중단, 서비스 지속

Previous Post

In Redis Sharding — Workload Isolation with Consistent Hashing, we eliminated the KEYS blocking anti-pattern, implemented 3-node Consistent Hashing, and isolated the blacklist to a dedicated instance. This post verifies the entire distributed architecture with a stress test.


Previous Posts Summary

PostKey Metrics
Redis L2 Cache + Stateless MigrationL1 73% + L2 9% = 82% hit, Lettuce P95 2.5ms
MySQL ReplicationGTID async, Primary 5 + Replica 15, Lag 0~1s
App Scale-Out482ms → 37ms (92% down), errors 13.25% → 0%, CPU 100% → 50%
CDC — Event-Driven SynchronizationPostService dual-write removed, OCP compliant
Redis ShardingKEYS→SCAN, 3-node separation, workload isolation

Load test after Redis Sharding (100 VU, 20 min):

MetricResult
Avg Response Time42.8ms
P95190ms
Error Rate0.00%
Throughput (peak)~58 req/s

Each component has been verified independently. However, the limits when everything operates together under high load had not been confirmed.


1. Why Now

Difference from the Stress Test

The stress test identified the single-server limit:

  • Single server (1 App + 1 MySQL + 0 Redis) → 200 VU, 25 min → CPU 100%, P95 1.4s
  • Established the rationale for “scale-out is needed”

This test identifies the distributed architecture’s limit:

  • Distributed architecture (2 Apps + MySQL Primary/Replica + Redis 3-shard + Kafka + Debezium) → stress (re-exploring the limit)
  • Answers “Is it production-ready?”

Deferred Soak Test

The soak test (50 VU, 4 hours) explicitly deferred in the stress test is only meaningful after the distributed infrastructure is in place. On a single server, 50 VU x 4 hours would quickly crash at CPU 100%, but with a distributed architecture, we look for problems that emerge over time while CPU has headroom.

“Time-based” problems that soak tests find:

  • Memory leaks: Heap usage gradually rises even after GC → OOM
  • GC Pause accumulation: G1 GC Mixed/Full GC frequency increases over time
  • DB Connection Pool Drift: HikariCP active connections gradually rise → pool exhaustion
  • Redis Memory Drift: used_memory gradually rises → eviction → risk of blacklist key loss
  • Kafka Consumer Lag accumulation: Initially 0 but lag increases over time
  • Replication Lag accumulation: MySQL Replica gradually falls behind under load

Industry examples:

  • Netflix: Found a DirectByteBuffer leak in Zuul proxy after 48 hours of soak testing — GC handled it in short tests but caused OOM over long durations
  • LinkedIn: Found repeated rebalancing due to misconfigured max.poll.records in Kafka Consumer during a 4-hour soak
  • Stripe: Soak test revealed Ruby GC heap fragmentation growing proportionally with time, causing P99 latency to triple after 2 hours

2. Test Environment

Infrastructure Configuration

Current distributed architecture

ServerRoleSpecs
Server 1App + DB Primary + Redis + NginxARM 2-core, 12GB
Server 2App + DB Replica + Redis Shard + KafkaARM 2-core, 12GB
Server 3Grafana + Loki + k6 + InfluxDBAMD 1-core, 6GB
Server 4Prometheus + Node ExporterAMD 1-core, 6GB

Data State

ItemValue
posts table~14.25M rows
Lucene index~20GB (Primary + Replica)
Redis autocomplete KVTens to hundreds of thousands of keys (distributed across 3 shards)
Redis cache L2Thousands of hot post keys (distributed across 3 shards)
Redis token blacklistHundreds to thousands of keys (dedicated instance)

3. Stress Test Plan

VU Target Rationale

In the stress test, the ARM 2-core single server hit its limit at 100-150 VU (CPU 100%). After App Scale-Out to 2 App instances, CPU stabilized at 50% under 100 VU. With simple linear scaling, the limit would be 200-300 VU, but Amdahl’s Law means contention on shared resources (MySQL Primary, Nginx, Redis) prevents linear scaling.

Profile (25 min)

0~3 min: 0 → 100 VU (warm-up)
3~8 min: 100 VU hold (normal load — Baseline comparison)
8~12 min: 100 → 200 VU (overload entry)
12~22 min: 200 VU hold (limit exploration)
22~25 min: 200 → 0 VU (recovery)

Traffic Distribution

ScenarioRatioDescription
Search30%Mixed Korean/English 15 keywords
Autocomplete25%12 prefixes
Detail View20%Hot posts 80% + random 20%
List View15%Pages 0~9
Write5%Post creation
Likes5%Targeting hot posts

Success Criteria

MetricStress Test (single)Target (distributed)
Max VU (error rate < 1%)~100-150 VU200+ VU
P95 (100 VU)1,400ms< 200ms
Error Rate (200 VU)13.25% (at 100 VU)< 1%

4. Stress Test Results

k6 Console Results

k6 stress console results

ScenarioAvgP95
Overall897.69ms1,910.67ms
Search (all)955.03ms1,985.55ms
Search (rare 10%)785.60ms1,861.00ms
Search (mid-frequency 60%)940.54ms1,926.67ms
Search (high-frequency 30%)1,042.18ms1,999.72ms
Autocomplete693.41ms1,797.56ms
Latest Posts672.73ms1,714.21ms
Detail View1,305.00ms2,299.64ms
Write (create + likes)519.76ms1,607.26ms
Error Rate0.09%
Total Requests54,165

k6 Overview Dashboard

k6 Overview — response time, throughput, error rate, VU

MetricValueAnalysis
Avg Response Time1.08sSpiked at 200 VU compared to 100 VU segment (42.8ms)
P952.20s11.5x worse than Redis Sharding P95 (190ms)
P995.73sExtreme latency — queue explosion during CPU saturation
Error Rate0.105%Target (< 1%) achieved
Peak Throughput109 req/sPeak during 100 VU segment, actually dropped at 200 VU

Response Time Trend:

  • 0~8 min (0→100 VU): Avg ~50ms, P95 ~200ms — matches Baseline
  • 8~12 min (100→200 VU): P95 surges by the minute — entering CPU saturation
  • 12~22 min (200 VU hold): P95/P99 explode to seconds — queueing avalanche
  • 22~25 min (200→0 VU): Returns to normal within ~2 min after load reduction

5. Root Cause Analysis — App CPU Saturation as the Fundamental Bottleneck

100 VU vs 200 VU Comparison

Metric100 VU Segment200 VU SegmentDegradation
Avg~50ms897ms18x
P95~200ms1,911ms9.5x
P99~400ms5,730ms14x
Throughput~60 req/s~25 req/s (drop)-58%
App CPU~50%80-100%Saturated

The throughput actually dropping is a queueing avalanche — new requests arrive faster than the app can process them, causing the queue to snowball.

Elimination Method — Evidence That CPU Is the Bottleneck

ComponentStatus at 200 VUBottleneck?
App CPU80-100%, Load Average ~15 (7.5x for 2 cores)YES
MySQLInnoDB hit rate 100%, Slow Query 0 (Primary), Row Lock 12ms x1No
RedisOPS 50 ops/s (limit 100K), memory 0.6%, Eviction 0, Slowlog 0No
KafkaConsumer Lag briefly 3K → converges to 0, CDC Lag ~40msNo (transient)
NginxActive 200+, negligible CPU usageNo (waiting on app)
NetworkTTFB 3.95s — mostly server response waitNo (consequence of CPU)
ReplicationLag 0~1s, IO/SQL Thread RunningNo

Same conclusion as the stress test (single server): The bottleneck is App CPU (Lucene BM25 scoring + Nori morphological analysis). App Scale-Out to 2 App instances stabilized CPU at 50% for 100 VU, but CPU saturation is reached again at 200 VU.

Supporting 200+ VU

AlternativeEffectFeasibility
Scale to 3 AppsCPU 4→6 cores, rho ≈ 0.67 (200 VU)Exceeds OCI Free Tier limit
Improve cache hit rateFewer origin lookups → CPU savingsL2 65% → 80%+ possible (TTL tuning)
Strengthen detail view cacheImprove the slowest scenario (P95 2.3s)Room for postDetail L1/L2 hit rate improvement

6. Grafana Dashboards

Per-Scenario Response Time

Per-scenario avg/P95 + frequency comparison

  • At 200 VU, all scenarios degrade simultaneously — not a specific API bottleneck but system-wide CPU saturation
  • Detail view (P95 9.83s) is the slowest — high origin hit rate (Lucene + MySQL)
  • Rare token (10%) search is faster than high-frequency (30%) — high-frequency tokens match more documents, increasing BM25 scoring cost

Spring Boot HTTP + JVM

HTTP avg/P95/P99, throughput, error rate, JVM Heap/GC

  • Heap: Both instances ~200-300MB usage, stable within 1G limit
  • GC Pause: ~0.5ms at 100 VU, rises to 6ms upon entering 200 VU — increased G1 Evacuation Pause

Tiered Cache + Lettuce

L1/L2/Origin hit rates, Lettuce P95/P99

  • Origin reach rate: rises to 10 ops/s at 200 VU — increased cache misses concentrate load on Lucene/MySQL
  • Lettuce P95: ~2ms at 100 VU → spikes to 10ms at 200 VU (event loop delay from CPU contention)

HikariCP + CPU

HikariCP connection pool, connection acquisition time, CPU, thread count

  • Replica pool: Active reaches max 14~15 (200 VU) — read load concentration
  • Connection acquisition time: 200ms spike right after entering 200 VU — symptom of CPU saturation
  • App CPU: ~40-60% at 100 VU, 80-100% at 200 VUbottleneck confirmed

Per-Container Resources

Per-container CPU, memory, network, disk I/O

  • wiki-app-prod CPU: Peak 180% (relative to 2 cores)
  • wiki-mysql-prod CPU: Nearly 0% — DB is not the bottleneck
  • Memory: App ~1G, MySQL ~3.5G — stable, no OOM risk

Redis Per-Shard

Per-shard memory, OPS, key count, network, connections

  • OPS: 50 ops/s at 200 VU — plenty of headroom (Redis limit 100K ops/s)
  • Key count: shard-1 916, shard-2 436, shard-3 527
  • Connections: 3 per shard

Redis Aggregate

Memory usage, L2 hit rate, OPS, Eviction, Slowlog

  • Memory usage: 0.597% — extremely spare
  • L2 cache hit rate: 65.0% — cold query ratio increased under stress load
  • Eviction: 0, Slowlog: 0

Redis network I/O, Uptime

Nginx

Nginx status, connections, request count

  • Active Connections: rises to 200+ at 200 VU
  • Waiting connections: surges at 200 VU — connections waiting for app response

MySQL

MySQL QPS, connections, InnoDB buffer pool, Slow Query, Row Lock

  • InnoDB buffer pool hit rate: 100% for both Primary/Replica — no DB I/O bottleneck
  • Table Locks: 0

InnoDB Row Lock wait time, per-operation Row throughput

MySQL Replication

Replication Lag, thread status, Primary vs Replica command comparison

  • Replication Lag: oscillates between 0~1s — within normal range
  • R/W split: Primary SELECT ~10 ops/s, Replica SELECT ~70 ops/s

Kafka + CDC

Kafka messages received/min, Consumer Lag

  • Consumer Lag: 0 at 100 VU, rises to max ~3K at 200 VU → then converges to 0
  • Peak messages: ~28 msg/s

Debezium

Debezium connection status, CDC Lag, Events/sec

  • Connected: CONNECTED (entire duration), Erroneous Events: 0
  • CDC Lag: Peak ~40ms — near real-time

Network Details

HTTP request phase-by-phase duration, network traffic

  • TTFB: 3.95s — server response wait dominates at 200 VU
  • Total received data: 242 MiB

Host

Host CPU, memory, Swap, disk, Load Average

  • Server 1 memory: 63.3%, Swap: 12.2%
  • Server 1 Load Average 1m: Peak ~15 (7.5x for 2 cores)

Kafka Topics

Kafka per-topic partition count


7. Performance Summary Comparison

Stress Test (Single) vs Distributed

MetricStress Test (single)Stress (distributed)Improvement
Max VU (error < 1%)~100-150 VU~150-200 VU~1.3-2x
P95 (100 VU)1,400ms~200ms7x improvement
Error Rate (200 VU)13.25% (at 100 VU)0.09% (at 200 VU)Errors nearly eliminated
Peak Throughput~30 req/s~109 req/s3.6x
RecoveryNot measuredNormal within ~2 min
BottleneckApp CPU (single 2-core)App CPU (distributed 4-core)Same bottleneck, higher limit

100 VU Segment Comparison (Normal Load)

MetricRedis Sharding (Baseline)Stress 100 VU Segment
Avg42.8ms~50ms
P95190ms~200ms
Error Rate0.00%0.00%

At 100 VU, identical to Baseline. SLA (P95 < 300ms) met.

Key Conclusions

  1. P95 200ms at 100 VU — SLA met under normal load
  2. 0.09% error rate at 200 VU — dramatically improved from stress test (single, 13.25% at 100 VU)
  3. App CPU remains the fundamental bottleneck — 200 VU is the practical limit without scaling beyond 3+ instances
  4. MySQL, Redis, Kafka, Nginx all have headroom — resolving App CPU alone enables further scaling

Future Plans

Soak Test (50 VU, 4 Hours)

Planned for the final architecture after search pipeline improvements (synonym expansion, query understanding, LTR, etc.) are complete. Will monitor memory leaks, GC Pause accumulation, connection pool drift, Redis memory drift, and Kafka Consumer Lag accumulation at 30-minute intervals.

Chaos Engineering (Fault Injection)

8 scenarios planned for the final architecture:

#ScenarioExpected Result
1aApp graceful stop (SIGTERM)Nginx → routes to other App, 0 errors
1bApp crash (SIGKILL)In-flight requests lost, then routes to other App
2Redis shard crashCache miss only for that shard
3Token BL Redis crashConservative policy (reject all tokens)
4MySQL Replica crashReads → routed to Primary
5Kafka crashCDC halted, fallback behavior
6Debezium crashCDC halted, catch-up after recovery
7Network delay 100msResponse time increase, functionality intact
8Redis + Kafka simultaneous crashCache miss + CDC halt, service continues

Author
작성자 @범수

오늘의 노력이 내일의 전문성을 만든다고 믿습니다.

댓글

댓글 수정/삭제는 GitHub Discussions에서 가능합니다.