View
새벽 3시에 슬랙 알람이 울려 일어났는데 "Citus shard placement inconsistent"라는 메시지가 떠 있다면 누구라도 당황한다. Citus 샤딩 복구는 한국어 자료가 거의 없어 영문 깃허브 이슈를 뒤지다 시간만 날리는 경우가 보통이다. 그래서 실전에서 쓸 만한 진단 흐름과 복구 절차, 그리고 직접 겪었던 장애 사례 3개를 한 번에 정리한다.
Citus는 PostgreSQL 위에 얹는 분산 익스텐션으로, 코디네이터(coordinator) 노드가 메타데이터를 보유하고 워커(worker) 노드들이 실제 샤드를 보유하는 구조다. 운영하다 보면 워커가 죽거나, 리밸런싱(rebalancing) 도중 락이 걸려 멈추거나, pg_dist_shard 같은 메타데이터가 워커의 실제 상태와 어긋나는 일이 상당히 잦다. 한 번 터지면 데이터 손실 위험까지 가니 차분하게 단계별로 가야 한다.
일단 진정하고, 먼저 확인해야 할 것들

출처: datadoghq.com
Citus 샤딩 복구의 첫 단계는 진단이다. 무조건 진단부터 한다. 무언가를 고치겠다며 메타데이터부터 건드리면 상황은 더 악화된다. 정말로 그렇다.
코디네이터의 생존 여부부터 체크
코디네이터에 접속해 일단 Citus 익스텐션이 응답하는지 확인하면 된다.
SELECT citus_version();
SELECT * FROM citus_check_cluster_node_health();
이것부터 동작하지 않으면 코디네이터 자체가 문제다. 코디네이터의 PostgreSQL 로그를 살펴보고 디스크가 가득 찼는지, WAL이 꽉 찼는지, prepared transaction이 누적되었는지 확인해야 한다. pg_stat_activity에 idle in transaction (aborted)이 쌓여 있다면 거기부터 정리해야 한다.
워커 노드 상태 점검
코디네이터가 워커를 어떻게 인식하고 있는지 확인한다.
SELECT nodeid, nodename, nodeport, isactive, noderole
FROM pg_dist_node;
isactive=false인 워커가 있다면 그것이 1차 용의자다. 다만 isactive=true임에도 실제로는 ping조차 가지 않는 워커가 있다. 그래서 코디네이터에서 직접 워커로 psql 접속이 가능한지 체크해야 한다.
메타데이터와 실제 샤드의 일치 여부
이것이 진정 중요하다. 코디네이터의 pg_dist_placement가 보유한 정보와 워커에 실제로 존재하는 테이블이 다르다면 거기서 깨진 것이다.
SELECT shardid, shardstate, nodename, nodeport
FROM pg_dist_placement
JOIN pg_dist_node USING (groupid)
WHERE shardstate != 1;
shardstate가 1이면 정상, 3이면 비활성, 4면 to-be-deleted다. 1이 아닌 것이 잔뜩 나오면 거기가 폭발 지점이다. 이때 워커에 직접 들어가 \dt+ *_를 실행하면서 정말로 그 샤드 테이블이 워커에 있는지 교차 검증해야 한다.
Citus 샤드 깨짐 패턴은 보통 이 5가지로 좁혀진다

출처: ByteByteGo Newsletter (415KB)
수백 번 트러블슈팅한 결과, Citus 샤드 깨짐 패턴은 결국 다음 다섯 가지 중 하나다. 그 외의 사례는 거의 없다.
워커 노드 다운 또는 네트워크 분리
EBS 볼륨이 망가지거나, 인스턴스가 STP에서 통째로 격리되거나, OOM 킬러가 PostgreSQL을 잡아먹는 경우다. 코디네이터는 살아 있는데 워커가 응답하지 않으면 그 워커가 보유한 샤드는 전부 unavailable이 된다.
리밸런싱 도중 멈춤 (rebalance stuck)
이것이 정말 자주 발생한다. citus_rebalance_start()를 돌려놓고 자러 갔는데 아침에 보니 0% 진척으로 멈춰 있는 경우다. advisory lock을 잡고 있는 다른 세션 때문이거나, 디스크 공간이 부족해 새 샤드를 만들지 못하거나, 네트워크가 끊겨 데이터 복사 도중 멎은 것이다.
메타데이터 불일치 (pg_dist_*가 실제와 어긋남)
pg_dist_shard, pg_dist_placement, pg_dist_partition이 실제 워커 상태와 맞지 않는 케이스다. 보통 누군가 워커에서 직접 DROP TABLE을 했거나, 백업 복원이 잘못되었거나, 리밸런싱 도중 강제로 끊었을 때 발생한다.
레퍼런스 테이블이 한쪽 워커에만 남는 경우
레퍼런스 테이블(reference table)은 모든 워커에 동일하게 복제되어야 정상이다. 그런데 워커를 추가했음에도 replicate_reference_tables()를 실행하지 않으면 새 워커에는 레퍼런스 테이블이 없다. JOIN 쿼리를 돌리면 그 워커를 거치는 순간 폭발한다.
분산 트랜잭션이 prepared 상태로 잔존
Citus는 2PC(2-phase commit)를 사용하는데, 이것이 commit 단계에서 끊기면 prepared transaction이 좀비처럼 남는다. pg_prepared_xacts에 쌓이면서 락이 풀리지 않고, autovacuum도 동작하지 않는다. 이것을 방치하면 정말로 며칠 안에 DB가 마비된다.
Citus 메타데이터 복구 — 그래도 가장 안전한 시나리오다

