PostgreSQL BaseBackup 중 Archive 파일을 삭제하면 무슨 일이 벌어질까? 🤔


PostgreSQL 운영 중 basebackup(핫백업)을 진행하는 경우,
많은 트랜잭션이 발생하면서 WAL(Write-Ahead Log) 파일과 Archive 파일이 지속적으로 생성됩니다.

그런데 백업 중에 Archive 파일을 삭제하면 어떤 일이 벌어질까요...?
간단히 말해서 "복구 불가능", 심각한 장애로 이어질 수 있습니다.

이번 글에서는 basebackup 중 Archive 파일을 삭제하면 벌어지는 문제를 상세히 정리해보겠습니다.


 

1. PostgreSQL의 Basebackup 구조 이해

PostgreSQL에서 basebackup은 다음 절차로 진행됩니다.

  • pg_start_backup() → 데이터 파일 스냅샷 시작
  • 디스크에서 데이터 파일 복사 진행
  • 트랜잭션 기록은 지속적으로 WAL에 남김
  • pg_stop_backup() → 복구를 위한 최종 지점(mark)을 찍음

핵심 포인트:

  • basebackup은 "파일 백업"만 하는 것이고,
  • 백업 중 발생하는 변경 기록은 모두 WAL 파일(및 Archive)에 남아야 한다.

복구 시에는

  1. 백업 파일을 복구
  2. backup 시작 지점 이후의 WAL 기록을 적용하여 데이터 일관성을 맞춘다

즉, WAL 파일이 없다면 basebackup만으로는 절대 복구할 수 없습니다.

 

 

2. basebackup 중 Archive 파일 삭제 시 발생하는 문제

만약 basebackup이 진행되는 동안,
WAL archive 파일이 삭제된다면 어떤 문제가 발생할까요?

➡️ 복구가 실패합니다.


복구 시 PostgreSQL은 다음과 같은 오류를 발생시킵니다:

FATAL: could not locate WAL file "0000000100000000000000AB" DETAIL: Archive file not found.
이 오류는 복구에 필요한 WAL 파일을 찾을 수 없다는 뜻.

결국, basebackup은 무용지물이 되고, 데이터 손실 혹은 서비스 중단까지 이어질 수 있습니다.

 

 

왜 basebackup 중에는 Archive 파일을 삭제하면 안 되는가?

항목 설명
basebackup 역할 데이터 디렉터리 스냅샷 백업
백업 중 트랜잭션 기록 WAL 파일에 기록됨
복구 시 필요한 것 백업 파일 + basebackup 시점 이후의 WAL
Archive 삭제 시 문제 필요한 WAL 손실 → 복구 실패

✅ 결론: Basebackup 중에는 archive 파일을 절대로 삭제해서는 안 된다.

 

 

그러면, Archive File 이 DISK FULL 차기 전 대응 방법

  • 먼저 basebackup 시작 전, archive 디스크 용량 충분히 확보하기
  • backup_label 파일이 존재하는 동안 archive 파일 삭제 금지


한줄 요약

PostgreSQL BaseBackup 중 Archive 파일을 삭제하면 복구가 불가능해진다.
BaseBackup 완료 전까지는 Archive 파일을 반드시 안전하게 보존해야 한다.

 

 

 

🙌 댓글, 공감, 공유는 큰 힘이 됩니다! 😄

 

참조 :
https://www.postgresql.org/docs/current/app-pgbasebackup.html

https://www.postgresql.org/docs/current/continuous-archiving.html

https://www.postgresql.org/docs/current/progress-reporting.html#BASEBACKUP-PROGRESS-REPORTING

 

 

📝 RDBMS DBA 관점에서  바라보는 MongoDB 커리큘럼

[1] MongoDB 소개와 기본 개념 이해

  • RDBMS vs NoSQL
    • 관계형 vs 비관계형 개념 비교
    • 스키마 유연성 개념
  • MongoDB 기본 구조
    • Database (RDBMS) → Database (MongoDB)
    • Table → Collection
    • Row → Document
    • Column → Field
    • Primary Key → _id 필드
    • JOIN 연산 → Embedded Document, Reference
  • JSON과 BSON 데이터 형식 이해
    • BSON(Binary JSON)의 개념과 사용법 이해

[2] MongoDB 설치 및 기본 환경 구축

  • MongoDB 설치
    • 설치 및 구성 (단일 노드, 레플리카 세트 구성)
    • 환경 변수 설정 및 서비스 등록
  • CLI 툴 사용
    • mongosh, mongoimport, mongodump, mongorestore

[3] CRUD 및 쿼리 활용

  • 기본 CRUD
    • SELECT → find(), findOne()
    • INSERT → insertOne(), insertMany()
    • UPDATE → updateOne(), updateMany(), replaceOne()
    • DELETE → deleteOne(), deleteMany()
  • 쿼리 필터링과 프로젝션
    • 조건 연산자($eq, $ne, $gt, $lt, $gte, $lte, $in, $nin)
    • 정규 표현식 검색, 문자열 검색
    • 정렬, 페이징 처리(sort, skip, limit)
  • 집계 연산(Aggregation Framework)
    • GROUP BY → $group
    • HAVING → $match (after group)
    • SUM, COUNT, AVG 연산

