(KO) PostgreSQL 대기 이벤트와 진행 상황 보고
목차
- 이론적 배경
- DBMS의 일반 설계
- PostgreSQL의 접근 방식
- 소스 코드 워크스루
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 동향
- 출처
이론적 배경
섹션 제목: “이론적 배경”모든 프로덕션 데이터베이스 엔진은 겉보기에 단순한 운영 질문에 답해야 한다. “지금 각 프로세스가 무엇을 하고 있는가, 그리고 진행이 막혀 있다면 무엇에 블로킹됐는가?” 이것이 인트로스펙션(introspection) 또는 온라인 관찰 가능성(online observability) 문제다. 이 문제는 누적 통계(cumulative statistics) 문제 — 마지막 리셋 이후 이 테이블에 인덱스 스캔이 몇 번 발생했는가, 형제 문서 postgres-cumulative-stats.md 참조 — 와 구분된다. 인트로스펙션은 순간적인 프로세스별 스냅샷을 다루고, 누적 통계는 시간에 걸쳐 집계되는 단조 증가 카운터를 다룬다.
프로세스별 인트로스펙션 기능의 설계 공간을 세 가지 속성이 규정한다.
-
핫 패스에서의 쓰기 비용. 계측 코드는 백엔드가 무언가를 시작하는 정확한 순간에 삽입된다 — 잠금 획득, I/O 발행, 메인 루프 진입. “나는 지금 X를 기다리는 중”을 게시하는 데 잠금 획득이나 시스템 콜이 필요하다면, 계측이 측정 대상 자체를 교란한다(프로브 효과). 설계는 일반적인 쓰기를 경합 없고 동기화 없는 단일 저장 연산으로 만들어야 한다.
-
쓰기 측을 블로킹하지 않는 읽기 일관성. 모니터링 쿼리(
SELECT * FROM pg_stat_activity)는 수천 개 다른 백엔드의 상태를 읽는다. 피관찰 백엔드가 필요로 하는 잠금을 읽기 측이 절대 취득해서는 안 된다. 그렇지 않으면 읽기 전용 진단이 경합의 원천이 되거나, 최악의 경우 교착 상태를 유발한다. 고전적인 답은 시퀀스 락(seqlock)이다. 쓰기 측은 변경 전후에 카운터를 증가시키고, 읽기 측은 레코드를 복사한 뒤 카운터를 다시 읽어 값이 바뀌었으면 재시도한다. 쓰기는 저렴하게 유지되고, 읽기는 이따금 재시도 비용을 부담한다. -
어휘 관리. “X를 기다리는 중”에는 X가 될 수 있는 것들의 통제된 어휘가 필요하다. 잠금 대기, I/O 대기, IPC 대기, 메인 루프 유휴 상태 등 클래스로 나눠야 운영자가 적절한 추상 수준에서 판단할 수 있다. 엔진이 커질수록 이 어휘는 수백 개 항목으로 늘어난다. 열거형, C 이름 조회 함수, 사용자 문서를 세 곳에서 수작업으로 관리하면 차이가 생기므로, 성숙한 엔진은 하나의 선언적 테이블에서 세 가지 결과물 모두를 생성한다.
네 번째 관심사는 장기 실행 유지 관리 명령(VACUUM, CREATE INDEX, CLUSTER, 베이스 백업)의 진행 상황 보고다. 대기 이벤트는 단일 범주형 값인 반면, 진행 상황은 정수 소벡터(단계, 스캔한 블록, 처리한 튜플, 총량)이고 그 의미는 명령마다 다르다. 엔진은 이 벡터를 위한 범용 전송 수단과 그 위에 얹힌 명령별 관례가 필요하다.
Database System Concepts(Silberschatz, Korth, Sudarshan)는 DBA의 모니터링 루프를 “현재 실행 중인 트랜잭션의 상태와 그들이 보유하거나 대기 중인 자원을 관찰하는 것”으로 정의하며, 잠금 대기와 I/O 대기가 정체된 OLTP 워크로드 진단을 지배한다고 설명한다. Architecture of a Database System(Hellerstein, Stonebraker, Hamilton)은 “프로세스 모델” 논의에서, 다중 프로세스 엔진은 단일 프로세스가 전체를 볼 수 없기 때문에 공유 메모리에 워커별 상태를 게시해야 한다고 지적한다. 모니터링 프로세스는 워커에게 질의하는 대신 워커가 스스로 보고한 상태를 읽어야 한다. Database Internals(Petrov)는 이를 잠금 프리/대기 프리 문헌과 연결한다. 메모리 배리어로 게시되는 단일 쓰기자/다중 읽기자 레코드가 동시적 시스템에서 낮은 오버헤드 텔레메트리를 위한 표준 메커니즘이다.
PostgreSQL의 답은 네 가지 조각을 조립한다. 범주형 대기를 위한 4바이트 wait_event_info 워드, 어휘를 위한 테이블 주도 코드 생성(wait_event_names.txt), 나머지 활동 스냅샷을 위한 st_changecount 시퀀스 락으로 보호되는 백엔드별 PgBackendStatus 슬롯, 그리고 같은 슬롯 내의 20개 원소 st_progress_param[] 벡터.
DBMS의 일반 설계
섹션 제목: “DBMS의 일반 설계”이 섹션은 프로세스별 인트로스펙션에서 엔진들이 채택하는 반복적인 엔지니어링 패턴을 정리한다. PostgreSQL의 구체적인 선택을 공유된 공간 내의 선택지로 읽기 위해서다.
워커별 단일 쓰기자 공유 메모리 상태 슬롯
섹션 제목: “워커별 단일 쓰기자 공유 메모리 상태 슬롯”모든 다중 프로세스(또는 다중 스레드) 엔진은 안정적인 워커 ID로 인덱싱된, 워커당 하나의 고정 크기 공유 메모리 슬롯을 예약한다. 해당 워커만이 자신의 슬롯을 쓸 수 있고, 다른 프로세스는 모두 읽기만 한다. 단일 쓰기자 불변성이 저렴한 게시를 가능하게 한다. 슬롯에 쓰기-쓰기 경합은 발생하지 않는다. 읽기 중 쓰기 경합은 상대적으로 드물게 발생하고, 시퀀스 락이 이를 처리한다. PostgreSQL은 배열 크기를 MaxBackends + NUM_AUXILIARY_PROCS로 잡고, ProcNumber로 인덱싱한다.
시퀀스 락(changecount) 프로토콜
섹션 제목: “시퀀스 락(changecount) 프로토콜”읽기 측은 쓰기 측이 필요로 하는 잠금을 취득할 수 없다. 따라서 표준적인 잠금 프리 게시는 짝수/홀수 버전 카운터를 사용한다.
- 쓰기 측:
count++(홀수 됨) → 쓰기 배리어 → 필드 변경 → 쓰기 배리어 →count++(짝수 됨). - 읽기 측:
count읽기(짝수여야 함) → 읽기 배리어 → 필드 복사 → 읽기 배리어 →count재읽기. 값이 변하지 않고 짝수라면 복사본이 일관된 것이다. 그렇지 않으면 재시도.
홀수 값은 “변경 진행 중”을 나타낸다. 메모리 배리어는 CPU나 컴파일러가 카운터 증가를 필드 쓰기 주변에서 재배열하지 못하게 막는다. 이는 Linux의 seqlock을 백엔드별 레코드에 적용한 것과 정확히 같다.
가장 핫한 신호를 위한 별도 “무료” 계측 워드
섹션 제목: “가장 핫한 신호를 위한 별도 “무료” 계측 워드”가장 뜨거운 신호 — “방금 대기를 시작했다 / 방금 대기를 마쳤다” — 는 너무 자주 삽입된다. 모든 잠금, 모든 버퍼 I/O마다 찌르기 때문에, 시퀀스 락의 배리어조차 비용이 크다. 엔진들은 이것을 단일 머신 워드 저장으로 특수 처리한다. 정렬 덕분에 원자적이며, 카운터도 배리어도 필요 없고, 읽기 측이 순간적으로 오래된 값을 관찰할 수 있음을 허용한다. PostgreSQL은 대기 이벤트를 PgBackendStatus에서 완전히 분리한다. MyProc->wait_event_info에 독립적으로 존재하며, 단순한 *(volatile uint32 *) 저장으로 기록된다.
클래스 태그가 붙은 범주형 코드
섹션 제목: “클래스 태그가 붙은 범주형 코드”대기 이유는 상위 비트가 클래스(잠금, I/O, IPC, 타임아웃, 클라이언트, 활동)를 나타내고 하위 비트가 클래스 내 특정 이벤트를 나타내는 정수로 인코딩된다. 읽기 측은 클래스를 마스킹해서 타입 컬럼과 이벤트 컬럼을 별도로 렌더링한다. 이 방식은 와이어 표현을 단일 정수로 유지하면서 두 수준 분류 체계를 보존한다.
테이블 주도 어휘 코드 생성
섹션 제목: “테이블 주도 어휘 코드 생성”열거형, 정수-이름 변환 함수, 문서 테이블을 수작업으로 작성하는 대신, 성숙한 엔진은 하나의 선언적 목록을 유지하고 모든 결과물을 생성한다. 이렇게 하면 pg_stat_activity 이름, C 열거형 기호, 매뉴얼 항목이 절대 어긋나지 않는다.
명령별 의미를 가진 범용 진행 벡터
섹션 제목: “명령별 의미를 가진 범용 진행 벡터”유지 관리 명령의 진행 상황은 같은 워커별 슬롯에 게시되는 고정 너비 정수 배열이다. 전송은 범용적이다. 명령별 헤더 파일이 각 슬롯 인덱스의 의미를 할당하고(슬롯 0 = 단계, 슬롯 1 = 힙 블록 총수, …), SQL 뷰가 원시 정수를 친숙한 컬럼으로 매핑한다.
이론 ↔ PostgreSQL 매핑
섹션 제목: “이론 ↔ PostgreSQL 매핑”| 이론 / 관례 | PostgreSQL 이름 |
|---|---|
| 워커별 상태 슬롯 | BackendStatusArray[ProcNumber], 타입 PgBackendStatus (backend_status.c) |
| 자신의 슬롯에 대한 단일 쓰기자 포인터 | MyBEEntry |
| 시퀀스 락 버전 카운터 | st_changecount + PGSTAT_BEGIN/END_WRITE_ACTIVITY 매크로 |
| 읽기 측 재시도 루프 | pgstat_begin_read_activity / pgstat_read_activity_complete |
| 핫 패스 무료 워드 | MyProc->wait_event_info, pgstat_report_wait_start로 기록 |
| 클래스 비트 / 이벤트 비트 | WAIT_EVENT_CLASS_MASK (0xFF000000) / WAIT_EVENT_ID_MASK (0x0000FFFF) |
| 대기 클래스 테이블 | wait_classes.h (PG_WAIT_LWLOCK … PG_WAIT_INJECTIONPOINT) |
| 어휘 소스 | wait_event_names.txt |
| 어휘 코드 생성 | generate-wait_event_types.pl → wait_event_types.h, pgstat_wait_event.c |
| 정수 → 타입 문자열 | pgstat_get_wait_event_type |
| 정수 → 이벤트 문자열 | pgstat_get_wait_event |
| 현재 활동 문자열 | st_activity_raw (원시, 멀티바이트 중간 잘림 가능) → pgstat_clip_activity |
| 세션별 스냅샷 복사 | pgstat_read_current_status → localBackendStatusTable |
| 진행 명령 태그 | st_progress_command (ProgressCommandType) |
| 진행 벡터 | st_progress_param[PGSTAT_NUM_PROGRESS_PARAM] (20슬롯) |
| 병렬 워커 진행 중계 | parallel.c에서 처리하는 PqMsg_Progress 메시지 |
PostgreSQL의 접근 방식
섹션 제목: “PostgreSQL의 접근 방식”PostgreSQL은 백엔드의 “지금 무엇을 하는가” 상태를 의도적으로 두 곳의 공유 메모리에 나눠 저장한다.
-
대기 이벤트는
MyProc->wait_event_info(PGPROC필드)에 저장된다. changecount 없이, 잠금 없이, 인라인 단일 워드 저장으로 기록한다. 가장 뜨거운 신호이므로 메모리 저장에 가장 가깝게 무비용으로 만든다. -
그 외 모든 것 — 세션 상태(
active/idle/idle in transaction), 현재 쿼리 텍스트, 애플리케이션 이름, 쿼리 및 플랜 식별자, 진행 벡터 — 은st_changecount시퀀스 락으로 보호되는 백엔드별 슬롯PgBackendStatus에 저장된다.
pg_stat_activity는 두 곳의 합류점이다. wait_event_type / wait_event 컬럼은 wait_event.c가 디코딩한 MyProc->wait_event_info에서, state, query, application_name, query_id 등은 backend_status.c가 디코딩한 PgBackendStatus에서 나온다. 진행 뷰(pg_stat_progress_vacuum, pg_stat_progress_create_index, …)는 같은 PgBackendStatus 슬롯의 st_progress_command와 st_progress_param[]을 읽는다.
flowchart TB
subgraph Backend["피관찰 백엔드 (단일 쓰기자)"]
proc["MyProc->wait_event_info<br/>(단일 uint32, 잠금 프리 저장)"]
be["MyBEEntry = &BackendStatusArray[ProcNumber]<br/>PgBackendStatus 슬롯"]
be --> st["st_state / st_activity_raw<br/>st_query_id / st_plan_id"]
be --> prog["st_progress_command<br/>st_progress_param[20]"]
end
subgraph Monitor["모니터링 백엔드 (읽기자)"]
view["pg_stat_activity /<br/>pg_stat_progress_*"]
end
proc -. "pgstat_get_wait_event_type / _event" .-> view
be -. "pgstat_read_current_status<br/>(changecount 재시도 루프)" .-> view
classDef w fill:#eef,stroke:#446;
class proc,be,st,prog w;
그림 1 — 백엔드별 실시간 상태의 두 저장소. 대기 이벤트는 PGPROC의 무료 단일 워드 저장소에, 나머지는 시퀀스 락으로 보호되는 PgBackendStatus 슬롯에 있다. 모니터링 백엔드는 전자를 직접 디코딩하고, 후자는 changecount 재시도 루프로 스냅샷한다.
대기 이벤트 워드: 클래스 바이트 + 이벤트 ID
섹션 제목: “대기 이벤트 워드: 클래스 바이트 + 이벤트 ID”대기 이벤트는 단일 uint32다. 상위 바이트는 클래스이고, 하위 2바이트는 해당 클래스 내 이벤트 ID다. 클래스 상수는 wait_classes.h에 있다.
// wait class constants — include/utils/wait_classes.h#define PG_WAIT_LWLOCK 0x01000000U#define PG_WAIT_LOCK 0x03000000U#define PG_WAIT_BUFFERPIN 0x04000000U#define PG_WAIT_ACTIVITY 0x05000000U#define PG_WAIT_CLIENT 0x06000000U#define PG_WAIT_EXTENSION 0x07000000U#define PG_WAIT_IPC 0x08000000U#define PG_WAIT_TIMEOUT 0x09000000U#define PG_WAIT_IO 0x0A000000U#define PG_WAIT_INJECTIONPOINT 0x0B000000Uwait_event.c는 두 상수로 워드를 마스킹해서 클래스와 ID를 분리한다.
// class/id masks — utils/activity/wait_event.c#define WAIT_EVENT_CLASS_MASK 0xFF000000#define WAIT_EVENT_ID_MASK 0x0000FFFF0x0A000007은 클래스 PG_WAIT_IO (0x0A000000), 이벤트 ID 7로 읽힌다. 이를 게시하는 잠금 프리 저장은 “무료 계측 워드” 패턴의 교과서적 사례다. pgstat_track_activities를 확인하지 않는다는 점이 눈에 띈다. 확인 비용이 저장 비용보다 크기 때문이다.
// pgstat_report_wait_start — include/utils/wait_event.hstatic inline voidpgstat_report_wait_start(uint32 wait_event_info){ /* * Since this is a four-byte field which is always read and written as * four-bytes, updates are atomic. */ *(volatile uint32 *) my_wait_event_info = wait_event_info;}my_wait_event_info는 처음에 프로세스 로컬 변수(local_my_wait_event_info)를 가리킨다. MyProc가 존재하기 전에도 저장이 안전한 이유다. pgstat_set_wait_event_storage가 나중에 공유 메모리를 가리키도록 리디렉션한다. pgstat_report_wait_end는 단순히 0을 저장하며, 0 워드는 “대기 중 아님”을 나타내는 센티넬이다.
워드 디코딩: 타입 문자열과 이벤트 문자열
섹션 제목: “워드 디코딩: 타입 문자열과 이벤트 문자열”두 함수가 pg_stat_activity를 위해 워드를 디코딩한다. pgstat_get_wait_event_type은 클래스를 마스킹해서 타입 컬럼 문자열을 반환한다.
// pgstat_get_wait_event_type — utils/activity/wait_event.c (condensed)const char *pgstat_get_wait_event_type(uint32 wait_event_info){ uint32 classId; if (wait_event_info == 0) return NULL; /* not waiting */ classId = wait_event_info & WAIT_EVENT_CLASS_MASK; switch (classId) { case PG_WAIT_LWLOCK: return "LWLock"; case PG_WAIT_LOCK: return "Lock"; case PG_WAIT_IO: return "IO"; case PG_WAIT_IPC: return "IPC"; /* ... Activity, Client, Timeout, BufferPin, Extension ... */ default: return "???"; }}pgstat_get_wait_event는 클래스별 이름 조회로 디스패치한다. LWLock과 Lock은 각자의 코드(GetLWLockIdentifier, GetLockNameFromTagType — postgres-lwlock-spinlock.md 참조)로 처리되고, 생성된 클래스는 코드 생성된 pgstat_get_wait_* 헬퍼로 라우팅된다.
// pgstat_get_wait_event — utils/activity/wait_event.c (condensed)const char *pgstat_get_wait_event(uint32 wait_event_info){ uint32 classId = wait_event_info & WAIT_EVENT_CLASS_MASK; uint16 eventId = wait_event_info & WAIT_EVENT_ID_MASK; switch (classId) { case PG_WAIT_LWLOCK: return GetLWLockIdentifier(classId, eventId); /* own code */ case PG_WAIT_LOCK: return GetLockNameFromTagType(eventId); /* own code */ case PG_WAIT_EXTENSION: case PG_WAIT_INJECTIONPOINT: return GetWaitEventCustomIdentifier(wait_event_info); case PG_WAIT_IO: return pgstat_get_wait_io((WaitEventIO) wait_event_info); /* ... IPC, Activity, Client, Timeout, BufferPin ... */ }}pgstat_get_wait_io, pgstat_get_wait_ipc 등의 헬퍼는 수작업으로 작성된 것이 아니다. wait_event.c 하단의 #include가 이를 드러낸다.
// tail of utils/activity/wait_event.c#include "utils/pgstat_wait_event.c"확장을 위한 커스텀 대기 이벤트
섹션 제목: “확장을 위한 커스텀 대기 이벤트”익스텐션은 이름으로 자신의 대기 이벤트(클래스 PG_WAIT_EXTENSION)를 등록한다. 레지스트리는 공유 메모리의 두 해시 테이블(info 기준, name 기준)과 스핀락으로 보호되는 카운터에 있다. WaitEventExtensionNew가 공개 진입점이며, WaitEventCustomNew에 위임한다.
// WaitEventCustomNew — utils/activity/wait_event.c (condensed)static uint32WaitEventCustomNew(uint32 classId, const char *wait_event_name){ /* fast path: name already registered? return its id */ LWLockAcquire(WaitEventCustomLock, LW_SHARED); entry_by_name = hash_search(WaitEventCustomHashByName, wait_event_name, HASH_FIND, &found); LWLockRelease(WaitEventCustomLock); if (found) return entry_by_name->wait_event_info;
/* slow path: take exclusive, recheck, allocate a fresh event id */ LWLockAcquire(WaitEventCustomLock, LW_EXCLUSIVE); /* ... recheck ... */ SpinLockAcquire(&WaitEventCustomCounter->mutex); eventId = WaitEventCustomCounter->nextId++; SpinLockRelease(&WaitEventCustomCounter->mutex);
wait_event_info = classId | eventId; /* fold class into the id */ /* register in both hash directions, then release the LWLock */}대기 이벤트 기구 중 실제 잠금이 필요한 유일한 부분이다. 단일 백엔드의 워드가 아닌 공유 레지스트리를 변경하기 때문이다. 이중 확인 잠금(공유 탐색, 그 다음 독점 재확인)은 흔한 “이미 등록됨” 케이스에 대한 의도적인 최적화다.
어휘 코드 생성: wait_event_names.txt
섹션 제목: “어휘 코드 생성: wait_event_names.txt”내장 대기 이벤트의 통제된 어휘는 단일 탭 구분 파일 wait_event_names.txt에 있다. 각 줄은 열거형/이벤트 이름과 문서 문장을 제공하며, Section: ClassName - WaitEvent<Class> 헤더 아래에 그룹화된다.
# wait_event_names.txt (excerpt) — class headers + entriesSection: ClassName - WaitEventIOAIO_IO_COMPLETION "Waiting for another process to complete IO."BUFFILE_READ "Waiting for a read from a buffered file."CONTROL_FILE_SYNC "Waiting for the pg_control file to reach durable storage."
Section: ClassName - WaitEventIPCAPPEND_READY "Waiting for subplan nodes of an Append plan node to be ready."BACKEND_TERMINATION "Waiting for the termination of another backend."generate-wait_event_types.pl은 빌드 시 이 파일을 읽어 세 가지 결과물을 생성한다. wait_event_types.h(클래스별 C 열거형), pgstat_wait_event.c(wait_event.c 끝에 #include되는 pgstat_get_wait_<class> 조회), SGML 문서 테이블이다. 스크립트는 SCREAMING_SNAKE 토큰을 WAIT_EVENT_ 열거형 기호와 CamelCase 표시 문자열로 변환한다.
# generate-wait_event_types.pl (condensed) — name + enum derivationmy $waiteventenumname = "WAIT_EVENT_$waiteventname"; # WAIT_EVENT_BUFFILE_READ# CamelCase the display name (LWLock/Lock classes are left verbatim)my @waiteventparts = split("_", $waiteventname);foreach my $waiteventpart (@waiteventparts){ $waiteventdescription .= substr($waiteventpart, 0, 1) . lc(substr($waiteventpart, 1)); # "BufFileRead"}각 클래스의 첫 번째 멤버는 클래스 상수에 고정되어, 하위 비트가 자동으로 이벤트 ID가 된다.
# generate-wait_event_types.pl (condensed) — enum base = class constantprintf $h "typedef enum\n{\n";$pg_wait_class = "PG_WAIT_" . $lastuc; # e.g. PG_WAIT_IOprintf $h "\t%s = %s", $wev->[0], $pg_wait_class; # first = PG_WAIT_IO# subsequent members just ", NEXT_NAME" — C auto-increments the id생성된 WaitEventIO 열거형은 PG_WAIT_IO (0x0A000000)에서 시작하고, 이후 각 이벤트는 +1이다. WAIT_EVENT_ID_MASK로 마스킹하면 클래스별 서수가 복원되는 이유다. 네 클래스는 C 생성에서 제외된다. WaitEventExtension, WaitEventInjectionPoint, WaitEventLWLock, WaitEventLock이다. 이름을 동적으로 해석하거나(익스텐션/인젝션 포인트 레지스트리) 각자의 서브시스템으로 처리하기 때문에(LWLock, Lock), 스크립트는 SGML 문서만 생성한다.
flowchart LR
txt["wait_event_names.txt<br/>(선언적 테이블)"]
pl["generate-wait_event_types.pl<br/>(빌드 단계)"]
h["wait_event_types.h<br/>(클래스별 열거형)"]
c["pgstat_wait_event.c<br/>(이름 조회)"]
sgml["wait_event_types.sgml<br/>(문서 테이블)"]
txt --> pl
pl --> h
pl --> c
pl --> sgml
h -. "WaitEventIO 열거형" .-> wec["wait_event.c<br/>pgstat_get_wait_event"]
c -. "#include at tail" .-> wec
classDef g fill:#efe,stroke:#484;
class txt,pl,h,c,sgml g;
그림 2 — 하나의 선언적 테이블에서 열거형, C 이름 조회, 문서가 생성된다. 단일 소스 덕분에 pg_stat_activity 문자열, C 열거형 기호, 매뉴얼 항목 사이의 불일치가 구조적으로 불가능하다.
PgBackendStatus 슬롯과 changecount 시퀀스 락
섹션 제목: “PgBackendStatus 슬롯과 changecount 시퀀스 락”백엔드의 나머지 실시간 상태는 PgBackendStatus 구조체에 있다. 배열은 포스트마스터 시작 시 한 번 할당되며, 가능한 ProcNumber마다 슬롯 하나다. 가변 길이 문자열(st_appname, st_clienthostname, st_activity_raw)은 별도 공유 버퍼에서 잘라내어 포인터로 참조된다.
// PgBackendStatus core fields — include/utils/backend_status.h (condensed)typedef struct PgBackendStatus{ int st_changecount; /* seqlock version: odd = write in flight */ int st_procpid; /* slot valid iff st_procpid > 0 */ BackendType st_backendType; TimestampTz st_proc_start_timestamp; TimestampTz st_xact_start_timestamp; TimestampTz st_activity_start_timestamp; TimestampTz st_state_start_timestamp; Oid st_databaseid; Oid st_userid; BackendState st_state; /* STATE_RUNNING / STATE_IDLE / ... */ char *st_appname; /* into BackendAppnameBuffer */ char *st_activity_raw; /* into BackendActivityBuffer; may be mid-mb-truncated */ ProgressCommandType st_progress_command; Oid st_progress_command_target; int64 st_progress_param[PGSTAT_NUM_PROGRESS_PARAM]; /* 20 slots */ int64 st_query_id; int64 st_plan_id;} PgBackendStatus;백엔드는 pgstat_beinit이 설정하는 MyBEEntry(MyBEEntry = &BackendStatusArray[MyProcNumber])로 자신의 슬롯에 접근한다. 모든 변경은 시퀀스 락 매크로로 감싸진다. 매크로가 크리티컬 섹션이라는 점이 핵심이다. 그 안에서 발생하는 오류는 PANIC으로 승격된다. st_changecount를 짝수로 복원하는 언와인드가 없기 때문이다. 따라서 괄호로 묶인 영역은 짧고, 직선적이고, 할당이 없어야 한다.
// changecount seqlock macros — include/utils/backend_status.h#define PGSTAT_BEGIN_WRITE_ACTIVITY(beentry) \ do { START_CRIT_SECTION(); \ (beentry)->st_changecount++; \ pg_write_barrier(); } while (0)
#define PGSTAT_END_WRITE_ACTIVITY(beentry) \ do { pg_write_barrier(); \ (beentry)->st_changecount++; \ Assert(((beentry)->st_changecount & 1) == 0); \ END_CRIT_SECTION(); } while (0)pgstat_report_activity가 표준 쓰기자다. 백엔드는 모든 상태 전환마다 tcop/postgres.c에서 이를 호출한다. 비용이 큰 작업(타임스탬프 획득, 문자열 길이 계산)은 모두 크리티컬 섹션 진입 전에 처리하고, 섹션 내부에서는 저장만 수행한다.
// pgstat_report_activity — utils/activity/backend_status.c (condensed)voidpgstat_report_activity(BackendState state, const char *cmd_str){ volatile PgBackendStatus *beentry = MyBEEntry; /* ... handle track_activities disabled: one final DISABLED update ... */
/* fetch everything BEFORE the critical section */ start_timestamp = GetCurrentStatementStartTimestamp(); if (cmd_str != NULL) len = Min(strlen(cmd_str), pgstat_track_activity_query_size - 1); current_timestamp = GetCurrentTimestamp(); /* ... accumulate conn active/idle time on state change ... */
PGSTAT_BEGIN_WRITE_ACTIVITY(beentry); beentry->st_state = state; beentry->st_state_start_timestamp = current_timestamp; if (state == STATE_RUNNING) { beentry->st_query_id = INT64CONST(0); /* reset; set later at parse analysis */ beentry->st_plan_id = INT64CONST(0); } if (cmd_str != NULL) { memcpy(beentry->st_activity_raw, cmd_str, len); beentry->st_activity_raw[len] = '\0'; beentry->st_activity_start_timestamp = start_timestamp; } PGSTAT_END_WRITE_ACTIVITY(beentry);}st_activity_raw는 원시 상태로 저장된다. 멀티바이트 문자 중간에서 잘릴 수 있다. 읽기보다 쓰기가 훨씬 잦기 때문에, 올바른 UTF-8 클리핑 비용은 읽기 측으로 미룬다. pgstat_clip_activity가 그 역할을 맡는다.
배열 읽기: 스냅샷 복사
섹션 제목: “배열 읽기: 스냅샷 복사”모니터링 백엔드는 다른 슬롯을 필드별로 즉석에서 읽지 않는다. pgstat_read_current_status가 트랜잭션당 한 번, 시퀀스 락의 읽기 측을 준수하면서 전체 배열을 로컬 메모리에 복사한다. 항목별 복사 루프는 짝수이고 변하지 않은 st_changecount를 관찰할 때까지 재시도한다.
// pgstat_read_current_status — utils/activity/backend_status.c (condensed)for (;;){ int before_changecount, after_changecount;
pgstat_begin_read_activity(beentry, before_changecount); localentry->backendStatus.st_procpid = beentry->st_procpid; if (localentry->backendStatus.st_procpid > 0) { memcpy(&localentry->backendStatus, unvolatize(PgBackendStatus *, beentry), sizeof(PgBackendStatus)); strcpy(localappname, beentry->st_appname); localentry->backendStatus.st_appname = localappname; /* re-point at local copy */ strcpy(localactivity, beentry->st_activity_raw); localentry->backendStatus.st_activity_raw = localactivity; } pgstat_end_read_activity(beentry, after_changecount);
if (pgstat_read_activity_complete(before_changecount, after_changecount)) break; CHECK_FOR_INTERRUPTS(); /* don't spin forever on a stuck writer */}아웃라인 문자열은 공유 버퍼의 포인터이므로, 복사는 로컬 구조체의 포인터를 로컬 문자열 복사본으로 다시 설정한다. strcpy가 동시 쓰기에 안전한 이유는 각 공유 버퍼가 항상 NUL로 종료되기 때문이다. 교착 상태 감지기는 다른 경로를 택한다. pgstat_get_backend_current_activity는 전체 스냅샷 없이 단일 슬롯을 직접 읽는다. 대상이 블로킹되어 안정적이라는 것을 이미 알기 때문이다.
명령 진행 상황 보고
섹션 제목: “명령 진행 상황 보고”진행 상황 보고는 같은 PgBackendStatus 슬롯을 재사용하되, 다른 필드들을 사용한다. 명령 태그와 20개 정수 벡터다. 명령 태그는 작은 열거형이고, 벡터 너비는 고정되어 있다.
typedef enum ProgressCommandType{ PROGRESS_COMMAND_INVALID, PROGRESS_COMMAND_VACUUM, PROGRESS_COMMAND_ANALYZE, PROGRESS_COMMAND_CLUSTER, PROGRESS_COMMAND_CREATE_INDEX, PROGRESS_COMMAND_BASEBACKUP, PROGRESS_COMMAND_COPY,} ProgressCommandType;
#define PGSTAT_NUM_PROGRESS_PARAM 20명령은 pgstat_progress_start_command(cmdtype, relid)로 진행을 “연다”. 태그, 대상 릴레이션을 설정하고 벡터를 0으로 초기화한다. 모두 시퀀스 락 안에서다.
// pgstat_progress_start_command — utils/activity/backend_progress.cvoidpgstat_progress_start_command(ProgressCommandType cmdtype, Oid relid){ volatile PgBackendStatus *beentry = MyBEEntry; if (!beentry || !pgstat_track_activities) return; PGSTAT_BEGIN_WRITE_ACTIVITY(beentry); beentry->st_progress_command = cmdtype; beentry->st_progress_command_target = relid; MemSet(&beentry->st_progress_param, 0, sizeof(beentry->st_progress_param)); PGSTAT_END_WRITE_ACTIVITY(beentry);}각 명령은 진행하면서 개별 슬롯을 갱신한다. 각 인덱스의 의미는 commands/progress.h에서 명령별로 정의된다(VACUUM의 경우 슬롯 0은 단계, 슬롯 1은 총 힙 블록). pgstat_progress_update_param은 슬롯 하나를 쓰고, pgstat_progress_update_multi_param은 여러 슬롯을 원자적으로 쓴다. 시퀀스 락 괄호 하나로 처리하므로 읽기 측이 절반만 갱신된 벡터를 볼 수 없다.
// pgstat_progress_update_multi_param — utils/activity/backend_progress.c (condensed)voidpgstat_progress_update_multi_param(int nparam, const int *index, const int64 *val){ volatile PgBackendStatus *beentry = MyBEEntry; if (!beentry || !pgstat_track_activities || nparam == 0) return; PGSTAT_BEGIN_WRITE_ACTIVITY(beentry); for (int i = 0; i < nparam; ++i) { Assert(index[i] >= 0 && index[i] < PGSTAT_NUM_PROGRESS_PARAM); beentry->st_progress_param[index[i]] = val[i]; } PGSTAT_END_WRITE_ACTIVITY(beentry);}pgstat_progress_end_command는 태그를 PROGRESS_COMMAND_INVALID로 되돌린다. 진행 뷰가 백엔드의 해당 명령 실행이 종료됐음을 판단하는 신호다.
병렬 워커의 진행 상황
섹션 제목: “병렬 워커의 진행 상황”병렬 CREATE INDEX나 VACUUM은 작업을 여러 워커 프로세스에 분산한다. 그러나 사용자가 읽는 진행 벡터는 리더의 PgBackendStatus에만 있다. 워커는 리더의 슬롯을 쓸 수 없다(단일 쓰기자 불변성). 워커는 병렬 워커 메시지 큐로 리더에게 PqMsg_Progress 메시지를 보낸다. pgstat_progress_parallel_incr_param이 경로를 선택한다.
// pgstat_progress_parallel_incr_param — utils/activity/backend_progress.cvoidpgstat_progress_parallel_incr_param(int index, int64 incr){ if (IsParallelWorker()) { static StringInfoData progress_message; initStringInfo(&progress_message); pq_beginmessage(&progress_message, PqMsg_Progress); pq_sendint32(&progress_message, index); pq_sendint64(&progress_message, incr); pq_endmessage(&progress_message); } else pgstat_progress_incr_param(index, incr); /* leader: write own slot directly */}리더는 병렬 메시지 핸들러(access/transam/parallel.c의 HandleParallelMessage)에서 메시지를 받아 자신의 슬롯에 증분을 적용한다. 단일 쓰기자 불변성이 유지된다. 리더만이 리더의 벡터를 쓴다.
// HandleParallelMessage — access/transam/parallel.c (condensed)case PqMsg_Progress:{ int index = pq_getmsgint(msg, 4); int64 incr = pq_getmsgint64(msg); pq_getmsgend(msg); pgstat_progress_incr_param(index, incr); /* leader updates its own slot */ break;}증분 전용 진행 API만 병렬 변형이 있는 이유가 여기에 있다. “슬롯 N을 V로 설정”을 중계하면 워커 간 경쟁이 생기지만, “슬롯 N에 incr을 더해”는 단일 리더로 직렬화하면 깔끔하게 합성된다.
소스 코드 워크스루
섹션 제목: “소스 코드 워크스루”이 섹션은 호출 흐름별로 그룹화된 안정적인 심볼 목록이다. 줄 번호는 마지막의 위치 힌트 테이블로 미뤘다. 리팩터링이 있어도 심볼 이름은 살아남는다.
대기 이벤트 인코딩과 디코딩 (wait_event.c, wait_event.h, wait_classes.h)
섹션 제목: “대기 이벤트 인코딩과 디코딩 (wait_event.c, wait_event.h, wait_classes.h)”PG_WAIT_LWLOCK…PG_WAIT_INJECTIONPOINT(wait_classes.h) — 10개 클래스 상수. 각각 4바이트 워드의 상위 바이트를 차지한다.WAIT_EVENT_CLASS_MASK/WAIT_EVENT_ID_MASK(wait_event.c) —0xFF000000/0x0000FFFF. 워드를 클래스와 이벤트 ID로 분리한다.pgstat_report_wait_start/pgstat_report_wait_end(wait_event.h, 인라인) — 잠금 프리 단일 워드 게시. 서버에서 가장 뜨거운 계측 경로다.my_wait_event_info에 직접 저장한다.my_wait_event_info/local_my_wait_event_info(wait_event.c) — 리디렉션 가능한 포인터. 공유 메모리 이전에는 로컬 변수를 가리키다가,pgstat_set_wait_event_storage가PGPROC안을 가리키도록 바꾼다.pgstat_set_wait_event_storage/pgstat_reset_wait_event_storage(wait_event.c) — 백엔드 시작/종료 시 게시 대상을 리디렉션/복원한다.pgstat_get_wait_event_type(wait_event.c) — 클래스 바이트 →"LWLock"/"IO"/ … (wait_event_type컬럼).pgstat_get_wait_event(wait_event.c) — 전체 워드 → 이벤트 이름. LWLock/Lock은 서브시스템 코드로, 나머지는 코드 생성된pgstat_get_wait_*으로 디스패치한다.
커스텀(익스텐션) 대기 이벤트 레지스트리 (wait_event.c)
섹션 제목: “커스텀(익스텐션) 대기 이벤트 레지스트리 (wait_event.c)”WaitEventExtensionNew/WaitEventInjectionPointNew— 두 커스텀 클래스의 공개 등록 진입점.WaitEventCustomNew— 이중 확인 잠금으로 할당하거나 찾는 함수. 대기 이벤트 서브시스템에서 실제 잠금이 있는 유일한 경로.WaitEventCustomCounterData/WaitEventCustomCounter— 공유 메모리의 스핀락 보호nextId카운터.WaitEventCustomHashByInfo/WaitEventCustomHashByName— 두 공유 해시 테이블(info→name, name→info).GetWaitEventCustomIdentifier— 커스텀 클래스에서pgstat_get_wait_event가 사용하는 역방향 조회.GetWaitEventCustomNames— 클래스에 등록된 이름을 열거한다(pg_get_wait_events()뒷받침).WaitEventCustomShmemInit/WaitEventCustomShmemSize— 레지스트리의 공유 메모리 설정.
어휘 코드 생성
섹션 제목: “어휘 코드 생성”wait_event_names.txt— 선언적 테이블.Section: ClassName - WaitEvent<Class>헤더 아래 이벤트당 한 줄.generate-wait_event_types.pl— 빌드 시 실행되는 생성기.wait_event_types.h,pgstat_wait_event.c, SGML 문서를 생성한다.WaitEventExtension,WaitEventInjectionPoint,WaitEventLWLock,WaitEventLock은 C 생성을 건너뛴다.WAIT_EVENT_*열거형 멤버 /pgstat_get_wait_<class>— 생성된 것들. 후자는utils/pgstat_wait_event.c가wait_event.c끝에#include로 삽입된다.
백엔드 상태 슬롯 수명 주기 (backend_status.c, backend_status.h)
섹션 제목: “백엔드 상태 슬롯 수명 주기 (backend_status.c, backend_status.h)”PgBackendStatus(backend_status.h) — 백엔드별 슬롯 구조체.st_changecount+PGSTAT_BEGIN_WRITE_ACTIVITY/PGSTAT_END_WRITE_ACTIVITY— 쓰기 측 시퀀스 락(크리티컬 섹션: 내부 오류 → PANIC).pgstat_begin_read_activity/pgstat_end_read_activity/pgstat_read_activity_complete— 읽기 측 시퀀스 락.BackendStatusArray/MyBEEntry— 공유 배열과 백엔드의 자기 슬롯 포인터.BackendStatusShmemInit/BackendStatusShmemSize— 배열과 appname/hostname/activity 문자열 버퍼 할당.pgstat_beinit—MyBEEntry설정, 종료 훅 등록.pgstat_bestart_initial/pgstat_bestart_security/pgstat_bestart_final— 백엔드 시작 시 슬롯을 세 단계로 채운다(STATE_STARTING→ 보안 세부사항 →STATE_UNDEFINED+ appname).pgstat_beshutdown_hook—st_procpid를 0으로 설정해 슬롯을 빈 것으로 표시한다.
활동 보고와 읽기 (backend_status.c)
섹션 제목: “활동 보고와 읽기 (backend_status.c)”pgstat_report_activity— 메인 쓰기자(상태 + 쿼리 텍스트).tcop/postgres.c에서 호출.pgstat_report_appname/pgstat_report_query_id/pgstat_report_plan_id/pgstat_report_xact_timestamp— 단일 필드 대상 쓰기자들.pgstat_read_current_status— 전체 배열을localBackendStatusTable에 스냅샷한다(시퀀스 락 읽기 루프).pgstat_get_beentry_by_proc_number/pgstat_get_local_beentry_by_index/pgstat_fetch_stat_numbackends—pg_stat_get_activitySRF가 사용하는 로컬 스냅샷 접근자들.pgstat_get_backend_current_activity— 교착 상태 감지기가 사용하는 단일 슬롯 직접 읽기.pgstat_get_crashed_backend_activity— 크래시 보고를 위한 포스트마스터 측 의도적 비동기화 읽기.pgstat_clip_activity— 읽기 시st_activity_raw를 멀티바이트 안전하게 잘라낸다.
진행 상황 보고 (backend_progress.c, backend_progress.h)
섹션 제목: “진행 상황 보고 (backend_progress.c, backend_progress.h)”ProgressCommandType+PGSTAT_NUM_PROGRESS_PARAM(backend_progress.h) — 명령 태그 열거형과 고정 20슬롯 벡터 너비.pgstat_progress_start_command— 태그 + 대상 설정, 벡터를 0으로 초기화.pgstat_progress_update_param/pgstat_progress_incr_param/pgstat_progress_update_multi_param— 슬롯 하나 쓰기 / 하나 증분 / 여러 슬롯 원자적 쓰기.pgstat_progress_parallel_incr_param— 워커는PqMsg_Progress발송, 리더는 직접 증분.pgstat_progress_end_command— 태그를PROGRESS_COMMAND_INVALID로 초기화.HandleParallelMessage(parallel.c) — 리더 측PqMsg_Progress핸들러. 워커의 증분을 리더 자신의 슬롯에 적용한다.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
PG_WAIT_LWLOCK … PG_WAIT_INJECTIONPOINT | src/include/utils/wait_classes.h | 18–27 |
WAIT_EVENT_CLASS_MASK / WAIT_EVENT_ID_MASK | src/backend/utils/activity/wait_event.c | 42–43 |
WaitEventCustomShmemInit | src/backend/utils/activity/wait_event.c | 119 |
WaitEventExtensionNew | src/backend/utils/activity/wait_event.c | 163 |
WaitEventCustomNew | src/backend/utils/activity/wait_event.c | 175 |
GetWaitEventCustomIdentifier | src/backend/utils/activity/wait_event.c | 276 |
GetWaitEventCustomNames | src/backend/utils/activity/wait_event.c | 306 |
pgstat_set_wait_event_storage | src/backend/utils/activity/wait_event.c | 349 |
pgstat_get_wait_event_type | src/backend/utils/activity/wait_event.c | 373 |
pgstat_get_wait_event | src/backend/utils/activity/wait_event.c | 431 |
#include "utils/pgstat_wait_event.c" | src/backend/utils/activity/wait_event.c | 506 |
pgstat_report_wait_start / _end (인라인) | src/include/utils/wait_event.h | 68–88 |
| 이름 + 열거형 파생 | src/backend/utils/activity/generate-wait_event_types.pl | 106–130 |
| 열거형 베이스 = 클래스 상수 | src/backend/utils/activity/generate-wait_event_types.pl | 205–212 |
PgBackendStatus 구조체 | src/include/utils/backend_status.h | 98–177 |
PGSTAT_BEGIN/END_WRITE_ACTIVITY | src/include/utils/backend_status.h | 209–222 |
pgstat_begin_read_activity / _complete | src/include/utils/backend_status.h | 224–238 |
BackendStatusShmemInit | src/backend/utils/activity/backend_status.c | 114 |
pgstat_beinit | src/backend/utils/activity/backend_status.c | 245 |
pgstat_bestart_initial | src/backend/utils/activity/backend_status.c | 270 |
pgstat_beshutdown_hook | src/backend/utils/activity/backend_status.c | 509 |
pgstat_report_activity | src/backend/utils/activity/backend_status.c | 572 |
pgstat_read_current_status | src/backend/utils/activity/backend_status.c | 820 |
pgstat_get_backend_current_activity | src/backend/utils/activity/backend_status.c | 996 |
pgstat_clip_activity | src/backend/utils/activity/backend_status.c | 1315 |
ProgressCommandType / PGSTAT_NUM_PROGRESS_PARAM | src/include/utils/backend_progress.h | 22–33 |
pgstat_progress_start_command | src/backend/utils/activity/backend_progress.c | 27 |
pgstat_progress_update_param | src/backend/utils/activity/backend_progress.c | 48 |
pgstat_progress_incr_param | src/backend/utils/activity/backend_progress.c | 69 |
pgstat_progress_parallel_incr_param | src/backend/utils/activity/backend_progress.c | 91 |
pgstat_progress_update_multi_param | src/backend/utils/activity/backend_progress.c | 121 |
pgstat_progress_end_command | src/backend/utils/activity/backend_progress.c | 150 |
PqMsg_Progress 핸들러 | src/backend/access/transam/parallel.c | 1222 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”위의 모든 심볼과 발췌문은 REL_18_STABLE 커밋 273fe94의 /data/hgryoo/references/postgres에서 확인했다. 풀 이후 재실행할 만한 스팟 체크들이다.
- 클래스 상수와 마스크.
grep -n "0x0.000000U" wait_classes.h는 정확히 10개의PG_WAIT_*클래스를 출력해야 한다.WAIT_EVENT_CLASS_MASK(0xFF000000)와WAIT_EVENT_ID_MASK(0x0000FFFF)는wait_event.c상단에 정의되어 있다. 새 클래스가 추가되면 새로운 상위 바이트 값과wait_event_names.txt의 새Section:항목이 생긴다. - 잠금 프리 게시에 track-activities 가드 없음.
wait_event.h의pgstat_report_wait_start는 단순히*(volatile uint32 *) my_wait_event_info = wait_event_info;다. 파일 헤더 주석은pgstat_track_activities확인이 절약하는 것보다 비용이 크다고 명시한다. 가드가 여전히 없는지 확인한다. - 시퀀스 락은 크리티컬 섹션이다.
PGSTAT_BEGIN_WRITE_ACTIVITY는START_CRIT_SECTION()으로 열고PGSTAT_END_WRITE_ACTIVITY는END_CRIT_SECTION()으로 닫는다. 헤더 주석은 그 사이의 오류가 PANIC으로 승격된다고 경고한다. 모든 쓰기자가 타임스탬프와 문자열 길이를 괄호 전에 가져오는 이유다. - 진행 벡터 너비는 20.
backend_progress.h의PGSTAT_NUM_PROGRESS_PARAM은20이다. 명령별 슬롯 관례는commands/progress.h에 있다(예:PROGRESS_VACUUM_*). 여기서는 범위 밖이다. - 병렬 진행은 증분 전용.
pgstat_progress_parallel_incr_param만 존재한다(병렬 set / multi 변형 없음). 리더의parallel.c핸들러는pgstat_progress_incr_param을 호출한다. 양쪽 모두 여전히 증분 형태를 사용하는지 확인한다. - 코드 생성에서 제외되는 네 클래스.
generate-wait_event_types.pl에서next if (...)가드가WaitEventExtension,WaitEventInjectionPoint,WaitEventLWLock,WaitEventLock을 C 생성에서 건너뛴다. SGML만 생성된다. - 빌드 결과물 참고.
wait_event_types.h와pgstat_wait_event.c는 생성된 것들이다. 클린 체크아웃에는 없다. 여기의 발췌문은generate-wait_event_types.pl과wait_event.c끝의#include에서 재구성했다.
PostgreSQL 너머 — 비교 설계와 연구 동향
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 동향”Oracle: 표준 대기 인터페이스
섹션 제목: “Oracle: 표준 대기 인터페이스”PostgreSQL의 대기 이벤트는 Oracle의 Wait Interface를 의식적으로 따온 것이다. “대기 이벤트”를 업계 어휘로 만든 원조 모델이다. Oracle은 세션별 대기 상태를 SGA에 게시하며(v$session_wait, v$session_event, v$system_event), 결정적으로 각 대기를 타이밍한다. time_waited는 이벤트 클래스별 마이크로초를 누적하며, “DB time” / “Active Session History” 방법론의 토대가 된다. PostgreSQL의 핵심 pg_stat_activity는 상대적으로 간소하다. 현재 대기 이벤트를 보여주지만, 코어에서 이벤트별 대기 시간을 누적하지는 않는다. PostgreSQL에서 Oracle 스타일 ASH를 얻는 표준 방법은 샘플링이다. 익스텐션(pg_wait_sampling, 또는 상용 pg_stat_statements 인접 샘플러)이 주기적으로 모든 백엔드의 MyProc->wait_event_info를 읽어 히스토그램화한다. 잠금 프리 단일 워드 설계 덕분에 이런 고빈도 샘플러가 저렴하다. 샘플러는 백엔드가 필요로 하는 잠금을 절대 취득하지 않고 모든 백엔드의 대기 워드를 읽을 수 있다.
MySQL / InnoDB: Performance Schema 계측
섹션 제목: “MySQL / InnoDB: Performance Schema 계측”MySQL의 Performance Schema는 반대 절충점을 취한다. 하나의 범주형 워드 대신, 수천 개의 이름 붙은 계측 지점(wait/synch/mutex/..., wait/io/file/..., stage/sql/...)에 런타임에 구성 가능한 이벤트별 타이머와 집계를 붙인다. “stage” 계측은 PostgreSQL 명령 진행 상황의 MySQL 유사물이다. SELECT ... FROM performance_schema.events_stages_current가 현재 실행 단계를 보여준다. 비용 모델이 크게 다르다. Performance Schema는 시간을 측정하고 집계된 이력을 공유 메모리에 항상 유지한다(문서화된 오버헤드 포함). PostgreSQL은 핫 패스를 단일 비타이밍 저장으로 유지하고 집계를 선택적 샘플러 바깥으로 민다. 이것이 항상 켜진 풍부한 텔레메트리 대 저렴한 원시 신호 + 외부 집계의 고전적 분리다.
SQL Server: 일급 튜닝 수단으로서의 대기 통계
섹션 제목: “SQL Server: 일급 튜닝 수단으로서의 대기 통계”SQL Server는 누적 대기 통계를 직접 노출한다(sys.dm_os_wait_stats, sys.dm_exec_requests.wait_type). 대기 분류 체계(PAGEIOLATCH_*, LCK_M_*, CXPACKET, …)는 성능 튜닝 방법론의 주된 진입점이다. Oracle처럼 엔진이 코어에서 타입별 대기 시간을 누적한다. PostgreSQL이 코어에서 대기를 타이밍하지 않기로 선택한 것은 의식적인 오버헤드 결정이다. 커뮤니티는 코어 내 대기 타이밍 추가를 반복해서 논의해왔지만, 지금까지는 핫 패스에 세금을 부과하지 않으려 샘플링 익스텐션을 선호했다.
시퀀스 락과 잠금 프리 텔레메트리
섹션 제목: “시퀀스 락과 잠금 프리 텔레메트리”st_changecount 프로토콜은 단일 쓰기자/다중 읽기자 시퀀스 락이다. Linux가 gettimeofday 류 빠른 읽기에 쓰는 것과 같은 기본 요소다. 연구 계보는 잠금 프리/대기 프리 문헌을 거슬러 올라간다. 쌍으로 된 메모리 배리어가 있는 단일 쓰기자 레코드는 잠금 없이 동시 읽기자들에게 다중 필드 레코드를 게시하는 가장 저렴한 알려진 방법이다. 의도적인 비대칭 — 저렴한 쓰기, 재시도하는 읽기 — 은 텔레메트리에 정확히 맞는다. 쓰기자는 핫 패스에 있고, 읽기자는 드물게 발생하는 모니터링 쿼리이기 때문이다. PostgreSQL은 가장 뜨거운 필드(대기 워드)를 배리어 없는 단일 저장으로 분리해서 이를 더 밀어붙인다. 순간적인 읽기 측 지연을 허용하는 대신 거의 0에 가까운 쓰기 비용을 얻는다. dbms-general/dbms-papers.md의 apt 참고 문헌은 이를 버전 / 낙관적 읽기에 관한 더 넓은 동시 데이터 구조 연구 아래에 배치한다.
연구 동향
섹션 제목: “연구 동향”- 코어 내 대기 타이밍 / 샘플링. 이벤트별 대기 시간을 코어에서 누적할 것인가(Oracle/SQL Server 방식) 아니면 외부 샘플러에 의존할 것인가는 열린 커뮤니티 절충점이다. 전자의 핫 패스 비용이 걸림돌이다.
- eBPF / USDT 프로브. PostgreSQL은 USDT 추적점을 탑재한다(예:
pgstat_report_activity내부에서TRACE_POSTGRESQL_STATEMENT_STATUS가 발화). 공유 메모리 경로를 완전히 우회하는 프로세스 외부 추적이 가능하다. 낮은 오버헤드의 동적 부착 관찰 가능성의 최전선이다. - 더 풍부한 진행 모델. 현재 진행 벡터는 명령별 관례를 가진 20개 평탄 정수다. 단일 쓰기자 불변성을 깨지 않고 구조적/중첩 단계 보고(병렬 플랜 등)로 확장하는 것은
PqMsg_Progress증분 중계가 암시하는 활성 설계 공간이다. - 익스텐션용 커스텀 대기 이벤트.
WaitEventExtensionNew레지스트리(익스텐션이 단일Extension이벤트에서 충돌하지 않도록 추가됨)는 최근의 일반화다. 인젝션 포인트 대기 이벤트는 결정론적 테스트를 위해 같은 기구를 재사용한다.
- PostgreSQL 소스, REL_18_STABLE @ 273fe94
(
/data/hgryoo/references/postgres):src/backend/utils/activity/wait_event.c— 인코딩/디코딩, 커스텀 레지스트리.src/backend/utils/activity/backend_status.c—PgBackendStatus수명 주기, 활동 보고, 스냅샷 읽기.src/backend/utils/activity/backend_progress.c— 진행 API와 병렬 중계.src/backend/utils/activity/wait_event_names.txt— 선언적 어휘.src/backend/utils/activity/generate-wait_event_types.pl— 코드 생성기.src/include/utils/wait_event.h,wait_classes.h,backend_status.h,backend_progress.h— 인라인 게시, 클래스 상수, 슬롯 구조체 + 시퀀스 락 매크로, 진행 열거형.src/backend/access/transam/parallel.c— 리더 측PqMsg_Progress핸들러.
- 교차 참조 (이 KB):
postgres-cumulative-stats.md— 다른 통계 시스템(단조 카운터,pgstat.c공유 해시). 여기의 라이브 백엔드별 스냅샷과 대조된다.postgres-lwlock-spinlock.md—GetLWLockIdentifier,WaitEventCustomCounter뒤의 스핀락,pgstat_get_wait_event가 위임하는 LWLock 대기 클래스.postgres-wire-protocol.md— 병렬 진행 중계가 재사용하는pq_beginmessage/PqMsg_*프레이밍.
- 교과서 기준점 (
knowledge/research/dbms-general/):- Database System Concepts (Silberschatz, Korth, Sudarshan) — 실행 중 트랜잭션 DBA 모니터링, 잠금/I/O 대기 진단.
- Architecture of a Database System (Hellerstein, Stonebraker, Hamilton) — 프로세스 모델, 공유 메모리에 게시되는 워커별 상태.
- Database Internals (Petrov) — 잠금 프리 단일 쓰기자 텔레메트리, 버전 낙관적 읽기.
- 논문 참고 문헌:
dbms-general/dbms-papers.md(시퀀스 락 / 동시 버전 읽기apt항목). 비교 설계에 대해서는 Oracle Wait Interface와 MySQL Performance Schema 문서도 참조.