실제 데이터는 워커에 멀쩡히 있는데 코디네이터가 잘못 인식하는 경우다. Citus 샤딩 복구 케이스 중 가장 다행스러운 시나리오다. 데이터 손실 위험은 거의 없다.
master_copy_shard_placement로 placement 재생성
복제본(replica)이 살아 있고 placement만 망가진 상태라면 공식 함수만으로 끝난다.
SELECT master_copy_shard_placement(
102008, -- shardid
'worker-2', 5432, -- source (정상 노드)
'worker-3', 5432 -- target (복구할 노드)
);
이것 한 방으로 정상 노드에서 깨진 노드로 샤드가 복사된다. shard_replication_factor가 2 이상이고 한쪽 복제본이 살아 있어야 작동한다. 1로 운영했다면 이 길은 없다. Citus 11.0 이상이면 master_copy_shard_placement가 citus_copy_shard_placement로 이름이 바뀌었으니 주의해야 한다.
pg_dist_placement 직접 수정 — 진정한 마지막 수단
공식 문서에서는 권장하지 않지만, 운영하다 보면 어쩔 수 없이 직접 만져야 할 때가 있다. 가령 워커 IP만 바뀌었는데 메타데이터에 옛날 IP가 박혀 있는 경우다.
-- 백업부터 무조건 뜬다. 절대 빼먹지 마라.
CREATE TABLE pg_dist_placement_backup AS
SELECT * FROM pg_dist_placement;
-- 그 다음에 수정한다
UPDATE pg_dist_placement
SET shardstate = 1
WHERE shardid = 102008 AND groupid = 3;
이것을 잘못하면 되돌릴 방법이 없다. 무조건 백업 테이블을 떠 놓고, 한 줄 한 줄 검증하며 가야 한다. 가능하면 staging에서 먼저 동일하게 재현해 검증하고 prod에 적용하는 것이 정석이다.
복구 후 검증
수정이 끝났다면 무조건 검증한다.
SELECT * FROM citus_check_cluster_node_health();
SELECT * FROM run_command_on_workers($cmd$ SELECT count(*) FROM your_table $cmd$);
워커마다 행 수를 비교해 이상한 노드가 없는지 확인해야 안심할 수 있다.
실제 데이터까지 손상되었을 때 — 정말 까다로운 시나리오
워커의 디스크가 망가졌거나, 누군가 DROP TABLE을 실행했거나, 파일시스템이 깨진 경우다. 메타데이터만 고쳐서는 해결되지 않으며 실제 데이터를 살려야 한다.
복제본에서 끌어오기
citus.shard_replication_factor를 2 이상으로 운영했다면 다른 워커에 복제본이 있다. master_copy_shard_placement 또는 citus_copy_shard_placement로 정상 복제본에서 끌어오면 된다. 운영 환경에서 이것을 켜놓지 않고 1로 갔다가 워커 한 대의 디스크가 나가면 그 샤드 데이터는 사실상 모두 사라진다.
WAL 기반 PITR 복구
streaming replica를 별도로 운영했거나, archive WAL을 보관 중이라면 PITR(point-in-time recovery)로 시간을 되돌릴 수 있다. 워커 1대만 PITR을 돌리고 코디네이터는 그대로 두는 식이라면 메타데이터와 실제가 또 어긋날 수 있어 신중해야 한다. PostgreSQL 공식 문서의 continuous archiving 설명이 정말로 도움이 된다.
마지막 수단 — 부분 데이터 손실을 인정하고 reshard
복제본도 없고 PITR도 안 된다면 현실을 받아들여야 한다. 깨진 샤드가 보유한 데이터 범위는 잃어버린 것이고, 나머지 샤드는 살아 있다. 깨진 샤드 placement를 지우고 빈 샤드를 새로 만든 뒤 reshard를 돌리면 서비스는 살아남는다. 다만 그 샤드의 row들은 사라진 것이다. 이 결정은 비즈니스 측과 함께 해야지 DBA 혼자 결정할 사안이 아니다.
리밸런싱이 멈췄을 때 푸는 법