[4] MongoDB 데이터 모델링과 설계

  • 정규화 vs 비정규화 전략
    • Embedding (내장) vs Referencing (참조)
    • 성능과 유지보수성 고려 설계
  • 모델링 사례 연구 (Oracle → MongoDB 변환)
    • Oracle 스키마의 MongoDB로의 전환 연습

[5] 성능 튜닝과 인덱스 관리

  • MongoDB 인덱스 이해
    • 단일 필드 인덱스, 복합 인덱스
    • TTL 인덱스, 부분 인덱스
    • Explain Plan (쿼리 실행 계획 확인)
  • 성능 이슈와 튜닝
    • 느린 쿼리 분석 및 최적화
    • 인덱스 커버리지, 힌트 기능 활용
    • MongoDB Profiler 및 로깅 분석

[6] MongoDB 아키텍처 심화

  • WiredTiger 스토리지 엔진 이해
    • 데이터 파일 구조, 캐싱 메커니즘
    • Checkpoint, Journal
  • 레플리카 셋 구성 및 관리
    • Primary, Secondary, Arbiter 개념
    • Failover와 High Availability 설정
    • Read Preference 설정
  • 샤딩 클러스터 이해
    • 샤드 구성 요소 (mongos, config server, shard node)
    • 데이터 분산 전략, Shard Key 설계

[7] 백업과 복구 전략

  • 백업 방법
    • mongodump/mongorestore (논리적 백업)
    • 파일 시스템 기반 백업(Snapshot 기반, 물리적 백업)
  • 복구 시나리오
    • Oplog를 이용한 Point-in-Time Recovery (PITR)
    • 레플리카 세트 복구 및 노드 교체

[8] MongoDB 보안 관리

  • 사용자 관리와 권한 설정
    • Role-Based Access Control(RBAC) 이해
  • 암호화 및 감사 로깅
    • 데이터 암호화(at rest, in transit)
    • 감사 로그 설정 및 분석 방법

[9] MongoDB 모니터링 및 운영 관리

  • 모니터링 툴 활용
    • MongoDB Compass, MongoDB Ops Manager, Percona Monitoring and Management (PMM)
    • Prometheus 및 Grafana를 통한 커스터마이징
  • 운영 트러블슈팅
    • Disk 사용량, 메모리 관리, CPU 병목 분석
    • slow operation 분석 및 개선 방법

[10] 클라우드 환경의 MongoDB

  • MongoDB Atlas (DBaaS)
    • Atlas에서의 관리, 모니터링, 백업
  • 클라우드 환경에서의 운영
    • AWS, Azure, GCP에서의 배포 및 운영 전략
    • 클라우드 마이그레이션 전략(Oracle에서 MongoDB Atlas로 마이그레이션)

 

📚추천 교재 및 공식 문서

 

 

🙌 댓글, 공감, 공유는 큰 힘이 됩니다! 😄

 

 

 


PostgreSQL (Write-Ahead Logging) 관리 방법 

 

 

💡 PostgreSQL WAL 파일은 무엇일까?

 

  • 모든 변경 작업(INSERT, UPDATE, DELETE)은 먼저 WAL 로그에 기록된 후 디스크에 반영된다
    그래서 WAL은 "Write-Ahead Logging" 이다.
  • PostgreSQL은 장애 발생 시 WAL을 Replay하여 복구한다

 

 

✅ 1. 복제 slot 정리

  • 사용하지 않는 replication slot을 반드시 DROP
  • 비활성 slot은 WAL 파일을 무한 보존하게 되어 Disk 공간 낭비를 만듬
-- replication slot 별 WAL 차이 확인
SELECT slot_name, restart_lsn, pg_current_wal_lsn(), 
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_lag
FROM pg_replication_slots;

-- 현재 디스크상 WAL 세그먼트 개수
SELECT COUNT(*) FROM pg_ls_dir('pg_wal') WHERE pg_ls_dir ~ '^[0-9A-F]{24}$';

SELECT pg_drop_replication_slot('slot_name');
Replication운영 중에 비 정상적인 발생으로 나타나거나, Migration 으로 인해 발생되는 현상 

✅ 2. Checkpoints 튜닝

  • WAL 삭제는 checkpoint 이후 가능하기 때문에, checkpoint 간격이 너무 길면 WAL이 오래 쌓일 수도 있음.
postgresql.conf 설정

checkpoint_timeout = 10min # 기본값 5min
checkpoint_completion_target = 0.9 # 디스크 부하를 줄이기 위해 천천히 completion 진행

✅ 3. WAL 보존 크기 제한 설정

  • PostgreSQL 13 이상에서는 WAL 파일 제한 parameter 존재 
postgresql.conf 설정

wal_keep_size = 512MB                -- WAL 최소 보존 크기
max_wal_size = 1GB                   -- WAL 최대 크기 초과 시 강제 checkpoint
min_wal_size = 80MB                  -- checkpoint 이후 보존할 최소 WAL
max_slot_wal_keep_size = 1GB         -- slot WAL 보존 최대치
 
💡 max_slot_wal_keep_size는 복제 slot의 WAL 누적을 제한하여 비활성 slot 문제를 방지 할 수 있음.

