PostgreSQL 어드바이저리 잠금을 이용한 분산 작업 조정
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
분산 시스템의 세계에서 여러 독립적인 인스턴스 간의 작업을 조정하는 것은 흔하지만 어려운 문제입니다. 중요한 작업을 한 번에 하나의 인스턴스만 수행하도록 하거나 충돌 없이 공유 리소스를 관리하려면 강력한 동기화 메커니즘이 필요합니다. 전통적인 접근 방식은 종종 전용 큐 서비스 또는 복잡한 합의 알고리즘을 포함하며, 이는 추가 인프라 오버헤드와 복잡성을 초래할 수 있습니다. 그러나 많은 시나리오에서 더 간단하고 데이터베이스 중심적인 솔루션이 놀라울 정도로 효과적일 수 있습니다. 이 글에서는 PostgreSQL의 어드바이저리 잠금이 분산 작업 조정을 달성하기 위한 강력하지만 종종 간과되는 기본 요소를 제공하는 방법, PostgreSQL을 이미 활용하고 있는 애플리케이션을 위한 가볍고 효율적이며 즉시 사용 가능한 솔루션을 제공하는 방법에 대해 자세히 알아봅니다.
어드바이저리 잠금 및 적용 이해
구현에 대해 자세히 알아보기 전에 관련된 핵심 개념을 명확하게 이해해 보겠습니다.
어드바이저리 잠금은 PostgreSQL에서 관리하는 애플리케이션 수준 잠금입니다. 데이터베이스 트랜잭션 관리자가 자동으로 획득하고 해제하는 테이블 또는 행 잠금과 달리 어드바이저리 잠금은 명시적입니다. 애플리케이션 코드에서 획득 및 해제되므로 개발자가 수명 주기를 세부적으로 제어할 수 있습니다. 중요한 것은 어드바이저리 잠금은 특정 데이터베이스 개체에 연결되지 않으며 테이블 또는 행에 대한 일반 SQL 작업을 차단하지 않는다는 것입니다. 이는 애플리케이션별 동기화 지점을 정의하는 데 완벽한 애플리케이션에서 알고 있는 임의의 정수 키로 존재합니다. PostgreSQL은 세션 수준 및 트랜잭션 수준 어드바이저리 잠금을 모두 제공합니다. 세션 수준 잠금은 데이터베이스 세션의 지속 시간 동안 또는 명시적으로 해제될 때까지 유지되는 반면, 트랜잭션 수준 잠금은 트랜잭션이 끝날 때 자동으로 해제됩니다. 분산 작업 조정을 위해 세션 수준 잠금이 일반적으로 더 유용합니다. 이는 단일 인스턴스가 여러 작업에 걸쳐 또는 장기간 동안 잠금을 보유할 수 있기 때문입니다.
분산 작업 조정은 분산 환경에서 여러 독립적인 프로세스 또는 서비스에서 수행되는 작업을 관리하고 동기화하는 과제를 나타냅니다. 여기에는 배치 작업이 한 번만 실행되도록 하거나, 여러 인스턴스 중에서 리더를 선출하거나, 외부 리소스와 상호 작용하는 코드의 중요 섹션을 보호하는 시나리오가 포함됩니다.
작동 원리
분산 작업 조정을 위해 어드바이저리 잠금을 사용하는 기본 원리는 간단합니다. 각 분산 작업 또는 중요 섹션에는 고유 식별자(일반적으로 정수)가 할당됩니다. 인스턴스가 이 작업을 실행하려고 하면 해당 식별자와 관련된 어드바이저리 잠금을 획득하려고 시도합니다.
- 잠금을 사용할 수 있으면 인스턴스가 잠금을 획득하고 작업을 계속합니다. 이는 해당 작업의 "소유자" 또는 "실행자"임을 나타냅니다.
- 잠금이 이미 다른 인스턴스에서 보유하고 있는 경우 현재 인스턴스는 잠금 해제를 기다리거나(차단 동작) 즉시 대체 경로(비차단 동작)를 계속할 수 있습니다. 차단 및 비차단 선택은 특정 조정 요구 사항에 따라 달라집니다.
작업이 완료되면 인스턴스가 명시적으로 잠금을 해제하여 다른 인스턴스가 사용할 수 있도록 합니다. 이 메커니즘은 특정 시간에 한 번의 인스턴스만 특정 잠금을 성공적으로 획득하고 보유할 수 있도록 하여 공유 리소스에 대한 액세스를 조정하거나 작업의 독점적 실행을 보장합니다.
구현 예: 배치 작업 한 번 실행 보장
일반적인 시나리오를 생각해 보겠습니다. 여러 애플리케이션 인스턴스가 실행되도록 예약되어 있더라도 단일 인스턴스에서만 실행되어야 하는 일일 배치 작업입니다.
db_pgsql` 확장을 사용한 PHP를 사용하겠지만, 이 개념은 다른 언어 및 ORM으로 쉽게 이전될 수 있습니다.
<?php // 데이터베이스 연결 세부 정보 $dsn = 'pgsql:host=localhost;port=5432;dbname=mydatabase'; $user = 'myuser'; $password = 'mypassword'; // 어드바이저리 잠금을 위한 배치 작업의 고유 식별자 // 이를 상수 또는 의미 있는 문자열에서 파생하는 것이 좋습니다. const BATCH_JOB_LOCK_ID = 123456; try { $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_CASE => PDO::CASE_NATURAL, ]); echo "배치 작업 잠금 획득 시도 중... "; // 세션 수준 어드바이저리 잠금(비차단) 획득 시도 // pg_try_advisory_lock(key)는 획득 성공 시 true, 그렇지 않은 경우 false를 반환합니다. $stmt = $pdo->prepare("SELECT pg_try_advisory_lock(:lock_id)"); $stmt->execute([':lock_id' => BATCH_JOB_LOCK_ID]); $result = $stmt->fetchColumn(); if ($result) { echo "잠금 성공적으로 획득! 배치 작업 실행 중... "; // 배치 작업 실행 시뮬레이션 sleep(5); // 실제 배치 작업 로직이 여기에 들어갑니다. echo "배치 작업 완료. "; // 잠금 해제 // pg_advisory_unlock(key) $stmt = $pdo->prepare("SELECT pg_advisory_unlock(:lock_id)"); $stmt->execute([':lock_id' => BATCH_JOB_LOCK_ID]); echo "잠금 해제됨. "; } else { echo "잠금을 획득할 수 없습니다. 다른 인스턴스가 배치 작업을 실행 중일 가능성이 높습니다. 건너뜁니다. "; } } catch (PDOException $e) { echo "데이터베이스 오류: " . $e->getMessage() . " "; exit(1); } ?>
설명:
- 고유 잠금 ID:
BATCH_JOB_LOCK_ID는 _이 특정 배치 작업_을 모든 인스턴스에서 고유하게 식별하는 임의의 정수입니다. pg_try_advisory_lock(): 이것이 핵심 함수입니다. 세션 수준 어드바이저리 잠금을 획득하려고 시도합니다. 잠금이 이미 다른 세션(다른 애플리케이션 인스턴스의 세션이라도)에서 보유하고 있는 경우 차단하지 않고 즉시false를 반환합니다. 이는 교착 상태를 피하고 다른 인스턴스가 작업을 정상적으로 건너뛸 수 있도록 하는 데 중요합니다.- 핵심 섹션:
if ($result)블록 안의 코드는 핵심 섹션으로, 하나의 인스턴스만 실행해야 하는 코드 부분입니다. pg_advisory_unlock(): 핵심 섹션이 완료되면(또는 오류가 발생하고 잠금을 명시적으로 해제하려는 경우)pg_advisory_unlock()을 호출하여 잠금을 해제합니다. 명시적 호출이나 세션 종료를 통해 항상 잠금이 해제되도록 하는 것이 중요합니다.
적용 시나리오
- 리더 선출: 여러 인스턴스 세트가 특정 어드바이저리 잠금을 획득하기 위해 경쟁할 수 있습니다. 성공하는 인스턴스가 리더가 됩니다. 리더가 실패하면 세션이 결국 종료되고 잠금이 해제되어 다른 인스턴스가 잠금을 획득할 수 있습니다.
- 외부 API에 대한 속도 제한: 속도 제한이 있는 외부 서비스에 대한 호출을 하기 전에 인스턴스가 "속도 제한 슬롯"을 나타내는 어드바이저리 잠금을 획득할 수 있습니다. 이는 동시 호출 수를 제한합니다.
- 분산 캐시 무효화: 캐시를 무효화해야 할 때 하나의 인스턴스가 해당 무효화를 수행하기 위해 잠금을 획득하여 다른 인스턴스가 동시에 동일한 비용이 많이 드는 작업을 시도하는 것을 방지할 수 있습니다.
- 리소스 예약: 어드바이저리 잠금을 보유하여 특정 기간 동안 공유되고 제한된 리소스를 예약합니다.
고려 사항 및 모범 사례
- 잠금 ID: 우발적인 충돌을 피하기 위해 잠금 ID에 크고 명확한 정수 값을 사용합니다. 문자열을 해싱하거나 식별자를 결합하여 프로그래밍 방식으로 고유 잠금 ID를 생성하는 것을 고려하십시오. PostgreSQL은 128비트 키를 위해
pg_advisory_lock(bigint key1, bigint key2)를 제공하여 더 많은 네임스페이스를 제공합니다. - 오류 처리: 데이터베이스 연결 오류를 항상 처리하고 중요 섹션에 실패하더라도 잠금이 해제되도록 합니다.
- 세션 종료: 세션 수준 어드바이저리 잠금은 데이터베이스 세션이 종료되면(예: 애플리케이션 충돌, 연결 손실) 자동으로 해제됩니다. 이것은 중요한 자동 복원 메커니즘입니다.
- 차단 vs. 비차단: 비차단 동작(다른 인스턴스가 건너뛰어야 하는 "한 번 실행" 작업에 적합)에는
pg_try_advisory_lock()을 사용하고, 인스턴스가 차례를 기다리게 하려면(차단하려는 경우)pg_advisory_lock()을 사용합니다. 교착 상태 또는 장기 대기를 피하기 위해 분산 시스템에서 차단 잠금을 사용할 때 주의하십시오. - 모니터링: 어드바이저리 잠금은 표준 PostgreSQL 모니터링 뷰에 직접 노출되지 않지만,
pg_locks를 쿼리하여 활성 어드바이저리 잠금을 보고 잠재적인 문제를 식별할 수 있습니다.locktype = 'advisory'를 찾으십시오. - 세분성: 잠금의 세분성에 주의하십시오. 너무 광범위한 잠금은 동시성을 줄일 수 있고, 너무 세부적인 잠금은 복잡성을 증가시킬 수 있습니다.
결론
PostgreSQL 어드바이저리 잠금은 분산 작업 조정을 위한 강력하고 가벼우며 종종 과소평가되는 메커니즘을 제공합니다. 이러한 애플리케이션 수준 잠금을 활용함으로써 개발자는 많은 일반적인 시나리오에 대해 별도의 조정 서비스의 오버헤드 및 복잡성을 피하면서 데이터베이스 내에서 직접 강력한 동기화 패턴을 구현할 수 있습니다. 이 접근 방식은 단순성, 안정성 및 효율성을 제공하므로 PostgreSQL 기반 분산 시스템에서 작업을 조정하고 공유 리소스를 관리하는 데 탁월한 도구입니다.

