콘텐츠로 이동

(KO) PostgreSQL Lock Manager — 헤비웨이트 잠금 테이블, 잠금 모드, 그리고 데드락 탐지

목차

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 설계를 좌우하는 두 가지 정제 요소가 있다.

  1. 잠금 단위와 다중 단위 잠금(multi-granularity locking, MGL). 잠금이 테이블 전체를 대상으로 할 수도 있고, 페이지나 개별 튜플을 대상으로 할 수도 있다. 단위가 클수록 추적 비용은 낮지만 동시성이 떨어지고, 단위가 작을수록 동시성은 높지만 잠금 테이블이 폭발한다. Database System Concepts §18.3(“Multiple Granularity”)은 데이터베이스 → 테이블 → 페이지 → 튜플 계층과 의도 모드(intention mode) (IS, IX, SIX)로 이 문제를 해결한다. S 또는 X 모드로 잠그기 전에 상위 노드에 대응하는 의도 모드를 먼저 잡아 두면, 굵은 단위 잠금 요청자가 전체 하위 트리를 순회하지 않고도 세밀한 잠금 보유자를 발견할 수 있다. 모드 집합의 풍부함과 모드 간 충돌을 정의하는 충돌 테이블이 설계의 핵심이다.

  2. 데드락 처리. 트랜잭션이 잠금을 요청하는 순서는 예측 불가능하므로 사이클이 생길 수 있다. 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를 도입하게 된 동기다.

교과서는 모델을 제공한다(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 명칭
잠금 가능 객체 식별자LOCKTAG — 16바이트 태그, locktag_typeLockTagType
객체당 보유자·대기자 레코드LOCK (공유 LockMethodLockHash)
보유자당 레코드PROCLOCK (공유 LockMethodProcLockHash)
백엔드별 잠금 캐시LOCALLOCK (백엔드 전용 LockMethodLocalHash)
충돌 테이블 (모드×모드 비트 행렬)LockConflicts[] + LockMethodData.conflictTab
8가지 잠금 모드AccessShareLockAccessExclusiveLock (lockdefs.h)
집계 승인 모드 마스크LOCK.grantMask; 대기 중 모드는 LOCK.waitMask
파티셔닝 래칭NUM_LOCK_PARTITIONS (=16) 파티션 LWLock, LOCKTAG 해시 기반
약한 관계 잠금 fast pathPGPROC.fpRelId[] / fpLockBits[]; FastPathStrongRelationLocks
객체 위의 대기 큐LOCK.waitProcs (PGPROC 리스트); 요청은 PGPROC에 저장
Waits-for graph + 탐지DeadLockCheck / FindLockCycle (deadlock.c)
낙관적 대기 + 타임아웃ProcSleepdeadlock_timeout을 설정; CheckDeadLock이 탐색 실행

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 없이 대기 큐 재배치로 해소한다.

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은 자신의 LOCKprocLocks 리스트와 소유 PGPROC 양쪽에 이중 연결된다. fast path로 취한 잠금이라면 LOCALLOCK.lock.proclock은 NULL이다.

LOCK은 “잠금 가능 객체 하나당”, PROCLOCK은 “잠금·요청자 쌍 하나당” 레코드다(README). 공유 구조는 “잠금 가능 객체·잠금 모드·백엔드 조합당 하나의 잠금 승인만 허용” 한다. 한 트랜잭션이 같은 잠금을 두 번 취하는 경우의 내부 요청 횟수는 LOCALLOCK이 보관하므로 공유 데이터는 건드리지 않아도 된다.

// LOCK — src/include/storage/lock.h
typedef 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.h
typedef 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)는 보장 덕분이다. holdMaskLOCK.grantMask에서 이 백엔드의 몫이다. dlist_node 두 개는 같은 객체를 LOCK의 보유자 리스트와 PGPROC의 보유 잠금 리스트에 동시에 연결한다.

// LOCALLOCK + LOCALLOCKTAG — src/include/storage/lock.h
typedef 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.h
typedef 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_RELATIONdbOid, relOid테이블 전체 (대부분의 DML/DDL 잠금)
LOCKTAG_RELATION_EXTENDdbOid, relOid관계에 블록 추가 권한
LOCKTAG_DATABASE_FROZEN_IDSdbOidpg_database.datfrozenxid 갱신
LOCKTAG_PAGEdbOid, relOid, blocknum페이지 하나 (GIN 정리 등)
LOCKTAG_TUPLEdbOid, relOid, blocknum, offnum물리 튜플 하나 (SELECT FOR …)
LOCKTAG_TRANSACTIONxid트랜잭션 완료 대기
LOCKTAG_VIRTUALTRANSACTIONprocNumber, lxidvxid 대기 (CIC, hot standby)
LOCKTAG_SPECULATIVE_TOKENxid, token투기적 삽입 (INSERT ... ON CONFLICT)
LOCKTAG_OBJECTdbOid, classOid, objOid, objsubid비관계 카탈로그 객체
LOCKTAG_USERLOCK(예약)구 contrib/userlock
LOCKTAG_ADVISORY사용자 id 4개pg_advisory_lock(...)
LOCKTAG_APPLY_TRANSACTIONdbOid, subOid, xid, objid논리 복제 적용 충돌

