SQL프로그래밍수석 데이터베이스 엔지니어

PostgreSQL의 어드바이저리 잠금의 어떤 특정 속성이 세션 범위에서 중재 잠금을 방지하여 중복 비즈니스 키 수집을 방지할 수 있습니까?

Hintsage AI 어시스턴트로 면접 통과

질문에 대한 답변.

질문 역사.

어드바이저리 잠금은 PostgreSQL 8.2에서 처음 등장하여 MVCC 튜플 가시성 시스템 외부에서 작동하는 경량 애플리케이션 수준 동기화 원시를 제공합니다. 이들은 테이블 기반 잠금이 의미적으로 부적절하거나 성능적으로 비효율적인 워크플로우(예: 큐 처리 및 멱등 수집)를 위해 설계되었습니다. 특정 테이블 튜플에 묶여 있는 행 수준 잠금과 달리 어드바이저리 잠금은 공유 메모리 잠금 관리자 내에 완전히 존재하여 데드 튜플이나 WAL 트래픽을 생성하지 않고 추상 자원에 대한 접근을 조정하는 메커니즘을 제공합니다.

문제.

높은 동시성 멱등 수집 파이프라인에서 비즈니스 키(예: 외부 UUID)에 대한 유일성을 전통적인 INSERT ... ON CONFLICT 또는 SELECT FOR UPDATE를 통해 강제하면 심각한 병목 현상이 발생합니다. 행 수준 접근 방식은 잠금 비트를 설정하기 위해 힙에 기록해야 하며, 이는 테이블을 부풀게 하고 VACUUM 압력을 가속화하여 고유 인덱스에서의 갈등 해결 중 핫스팟을 초래합니다. 문제는 저장소 레이어를 만지지 않고 해시된 비즈니스 키와 같은 논리 형식에 대한 상호 배제를 제공하는 것입니다. 잠금 실패가 지속적인 연결 풀에 리소스를 유출하지 않도록 보장해야 합니다.

해결책.

중요한 속성은 어드바이저리 잠금이 LOCKMETHOD_ADVISORY를 사용하여 공유 메모리 내의 LOCKTAG 해시 테이블에 독점적으로 저장되며, 따라서 기본 관계 페이지를 수정하지 않는다는 것입니다. **pg_advisory_xact_lock(hashtext(business_key))**를 사용하여 애플리케이션은 COMMIT 또는 ROLLBACK 시 자동으로 해제되는 트랜잭션 범위 잠금을 획득하며, 세션 수준 pg_advisory_lock과 관련된 잠금 유출을 방지합니다. 이 접근 방식은 잠금이 메모리의 경량 항목으로만 존재하므로 테이블 부풀림과 인덱스 갈등을 제거합니다. 아래 예시에서 볼 수 있습니다:

BEGIN; -- 해시된 비즈니스 키에 대한 트랜잭션 기반 잠금 획득 SELECT pg_advisory_xact_lock(hashtext('a1b2c3d4')); -- 삽입 안전; 다른 세션이 잠금을 보유하는 경우 고유 인덱스 갈등 없음 INSERT INTO events (business_key, payload) VALUES ('a1b2c3d4', '{"event":"click"}') ON CONFLICT (business_key) DO NOTHING; COMMIT;

실제 상황

텔레메트리 회사의 데이터 플랫폼 팀은 Kafka에서 PostgreSQL로 수집되는 초당 50,000개의 이벤트에 대해 정확히 한 번만 처리를 보장해야 했습니다. 각 이벤트는 멱등성 키 역할을 하는 클라이언트 생성 UUID를 담고 있었습니다. 고유 UUID 열에서 INSERT ... ON CONFLICT DO NOTHING을 사용한 초기 로드 테스트는 고유 B-tree 인덱스에서의 스핀락 갈등으로 인한 심각한 꼬리 지연을 초래했으며 HOT 업데이트 실패로 인한 신속한 부풀림이 있었습니다. 피크 시간 동안 WAL 생성 속도는 두 배가 되어 복제 지연과 저장소 용량을 위협했습니다.

제안된 수정 중 하나는 SELECT * FROM events WHERE business_key = $1 FOR UPDATE를 사용하여 키 존재 여부를 사전 확인한 후 결과가 비어 있을 경우에만 삽입하는 것이었습니다. 이는 중복을 방지했지만 모든 작성자가 기존 행이나 대리 예약 행 중 하나에 잠금을 획득해야 하므로 예약 테이블의 페이지에서 큰 핫스팟을 생성했습니다. 이 접근 방식은 매 15분마다 VACUUM을 요구하여 사망한 튜플을 회수해야 했으며 잠금을 전체 트랜잭션 기간 동안 보유하지 않으면 체크와 삽입 간의 경쟁 조건을 방지할 수 없었습니다.

아키텍처 팀은 삽입을 제한하기 위해 외부 Redis 캐시로 조정을 이동할 것을 제안했습니다. 이를 통해 데이터베이스 부풀림을 제거하고 PostgreSQL 부하를 줄일 수 있었으나, Redis 클러스터와 데이터베이스 간의 네트워크 분할이 Redis 잠금이 만료되었지만 PostgreSQL 트랜잭션이 아직 커밋되지 않은 경우 중복 삽입을 허용할 수 있는 심각한 실패 모드를 초래했습니다. 더욱이 두 분산 시스템 간에 일관성을 유지하는 것은 운영 복잡성을 증가시키고 Redlock 또는 유사한 알고리즘을 구현해야 하며, 이로 인해 각 작업의 지연 시간이 약 5 밀리초 증가했습니다.

