본문 바로가기
엑셈 경쟁력/DB 인사이드

DB 인사이드 | PWI - LOCK > Relation-level Lock

by exemtech 2024. 6. 11.

 

 

📢 PWI(PostgreSQL Wait Interface) - LOCK

PostgreSQL의 Wait Event에 대하여 다루기 전에, 먼저 PostgreSQL에서 사용하는 Lock에 대한 전반적인 내용을 다룰 예정입니다.
PostgreSQL은 Relation과 같은 Object를 보호하는 Heavyweight Lock(HWLock), Relation의 구성 요소의 하나인 Row를 다루는 Row-level Lock, 그리고 일반적으로 공유 메모리의 데이터 구조에 접근할 때 사용하는 Lightweight Lock(LWLock) 등 다양한 유형의 Lock을 제공합니다.
앞으로 PWI - LOCKS에서는 PostgreSQL에서 사용하는 Lock의 종류와 특징, 동작 방식 등을 알아보고, 사용 예시를 통하여 Lock을 획득 및 해제하는 과정을 확인해 보도록 하겠습니다.

 

Relation-level Lock

PostgreSQL에서는 Database Object 중, 테이블과 같은 구조를 갖는 Object를 Relation이라고 하며, 대표적으로 테이블, 인덱스, 뷰, 시퀀스가 있습니다.

Relation Lock은 동시 업데이트로부터 Relation을 보호하고, Relation의 구조를 변경하는 동안 해당 Relation에 대한 사용을 금지하는 역할을 합니다. Relation Lock은 Heavyweight Lock의 일종으로, PostgreSQL 내부 메모리 공간에 저장되어 Lock Manager를 통해 관리되기 때문에 pg_locks를 통해 조회 가능합니다. ( locktype = relation )

 

Lock Mode

Relation Lock은 Access Share, Row Share, Row Exclusive, Share Update Exclusive, Share, Share Row Exclusive, Exclusive, Access Exclusive의 총 8가지 모드를 제공합니다. 이처럼 다양한 모드를 제공하는것은 Relation에 대해 실행할 수 있는 동시 Command 수를 최대화하기 위함입니다.

Lock Mode는 수행(요청)하는 SQL Command에 따라 다르게 선택되는데, SQL Command에 따라 어떤 모드가 선택되는지, 그리고 어떤 모드와 동시에 작업이 가능한지 호환성을 확인하기 위해서는 아래 표를 통해 확인할 수 있습니다.

Locking
Mode
Access
Share
Row
Share
Row
Exclusive
Share
Update
Exclusive
Share Share
Row

Exclusive
Exclusive Access
Exclusive
SQL
Commands
Access
Share
              X SELECT
Row
Share
            X X SELECT FOR
UPDATE/SHARE
Row
Exclusive
        X X X X INSERT, UPDATE,
DELETE
Share
Update
Exclusive
      X X X X X VACUUM,
ALTER TABLE(1),
CREATE INDEX CONCURRENTLY
Share     X X   X X X CREATE INDEX
Share
Row
Exclusive
    X X X X X X CREATE TRIGGER,
ALTER TABLE(2)
Exclusive   X X X X X X X REFRESH MAT.VIEW CONCURRENTLY
Access
Exclusive
X X X X X X X X DROP, TRUNCATE,
VACUUM FULL,
LOCK TABLE,
ALTER TABLE(3),
REFRESH MAT.VIEW

📢 ALTER TABLE Command는 다양한 특징이 있으며, 특징마다 다른 레벨의 Lock이 필요합니다. 위 표를 확인하면 3개의 모드에 대해서 SQL Commands 컬럼에 ALTER TABLE이 표시되어 있습니다. ALTER TABLE의 상세 Command는 다음과 같습니다.

