

# RDS for PostgreSQL에서 REPLICA IDENTITY FULL의 성능 문제 방지
<a name="PostgreSQL.ReplicaIdentityFull"></a>

PostgreSQL 논리적 복제를 사용하려면 구독자가 업데이트하거나 삭제할 올바른 행을 찾을 수 있도록 게시된 각 테이블에 *복제본 ID*가 있어야 합니다. 기본적으로 프라이머리 키는 복제본 ID 역할을 합니다. 테이블에 프라이머리 키 또는 적절한 고유 인덱스가 없는 경우 복제본 ID를 `FULL`로 설정하여 PostgreSQL이 전체 행을 키로 사용하도록 할 수 있습니다.

`REPLICA IDENTITY FULL`은 프라이머리 키 없이 테이블을 복제하는 즉각적인 문제를 해결하지만 게시자와 구독자 모두에게 심각한 성능 문제를 일으킬 수 있습니다. 블루/그린 배포와 같이 내부적으로 논리적 복제에 의존하는 기능을 포함하여 RDS for PostgreSQL에서 논리적 복제를 사용하는 모든 사용자에게 이러한 영향을 이해하는 것이 중요합니다.

## REPLICA IDENTITY FULL로 인해 문제가 발생하는 이유
<a name="PostgreSQL.ReplicaIdentityFull.WhyProblems"></a>

### 게시자의 WAL 볼륨 증가
<a name="PostgreSQL.ReplicaIdentityFull.WALVolume"></a>

