Capacity Bounds
instance를 몇 대로 굴릴지는 계산의 결과다. 하한은 수요가, 상한은 공유 자원이 정하는데, 두 값이 겹치지 않을 때가 진짜 문제다.
Intro
instance를 몇 대로 굴릴지 정하는 자리에서 나오는 근거는 대체로 셋 중 하나다.
전에 쓰던 수, 어림짐작, 장애 한 번 겪고 늘린 수.
미리 말하지만 우리는 instance를 몇 대로 해야할지 분명히 계산할 수 있다.
하한과 상한을 정하면 된다.
“트래픽을 감당하려면 최소 몇 대는 있어야 하고”, 의 최소가 하한이다.
반대편에는 상한이 있다. instance마다 DB connection이든 GPU든 유한한 자원을 쓰니 무한정 늘릴 수 없다.
운영 가능 구간은 다.
하한은 여러 요구 중 가장 빡센 값이다. 상한은 반대로 가장 낮은 천장이 결정한다.
Little’s Law
계산의 출발점은 대기행렬 이론의 가장 단순한 공식이다.
시스템 안에 평균적으로 머무는 요청 수는 도착률과 체류시간의 곱이다. 초당 600 요청이 들어오고 요청 하나가 80ms를 머문다면, 어느 순간을 잘라도 시스템 안에는 48개 요청이 떠 있다.
이 법칙이 강력한 이유는 어느 계층에 적용해도 성립하기 때문이다. 시스템 전체에도 되고 그 안의 특정 구간에만 잘라서 적용해도 된다.
같은 트래픽인데 계층별 동시성이 10배 차이 난다. 요청이 80ms를 사는 동안 DB를 실제로 붙잡고 있는 건 query 2회 합쳐 8ms뿐이기 때문이다.
계층마다 동시성이 다르다는 데서 pool 설정의 전제 하나가 무너진다.
“thread를 200개 띄웠으니 connection도 200개”라는 설정이 흔한데, Little’s Law로 보면 근거가 없다.
이 DB 계층 동시성이 곧 pool 크기를 다시 잡는 근거가 된다.
The Floor
하한은 Utilization Law로 계산한다. 필요한 용량은 도착률과 요청당 자원 시간의 곱이다.
U_target이 왜 1이 아닌지가 중요하다. 대기행렬에서 응답시간은 utilization에 비례하지 않는다.
| utilization | 응답시간 |
|---|---|
| 50% | 2S |
| 80% | 5S |
| 90% | 10S |
| 99% | 100S |
네 점 사이가 어떻게 휘는지는 곡선으로 봐야 한다.
{
"diagram": "function-plot",
"aria": "utilization이 0에서 1로 갈수록 응답시간이 완만하게 오르다가 80% 부근부터 급격히 치솟는 곡선",
"x": { "label": "utilization", "min": 0, "max": 1, "ticks": 5, "decimals": 2 },
"y": { "label": "response time / S", "min": 0, "max": 50, "ticks": 5, "decimals": 0, "readoutDecimals": 1 },
"curves": [{ "expr": "1 / (1 - x)" }],
"markers": [{ "x": 0.8 }],
"point": { "x": 0.65, "max": 0.98 },
"readout": [{ "label": "queue", "expr": "x / (1 - x)", "decimals": 2 }]
}
utilization 50%에서 80%로 가는 비용과 80%에서 90%로 가는 비용이 완전히 다르다.
이 곡선 때문에 목표를 보통 65~75% 언저리로 잡는다. 100%를 목표로 두면 latency가 발산한다.
숫자를 넣어보자.
| 항목 | 값 |
|---|---|
| peak 트래픽 | 600 rps |
| 요청당 CPU 시간 | 20ms |
| instance당 코어 | 4 |
| 목표 utilization | 65% |
N+1은 1대가 빠져도 SLO를 지킨다는 뜻이다.
하한은 6대다.
여기서 놓치기 쉬운 게 latency 요구다. 5대로도 트래픽 자체는 소화되지만 그때 utilization이 78%라 p99가 SLO를 넘는다면, 하한을 정하는 건 처리량이 아니라 latency다.
처리량만 보고 대수를 정하면 안 죽는데 느린 상태에 빠진다.
이중화 요구가 곱셈이라는 점도 놓치기 쉽다. 3개 AZ에 고르게 분산하면서 AZ 하나가 통째로 사라져도 버티려면 대가 필요하다.
N+1로는 모자라고 AZ+1 기준으로 잡아야 한다.
The Ceiling
모든 instance는 자기 밖에 있는 유한한 무언가를 N에 비례해서 소비한다.
무엇이 천장이 되는지는 시스템마다 다르지만, 후보는 대체로 정해져 있다.
| 공유 자원 | 상한 식 | 넘으면 |
|---|---|---|
| DB connection | N × pool ≤ max_connections | 연결 거부, DB 메모리 압박 |
| Kafka partition | N ≤ partition 수 | 초과 consumer는 그냥 논다 |
| GPU | N × gpu_per_replica ≤ 총 GPU | 스케줄 불가 |
| NAT SNAT port | N × 동시 outbound ≤ 64k | 간헐적 연결 실패 |
| external API quota | N × per_instance_rate ≤ quota | 429 |
| subnet IP | N ≤ 가용 IP | pod 스케줄 실패 |
앞의 예제에 DB 천장을 씌워보자.
| 항목 | 값 |
|---|---|
max_connections | 100 |
| batch, admin, migration 예약 | 20 |
| 안전 여유 | 20% |
| instance당 pool 크기 | 16 |
상한은 4대다.
Floor Above Ceiling
하한 6, 상한 4. 구간이 비었다.
이 상황을 처음 만나면 둘 중 하나를 고르려는 충동이 드는데, 둘 다 함정이다.
하나는 6대로 밀어붙이고 max_connections를 올리는 길이다.
PostgreSQL은 connection 하나가 프로세스 하나다. 거기에 work_mem이 connection마다 붙는다.
정렬이나 해시 조인이 겹치면 메모리 사용량이 곱으로 늘어난다. app 쪽 용량을 늘리려다 DB를 죽이는 선택이다.
다른 하나는 4대로 줄이는 길인데, SLO를 포기해야 한다.
게다가 N+1 여유가 사라져서 배포 중에 1대만 빠져도 용량이 모자란다.
구간이 비었다는 건 곱셈의 어느 항이 잘못됐다는 신호지, 둘 중 하나를 포기하라는 뜻이 아니다.
N × pool ≤ 64라는 부등식에서 우리는 N만 붙들고 씨름했다. pool이 16이어야 할 이유는 한 번도 확인하지 않았다.
Pool Sizing
앞에서 계산해둔 값이 있다. DB 계층의 동시성은 4.8이었다.
클러스터 전체에 필요한 connection이 5개다. 6대 × 16 = 96이 아니라.
물론 평균값이라 그대로 쓰면 안 된다.
트래픽 burst와 query 시간의 p99 편차를 감안해 3배 여유를 줘도 15개다. 6대로 나누면 대당 2.5개, 올림해서 pool 4면 충분하다.
충돌이 사라졌다. 하한 6을 그대로 지키면서, 상한에는 여유가 40이나 남는다.
원래 설정은 96을 요구했고 실제로는 15면 됐다.
pool을 줄이면 대기가 생기지 않겠냐는 반발이 나올 수 있다.
생기긴 하는데 그게 오히려 낫다. 어차피 pool을 크게 잡는다고 DB가 빨라지지 않는다. DB의 처리 능력은 코어 수와 disk로 정해져 있다.
pool을 키우는 건 대기 줄을 app에서 DB 안으로 옮기는 것뿐이고, DB 안의 줄이 훨씬 비싸다.
lock 경합, context switch, connection별 메모리가 모두 거기서 늘어난다.
반면 app 쪽 pool에서 기다리면 pool wait time으로 측정되고, 획득 timeout으로 제어되고, 어느 서비스가 DB를 굶기는지 귀속된다.
References
- Neil Gunther — Universal Scalability Law
http://www.perfdynamics.com/Manifesto/USLscalability.html - HikariCP — About Pool Sizing
https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing - PostgreSQL Wiki — Number Of Database Connections
https://wiki.postgresql.org/wiki/Number_Of_Database_Connections - PgBouncer — Feature Matrix for Pooling Modes
https://www.pgbouncer.org/features.html - Kubernetes — Horizontal Pod Autoscaling
https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/ - Brendan Gregg — Systems Performance (2nd Edition), Chapter 2: Methodologies
https://www.brendangregg.com/systems-performance-2nd-edition-book.html