더보기
ALTER TABLE VALIDATE CONSTRAINT
ALTER TABLE SET WITHOUT CLUSTER
ALTER TABLE SET TOAST
ALTER TABLE SET STATISTICS
ALTER TABLE SET N_DISTINCT
ALTER TABLE SET FILLFACTOR
ALTER TABLE SET AUTOVACUUUM
ALTER TABLE DETACH PARTITION CONCURRENTLY (PARENT)
ALTER TABLE CLUSTER ON
ALTER TABLE ATTACH PARTITION (PARENT)
ALTER INDEX (RENAME)
더보기
ALTER TABLE ENABLE/DISABLE TRIGGER
ALTER TABLE ADD FOREIGN KEY NOT VALID (PARENT)
ALTER TABLE ADD FOREIGN KEY NOT VALID (CHILD)
ALTER TABLE ADD FOREIGN KEY (CHILD)
더보기
ALTER TABLE SET/DROP DEFAULT
ALTER TABLE SET TABLESPACE
ALTER TABLE SET STORAGE
ALTER TABLE SET SEQUENCE
ALTER TABLE SET DATA TYPE
ALTER TABLE SET COMPRESSION
ALTER TABLE RESET STORAGE
ALTER TABLE RENAME
ALTER TABLE INHERIT PARENT
ALTER TABLE ENABLE/DISABLE RULE
ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY
ALTER TABLE DROP EXPRESSION
ALTER TABLE DROP CONSTRAINT
ALTER TABLE DROP COLUMN
ALTER TABLE DETACH PARTITION (PARENT)
ALTER TABLE DETACH PARTITION (TARGET/DEFAULT)
ALTER TABLE DETACH PARTITION CONCURRENTLY (TARGET/DEFAULT)
ALTER TABLE ATTACH PARTITION (TARGET/DEFAULT)
ALTER TABLE ALTER CONSTRAINT
ALTER TABLE ADD COLUMN
ALTER TABLE ADD CONSTRAINT
ALTER INDEX SET TABLESPACE
ALTER INDEX SET FILLFACTOR
ALTER INDEX ATTACH PARTITION
  • ‘Access Share’ 모드는 다른 모드와의 호환성이 가장 높으며, ‘Access Exclusive’는 자기 자신을 포함한 모든 모드와 호환되지 않습니다.
    (즉, SELECT는 대부분의 Command와 동시에 사용될 수 있지만, DROP, TRUNCATE 같이 Data 구조 자체를 변경하는 Command의 경우, VACUUM과 같은 시스템 백그라운드 작업뿐만 아니라 SELECT처럼 가벼운 읽기 작업도 함께 수행될 수 없습니다.)
  • 상위 4개 모드는 테이블 데이터의 동시 변경을 허용하지만, 하위 4개 모드는 허용하지 않습니다.
  • 일반적으로 인덱스 생성(CREATE INDEX)은 데이터 변경 작업과 함께 수행할 수 없는 ‘Share’ 모드를 사용합니다. 작업 간, 동시성 영향을 주고 싶지 않다면 인덱스 생성 시 CONCURRENTLY 옵션을 추가하여 ‘Share’가 아닌 ‘Share Update Exclusive’ 모드를 선택합니다.
    더보기
    pid : 2306020
        create index relation_test2_idx on relation_test2(a);
    pid : 2309053
        update relation_test2 set a = 5 where a = 6;

    CREATE INDEX Command를 사용한 경우, ①Relation에 대하여 ShareLock 모드로 Lock을 획득하였고, ②다른 프로세스에서 실행한 UPDATE Command는 같은 Relation에 대해 RowExclusiveLock 모드로 Lock을 요청하였지만, granted 값이 false로, Lock 획득에 실패한 것을 확인할 수 있습니다.

    pid : 2306020
    	create index concurrently relation_test2_idx on relation_test2(a);
    pid : 2309053
    	update relation_test2 set a = 5 where a = 6;

     

    반면, CREATE INDEX CONCURRENTLY Command를 사용했을 때는 ①Relation에 대하여 ShareUpdateExclusiveLock 모드로 Lock을 획득하였으므로, ②이후 UPDATE Command를 실행한 다른 프로세스는 문제없이 같은 Relation에 대한 RowExclusiveLock 모드의 Lock을 획득 성공한 것을 확인할 수 있습니다. (granted = true)

  • REFRESH MATERIALIZED VIEW Command도 옵션에 따라 서로 다른 모드가 선택될 수 있습니다. 일반적으로 MATERIALIZED VIEW를 REFRESH하는 작업은 ‘Access Exclusive’ 모드가 선택되므로 읽기 작업과 호환이 안되지만, CONCURRENTLY 옵션을 사용하면, ‘Exclusive’ 모드가 선택되어 읽기 작업과의 호환성을 가집니다.
    더보기
    pid : 2316254 
    	REFRESH MATERIALIZED VIEW relation_view;
    pid : 2316478
    	select * from relation_view;

    REFRESH MATERIALIZED VIEW Command를 사용하면 ①Relation에 대하여 AccessExclusiveLock 모드로 Lock을 획득하므로, ②다른 프로세스에서 같은 Relation에 대하여 실행한 단순 SELECT Command도 Lock을 획득하지 못하고 실패한 것을 볼 수 있습니다. (granted = false)

    pid : 2316254
    	REFRESH MATERIALIZED VIEW CONCURRENTLY relation_view;
    pid : 2316478
    	select * from relation_view;

    하지만 위와 같이 ①CONCURRENTLY 옵션을 추가하여 실행하면, ①ExclusiveLock 모드의 Lock을 획득하도록 작동하여 SELECT Command를 실행한 프로세스도 해당 Relation에 대하여 Lock을 획득한 것을 확인할 수 있습니다. (granted = true)

 