출처: MarkLogic (15KB)
Citus 리밸런싱 실패 케이스는 정말 흔하다. 이것은 거의 단골이다.
상태 확인
SELECT * FROM citus_rebalance_status();
SELECT pid, state, query, wait_event, wait_event_type
FROM pg_stat_activity
WHERE query LIKE '%rebalance%' OR query LIKE '%shard%';
wait_event가 advisory lock 관련이라면 누군가 락을 잡고 있는 것이다. 또한 워커마다 run_command_on_workers로 디스크 사용량도 확인해야 한다. 디스크가 가득 찼다면 리밸런싱은 절대 끝나지 않는다.
advisory lock을 잡고 있는 세션 종료
락을 잡고 있는 세션을 찾아 종료한다.
SELECT pid, locktype, mode, granted
FROM pg_locks
WHERE locktype = 'advisory' AND NOT granted;
-- 진짜로 죽일 거면
SELECT pg_terminate_backend(<pid>);
다만 이때 정말로 그 세션이 의미 있는 작업 중인지부터 확인해야 한다. 잘못 종료하면 리밸런싱 결과가 어중간하게 남아 더 꼬인다.
강제 중단 후 한 샤드씩 수동 이동
전체 리밸런싱을 한 번에 하지 말고 끊고 다시 시작한다.
SELECT citus_rebalance_stop();
-- 한 샤드씩 옮기기
SELECT citus_move_shard_placement(
102045,
'worker-1', 5432,
'worker-4', 5432,
shard_transfer_mode := 'block_writes'
);
block_writes는 쓰기를 잠시 막고 깔끔하게 옮긴다. force_logical은 logical replication을 사용해 무중단으로 옮긴다. 무중단이 좋긴 하지만 logical replication 셋업이 되어 있지 않으면 실패한다. 보통은 작은 샤드부터 한두 개씩 손으로 옮기며 클러스터를 안정화시키는 것이 안전하다.
실전 사례 3가지 — 실제로 이렇게 해결했다