선택된 설계는 **pg_advisory_xact_lock(hashtext(business_key))**를 통해 PostgreSQL의 기본 어드바이저리 잠금을 활용하여 삽입을 시도하기 전에 해시된 UUID에 대한 트랜잭션 경계 잠금을 획득했습니다. 이러한 잠금은 공유 메모리에만 존재하고 힙을 만지지 않기 때문에 저장소 오버헤드가 전혀 없으며 트랜잭션 종료 시 자동으로 해제되어 세션 수준 잠금에서 관찰된 잠금 유출을 방지합니다. 검출할 수 없는 데드락을 피하기 위해 애플리케이션 계층은 모든 UUID를 해시된 정수 값에 따라 정렬하여 잠금을 획득하기 전에 모든 동시 작업자 간의 글로벌 정렬 프로토콜을 보장했습니다.

어드바이저리 잠금은 가장 낮은 지연시간(서브 밀리초 획득)과 영구적인 저장소 측면 효과가 없는 상태에서 외부 종속성 없이 엄격한 정확성을 유지하기 때문에 선택되었습니다. Redis 접근과 달리 잠금의 수명은 데이터베이스 트랜잭션에 묶여있어 잠금 획득과 삽입 커밋 간의 원자성을 보장합니다. SELECT FOR UPDATE와는 달리 테이블 부풀림이 생성되지 않았으며, 원시 ON CONFLICT와는 달리 충돌하는 동시 삽입으로 인해 고유 인덱스에 스트레스가 발생하지 않았습니다. 이는 힙 접근 전에 직렬화가 이루어지기 때문입니다.

배치 후, 수집 파이프라인은 초당 80,000개의 이벤트를 유지하며 p99 지연 시간이 10 밀리초 이하였으며, 이전의 200ms 스파이크와 비교되었습니다. 테이블 부풀림은 미미한 수준으로 줄어들어 autovacuum이 비피크 시간에만 실행되었으며, WAL 볼륨은 40% 감소하여 아카이벌 저장 비용과 복제 지연을 대폭 절감했습니다. 이 시스템은 여러 데이터베이스 재시작 및 연결 풀 변동을 통해 정확히 한 번의 의미를 유지하며 중복 이벤트나 데드락으로 인한 타임아웃 없이 작동했습니다.

후보자들이 자주 놓치는 점

pg_advisory_lock (세션 범위) 대신 pg_advisory_xact_lock를 사용하는 것이 높은 처리량 작업 아키텍처에서 연결 풀 소진 및 중복 수집의 위험을 초래합니까?

후보자들은 종종 pg_advisory_lock이 명시적으로 잠금을 해제하거나 세션이 연결이 끊길 때까지 지속된다는 것을 인식하지 못합니다. 장기 연결을 재사용하는 풀 환경에서 잠금 해제 호출을 우회하는 논리 오류나 예외가 발생하면 잠금이 무기한 유지되어 같은 비즈니스 키를 처리하는 후속 작업자가 무한정 기다리게 됩니다. 대신 pg_advisory_xact_lock을 사용해야 하는 이유는 잠금 수명을 트랜잭션 경계에 묶어 ROLLBACK 시 자동 해제를 보장하여 잠금 누수를 방지하고 작업자 풀을 굶주리게 하여 수집 파이프라인을 중단시키지 않도록 합니다.

여러 어드바이저리 잠금을 획득할 때 전체 순서 보장 부재가 어떻게 검출할 수 없는 데드락으로 이어지며, 이 위험을 제거하는 특정 애플리케이션 패턴은 무엇입니까?

행 수준의 데드락처럼, PostgreSQLdeadlock_timeout 감지기가 희생 트랜잭션을 종료함으로써 해결하는 데드락은 사용자 정의 네임스페이스에서 발생하기 때문에 엔진에 보이지 않습니다. 만약 작업자 A가 자원 X를 잠그고 그 다음 Y를 잠그고, 작업자 B가 Y를 잠그고 그 다음 X를 잠군다면, 두 세션은 오류 없이 무기한 기다리게 됩니다. 필수 패턴은 모든 자원 식별자(예: hashtext(uuid) 값)를 엄격한 단조 순서(오름차순 또는 내림차순)로 정렬한 후 잠금 요청을 발행하는 것입니다. 이 글로벌 정렬은 대기 그래프가 비순환적으로 유지되도록 보장하여 순환 의존성을 불가능하게 하고 침묵하는 대기 위험을 제거합니다.

하나의 트랜잭션이 보유할 수 있는 어드바이저리 잠금 수를 제한하는 공유 메모리 제한은 무엇이며, max_locks_per_transaction를 초과했을 때의 증상은 행 수준 잠금 소진에 비해 어떻게 나타납니까?

많은 후보자들은 어드바이저리 잠금이 무한하다고 가정하지만, 이들은 max_locks_per_transaction 설정 매개변수(기본값 64)에 의해 관리되는 공유 잠금 테이블의 항목을 소모합니다. 이 한 트랜잭션에서 이 한도를 초과하여 더 많은 잠금을 보유하게 되면 **ERROR: out of shared memory (SQLSTATE 53200)**가 발생하여 즉시 트랜잭션이 중단됩니다. 이는 한도를 초과할 경우 일반적으로 잠금 업그레이드 또는 lock_timeout에 따라 대기하도록 하는 행 수준 잠금과는 대조적입니다. 완전한 공유 메모리 풀을 소진하지는 않습니다. 이를 완화하기 위해 작업을 더 작은 하위 트랜잭션으로 배치하거나 여러 논리 자원을 단일 어드바이저리 잠금 키로 집합 해싱하는 것이 좋습니다. 수천 개의 개별 키를 동시에 잠그려고 시도하는 대신에요.