`REPLICA IDENTITY` 설정은 PostgreSQL이 미리 쓰기 로그(WAL)에 쓰는 정보를 제어하여 업데이트되거나 삭제된 행을 식별합니다. 기본 복제본 ID(프라이머리 키)에서는 키 열만 이전 행 ID로 로깅됩니다. `FULL`을 사용하면 PostgreSQL은 각 `UPDATE` 및 `DELETE`에 대해 *모든* 열의 이전 값을 기록합니다. 이로 인해 다음과 같은 여러 결과가 발생합니다.
+ **WAL 크기가 크게 증가합니다.** 업데이트의 경우 모든 열의 이전 값과 새 값이 모두 기록되므로 각 WAL 레코드의 크기가 거의 두 배로 늘어납니다. 테이블에 [TOAST](https://www.postgresql.org/docs/current/storage-toast.html)를 사용하여 저장된 큰 값이 포함된 경우 업데이트에 의해 수정되지 않은 경우에도 TOASTed 값을 가져와서 WAL에 기록해야 하므로 증가량이 훨씬 더 클 수 있습니다.
+ **게시자의 I/O 및 CPU 사용량이 더 높습니다.** 추가 WAL 쓰기는 특히 쓰기 작업이 많은 워크로드의 경우 더 많은 디스크 I/O 대역폭과 CPU 주기를 소비합니다.
+ **구독자에게 더 많은 데이터가 전송됩니다.** 게시자는 네트워크를 통해 더 큰 WAL 레코드를 각 구독자에게 전송하여 대역폭 소비를 늘려야 합니다.

### 구독자의 느린 행 조회
<a name="PostgreSQL.ReplicaIdentityFull.SlowLookups"></a>

구독자가 `UPDATE` 또는 `DELETE` 로그 레코드를 수신하면 테이블의 로컬 복사본에서 일치하는 행을 찾아야 합니다. `REPLICA IDENTITY FULL`을 사용하면 구독자는 이전 행 이미지의 *모든* 열 값과 일치하는 행을 검색합니다.

PostgreSQL이 이 검색을 수행하는 방법은 PostgreSQL 메이저 버전에 따라 다릅니다.
+ **PostgreSQL 16 이전:** 테이블에 프라이머리 키가 없고 명시적으로 구성된 복제본 ID 인덱스가 없는 경우 구독자는 모든 단일 `UPDATE` 또는 `DELETE` 작업에 대해 전체 테이블을 순차적으로 스캔합니다. 큰 테이블에서는 이렇게 하면 적용 성능이 매우 느려집니다.
+ **PostgreSQL 16 이상:** 인덱스가 복제본 ID로 명시적으로 설정되지 않은 경우에도 구독자는 행 조회에 btree 또는 해시 인덱스를 사용할 수 있습니다. 그러나 구독자는 어떤 인덱스가 가장 효율적인지 평가하지 않습니다. [버전 16부터](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=89e46da5e) PostgreSQL은 찾은 [첫 번째 적합한 인덱스](https://www.postgresql.org/docs/18/logical-replication-publication.html#LOGICAL-REPLICATION-PUBLICATION-REPLICA-IDENTITY)를 선택하며 사용자는 이 선택을 제어할 수 없습니다. 선택한 인덱스의 선택도가 낮은 경우(예: 부울 또는 상태 열의 인덱스) 행 조회는 순차 스캔만큼 느릴 수 있습니다. 따라서 `REPLICA IDENTITY FULL`에서 암시적 인덱스 선택에 의존하는 것은 신뢰할 수 없으며 권장 구성이 아닌 폴백으로 간주해야 합니다.

### REPLICA IDENTITY FULL이 복제 지연을 유발하는 방법
<a name="PostgreSQL.ReplicaIdentityFull.ReplicationLag"></a>

게시자의 더 큰 WAL과 구독자의 더 느린 행 조회라는 두 가지 문제가 결합되어 복제 지연이 발생합니다.

기본적으로 PostgreSQL 논리적 복제는 구독당 단일 *적용 작업자* 프로세스를 사용하여 게시자로부터 변경 사항을 수신하고 구독자의 테이블에 적용합니다. 적용 작업자는 한 번에 한 행씩 순차적으로 변경 사항을 커밋 순서로 처리합니다. 즉, 구독자의 처리량은 각 개별 변경 사항을 얼마나 빨리 적용할 수 있는지에 따라 제한됩니다.

`REPLICA IDENTITY FULL`이 적절한 인덱스가 없는 테이블에 설정된 경우 `UPDATE` 및 `DELETE`마다 일치하는 행을 찾기 위해 전체 테이블을 순차적으로 스캔해야 합니다. 테이블에 수백만 개의 행이 있는 경우 이러한 각 작업은 몇 초 이상 걸릴 수 있습니다. 그 결과 계단식 문제가 발생합니다.

1. **게시자는 구독자가 적용할 수 있는 것보다 더 빠르게 변경 사항을 생성합니다.** 게시자의 쓰기 워크로드는 정상 속도로 지속되지만 구독자의 적용 작업자는 각 행 조회에 대해 순차적 스캔 또는 선택도가 낮은 인덱스에서 병목 현상이 발생합니다.

1. **WAL은 게시자에 누적되며 스토리지를 소진할 수 있습니다.** PostgreSQL은 구독자가 WAL 세그먼트를 적용했음을 확인할 때까지 WAL 세그먼트를 회수할 수 없습니다. 구독자가 더 뒤처지면 게시자는 디스크에 WAL을 누적합니다. RDS for PostgreSQL에서 이는 CloudWatch에서 `OldestReplicationSlotLag`가 증가하는 것으로 나타납니다. 심각한 경우 사용 가능한 모든 스토리지를 사용하고 게시자가 쓰기 수락을 중지할 수 있습니다.

1. **지연은 자기 강화적입니다.** 구독자가 뒤처지면 구독자의 테이블이 복제된 삽입에서 계속 증가하여 각 순차 스캔이 더 느려집니다. 개입 없이 지연은 제한 없이 증가합니다.

이 문제는 자주 `UPDATE` 또는 `DELETE` 작업을 받는 테이블의 경우 특히 심각합니다. 구독자에 대한 행 조회가 필요하지 않으므로 `INSERT` 작업은 영향을 받지 않습니다.

**참고**  
PostgreSQL 16부터 적용 작업자는 대규모 스트리밍 트랜잭션에 병렬 적용을 사용할 수 있으므로 처리량에 도움이 될 수 있습니다. 그러나 인덱스가 없는 `REPLICA IDENTITY FULL`의 기본 행 조회 병목 현상은 남아 있습니다. 각 개별 행을 찾으려면 여전히 스캔이 필요하기 때문입니다.

### 블루/그린 배포에 미치는 영향
<a name="PostgreSQL.ReplicaIdentityFull.BlueGreen"></a>

Amazon RDS의 블루/그린 배포는 데이터베이스당 단일 구독을 설정하여 그린 환경을 블루 환경과 동기화된 상태로 유지하기 위해 내부적으로 논리적 복제를 사용합니다. 그린 환경의 논리적 복제 *적용 프로세스*는 단일 스레드입니다. 단일 적용 작업자 프로세스는 블루 환경에서 모든 변경 사항을 수신하고 한 번에 하나씩 커밋 순서로 적용합니다. 블루/그린 복제 경로에는 병렬이 적용되지 않습니다.

이 단일 스레드 설계는 블루 환경을 따라잡는 그린 환경의 기능은 전적으로 적용 작업자가 각 개별 변경을 처리할 수 있는 속도에 달려 있음을 의미합니다. 테이블이 프라이머리 키 또는 적절한 인덱스 없이 `REPLICA IDENTITY FULL`을 사용하는 경우 적용 작업자에 미치는 영향은 PostgreSQL 버전에 따라 달라집니다. 16 이전 버전에서는 해당 테이블의 모든 `UPDATE` 및 `DELETE`에서 적용 작업자가 전체 테이블을 순차적으로 스캔하여 일치하는 행을 찾도록 강제합니다. 버전 16 이상에서 PostgreSQL은 [적절한 인덱스](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=89e46da5e)를 사용할 수 있는 경우 적절한 인덱스를 사용하지만, 적격 인덱스가 없는 경우에도 적용 작업자는 여전히 순차적 스캔으로 돌아갑니다. 적용 작업자가 한 행에 대해 큰 테이블을 스캔하는 동안 모든 테이블에서 대기 중인 다른 모든 변경 사항은 대기 중입니다.

블루/그린 배포의 결과는 중요합니다.
+ **복제 지연은 지속적으로 증가합니다.** 블루 환경이 단일 적용 작업자가 처리할 수 있는 것보다 더 빠르게 쓰기 트래픽을 생성하는 경우 그린 환경은 점점 더 뒤쳐집니다. 적용 작업자는 단일 스레드이므로 캐치업을 병렬화할 방법이 없습니다.
+ **전환을 차단할 수 있습니다.** 블루/그린 전환을 수행하려면 그린 환경을 블루 환경과 완전히 동기화해야 합니다. 복제 지연이 너무 높으면 제한 시간 내에 전환을 완료할 수 없습니다.
+ **그린 환경은 절대 따라잡지 못할 수 있습니다.** `REPLICA IDENTITY FULL`을 사용하고 인덱스가 없는 대용량 테이블이 있는 쓰기 중심 워크로드의 경우 적용 속도가 너무 느려서 그린 환경이 영구적으로 뒤처져 복제본 ID 구성을 먼저 해결하지 않으면 전환이 불가능해질 수 있습니다.
+ **WAL은 블루 환경에 누적됩니다.** 그린 환경이 뒤쳐져 있는 동안 블루 환경은 복제 슬롯에 대한 WAL 세그먼트를 유지합니다. 이렇게 하면 블루(프로덕션) 환경에서 스토리지 사용량이 증가하고 프로덕션 성능에 영향을 미칠 수 있습니다.

이러한 문제를 방지하려면 블루/그린 배포를 생성하기 *전에* 모든 테이블에 `ALTER TABLE ... REPLICA IDENTITY USING INDEX`를 사용하여 복제본 ID로 명시적으로 구성된 프라이머리 키 또는 적절한 고유 인덱스가 있는지 확인합니다. PostgreSQL 16 이상에서 암시적 인덱스를 선택할 때 `REPLICA IDENTITY FULL`에 의존하지 마세요. 구독자가 선택이 잘못된 인덱스를 선택하거나 순차 스캔으로 돌아갈 수 있기 때문입니다. 대표적인 쓰기 워크로드로 배포를 테스트하여 그린 환경이 따라갈 수 있는지 확인합니다.

블루/그린 배포 제한 사항에 대한 자세한 내용은 [Amazon RDS 블루/그린 배포 관련 제한 사항 및 고려 사항](blue-green-deployments-considerations.md) 섹션을 참조하세요. 모범 사례는 [블루/그린 배포 모범 사례에 대한 RDS for PostgreSQL 모범 사례](blue-green-deployments-best-practices.md#blue-green-deployments-best-practices-postgres) 섹션을 참조하세요.

## REPLICA IDENTITY FULL을 사용하여 테이블을 식별하는 방법
<a name="PostgreSQL.ReplicaIdentityFull.Identify"></a>

다음 쿼리를 실행하여 `REPLICA IDENTITY FULL`에서 모든 테이블을 찾습니다.

```
SELECT n.nspname AS schema, c.relname AS table_name, c.relreplident
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
  AND c.relreplident = 'f'
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY n.nspname, c.relname;
```

`relreplident` 열 값은 다음과 같습니다.
+ `d` - 기본값(프라이머리 키)
+ `n` — 없음
+ `f` - 전체(전체 행)
+ `i` - 특정 인덱스

## 해결 방법 및 모범 사례
<a name="PostgreSQL.ReplicaIdentityFull.Workarounds"></a>

### 가능한 경우 프라이머리 키 추가
<a name="PostgreSQL.ReplicaIdentityFull.AddPrimaryKey"></a>

가장 효과적인 솔루션은 프라이머리 키가 없는 테이블에 프라이머리 키를 추가하는 것입니다. 프라이머리 키가 있는 경우 PostgreSQL은 이를 기본 복제본 ID로 사용하여 구독자에 대한 효율적인 행 조회를 제공하고 게시자의 WAL 오버헤드를 최소화합니다.

```
ALTER TABLE my_table ADD COLUMN id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY;
```

**중요**  
기본값 표현식은 휘발성인 `nextval()`를 사용하기 때문에 이 문은 `ACCESS EXCLUSIVE` 잠금을 획득하고 전체 테이블을 다시 작성합니다. 테이블에 대한 모든 읽기 및 쓰기는 재작성 기간 동안 차단됩니다. 대형 테이블의 경우 이로 인해 상당한 가동 중지 시간이 발생할 수 있습니다. 유지 관리 기간 동안 이 변경을 계획하거나 먼저 열을 null로 생성한 다음 별도의 단계에서 제약 조건을 채우고 추가하는 등의 대체 접근 방식을 고려합니다.

애플리케이션 제약으로 인해 프라이머리 키를 추가할 수 없는 경우 `NOT NULL` 열 세트에 고유 인덱스를 추가하고 이를 복제본 ID로 설정하는 것이 좋습니다.

```
CREATE UNIQUE INDEX my_table_replica_idx ON my_table (col1, col2);
ALTER TABLE my_table REPLICA IDENTITY USING INDEX my_table_replica_idx;
```

**참고**  
인덱스가 빌드되는 동안 쓰기가 차단되지 않도록 하려면 [https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY](https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY) 절인 `CREATE UNIQUE INDEX CONCURRENTLY my_table_replica_idx ON my_table (col1, col2);`를 사용합니다.

**참고**  
복제본 ID에 사용되는 인덱스는 고유해야 하고, 일부가 아니어야 하며, 연기할 수 없어야 하고, `NOT NULL` 제약 조건이 있는 열만 포함해야 합니다.

### 암시적 인덱스 선택에 의존하지 마세요(PostgreSQL 16 이상).
<a name="PostgreSQL.ReplicaIdentityFull.SubscriberIndexes"></a>

PostgreSQL 16부터 구독자의 적용 작업자는 복제본 ID가 `FULL`로 설정된 경우 해당 인덱스가 복제본 ID로 명시적으로 구성되지 않은 경우에도 행 조회에 btree 또는 해시 인덱스를 사용할 수 있습니다. 이렇게 하면 경우에 따라 순차적 스캔을 방지할 수 있지만 다음과 같은 이유로 이러한 암시적 동작에 의존하는 것이 안티 패턴입니다.
+ **선택한 인덱스는 제어하지 않습니다.** PostgreSQL은 가장 선택적이거나 효율적인 인덱스가 아니라 카탈로그 순서로 찾은 첫 번째 적격 인덱스를 선택합니다. 테이블에 적격 인덱스가 여러 개 있는 경우 선택한 인덱스의 선택도가 낮아 조회 성능이 저하될 수 있습니다.
+ **동작은 취약합니다.** 인덱스를 추가, 삭제 또는 재구축하면 적용 작업자가 사용하는 인덱스가 변경되어 복제 시 예기치 않은 성능 회귀가 발생할 수 있습니다.
+ **기본 문제를 마스킹합니다.** 프라이머리 키 또는 명시적 복제본 ID가 없는 테이블은 본질적으로 논리적 복제에 위험합니다. 암시적 인덱스 선택에 의존하면 문제가 해결되지 않습니다.

대신 복제된 모든 테이블에 대해 복제본 ID를 명시적으로 구성합니다.
+ **가장 좋은 옵션:** 프라이머리 키를 추가합니다. 가장 안정적이고 효율적인 복제본 ID입니다.
+ **대안:** `NOT NULL` 열만 있는 특정 고유하고 부분적이지 않으며 지연 불가능한 인덱스를 지정하는 데 `ALTER TABLE ... REPLICA IDENTITY USING INDEX`를 사용합니다. 이를 통해 행 식별에 사용되는 열을 명시적으로 제어할 수 있습니다.

두 옵션 모두 사용할 수 없는 테이블에 대해서만 `REPLICA IDENTITY FULL`을 예약하고 성능이 직접 제어할 수 없는 요인에 따라 달라진다는 점을 이해합니다.

### 복제 지연 모니터링
<a name="PostgreSQL.ReplicaIdentityFull.MonitorLag"></a>

`REPLICA IDENTITY FULL`을 사용하는 경우 복제 지연을 면밀히 모니터링하여 구독자가 심각해지기 전에 속도 저하를 감지합니다.

**게시자에서** 현재 WAL 위치와 구독자가 확인한 내용 간의 지연을 확인합니다.

```
SELECT slot_name, confirmed_flush_lsn, pg_current_wal_lsn(),
       (pg_current_wal_lsn() - confirmed_flush_lsn) AS lag_bytes
FROM pg_replication_slots
WHERE slot_type = 'logical';
```

`lag_bytes` 값이 꾸준히 증가하면 구독자가 뒤처지고 있음을 나타냅니다. `pg_stat_replication_slots` 뷰는 각 복제 슬롯의 사용량에 대한 추가 통계를 제공합니다.

**구독자에서** `pg_stat_subscription` 뷰는 수신 및 보고된 마지막 WAL 위치를 포함하여 각 적용 작업자의 상태를 표시합니다.

```
SELECT subname, received_lsn, latest_end_lsn,
       last_msg_send_time, last_msg_receipt_time
FROM pg_stat_subscription;
```

**참고**  
PostgreSQL 16 이상에서는 기본 적용 작업자와 병렬 적용 작업자를 구분하도록 `worker_type`을 선택할 수도 있습니다.

`received_lsn`과 `latest_end_lsn` 사이의 큰 간격 또는 `last_msg_send_time`의 기한 경과 타임스탬프는 적용 작업자가 따라잡기 위해 어려움을 겪고 있음을 나타낼 수 있습니다. 또한 `pg_stat_subscription_stats` 뷰는 지연에 영향을 미칠 수 있는 적용 오류 및 충돌을 추적합니다.

**RDS for PostgreSQL**의 경우 복제 슬롯에서 가장 뒤쳐진 슬롯의 바이트 지연을 추적하는 `OldestReplicationSlotLag` CloudWatch 지표를 모니터링할 수도 있습니다. 값 상승은 복제 지연의 초기 경고 신호입니다.

**적용 중에 최적화되지 않은 인덱스를 사용할 수 있는 테이블 확인**

구독자는 적용 작업자가 과도한 힙 읽기를 수행하는 테이블을 식별할 수 있습니다. 이는 적용 중에 테이블에 행 조회에 대한 효율적인 인덱스가 없음을 나타낼 수 있습니다. 구독자에 대해 다음 쿼리를 실행합니다.

```
SELECT relname, heap_blks_read, heap_blks_hit,
       idx_blks_read, idx_blks_hit,
       heap_blks_read + heap_blks_hit AS total_heap_access
FROM pg_statio_user_tables
WHERE heap_blks_read > 0
ORDER BY heap_blks_read DESC
LIMIT 10;
```

`idx_blks_read`에 비해 `heap_blks_read` 값이 높은 테이블은 적용 작업자가 효율적인 인덱스를 사용하여 `UPDATE` 및 `DELETE` 작업에 대한 행을 찾지 못하고 있음을 나타낼 수 있습니다. 이는 `REPLICA IDENTITY FULL`이 사용 중일 때 복제 지연의 일반적인 원인입니다.

**참고**  
이 쿼리를 사용하려면 구독자에서 [https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-COUNTS](https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-COUNTS) 파라미터를 활성화해야 합니다. 이 파라미터는 기본적으로 활성화됩니다.

### REPLICA IDENTITY FULL이 필요한지 평가
<a name="PostgreSQL.ReplicaIdentityFull.Evaluate"></a>

`REPLICA IDENTITY FULL`을 설정하기 전에 실제로 필요한지 여부를 고려하세요. 이를 사용하는 일반적인 이유는 다음과 같습니다.
+ 테이블에는 프라이머리 키 또는 고유 인덱스가 없습니다.
+ 변경 데이터 캡처(CDC) 소비자를 위한 행의 전체 사전 이미지가 필요합니다.
+ 해당 열을 수정하지 않는 업데이트의 경우 복제 이벤트에 TOASTed 열 값이 포함되어야 합니다.

프라이머리 키가 없어서 유일한 이유가 있다면 거의 항상 프라이머리 키를 추가하는 것이 더 나은 경로입니다. CDC에 대한 전체 사전 이미지가 필요한 경우 CDC 소비자가 상태를 외부로 유지하여 전체 행을 재구성할 수 있는지 여부를 고려하면 `REPLICA IDENTITY FULL`의 WAL 및 구독자 오버헤드를 피할 수 있습니다.

## 권장 사항 요약
<a name="PostgreSQL.ReplicaIdentityFull.Summary"></a>


| 시나리오 | 권장 사항 | 
| --- | --- | 
| 테이블에 프라이머리 키가 있음 | 기본 복제본 ID 사용(작업 필요 없음) | 
| 테이블에 고유한 NOT NULL 인덱스가 있음 | ALTER TABLE ... REPLICA IDENTITY USING INDEX를 사용하여 해당 인덱스를 복제본 ID로 설정 | 
| 테이블에 적합한 키가 없음(PostgreSQL 16 이상) | 프라이머리 키 또는 고유 인덱스를 추가합니다. 암시적 인덱스 선택과 함께 REPLICA IDENTITY FULL을 사용하는 것은 신뢰할 수 없으며 최후의 수단이어야 합니다. | 
| 테이블에 적합한 키가 없음(PostgreSQL 16 이전) | 프라이머리 키 또는 고유 인덱스를 추가하고 가능하면 REPLICA IDENTITY FULL은 피합니다. | 
| 대용량/TOASTed 열이 있는 쓰기 작업이 많은 워크로드 | WAL 볼륨 증폭으로 인한 REPLICA IDENTITY FULL 방지 | 