
1. 프로젝트 시작: "혼자서 10만 명을 감당할 수 있을까?"
채팅 서버를 개발하면서 가장 궁금했던 것은 "실제 프로덕션 환경에서 이 서버가 어디까지 버틸 수 있는가?" 였습니다. 단순히 10만 개의 WebSocket 연결을 유지(Idle)하는 것은 어렵지 않습니다. 하지만 10만 명이 연결된 상태에서 수천 명이 동시에 떠들기 시작한다면 이야기는 달라집니다.
이 프로젝트는 10만 동시 접속(Concurrent Users) 상황에서 초당 2,000건의 메시지(TPS)가 오갈 때, 사용자 경험을 해치지 않는지 검증하는 것을 목표로 했습니다.
결론부터 말씀드리면, 초기 테스트에서 27초라는 절망적인 지연시간(Latency)을 마주했고, 숱한 튜닝과 아키텍처 변경 끝에 12ms까지 단축하는 데 성공했습니다. 그 치열했던 삽질기와 인사이트를 공유합니다.
2. 테스트 환경 및 시나리오
실제와 가장 유사한 환경을 만들기 위해 Vultr 클라우드에 인프라를 구축했습니다.
🛠 인프라 구성 (Vultr Cloud, Seoul)
- SUT (System Under Test) 서버: 1대
- Spec: 16 vCPU / 58GB RAM (High Frequency)
- Stack: HAProxy (L4/L7) + Centrifugo (x3 Node) + KeyDB + API Server (Go)
- 부하 생성기 (Client): 2대
- Spec: 4 vCPU / 8GB RAM
- 역할: 각각 5만 개의 연결 생성 (총 10만 연결)
🧪 테스트 시나리오 (Mixed Load)
- 총 접속자: 100,000명
- 활성 사용자: 10% (10,000명)이 5초마다 메시지 전송
- 메시지 부하: 초당 약 2,000 TPS
- 데이터 크기: 단문(100B) 90% + 장문(2KB) 10% 혼합
3. 첫 번째 장벽: 84K의 벽 (커널 튜닝)
테스트를 시작하자마자 첫 번째 문제에 봉착했습니다. 클라이언트 연결이 84,000개 부근에서 더 이상 늘어나지 않고 에러를 뱉기 시작했습니다.
🚨 원인 분석
서버 리소스(CPU, RAM)는 널널했습니다. 로그를 분석해보니 문제는 '포트 고갈(Ephemeral Port Exhaustion)'이었습니다.
리눅스의 기본 임시 포트 범위는 약 28,000개입니다. Centrifugo 노드가 3개였으므로, 28,000 * 3 ≈ 84,000개에서 물리적인 한계가 온 것이었죠.
✅ 해결: sysctl 튜닝
호스트와 도커 컨테이너 레벨 모두에서 커널 파라미터를 수정했습니다.
# 임시 포트 범위 확장
net.ipv4.ip_local_port_range = 1024 65535
# 파일 디스크립터 및 연결 제한 해제
fs.file-max = 2097152
net.core.somaxconn = 65535
이 튜닝을 통해 가용 포트를 64,512개로 늘렸고, 3개 노드 합산 약 19만 개의 연결이 가능해지면서 10만 접속(100K) 달성에 성공했습니다.
4. 두 번째 장벽: p99 지연시간 27초의 악몽
연결은 성공했지만, 본격적인 부하 테스트(메시지 전송)를 시작하자 충격적인 결과가 나왔습니다.
메시지를 보냈는데 상대방이 받기까지 하위 1%의 경우 27초 이상 걸리는 현상이 발생했습니다. 채팅 서비스에서 27초 지연은 서비스 불가 수준입니다.
📉 단계별 최적화 시도 (Phase A ~ C)
처음에는 단순히 서버가 힘들어한다고 생각하여 CPU 사용량을 줄이는 데 집중했습니다.
- Phase A (설정 튜닝): Go GC 튜닝, KeyDB 메모리 최적화.
- Phase B (코드 최적화): 로직 개선, 불필요한 객체 생성 최소화.
- Phase C (프로토콜 변경): JSON 대신 Protobuf 도입, 로드밸런싱 알고리즘 변경(
leastconn).
결과: CPU 사용량은 1,800%에서 180%까지 90%나 감소했습니다. 서버는 아주 쾌적해졌죠.
하지만 여전히 p99 지연시간은 27초였습니다.
💡 깨달음: 문제는 CPU가 아니라 '구조'다
CPU가 놀고 있는데도 느리다면, 어딘가에서 Blocking이 발생하고 있다는 뜻입니다. 기존 아키텍처를 다시 살펴봤습니다.
[기존 아키텍처]
- Client가 API 서버로
POST요청 (메시지 전송) - API 서버가 DB 저장 후 Centrifugo로
Publish요청 - API 서버는 Centrifugo가 응답할 때까지 대기 (Blocking)
- Centrifugo 처리가 늦어지면 API 서버의 스레드(Go 루틴)가 묶임
- 요청 큐(Queue) 적체 발생 → 지연 시간 폭증
API 서버가 Centrifugo의 응답을 기다리는 동기식(Synchronous) 구조가 병목의 핵심이었습니다.
5. 최종 해결책: Centrifugo Proxy Mode 도입 (Phase D)
이 문제를 해결하기 위해 아키텍처를 완전히 뒤집었습니다. Centrifugo의 'Proxy Mode'를 도입했습니다.
🔄 변경된 아키텍처 (비동기 흐름)
이제 클라이언트는 API 서버를 거치지 않고, WebSocket을 통해 직접 메시지를 발행(Publish)합니다.
- Client → WebSocket → Centrifugo (메시지 수신)
- Centrifugo → (HTTP Request) → API Server (권한 검증)
- API Server → (5ms 이내 응답) → Centrifugo "OK, 전송해"
- Centrifugo → KeyDB(Pub/Sub) → 상대방 Client
핵심은 API 서버가 메시지 배달을 기다리지 않고, "보내도 된다"는 허락(검증)만 빠르게 하고 빠지는 것입니다. Outbound 트래픽 대기가 사라지니 큐 적체가 근본적으로 해결되었습니다.
6. 최종 결과: 27초 → 12ms
아키텍처 변경 후 다시 돌린 테스트 결과는 극적이었습니다.
(실제 그래프 이미지 삽입)
| 지표 | 목표값 | 튜닝 전 (Phase C) | 최종 결과 (Phase D) | 달성 여부 |
|---|---|---|---|---|
| 동시 접속자 | 100,000 | 100,000 | 100,000 | ✅ |
| TPS | > 1,500 | ~1,900 | 1,933 | ✅ |
| p99 지연시간 | < 100ms | 27,300ms | 12.4ms | ✅ |
| 메시지 수신률 | > 95% | 79.8% | 99.4% | ✅ |
- p99 Latency: 27.3초에서 0.012초로 약 2,300배 개선되었습니다.
- CPU 사용률: 피크 시 52% 수준으로 안정적.
- 에러율: 0% (메시지 유실 없음).
7. 한계 테스트: 어디까지 버틸까?
안정적인 운영을 위해 "서버가 터지는 지점"을 알아야 했습니다. Stress Test 모드로 TPS를 계속 올려보았습니다.
- ~11,000 TPS: p99 지연시간이 800ms 이하로 안정적. (운영 권장 구간)
- 13,000 TPS 초과: 지연시간이 1초를 넘어가며 에러 발생 시작.
결론적으로 현재 스펙(16 vCPU) 하나로 10만 명 접속 상황에서 초당 1만 건 이상의 메시지 처리가 가능하다는 것을 확인했습니다.
8. 마치며: 이번 프로젝트로 얻은 3가지 인사이트
이번 부하 테스트를 통해 얻은 기술적 교훈을 정리하며 글을 마칩니다.
1️⃣ 수직 확장은 '충분조건'이 아니라 '필요조건'이다
초기에 8 vCPU에서 테스트할 때는 CPU가 100%를 찍어서 원인을 파악할 수 없었습니다. 16 vCPU로 과감하게 스펙을 올린 후에야 리소스 부족이 아닌 아키텍처 병목이 보이기 시작했습니다. 성능 테스트 시에는 일단 하드웨어 스펙을 넉넉히 잡고 병목을 찾은 뒤, 다시 줄이는 방식이 효율적입니다.
2️⃣ 쉬운 것부터 하나씩 변인을 통제하라
처음부터 아키텍처를 뜯어고쳤다면 원인을 몰랐을 겁니다.커널 튜닝 → 코드 최적화 → 프로토콜 변경 순서로 하나씩 변수를 제거해 나갔기 때문에, 마지막에 남은 원인이 '구조적 문제'라는 것을 확신할 수 있었습니다.
3️⃣ 결국은 아키텍처 싸움이다
아무리 코드를 최적화하고(Phase B), 가벼운 프로토콜을 써도(Phase C), 구조적인 블로킹(Blocking I/O)이 있으면 성능 개선에는 한계가 있었습니다.
"코드를 빠르게 만드는 것보다, 느려질 구간을 없애는 설계"가 훨씬 강력한 성능 향상을 가져온다는 것을 뼈저리게 느꼈습니다.
'Claude Code' 카테고리의 다른 글
| [개발 회고] 주니어 개발자의 동접 1만 명을 위한 홈 화면 아키텍처: 1.9초를 1.5ms로 줄이기까지 (0) | 2026.01.06 |
|---|---|
| [개발 회고] 주니어 개발자가 Claude Code와 협업하며 깨달은 3가지 필승 전략 (1) | 2026.01.06 |