(KO) PostgreSQL Lock Manager — 헤비웨이트 잠금 테이블, 잠금 모드, 그리고 데드락 탐지
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
학술적 배경
섹션 제목: “학술적 배경”Lock manager란 트랜잭션이 논리적 객체를 읽거나 쓰기 전에 특정 모드의 잠금을 요청하면, 그 잠금을 승인하거나 요청자를 블록하거나 사이클이 있을 때는 누군가를 abort시키는 비관적 동시성 제어(pessimistic concurrency control) 를 실현하는 컴포넌트다. Database System Concepts(Silberschatz, 7판, 18장 “Concurrency Control”)는 이를 잠금 기반 프로토콜(lock-based protocol) 계열로 분류하고, 그 대표 구성원으로 2단계 잠금(two-phase locking, 2PL) 을 제시한다. 2PL에서 모든 트랜잭션은 잠금을 획득하기만 하는 성장 단계(growing phase) 와 해제만 하는 축소 단계(shrinking phase) 를 거친다. 한 번 해제한 뒤에는 새 잠금을 획득할 수 없다는 점이 핵심이다. 2PL은 충돌 직렬성(conflict-serializability)을 보장한다. 거의 모든 엔진이 채택하는 변형인 strict 2PL 은 배타 잠금을 커밋·어보트 시점까지 유지해 회복 가능성과 연쇄 어보트 방지도 함께 달성한다. PostgreSQL은 트랜잭션 수준 잠금을 트랜잭션 종료 시까지 보유하므로, 잠금을 취하는 범위 내에서 strict-2PL 엔진이다.
실제 lock manager 설계를 좌우하는 두 가지 정제 요소가 있다.
-
잠금 단위와 다중 단위 잠금(multi-granularity locking, MGL). 잠금이 테이블 전체를 대상으로 할 수도 있고, 페이지나 개별 튜플을 대상으로 할 수도 있다. 단위가 클수록 추적 비용은 낮지만 동시성이 떨어지고, 단위가 작을수록 동시성은 높지만 잠금 테이블이 폭발한다. Database System Concepts §18.3(“Multiple Granularity”)은 데이터베이스 → 테이블 → 페이지 → 튜플 계층과 의도 모드(intention mode) (IS, IX, SIX)로 이 문제를 해결한다. S 또는 X 모드로 잠그기 전에 상위 노드에 대응하는 의도 모드를 먼저 잡아 두면, 굵은 단위 잠금 요청자가 전체 하위 트리를 순회하지 않고도 세밀한 잠금 보유자를 발견할 수 있다. 모드 집합의 풍부함과 모드 간 충돌을 정의하는 충돌 테이블이 설계의 핵심이다.
-
데드락 처리. 트랜잭션이 잠금을 요청하는 순서는 예측 불가능하므로 사이클이 생길 수 있다. A가 x를 보유한 채 y를 기다리고, B가 y를 보유한 채 x를 기다리는 경우가 전형적이다. 교과서(§18.2.2)는 두 계열을 제시한다. 예방(prevention) — 타임아웃, wait-die / wound-wait 타임스탬프 순서, 또는 모든 잠금 원자적 선취득 — 과 탐지 후 복구 (detection-and-recovery) — 트랜잭션마다 노드를 두고 A가 B가 보유한 잠금을 기다릴 때 A→B 에지를 추가하는 waits-for graph(WFG) 를 주기적으로 사이클 검사하여 피해자를 abort시키는 방식 — 가 그것이다. PostgreSQL은 탐지 쪽을 선택한다. 사이클이 형성되도록 내버려 두고, 타임아웃이 지난 뒤 WFG 탐색을 수행한다.
Lock manager는 MVCC와 의도적으로 분리되어 있다. 스냅샷 격리 아래서
읽기는 행 잠금을 전혀 취하지 않고 가시적 버전을 읽는다
(postgres-mvcc-visibility.md 참고). 헤비웨이트 lock manager가
존재하는 이유는 MVCC가 버전 관리로 해결할 수 없는 충돌, 즉
쓰기/쓰기 직렬화, DDL 대 DML, 명시적 LOCK TABLE, SELECT ... FOR UPDATE, 그리고 다른 트랜잭션의 완료를 기다리는 상황 때문이다.
읽기 중심 OLTP 워크로드가 충돌 없는 약한(weak) 관계 잠금을 수백만
건씩 취하는 관측, 그것이 fast path를 도입하게 된 동기다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”교과서는 모델을 제공한다(2PL, MGL, WFG). 이 절은 PostgreSQL, Oracle, DB2, SQL Server, MySQL/InnoDB, CUBRID 등 거의 모든 프로덕션 lock manager가 멀티코어 환경에서 그 모델을 실행하기 위해 채택하는 공학적 관례(engineering convention) 를 정리한다. 다음 절에서 살펴볼 PostgreSQL의 구체적 선택들은 이 공유 공간 안에서 조정한 다이얼 값이다.
객체 식별자를 키로 하는 중앙 잠금 테이블
섹션 제목: “객체 식별자를 키로 하는 중앙 잠금 테이블”모든 엔진은 잠금 대상 객체(테이블 OID, 페이지 번호, 튜플 주소, 트랜잭션 ID)를 키로 하는 해시 테이블을 유지한다. 값 레코드는 두 가지가 거의 보편적이다. 하나는 객체당 레코드(해당 객체의 보유자·대기자 큐)이고, 다른 하나는 보유자당 레코드(한 트랜잭션이 이 객체에서 보유·대기하는 정보)다. 객체당 레코드는 모든 승인된 모드를 집계해 두어, 충돌 검사가 목록 순회 없이 비트 연산 하나로 끝나도록 한다.
테이블 구동 충돌 의미론
섹션 제목: “테이블 구동 충돌 의미론”“모드 M1이 모드 M2와 충돌하는가”의 의미는 분기 코드가 아니라 정적 충돌 테이블(2차원 비트 행렬)에 담긴다. 요청 검사는 객체의 집계 “승인된 모드” 마스크에 요청자의 충돌 행을 AND하는 것으로 끝난다. 모드 집합이 분기 코드가 아닌 데이터 이므로, 획득 경로를 손대지 않고 모드를 추가하거나 조정할 수 있다.
백엔드별 잠금 캐시
섹션 제목: “백엔드별 잠금 캐시”한 트랜잭션 내에서 같은 잠금이 여러 번 요청·해제된다. 카탈로그 조회마다
pg_class를 다시 잠그는 것이 전형적인 예다. 공유 메모리를 매번 건드리면
백엔드들이 래치 위에서 직렬화된다. 이를 피하기 위해 모든 엔진은 이미
보유한 잠금의 획득 횟수를 백엔드 로컬 카운트 로 관리한다.
두 번째 이후 획득은 공유 메모리를 전혀 건드리지 않는다.
확장성을 위한 파티셔닝 래칭
섹션 제목: “확장성을 위한 파티셔닝 래칭”잠금 테이블 위의 전역 뮤텍스 하나는 고전적인 멀티코어 병목이다. A Scalable Lock Manager for Multicores(Jung et al., SIGMOD 2013)는 MySQL의 처리량이 16코어에서 붕괴 하는 원인을 읽기 전용·무충돌 워크로드에서도 잠금 테이블 뮤텍스 하나에 대한 캐시 라인 바운싱으로 추적했다. 표준 해법은 잠금 테이블을 키 해시로 파티셔닝 하고 각 파티션에 전용 래치를 부여하는 것이다. 관련 없는 객체들은 서로 경쟁하지 않는다. 해당 논문이 나아간 경계 — 잠금 할당과 잠금 조작을 분리해 조작을 래치-프리로 만드는 것 — 는 정확히 PostgreSQL의 fast path가 다른 각도로 공략하는 지점이다.
일반적이고 충돌 없는 경우를 위한 fast path
섹션 제목: “일반적이고 충돌 없는 경우를 위한 fast path”파티셔닝을 해도 핫 테이블은 모든 잠금 요청자를 하나의 파티션 래치로 집중시킨다. 그래서 엔진들은 탈출구를 추가한다. 잠금이 약하고(충돌 가능성 낮음) 충돌하는 강한 잠금이 없음을 저렴하게 증명할 수 있다면, 그 잠금을 백엔드 전용 배열 에 기록하고 공유 테이블을 완전히 건너뛴다. 비용은 드문 강한 잠금 요청자에게 전가된다. 강한 잠금 요청자는 충돌 검사 전에 모든 백엔드의 fast-path 항목을 공유 테이블로 “플러시”해야 한다.
비동기적으로 지연된 데드락 탐지
섹션 제목: “비동기적으로 지연된 데드락 탐지”블록이 발생할 때마다 WFG 탐색을 수행하면 데드락이 없는 일반적인 경우를
지배하게 된다. 관례는 낙관적 대기(optimistic waiting) 다. 즉, 검사
없이 즉시 블록하고 타이머를 걸어 두며, 대기가 타임아웃을 넘겼을 때만
WFG를 탐색한다. PostgreSQL의 deadlock_timeout(기본값 1초)보다 먼저
해소된 데드락은 비용이 없다.
이론 ↔ PostgreSQL 대응표
섹션 제목: “이론 ↔ PostgreSQL 대응표”| 이론 / 관례 | PostgreSQL 명칭 |
|---|---|
| 잠금 가능 객체 식별자 | LOCKTAG — 16바이트 태그, locktag_type ∈ LockTagType |
| 객체당 보유자·대기자 레코드 | LOCK (공유 LockMethodLockHash) |
| 보유자당 레코드 | PROCLOCK (공유 LockMethodProcLockHash) |
| 백엔드별 잠금 캐시 | LOCALLOCK (백엔드 전용 LockMethodLocalHash) |
| 충돌 테이블 (모드×모드 비트 행렬) | LockConflicts[] + LockMethodData.conflictTab |
| 8가지 잠금 모드 | AccessShareLock … AccessExclusiveLock (lockdefs.h) |
| 집계 승인 모드 마스크 | LOCK.grantMask; 대기 중 모드는 LOCK.waitMask |
| 파티셔닝 래칭 | NUM_LOCK_PARTITIONS (=16) 파티션 LWLock, LOCKTAG 해시 기반 |
| 약한 관계 잠금 fast path | PGPROC.fpRelId[] / fpLockBits[]; FastPathStrongRelationLocks |
| 객체 위의 대기 큐 | LOCK.waitProcs (PGPROC 리스트); 요청은 PGPROC에 저장 |
| Waits-for graph + 탐지 | DeadLockCheck / FindLockCycle (deadlock.c) |
| 낙관적 대기 + 타임아웃 | ProcSleep이 deadlock_timeout을 설정; CheckDeadLock이 탐색 실행 |
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”PostgreSQL의 README(src/backend/storage/lmgr/ 소재)는 이것들을
“regular locks” 또는 “heavyweight locks”라 부르며, 세 가지 가벼운
프로세스 간 잠금 — 스핀락·LWLock(postgres-lwlock-spinlock.md),
SIReadLock 술어 잠금(postgres-ssi-predicate-locking.md) — 과
대비시킨다. 헤비웨이트 매니저만이 “다양한 잠금 모드의 테이블 기반
의미론, 완전한 데드락 탐지, 트랜잭션 종료 시 자동 해제”를 갖춘다.
아래는 그 매니저에 대한 분석이다.
PostgreSQL의 설계를 특징짓는 선택은 다섯 가지다. (1) 잠금 레코드
3계층 — 백엔드 로컬 LOCALLOCK, 공유 LOCK, 공유 PROCLOCK — 으로
반복 획득을 래치 없이 처리하고, 공유 상태에는 백엔드·객체·모드 조합당
하나의 승인만 저장한다. (2) 충돌 의미론이 정적 8×8 비트 테이블 이다.
(3) 공유 해시가 LOCKTAG 해시로 16방향 파티셔닝 된다. (4) fast
path 덕분에 약한 관계 잠금은 공유 메모리를 완전히 우회한다.
(5) 데드락 탐지는 deadlock_timeout까지 지연 되며, 소프트 사이클을
abort 없이 대기 큐 재배치로 해소한다.
잠금 레코드 3계층과 그 관계
섹션 제목: “잠금 레코드 3계층과 그 관계”flowchart LR
subgraph BK["백엔드 전용 메모리"]
LL["LOCALLOCK<br/>tag = (LOCKTAG, mode)<br/>nLocks, lockOwners[]<br/>lock*, proclock*"]
end
subgraph SHM["공유 메모리 (파티셔닝됨)"]
LK["LOCK<br/>tag = LOCKTAG<br/>grantMask, waitMask<br/>granted[], requested[]<br/>procLocks, waitProcs"]
PL["PROCLOCK<br/>tag = (myLock, myProc)<br/>holdMask, releaseMask<br/>groupLeader"]
end
PG["PGPROC<br/>(이 백엔드)<br/>waitLock, waitLockMode<br/>fpRelId[], fpLockBits[]"]
LL -- "lock*" --> LK
LL -- "proclock*" --> PL
PL -- "tag.myLock" --> LK
PL -- "tag.myProc" --> PG
LK -- "procLocks (dlist)" --> PL
LK -- "waitProcs (dclist)" --> PG
그림 1 — 잠금 레코드 3계층. LOCALLOCK은 (객체, 모드)를 키로 하는
백엔드 전용 캐시로, 로컬 획득 횟수를 보관한다. 잠금이 메인 테이블에
있을 때는 공유 LOCK(객체당)과 PROCLOCK(백엔드·객체당)을 가리킨다.
PROCLOCK은 자신의 LOCK의 procLocks 리스트와 소유 PGPROC 양쪽에
이중 연결된다. fast path로 취한 잠금이라면 LOCALLOCK.lock과
.proclock은 NULL이다.
LOCK은 “잠금 가능 객체 하나당”, PROCLOCK은 “잠금·요청자 쌍 하나당”
레코드다(README). 공유 구조는 “잠금 가능 객체·잠금 모드·백엔드 조합당
하나의 잠금 승인만 허용” 한다. 한 트랜잭션이 같은 잠금을 두 번 취하는
경우의 내부 요청 횟수는 LOCALLOCK이 보관하므로 공유 데이터는 건드리지
않아도 된다.
// LOCK — src/include/storage/lock.htypedef struct LOCK{ LOCKTAG tag; /* unique identifier of lockable object */ LOCKMASK grantMask; /* bitmask for lock types already granted */ LOCKMASK waitMask; /* bitmask for lock types awaited */ dlist_head procLocks; /* list of PROCLOCK objects assoc. with lock */ dclist_head waitProcs; /* list of PGPROC objects waiting on lock */ int requested[MAX_LOCKMODES]; /* counts of requested locks */ int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */} LOCK;grantMask는 충돌 검사가 AND하는 집계 마스크다. granted[i]는 그
뒤에 있는 모드별 카운트다. 비트 i가 켜지는 조건은 granted[i] > 0이다.
waitMask는 어떤 대기자가 원하지만 아직 받지 못한 모드를 표시한다.
nRequested가 0으로 떨어지면 LOCK 자체가 해제된다.
// PROCLOCK + PROCLOCKTAG — src/include/storage/lock.htypedef struct PROCLOCKTAG{ LOCK *myLock; /* link to per-lockable-object information */ PGPROC *myProc; /* link to PGPROC of owning backend */} PROCLOCKTAG;
typedef struct PROCLOCK{ PROCLOCKTAG tag; /* unique identifier of proclock object */ PGPROC *groupLeader; /* proc's lock group leader, or proc itself */ LOCKMASK holdMask; /* bitmask for lock types currently held */ LOCKMASK releaseMask; /* bitmask for lock types to be released */ dlist_node lockLink; /* list link in LOCK's list of proclocks */ dlist_node procLink; /* list link in PGPROC's list of proclocks */} PROCLOCK;PROCLOCKTAG가 키로 날 포인터를 쓰는 것은 “PROCLOCK은 자신의 lock이나
proc보다 오래 살아남지 않는다”(README)는 보장 덕분이다. holdMask는
LOCK.grantMask에서 이 백엔드의 몫이다. dlist_node 두 개는 같은
객체를 LOCK의 보유자 리스트와 PGPROC의 보유 잠금 리스트에
동시에 연결한다.
// LOCALLOCK + LOCALLOCKTAG — src/include/storage/lock.htypedef struct LOCALLOCKTAG{ LOCKTAG lock; /* identifies the lockable object */ LOCKMODE mode; /* lock mode for this table entry */} LOCALLOCKTAG;
typedef struct LOCALLOCK{ LOCALLOCKTAG tag; /* unique identifier of locallock entry */ uint32 hashcode; /* copy of LOCKTAG's hash value */ LOCK *lock; /* associated LOCK object, if any */ PROCLOCK *proclock; /* associated PROCLOCK object, if any */ int64 nLocks; /* total number of times lock is held */ int numLockOwners; /* # of relevant ResourceOwners */ int maxLockOwners; /* allocated size of array */ LOCALLOCKOWNER *lockOwners; /* dynamically resizable array */ bool holdsStrongLockCount; /* bumped FastPathStrongRelationLocks */ bool lockCleared; /* we read all sinval msgs for lock */} LOCALLOCK;LOCALLOCK의 태그에 모드 가 포함된다는 점에 주목할 필요가 있다.
백엔드가 접근하는 (객체, 모드) 쌍마다 LOCALLOCK이 하나 존재하는 반면,
LOCK은 모든 모드를 아울러 객체당 하나다. nLocks는 로컬 재진입 횟수다.
lockOwners[]는 어떤 ResourceOwner가 보유하는지 추적해, 서브트랜잭션
롤백 시 자신의 몫만 정확히 해제할 수 있게 한다(postgres-backend-lifecycle.md의
리소스 오너 트리 참고).
LOCKTAG: 16바이트로 모든 잠금 가능 객체를 명명
섹션 제목: “LOCKTAG: 16바이트로 모든 잠금 가능 객체를 명명”// LOCKTAG — src/include/storage/lock.htypedef struct LOCKTAG{ uint32 locktag_field1; /* a 32-bit ID field */ uint32 locktag_field2; /* a 32-bit ID field */ uint32 locktag_field3; /* a 32-bit ID field */ uint16 locktag_field4; /* a 16-bit ID field */ uint8 locktag_type; /* see enum LockTagType */ uint8 locktag_lockmethodid; /* lockmethod indicator */} LOCKTAG;이 구조체는 “악의적 의도로 패딩 없이 16바이트에 맞도록 정의됐다”고
소스가 밝힌다. 패딩 바이트가 있으면 구조체 전체가 해시될 때 임의값이
섞이기 때문이다. locktag_type은 네 개의 ID 필드를 어떻게 읽을지를
결정한다. SET_LOCKTAG_* 매크로들이 그 관례를 인코딩한다. 12가지
타입(LockTagType)은 단위 계층과 여러 비관계 객체를 아우른다.
locktag_type | 식별 방식 (필드) | 대표 용도 |
|---|---|---|
LOCKTAG_RELATION | dbOid, relOid | 테이블 전체 (대부분의 DML/DDL 잠금) |
LOCKTAG_RELATION_EXTEND | dbOid, relOid | 관계에 블록 추가 권한 |
LOCKTAG_DATABASE_FROZEN_IDS | dbOid | pg_database.datfrozenxid 갱신 |
LOCKTAG_PAGE | dbOid, relOid, blocknum | 페이지 하나 (GIN 정리 등) |
LOCKTAG_TUPLE | dbOid, relOid, blocknum, offnum | 물리 튜플 하나 (SELECT FOR …) |
LOCKTAG_TRANSACTION | xid | 트랜잭션 완료 대기 |
LOCKTAG_VIRTUALTRANSACTION | procNumber, lxid | vxid 대기 (CIC, hot standby) |
LOCKTAG_SPECULATIVE_TOKEN | xid, token | 투기적 삽입 (INSERT ... ON CONFLICT) |
LOCKTAG_OBJECT | dbOid, classOid, objOid, objsubid | 비관계 카탈로그 객체 |
LOCKTAG_USERLOCK | (예약) | 구 contrib/userlock |
LOCKTAG_ADVISORY | 사용자 id 4개 | pg_advisory_lock(...) |
LOCKTAG_APPLY_TRANSACTION | dbOid, subOid, xid, objid | 논리 복제 적용 충돌 |
locktag_lockmethodid는 잠금 메서드 를 선택한다. 시스템 주도 잠금에는
DEFAULT_LOCKMETHOD(1), 어드바이저리 잠금에는 USER_LOCKMETHOD(2)를
쓴다. 두 메서드는 같은 모드 집합과 충돌 테이블을 공유한다. 차이는
디버그 추적 플래그와 수명 규칙뿐이다. 어드바이저리·사용자 잠금은
트랜잭션을 넘어 살아남을 수 있다(README §“User Locks” 참고).
8가지 잠금 모드와 충돌 테이블
섹션 제목: “8가지 잠금 모드와 충돌 테이블”// lock modes — src/include/storage/lockdefs.h#define NoLock 0 /* not a lock; "don't get a lock" */#define AccessShareLock 1 /* SELECT */#define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */#define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */#define ShareUpdateExclusiveLock 4 /* VACUUM, ANALYZE, CREATE INDEX CONCURRENTLY */#define ShareLock 5 /* CREATE INDEX (non-concurrent) */#define ShareRowExclusiveLock 6 /* like EXCLUSIVE but allows ROW SHARE */#define ExclusiveLock 7 /* blocks ROW SHARE / SELECT FOR UPDATE */#define AccessExclusiveLock 8 /* ALTER/DROP TABLE, VACUUM FULL, LOCK TABLE */이것들은 테이블 수준 모드다. PostgreSQL의 MGL 계층은 얕다. 일반
MVCC 읽기를 위해 페이지·튜플 S/X 잠금을 취하지 않는다. 하위 모드들
(AccessShareLock, RowShareLock, RowExclusiveLock)은 DML이 취하는
것들로 서로 호환된다. 상위 모드는 DDL·유지 관리용이며 점진적으로 배타
범위가 넓어진다. 충돌 의미론은 배열 하나에 완전히 담긴다.
// LockConflicts[] — src/backend/storage/lmgr/lock.c (condensed)static const LOCKMASK LockConflicts[] = { 0, /* AccessShareLock */ LOCKBIT_ON(AccessExclusiveLock), /* RowShareLock */ LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock), /* RowExclusiveLock */ LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) | LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock), /* ShareUpdateExclusiveLock */ LOCKBIT_ON(ShareUpdateExclusiveLock) | LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) | LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock), /* ShareLock */ LOCKBIT_ON(RowExclusiveLock) | LOCKBIT_ON(ShareUpdateExclusiveLock) | LOCKBIT_ON(ShareRowExclusiveLock) | LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock), /* ShareRowExclusiveLock */ LOCKBIT_ON(RowExclusiveLock) | LOCKBIT_ON(ShareUpdateExclusiveLock) | LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) | LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock), /* ExclusiveLock */ LOCKBIT_ON(RowShareLock) | LOCKBIT_ON(RowExclusiveLock) | LOCKBIT_ON(ShareUpdateExclusiveLock) | LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) | LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock), /* AccessExclusiveLock */ LOCKBIT_ON(AccessShareLock) | LOCKBIT_ON(RowShareLock) | LOCKBIT_ON(RowExclusiveLock) | LOCKBIT_ON(ShareUpdateExclusiveLock) | LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) | LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock)};LockConflicts[i]는 모드 i와 충돌하는 모드 집합이다. 행렬은 대칭이다.
관례적 표현(✗ = 충돌):
| ACS | RS | RE | SUE | S | SRE | E | AE | |
|---|---|---|---|---|---|---|---|---|
| AccessShare (1) | ✗ | |||||||
| RowShare (2) | ✗ | ✗ | ||||||
| RowExclusive (3) | ✗ | ✗ | ✗ | ✗ | ||||
| ShareUpdateExcl (4) | ✗ | ✗ | ✗ | ✗ | ✗ | |||
| Share (5) | ✗ | ✗ | ✗ | ✗ | ✗ | |||
| ShareRowExcl (6) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ||
| Exclusive (7) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | |
| AccessExclusive (8) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
이 표는 테이블 단위로 납작하게 펼쳐진 MGL·의도 모드 그림으로 읽을 수 있다.
AccessShareLock(읽기)은 AccessExclusiveLock(테이블 재작성)과만 충돌하므로
SELECT와 대부분의 DDL-프리 작업은 자유롭게 공존한다. DML 세 모드(1–3)는
서로 호환되는 블록을 형성해, 여러 INSERT/UPDATE/DELETE 백엔드가 한 테이블에
동시에 실행될 수 있다. ShareUpdateExclusiveLock은 자기 자신과 충돌하므로
한 테이블에 VACUUM/ANALYZE/CREATE INDEX CONCURRENTLY가 동시에 하나만
실행된다. 테이블의 공개 인터페이스는 DoLockModesConflict 함수 하나다.
// DoLockModesConflict — src/backend/storage/lmgr/lock.cboolDoLockModesConflict(LOCKMODE mode1, LOCKMODE mode2){ LockMethod lockMethodTable = LockMethods[DEFAULT_LOCKMETHOD];
if (lockMethodTable->conflictTab[mode1] & LOCKBIT_ON(mode2)) return true; return false;}파티셔닝된 공유 해시
섹션 제목: “파티셔닝된 공유 해시”두 공유 해시(LockMethodLockHash는 LOCK용, LockMethodProcLockHash는
PROCLOCK용)는 16방향으로 파티셔닝되어 있다. 8.2 이전에는 전역
LockMgrLock 하나가 모든 것을 보호했고 “경쟁 병목”이 되었다(README).
해결책은 잠금 가능 객체를 LOCKTAG 해시의 하위 비트로 파티션에 배정하고
각 파티션에 전용 LWLock을 두는 것이었다.
// partition selection — src/include/storage/lock.h + lwlock.h#define LOG2_NUM_LOCK_PARTITIONS 4#define NUM_LOCK_PARTITIONS (1 << LOG2_NUM_LOCK_PARTITIONS) /* = 16 */
#define LockHashPartition(hashcode) \ ((hashcode) % NUM_LOCK_PARTITIONS)#define LockHashPartitionLock(hashcode) \ (&MainLWLockArray[LOCK_MANAGER_LWLOCK_OFFSET + \ LockHashPartition(hashcode)].lock)일반적인 획득·해제는 하나의 파티션 잠금만 건드린다. 파티션 잠금 하나로
두 해시를 모두 커버할 수 있는 비결은 PROCLOCK 전용 해시 함수에 있다.
이 함수는 PROCLOCK을 자신의 LOCK과 같은 파티션에 강제 배치한다.
// proclock_hash — src/backend/storage/lmgr/lock.cstatic uint32proclock_hash(const void *key, Size keysize){ const PROCLOCKTAG *proclocktag = (const PROCLOCKTAG *) key; uint32 lockhash; Datum procptr;
/* Look into the associated LOCK object, and compute its hash code */ lockhash = LockTagHashCode(&proclocktag->myLock->tag);
/* xor the proc address in, left-shifted past the partition-number bits */ procptr = PointerGetDatum(proclocktag->myProc); lockhash ^= ((uint32) procptr) << LOG2_NUM_LOCK_PARTITIONS; return lockhash;}proc 포인터는 하위 4비트 위에 믹스인되므로 파티션 번호는 LOCK에서
그대로 상속되고, 나머지 해시 비트는 PROCLOCK들을 해시 체인에 분산시킨다.
전체 그래프를 봐야 하는 데드락 검사는 모든 16개 파티션을 파티션 번호
순서로 잠근다. 순서를 고정하면 파티션 잠금들 사이의 LWLock 데드락이
발생하지 않는다.
LockAcquire: 획득 경로
섹션 제목: “LockAcquire: 획득 경로”LockAcquire(및 내부 구현 LockAcquireExtended)는 매니저의 핵심이다.
제어 흐름에는 네 가지 정상 출구 — 이미 보유(로컬 카운트 증가),
fast-path 승인, 즉시 공유 테이블 승인, 대기 후 승인 — 와 데드락·nowait
오류 출구가 있다.
flowchart TD
A["LockAcquireExtended(tag, mode)"] --> B["LOCALLOCK 찾기/생성<br/>(백엔드 전용 해시)"]
B --> C{"locallock.nLocks > 0?<br/>(이미 보유)"}
C -- "예" --> C1["GrantLockLocal:<br/>로컬 카운트 증가, 반환"]
C -- "아니오" --> D{"관계 fast path<br/>대상인가?"}
D -- "예" --> E{"파티션 강한 잠금<br/>카운트 == 0?"}
E -- "예" --> E1["FastPathGrantRelationLock<br/>→ PGPROC 배열에 기록,<br/>LOCKACQUIRE_OK 반환"]
E -- "아니오" --> F
D -- "아니오" --> F{"fast path와 충돌하는<br/>강한 잠금인가?"}
F -- "예" --> F1["BeginStrongLockAcquire +<br/>FastPathTransferRelationLocks<br/>(다른 백엔드 fp 잠금 플러시)"]
F1 --> G
F -- "아니오" --> G["LWLockAcquire(partitionLock)"]
G --> H["SetupLockInTable:<br/>LOCK + PROCLOCK 찾기/생성"]
H --> I{"conflictTab[mode] & waitMask<br/>OR LockCheckConflicts?"}
I -- "충돌 없음" --> J["GrantLock → OK"]
I -- "충돌" --> K["JoinWaitQueue(dontWait)"]
K --> L{"결과?"}
L -- "OK (즉시 승인)" --> J
L -- "ERROR (조기 데드락<br/>또는 dontWait)" --> M["정리, DeadLockReport<br/>또는 NOT_AVAIL 반환"]
L -- "WAITING" --> N["WaitOnLock → ProcSleep<br/>(세마포어 대기)"]
N --> O{"승인 또는<br/>데드락?"}
O -- "승인" --> J
O -- "데드락" --> M
그림 2 — LockAcquireExtended 제어 흐름. 저렴한 두 출구(로컬 재획득,
fast-path 승인)는 공유 메모리를 전혀 건드리지 않는다. 진짜 공유 테이블
충돌이 발생할 때만 백엔드가 JoinWaitQueue → WaitOnLock → ProcSleep으로
내려간다. 강한 잠금 획득 시에는 먼저 모든 백엔드의 해당 fast-path 잠금을
공유 테이블로 옮겨, 충돌·데드락 탐지가 전체를 볼 수 있게 한다.
이미 보유 검사가 가장 첫 번째이자 가장 저렴한 출구다.
// LockAcquireExtended — src/backend/storage/lmgr/lock.c (condensed)locallock = (LOCALLOCK *) hash_search(LockMethodLocalHash, &localtag, HASH_ENTER, &found);/* ... initialize if !found ... */
/* If we already hold the lock, just increase the count locally. */if (locallock->nLocks > 0){ GrantLockLocal(locallock, owner); if (locallock->lockCleared) return LOCKACQUIRE_ALREADY_CLEAR; else return LOCKACQUIRE_ALREADY_HELD;}이미 보유하지 않은 경우, 공유 테이블 충돌 검사는 두 단계다. 먼저 대기자
상태에 대한 저렴한 마스크 검사, 그다음 전체 LockCheckConflicts다.
// LockAcquireExtended — shared-table grant decision (condensed)proclock = SetupLockInTable(lockMethodTable, MyProc, locktag, hashcode, lockmode);/* ... */locallock->proclock = proclock;lock = proclock->tag.myLock;locallock->lock = lock;
/* If request conflicts with waiters, must queue; else check held locks. */if (lockMethodTable->conflictTab[lockmode] & lock->waitMask) found_conflict = true;else found_conflict = LockCheckConflicts(lockMethodTable, lockmode, lock, proclock);
if (!found_conflict){ GrantLock(lock, proclock, lockmode); /* No conflict — granted */ waitResult = PROC_WAIT_STATUS_OK;}else waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait);waitMask 먼저 검사하는 것이 FIFO 공정성 구현이다. 새 요청자가 당장
승인받을 수 있더라도 비호환 대기자보다 앞서 나갈 경우 큐에 넣는다.
그렇지 않으면 호환되는 약한 잠금 스트림이 대기 중인 강한 잠금 요청자를
굶릴 수 있기 때문이다.
GrantLock은 공유 구조에 승인을 기록하는 장부 처리다.
// GrantLock — src/backend/storage/lmgr/lock.cvoidGrantLock(LOCK *lock, PROCLOCK *proclock, LOCKMODE lockmode){ lock->nGranted++; lock->granted[lockmode]++; lock->grantMask |= LOCKBIT_ON(lockmode); if (lock->granted[lockmode] == lock->requested[lockmode]) lock->waitMask &= LOCKBIT_OFF(lockmode); proclock->holdMask |= LOCKBIT_ON(lockmode);}LockCheckConflicts: 그룹 잠금 감산을 포함한 충돌 검사
섹션 제목: “LockCheckConflicts: 그룹 잠금 감산을 포함한 충돌 검사”// LockCheckConflicts — src/backend/storage/lmgr/lock.c (condensed)int conflictMask = lockMethodTable->conflictTab[lockmode];
/* global conflict: does any granted mode conflict with my request? */if (!(conflictMask & lock->grantMask)) return false; /* nobody conflicts — granted */
/* Subtract out locks I already hold myself. */myLocks = proclock->holdMask;for (i = 1; i <= numLockModes; i++){ if ((conflictMask & LOCKBIT_ON(i)) == 0) { conflictsRemaining[i] = 0; continue; } conflictsRemaining[i] = lock->granted[i]; if (myLocks & LOCKBIT_ON(i)) --conflictsRemaining[i]; totalConflictsRemaining += conflictsRemaining[i];}if (totalConflictsRemaining == 0) return false; /* only my own locks conflicted */
/* If no group locking, it's definitely a conflict. */if (proclock->groupLeader == MyProc && MyProc->lockGroupLeader == NULL) return true;/* ... otherwise subtract conflicts held by my parallel lock-group peers ... */첫 번째 AND가 일반적인 경우의 빠른 검사다. 감산은 “실제로는 충돌이
아닌 충돌” 두 가지를 처리한다. 프로세스는 자신이 이미 보유한 잠금과
충돌하지 않는다(약한 잠금을 보유한 채 더 강한 잠금을 취할 수 있다).
그리고 병렬 쿼리 이후, 같은 잠금 그룹(리더와 병렬 워커들)의 구성원은
서로의 잠금을 충돌로 취급하지 않는다. 단, LOCKTAG_RELATION_EXTEND는
그룹 내에서도 충돌한다. 그룹 잠금이 있는 이유는 병렬 워커가 자신의
리더가 보유한 잠금에 자기 데드락을 일으키지 않도록 하기 위해서다.
README §“Group Locking”은 워커의 잠금 요청을 예측하는 방식을 왜
거부하고 그룹 내 잠금을 비충돌로 선언하는 쪽을 선택했는지 설명한다.
약한 관계 잠금을 위한 fast path
섹션 제목: “약한 관계 잠금을 위한 fast path”읽기 중심 워크로드가 테이블 하나를 집중 공략하면 모든 백엔드가 같은
파티션 잠금으로 몰린다. “2코어 서버에서도 측정 가능하고, 코어 수가
늘수록 아주 두드러진다”(README). 9.2부터 도입된 fast path는 백엔드가
약한 관계 잠금(AccessShareLock, RowShareLock, RowExclusiveLock —
모두 ShareUpdateExclusiveLock 미만)을 자신의 PGPROC 안의 전용 배열에
기록하고 공유 해시를 전혀 건드리지 않도록 한다.
// PGPROC fast-path fields — src/include/storage/proc.hLWLock fpInfoLock; /* protects per-backend fast-path state */uint64 *fpLockBits; /* lock modes held for each fast-path slot */Oid *fpRelId; /* slots for rel oids */슬롯은 그룹으로 묶이며(FP_LOCK_SLOTS_PER_GROUP = 16 슬롯/그룹), 관계는
FAST_PATH_REL_GROUP으로 그룹에 해시된다. 슬롯당 3비트가 세 가지 약한
모드 중 어느 것을 보유하는지 인코딩한다. 대상 여부 판단과 승인 로직은
다음과 같다.
// EligibleForRelationFastPath + grant — src/backend/storage/lmgr/lock.c (condensed)#define EligibleForRelationFastPath(locktag, mode) \ ((locktag)->locktag_lockmethodid == DEFAULT_LOCKMETHOD && \ (locktag)->locktag_type == LOCKTAG_RELATION && \ (locktag)->locktag_field1 == MyDatabaseId && \ MyDatabaseId != InvalidOid && \ (mode) < ShareUpdateExclusiveLock)
if (EligibleForRelationFastPath(locktag, lockmode) && /* group not full */ ){ uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode); LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE); if (FastPathStrongRelationLocks->count[fasthashcode] != 0) acquired = false; /* a strong locker is present — fall back */ else acquired = FastPathGrantRelationLock(locktag->locktag_field2, lockmode); LWLockRelease(&MyProc->fpInfoLock); if (acquired) { locallock->lock = NULL; locallock->proclock = NULL; GrantLockLocal(locallock, owner); return LOCKACQUIRE_OK; }}정확성의 핵심은 FastPathStrongRelationLocks다. 1024방향 강한 잠금 카운트
배열(“잠금 공간의 1024방향 파티셔닝”, README)이다.
// FastPathStrongRelationLockData — src/backend/storage/lmgr/lock.c#define FAST_PATH_STRONG_LOCK_HASH_PARTITIONS (1 << 10) /* = 1024 */typedef struct { slock_t mutex; uint32 count[FAST_PATH_STRONG_LOCK_HASH_PARTITIONS];} FastPathStrongRelationLockData;static volatile FastPathStrongRelationLockData *FastPathStrongRelationLocks;약한 잠금 요청자는 파티션의 강한 잠금 카운트가 0일 때만 fast path를 쓸 수
있다. 강한 잠금 요청자(ShareUpdateExclusiveLock 초과)가 도착하면 반대
과정이 일어난다. 강한 잠금 카운트를 올리고, 모든 백엔드의 해당 fast-path
항목을 공유 테이블로 이전해 충돌·데드락 기구가 전체를 볼 수 있게
한다.
sequenceDiagram
participant W as 약한 잠금 요청자 (DML)
participant FP as MyProc fast-path 배열
participant CNT as FastPathStrongRelationLocks
participant S as 강한 잠금 요청자 (DDL)
participant SHM as 공유 LOCK 테이블
Note over W,CNT: 일반 경우 — 강한 잠금 없음
W->>CNT: count[partition] == 0 확인
W->>FP: FastPathGrantRelationLock (비트 설정), OK 반환
Note over S,SHM: 강한 잠금 도착
S->>CNT: BeginStrongLockAcquire: count[partition]++
S->>FP: FastPathTransferRelationLocks: 모든 PGPROC 순회
FP->>SHM: 각 fp 항목에 SetupLockInTable + GrantLock
S->>SHM: 이제 (완전해진) 공유 테이블로 충돌 검사
그림 3 — Fast-path 상호작용. 메모리 순서 보장(README): LWLock 획득은
시퀀스 포인트이므로, 자신의 fpInfoLock을 보유한 채 강한 잠금 카운트가
0임을 읽은 약한 잠금 요청자는 안전하다. 강한 잠금 요청자는 잠금을
이전하려면 모든 백엔드의 fpInfoLock을 순서대로 획득해야 하고, 따라서
자신의 카운트 증가 이후에 취해진 약한 잠금도 반드시 보게 된다.
“데드락 탐지는 fast-path 자료 구조를 검사할 필요가 없다. 데드락에
연루될 수 있는 잠금은 사전에 메인 테이블로 이전됐기 때문이다.”
대기: JoinWaitQueue와 ProcSleep
섹션 제목: “대기: JoinWaitQueue와 ProcSleep”요청이 충돌할 때 JoinWaitQueue는 LOCK.waitProcs의 어디에 이 백엔드를
넣을지 결정한다. 기본은 꼬리(FIFO)다. 그러나 데드락을 선제 차단하는
예외가 하나 있다. 요청자가 이미 어떤 앞선 대기자의 요청과 충돌하는 잠금을
보유하고 있다면, 그 대기자 바로 앞에 삽입한다. 그렇지 않으면
ProcLockWakeup이 앞선 대기자를 먼저 깨워야 하므로, 데드락 탐지기가
나중에 풀어야 할 대기 사이클이 생긴다.
// JoinWaitQueue — src/backend/storage/lmgr/proc.c (condensed)dclist_foreach(iter, waitQueue){ PGPROC *proc = dlist_container(PGPROC, links, iter.cur); /* Must he wait for me? (my held locks conflict with his request) */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { /* Must I wait for him? (his held locks conflict with my request) */ if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) { RememberSimpleDeadLock(MyProc, lockmode, lock, proc); early_deadlock = true; /* immediate, no timeout wait */ break; } /* I go before him; if I conflict with nothing ahead, grant now */ if ((lockMethodTable->conflictTab[lockmode] & aheadRequests) == 0 && !LockCheckConflicts(lockMethodTable, lockmode, lock, proclock)) { GrantLock(lock, proclock, lockmode); return PROC_WAIT_STATUS_OK; } insert_before = proc; break; } aheadRequests |= LOCKBIT_ON(proc->waitLockMode);}PGPROC 대기 필드들(waitLock, waitProcLock, waitLockMode,
heldLocks)은 WFG의 발신 에지다. 이 백엔드가 무엇 때문에 블록됐는지를
정확히 기록한다. 큐에 들어가면 ProcSleep이 deadlock_timeout 타이머를
설정하고 승인 신호나 탐지 신호가 올 때까지 백엔드의 세마포어에서 블록된다.
// ProcSleep — src/backend/storage/lmgr/proc.c (the timeout arming, condensed)deadlock_state = DS_NOT_YET_CHECKED;got_deadlock_timeout = false;/* Set a timer so CheckDeadLock() fires after deadlock_timeout ms. *//* ... enable_timeout_after(DEADLOCK_TIMEOUT, DeadlockTimeout) ... */보유자가 해제하면 ProcLockWakeup이 대기 큐를 앞에서부터 순회하며,
보유 잠금이나 앞선 깨울 수 없는 대기자의 요청 중 어느 것과도 충돌하지
않는 대기자들을 깨운다. 충돌하는 요청들 사이에서는 도착 순서가 보존된다.
// ProcLockWakeup — src/backend/storage/lmgr/proc.c (condensed)dclist_foreach_modify(miter, waitQueue){ PGPROC *proc = dlist_container(PGPROC, links, miter.cur); LOCKMODE lockmode = proc->waitLockMode; if ((lockMethodTable->conflictTab[lockmode] & aheadRequests) == 0 && !LockCheckConflicts(lockMethodTable, lockmode, lock, proc->waitProcLock)) { GrantLock(lock, proc->waitProcLock, lockmode); ProcWakeup(proc, PROC_WAIT_STATUS_OK); } else aheadRequests |= LOCKBIT_ON(lockmode); /* keep blocking those behind */}데드락 탐지: waits-for graph
섹션 제목: “데드락 탐지: waits-for graph”deadlock_timeout이 잠금 승인 전에 만료되면 시그널 핸들러가 CheckDeadLock을
실행한다. 이 함수는 16개 파티션 모두를 잠그고 DeadLockCheck를 호출한다.
그래프는 표준 WFG다. 프로세스마다 노드 하나, A가 B가 보유한 잠금을 기다릴
때(또는 충돌하는 요청에서 A가 B 뒤에 있을 때) A→B 에지가 추가된다.
PostgreSQL은 에지를 두 종류로 구분한다.
- 하드 에지 — B가 A의 요청과 충돌하는 잠금을 이미 보유한다. 누군가를 abort하지 않고는 해소할 수 없다.
- 소프트 에지 — B가 충돌하는 요청으로 대기 큐에서 A보다 앞에
있을 뿐이라
ProcLockWakeup이 B를 먼저 깨운다. 대기 큐를 재배치하면 해소할 수 있다.
flowchart TD
A["CheckDeadLock 실행<br/>(deadlock_timeout 만료)"] --> B["파티션 순서대로<br/>16개 파티션 잠금"]
B --> C{"아직 대기 큐에<br/>있는가?"}
C -- "아니오 (그새 깨어남)" --> Z["잠금 해제, 재개"]
C -- "예" --> D["DeadLockCheck →<br/>DeadLockCheckRecurse"]
D --> E["FindLockCycle: 하드 + 소프트<br/>waits-for 에지를 DFS"]
E --> F{"시작점을 통한<br/>사이클?"}
F -- "사이클 없음" --> G["DS_NO_DEADLOCK<br/>(또는 DS_BLOCKED_BY_AUTOVACUUM)"]
F -- "사이클, 전부 하드 에지" --> H["DS_HARD_DEADLOCK<br/>→ 이 트랜잭션 abort"]
F -- "소프트 에지 포함 사이클" --> I["소프트 에지 역전 시도:<br/>대기 큐 위상 정렬"]
I --> J{"재배치로 새 사이클<br/>없이 모든 사이클<br/>제거 가능?"}
J -- "예" --> K["DS_SOFT_DEADLOCK:<br/>새 대기 순서 적용,<br/>ProcLockWakeup"]
J -- "아니오 (소진)" --> H
G --> Z
K --> Z
그림 4 — 데드락 탐지 상태 공간. 재귀 탐색(DeadLockCheckRecurse)은 각
소프트 에지 역전을 위상 정렬 제약으로 시도하며, 모든 사이클을 제거하는
큐 재배치를 찾거나 그런 것이 없음(하드 데드락)을 증명할 때까지 반복한다.
그 후에야 트랜잭션을 abort한다.
FindLockCycleRecurse가 DFS 구현이다. 방문한 proc을 표시하며, 인덱스 0(시작
proc)으로 돌아오면 데드락 사이클이고, 시작점을 통하지 않는 사이클은 “다른
누군가의 데드락”이므로 무시한다. 그 사이클을 닫은 프로세스가 자신의
타임아웃이 만료될 때 탐지한다.
// FindLockCycleRecurse — src/backend/storage/lmgr/deadlock.c (condensed)for (i = 0; i < nVisitedProcs; i++){ if (visitedProcs[i] == checkProc) { if (i == 0) /* returned to start: real cycle */ { nDeadlockDetails = depth; return true; } return false; /* cycle not through start: ignore */ }}visitedProcs[nVisitedProcs++] = checkProc;하드 에지 스캔은 대기 중인 잠금의 procLocks에서 충돌하는 holdMask를
가진 보유자를 찾는다. 소프트 에지 스캔은 대기 큐(또는 가상의 재배치된
큐)에서 충돌하는 요청을 가진 앞선 대기자를 찾는다.
// FindLockCycleRecurseMember — hard-edge scan (condensed)dlist_foreach(proclock_iter, &lock->procLocks){ PROCLOCK *proclock = dlist_container(PROCLOCK, lockLink, proclock_iter.cur); proc = proclock->tag.myProc; leader = proc->lockGroupLeader == NULL ? proc : proc->lockGroupLeader; if (leader != checkProcLeader) /* never blocks self/group */ for (lm = 1; lm <= numLockModes; lm++) if ((proclock->holdMask & LOCKBIT_ON(lm)) && (conflictMask & LOCKBIT_ON(lm))) { if (FindLockCycleRecurse(proc, depth + 1, softEdges, nSoftEdges)) return true; /* hard edge in a cycle */ /* note an autovacuum directly hard-blocking us, for cancel */ if (checkProc == MyProc && proc->statusFlags & PROC_IS_AUTOVACUUM) blocking_autovacuum_proc = proc; break; }}탐지기는 세 가지 결과를 낸다(DeadLockState). DS_NO_DEADLOCK,
DS_SOFT_DEADLOCK(위상 정렬 기반 큐 재배치로 해소 — 에지 역전 탐색은
README 참고), DS_HARD_DEADLOCK(DeadLockReport로 abort, 반환 없음)이다.
네 번째 코드 DS_BLOCKED_BY_AUTOVACUUM은 특수 신호다. “데드락은 아니지만
autovacuum 워커가 방해하는 중”이라는 뜻으로, 탐지기를 “남용”(README)해
autovacuum의 낮은 잠금 우선순위를 구현한다. 블록된 백엔드가 직접 방해하는
autovacuum을 기다리는 대신 취소 신호를 보낼 수 있다.
README가 증명하는 두 가지 중요한 보장이 있다. 비동기적 검사로 데드락을
놓치는 일은 없다. 사이클을 닫는 마지막 프로세스가 자신의 타임아웃이
만료될 때 반드시 탐지한다. 소프트 에지 재배치는 새 데드락을 만들지 않는다.
적용 전에 FindLockCycle을 재실행해 검증하기 때문이다.
해제와 트랜잭션 종료
섹션 제목: “해제와 트랜잭션 종료”LockRelease는 LOCALLOCK 카운트를 줄인다. 0에 도달할 때만 공유 메모리를
건드리거나(또는 fast-path 비트를 지운다). 트랜잭션 종료 시 호출되는
LockReleaseAll은 백엔드의 LOCALLOCK 해시와 fast-path 배열을 한 번에
순회하고, 각 해제된 객체의 큐를 ProcLockWakeup한다. 트랜잭션 잠금은
커밋·어보트까지 살아 있으므로, strict-2PL의 축소 단계가 여기서 한꺼번에
발생한다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”줄 번호가 아닌 심볼 이름 으로 찾는다. lmgr 소스는 릴리스마다 이동하지만 함수·구조체 이름은 대부분의 리팩터링에서 살아남는다.
git grep -n '<symbol>' src/backend/storage/lmgr/으로 현재 위치를 확인할 수 있다. 아래 줄 번호는 커밋 273fe94(REL_18) 기준 힌트다.
구조체 (src/include/storage/)
섹션 제목: “구조체 (src/include/storage/)”struct LOCKTAG,enum LockTagType(lock.h) — 16바이트 객체 키와 12가지 객체 타입;SET_LOCKTAG_*매크로가 각 타입을 인코딩.struct LOCK(lock.h) — 객체당 레코드:grantMask,waitMask,granted[]/requested[],procLocks,waitProcs.struct PROCLOCK+PROCLOCKTAG(lock.h) — 백엔드·객체당:holdMask,groupLeader, 두 개의dlist_node링크.struct LOCALLOCK+LOCALLOCKTAG(lock.h) — 백엔드 전용 캐시, (객체, 모드) 키;nLocks,lockOwners[].enum DeadLockState(lock.h) — 탐지기의 네 가지 판정.- 잠금 모드 상수
AccessShareLock…AccessExclusiveLock,MaxLockMode(lockdefs.h). struct PGPROCfast-path + 대기 필드fpRelId/fpLockBits/fpInfoLock,waitLock/waitProcLock/waitLockMode/heldLocks(proc.h).
충돌 의미론 (src/backend/storage/lmgr/lock.c)
섹션 제목: “충돌 의미론 (src/backend/storage/lmgr/lock.c)”LockConflicts[]— 8×8 충돌 비트 행렬.lock_mode_names[],default_lockmethod,user_lockmethod,LockMethods[]— 잠금 메서드 테이블.DoLockModesConflict— 두 모드의 충돌 여부 공개 테스트.LockCheckConflicts— 자기 잠금·그룹 감산을 포함한 전체 충돌 검사.
해시 + 파티션 (lock.c)
섹션 제목: “해시 + 파티션 (lock.c)”LockTagHashCode—LOCKTAG해시; 하위 4비트가 파티션 번호.proclock_hash/ProcLockHashCode—PROCLOCK을 자신의LOCK의 파티션에 강제 배치.LockHashPartition,LockHashPartitionLock(매크로,lock.h).
획득·승인 (lock.c)
섹션 제목: “획득·승인 (lock.c)”LockAcquire/LockAcquireExtended— 획득 상태 기계.SetupLockInTable— 공유LOCK과PROCLOCK찾기·생성.GrantLock/GrantLockLocal— 공유·로컬 상태에 승인 기록.UnGrantLock/CleanUpLock— 역방향; 빈 객체 가비지 컬렉션.
Fast path (lock.c)
섹션 제목: “Fast path (lock.c)”FastPathGrantRelationLock/FastPathUnGrantRelationLock— 백엔드 전용 배열 조작.FastPathTransferRelationLocks— 강한 잠금 도착 시 모든 백엔드의 fast-path 항목을 공유 테이블로 플러시.FastPathStrongRelationLockData/FastPathStrongLockHashPartition— 1024방향 강한 잠금 카운터.BeginStrongLockAcquire/FinishStrongLockAcquire/AbortStrongLockAcquire.
대기 큐 (proc.c)
섹션 제목: “대기 큐 (proc.c)”JoinWaitQueue— 큐 위치 선택; 즉시(조기) 데드락 탐지.ProcSleep—deadlock_timeout설정, 세마포어 블록.ProcLockWakeup— 해제 시 도착 순서대로 깨울 수 있는 대기자 깨우기.CheckDeadLock— 타임아웃 핸들러: 모든 파티션 잠금 후 탐지기 실행.RemoveFromWaitQueue(lock.c) — proc 디큐 (데드락 피해자·취소).
데드락 탐지기 (deadlock.c)
섹션 제목: “데드락 탐지기 (deadlock.c)”DeadLockCheck— 진입점;DeadLockState반환, 소프트 수정 적용.DeadLockCheckRecurse/TestConfiguration— 소프트 에지 역전 탐색.FindLockCycle/FindLockCycleRecurse/FindLockCycleRecurseMember— 하드·소프트 에지를 통한 WFG DFS.ExpandConstraints/TopoSort— 가상의 재배치된 대기 큐 구성.DeadLockReport— 피해자를 abort하는ERROR발생.InitDeadLockChecking—MaxBackends에서 최악의 경우 작업 공간 사전 할당.
래퍼 (lmgr.c)
섹션 제목: “래퍼 (lmgr.c)”LockRelationOid/UnlockRelationOid/LockRelation— 백엔드 대부분이 실제로 호출하는 관계 잠금 진입점.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
LockConflicts[] | lock.c | 65 |
lock_mode_names[] | lock.c | 108 |
LockMethods[] | lock.c | 150 |
FastPathStrongRelationLockData | lock.c | 306 |
LockTagHashCode | lock.c | 556 |
proclock_hash | lock.c | 573 |
DoLockModesConflict | lock.c | 622 |
LockAcquire | lock.c | 808 |
LockAcquireExtended | lock.c | 835 |
SetupLockInTable | lock.c | 1282 |
LockCheckConflicts | lock.c | 1528 |
GrantLock | lock.c | 1657 |
UnGrantLock | lock.c | 1680 |
CleanUpLock | lock.c | 1737 |
LockRelease | lock.c | 2070 |
LockReleaseAll | lock.c | 2275 |
FastPathGrantRelationLock | lock.c | 2750 |
FastPathTransferRelationLocks | lock.c | 2829 |
VirtualXactLock | lock.c | 4713 |
JoinWaitQueue | proc.c | 1173 |
ProcSleep | proc.c | 1342 |
ProcLockWakeup | proc.c | 1772 |
CheckDeadLock | proc.c | 1820 |
DeadLockCheck | deadlock.c | 220 |
DeadLockCheckRecurse | deadlock.c | 312 |
FindLockCycle | deadlock.c | 446 |
FindLockCycleRecurse | deadlock.c | 457 |
FindLockCycleRecurseMember | deadlock.c | 536 |
TopoSort | deadlock.c | 862 |
DeadLockReport | deadlock.c | 1075 |
LockRelationOid | lmgr.c | 107 |
struct LOCKTAG | lock.h | 165 |
struct LOCK | lock.h | 309 |
struct PROCLOCK | lock.h | 370 |
struct LOCALLOCK | lock.h | 427 |
enum DeadLockState | lock.h | 509 |
AccessShareLock … MaxLockMode | lockdefs.h | 36 |
NUM_LOCK_PARTITIONS | lwlock.h | 97 |
FP_LOCK_SLOTS_PER_GROUP | proc.h | 98 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”각 항목은 커밋 273fe94(REL_18_STABLE)의 현재 소스에 대한 사실 로 시작한다. 외부 자료 없이도 읽을 수 있다. 검증 방법과 기록된 미결 사항은 뒤에 따른다.
검증된 사실
섹션 제목: “검증된 사실”-
표준 잠금 모드는
NoLock(0) 포함 정확히 8개에 하나의 비잠금 코드가 있다.lockdefs.h에서 확인.AccessShareLock=1…AccessExclusiveLock=8,MaxLockMode=8.lock.h의MAX_LOCKMODES=10은 배열 크기 상수(모드 수 + 여유)이고 모드 개수가 아니다.InplaceUpdateTupleLock은ExclusiveLock으로#define된 별칭이며 아홉 번째 모드가 아니다. -
DEFAULT와 USER 잠금 메서드는 하나의 충돌 테이블을 공유한다.
lock.c에서default_lockmethod와user_lockmethod모두 같은LockConflicts[]와lock_mode_names[]를 가리킨다. 차이는 디버그 추적 플래그와 다른 곳에서 강제되는 수명 규칙뿐이다. 모드 의미론은 동일하다. -
공유 잠금 해시는 정확히 16방향으로 파티셔닝된다.
LOG2_NUM_LOCK_PARTITIONS = 4⇒NUM_LOCK_PARTITIONS = 16(lwlock.h). 파티션은hashcode % 16(LockHashPartition)이고,proclock_hash는 proc 포인터를 하위 4비트 위에 믹스해PROCLOCK이 자신의LOCK의 파티션을 유지하게 한다. 매크로와proclock_hash모두 읽어서 확인. -
Fast-path 대상은 엄격히 현재 데이터베이스의 비공유 관계에 대한 세 가지 약한 관계 모드다.
EligibleForRelationFastPath는DEFAULT_LOCKMETHOD,LOCKTAG_RELATION,locktag_field1 == MyDatabaseId,mode < ShareUpdateExclusiveLock을 요구한다.ShareUpdateExclusiveLock이 제외되는 이유는 자기 충돌 모드이기 때문이다. 주석은 “그런 잠금과 충돌하지 않으므로 완전히 무시할 수 있다”고 설명한다. -
Fast-path 그룹 카운트는
FP_LOCK_SLOTS_PER_GROUP = 16슬롯/그룹이며 “변경 금지”로 표시됐다.proc.h에서 확인. 백엔드당 그룹 수 (FastPathLockGroupsPerBackend)는 시작 시max_locks_per_transaction에서 최대FP_LOCK_GROUPS_PER_BACKEND_MAX = 1024까지 도출된다. 이는 PG18 시대의 개선이다. 이전 릴리스는 고정 16슬롯 배열이었다. -
강한 잠금 카운터는 파티션 잠금 16개가 아닌 1024방향 배열이다.
FAST_PATH_STRONG_LOCK_HASH_PARTITIONS = 1 << 10 = 1024(lock.c). 잠금 테이블 16개 파티션보다 세분화되어 있으므로, 한 관계의 강한 잠금이 무관한 관계의 fast path를 비활성화하는 일이 드물다. -
데드락 탐지는
deadlock_timeout까지 지연되며 모든 16개 파티션 잠금 아래서 실행된다.ProcSleep이 타이머를 설정하고,CheckDeadLock이DeadLockCheck를 호출하기 전에0..15순서로 모든LockHashPartitionLockByIndex(i)를 획득한다. 두 함수를 읽어서 확인. 고정 오름차순이 파티션 잠금들 사이의 LWLock 데드락을 방지한다. -
탐지기는 abort 전에 대기 큐 재배치로 소프트 사이클을 해소한다.
DeadLockCheck는 위상 정렬 재배치가 모든 사이클을 제거할 때DS_SOFT_DEADLOCK을 반환하고lock->waitProcs를waitOrders[]에서 재작성한다. 해소 불가능한 (전부 하드인) 사이클만DS_HARD_DEADLOCK과 abort로 이어진다.DeadLockCheck/DeadLockCheckRecurse에서 확인. -
LOCKTAG_RELATION_EXTEND는 병렬 잠금 그룹 내에서도 충돌하며, 데드락 사이클에 있을 수 없다.LockCheckConflicts는 그룹 감산 루프 전에 이를 특수 처리하고,FindLockCycleRecurseMember는 이 경우 조기 반환한다.IsRelationExtensionLockHeld단언은 이 잠금을 보유한 채 다른 헤비웨이트 잠금을 취하지 않도록 강제해, 대기 체인 연장이 불가능하다. -
데드락 탐지기는 autovacuum 취소 구현에 재사용된다.
blocking_autovacuum_proc이 설정된DS_BLOCKED_BY_AUTOVACUUM반환은 블록된 백엔드가 직접 방해하는 autovacuum 워커를 기다리지 않고 취소 신호를 보낼 수 있게 한다.FindLockCycleRecurseMember의PROC_IS_AUTOVACUUM분기와GetBlockingAutoVacuumPgproc에서 확인.
미결 사항
섹션 제목: “미결 사항”-
약한 메모리 모델에서 fast-path / 강한 잠금 핸드셰이크의 메모리 순서 정확성.
README는 SMP에서의 LWLock-시퀀스-포인트 보장을 설명하지만, ARM/POWER에서 LWLock 획득·해제 외에 어떤 배리어가 의존되는지는 여기서 추적하지 않았다. 조사 경로:BeginStrongLockAcquire/FastPathTransferRelationLocks를postgres-lwlock-spinlock.md의 LWLock 메모리 배리어 의미론과 함께 읽는다. -
소프트 에지 재배치 탐색의 최악 비용.
DeadLockCheckRecurse는 소프트 에지 역전을 재귀적으로 시도한다.README는 사이클이 적고 작아서 조합 폭발이 “실제로는 큰 문제가 아닐 것”이라고 하지만, 대기 큐 길이와 그룹 크기의 함수로서의 실제 상한은 확립되지 않았다. 조사 경로: 합성 다중 소프트 데드락 워크로드에서nCurConstraints/nPossibleConstraints를 계측한다. -
미래 병렬 쓰기 아래서 비관계 잠금과의 그룹 잠금 상호작용.
README는 그룹 내 잠금을 비충돌로 선언하는 것이 병렬 모드가 현재 읽기 전용이기 때문에 안전하다고 밝히며, 병렬 쓰기가 활성화될 경우 GIN 페이지 잠금이 위험 요소임을 표시한다. REL_18이 이 부분을 강화했는지는 여기서 검증하지 않았다. 조사 경로:access/transam/README.parallel과 PG18 병렬 쓰기 관련 작업을 교차 검토한다.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”포인터만 제공한다. 각 항목은 후속 문서의 시작점이다.
-
A Scalable Lock Manager for Multicores (Jung, Han, Fekete, Heiser, Yeom, SIGMOD 2013) — 앵커 논문. MySQL의 처리량 붕괴를 잠금 테이블 래치 경쟁으로 진단하고 단계적 잠금 할당을 제안한다. 할당·해제(비동기, 가비지 컬렉션 방식 벌크 처리)와 조작(RAW 메모리 배리어로 래치-프리 처리)의 분리가 핵심이다. PostgreSQL은 같은 병목을 반대 방향에서 공략한다. 파티셔닝에 약한 잠금을 위한 fast path를 더해 공유 테이블 자체를 회피하는 방식이다. “테이블 회피” 대 “테이블 래치-프리화”의 비교 연구는 각각 무엇을 희생하는지를 정량화할 것이다.
-
CUBRID 헤비웨이트 lock manager (
cubrid-lock-manager.md) — CUBRID는 PG의LOCK/PROCLOCK에 유사한lk_res/lk_entry레코드를 쓰지만, SERIALIZABLE에서는 PG처럼 MVCC + SSI 술어 잠금에 의존하지 않고 행 수준 잠금(first-updater-wins)을 취한다. CUBRID의 피해자 정책은 “가장 최근에 블록된”; PG는 사이클을 닫은 프로세스를 abort한다. 두 데드락 피해자 정책과 단위 선택을 비교하는 것이 자연스러운 후속 작업이다. -
SSI 술어 잠금 (
postgres-ssi-predicate-locking.md; Cahill 2008, Ports & Grittner 2012) — 다른 lock manager. SIReadLock은 rw-반의존성 사이클을 탐지해 SI를 직렬화 가능하게 만드는 별도 서브시스템이며 (README-SSI), 여기서 분석한 헤비웨이트 매니저를 보완한다(대체하지 않는다). 두 서브시스템의 경계 — 각자 무엇을 담당하는가 — 는 별도 교차 분석의 가치가 있다. -
Wait-die / wound-wait 타임스탬프 순서 (Database System Concepts §18.2) — PostgreSQL이 탐지 대신 거부한 데드락 예방 방식. Yu et al., Staring into the Abyss(VLDB 2015)는 1000코어에서 탐지 대 예방을 벤치마킹한다. PG의 전체 파티션 잠금 탐지가 확장성 한계에 도달할 경우 참고할 만하다.
-
잠금 테이블 없는 / 낙관적 설계 (Hekaton, Silo, Cicada) — 중앙 잠금 테이블을 레코드별 메타데이터나 낙관적 검증으로 대체하는 인메모리 엔진들. PG의 디스크 기반 설계와 직교하지만, 잠금 자체에 내재하는 비용과 중앙 집중식 잠금 테이블 에 내재하는 비용을 분리하는 데 유익하다.
트리 내부 README
섹션 제목: “트리 내부 README”src/backend/storage/lmgr/README— 공식 설계 문서. 잠금 자료 구조, 내부 파티셔닝, fast-path 잠금, 데드락 알고리즘(하드·소프트 에지, 위상 정렬 재배치), 그룹 잠금, 사용자 잠금, hot-standby 잠금.
PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)
섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)”src/backend/storage/lmgr/lock.c— 기본 잠금 메커니즘.src/backend/storage/lmgr/proc.c— 대기 큐,ProcSleep,CheckDeadLock.src/backend/storage/lmgr/deadlock.c— WFG 탐지기와 소프트 에지 해결기.src/backend/storage/lmgr/lmgr.c— 관계 잠금 래퍼.src/include/storage/lock.h,src/include/storage/lockdefs.h— 구조체와 잠금 모드 상수.src/include/storage/proc.h,src/include/storage/lwlock.h—PGPROCfast-path·대기 필드와 파티션 상수.
논문 (knowledge/research/dbms-papers/ 소재)
섹션 제목: “논문 (knowledge/research/dbms-papers/ 소재)”scalable-lock-manager.md— Jung et al., A Scalable Lock Manager for Multicores, SIGMOD 2013.
교과서 챕터 (knowledge/research/dbms-general/ 소재)
섹션 제목: “교과서 챕터 (knowledge/research/dbms-general/ 소재)”- Database System Concepts (Silberschatz et al., 7판), 18장 “Concurrency Control” — §18.1 잠금 기반 프로토콜·2PL, §18.2 데드락 처리(예방 대 탐지, waits-for graph), §18.3 다중 단위·의도 잠금.
형제 문서 (교차 참조, 메커니즘 중복 없음)
섹션 제목: “형제 문서 (교차 참조, 메커니즘 중복 없음)”postgres-lwlock-spinlock.md— 파티션 래치와fpInfoLock의 기반인 경량 잠금.postgres-ssi-predicate-locking.md— 별도 SIReadLock 서브시스템.postgres-shared-memory-ipc.md— 공유 해시와 PGPROC 배열이 사는 곳.postgres-backend-lifecycle.md—LOCALLOCK.lockOwners와LockReleaseAll뒤의 ResourceOwner 트리.