이론은 그만하고 케이스 스터디로 넘어가자. 익명화하긴 했지만 모두 실제로 일어난 일들이다.
사례 1 — EBS 볼륨이 나가면서 워커 1대를 통째로 잃은 경우
스타트업 A가 워커 4대로 운영 중이었는데 워커 2번의 EBS 볼륨이 갑자기 detach되었다. AWS 측에서 복구 불가 통보를 받았고, shard_replication_factor=2로 운영 중이라 복제본은 워커 1, 3에 분산되어 있었다.
복구 절차는 다음과 같았다. 새 EC2에 PostgreSQL과 Citus를 설치해 빈 워커로 띄웠다. 코디네이터에서 citus_remove_node('worker-2', 5432)로 죽은 워커를 정리했다. citus_add_node('worker-new', 5432)로 새 워커를 등록했다. 그다음 깨진 placement들에 대해 master_copy_shard_placement를 정상 복제본 → 새 워커 방향으로 실행했다. 총 47개 샤드, 약 1.4시간이 걸려 끝났다. 데이터 손실은 0건이었다. shard_replication_factor=2가 정말로 살린 것이다.
사례 2 — 리밸런싱이 12시간 멈춰 있던 것을 advisory lock을 풀어 살린 경우
서비스 B가 워커 6대 → 8대로 확장하면서 citus_rebalance_start()를 실행했는데 12시간이 지나도 진척이 0%였다. citus_rebalance_status()를 보니 첫 샤드에서 멈춰 있었다.
pg_locks를 조회해보니 advisory lock 하나가 granted=false로 대기 중이었다. 그 락의 holder는 자정에 돌아간 배치 잡 세션이었는데 connection은 끊겼지만 prepared transaction이 남아 있었다. pg_prepared_xacts를 확인하니 좀비 트랜잭션 3개가 발견되었다. ROLLBACK PREPARED 'gid_xxx'로 정리하니 락이 풀리고 리밸런싱이 다시 진행되었다. 결국 4시간이 더 걸려 끝났다. 교훈은 prepared transaction 모니터링을 무조건 해야 한다는 것이다.
사례 3 — 메타데이터만 꼬인 것을 직접 UPDATE로 살린 경우
서비스 C는 staging에서 prod로 데이터를 복원하다가 코디네이터 메타데이터는 prod 그대로인데 워커 데이터는 staging 시점으로 되돌아간 경우다. pg_dist_placement가 보유한 shardstate는 1(active)인데 실제로는 일부 샤드 테이블이 워커에서 사라졌다.
복구는 다음과 같이 진행했다. 먼저 pg_dist_placement를 통째로 백업 테이블로 떴다. 그다음 워커마다 \dt로 실제 존재하는 샤드 테이블 목록을 뽑고, 메타데이터와 비교해 없는 placement는 shardstate=4로 마킹 후 삭제했다. 이후 master_copy_shard_placement로 살아 있는 복제본에서 다시 끌어왔다. 이 케이스는 데이터 일부 손실(약 0.3%)을 인정하고 비즈니스 합의를 받은 다음 진행한 것이다. 메타데이터 직접 수정은 정말로 마지막 수단이고, 백업과 합의 없이 절대 해서는 안 된다.
다시 깨지지 않게 하려면 — 예방이 곧 답이다
장애를 한 번 겪고 나면 예방이 진정한 답이라는 사실을 깨닫게 된다. Citus 샤딩 복구를 다시 하지 않으려면 다음을 챙겨야 한다.
shard_replication_factor를 2 이상으로 운영
가장 기본이다. 1로 운영하면 워커 한 대가 나가는 순간 데이터를 잃는다. 복구 옵션 자체가 사라진다. 비용이 조금 더 들더라도 무조건 2 이상으로 가는 것이 맞다. Citus 공식 문서에서도 production은 2 이상을 권장한다.
모니터링 자동화
pg_dist_node의 isactive, pg_prepared_xacts 카운트, citus_check_cluster_node_health() 결과를 Prometheus로 긁어 알람을 걸어둔다. prepared transaction은 5분 이상 살아 있으면 무조건 알람을 발생시킨다. 디스크 사용량이 80%를 넘으면 리밸런싱 시작 전에 미리 알람을 띄운다.
정기 리밸런싱과 백업
리밸런싱은 트래픽이 적은 시간대(주말 새벽)에 자동으로 실행하되, 한 번에 모두 하지 말고 chunk 단위로 끊어 진행한다. 백업은 코디네이터 메타데이터와 워커 데이터를 같은 시점으로 일관되게 떠야 의미가 있다. 워커만 백업하고 메타데이터를 빼먹으면 복원해도 사용할 수 없다. Citus GitHub 이슈에 백업 일관성 관련 사례가 많이 올라와 있다.
핫 샤드를 만들지 않는 샤드 키 설계
샤드 키(distribution column)가 잘못 잡히면 한 샤드에만 트래픽이 몰리고 거기만 디스크가 폭발하는 상황이 온다. user_id 같은 카디널리티가 높은 컬럼을 쓰는 것이 정석이며, 시간 기반 컬럼은 절대 메인 샤드 키로 두면 안 된다. 시간 기반은 partitioning과 함께 써야 한다.
Citus 샤딩 복구 핵심 5단계 요약
Citus 샤딩 복구는 결국 진단 → 안전한 복구 → 마지막 수단의 순서를 지키는 것이 핵심이다. 정리하면 다음과 같다.
- 1단계: pg_dist_node, pg_dist_placement, pg_prepared_xacts를 보고 어디서 깨진 것인지 정확히 짚는다
- 2단계: 복제본이 살아 있다면 master_copy_shard_placement가 가장 안전하다
- 3단계: 리밸런싱 멈춤은 advisory lock과 prepared transaction부터 의심한다
- 4단계: 메타데이터 직접 수정은 백업을 뜨고, staging에서 검증하고, 비즈니스 합의를 받은 다음에만 한다
- 5단계: shard_replication_factor=2 이상과 모니터링이 진정 예방의 80%다
샤드가 깨졌을 때 가장 위험한 것은 황급히 메타데이터를 직접 수정하는 행위다. 새벽에 졸린 눈으로 UPDATE pg_dist_placement를 때리면 그날 회사가 망한다. 무조건 백업을 뜨고, 가능하면 복제본 활용 경로부터 가야 한다.
지금 운영 중인 Citus 클러스터가 있다면 이 글을 닫기 전에 SHOW citus.shard_replication_factor를 한 번 실행해 보라. 1이라면 이번 주 안에 무조건 2 이상으로 올려야 한다. 그것이 다음에 새벽 알람을 받지 않을 가장 확실한 방법이다.
'Database' 카테고리의 다른 글
| DB max_connections=100인데 pool 200을 잡으면 어떻게 되는가 — 응급처치 3가지 직접 비교 (0) | 2026.05.03 |
|---|---|
| DB Connection Pool은 클수록 좋다는 거짓말 — PostgreSQL 풀을 1부터 256까지 늘려본 결과 (0) | 2026.05.03 |
| RocksDB란 무엇이며 왜 다들 사용하는가 (0) | 2026.04.30 |
| Vitess가 갑자기 부상하는 이유 (0) | 2026.04.28 |
| Postgres는 테이블 락만 가능한가? 아니면 행 단위 락도 가능한가? 결론부터 정리한다 (0) | 2026.04.28 |
