Hubstaff는 ‘8월의 저주’를 극복하는 과정에서 80/20 법칙에 따라 최소한의 노력으로 최대의 효과를 얻을 수 있는 세 가지 핵심 교훈을 얻었습니다. 이 교훈들은 모든 규모의 PostgreSQL 환경에 적용 가능하며 즉각적인 성능 개선을 약속합니다.
1. PostgreSQL 기본 설정 변경
PostgreSQL은 다양한 환경에서 작동하도록 설계되어 기본 설정이 최적화되어 있지 않습니다. 다음 세 가지 설정을 조정하여 성능을 크게 향상시킬 수 있습니다.
-
shared_buffers: 데이터 캐싱을 위해 PostgreSQL에 할당할 메모리 양을 지정합니다. 총 메모리의 25-40%를 할당하는 것이 좋습니다. 읽기 집약적인 워크로드에는 40%까지 늘릴 수 있습니다. -
effective_cache_size: 쿼리 플래너에게 시스템에 사용 가능한 총 메모리 양에 대한 힌트를 제공합니다. 총 메모리의 50-75%로 설정하면 플래너가 메모리를 더 많이 활용하는 계획을 세우는 데 도움이 됩니다. -
random_page_cost: 디스크에서 임의 페이지를 읽는 비용을 나타냅니다. 기본값 4.0은 구식 회전 디스크에 적합하며, SSD 환경에서는 1.5-2.0으로 낮추어 인덱스 스캔을 더 선호하도록 유도해야 합니다. 이를 통해 쿼리 속도가 획기적으로 빨라질 수 있습니다.
2. Autovacuum 튜닝
PostgreSQL의 MVCC(다중 버전 동시성 제어) 모델은 업데이트 및 삭제 시 데이터를 물리적으로 제거하지 않고 ‘dead tuples’로 표시합니다. 이 dead tuples는 공간을 차지하고 인덱스 효율성을 저하시키며 ‘블로트(bloat)’ 현상을 유발합니다. Autovacuum은 이러한 dead tuples를 정리하는 자동화된 프로세스입니다. 그러나 대규모 테이블에서는 Autovacuum이 지연될 수 있으므로 다음 설정을 조정해야 합니다.
-
autovacuum_vacuum_scale_factor: 테이블 크기 대비 dead tuples의 비율이 얼마가 되어야 vacuum이 시작될지 결정합니다. 기본값 0.2(20%)는 너무 높으므로 0.03-0.05(3-5%) 범위로 낮추어 블로트 축적을 조기에 방지해야 합니다. -
autovacuum_analyze_scale_factor: 테이블 변경 비율이 얼마가 되어야 analyze가 실행될지 결정합니다. 기본값 0.1(10%)은 쿼리 플랜이 최신 상태로 유지되기에는 너무 드물므로 0.05(5%) 이하로 설정하여 쿼리 플래너가 최적의 실행 계획을 수립하도록 돕습니다.
3. 장기 실행 트랜잭션 방지
트랜잭션 내에서 HTTP 호출, 과도한 Ruby 로직 처리, 장시간 데이터베이스 락 유지와 같은 느린 작업을 수행하면 심각한 문제가 발생합니다.
-
성능 저하 및 DDL 블로킹: 애플리케이션 속도가 느려지고, 단순한
SELECT쿼리라도ALTER TABLE과 같은 DDL(데이터 정의 언어) 작업을 블로킹하여 무정지 배포를 방해할 수 있습니다. -
Autovacuum 방해: 장기 실행 트랜잭션은 Autovacuum이 dead tuples를 정리하는 것을 막아 블로트를 가속화합니다.
-
Transaction ID Wraparound 위험: PostgreSQL은 모든 트랜잭션에 32비트 XID(Transaction ID)를 할당합니다. 40억 개의 XID가 모두 사용되고 20억 개의 unvacuued dead tuples가 축적되면 PostgreSQL은 더 이상 쓰기 작업을 허용하지 않는 치명적인 ‘Transaction ID Wraparound’ 상태에 빠질 수 있습니다.
이를 방지하기 위해 다음 설정을 적용해야 합니다.
-
statement_timeout: 개별 SQL 문에 대한 실행 시간 제한을 설정합니다. 이상적으로는 5초 미만으로 설정하여 느린 쿼리를 조기에 차단합니다. -
idle_in_transaction_session_timeout: 트랜잭션이 열려 있지만 아무 작업도 수행하지 않는 ‘유휴’ 상태일 때 자동으로 트랜잭션을 종료하는 시간 제한을 설정합니다. 이를 통해 장기 유휴 트랜잭션으로 인한 문제를 방지합니다.