pgbouncer

2 개의 포스트

figma

PGKeeper: Postgres에 꼭 필요했던 보안관 만들기 | Figma 블로그 (새 탭에서 열림)

Figma는 데이터베이스 트래픽과 제품 규모가 커지면서 기존 PgBouncer의 확장성·부하 제어·연결 관리 한계에 부딪혔고, 이를 대체할 자체 서비스 PGKeeper를 구축했다. PGKeeper는 DBProxy와 PostgreSQL 사이에서 연결 풀링과 트래픽 관리를 담당하며, 과부하로부터 데이터베이스와 연결을 보호하는 것을 목표로 한다. 글은 Figma의 데이터베이스 구조와 PgBouncer를 교체하게 된 이유, 그리고 PGKeeper를 직접 개발한 배경을 설명한다. ## Figma의 PostgreSQL 데이터베이스 구조 - PostgreSQL은 Figma의 OLTP 시스템 기반이다. - 데이터 증가에 대응하기 위해 데이터베이스를 수평·수직으로 확장하고 여러 PostgreSQL 인스턴스를 운영했다. - 애플리케이션이 샤딩 구조를 직접 알 필요 없도록 **DBProxy**라는 요청 라우팅 계층을 구축했다. - DBProxy는 쿼리를 분석해 적절한 PostgreSQL 인스턴스를 선택하고, 필요하면 여러 인스턴스를 대상으로 쿼리를 재작성한다. - DBProxy와 PostgreSQL 사이에는 연결 풀러가 위치한다. - 각 PostgreSQL 머신에는 전용 연결 풀러 복제본 그룹이 배치되어, 여러 풀러가 하나의 데이터베이스에 연결되는 n-to-1 구조를 이룬다. ## PgBouncer가 한계에 도달한 이유 - **확장성 부족** - PgBouncer는 단일 스레드 아키텍처라 수직 확장에 한계가 있었다. - 복제본을 늘려 수평 확장했지만 트래픽 분배가 치우치면서 성능 저하가 발생했다. - **정교한 부하 관리 부재** - 중요한 트래픽을 낮은 우선순위의 문제성 트래픽보다 먼저 처리할 수 없었다. - 백프레셔가 없어 급격한 트래픽 증가를 우아하게 제어하기 어려웠다. - 대기 시간에 기반해 작업을 버리는 CoDel(Controlled Delay) 같은 부하 완화 알고리즘도 지원하지 않았다. - **연결 폭증과 연결 churn 위험** - PostgreSQL 연결은 많은 자원을 사용한다. - 연결이 빠르게 생성되거나 과도하게 재생성되면 데이터베이스 안정성이 크게 떨어진다. - 장애 이후 무제한으로 연결을 다시 만들면 복구 과정 자체가 PostgreSQL을 압박할 수 있다. - 이로 인해 연결 churn과 연쇄적인 과부하가 장시간 지속될 수 있다. - **확장성과 운영 제어의 한계** - 트래픽 유형별 admission control, 공정한 자원 분배 등 세밀한 정책이 필요했다. - 심층적인 관측성, 기능 플래그 기반의 안전한 롤아웃도 요구됐다. - PgBouncer에 작은 수정 사항을 유지하는 것만으로도 부담이 컸기 때문에, 대규모 확장은 장기적인 유지보수 비용을 초래할 가능성이 컸다. ## DBProxy에 연결 풀링을 통합하지 않은 이유 - PostgreSQL 인스턴스 하나의 연결 풀은 대략 100개 규모로 운영됐다. - 반면 DBProxy는 수백 개의 상태 비저장 복제본으로 구성됐다. - 각 DBProxy가 독립적으로 연결 풀을 관리하면: - 전체 PostgreSQL 연결 제한을 초과할 수 있고, - 복제본 간 복잡한 조정이 필요하며, - 고정된 소규모 연결 풀을 수백 개 인스턴스에 분산하기 어렵다. - 따라서 연결 풀링은 DBProxy 내부가 아니라 별도의 중앙화된 계층에서 담당해야 했다. ## PGCat 대신 자체 서비스를 만든 배경 - Figma는 PgBouncer의 단일 스레드 한계를 해결하기 위해 멀티스레드 PostgreSQL 프록시인 PGCat도 검토했다. - 그러나 필요한 기능을 추가하려면 PGCat의 핵심 실행 경로를 크게 수정해야 했다. - 관측성, 기능 플래그, admission control을 upstream에 반영하기도 쉽지 않았다. - 결국 PGCat을 포크해 장기적으로 유지해야 할 가능성이 높았으므로, Figma는 요구사항에 맞는 Go 기반 자체 서비스 **PGKeeper**를 개발했다. ## PGKeeper의 역할 - PGKeeper는 DBProxy와 PostgreSQL 사이에 위치하는 Go 서비스다. - 주요 목적은 다음과 같다. - 데이터베이스를 과도하거나 잘못된 트래픽으로부터 보호 - PostgreSQL 연결의 급격한 생성과 churn 방지 - 트래픽 우선순위와 자원 사용을 세밀하게 제어 - 대규모 환경에 맞는 확장성과 운영 도구 제공 - 이름은 골키퍼처럼 위험한 트래픽이 시스템을 압도하지 못하도록 막고, 연결 자체도 보호한다는 의미에서 붙여졌다. Figma의 사례는 단순히 연결 풀러를 교체한 것이 아니라, 데이터베이스 앞단에서 트래픽을 선별하고 과부하를 제어하는 별도 인프라 계층이 필요해졌음을 보여준다. PostgreSQL 연결 수가 제한적이고 프록시 복제본이 많은 환경에서는 풀링을 애플리케이션 프록시에 분산하기보다, 독립적인 계층으로 관리하는 편이 적합하다.