locktag_lockmethodid잠금 메서드 를 선택한다. 시스템 주도 잠금에는 DEFAULT_LOCKMETHOD(1), 어드바이저리 잠금에는 USER_LOCKMETHOD(2)를 쓴다. 두 메서드는 같은 모드 집합과 충돌 테이블을 공유한다. 차이는 디버그 추적 플래그와 수명 규칙뿐이다. 어드바이저리·사용자 잠금은 트랜잭션을 넘어 살아남을 수 있다(README §“User Locks” 참고).

// 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와 충돌하는 모드 집합이다. 행렬은 대칭이다. 관례적 표현(✗ = 충돌):

ACSRSRESUESSREEAE
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.c
bool
DoLockModesConflict(LOCKMODE mode1, LOCKMODE mode2)
{
LockMethod lockMethodTable = LockMethods[DEFAULT_LOCKMETHOD];
if (lockMethodTable->conflictTab[mode1] & LOCKBIT_ON(mode2))
return true;
return false;
}

두 공유 해시(LockMethodLockHashLOCK용, LockMethodProcLockHashPROCLOCK용)는 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.c
static uint32
proclock_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(및 내부 구현 LockAcquireExtended)는 매니저의 핵심이다. 제어 흐름에는 네 가지 정상 출구 — 이미 보유(로컬 카운트 증가), fast-path 승인, 즉시 공유 테이블 승인, 대기 후 승인 — 와 데드락·nowait 오류 출구가 있다.

flowchart TD
  A["LockAcquireExtended(tag, mode)"] --> B["LOCALLOCK 찾기/생성<br/>(백엔드 전용 해시)"]
  B --> C{"locallock.nLocks &gt; 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] &amp; 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 승인)는 공유 메모리를 전혀 건드리지 않는다. 진짜 공유 테이블 충돌이 발생할 때만 백엔드가 JoinWaitQueueWaitOnLockProcSleep으로 내려간다. 강한 잠금 획득 시에는 먼저 모든 백엔드의 해당 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.c
void
GrantLock(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”은 워커의 잠금 요청을 예측하는 방식을 왜 거부하고 그룹 내 잠금을 비충돌로 선언하는 쪽을 선택했는지 설명한다.

읽기 중심 워크로드가 테이블 하나를 집중 공략하면 모든 백엔드가 같은 파티션 잠금으로 몰린다. “2코어 서버에서도 측정 가능하고, 코어 수가 늘수록 아주 두드러진다”(README). 9.2부터 도입된 fast path는 백엔드가 약한 관계 잠금(AccessShareLock, RowShareLock, RowExclusiveLock — 모두 ShareUpdateExclusiveLock 미만)을 자신의 PGPROC 안의 전용 배열에 기록하고 공유 해시를 전혀 건드리지 않도록 한다.

// PGPROC fast-path fields — src/include/storage/proc.h
LWLock 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 자료 구조를 검사할 필요가 없다. 데드락에 연루될 수 있는 잠금은 사전에 메인 테이블로 이전됐기 때문이다.”

요청이 충돌할 때 JoinWaitQueueLOCK.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의 발신 에지다. 이 백엔드가 무엇 때문에 블록됐는지를 정확히 기록한다. 큐에 들어가면 ProcSleepdeadlock_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 */
}

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을 재실행해 검증하기 때문이다.

LockReleaseLOCALLOCK 카운트를 줄인다. 0에 도달할 때만 공유 메모리를 건드리거나(또는 fast-path 비트를 지운다). 트랜잭션 종료 시 호출되는 LockReleaseAll은 백엔드의 LOCALLOCK 해시와 fast-path 배열을 한 번에 순회하고, 각 해제된 객체의 큐를 ProcLockWakeup한다. 트랜잭션 잠금은 커밋·어보트까지 살아 있으므로, strict-2PL의 축소 단계가 여기서 한꺼번에 발생한다.

줄 번호가 아닌 심볼 이름 으로 찾는다. lmgr 소스는 릴리스마다 이동하지만 함수·구조체 이름은 대부분의 리팩터링에서 살아남는다. git grep -n '<symbol>' src/backend/storage/lmgr/으로 현재 위치를 확인할 수 있다. 아래 줄 번호는 커밋 273fe94(REL_18) 기준 힌트다.

  • 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) — 탐지기의 네 가지 판정.
  • 잠금 모드 상수 AccessShareLockAccessExclusiveLock, MaxLockMode (lockdefs.h).
  • struct PGPROC fast-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 — 자기 잠금·그룹 감산을 포함한 전체 충돌 검사.
  • LockTagHashCodeLOCKTAG 해시; 하위 4비트가 파티션 번호.
  • proclock_hash / ProcLockHashCodePROCLOCK을 자신의 LOCK의 파티션에 강제 배치.
  • LockHashPartition, LockHashPartitionLock (매크로, lock.h).
  • LockAcquire / LockAcquireExtended — 획득 상태 기계.
  • SetupLockInTable — 공유 LOCKPROCLOCK 찾기·생성.
  • GrantLock / GrantLockLocal — 공유·로컬 상태에 승인 기록.
  • UnGrantLock / CleanUpLock — 역방향; 빈 객체 가비지 컬렉션.
  • FastPathGrantRelationLock / FastPathUnGrantRelationLock — 백엔드 전용 배열 조작.
  • FastPathTransferRelationLocks — 강한 잠금 도착 시 모든 백엔드의 fast-path 항목을 공유 테이블로 플러시.
  • FastPathStrongRelationLockData / FastPathStrongLockHashPartition — 1024방향 강한 잠금 카운터.
  • BeginStrongLockAcquire / FinishStrongLockAcquire / AbortStrongLockAcquire.
  • JoinWaitQueue — 큐 위치 선택; 즉시(조기) 데드락 탐지.
  • ProcSleepdeadlock_timeout 설정, 세마포어 블록.
  • ProcLockWakeup — 해제 시 도착 순서대로 깨울 수 있는 대기자 깨우기.
  • CheckDeadLock — 타임아웃 핸들러: 모든 파티션 잠금 후 탐지기 실행.
  • RemoveFromWaitQueue (lock.c) — proc 디큐 (데드락 피해자·취소).
  • DeadLockCheck — 진입점; DeadLockState 반환, 소프트 수정 적용.
  • DeadLockCheckRecurse / TestConfiguration — 소프트 에지 역전 탐색.
  • FindLockCycle / FindLockCycleRecurse / FindLockCycleRecurseMember — 하드·소프트 에지를 통한 WFG DFS.
  • ExpandConstraints / TopoSort — 가상의 재배치된 대기 큐 구성.
  • DeadLockReport — 피해자를 abort하는 ERROR 발생.
  • InitDeadLockCheckingMaxBackends에서 최악의 경우 작업 공간 사전 할당.
  • LockRelationOid / UnlockRelationOid / LockRelation — 백엔드 대부분이 실제로 호출하는 관계 잠금 진입점.