✅ 5. WAL 정리 수동 삭제

  • 수동으로 WAL 정리하고 싶다면 checkpoint 강제 트리거도 가능
-- 해당 기능을 무조건 수행한다고 해서 WAL파일이 정리되진 않는다.
CHECKPOINT;

 

 

🚨 여기서 주의할 점

  • pg_wal을 직접 지우면 절대 안 됩니다 ❌ 진짜 조심... 😨
  • 수동으로 파일 삭제하면 PostgreSQL이 부팅 실패하거나 데이터 손실 가능성 있습니다.

 

 

참조 : https://blog.ex-em.com/1817

 

🙌 댓글, 공감, 공유는 큰 힘이 됩니다! 😄

 

 

PostgreSQL에서의 RBO, CBO 과연 어떤 방식을 채택할까? 🤔

 

 

🔍 용어 정리

RBO (Rule-Based Optimizer) - 사람이 정의한 규칙 기반으로 쿼리 실행 계획을 고름
CBO (Cost-Based Optimizer) - 비용 기반으로 통계, 행 수 등을 고려해서 최적 계획 선택

 

 

PostgreSQL 얘기 전에 앞서

Oracle 에서는 8i 이하 버전에서 RBO를 채택하였고, 9i 버전에서는 CBO를 본격적으로 도입해 사용했다.

11g 버전이후 부터 RBO는 더 이상 사용이 불가능하게 되었고 현재까지 CBO 방식으로 사용하고 있다.

 

 

 

PostgreSQL은 RBO와 CBO 어떤 방식을 채택했을까

 

→ 답은 CBO 방식이다.

 

애초에 RBO를 공식적으로 채택해서 사용한 적이 없었다.

PostgreSQL 최초 버전인 6.0 버전(1997년)부터 CBO를 채택하여 사용하였다. 

 

그럼, PostgreSQL은 CBO 비용 계산 기준 요소는 다음과 같다.

항목 설명 기본값(postgresql.conf)
seq_page_cost 순차 I/O 1페이지 비용 1.0
random_page_cost 랜덤 I/O 1페이지 비용 4.0 (디스크 기준, SSD는 낮춤)
cpu_tuple_cost 튜플 한 개 처리 비용 0.01
cpu_index_tuple_cost 인덱스 튜플 처리 비용 0.005
cpu_operator_cost 연산자 1회 비용 0.0025
통계정보 ANALYZE로 수집된 통계 (row count, NDV 등) pg_statistic 뷰에 저장
해당 Cost들을 옵티마이저가 계산하여  Plan을 결정함.

 

 

위 Cost들을 기반으로, 어떤 실행계획을 수행할지 옵티마이저가 판단하고 옵티마이저는 DB버전이 올라갈 수록 성능이 뛰어날 수도 있다.(오히려 잘못된 Plan을 타는 경우도 간혹 있음) 따라서 Version에 따라 쿼리 수행 검증은 필수적으로 필요하며 개발자와 DBA간의 원활한 소통으로 검증 방식을 적절한 방법에 타협을 해야 한다.

 

 

PostgreSQL은 다양한 실행 계획을 생성해서 비교하고 가장 효율적인 것을 선택한다.

항목 설명
Seq Scan 테이블의 모든 튜플(행)을 순차적으로 스캔
→ 인덱스를 사용하지 않고 디스크 블록을 순서대로 읽음
Index Scan B-Tree 등의 인덱스를 사용해 필터 조건에 맞는 row만 빠르게 조회
Bitmap Heap Scan 여러 인덱스 조건을 비트맵으로 결합한 후, 실제 테이블에서 필요한 블록만 읽음
Nested Loop 하나의 테이블을 반복하면서 다른 테이블을 반복적으로 참조
→ 가장 기본적인 조인 알고리즘
Merge Join 두 입력 테이블이 정렬되어 있을 때 병렬적으로 병합
→ 마치 merge sort처럼 두 집합을 합쳐 조인
Hash Join 한 테이블을 메모리에 올려 해시 테이블로 만들고, 다른 테이블의 조인 키로 검색
옵티마이저는 최적의 실행 계획을 Cost가 적은 방법을 선택한다.

 

 

그럼 잘못된 Plan을 타는 경우에는 어떻게 해야될까?

Oracle은 힌트를 사용하면 되지만, PostgreSQL에서는 공식적으로 힌트를 지원하지 않는다.

그치만, 외부 확장 모듈인 pg_hint_plan를 사용하면 힌트를 사용할 수 있다. 

 

🤔 pg_hint_plan이란?

  • PostgreSQL에서 SQL 문 내에 힌트를 작성하여 쿼리 실행 계획을 강제하는 확장(extension) 기능
  • Oracle의 /*+ HINT */ 문법과 유사하게 동작함
  • PostgreSQL 공식 패키지는 아니지만, OSS community에서 활발히 유지보수 중 (by OSS Japan)
PostgreSQL에서 공식적으로 지원하지 않는 기능이지만, 상황이 불가피 하다면 사용을 막을 순 없을 것 같다.
애초에 테이블 설계와 적절한 인덱스를 생성해서 힌트를 기피하는 방법이 최선이 아닐까 싶다.  

 

 

 