figma

데이터베이스 아키텍처 (새 탭에서 열림)

Figma는 사용자와 기능 증가로 단일 PostgreSQL 데이터베이스의 CPU 사용률과 지연 시간이 급증하자, 장애 위험을 선제적으로 낮추기 위해 멀티 데이터베이스 구조로 전환했다. 단기적으로 인스턴스 확장, 읽기 복제본, PgBouncer 등을 도입했지만 쓰기 부하와 복제 지연 문제는 해결하지 못했다. 수평 샤딩이나 NoSQL 전환 대신, 연관된 테이블 그룹을 별도 데이터베이스로 옮기는 수직 파티셔닝을 장기 해법으로 선택했다. ## 단일 데이터베이스의 확장 한계 - Figma는 권한, 파일 정보, 댓글 등 대부분의 메타데이터를 하나의 대형 Amazon RDS 데이터베이스에 저장했다. - 데이터베이스 트래픽은 매년 약 3배씩 증가했다. - 2020년 피크 시간대 CPU 사용률이 65%를 넘었고, 사용량이 한계에 가까워질수록 지연 시간이 예측하기 어려워졌다. - 데이터베이스가 완전히 포화되면 Figma 전체가 작동을 멈출 수 있었기 때문에, 장애가 발생하기 전에 확장 문제를 해결할 필요가 있었다. ## 단기적인 안정화 조치 Figma는 장기적인 구조 변경을 준비하는 동안 약 1년의 추가 여유를 확보하기 위해 다음 조치를 시행했다. - RDS 인스턴스를 `r5.12xlarge`에서 `r5.24xlarge`로 확장해 CPU 처리 여력을 늘렸다. - 여러 개의 read replica를 구성해 읽기 트래픽을 분산했다. - 새로운 사용 사례에는 별도 데이터베이스를 사용해 기존 데이터베이스의 성장을 제한했다. - PostgreSQL 연결 풀러인 PgBouncer를 도입해 수천 개에 달하는 데이터베이스 연결이 주 데이터베이스에 미치는 영향을 줄였다. - PgBouncer는 애플리케이션과 RDS 사이에서 연결을 관리하고 재사용하는 계층으로 동작했다. ## 읽기 복제본만으로는 부족했던 이유 - 데이터베이스 사용량을 분석한 결과, 데이터 수집·수정·삭제와 같은 쓰기 작업도 상당한 부하를 차지했다. - 모든 읽기 요청을 replica로 옮길 수 없었다. - 일부 기능은 복제 지연(replication lag)에 민감해 최신 데이터가 반드시 주 데이터베이스에서 제공되어야 했다. - 따라서 읽기와 쓰기 양쪽 모두에서 원본 데이터베이스의 작업을 추가로 분산해야 했다. ## 수평 확장 방안의 검토 Figma는 테이블을 여러 데이터베이스에 분산하는 수평 샤딩도 검토했지만, 다음과 같은 부담이 있었다. - Figma가 사용하는 PostgreSQL과 기본적으로 호환되지 않는 관리형 수평 확장 솔루션이 많았다. - NoSQL이나 MySQL 기반 Vitess로 이전하려면 애플리케이션에 큰 변경이 필요했다. - 마이그레이션 과정에서 이중 읽기·쓰기 구조를 운영해야 하므로 복잡성이 커졌다. - PostgreSQL 호환 NewSQL을 선택하면 클라우드 환경에서 매우 큰 분산 PostgreSQL 클러스터를 운영하는 초기 고객이 될 위험이 있었다. - 관리형 솔루션은 내부 동작과 확장 한계를 통제하기 어렵고, Figma 규모에서 충분히 검증되지 않은 문제를 직접 겪을 수 있었다. - 직접 호스팅하면 운영 지식과 인력을 새로 확보해야 하며, 상당한 운영 비용이 발생했다. ## 테이블 그룹 단위의 수직 파티셔닝 Figma는 수평 샤딩 대신 수직 파티셔닝을 선택했다. - 수직 파티셔닝은 테이블 또는 컬럼을 다른 데이터베이스로 이동하는 방식이다. - Figma는 개별 테이블의 행을 여러 데이터베이스로 쪼개는 대신, 서로 관련된 테이블 그룹 전체를 별도 데이터베이스로 옮겼다. - 이 방식은 기존 데이터베이스의 부하를 즉시 줄일 수 있었다. - 장기적으로는 특정 테이블 그룹에 대해 향후 수평 샤딩을 적용할 수 있는 기반도 제공했다. - 기존 PostgreSQL 생태계와 애플리케이션 구조를 크게 바꾸지 않고 확장할 수 있다는 점이 장점이었다. ## 분할 대상 선정 기준 데이터베이스를 분리하기 전에 어떤 테이블을 옮길지 평가해야 했다. 주요 기준은 다음 두 가지였다. - **영향도(Impact)** - 테이블을 이동했을 때 데이터베이스 작업량이 크게 줄어야 했다. - Figma는 쿼리에 대한 평균 활성 세션 수(AAS)를 사용해 부하를 측정했다. - PostgreSQL의 `pg_stat_activity`를 10밀리초 간격으로 조회해 쿼리별 활성 상태와 CPU 대기 관련 정보를 분석했다. - **격리성(Isolation)** - 분리 대상 테이블이 다른 테이블과 강하게 결합되어 있지 않아야 했다. - 테이블 간 의존성이 높으면 데이터베이스 간 조인과 트랜잭션 처리가 복잡해지므로, 독립적으로 이동할 수 있는 영역을 우선했다. 결국 Figma의 접근 방식은 단일 데이터베이스를 무리하게 확장하는 대신, 부하가 크고 다른 기능과의 결합도가 낮은 테이블 그룹부터 별도 데이터베이스로 분리하는 것이었다. 실무에서도 먼저 읽기 부하뿐 아니라 쓰기 부하와 복제 지연을 함께 측정하고, 수평 샤딩보다 운영 복잡성이 낮은 수직 분할부터 검토하는 것이 현실적인 전략이다.