H
하베스트
AI로 요약된 콘텐츠

5년간 PostgreSQL 확장에서 얻은 교훈

요약:
OneSignal이 5년간 PostgreSQL을 수십 TB 규모로 확장하면서 겪은 실제 문제들과, 이를 해결했던 노하우들을 시간순으로 자세히 공유합니다. 대규모 실무 경험을 바탕으로, 데이터 구조, 소프트웨어 및 인덱스 비대화(블로트), 자동 관리, 업그레이드, 파티셔닝과 샤딩 등 주요 이슈별로 실전 팁이 이어집니다. PostgreSQL 확장을 고민한다면 꼭 알아둬야 할 현실적인 교훈과 조언이 가득합니다.


1. 데이터 구조와 워크로드 개요

OneSignal은 다채널 메시징 플랫폼으로, 핵심 데이터는 subscribers(구독자)notifications(알림)입니다.
subscribers 테이블은 10억 개가 넘는 활성 구독 토큰을 다루고, 앱이 열릴 때마다 계속 기록이 갱신되기 때문에 쓰기(INSERT/UPDATE) 부하가 매우 높고, 분석 쿼리도 자주 실행됩니다. 예를 들어 "특정 속성을 가진 수백만 사용자에게 푸시를 보낸다" 같은 쿼리가 흔하게 몇 분씩 걸릴 만큼 복잡하게 실행됩니다.

"앱이 열릴 때마다 해당 구독자의 마지막 방문 시간 정보를 반드시 업데이트해야 합니다."

한편, notifications 테이블은 용량이 다양하고, 생성 시 기록된 후엔 UPDATE와 유지를 위한 삭제가 빈번히 발생합니다. 알림 데이터도 파티션·샤드되어 처리됩니다.

이제, 이런 데이터 구조 위에서 등장한 현실적인 문제들 ― 블로트, 업그레이드, XID 래퍼라운드, 복제본 승격, 파티셔닝, 샤딩 등이 어떻게 발생하고 해결됐는지 살펴봅니다. 😊


2. 소프트웨어 블로트: 테이블·인덱스의 비대화 현상

데이터가 크고 변경이 빈번한 환경에서 PostgreSQL의 저장공간 블로트 문제가 심각하게 드러났습니다.
블로트란, 원래 쓸모있는 데이터보다 디스크를 훨씬 많이 차지하게 되어 버리는 현상을 말합니다.

2-1. 테이블 블로트

테이블의 dead tuple(쓸모없는 레코드 조각들)들이 공간을 계속 차지합니다.
심지어 DELETE 후에도 그 공간을 다른 테이블이나 인덱스가 재사용할 수 없습니다.

"만약 10개의 레코드를 삽입한 후 9개를 삭제하면, 해당 공간은 그대로 남아서 테이블의 '죽은 튜플'이 됩니다."

정기적으로 VACUUM을 해야만 테이블 내부 공간이 다시 사용 가능해지지만, 대량 데이터 보존 정책에서 방향을 바꾸면서(예: 300GB → 10GB 보관), 남는 공간은 결국 비효율로 남았습니다.

2-2. 인덱스 블로트

PostgreSQL은 업데이트가 많은 테이블에서 지수적으로 인덱스 블로트가 발생합니다.
'직접 인덱스' 구조상, 한 번만 인덱스 값을 바꿔도 반드시 인덱스가 갱신되고, 그 덕에 매번 dead index entry가 누적됩니다.

"우리 'subscribers'데이터에는 21개의 인덱스가 있어서 한 번의 업데이트마다 20개의 dead entry가 생깁니다."

2-3. 블로트 줄이기: 실전 대응

(1) autovacuum 튜닝

자동 VACUUM을 자주 돌리면 dead tuple이 많이 줄어듭니다. 하지만 너무 자주 해도 시스템 성능에 부담이 가기 때문에, 싱글 데이터 워크로드에 맞춘 세심한 설정이 필요합니다.