🙌 댓글, 공감, 공유는 큰 힘이 됩니다! 😄

 

 

 

 

운영 중 발생 가능한 🧨Block Corruption 을 대응(해결)하기 위해 테스트 후 분석하는 글입니다. 



OS환경 : Rocky 8.10  (64bit)
DB 환경 : PostgreSQL 17.4
테스트 DB: mydb 
데이터 디렉토리: /postgres_data/test
로그 디렉토리: /postgres_log/test

🛠️ 초기 환경 셋업: Checksums 기능 활성화

PostgreSQL은 페이지 단위로 블록의 무결성을 검사하기 위해 page-level checksum 기능이 존재하며 
활성화 시 블록 손상 시 PostgreSQL이 감지할 수 있습니다.

✅ 기존 데이터 디렉토리에 Checksums 활성화

# checksums 확인
show data_checksums;

 data_checksums
----------------
 off
(1 row)


# PostgreSQL 인스턴스 중지
pg_ctl stop

# checksums enable
pg_checksums --enable -D /postgres_data/test

# PostgreSQL 재시작
pg_ctl start

# checksums 확인
show data_checksums;
 
 data_checksums
----------------
 on
(1 row)

 


🧪 Block Corruption 테스트 시작

📦 테이블 Block Corruption CASE

📌 테스트 테이블 생성

CREATE DATABASE mydb;


CREATE TABLE corrupted_table (
    id SERIAL PRIMARY KEY,
    data TEXT
);


INSERT INTO corrupted_table (data)
SELECT 'Row #' || generate_series(1, 15000);


SELECT COUNT(*) FROM corrupted_table;
 count
-------
 15000
(1 row)

📂 테이블 데이터 파일 경로 찾기

# 데이터베이스 oid 확인
SELECT oid FROM pg_database WHERE datname = 'mydb';

  oid
-------
 17015
(1 row)


# 테이블 relfilenode 확인
SELECT relfilenode FROM pg_class WHERE relname = 'corrupted_table';

 relfilenode
-------------
       17027
(1 row)


-- 편하게 relfilenode 조회
SELECT pg_relation_filepath('corrupted_table');
pg_relation_filepath
----------------------
 base/17015/17027
(1 row)

-- 손상시킬 영역 확인
-- 각 row의 block 위치 확인(37 Block)
mydb=# SELECT ctid, id FROM corrupted_table WHERE id BETWEEN 7000 AND 7500 LIMIT 100;
   ctid   |  id
----------+------
 (37,155) | 7000
 (37,156) | 7001
 (37,157) | 7002
 (37,158) | 7003
 (37,159) | 7004
 (37,160) | 7005
 (37,161) | 7006
 (37,162) | 7007
 (37,163) | 7008
 (37,164) | 7009
 (37,165) | 7010
 (37,166) | 7011
 (37,167) | 7012
 (37,168) | 7013
 (37,169) | 7014

 ※ 예시 경로: /postgres_data/test/base/<database_oid>/<relfilenode>

💥 Block Corruption 시뮬레이션

-- DB종료
pg_ctl stop

-- dd로 Block값 zere로 밀어넣기
dd if=/dev/zero of=$PGDATA/base/17015/17027 bs=1 seek=$((37 * 8192 + 24)) count=512 conv=notrunc

-- DB시작
pg_ctl start

 

옵션 설명
dd 데이터를 블록 단위로 복사하는 명령어
if=/dev/urandom 입력 파일 (input file)로, 난수 데이터를 생성하는 특수 디바이스
of=$PGDATA/base/16949/16951 출력 파일 (output file). PostgreSQL 테이블의 실제 데이터 파일일 가능성이 높음
bs=8192 블록 크기 (bytes per block). 8KB는 PostgreSQL의 기본 페이지 크기와 동일
seek=50 출력 파일에서 50 블록(= 50 * 8192 bytes = 409600 bytes) 만큼 건너뛰고 데이터 쓰기 시작
count=1 1 블록(8KB)만 복사
conv=notrunc 출력 파일을 자르지 않고 유지. 기존 파일 크기를 줄이지 않음

🧨 Block Corruption 확인

-- 손상된 영역 접근하기
SELECT * FROM corrupted_table WHERE ctid >= '(37,0)' AND ctid < '(39,0)';
WARNING:  page verification failed, calculated checksum 43097 but expected 48841
ERROR:  invalid page in block 37 of relation base/17015/17027



-- 손상되지 않는 영역 접근하기
mydb=# SELECT * FROM corrupted_table WHERE ctid >= '(1,0)' AND ctid < '(10,0)';
  id  |   data
------+-----------
  186 | Row #186
  187 | Row #187
  188 | Row #188
  189 | Row #189
  190 | Row #190
  191 | Row #191
  192 | Row #192
  193 | Row #193
  194 | Row #194
  195 | Row #195
  196 | Row #196
  197 | Row #197
  198 | Row #198
  199 | Row #199
  200 | Row #200
  201 | Row #201
  202 | Row #202
  203 | Row #203
  204 | Row #204
  205 | Row #205
  206 | Row #206
  207 | Row #207
  208 | Row #208
  209 | Row #209
  210 | Row #210
  211 | Row #211
  212 | Row #212
  213 | Row #213
