본문 바로가기

IT & AI

스타트업 Postgres가 무너지기 전에 챙길 운영 원칙

728x90

스타트업 Postgres가 무너지기 전에 챙길 운영 원칙

스타트업 Postgres 운영 가이드 썸네일
스타트업 Postgres 운영 가이드 썸네일
728x90

처음에는 인덱스 몇 개만 추가해도 Postgres가 잘 버텨요. 트래픽과 데이터가 늘면 잠금, 오래 열린 트랜잭션, 낡은 통계, 연결 폭증처럼 서로 다른 문제가 한꺼번에 나타나요. 워크플로 실행 플랫폼 Hatchet은 2년간 프로덕션에서 겪은 문제를 토대로 설계부터 대형 테이블 이전까지 이어지는 운영 원칙을 공개했어요. 1

핵심 요약

구분핵심왜 볼 만한가요
읽기 성능필터와 정렬 순서에 맞춘 복합 인덱스를 설계하고 실행 계획을 확인해요인덱스가 있어도 플래너가 순차 스캔을 고르는 이유를 찾을 수 있어요
쓰기 안정성트랜잭션을 짧게 유지하고 필요한 행만 잠가요외부 API 호출이나 대형 작업이 잠금을 오래 붙드는 일을 줄여요
처리량여러 쓰기를 배치로 묶고 연결 풀을 사용해요Hatchet의 측정에서는 배치 처리량이 약 10배 늘었어요
장기 운영autovacuum, 파티셔닝, 온라인 마이그레이션을 미리 설계해요dead tuple과 장시간 잠금이 장애로 번지기 전에 대응할 수 있어요

1. 인덱스보다 먼저 실행 계획과 잠금 시간을 봐야 해요

Hatchet의 출발점은 스키마예요. 읽기와 쓰기 중 어느 쪽이 많은지, 자주 거는 필터가 무엇인지, 어떤 열을 반복해서 갱신하는지부터 묻도록 권해요. 기본 키와 시간 타입 같은 선택도 배포 뒤에는 바꾸기 까다로워요. ORM을 쓰더라도 규모가 커지면 생성된 SQL을 확인하고 필요한 구간에서는 직접 쿼리를 제어할 수 있어야 해요. 2

목록 쿼리에서는 `WHERE` 조건만 맞춘 인덱스로 부족할 수 있어요. 조직별 문서를 최신순으로 읽는다면 `(organization_id, created_at DESC)`처럼 필터 열과 정렬 열을 함께 넣는 복합 인덱스가 후보가 돼요. 인덱스는 읽기를 빠르게 만들지만 쓰기 비용과 저장 공간도 늘려요. 쿼리마다 인덱스를 하나씩 더하는 방식은 오래가기 어려워요. 2

`EXPLAIN ANALYZE`는 플래너가 예상한 행 수와 실제 처리한 행 수를 비교할 때 유용해요. 다만 쿼리를 실제로 실행하므로 운영 데이터베이스에서는 쓰기 쿼리와 무거운 조회를 특히 조심해야 해요. 실행 없이 계획만 보고 싶다면 `ANALYZE`를 뺀 `EXPLAIN`을 먼저 쓸 수 있어요. 작은 테이블이나 많은 행을 읽는 조건에서는 순차 스캔이 인덱스 탐색보다 쌀 수 있으니, `Seq Scan`이라는 표시만 보고 실패로 판단하면 안 돼요. 2

쓰기 쪽에서는 트랜잭션이 열려 있는 시간이 중요해요. 트랜잭션 안에서 외부 서비스를 호출하면 네트워크 응답을 기다리는 동안 행 잠금도 유지될 수 있어요. 갱신 대상을 좁히고 잠금 순서를 일정하게 맞추면 대기와 교착 상태 위험을 줄일 수 있어요. 이미 큰 테이블에 인덱스를 붙일 때는 일반 `CREATE INDEX`가 쓰기를 막을 수 있으므로 `CREATE INDEX CONCURRENTLY`를 검토해야 해요. 2