Locks on Transaction IDs

위의 예제들에서 Relation Lock의 확인을 위해 조회한 pg_locks 결과 중, locktype 값이 transactionidvirtualxid로 표시되는 것을 볼 수 있습니다.

각각은 TransactionID와 Virtual TransactionID에 대한 Lock으로, 이들은 Heavyweight Lock에 해당하며, 'Exclusive'와 'Share' 두 가지 잠금 모드를 제공합니다.

모든 트랜잭션은 Virtual TransactionID에 대한 Lock을 ‘Exclusive’ 모드로 획득, 트랜잭션 종료 시까지 유지합니다. 그리고 TransactionID가 할당된 경우(일반적으로 트랜잭션이 데이터베이스 상태를 변경하는 경우), TransactionID에 대해서도 ‘Exclusive’ 모드로 Lock을 획득합니다.

이와 별개로, 한 트랜잭션이 다른 트랜잭션 종료 시까지 기다려야 한다고 판단하면 다른 트랜잭션의 ID(상황에 따라 Virtual TransactionID 또는 TransactionID)에 대한 Lock을 ‘Share’ 모드로 획득하려고 시도합니다. 물론, 해당 트랜잭션에 의해 이미 ‘Exclusive’ 모드로 Lock을 획득한 상태이므로 트랜잭션이 종료될 때까지 획득에 실패하고 Sleep 상태로 대기합니다.

위 결과를 보면 ①pid1162505인 프로세스가 실행한 트랜잭션의 ID가 1772이며, 해당 transactionid에 대하여 ExclusiveLock 모드로 Lock을 획득하였습니다. 그리고 ②pid114406인 또 다른 프로세스가 transactionid1772에 대한 Lock을 SharedLock모드로 요청했지만 획득 실패하여 Sleep 상태로 대기 중임을 확인할 수 있습니다. (granted=false)

이후 선행 트랜잭션이 종료되면 해당 transactionid에 대한 ExclusiveLock 모드의 Lock이 해제되므로, Sleep 상태의 프로세스가 Lock을 획득할 수 있게 되고, 이 과정에서 선행 트랜잭션의 종료를 알 수 있습니다.

이처럼 트랜잭션 ID(Virtual TransactionID, TransactionID)에 대한 Lock은 트랜잭션의 시작/종료 시점 추적을 목적으로 사용할 수 있습니다.

📢 Virtual TransactionID와 TransactionID
PostgreSQL은 트랜잭션을 식별하기 위해 Virtual TransactionID와 TransactionID를 사용합니다. 차이점은 Local과 Global에 있습니다.
  • Virtual TransactionID
    Virtual TransactionID는 트랜잭션이 시작하는 시점에 바로 할당되며, Backend ID와 Local XID으로 구성됩니다. 예시의 9/294는 Backend ID는 9, Local XID 294를 의미합니다. (여기서 Backend ID는 운영체제의 프로세스 ID가 아닌 PostgreSQL에서 프로세스를 식별하기 위해 사용하는 ID이며, Local XID는 각 Backend에 순차적으로 부여된 Local ID입니다.)이 값은, 디스크에 저장되지 않고 메모리에만 존재합니다. 그렇기 때문에 MVCC에 사용되지 않고 Vacuum의 대상이 되지 않으므로, Vacuum 작업과 관련이 없다는 특징이 있습니다.
  • TransactionID
    트랜잭션이 처음 쓰기 작업을 하는 시점, 즉 데이터 수정이 필요한 경우에만 사용합니다. PostgreSQL Cluster 내에 존재하는 Global Counter에 의해 순차적으로 부여된 ID로, 데이터베이스 변경의 순서를 나타냅니다. 이 값은, 디스크에 저장되며 영구적인 값입니다.