손상된 영역은 접근 불가, 손상 되지 않은 영역은 조회 가능  

 

🤔 Block Corruption 분석

\c mydb

CREATE EXTENSION IF NOT EXISTS pageinspect;

SELECT * FROM heap_page_items(get_raw_page('corrupted_table', 37));
WARNING:  page verification failed, calculated checksum 43097 but expected 48841
ERROR:  invalid page in block 37 of relation base/17015/17027


SELECT * FROM heap_page_items(get_raw_page('corrupted_table', 36));
 lp  | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 |  t_ctid  | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |             t_data
-----+--------+----------+--------+--------+--------+----------+----------+-------------+------------+--------+--------+-------+--------------------------------
   1 |   8152 |        1 |     38 |   1042 |      0 |        0 | (36,1)   |           2 |       2306 |     24 |        |       | \x051a000015526f77202336363631
   2 |   8112 |        1 |     38 |   1042 |      0 |        0 | (36,2)   |           2 |       2306 |     24 |        |       | \x061a000015526f77202336363632
   3 |   8072 |        1 |     38 |   1042 |      0 |        0 | (36,3)   |           2 |       2306 |     24 |        |       | \x071a000015526f77202336363633
   4 |   8032 |        1 |     38 |   1042 |      0 |        0 | (36,4)   |           2 |       2306 |     24 |        |       | \x081a000015526f77202336363634
   5 |   7992 |        1 |     38 |   1042 |      0 |        0 | (36,5)   |           2 |       2306 |     24 |        |       | \x091a000015526f77202336363635
   6 |   7952 |        1 |     38 |   1042 |      0 |        0 | (36,6)   |           2 |       2306 |     24 |        |       | \x0a1a000015526f77202336363636
   7 |   7912 |        1 |     38 |   1042 |      0 |        0 | (36,7)   |           2 |       2306 |     24 |        |       | \x0b1a000015526f77202336363637
   8 |   7872 |        1 |     38 |   1042 |      0 |        0 | (36,8)   |           2 |       2306 |     24 |        |       | \x0c1a000015526f77202336363638
   9 |   7832 |        1 |     38 |   1042 |      0 |        0 | (36,9)   |           2 |       2306 |     24 |        |       | \x0d1a000015526f77202336363639
.... 중략


-- pg_amcheck
\c mydb
CREATE EXTENSION amcheck;
$ pg_amcheck -d mydb -t public.corrupted_table --no-strict-names --heapallindexed --verbose

pg_amcheck: including database "mydb"
pg_amcheck: in database "mydb": using amcheck version "1.4" in schema "public"
pg_amcheck: checking heap table "mydb.public.corrupted_table"
WARNING:  XX001: page verification failed, calculated checksum 43097 but expected 48841
LOCATION:  PageIsVerifiedExtended, bufpage.c:153
heap table "mydb.public.corrupted_table":
    ERROR:  XX001: invalid page in block 37 of relation base/17015/17027
    LOCATION:  WaitReadBuffers, bufmgr.c:1542
query was: SELECT v.blkno, v.offnum, v.attnum, v.msg FROM pg_catalog.pg_class c, "public".verify_heapam(
relation := c.oid, on_error_stop := false, check_toast := true, skip := 'none'
) v WHERE c.oid = 17027 AND c.relpersistence != 't'
pg_amcheck: checking btree index "mydb.public.corrupted_table_pkey"
WARNING:  XX001: page verification failed, calculated checksum 43097 but expected 48841
LOCATION:  PageIsVerifiedExtended, bufpage.c:153
btree index "mydb.public.corrupted_table_pkey":
    ERROR:  XX001: invalid page in block 37 of relation base/17015/17027
    LOCATION:  WaitReadBuffers, bufmgr.c:1542
query was: SELECT "public".bt_index_check(index := c.oid, heapallindexed := true )
FROM pg_catalog.pg_class c, pg_catalog.pg_index i WHERE c.oid = 17033 AND c.oid = i.indexrelid AND c.relpersistence != 't' AND i.indisready AND i.indisvalid AND i.indislive
pg_amcheck: checking btree index "mydb.pg_toast.pg_toast_17027_index"
pg_amcheck: checking heap table "mydb.pg_toast.pg_toast_17027"


-- PostgreSQL Server Log
2025-04-22 01:01:18 UTC postgres@[local] /mydb (2025625)WARNING:  page verification failed, calculated checksum 43097 but expected 48841
2025-04-22 01:01:18 UTC postgres@[local] /mydb (2025625)ERROR:  invalid page in block 37 of relation base/17015/17027
✔️ Checksum mismatch
calculated checksum 43097 but expected 48841
→ 손상된 블록의 헤더는 살아 있지만, 내용이 손상되어 checksum 검증 실패

✔️ Invalid Page
invalid page in block 37 of relation base/17015/17027
→ PostgreSQL이 해당 블록을 읽을 수 없음 또는 구조적으로 무결하지 않음

 

🚑 테이블 복구 진행

-- 복구 전 최종 확인, 해당 Block은 접근 불가
SELECT ctid, * FROM corrupted_table WHERE ctid >= '(37,0)' AND ctid < '(38,0)';
WARNING:  page verification failed, calculated checksum 43097 but expected 48841
ERROR:  invalid page in block 37 of relation base/17015/17027