처리량을 높일 때는 개별 쿼리의 속도만 보지 않아요. 애플리케이션과 데이터베이스 사이를 왕복하고, 연결 풀에서 연결을 빌리고, Postgres가 내부 잠금을 처리하는 데도 비용이 들어요. Hatchet은 여러 쓰기를 한 번에 보내는 배치 처리로 측정 처리량을 약 10배 높였어요. 새 연결을 요청마다 만들지 않고 PgBouncer나 애플리케이션 연결 풀을 쓰면 연결 폭증도 피하기 쉬워요. 이 수치는 Hatchet의 시스템에서 나온 결과라서 다른 서비스에서는 배치 크기와 지연 시간을 함께 측정해야 해요. 2

autovacuum은 저장 공간 정리만 맡는 기능이 아니에요. 갱신과 삭제 뒤에 남은 dead tuple을 회수하고 transaction ID 고갈을 막아요. 쓰기 속도가 기본 설정의 정리 속도를 앞지르면 테이블과 인덱스가 불어나고, 심하면 transaction ID wraparound 대응으로 긴 중단이 생길 수 있어요. 장시간 트랜잭션도 오래된 행 버전의 회수를 막기 때문에 활성 세션과 vacuum 실행 시간을 같이 봐야 해요. 2

규모가 더 커지면 작업 성격에 맞는 구조가 필요해요. 여러 워커가 같은 큐를 소비할 때 `FOR UPDATE SKIP LOCKED`로 이미 선택된 행을 건너뛸 수 있어요. 시계열이나 오래된 작업 데이터는 파티션으로 나누면 각 파티션을 따로 정리하고 오래된 범위를 빠르게 제거할 수 있어요. 대형 테이블을 옮길 때는 긴 단일 트랜잭션 대신 배치 백필과 트리거를 조합해 이전 중 들어오는 쓰기를 새 테이블에도 반영하는 방식을 제안해요. 2

왜 중요한가요

초기 스타트업은 기능 출시 속도를 먼저 챙길 수밖에 없어요. 그래도 트랜잭션 경계, 인덱스 생성 방식, 연결 수 제한은 데이터가 작을 때 정해 두는 편이 싸요. 장애가 난 뒤에는 쿼리 하나만 고쳐도 되는지, 스키마를 바꿔야 하는지, 오래 열린 세션부터 끊어야 하는지 구분하기 어렵기 때문이에요. Hatchet의 가이드는 이 판단 순서를 읽기, 쓰기, 유지보수, 확장 단계로 나눠 보여줘요. 2

가이드의 범위도 확인해야 해요. GeekNews에 연결된 토론에서는 백업과 복구, 모니터링과 호출 경보가 생존 가이드에 더 분명히 들어가야 한다는 지적이 나왔어요. 쿼리 최적화와 vacuum 튜닝이 잘돼 있어도 복구 시점 목표와 실제 복원 훈련이 없으면 데이터 손실 사고를 막기 어려워요. 직접 운영할 인력이 부족한 팀이라면 관리형 Postgres가 제공하는 자동 백업, 특정 시점 복구, 고가용성 옵션까지 비용 비교에 넣는 편이 현실적이에요. 3

모든 조언을 그대로 적용할 필요는 없어요. 작은 테이블의 순차 스캔은 충분히 빠를 수 있고, PgBouncer가 필요하지 않은 규모도 있어요. 파티셔닝과 직접 SQL은 복잡성을 늘려요. 느린 쿼리 기록, 잠금 대기 시간, 연결 수, dead tuple 증가율을 먼저 관찰하고 병목이 확인된 지점부터 바꾸는 편이 안전해요. 2

참고 자료

  1. 스타트업의 Postgres 생존 가이드 — GeekNews
  2. The startup's Postgres survival guide — Hatchet
  3. Hacker News discussion: The startup's Postgres survival guide — Hacker News
728x90