(2) 테이블 파티셔닝

데이터 보존 정책의 대상을 파티션 단위로 나누면, 보존 기간이 지난 데이터를 손쉽게 DROP TABLE 한 번에 삭제할 수 있습니다.
이를 자동으로 관리해주는 pg_partman 확장도 활용 가능!

(3) 빈번히 갱신되는 컬럼 분리

예: 대용량 문자열 컬럼(big_column)과 매우 자주 바뀌는 컬럼(int_column)이 함께 있을 때, 자주 바뀌는 컬럼만 별도 테이블로 분리해서 UPDATE 시 복제되는 공간 낭비를 줄입니다.


3. 블로트 복구: reclaiming space

3-1. VACUUM FULL

"전체 공간을 회수하려면 VACUUM FULL을 사용해야 하며, 이 방식은 테이블 전체와 모든 인덱스를 새로 복사하는 동안 전체 잠금이 걸립니다. 심각한 경우 외에는 사용하지 마세요."

즉, 실제 실무에서는 '긴급' 상황에서만 쓰라는 조언입니다.

3-2. pg_repack

pg_repack은 실제로 OneSignal이 주로 활용하는 도구입니다.
테이블/인덱스의 bloat를 락 거의 없이 해결할 수 있어 좋은데,
한계와 주의사항도 분명합니다.

  • 테이블 리팩 중, 쓰기 부하가 높으면 리팩이 끝없이 지연될 수 있다
  • 자동 데몬이 따로 없어 수동 실행 필요 (OneSignal은 자체 데몬을 만듦)
  • 인덱스만 자동 리팩이 안전, 테이블은 수동으로 함
  • 동시 인덱스 리팩이 제한적 (PostgreSQL/pg_repack의 한계)

"우리 데몬은 테이블 트래픽이 막히거나 데이터베이스 백업이 진행될 때 리팩을 자동으로 중단/대기하도록 만들었습니다."

3-3. pgcompacttable

pgcompacttable은 완전 비차단 방식이라, 서비스 가용성이 매우 중요할 때 pg_repack 대신 사용합니다. 다만 좀 더 느립니다.


4. 데이터베이스 업그레이드: 고가용성 유지 방법

4-1. 메이저(Major) 업그레이드

메이저 버전 업그레이드는 디스크 포맷까지 바뀌므로 단순 재시작이 아닌 전체 데이터 재구성이 필요합니다.

OneSignal은 논리 복제(logical replication) 기반 방식만 사용합니다.
기본적인 순서는 다음과 같습니다:

  1. 새 버전 서버 준비
  2. 논리 복제로 데이터 동기화(Hot Standby)
  3. 복제본을 신규 운영 DB로 전환

"우린 단 한 번도 pg_upgrade를 써서 다운타임을 감수한 적이 없습니다. 모두 논리복제로 스위치해서 서비스 중단 없이 진행됐습니다."

이때, 논리 복제의 디코딩 과정이 싱글스레드이므로, DB 쓰기 부하가 워낙 높으면 동기화 지연이 쌓일 수 있습니다. 그런 경우엔 테이블 단위로 논리 복제하며, 점진적으로 애플리케이션이 신규 DB로 접근하도록 코드를 조정합니다.

4-2. 마이너(Minor) 업그레이드

  • PostgreSQL 바이너리 업데이트 후 단순 재시작만으로 가능
  • 필요시, 스트리밍 리플리케이션·스위치오버를 쓰면 무중단도 가능

5. XID(트랜잭션 ID) 래퍼라운드와 예방

PostgreSQL은 32비트 트랜잭션 ID를 쓰는데, 대량 트랜잭션 환경에서 XID가 곧 고갈돼 데이터 손상이 올 수 있습니다.

"XID가 너무 가까워지면 데이터베이스가 명령을 더이상 받지 않고, 수동 복구를 해야만 재가동할 수 있습니다. 이를 방지하려면 남은 XID를 반드시 실시간 모니터링해야 합니다."