-- 백업 본이 없다는 가정하에 진행
-- 해당 블록은 데이터 유실로 가정하여 복구
CREATE TABLE corrupted_table_recovered AS
SELECT * FROM corrupted_table
WHERE ctid >= '(0,0)' AND ctid < '(37,0)';

INSERT INTO corrupted_table_recovered
SELECT * FROM corrupted_table
WHERE ctid >= '(38,0)';


-- 블록 확인
SELECT pg_relation_size('corrupted_table') / 8192 AS blocks;
 blocks
--------
     82
(1 row)

SELECT pg_relation_size('corrupted_table_recovered') / 8192 AS blocks;
 blocks
--------
     81
(1 row)
1개 Block Corrupt로인해 recovery Block 1개 유실

 

🚑 테이블 복구 진행 2

-- 테이블 조회 시도
SELECT * FROM corrupted_table;
WARNING:  page verification failed, calculated checksum 25081 but expected 18907
ERROR:  invalid page in block 36 of relation base/17015/17123

SELECT * FROM corrupted_table limit 1;
 id |  data
----+--------
  1 | Row #1
(1 row)


-- 해당 기능 on
show zero_damaged_pages;
 zero_damaged_pages
--------------------
 off
(1 row)

--Parameter 적용하기
ALTER SYSTEM SET zero_damaged_pages=on;


SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

show zero_damaged_pages;
 zero_damaged_pages
--------------------
 on
(1 row)


-- Block은 손상 났지만 조회는 가능함
SELECT * FROM corrupted_table;
WARNING:  page verification failed, calculated checksum 25081 but expected 18907
WARNING:  invalid page in block 36 of relation base/17015/17123; zeroing out page
  id   |    data
-------+------------
     1 | Row #1
     2 | Row #2
     3 | Row #3
     4 | Row #4
     5 | Row #5
     6 | Row #6
     7 | Row #7
     8 | Row #8
     9 | Row #9
    10 | Row #10
    11 | Row #11
    12 | Row #12
    13 | Row #13
    14 | Row #14
    15 | Row #15

-- 복구 진행
CREATE TABLE corrupted_table_recovered AS
SELECT * FROM corrupted_table;
Block Corruption Error 발생 했지만, 해당 테이블 조회는 가능함.

 

🧪 추가 테스트 VACUUM 진행 

-- 테이블 조회
select * from corrupted_table;
WARNING:  page verification failed, calculated checksum 30546 but expected 10092
ERROR:  invalid page in block 35 of relation base/17015/17123

-- VACUUM 수행, 정상 or 비정상
mydb=# vacuum corrupted_table;
VACUUM
PostgreSQL의 VACUUM은 "page skipping" 이라는 최적화 기법 사용하는데 해당 35번 Block을 건너 뛴 것으로 예상됨
건너 뛰지 않도록 테스트 수행

 

-- zero_damaged_pages=on
-- VACUUM이 모든 블록을 강제로 방문하게 만들고, 로그로 출력함
VACUUM (DISABLE_PAGE_SKIPPING, VERBOSE) corrupted_table;
INFO:  aggressively vacuuming "mydb.public.corrupted_table"
INFO:  finished vacuuming "mydb.public.corrupted_table": index scans: 0
pages: 0 removed, 82 remain, 82 scanned (100.00% of total)
tuples: 0 removed, 14630 remain, 0 are dead but not yet removable
removable cutoff: 1073, which was 0 XIDs old when operation ended
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 0.000 MB/s
buffer usage: 176 hits, 0 misses, 0 dirtied
WAL usage: 0 records, 0 full page images, 0 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s


-- zero_damaged_pages=off
VACUUM (DISABLE_PAGE_SKIPPING, VERBOSE) corrupted_table;
INFO:  aggressively vacuuming "mydb.public.corrupted_table"
WARNING:  page verification failed, calculated checksum 30546 but expected 10092
ERROR:  invalid page in block 35 of relation base/17015/17123
CONTEXT:  while scanning block 35 of relation "public.corrupted_table"
DISABLE_PAGE_SKIPPING 처리하여 VACUUM이 모든 블록을 강제로 방문하게 만들어서 Error 발생 시킴
결국 Block Corruption 난 TABLE은 VACUUM이 수행이 안될 수도 있다.

 

🧩 INDEX Block Corruption CASE

🔎 Index  대상 확인

-- 이전 작업 초기화
\c mydb
DROP TABLE IF EXISTSE corrupted_table;

CREATE TABLE corrupted_table (
    id SERIAL PRIMARY KEY,
    data TEXT
);


INSERT INTO corrupted_table (data)
SELECT 'Row #' || generate_series(1, 15000);


SELECT COUNT(*) FROM corrupted_table;
 count
-------
 15000
(1 row)


-- 인덱스 relfilenode 확인
SELECT relname, relfilenode FROM pg_class
WHERE relname = 'corrupted_table_pkey';
       relname        | relfilenode
----------------------+-------------
 corrupted_table_pkey |       17119
(1 row)


--블록 갯수 확인 
 SELECT pg_relation_size('corrupted_table_pkey') / 8192 AS blocks;
 blocks
--------
     43
