(KO) PostgreSQL 캐시 무효화 — sinval 메시지 큐, inval.c 디스패처, 트랜잭션 지연 전송
목차
이론적 배경
섹션 제목: “이론적 배경”카탈로그 튜플, 릴레이션 디스크립터, 파싱된 플랜 트리를 캐시하는 데이터베이스 엔진은 매번 디스크에 접근하는 방식에 비해 조회 속도를 몇 차수나 높일 수 있다. 그 대가는 일관성이다. 다른 세션이 어떤 테이블의 컬럼 이름을 바꿨는데 스스로는 낡은 캐시 항목을 갖고 있는 백엔드는 이전 스키마를 그대로 본다. 멀티프로세스 엔진은 메타데이터 변경을 해당 항목의 사본을 보유할 수 있는 모든 캐시에 전파하는 문제를 반드시 해결해야 한다.
Database System Concepts (Silberschatz, 7판, 25장 §“Buffer Management”)는 캐시 무효화를 분산 일관성 문제로 규정하고, 거의 모든 엔진이 두 가지 설계 선택으로 이 문제를 단순화한다고 설명한다. 첫째, 캐시는 공유가 아닌 프로세스별로 유지되므로 통보 대상의 수는 세션 수로 제한된다. 둘째, 무효화는 즉각적인 방식(eager)이 아니라 **지연 방식(lazy)**으로 이루어진다. 커밋하는 세션은 무엇이 바뀌었는지를 브로드캐스트하고, 수신하는 세션은 다음 안전한 시점에 낡은 사본을 버린다. 이 구조는 DDL을 모든 활성 세션에 블로킹하는 비용을 없앤다.
Database Internals (Petrov, 6장 §“System Catalogs”)는 즉각 무효화에서 지연 무효화로의 전환이 정확성 제약을 도입한다고 지적한다. 수신 백엔드는 다른 세션의 메시지를 처리하기 전에도 자신의 쿼리에 낡은 캐시 항목을 돌려주어서는 안 된다는 점이다. 이것이 명령 경계 지연 문제다. 트랜잭션 안에서 백엔드 자신의 카탈로그 변경도 로컬 무효화 경로를 거쳐야 백엔드가 자신의 DDL을 즉시 볼 수 있다.
지연 무효화 시스템의 형태를 결정하는 설계 축은 두 가지다.
-
브로드캐스트 매체. 채널이 백엔드별 시그널(푸시)인가, 모든 백엔드가 폴링하는 공유 링 버퍼(풀)인가, 아니면 그 조합인가. 순수 푸시 방식은 즉시 전달되지만 커밋마다 세션 수
N에 비례하는 시그널이 필요하다. 순수 풀 방식은 시그널이 필요 없지만 느린 백엔드가 임의로 뒤처져 오래된 메시지 회수를 막을 수 있다. 실제 시스템은 링 버퍼에 캐치업 시그널을 결합해 사용한다. -
무효화 메시지의 세분화 수준. 특정 캐시의 특정 튜플(해시 키 기준)을 지정하는가, 아니면 캐시 전체를 지정하는가. 세밀한 메시지는 선택적 축출을 가능케 하고, 거친 메시지(캐시 전체 리셋)는 생성·처리가 단순하며 버퍼 오버플로 복구에 사용된다. 실제 시스템은 일반 DDL에는 튜플별 메시지를, 오버플로 복구에는 전체 리셋을 함께 지원한다.
PostgreSQL은 4096슬롯 공유 링 버퍼(sinval)에 캐치업 인터럽트 메커니즘과 오버플로 대응용 전체 캐시 리셋을 결합한다. 무효화 디스패처(inval.c)는 트랜잭션 중 발생한 메시지를 수집해 브로드캐스트를 커밋 시까지 미루면서, 명령 경계에서는 로컬 캐시에 즉시 적용한다.
공통 DBMS 설계
섹션 제목: “공통 DBMS 설계”3단계 흐름: 생성 → 지연 → 브로드캐스트
섹션 제목: “3단계 흐름: 생성 → 지연 → 브로드캐스트”멀티프로세스 RDBMS에서 지연 무효화 시스템은 거의 예외 없이 같은 3단계 구조를 따른다.
1단계 — 생성. 카탈로그 튜플을 변경하는 코드(힙 삽입·갱신·삭제)는 즉시 무효화 등록 함수를 호출한다. 메시지는 아직 전송되지 않고 트랜잭션 버퍼에 누적된다. 이 구조는 무효화 생성을 변경 지점에 붙여두면서도 변경 지연 시간을 크로스프로세스 시그널링과 분리한다.
2단계 — 명령 로컬 적용. 각 명령 경계(CommandCounterIncrement() 이후)에서 현재 명령의 메시지를 로컬 캐시에만 적용한다. 변경 백엔드 자신의 다음 명령이 새 카탈로그 상태를 보아야 하기 때문이다. 메시지는 나중에 브로드캐스트하기 위해 트랜잭션 버퍼에 남아 있는다.
3단계 — 커밋 시 브로드캐스트. 트랜잭션 커밋 시 누적된 메시지 전체를 공유 매체(링 버퍼, 로그, 브로드캐스트 채널)에 푸시한다. 다른 백엔드는 다음 트랜잭션 시작 시 이 매체를 드레인한다. 중단(abort) 시에는 1단계 메시지를 버리고 2단계 적용분을 로컬에서 되돌린다.
catcache와 relcache의 무효화 순서
섹션 제목: “catcache와 relcache의 무효화 순서”튜플 수준 카탈로그 캐시(catcache)와 조립된 릴레이션 디스크립터 캐시(relcache)를 모두 가진 엔진은 catcache 무효화를 relcache 무효화보다 먼저 처리해야 한다. relcache 항목을 재빌드할 때 카탈로그 튜플을 읽는데, catcache가 여전히 낡은 튜플을 보유하고 있다면 재빌드된 relcache 항목이 처음부터 낡은 상태로 태어나기 때문이다. 따라서 올바른 순서는 catcache 먼저, relcache 나중이다.
오버플로 리셋을 갖춘 공유 링 버퍼
섹션 제목: “오버플로 리셋을 갖춘 공유 링 버퍼”프로세스 로컬 캐시 엔진에서 브로드캐스트 매체로 가장 보편적으로 쓰이는 것이 공유 메모리의 고정 크기 링 버퍼다. 핵심 불변 조건은 다음과 같다.
maxMsgNum은 생산자 커서(다음 쓰기 슬롯)다.minMsgNum은 전역 소비자 하한(이 이전은 어떤 독자도 필요하지 않음)이다.- 각 백엔드는 자신의
nextMsgNum커서를 갖는다. - 백엔드의
nextMsgNum이maxMsgNum보다MAXNUMMESSAGES슬롯 이상 뒤처지면 버퍼 내용이 이미 덮어쓰여진 것이다. 엔진은 해당 백엔드에 리셋 플래그를 설정하고, 백엔드가 다음에 메시지를 확인할 때 개별 메시지 처리 대신 캐시 전체를 버린다.
리셋은 정확성 안전망이다. 캐시 전체를 버리는 것은 언제나 안전하다. 다음 캐시 미스가 진실의 원천(시스템 카탈로그)에서 다시 로드하기 때문이다. 다만 리셋 이후 재로드 폭풍이 발생할 수 있으므로, 느린 백엔드에 캐치업 인터럽트를 보내 리셋이 일반 경로가 되지 않도록 예방한다.
등록 콜백
섹션 제목: “등록 콜백”플랜 캐시, 파티션 디스크립터 캐시, 이벤트 트리거 캐시처럼 catcache·relcache에서 파생된 상위 캐시를 유지하는 서브시스템은 입력이 무효화될 때 통보받아야 한다. 표준 패턴은 캐시별 콜백 레지스트리다. 서브시스템이 시작 시 함수 포인터를 등록하면, 무효화 디스패처가 관련 catcache·relcache 항목을 플러시할 때 그 함수를 호출한다. 이 구조는 디스패처가 상위 캐시를 직접 알 필요 없게 만든다.
이론 ↔ PostgreSQL 대응 표
섹션 제목: “이론 ↔ PostgreSQL 대응 표”| 이론 개념 | PostgreSQL 엔티티 |
|---|---|
| 무효화 메시지(튜플 수준, catcache) | id ≥ 0인 SharedInvalidationMessage (catcache ID) |
| 무효화 메시지(카탈로그 전체) | id == SHAREDINVALCATALOG_ID (-1) |
| 무효화 메시지(relcache) | id == SHAREDINVALRELCACHE_ID (-2) |
| 무효화 메시지(smgr·relmap·snapshot·relsync) | ID -3 ~ -6 |
| 공유 링 버퍼 | sinvaladt.c의 SISeg.buffer[MAXNUMMESSAGES] |
| 생산자 커서 | SISeg.maxMsgNum |
| 소비자 하한 | SISeg.minMsgNum |
| 백엔드별 커서 | ProcState.nextMsgNum |
| 리셋 플래그 | ProcState.resetState |
| 캐치업 시그널 | ProcState.signaled를 통한 PROCSIG_CATCHUP_INTERRUPT |
| 트랜잭션 내 누적 버퍼 | inval.c의 TransInvalidationInfo 체인 |
| 명령 로컬 적용 | CommandEndInvalidationMessages() |
| 커밋 브로드캐스트 | AtEOXact_Inval(isCommit=true) → SendSharedInvalidMessages |
| 중단 롤백 | AtEOXact_Inval(isCommit=false) → LocalExecuteInvalidationMessage |
| 서브시스템 콜백 | syscache_callback_list[] / relcache_callback_list[] |
PostgreSQL의 접근 방식
섹션 제목: “PostgreSQL의 접근 방식”메시지 타입
섹션 제목: “메시지 타입”sinval.h는 특수 메시지 타입에 음수 ID 여섯 개를 정의하고, 비음수는 catcache ID로 사용한다.
// SharedInvalidationMessage — src/include/storage/sinval.h#define SHAREDINVALCATALOG_ID (-1) /* 카탈로그 전체 플러시 */#define SHAREDINVALRELCACHE_ID (-2) /* relcache 항목 */#define SHAREDINVALSMGR_ID (-3) /* smgr 파일 참조 */#define SHAREDINVALRELMAP_ID (-4) /* 릴레이션 매퍼 */#define SHAREDINVALSNAPSHOT_ID (-5) /* 카탈로그 스냅샷 */#define SHAREDINVALRELSYNC_ID (-6) /* 논리 디코딩 relsync */SharedInvalidationMessage는 타입별 멤버 하나씩을 갖는 유니언이다. catcache 메시지(id ≥ 0)의 핵심 필드는 캐시 ID, 무효화된 키의 해시값, 데이터베이스 OID다. relcache 메시지의 핵심 필드는 데이터베이스 OID와 릴레이션 OID이며, InvalidOid는 relcache 전체 플러시를 의미한다.
inval.c의 누적 구조체
섹션 제목: “inval.c의 누적 구조체”inval.c는 TopTransactionContext에 할당된 두 개의 배열(catcache용, relcache용)과, 서브트랜잭션 중첩을 위한 TransInvalidationInfo 연결 리스트를 관리한다.
// TransInvalidationInfo — src/backend/utils/cache/inval.ctypedef struct TransInvalidationInfo{ struct InvalidationInfo ii; /* base: CurrentCmdInvalidMsgs + RelcacheInitFileInval */ InvalidationMsgsGroup PriorCmdInvalidMsgs; /* 이미 처리된 이전 명령 */ struct TransInvalidationInfo *parent; int my_level; /* 서브트랜잭션 중첩 깊이 */} TransInvalidationInfo;
// InvalidationMsgsGroup — 두 평면 배열에 대한 인덱스typedef struct InvalidationMsgsGroup{ int firstmsg[2]; /* [0]=CatCacheMsgs, [1]=RelCacheMsgs */ int nextmsg[2];} InvalidationMsgsGroup;두 배열은 복사되지 않는다. InvalidationMsgsGroup 구조체가 인덱스 범위만 갖는다. 따라서 서브트랜잭션의 메시지를 부모에 추가하는 것은 데이터 복사 없이 O(1) 인덱스 범위 조정으로 끝난다.
인플레이스 업데이트 경로(inplaceInvalInfo)는 pg_class.reltuples 같은 비트랜잭션 카탈로그 변경을 위한 별도 경로다. 같은 메시지 배열을 공유하지만 명령·트랜잭션 경계 로직을 우회하고, WAL 삽입 크리티컬 섹션 안에서 즉시 메시지를 전송한다.
카탈로그 변경 등록
섹션 제목: “카탈로그 변경 등록”heap_update나 heap_delete가 시스템 카탈로그 튜플을 변경하면 CacheInvalidateHeapTuple을 호출한다.
// CacheInvalidateHeapTuple — src/backend/utils/cache/inval.cvoidCacheInvalidateHeapTuple(Relation relation, HeapTuple tuple, HeapTuple newtuple){ CacheInvalidateHeapTupleCommon(relation, tuple, newtuple, PrepareInvalidationState);}공통 경로의 처리 단계는 다음과 같다.
- 비카탈로그 릴레이션이면 즉시 반환한다(
IsCatalogRelation확인). catcache.c의PrepareToInvalidateCacheTuple을 호출해 영향받는 catcache ID를 파악하고, ID별로RegisterCatcacheInvalidation을 호출해SharedInvalidationMessage를 큐에 추가한다.pg_class,pg_attribute,pg_index,pg_constraint(외래 키) 튜플이라면RegisterRelcacheInvalidation으로 소유 릴레이션의 relcache 플러시도 등록한다.- 릴레이션이 relcache 초기화 파일에 포함되어 있으면
RelcacheInitFileInval = true를 설정해 커밋 시 파일을 삭제한다.
// CacheInvalidateHeapTupleCommon (요약) — src/backend/utils/cache/inval.cstatic voidCacheInvalidateHeapTupleCommon(Relation relation, HeapTuple tuple, HeapTuple newtuple, InvalidationInfo *(*prepare_callback)(void)){ if (!IsCatalogRelation(relation)) return; if (IsToastRelation(relation)) return;
info = prepare_callback(); /* PrepareInvalidationState 또는 PrepareInplaceInvalidationState */
tupleRelId = RelationGetRelid(relation); if (RelationInvalidatesSnapshotsOnly(tupleRelId)) RegisterSnapshotInvalidation(info, databaseId, tupleRelId); else PrepareToInvalidateCacheTuple(relation, tuple, newtuple, RegisterCatcacheInvalidation, (void *) info);
/* pg_class / pg_attribute / pg_index / pg_constraint에 대한 relcache 플러시 */ if (tupleRelId == RelationRelationId) { relationId = ...; } else if (tupleRelId == AttributeRelationId) { relationId = ...; } else if (tupleRelId == IndexRelationId) { relationId = ...; } else if (tupleRelId == ConstraintRelationId) { /* FK만 */ } else return;
RegisterRelcacheInvalidation(info, databaseId, relationId);}명령 경계: 로컬 플러시
섹션 제목: “명령 경계: 로컬 플러시”각 CommandCounterIncrement() 호출 시 PostgreSQL은 CommandEndInvalidationMessages()를 호출한다. 현재 명령의 메시지를 로컬 캐시에만 적용한다. 아직 크로스프로세스 시그널은 없다.
// CommandEndInvalidationMessages — src/backend/utils/cache/inval.cvoidCommandEndInvalidationMessages(void){ if (transInvalInfo == NULL) return;
ProcessInvalidationMessages(&transInvalInfo->ii.CurrentCmdInvalidMsgs, LocalExecuteInvalidationMessage);
/* wal_level=logical이면 논리 디코딩을 위해 WAL에 기록 */ if (XLogLogicalInfoActive()) LogLogicalInvalidations();
AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs, &transInvalInfo->ii.CurrentCmdInvalidMsgs);}이 호출 이후 메시지는 CurrentCmdInvalidMsgs에서 PriorCmdInvalidMsgs로 이동한다. 같은 트랜잭션의 다음 명령을 위해 로컬 캐시가 최신 상태가 된다.
트랜잭션 종료: 브로드캐스트 또는 롤백
섹션 제목: “트랜잭션 종료: 브로드캐스트 또는 롤백”트랜잭션 커밋 시 AtEOXact_Inval(isCommit=true)는 다음 순서로 동작한다.
RelcacheInitFileInval이 설정되어 있으면RelationCacheInitFilePreInvalidate()를 호출해 초기화 파일을 삭제한다.CurrentCmdInvalidMsgs를PriorCmdInvalidMsgs에 추가한다.ProcessInvalidationMessagesMulti를SendSharedInvalidMessages처리기로 호출해 누적된 메시지 전체를SIInsertDataEntries로sinval버퍼에 푸시한다.RelcacheInitFileInval이 설정되어 있으면RelationCacheInitFilePostInvalidate()를 호출한다.
// AtEOXact_Inval (커밋 경로, 요약) — src/backend/utils/cache/inval.cvoidAtEOXact_Inval(bool isCommit){ // ... NULL 확인 ... if (isCommit) { if (transInvalInfo->ii.RelcacheInitFileInval) RelationCacheInitFilePreInvalidate();
AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs, &transInvalInfo->ii.CurrentCmdInvalidMsgs);
ProcessInvalidationMessagesMulti(&transInvalInfo->PriorCmdInvalidMsgs, SendSharedInvalidMessages);
if (transInvalInfo->ii.RelcacheInitFileInval) RelationCacheInitFilePostInvalidate(); } else /* 중단 */ { ProcessInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs, LocalExecuteInvalidationMessage); } transInvalInfo = NULL;}중단 시에는 명령 경계에서 이미 로컬에 적용된 PriorCmdInvalidMsgs를 다시 로컬에서 플러시해 캐시를 되돌린다. 아직 로컬에 적용되지 않은 CurrentCmdInvalidMsgs는 그냥 버린다.
sinval 링 버퍼 (sinvaladt.c)
섹션 제목: “sinval 링 버퍼 (sinvaladt.c)”SISeg는 링 버퍼와 백엔드별 상태 전체를 담는 공유 메모리 세그먼트다.
// SISeg — src/backend/storage/ipc/sinvaladt.ctypedef struct SISeg{ int minMsgNum; /* 가장 오래된 미독 메시지 */ int maxMsgNum; /* 다음 쓰기 슬롯 */ int nextThreshold; /* SICleanupQueue 호출 기준 채움 정도 */ slock_t msgnumLock; /* maxMsgNum 보호용 스핀락 */
SharedInvalidationMessage buffer[MAXNUMMESSAGES]; /* 4096슬롯 */
int numProcs; int *pgprocnos; ProcState procState[FLEXIBLE_ARRAY_MEMBER]; /* 백엔드 슬롯당 하나 */} SISeg;
// ProcState — 백엔드별 커서 및 플래그typedef struct ProcState{ pid_t procPid; /* 0 = 비활성 */ int nextMsgNum; /* 다음 읽을 메시지 */ bool resetState; /* 메시지 누락, 캐시 전체 리셋 필요 */ bool signaled; /* 캐치업 인터럽트 이미 전송됨 */ bool hasMessages; /* 미독 메시지 존재 */ bool sendOnly; /* Startup 프로세스: 송신 전용, 수신 안 함 */ // ... nextLXID} ProcState;MAXNUMMESSAGES = 4096이다. 백엔드의 nextMsgNum이 읽어야 할 슬롯이 이미 덮어쓰인 경우(maxMsgNum - nextMsgNum > MAXNUMMESSAGES), resetState가 true로 설정된다.
쓰기 (SIInsertDataEntries):
// SIInsertDataEntries (요약) — src/backend/storage/ipc/sinvaladt.cvoidSIInsertDataEntries(const SharedInvalidationMessage *data, int n){ while (n > 0) { int nthistime = Min(n, WRITE_QUANTUM); /* 64 */ n -= nthistime;
LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE);
/* 가득 찼으면 정리·리셋 */ for (;;) { numMsgs = segP->maxMsgNum - segP->minMsgNum; if (numMsgs + nthistime > MAXNUMMESSAGES || numMsgs >= segP->nextThreshold) SICleanupQueue(true, nthistime); else break; }
max = segP->maxMsgNum; while (nthistime-- > 0) segP->buffer[max++ % MAXNUMMESSAGES] = *data++;
SpinLockAcquire(&segP->msgnumLock); segP->maxMsgNum = max; /* 스핀락으로 메모리 배리어 제공 */ SpinLockRelease(&segP->msgnumLock);
for (i = 0; i < segP->numProcs; i++) segP->procState[segP->pgprocnos[i]].hasMessages = true;
LWLockRelease(SInvalWriteLock); }}SInvalWriteLock이 생산자를 직렬화한다. maxMsgNum의 스핀락은 메모리 배리어를 제공한다. buffer[]에 메시지가 실제로 기록된 이후에야 maxMsgNum이 증가하는 것이 보장된다.
읽기 (SIGetDataEntries):
// SIGetDataEntries (요약) — src/backend/storage/ipc/sinvaladt.cintSIGetDataEntries(SharedInvalidationMessage *data, int datasize){ if (!stateP->hasMessages) return 0; /* 빠른 경로: 대기 메시지 없음 */
LWLockAcquire(SInvalReadLock, LW_SHARED); stateP->hasMessages = false;
SpinLockAcquire(&segP->msgnumLock); max = segP->maxMsgNum; SpinLockRelease(&segP->msgnumLock);
if (stateP->resetState) { stateP->nextMsgNum = max; stateP->resetState = false; LWLockRelease(SInvalReadLock); return -1; /* -1 = 리셋 신호 */ }
n = 0; while (n < datasize && stateP->nextMsgNum < max) data[n++] = segP->buffer[stateP->nextMsgNum++ % MAXNUMMESSAGES];
// ... 부분 읽기 시 hasMessages 재설정 ... LWLockRelease(SInvalReadLock); return n;}여러 백엔드가 SInvalReadLock을 공유(shared) 모드로 잡고 SIGetDataEntries를 동시에 호출할 수 있다. 각 백엔드가 자신의 ProcState 필드만 수정하기 때문이다. 이 락은 통상적인 읽기 전용 의미가 아니라 자기 자신의 상태 변경을 허용하는 메모리 배리어로 사용된다.
메시지 수신 (AcceptInvalidationMessages)
섹션 제목: “메시지 수신 (AcceptInvalidationMessages)”각 백엔드는 트랜잭션 시작 시(StartTransaction) 및 락 획득 이후 같은 체크포인트에서 AcceptInvalidationMessages()를 호출한다. 이 함수는 ReceiveSharedInvalidMessages를 거쳐 큐가 소진될 때까지 SIGetDataEntries를 반복 호출한다.
// AcceptInvalidationMessages — src/backend/utils/cache/inval.cvoidAcceptInvalidationMessages(void){ ReceiveSharedInvalidMessages(LocalExecuteInvalidationMessage, InvalidateSystemCaches); // ... debug_discard_caches 경로(선택적)}LocalExecuteInvalidationMessage는 메시지 타입별로 디스패치한다.
// LocalExecuteInvalidationMessage (요약) — src/backend/utils/cache/inval.cvoidLocalExecuteInvalidationMessage(SharedInvalidationMessage *msg){ if (msg->id >= 0) /* catcache 튜플 */ { InvalidateCatalogSnapshot(); SysCacheInvalidate(msg->cc.id, msg->cc.hashValue); CallSyscacheCallbacks(msg->cc.id, msg->cc.hashValue); } else if (msg->id == SHAREDINVALCATALOG_ID) /* 카탈로그 전체 */ { InvalidateCatalogSnapshot(); CatalogCacheFlushCatalog(msg->cat.catId); } else if (msg->id == SHAREDINVALRELCACHE_ID) /* relcache 항목 */ { RelationCacheInvalidateEntry(msg->rc.relId); /* 또는 전체 플러시 */ /* relcache_callback_list 항목 호출 */ } else if (msg->id == SHAREDINVALSMGR_ID) { smgrreleaserellocator(...); } else if (msg->id == SHAREDINVALRELMAP_ID) { RelationMapInvalidate(...); } else if (msg->id == SHAREDINVALSNAPSHOT_ID) { InvalidateCatalogSnapshot(); } else if (msg->id == SHAREDINVALRELSYNC_ID) { CallRelSyncCallbacks(...); }}SIGetDataEntries가 -1(리셋)을 반환하면 InvalidateSystemCaches()가 대신 호출된다. 이 함수는 catcache와 relcache 항목을 전부 삭제하고 등록된 모든 syscache·relcache 콜백을 실행한다.
등록 콜백
섹션 제목: “등록 콜백”파생 상태를 캐시하는 서브시스템은 무효화 이벤트 통보를 위해 콜백을 등록한다.
// CacheRegisterSyscacheCallback — src/backend/utils/cache/inval.cvoidCacheRegisterSyscacheCallback(int cacheid, SyscacheCallbackFunction func, Datum arg){ // syscache_callback_list[]에 추가; syscache_callback_links[id]로 체인}
// CacheRegisterRelcacheCallback — src/backend/utils/cache/inval.cvoidCacheRegisterRelcacheCallback(RelcacheCallbackFunction func, Datum arg){ // relcache_callback_list[]에 추가}syscache 콜백은 최대 MAX_SYSCACHE_CALLBACKS = 64개, relcache 콜백은 최대 MAX_RELCACHE_CALLBACKS = 10개를 고정 배열로 지원한다. 콜백은 syscache_callback_links[]가 캐시 ID별 체인으로 연결해 O(1) 디스패치를 제공한다. 플랜 캐시, 파티션 디스크립터 캐시, 이벤트 트리거 캐시, 논리 디코딩 서브시스템이 이 인터페이스를 사용한다.
인플레이스 업데이트 경로
섹션 제목: “인플레이스 업데이트 경로”ANALYZE 중 pg_class.reltuples를 갱신하는 것처럼 트랜잭션이 아닌 카탈로그 변경에는 별도의 인플레이스 업데이트 경로가 있다. CacheInvalidateHeapTupleInplace가 메시지를 별도의 inplaceInvalInfo 구조체에 큐잉하고, AtInplace_Inval()이 WAL 삽입 크리티컬 섹션 안에서 직접 sinval 버퍼에 전송한다. PreInplace_Inval()은 크리티컬 섹션 전에 relcache 초기화 파일 삭제를 처리한다.
흐름 다이어그램
섹션 제목: “흐름 다이어그램”flowchart TD
A["heap_update / heap_delete\n(카탈로그 릴레이션)"] -->|CacheInvalidateHeapTuple| B["CacheInvalidateHeapTupleCommon\ninval.c"]
B -->|catcache 메시지| C["CurrentCmdInvalidMsgs\n(catcache 배열)"]
B -->|relcache 메시지| D["CurrentCmdInvalidMsgs\n(relcache 배열)"]
C --> E["CommandEndInvalidationMessages\n(CommandCounterIncrement)"]
D --> E
E -->|LocalExecuteInvalidationMessage| F["SysCacheInvalidate +<br/>RelationCacheInvalidateEntry\n(로컬 캐시)"]
E -->|이동| G["PriorCmdInvalidMsgs"]
G -->|커밋: AtEOXact_Inval| H["SendSharedInvalidMessages\n→ SIInsertDataEntries"]
G -->|중단: AtEOXact_Inval| I["LocalExecuteInvalidationMessage\n(이전 명령 변경 되돌리기)"]
H --> J["SISeg.buffer\n4096슬롯 링, 공유 메모리"]
J -->|AcceptInvalidationMessages\n다음 트랜잭션 시작| K["SIGetDataEntries"]
K -->|n > 0| L["LocalExecuteInvalidationMessage\n메시지별 처리"]
K -->|반환값 -1 리셋| M["InvalidateSystemCaches\n전체 삭제 + 모든 콜백"]
L --> N["SysCacheInvalidate\nRelationCacheInvalidateEntry\n콜백..."]
그림 1 — 카탈로그 변경에서 크로스 백엔드 전달까지의 캐시 무효화 흐름. 왼쪽은 생성 백엔드, 오른쪽은 수신 백엔드다. sinval 링 버퍼가 두 쪽을 연결하는 유일한 공유 구조다.
단일 변경에서 모든 백엔드로: 엔드투엔드 팬아웃
섹션 제목: “단일 변경에서 모든 백엔드로: 엔드투엔드 팬아웃”그림 1은 단일 백엔드 내부의 데이터 구조를 따라간다. 그림 2는 반대 시각을 취한다. 카탈로그 변경 하나가 공유 sinval 큐를 거쳐 모든 활성 백엔드로 어떻게 퍼져나가는지를 보여준다. 핵심 비대칭은 생산자가 한 번 실행하는 경로(등록 → 누적 → 커밋 시 브로드캐스트)와, 소비자 경로가 N번, 즉 링 버퍼를 드레인하는 각 백엔드에서 독립적으로 실행된다는 점이다. 각 소비자는 같은 종착 동작으로 끝난다. 낡은 catcache·relcache 항목을 버려 다음 조회가 카탈로그에서 다시 로드하도록 만든다.
LocalExecuteInvalidationMessage 안의 메시지별 데이터베이스 가드도 짚어둘 필요가 있다. catcache·relcache·relmap·snapshot 메시지는 모두 msg->*.dbId == MyDatabaseId || dbId == InvalidOid가 아니면 즉시 건너뛴다. 다른 데이터베이스의 DDL 메시지가 공유 링을 물리적으로 통과하더라도, 다른 데이터베이스에 연결된 백엔드는 그 메시지를 무시한다는 뜻이다.
flowchart TD
subgraph PROD["생산자 백엔드 (한 번 실행)"]
A["heap_update / heap_delete\n카탈로그 릴레이션"] -->|CacheInvalidateHeapTuple| B["CacheInvalidateHeapTupleCommon"]
B -->|PrepareToInvalidateCacheTuple| C["RegisterCatcacheInvalidation\n(catcache 메시지: id, hashValue, dbId)"]
B -->|pg_class / pg_attribute /<br/>pg_index / pg_constraint| D["RegisterRelcacheInvalidation\n(relcache 메시지: dbId, relId)"]
C --> E["CurrentCmdInvalidMsgs\n(트랜잭션 내 누적)"]
D --> E
E -->|CommandEndInvalidationMessages\nPriorCmdInvalidMsgs로 이동| F["AtEOXact_Inval\nisCommit = true"]
F -->|SendSharedInvalidMessages| G["SIInsertDataEntries\nSInvalWriteLock 보유 중"]
end
G --> Q["SISeg.buffer\n공유 메모리 4096슬롯 링\nmaxMsgNum 증가; 모든 백엔드에 hasMessages=true"]
Q --> H1["백엔드 1\nAcceptInvalidationMessages"]
Q --> H2["백엔드 2\nAcceptInvalidationMessages"]
Q --> H3["백엔드 N\nAcceptInvalidationMessages"]
subgraph CONS["소비자 백엔드 (N개 백엔드에서 각각 실행)"]
H1 -->|ReceiveSharedInvalidMessages| I["SIGetDataEntries"]
I -->|n >= 0: 메시지별| J["LocalExecuteInvalidationMessage\ndbId 가드: MyDatabaseId 또는 InvalidOid"]
I -->|반환값 -1: 오버플로| K["InvalidateSystemCaches\ncatcache + relcache 전체 삭제"]
J -->|id >= 0| L["SysCacheInvalidate +\nCallSyscacheCallbacks\n(catcache 항목 버림)"]
J -->|SHAREDINVALRELCACHE_ID| M["RelationCacheInvalidateEntry +\nrelcache_callback_list\n(relcache 항목 버림)"]
end
그림 2 — 카탈로그 변경 하나의 엔드투엔드 팬아웃. 생산자 경로는 한 번 실행되고 SInvalWriteLock 보호 아래 링 버퍼에 쓰는 것으로 끝난다. 모든 활성 백엔드는 다음 AcceptInvalidationMessages 체크포인트에서 같은 링을 독립적으로 드레인하고, 각자 영향받는 catcache·relcache 항목을 버린다(오버플로 시에는 모든 캐시 삭제). LocalExecuteInvalidationMessage의 dbId 가드 덕분에 다른 데이터베이스에 연결된 백엔드는 관련 없는 메시지를 건너뛴다.
논리 디코딩을 위한 WAL 통합
섹션 제목: “논리 디코딩을 위한 WAL 통합”wal_level = logical로 설정된 경우 CommandEndInvalidationMessages는 LogLogicalInvalidations()도 호출해 명령별 무효화 메시지를 WAL 스트림에 기록한다. 덕분에 논리 디코딩 백엔드(WAL 구독자)는 실시간 sinval 큐에 접근하지 않고도 카탈로그 변경을 재현하고 자체 catcache·relcache를 유지할 수 있다.
ProcessCommittedInvalidationMessages는 스탠바이에서 xact_redo_commit()이 호출하는, AtEOXact_Inval의 리두 시간 대응 함수다. 커밋 WAL 레코드에 포함된 무효화 메시지를 스탠바이의 sinval 큐에 브로드캐스트한다.
소스 워크스루
섹션 제목: “소스 워크스루”생성 측 (inval.c)
섹션 제목: “생성 측 (inval.c)”CacheInvalidateHeapTuple— 카탈로그 릴레이션의 힙 DML이 호출하는 진입점. 트랜잭션 상태 준비와 함께CacheInvalidateHeapTupleCommon으로 라우팅한다.CacheInvalidateHeapTupleInplace— 인플레이스 업데이트 변형. 트랜잭션 스택을 우회한다.CacheInvalidateCatalog— 카탈로그 전체 플러시를 등록한다(VACUUM FULL이 카탈로그에 사용).CacheInvalidateRelcache— 튜플 수준 트리거가 없는 경우(예:DROP INDEX) 특정 릴레이션의 relcache 플러시를 등록한다.CacheInvalidateRelcacheAll—InvalidOid를 브로드캐스트해 클러스터 전체 relcache를 플러시한다.PrepareInvalidationState— 현재 (서브)트랜잭션 중첩 수준에 맞는TransInvalidationInfo를 할당·재사용한다.
명령·트랜잭션 경계 (inval.c)
섹션 제목: “명령·트랜잭션 경계 (inval.c)”CommandEndInvalidationMessages—CurrentCmdInvalidMsgs를 로컬에 적용하고,wal_level=logical이면 WAL에 기록한 후PriorCmdInvalidMsgs로 이동시킨다.AtEOXact_Inval— 커밋: sinval로PriorCmdInvalidMsgs브로드캐스트. 중단: 로컬 적용(되돌리기).AtEOSubXact_Inval— 서브트랜잭션 커밋: 메시지를 부모로 전달. 서브트랜잭션 중단: 로컬 적용.AtInplace_Inval/PreInplace_Inval— WAL 크리티컬 섹션 안에서 인플레이스 업데이트 브로드캐스트.PostPrepare_Inval— PREPARE 경로: 중단처럼 동작해 로컬 변경을 되돌린다(트랜잭션이 최종 커밋할 때ProcessCommittedInvalidationMessages로 브로드캐스트가 도달한다).
수신 측
섹션 제목: “수신 측”AcceptInvalidationMessages(inval.c) — 외부 진입점.LocalExecuteInvalidationMessage와InvalidateSystemCaches를 디스패치 콜백으로ReceiveSharedInvalidMessages를 호출한다.LocalExecuteInvalidationMessage(inval.c) — 메시지별 디스패치.SysCacheInvalidate,RelationCacheInvalidateEntry, smgr, relmap, snapshot, relsync 핸들러로 라우팅한다.InvalidateSystemCaches/InvalidateSystemCachesExtended(inval.c) — 전체 리셋. catcache·relcache를 삭제하고 모든 콜백을 실행한다.ReceiveSharedInvalidMessages(sinval.c) — 얇은 래퍼.SIGetDataEntries를 반복 호출하며PROCSIG_CATCHUP_INTERRUPT를 처리한다.SIGetDataEntries(sinvaladt.c) — 링 버퍼에서 백엔드의 미독 메시지를 읽는다. 리셋 시 -1을 반환한다.SIInsertDataEntries(sinvaladt.c) — 락 보유 시간당 최대WRITE_QUANTUM=64개의 메시지를 기록하고, 버퍼가 채워지면SICleanupQueue를 호출한다.
콜백 등록
섹션 제목: “콜백 등록”CacheRegisterSyscacheCallback(inval.c) — 특정 syscache ID에 훅을 등록한다.syscache_callback_links[]로 캐시별 연결 리스트를 구성한다.CacheRegisterRelcacheCallback(inval.c) — 모든 relcache 플러시에 대한 훅을 등록한다.CacheRegisterRelSyncCallback(inval.c) — 논리 디코딩 relsync 훅.
위치 힌트 (2026-06-05 기준, 커밋 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, 커밋 273fe94)”| 심벌 | 파일 | 줄 |
|---|---|---|
CacheInvalidateHeapTuple | src/backend/utils/cache/inval.c | 1571 |
CacheInvalidateHeapTupleCommon | src/backend/utils/cache/inval.c | 1436 |
CacheInvalidateHeapTupleInplace | src/backend/utils/cache/inval.c | 1593 |
CacheInvalidateCatalog | src/backend/utils/cache/inval.c | 1612 |
CacheInvalidateRelcache | src/backend/utils/cache/inval.c | 1635 |
CommandEndInvalidationMessages | src/backend/utils/cache/inval.c | 1409 |
AtEOXact_Inval | src/backend/utils/cache/inval.c | 1199 |
AtEOSubXact_Inval | src/backend/utils/cache/inval.c | 1310 |
AtInplace_Inval | src/backend/utils/cache/inval.c | 1263 |
AcceptInvalidationMessages | src/backend/utils/cache/inval.c | 930 |
LocalExecuteInvalidationMessage | src/backend/utils/cache/inval.c | 823 |
InvalidateSystemCaches | src/backend/utils/cache/inval.c | 916 |
PrepareInvalidationState | src/backend/utils/cache/inval.c | 682 |
RegisterCatcacheInvalidation | src/backend/utils/cache/inval.c | 604 |
RegisterRelcacheInvalidation | src/backend/utils/cache/inval.c | 632 |
RegisterSnapshotInvalidation | src/backend/utils/cache/inval.c | 672 |
CacheRegisterSyscacheCallback | src/backend/utils/cache/inval.c | 1816 |
CacheRegisterRelcacheCallback | src/backend/utils/cache/inval.c | 1858 |
xactGetCommittedInvalidationMessages | src/backend/utils/cache/inval.c | 1012 |
ProcessCommittedInvalidationMessages | src/backend/utils/cache/inval.c | 1135 |
TransInvalidationInfo (구조체) | src/backend/utils/cache/inval.c | 241 |
SISeg (구조체) | src/backend/storage/ipc/sinvaladt.c | 165 |
ProcState (구조체) | src/backend/storage/ipc/sinvaladt.c | 136 |
SIInsertDataEntries | src/backend/storage/ipc/sinvaladt.c | 370 |
SIGetDataEntries | src/backend/storage/ipc/sinvaladt.c | 473 |
SharedInvalBackendInit | src/backend/storage/ipc/sinvaladt.c | 272 |
SICleanupQueue | src/backend/storage/ipc/sinvaladt.c | ~560 |
SendSharedInvalidMessages | src/backend/storage/ipc/sinval.c | 47 |
ReceiveSharedInvalidMessages | src/backend/storage/ipc/sinval.c | 69 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”검증된 사실
섹션 제목: “검증된 사실”-
MAXNUMMESSAGES = 4096은 컴파일 타임 상수다. 커밋 273fe94의sinvaladt.c129번째 줄에서 확인했다. GUC(Grand Unified Configuration)가 아니므로 재컴파일 없이 런타임 크기 조정이 불가능하다. -
catcache 메시지가 각 서브그룹 안에서 relcache 메시지보다 먼저 처리된다.
AtEOXact_Inval을 읽어 확인했다.ProcessInvalidationMessagesMulti가CatCacheMsgs서브그룹을 먼저,RelCacheMsgs를 나중에 처리한다. catcache를 정리한 뒤 relcache를 재빌드해야 한다는 설계 원칙이 코드에 반영된 것이다. -
SIGetDataEntries가 공유(shared)SInvalReadLock아래서 실행된다.sinvaladt.c에서 확인했다. 여러 백엔드가 각자의ProcState.nextMsgNum만 수정하므로 읽기 경로에서 경쟁 없이 동시 실행된다. 이 공유 락은 읽기 전용이 아니라 자기 자신의 상태 수정을 허용하는 메모리 배리어로 사용된다. -
hasMessages플래그가 락 획득 전 빠른 경로 검사를 제공한다.SIGetDataEntries는stateP->hasMessages가 거짓이면 즉시 0을 반환한다. 이 플래그는 생산자가SInvalWriteLock안에서maxMsgNum을 올린 직후 설정되므로 메모리 배리어 순서가 보장된다. -
인플레이스 업데이트 경로(
AtInplace_Inval)는 크리티컬 섹션(CritSectionCount > 0) 안에서SendSharedInvalidMessages를 호출한다.AtInplace_Inval의Assert(CritSectionCount > 0)으로 확인했다. sinval 버퍼 쓰기가 인플레이스 힙 변경에 대한 WAL 레코드와 원자적으로 이루어진다는 뜻이다. -
debug_discard_cachesGUC가 스트레스 테스트용으로 존재한다(기본값 0).AcceptInvalidationMessages에DISCARD_CACHES_ENABLED블록이 있어debug_discard_caches레벨만큼 재귀적으로InvalidateSystemCachesExtended(true)를 호출한다.--enable-discard-caches로 컴파일해야만 활성화된다.debug_discard_caches = 1모드는 과거CLOBBER_CACHE_ALWAYS였다. -
RelcacheInitFileInval플래그는 메시지별이 아니라 트랜잭션 단위 불리언이다.TransInvalidationInfo.ii.RelcacheInitFileInval에 하나만 존재한다.RelationIdIsInInitFile에 해당하는 릴레이션의RegisterRelcacheInvalidation이 호출될 때 설정되고, 초기화 파일은 커밋 시 메시지당이 아닌 딱 한 번 삭제된다. -
MAX_SYSCACHE_CALLBACKS = 64,MAX_RELCACHE_CALLBACKS = 10은 고정 배열이다. 동적으로 늘어나지 않는다. 초과 등록 시도 시elog(FATAL)이 발생한다. REL_18_STABLE 기준 표준 빌드에서 이 한도에 도달하는 회귀 테스트는 없고, 실제 등록 수도 한도에 훨씬 못 미친다.
미해결 사항
섹션 제목: “미해결 사항”-
SICleanupQueue리셋 휴리스틱.SIG_THRESHOLD = MAXNUMMESSAGES / 2(2048)가 뒤처진 백엔드에 캐치업 인터럽트를 보내는 기준이다. 주석은 “뒤처진 백엔드가 멈춰 있을 수 있다”고 말하지만 타임아웃 기반 리셋은 없다.AcceptInvalidationMessages를 호출하지 못하는 상태에 빠진 백엔드는 결국resetState가 설정되고 캐시 전체를 무효화한다. 단일 정지 백엔드가 4096슬롯 버퍼를 가득 채울 때 프로덕션에 미치는 영향은 측정이 필요하다. 조사 경로:pg_stat_activity의SICleanupQueue호출을 계측하거나 커스텀 익스텐션으로 모니터링한다. -
Startup 프로세스의
sendOnly의미론.ProcState.sendOnly = true는 복구 중 Startup 프로세스에 설정된다. 코드 주석은 “쿼리 백엔드가 스키마 변경을 볼 수 있도록 무효화 메시지를 발생시키지만 relcache를 유지하지는 않는다”고 설명한다. 복구 중 커밋된 트랜잭션이 재생될 때 카탈로그 변경이 sinval 경로를 어떻게 흐르는지, 핫 스탠바이 읽기 중 수신 백엔드가 relcache를 올바르게 재빌드하는지는 이 문서에서 완전히 추적하지 않았다. -
WAL 기록된 무효화와 논리 디코딩.
CommandEndInvalidationMessages의LogLogicalInvalidations가 명령별 무효화 메시지를 WAL에 기록한다. 구독자 쪽에서xlogreader와decode.c를 거쳐 소비되는 형식과 정확한 경로, 그리고 논리 디코딩이 유지하는reorderbuffer스냅샷과의 상호 작용은 별도 분석이 필요하다.
PostgreSQL 너머 — 비교 설계 및 연구 방향
섹션 제목: “PostgreSQL 너머 — 비교 설계 및 연구 방향”-
Oracle의 전역 공유 캐시 무효화. Oracle의 버퍼 캐시는 공유(SGA)이므로 캐시 무효화에 프로세스별 전파가 필요하지 않다. DDL이 커밋되면 Oracle은 공유 커서 캐시의 자식 커서를 라이브러리 캐시에서 무효 표시하고, 다음 실행 시 리바인딩이 일어난다. PostgreSQL의 프로세스별 sinval 링 버퍼 패턴은 프로세스별 아키텍처의 산물이다. 공유 캐시 엔진은 메타데이터 전파가 아닌 커서 무효화라는 다른 문제를 다룬다.
-
MySQL/InnoDB 테이블 정의 캐시(TDC)와 MDL. MySQL은 메타데이터 락(MDL)을 갖춘 중앙화된 테이블 정의 캐시를 사용한다. MDL은 테이블을 사용 중인 모든 세션이 참조를 해제할 때까지 DDL을 블로킹한다. 이는 즉각적인 모델이다. DDL이 커밋 후 무효화를 보내는 대신 캐시가 깨끗해질 때까지 기다린다. 트레이드오프는 낡은 읽기 위험이 없지만 동시 접근 중 DDL 지연이 높다는 점이다.
-
분산 DBMS에서의 락 기반 캐시 일관성. CockroachDB, Spanner, YugabyteDB 같은 분산 엔진은 리스 또는 버전 메커니즘으로 스키마 변경을 전파한다. 각 백엔드가 특정 리스 버전의 스키마를 캐시하고, DDL이 버전을 올린 뒤 모든 백엔드가 새 버전을 관측할 때까지 기다린다. PostgreSQL의 sinval 모델은 단일 노드 유사체다. “커밋 시 버전 증가, 모든 백엔드가 결국 큐를 드레인”하는 구조이지만 분산 조율은 없다.
-
락프리 sinval. 현재 설계는
SInvalReadLock(LWLock 공유)과SInvalWriteLock(LWLock 독점), 그리고maxMsgNum을 위한 스핀락을 사용한다. 원자적 읽기·쓰기와 하드웨어 메모리 배리어를 활용하면 독자 경로에서 LWLock을 완전히 제거할 수 있을지도 모른다. 세션 수가 매우 많을 때 경쟁을 줄이는 열린 최적화 방향으로 소스 주석에도 언급되어 있다.
원시 파일
섹션 제목: “원시 파일”없음 — 소스 트리에서 직접 합성했다.
교과서 장절
섹션 제목: “교과서 장절”- Database System Concepts (Silberschatz et al., 7판) — 11장 §“System Catalog”, 25장 §“Buffer Management”.
- Database Internals (Alex Petrov) — 6장 §“System Catalogs and Metadata”.
소스 코드 경로 (REL_18_STABLE, 커밋 273fe94)
섹션 제목: “소스 코드 경로 (REL_18_STABLE, 커밋 273fe94)”src/backend/utils/cache/inval.c— 무효화 디스패처 및 누적 로직.src/backend/storage/ipc/sinvaladt.c— 공유 링 버퍼 구현.src/backend/storage/ipc/sinval.c— sinvaladt의 얇은 래퍼.src/include/storage/sinval.h—SharedInvalidationMessage유니언과 ID 상수.src/include/utils/inval.h— 공개 API 선언.src/backend/utils/cache/catcache.c—PrepareToInvalidateCacheTuple,SysCacheInvalidate.src/backend/utils/cache/relcache.c—RelationCacheInvalidateEntry,RelationCacheInitFilePreInvalidate.
이 지식 베이스 내 교차 참조
섹션 제목: “이 지식 베이스 내 교차 참조”postgres-catcache-syscache.md—SysCacheInvalidate호출 시 catcache 항목이 무효화되는 방식. 음수 항목(negative entry)과 콜백 체인.postgres-relcache.md—RelationCacheInvalidateEntry가 relcache 항목을 재빌드·플러시하는 방식. 초기화 파일과 부트스트랩 네일링.postgres-xlog-wal.md— WAL 레코드 형식.LogLogicalInvalidations와 커밋 레코드에 무효화 메시지가 포함되는 방식.postgres-xact.md— 트랜잭션 생명주기.StartTransaction/CommitTransaction/CommandCounterIncrement에서AtEOXact_Inval과CommandEndInvalidationMessages가 호출되는 위치.postgres-shared-memory-ipc.md—SISeg할당. sinvaladt가 사용하는 LWLock과 스핀락 프리미티브.