Mbtigram의 대화 화면은 1.5초마다 서버에 새 메시지를 물어봤음. 1000명이 방에 있으면 그것만 초당 667건. 롱폴링으로 바꿔서 요청을 20분의 1로 줄인 기록.
어디서 무너지는지부터 측정
동시 접속 1000명을 한 번에 부으면 "안 된다"는 결론만 남고 고칠 자리가 안 보임. 그래서 200 → 500 → 1000명으로 단계를 올리면서 어디서 무너지는지 봤음.
| 재는 곳 | 1000명일 때 |
|---|---|
| nginx · TLS까지 (공개 화면만) | 690 req/s · 중간값 33ms · 실패 0% |
| 앱 읽기, 워커 1개 | 274 req/s · 중간값 2.0초 · 연결 실패 17% |
| 앱 읽기, 워커 2개 | 566 req/s · 중간값 78ms · 실패 0% |
| 읽기 + 쓰기 5%, 워커 2개 | 291 req/s · 중간값 2.5초 |
인프라는 여유가 있었음. 읽기는 워커를 늘리면 두 배가 됐는데, 쓰기가 조금만 섞여도 워커 수가 소용없었음. 그때 데이터베이스가 SQLite였고, SQLite는 쓰기를 한 줄로 세움.
병목은 폴링
대화방에 들어가 있으면 화면이 1.5초마다 새 메시지가 있는지 물어봄. 1000명이 방에 있으면 그것만 초당 약 667건. 워커를 늘리거나 데이터베이스를 바꾸는 것보다 묻는 횟수를 줄이는 쪽이 훨씬 싸게 먹힌다고 봤음.
쉬운 것부터 했음. 조용한 방은 3초, 대화가 오가는 동안만 1.5초, 새 메시지가 오거나 내가 보내면 바로 다시 묻게 했음. 이것만으로 1000명 재측정 결과가 701 req/s · 중간값 22ms · 실패 0%가 됐음.
롱폴링
그다음이 롱폴링. 화면이 ?wait=25를 붙여서 물으면, 서버는 새 메시지가 없어도
바로 "없음"을 돌려주지 않고 답을 쥐고 기다림. 기다리는 사이 메시지가 생기면 바로
돌려주고, 25초 동안 아무 일도 없으면 빈 응답을 돌려줌.
- 요청 수: 30초에 20건에서 1건으로.
- 상대 메시지가 내 화면에 뜨기까지: 예전 1.5~3초에서 1.2초로. 덜 묻는데도 더 빨라졌음.
걸렸던 함정 세 가지
핸들러는 반드시 async여야 함. FastAPI의 보통 핸들러(def)는
스레드 40개짜리 풀에서 돎. 요청 하나가 25초를 붙잡으면 40명이 동시에 대화하는 순간 스레드가
바닥나서 서버 전체가 멈춤. 기다리는 동안은 async로 쉬고, 데이터베이스를 볼 때만
스레드를 잠깐 빌려 쓰게 했음.
기다리는 시간은 프록시 제한보다 넉넉히 짧아야 함. 앞단 nginx가 응답을 65초까지 기다리는데, 서버가 그보다 오래 쥐고 있으면 nginx가 먼저 끊어 버림. 그래서 25초로 잡았음.
옛 화면도 그대로 돌아야 함. wait을 안 붙이면 예전처럼 즉시 돌아오게 해서,
이미 설치된 앱의 옛 화면과 기존 테스트가 그대로 돎. 롱폴링이 지켜야 할 네 가지는 테스트로
따로 묶어 뒀음.
아직 남은 것
1만 명은 지금 구조로는 안 됨. 대략 초당 1900건이 필요한데 앱 한 대가 감당하는 건 300~570건. 순서는 정해 뒀음. 폴링을 푸시로, 데이터베이스를 PostgreSQL로, 그다음 서버를 여러 대로. 두 번째는 이미 옮겼고, 그 이야기는 따로 적어 뒀음.