(1 row)

💣 인덱스 블록 손상

# DB 중지
pg_ctl -D $PGDATA stop

# 인덱스 파일의 블록 2번 손상 (헤더 포함 전부 덮어쓰기)
dd if=/dev/urandom of=$PGDATA/base/17015/17119 bs=8192 seek=2 count=1 conv=notrunc

# DB시작
pg_ctl -D $PGDATA start

🧨 인덱스 사용 강제 & 에러 유발

\c mydb

-- PRIMARY KEY는 id니까, 이 쿼리에서 인덱스가 쓰일 것
mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 1000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.042..0.043 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 1000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.078 ms
 Execution Time: 0.057 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 2000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.035..0.036 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 2000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.057 ms
 Execution Time: 0.049 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 3000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.068..0.069 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 3000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.056 ms
 Execution Time: 0.082 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 4000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.047..0.047 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 4000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.057 ms
 Execution Time: 0.060 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 5000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.065..0.068 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 5000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.116 ms
 Execution Time: 0.093 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 6000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.044..0.045 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 6000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.055 ms
 Execution Time: 0.059 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 7000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.038..0.039 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 7000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.060 ms
 Execution Time: 0.053 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 8000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.043..0.044 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 8000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.060 ms
 Execution Time: 0.058 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 9000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.036..0.037 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 9000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.055 ms
 Execution Time: 0.051 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 11000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.043..0.044 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 11000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.057 ms
 Execution Time: 0.057 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 12000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.038..0.039 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 12000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.067 ms
 Execution Time: 0.054 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 13000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.044..0.045 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 13000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.064 ms
 Execution Time: 0.058 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 14000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.054..0.055 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 14000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.057 ms
 Execution Time: 0.069 ms
(7 rows)

mydb=# EXPLAIN (ANALYZE, VERBOSE)
SELECT id FROM corrupted_table WHERE id = 15000;
                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using corrupted_table_pkey on public.corrupted_table  (cost=0.29..4.30 rows=1 width=4) (actual time=0.039..0.039 rows=1 loops=1)
   Output: id
   Index Cond: (corrupted_table.id = 15000)
   Heap Fetches: 0
 Query Identifier: 3834585159638330129
 Planning Time: 0.056 ms
 Execution Time: 0.053 ms
(7 rows)

✔ 모든 Index Only Scan을 하였지만, 에러 확인 되지 않음 



-- pg_amcheck 조회
$ pg_amcheck -d mydb -t public.corrupted_table --no-strict-names --heapallindexed --verbose
pg_amcheck: including database "mydb"
pg_amcheck: in database "mydb": using amcheck version "1.4" in schema "public"
pg_amcheck: checking heap table "mydb.public.corrupted_table"
pg_amcheck: checking btree index "mydb.public.corrupted_table_pkey"
WARNING:  XX001: page verification failed, calculated checksum 13516 but expected 50609
LOCATION:  PageIsVerifiedExtended, bufpage.c:153
btree index "mydb.public.corrupted_table_pkey":
    ERROR:  XX001: invalid page in block 2 of relation base/17015/17119
    LOCATION:  WaitReadBuffers, bufmgr.c:1542
query was: SELECT "public".bt_index_check(index := c.oid, heapallindexed := true )
FROM pg_catalog.pg_class c, pg_catalog.pg_index i WHERE c.oid = 17119 AND c.oid = i.indexrelid AND c.relpersistence != 't' AND i.indisready AND i.indisvalid AND i.indislive
pg_amcheck: checking btree index "mydb.pg_toast.pg_toast_17113_index"
pg_amcheck: checking heap table "mydb.pg_toast.pg_toast_17113"



-- 확장 설치
CREATE EXTENSION IF NOT EXISTS pageinspect;

-- block 2의 인덱스 내용 분석
SELECT * FROM bt_page_items(get_raw_page('corrupted_table_pkey', 2));
WARNING:  page verification failed, calculated checksum 13516 but expected 50609
ERROR:  invalid page in block 2 of relation base/17015/17119
Index Only Scan을 하였지만 에러 발생되지 않았고,   해당 블록을 직접 조회 확인하면 에러를 확인 할 수 있다,
아마 Index Only Scan 시에 장애난 블록을 건드리지 못한 모양이다.

🛠️ 인덱스 복구(인덱스 복구전 통계 수집 → 인덱스 복구)  

-- 인덱스 통계 확인
mydb=# SELECT *
mydb-# FROM pg_stat_user_indexes
mydb-# WHERE indexrelname = 'corrupted_table_pkey';
 relid | indexrelid | schemaname |     relname     |     indexrelname     | idx_scan |         last_idx_scan         | idx_tup_read | idx_tup_fetch
-------+------------+------------+-----------------+----------------------+----------+-------------------------------+--------------+---------------
 17113 |      17119 | public     | corrupted_table | corrupted_table_pkey |       16 | 2025-04-22 01:32:31.520818+00 |           16 |             0

idx_scan	인덱스가 몇 번 사용됐는지 (스캔 수)
idx_tup_read	인덱스에서 읽힌 튜플 수
idx_tup_fetch	인덱스 통해 heap에서 실제로 읽은 튜플 수



-- 통계 정보 최신화
ANALYZE corrupted_table;