위치 힌트 (2026-06-05 기준, REL_18 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일
LockConflicts[]lock.c65
lock_mode_names[]lock.c108
LockMethods[]lock.c150
FastPathStrongRelationLockDatalock.c306
LockTagHashCodelock.c556
proclock_hashlock.c573
DoLockModesConflictlock.c622
LockAcquirelock.c808
LockAcquireExtendedlock.c835
SetupLockInTablelock.c1282
LockCheckConflictslock.c1528
GrantLocklock.c1657
UnGrantLocklock.c1680
CleanUpLocklock.c1737
LockReleaselock.c2070
LockReleaseAlllock.c2275
FastPathGrantRelationLocklock.c2750
FastPathTransferRelationLockslock.c2829
VirtualXactLocklock.c4713
JoinWaitQueueproc.c1173
ProcSleepproc.c1342
ProcLockWakeupproc.c1772
CheckDeadLockproc.c1820
DeadLockCheckdeadlock.c220
DeadLockCheckRecursedeadlock.c312
FindLockCycledeadlock.c446
FindLockCycleRecursedeadlock.c457
FindLockCycleRecurseMemberdeadlock.c536
TopoSortdeadlock.c862
DeadLockReportdeadlock.c1075
LockRelationOidlmgr.c107
struct LOCKTAGlock.h165
struct LOCKlock.h309
struct PROCLOCKlock.h370
struct LOCALLOCKlock.h427
enum DeadLockStatelock.h509
AccessShareLockMaxLockModelockdefs.h36
NUM_LOCK_PARTITIONSlwlock.h97
FP_LOCK_SLOTS_PER_GROUPproc.h98

각 항목은 커밋 273fe94(REL_18_STABLE)의 현재 소스에 대한 사실 로 시작한다. 외부 자료 없이도 읽을 수 있다. 검증 방법과 기록된 미결 사항은 뒤에 따른다.

  • 표준 잠금 모드는 NoLock(0) 포함 정확히 8개에 하나의 비잠금 코드가 있다. lockdefs.h에서 확인. AccessShareLock=1AccessExclusiveLock=8, MaxLockMode=8. lock.hMAX_LOCKMODES=10은 배열 크기 상수(모드 수 + 여유)이고 모드 개수가 아니다. InplaceUpdateTupleLockExclusiveLock으로 #define된 별칭이며 아홉 번째 모드가 아니다.

  • DEFAULT와 USER 잠금 메서드는 하나의 충돌 테이블을 공유한다. lock.c에서 default_lockmethoduser_lockmethod 모두 같은 LockConflicts[]lock_mode_names[]를 가리킨다. 차이는 디버그 추적 플래그와 다른 곳에서 강제되는 수명 규칙뿐이다. 모드 의미론은 동일하다.

  • 공유 잠금 해시는 정확히 16방향으로 파티셔닝된다. LOG2_NUM_LOCK_PARTITIONS = 4NUM_LOCK_PARTITIONS = 16 (lwlock.h). 파티션은 hashcode % 16(LockHashPartition)이고, proclock_hash는 proc 포인터를 하위 4비트 위에 믹스해 PROCLOCK이 자신의 LOCK의 파티션을 유지하게 한다. 매크로와 proclock_hash 모두 읽어서 확인.

  • Fast-path 대상은 엄격히 현재 데이터베이스의 비공유 관계에 대한 세 가지 약한 관계 모드다. EligibleForRelationFastPathDEFAULT_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이 타이머를 설정하고, CheckDeadLockDeadLockCheck를 호출하기 전에 0..15 순서로 모든 LockHashPartitionLockByIndex(i)를 획득한다. 두 함수를 읽어서 확인. 고정 오름차순이 파티션 잠금들 사이의 LWLock 데드락을 방지한다.

  • 탐지기는 abort 전에 대기 큐 재배치로 소프트 사이클을 해소한다. DeadLockCheck는 위상 정렬 재배치가 모든 사이클을 제거할 때 DS_SOFT_DEADLOCK을 반환하고 lock->waitProcswaitOrders[]에서 재작성한다. 해소 불가능한 (전부 하드인) 사이클만 DS_HARD_DEADLOCK과 abort로 이어진다. DeadLockCheck / DeadLockCheckRecurse에서 확인.

  • LOCKTAG_RELATION_EXTEND는 병렬 잠금 그룹 내에서도 충돌하며, 데드락 사이클에 있을 수 없다. LockCheckConflicts는 그룹 감산 루프 전에 이를 특수 처리하고, FindLockCycleRecurseMember는 이 경우 조기 반환한다. IsRelationExtensionLockHeld 단언은 이 잠금을 보유한 채 다른 헤비웨이트 잠금을 취하지 않도록 강제해, 대기 체인 연장이 불가능하다.

  • 데드락 탐지기는 autovacuum 취소 구현에 재사용된다. blocking_autovacuum_proc이 설정된 DS_BLOCKED_BY_AUTOVACUUM 반환은 블록된 백엔드가 직접 방해하는 autovacuum 워커를 기다리지 않고 취소 신호를 보낼 수 있게 한다. FindLockCycleRecurseMemberPROC_IS_AUTOVACUUM 분기와 GetBlockingAutoVacuumPgproc에서 확인.

  1. 약한 메모리 모델에서 fast-path / 강한 잠금 핸드셰이크의 메모리 순서 정확성. README는 SMP에서의 LWLock-시퀀스-포인트 보장을 설명하지만, ARM/POWER에서 LWLock 획득·해제 외에 어떤 배리어가 의존되는지는 여기서 추적하지 않았다. 조사 경로: BeginStrongLockAcquire/FastPathTransferRelationLockspostgres-lwlock-spinlock.md의 LWLock 메모리 배리어 의미론과 함께 읽는다.

  2. 소프트 에지 재배치 탐색의 최악 비용. DeadLockCheckRecurse는 소프트 에지 역전을 재귀적으로 시도한다. README는 사이클이 적고 작아서 조합 폭발이 “실제로는 큰 문제가 아닐 것”이라고 하지만, 대기 큐 길이와 그룹 크기의 함수로서의 실제 상한은 확립되지 않았다. 조사 경로: 합성 다중 소프트 데드락 워크로드에서 nCurConstraints / nPossibleConstraints를 계측한다.

  3. 미래 병렬 쓰기 아래서 비관계 잠금과의 그룹 잠금 상호작용. 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의 디스크 기반 설계와 직교하지만, 잠금 자체에 내재하는 비용과 중앙 집중식 잠금 테이블 에 내재하는 비용을 분리하는 데 유익하다.

  • 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.hPGPROC fast-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.mdLOCALLOCK.lockOwnersLockReleaseAll 뒤의 ResourceOwner 트리.