일부 트랜잭션은 읽기 전용이거나 빈 트랜잭션과 같이 데이터베이스 변경을 일으키지 않기 때문에, TransactionID를 사용하는 것은 낭비가 될 수 있습니다. 이러한 이유로 데이터베이스에 변화를 주지 않는 트랜잭션에 대해서는 Virtual TransactionID만 할당하고, TransactionID는 할당하지 않습니다.

그리고 일반적으로 Virtual TransactionID는 개체를 고유하게 식별하는 데 사용합니다. Virtual TransactionID이 메모리에서 관리되므로 Lock 경합 발생 시 더 효율적으로 처리할 수 있기 때문입니다.

다음과 같이 Lock을 획득 또는 대기하고 있는 트랜잭션의 Virtual TransactionID를 pg_locksvirtualtransaction 컬럼을 통해 제공합니다.

pg_locks에서 locktype에 표시되는 virtualxid은 Lock의 대상이 되는 Object가 Virtual TransactionID라는 것을 의미하고, virtualtransaction 컬럼은 각각의 Lock을 보유하거나 대기 중인 트랜잭션의 Virtual TransactionID 값(ex. 9/294)을 표기합니다.

 

Lock Contention

Relation Lock에 대한 이해를 바탕으로 실제 여러 트랜잭션이 발생하는 상황에서 Relation Lock의 동작 과정을 알아보겠습니다.

[CASE 1] Lock Mode 간 호환이 안 되는 경우

두 개의 트랜잭션에서 같은 Relation에 대하여 호환되지 않는 Lock Mode를 선택하는 Command를 동시에 수행하였을 때, 어떻게 처리되는지 아래 테스트를 통해 살펴보겠습니다.

먼저 트랜잭션 1을 시작하여, 다음과 같이 트랜잭션과 프로세스 ID를 확인한 후, UPDATE Command를 수행합니다.

-- 트랜잭션 1 시작
BEGIN;

-- PID, transactionID 확인
SELECT pg_backend_pid(), txid_current();

 pg_backend_pid |   txid_current
----------------+----------------
        2487936 |           1572

-- UPDATE 수행    
UPDATE lock_test SET c3 = c3 + 100.00 WHERE c1 = 1;

트랜잭션 1을 시작한 프로세스 248936가 가지고 있는 Lock에 대한 결과는 pg_locks를 조회하여 확인할 수 있습니다.

Locks on Transaction IDs에서 설명한 바와 같이, ①트랜잭션 1의 트랜잭션 ID에 대해, ExclusiveLock 모드로 Lock(transactionid, virtualxid)을 획득합니다. 그리고 UPDATE의 대상 Relation인 lock_test에 대해서는 RowExclusiveLock모드로 Lock 획득에 성공하였음을 알 수 있습니다. (granted = true)

이어서, 다른 프로세스에서 트랜잭션 2를 시작하여 동일 테이블에 인덱스를 생성합니다.

-- 트랜잭션 2 시작
BEGIN;

-- PID, transactionID 확인
SELECT pg_backend_pid(), txid_current();

 pg_backend_pid | txid_current 
----------------+--------------
        2487939 |         1573

-- CREATE INDEX 수행
CREATE INDEX lock_idx ON lock_test (c2);

위와 같이 pg_locks 결과를 조회해 보면, ①트랜잭션 2도 자신의 트랜잭션 ID에 대한 Lock을 획득한 상태입니다. ②또한 인덱스 생성 대상 Relation인 lock_test에 대하여 ShareLock 모드로 Lock을 요청하였지만, 해당 Lock은 granted 값이 false 인 것을 통해 획득에 실패한 것을 알 수 있습니다. ③이는 lock_test에 대하여 트랜잭션 1이 RowExclusiveLock으로 Lock을 이미 획득하였기 때문이며, RowExclusiveLockShareLock은 상호호환되지 않으므로 트랜잭션 2는 트랜잭션 1 종료 시까지 대기하게 됩니다.

 