-- block 2의 인덱스 내용 분석
mydb=#  SELECT * FROM bt_page_items(get_raw_page('corrupted_table_pkey', 2));
WARNING:  page verification failed, calculated checksum 13516 but expected 50609
ERROR:  invalid page in block 2 of relation base/17015/17119

-- 인덱스 relfilenode 확인
SELECT relname, relfilenode FROM pg_class
WHERE relname = 'corrupted_table_pkey';
       relname        | relfilenode
----------------------+-------------
 corrupted_table_pkey |       17119
(1 row)


--REINDEX 수행
REINDEX INDEX corrupted_table_pkey;

-- block 2의 인덱스 내용 분석
SELECT * FROM bt_page_items(get_raw_page('corrupted_table_pkey', 2));
 itemoffset |  ctid   | itemlen | nulls | vars |          data           | dead |  htid   | tids
------------+---------+---------+-------+------+-------------------------+------+---------+------
          1 | (3,1)   |      16 | f     | f    | dd 02 00 00 00 00 00 00 |      |         |
          2 | (1,182) |      16 | f     | f    | 6f 01 00 00 00 00 00 00 | f    | (1,182) |
          3 | (1,183) |      16 | f     | f    | 70 01 00 00 00 00 00 00 | f    | (1,183) |
          4 | (1,184) |      16 | f     | f    | 71 01 00 00 00 00 00 00 | f    | (1,184) |
          5 | (1,185) |      16 | f     | f    | 72 01 00 00 00 00 00 00 | f    | (1,185) |
          6 | (2,1)   |      16 | f     | f    | 73 01 00 00 00 00 00 00 | f    | (2,1)   |
          7 | (2,2)   |      16 | f     | f    | 74 01 00 00 00 00 00 00 | f    | (2,2)   |
          8 | (2,3)   |      16 | f     | f    | 75 01 00 00 00 00 00 00 | f    | (2,3)   |
          9 | (2,4)   |      16 | f     | f    | 76 01 00 00 00 00 00 00 | f    | (2,4)   |
         10 | (2,5)   |      16 | f     | f    | 77 01 00 00 00 00 00 00 | f    | (2,5)   |


-- 인덱스 relfilenode 확인
SELECT relname, relfilenode FROM pg_class
mydb-# WHERE relname = 'corrupted_table_pkey';
       relname        | relfilenode
----------------------+-------------
 corrupted_table_pkey |       17121
(1 row)
통계정보를 재 수집해도 인덱스는 복구되지 않는다, 이미 해당 인덱스파일이 손상된 상태이기에 불가능 하였고
reindex 시에 refilenode가 바뀐 것을 확인 할 수 있다.   즉, 새로운 파일이 만들어 졌음.

 

 


🧱 Datafile Header Corruption

PostgreSQL Datafile Header는 데이터 파일내에 블록들을 해석하고 관리하기 위해 필요한 메타정보를 가지고 있습니다. Header 손상 시 PostgreSQL은 파일 자체를 인식하지 못할 수도 있으며 데이터 유실이 발생될 수 있습니다.

 

📌 Page Header (PageHeaderData)의 구조

필드 크기 설명
pd_lsn 8 bytes WAL 로그 시퀀스 번호 (Last modification LSN)
pd_checksum 2 bytes 블록의 체크섬 (data_checksum 설정 시 사용)
pd_flags 2 bytes 플래그 비트 (예: PD_HAS_FREE_LINES 등)
pd_lower 2 bytes ItemIdData 영역 끝 위치
pd_upper 2 bytes HeapTupleData 시작 위치 (Free Space 시작점)
pd_special 2 bytes Special space 시작 위치 (예: 인덱스에서 사용)
pd_pagesize_version 2 bytes 페이지 크기 & 포맷 버전 정보
pd_prune_xid 4 bytes VACUUM 시 필요한 XID (가장 오래된 삭제 필요 트랜잭션)

 

💥 Header Corruption 시뮬레이션

-- relfilenode 조회
SELECT pg_relation_filepath('corrupted_table');
pg_relation_filepath
----------------------
 base/17015/17113
(1 row)


-- db 종료
pg_ctl stop

-- currupt 수행
dd if=/dev/zero of=$PGDATA/base/17015/17113 bs=24 count=1 conv=notrunc

-- db 시작
pg_ctl start

🚨 조회 시 에러

SELECT * FROM corrupted_table LIMIT 1;
ERROR:  invalid page in block 0 of relation base/17015/17113
ERROR: invalid page header in block 0 of relation base/17015/17113

⚠️ 복구가 불가능한 상태, 물리/논리 백업으로 만 복구 가능


📌 관리 포인트 정리

  • initdb 시 반드시 --data-checksums 옵션을 활성화 하기
  • pg_checksums 명령어로 정기PM 시에 체크를 수행하기, DB 정상 종료 시 체크 가능
  • 정기적인 pg_dump & pg_basebackup 백업하기
  • Log 모니터링 및 기타 모니터링 솔루션 관제하기 

 

 

🙌 댓글, 공감, 공유는 큰 힘이 됩니다! 😄

 

참조 : https://postgresql.kr/docs/13/runtime-config-developer.html

+ Recent posts