OneSignal은 다음 쿼리로 잔여 XID를 체크하고,
250,000,000 이하가 되면 긴급 경고를 트리거합니다.

SELECT power(2, 31) - age(datfrozenxid) AS remaining
FROM pg_database
WHERE datname = current_database();

XID wraparound 문제로 인해 다운된 경험도 있었으나, 이후부터는

  • autovacuum_freeze_max_age 튜닝
  • 잔존 XID 적극 모니터링
    을 지속하고 있습니다.

추가로 감탄할 만한 다른 회고 사례:
sentry.io의 사고 보고서


6. 복제본(Replica) 승격 및 장애대응

장애나 계획된 업그레이드 때 복제본을 주 DB로 빠르게 승격시키는 것도 경험의 노하우입니다.
이를 위해 OneSignal은 DB/복제본을 모두 haproxy 뒤에 두고,

  • Read-Write, Read-Only 백엔드를 분리
  • 장애발생 시, health-check를 바탕으로 2~3초 내 복제본을 승격

"Patroni(HA PostgreSQL) 솔루션도 있지만, 우리는 아직 직접 관리하고 있습니다. 만약 처음부터 다시 시작한다면 적극 검토할 것 같습니다."


7. 테이블 파티셔닝 전략

데이터 규모가 큰 테이블은 파티셔닝으로 확실히 관리할 수 있습니다.
OneSignal도 subscribersnotifications 테이블 전부를 테넌트별 256개 파티션(향후 4096 예정)으로 분리 운영 중입니다.

"파티션 개수를 미리 넉넉히 나눠두기를 추천합니다. 파일시스템 성능 이슈 걱정 때문에 바로 4096으로 늘리지 못했는데, 최신 EXT4에서는 그게 문제가 되지 않는다는 걸 나중에 알았습니다."


8. 샤딩(Sharding)의 실제와 교훈

샤딩은 파티셔닝보다 한발 더 나아가는 확장 전략으로, 데이터를 여러 DB 프로세스(서버)로 분산 저장하는 방식입니다.
실무에서는 파티션 후 샤드에 분산 저장하는 방법을 택합니다.

  • OneSignal은 '테넌트 ID'를 기준으로 결정론적으로 분산했으나,
  • 향후엔 데이터 프록시(Data Proxy)를 구현해, 파티션·샤딩 구성 정보를 애플리케이션에서 분리하는 방향으로 전환하고 있습니다.

"처음부터 프록시를 뒀다면 파티션·샤드 구조 변경이 한결 쉬웠을 것입니다. 지금은 그때 빚진 기술부채를 일부 갚고 있습니다."


마치며

이 글에서는 5년간 실제 대규모 PostgreSQL 환경에서 겪었던 실제 문제, 의외의 장애, 그리고 실용적 해결책들을 아주 구체적으로 다뤘습니다. 🛠️
확장, 자동화, 이슈 사전 탐지와 관리 등 각 상황별 실전 팁과 주의점을 기억해둔다면, 여러분의 PostgreSQL 대규모 운영/확장에도 큰 도움이 될 것입니다.

"여기서 다 다루지 못한 이야기들도 많아요. PostgreSQL 확장 관련 질문이나 궁금한 점이 있다면 언제든 메시지 보내 주세요. 혹시 저희가 답을 모르더라도, 적절한 분을 안내해드릴게요!"


OneSignal 채용 공고 보기 (엔지니어를 채용 중입니다!) 🚀

요약 완료: 2025. 11. 4. 오전 11:26:52

이런 요약이 필요하신가요?

하베스트가 원클릭으로 요약해드립니다

5초 요약
AI 자동 분석
📱
모든 기기
웹, iOS, Chrome
🔍
스마트 검색
언제든 재발견
요약 시작하기
나도 요약하기