[CASE 2] Lock Mode 간 호환이 되는 경우

이번에는 두 개의 트랜잭션이 요청하는 Relation의 Lock Mode가 호환이 되는 경우에 대한 예시를 살펴보겠습니다.

-- 트랜잭션 1 시작
BEGIN;

-- PID, transactionID 확인
SELECT pg_backend_pid(), txid_current();

 pg_backend_pid | txid_current 
----------------+--------------
        2493574 |         1574
      
 -- 2. UPDATE 수행 
 => UPDATE lock_test SET c3 = c3 + 100.00 WHERE c1 = 1;

트랜잭션 1에서는 [CASE 1]과 동일한 UPDATE Command를 수행하였습니다. pg_locks를 조회하면, [CASE1]과 동일하게 ①트랜잭션 1의 트랜잭션 ID에 대해, ExclusiveLock 모드로 Lock(transactionid, virtualxid)을 획득하였으며, UPDATE의 대상 Relation인 lock_test에 대해서는 RowExclusiveLock모드로 Lock 획득에 성공하였음을 알 수 있습니다. (granted = true)

그리고 트랜잭션 2에서는 [CASE 1]과 다르게 SELECT FOR UPDATE Command를 수행하고 pg_locks 결과를 확인합니다.

-- 트랜잭션 2 시작
BEGIN;

-- PID, transactionID 확인
SELECT pg_backend_pid(), txid_current();

 pg_backend_pid | txid_current 
----------------+--------------
        2493575 |         1575

-- 2. SELECT FOR UPDATE 수행
=> SELECT * FROM lock_test WHERE c1 = 1 FOR UPDATE;

결과를 확인해 보면, ①트랜잭션 2도 자신의 트랜잭션 ID에 대한 Lock을 획득한 상태입니다. 그러나 [CASE 1]과 달리 ②트랜잭션 2를 시작한 프로세스 2492575는 Relation lock_test에 대하여 RowShareLock 모드로 Relation Lock 획득에 성공하였음을 알 수 있습니다. (granted = true) 이는 동일한 Relation에 대한 요청이지만, RowShareLock 모드가 ③트랜잭션 1이 lock_test에 대하여 획득한 RowExclusiveLock모드와 서로 동시 작업이 가능한 호환되는 모드이기 때문입니다.

📢 위 pg_locks 결과에서 * 7번째와 9번째 Row에 표시된 transactionid(lockid:1574)와 tuple에 대한 Lock은 이후 설명할 Row-level Lock과 관련됩니다.

 

마무리

이번 글에서는 PostgreSQL의 Lock, 그중에서 Relation-level의 Lock에 대해서 알아보았습니다. 주요 내용을 다시 한번 살펴보면 다음과 같습니다.

  • Relation은 Database Object 중, 테이블과 같은 구조를 갖는 Object를 가리킨다.
  • Relation-level Lock은 동시 업데이트로부터 Relation을 보호하고, Relation의 구조를 조정하는 동안 해당 Relation에 대한 사용을 금지하는 역할을 한다.
  • Relation Lock은 Access Share, Row Share, Row Exclusive, Share Update Exclusive, Share, Share Row Exclusive, Exclusive, Access Exclusive의 총 8가지 모드를 제공한다.
  • 트랜잭션을 식별하기 위해 Virtual TransactionID와 TransactionID를 사용하며, 트랜잭션의 시작과 종료 시점 추적을 목적으로 트랜잭션 ID(Virtual TransactionID, TransactionID)에 대한 Lock을 사용한다.

다음 글에서는 Relation의 구성 요소인 Row를 보호하기 위한 Lock에 대해서 알아보겠습니다. Row-level의 Lock은 PostgreSQL의 MVCC와 Update 동작 과정에 대한 이해가 선행되어야 하므로, 관련하여 아래 글을 먼저 읽어보시기 바랍니다.

📢 PostgreSQL Vacuum - MVCC (https://blog.ex-em.com/1663)
      PostgreSQL HOT - Update 동작 과정 (https://blog.ex-em.com/1771)

 

 

댓글