앱 서버를 늘려도 안 빨라진다 — 병목은 DB 한 대
트래픽이 늘어 앱 서버를 두 배로 늘렸는데 처리량은 그대로고 응답은 오히려 나빠졌어요. 앱 서버는 놀고 DB 만 100% 고요. 늘려도 안 되는 이유와 DB 부담을 덜어 내는 순서를 알 수 있어요.
트래픽이 늘어 앱 서버를 두 배로 늘렸어요. 그런데 처리량이 안 늘어요. 오히려 응답 시간이 나빠졌어요.
지표를 보니 앱 서버들의 CPU 는 여유가 있어요. 전부 DB 응답을 기다리고 있어요. DB 는 CPU 가 100% 고요.
서버를 더 늘려도 같아요. 늘릴수록 DB 커넥션만 늘고, 앞 편에서 본 대로 DB 가 더 느려져요.
수평 확장은 상태가 없는 계층에서만 통해요. 상태를 가진 계층은 그냥 늘릴 수 없어요.
그리고 대부분의 시스템에서 그 계층은 DB 하나예요. 확장의 상한이 거기서 정해지고, 그걸 모르면 돈만 쓰게 됩니다.
왜 그런가
애플리케이션 서버는 상태가 없어요. 요청 하나를 처리하고 잊어버리니 서버가 몇 대든 상관없어요. 그래서 늘리면 늘린 만큼 처리량이 늘어요.
DB 는 다릅니다. 데이터가 한 곳에 있어야 일관성이 유지돼요. 그냥 복제해서 늘리면 어느 쪽이 진짜인지 문제가 생겨요.
그래서 앱을 아무리 늘려도 결국 DB 한 대의 처리 능력에서 막혀요. 이걸 병목이라고 부르고, 병목을 못 늘리면 전체가 안 늘어납니다.
그리고 늘리는 게 오히려 해로울 수 있어요. 앱 서버마다 커넥션 풀이 있으니, 서버를 늘리면 DB 커넥션 수가 곱해져요. 앞 편에서 본 대로 커넥션이 많아지면 DB 가 더 느려져요. 확장이 성능을 떨어뜨리는 구간에 들어가는 거예요.
그래서 확장을 얘기하기 전에 무엇이 병목인지 알아야 해요. 병목이 아닌 계층을 늘리는 건 돈만 쓰고 아무것도 개선하지 않아요. 그리고 어떤 경우엔 악화시킵니다.
이건 앰달의 관점과 같아요. 전체 개선은 개선하지 않은 부분에 의해 제한됩니다.
코드로 보면
병목을 늘리는 방법은 다르다:
· 앱은 그냥 늘어난다 (상태 없음)
인스턴스 × 2 = 처리량 × 2
DB 는 그냥 안 늘어난다 (상태 있음)
앱만 늘리면 커넥션만 늘어 DB 가 더 느려진다
· DB 를 늘리는 방법 — 순서대로 검토한다
1) 쿼리·인덱스 개선 (가장 싸다)
2) 캐시로 읽기를 흡수
3) 읽기 복제본으로 읽기 분산
4) 수직 확장 (더 큰 장비)
5) 기능별 분리 (도메인마다 DB)
6) 샤딩 (마지막 수단, 복잡도가 크게 는다)✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.
실전에서는
병목을 늘리는 순서가 있어요. 싼 것부터 봅니다.
첫째, 쿼리와 인덱스 개선. 가장 싸고 효과가 클 때가 많아요. 앞 편에서 본 대로 총 시간이 큰 쿼리부터 고치면 DB 부하가 크게 줄어요. 확장을 검토하기 전에 이걸 먼저 해야 해요.
둘째, 캐시. 읽기의 상당 부분을 DB 까지 안 가게 만들 수 있어요. 다만 앞 편에서 본 스탬피드와 정합성 문제가 따라옵니다.
셋째, 읽기 복제본. 읽기가 압도적으로 많은 서비스라면 효과가 큽니다. 대신 복제 지연을 다뤄야 하고, 앞 편에서 본 대로 읽기 전용 선언이 제대로 되어 있어야 해요.
넷째, 수직 확장. 더 큰 장비로 옮기는 거예요. 단순하고 애플리케이션 변경이 없어서 실무에서 아주 유효한 카드예요. 요즘 장비는 상당히 크고, 대부분의 서비스는 이것만으로 충분합니다. 다만 한계가 있고 비용이 선형이 아니에요.
다섯째, 기능별 분리. 주문, 회원, 로그를 각각 다른 DB 로 나눠요. 각 DB 의 부하가 줄고 장애도 격리돼요. 대신 DB 를 넘는 조인과 트랜잭션을 포기해야 합니다.
여섯째, 샤딩. 같은 종류의 데이터를 키로 나눠 여러 DB 에 두는 거예요. 이론상 무한히 늘어나지만 복잡도가 크게 늘어요. 앞 편에서 본 핫 키 문제, 샤드 간 조인, 재분배, 트랜잭션 제약이 전부 따라옵니다.
여기서 중요한 판단이 있어요. 대부분의 서비스는 샤딩까지 안 가도 돼요. 쿼리 개선과 캐시와 수직 확장만으로 아주 멀리 갈 수 있어요. 샤딩을 너무 일찍 도입하면 얻는 것보다 잃는 게 큽니다.
처방
- 무엇이 병목인지 먼저 확인하세요. 병목이 아닌 곳을 늘리는 건 돈만 씁니다.
- 쿼리와 인덱스부터 개선하세요. 가장 싸고 효과가 큽니다.
- 캐시로 읽기를 흡수하세요. 다만 정합성과 스탬피드를 함께 설계하세요.
- 읽기 복제본을 검토하세요. 읽기 전용 선언이 제대로 되어 있는지 먼저 확인하고요.
- 수직 확장을 과소평가하지 마세요. 단순하고 효과가 확실합니다.
- 기능별 분리를 샤딩보다 먼저 검토하세요. 복잡도가 훨씬 낮습니다.
- 샤딩은 마지막에 하세요. 필요해지기 전에 도입하면 얻는 것보다 잃는 게 큽니다.
- 앱을 늘릴 때 DB 커넥션 총합을 계산하세요. 인스턴스 수 × 풀 크기가 max_connections 에 가까워지면 늘리는 게 해가 됩니다.
- 앱 앞에 pgbouncer 같은 풀러를 두는 것도 검토하세요. 앱을 늘려도 DB 커넥션은 안 늘어납니다.
- 병목이 정말 DB 인지 확인하세요. 외부 API·캐시·네트워크·로그 수집기가 병목인 경우도 있습니다.
지금 확인해 보세요
병목이 어디인가:
· 앱을 늘렸을 때 처리량이 비례해 느는가
안 늘면 다른 곳이 병목이다
오히려 나빠지면 늘린 것이 해가 되고 있다
· 각 계층의 여유
앱 CPU · DB CPU · 디스크 · 네트워크 · 커넥션 수
앱은 여유 있고 DB 만 꽉 차 있으면 답은 분명하다
· 앱 서버 수 × 풀 크기 = DB 커넥션 총합
max_connections 에 가까워지고 있지 않은지
· 읽기와 쓰기의 비율
읽기가 압도적이면 복제본이 유효하다
· 총 시간 상위 쿼리를 먼저 고쳤는가
확장 전에 이걸 안 했으면 순서가 틀렸다핵심 한 줄상태 없는 것만 그냥 늘어난다. 병목을 확인하고 나서 늘려라.