PostgreSQL : 데드락 감지 메커니즘과 처리 방식

hyezuu·2025년 3월 27일

시작하며

데이터베이스 시스템에서 동시성 제어는 핵심 기능 중 하나다. 여러 트랜잭션이 동시에 데이터에 접근할 때 데드락이 발생할 수 있는데, PostgreSQL은 이를 자동으로 감지하고 해결하는 메커니즘을 갖추고 있다.

데드락이란?

데드락은 두 개 이상의 트랜잭션이 서로가 보유한 락을 기다리며 무한정 대기하는 상태를 말한다. 예를 들어 :

  • 트랜잭션 A: 자원 1에 락을 건 상태에서 자원 2의 락을 기다림
  • 트랜잭션 B: 자원 2에 락을 건 상태에서 자원 1의 락을 기다림

이런 상황에서 두 트랜잭션 모두 영원히 진행하지 못하는 교착 상태가 발생한다.

PostgreSQL의 데드락 감지 동작 원리

PostgreSQL은 다음과 같은 방식으로 데드락을 감지하고 처리한다:

1. 감지 메커니즘

  • 의존성 그래프 분석: PostgreSQL은 트랜잭션 간의 락 의존성 그래프를 구성하고 순환 의존성을 찾아 데드락을 감지한다.
  • 타임아웃 기반 감지: deadlock_timeout 파라미터(기본값 1초) 이상 락을 기다리면 데드락 감지 프로세스가 실행된다.
  • 주기적 검사: 대기 중인 프로세스가 지정된 시간 이상 블로킹되면 자동으로 데드락 검사를 시작한다.
-- 데드락 감지 타임아웃 설정 변경 (밀리초 단위)
SET deadlock_timeout = '500ms';

2. 감지 후 처리 절차

  • 희생자 선택: 데드락이 감지되면 PostgreSQL은 "희생자(victim)" 트랜잭션을 선택한다.
  • 선택 기준: 일반적으로 가장 적은 작업을 수행한 트랜잭션이 희생자로 선택된다.
  • 자동 롤백: 선택된 트랜잭션은 자동으로 롤백되며 모든 변경사항이 취소된다.
  • 에러 반환: 롤백된 트랜잭션에는 다음과 같은 에러 메시지가 반환된다:
    ERROR: deadlock detected
    DETAIL: Process 1234 waits for ShareLock on transaction 5678; blocked by process 5678.
    Process 5678 waits for ShareLock on transaction 1234; blocked by process 1234.
    HINT: See server log for query details.

3. 로깅 및 모니터링

  • 서버 로그: 데드락 발생 시 서버 로그에 상세 정보가 기록된다.
  • 로그 항목: 관련 트랜잭션, 프로세스 ID, 락 종류, 관련 테이블과 인덱스 등이 포함된다.
  • 통계 정보: pg_stat_database 뷰를 통해 데이터베이스별 데드락 발생 통계를 확인할 수 있다.
-- 데이터베이스별 데드락 통계 확인
SELECT datname, deadlocks FROM pg_stat_database;

데드락 감지의 내부 구현

PostgreSQL의 데드락 감지는 다음과 같은 단계로 이루어진다:

  1. 대기 그래프 구성: 프로세스 간 락 대기 관계를 방향 그래프로 구성
  2. 사이클 탐지 알고리즘: 깊이 우선 탐색(DFS)을 사용하여 그래프 내 사이클 탐지
  3. 데드락 확인: 사이클이 발견되면 데드락으로 간주
  4. 희생자 선택 알고리즘: 희생자 선택 기준 적용 (일반적으로 가장 최근에 시작한 트랜잭션)

PostgreSQL의 소스 코드에서 데드락 감지 부분은 https://github.com/postgres/postgres/blob/master/src/backend/storage/lmgr/deadlock.c 에 구현되어 있다.

데드락 감지 시 주의사항

  1. 성능 영향: deadlock_timeout 값을 너무 낮게 설정하면 불필요한 데드락 검사로 성능이 저하될 수 있다.
  2. 희생자 롤백의 한계: 복잡한 트랜잭션이 롤백되면 많은 작업이 취소되어 효율성이 떨어질 수 있다.
  3. 분산 시스템의 한계: 여러 데이터베이스 인스턴스 간의 데드락은 감지할 수 없다.
  4. 실패 처리의 중요성: 애플리케이션은 데드락으로 인한 실패를 적절히 처리해야 한다.

실제 데드락 감지 예시 (SQL)

다음은 데드락이 발생할 수 있는 두 트랜잭션의 예시다:

세션 1:

BEGIN;
-- 먼저 hub_id '5e27b315-a4d4-43e2-81fd-e2f3771f46fc'의 상품들을 업데이트 (1)
UPDATE public.p_stock
SET quantity = quantity - 10, updated_at = NOW(), updated_by = 10
WHERE hub_id = '5e27b315-a4d4-43e2-81fd-e2f3771f46fc'
  AND product_id = '8a47ecaa-141f-4ebf-885c-dcc0bcbc644c';

SELECT pg_sleep(2);
-- 다음으로 다른 hub에 있는 상품 업데이트 시도 (3)
UPDATE public.p_stock
SET quantity = quantity + 5, updated_at = NOW(), updated_by = 10
WHERE hub_id = '80f07400-d252-4130-abe3-99df408eadf1'
  AND product_id = '2bbb56d6-9961-4ece-aa1e-e3851f47569e';

COMMIT;

세션 2:

-- 먼저 hub_id '80f07400-d252-4130-abe3-99df408eadf1'의 상품을 업데이트 (2)
UPDATE public.p_stock
SET quantity = quantity - 20, updated_at = NOW(), updated_by = 11
WHERE hub_id = '80f07400-d252-4130-abe3-99df408eadf1'
  AND product_id = '2bbb56d6-9961-4ece-aa1e-e3851f47569e';

-- 잠시 대기 (다른 세션이 락을 획득할 시간) (2)
SELECT pg_sleep(2);
-- 다음으로 다른 hub에 있는 상품들 업데이트 시도 (4)
UPDATE public.p_stock
SET quantity = quantity + 15, updated_at = NOW(), updated_by = 11
WHERE hub_id = '5e27b315-a4d4-43e2-81fd-e2f3771f46fc'
  AND product_id IN ('8a47ecaa-141f-4ebf-885c-dcc0bcbc644c', 'f000f6f5-a4ad-47be-9b11-5c8e060eb5e4');

COMMIT;

이런 상황에서 PostgreSQL은 데드락을 감지하고 둘 중 하나의 트랜잭션을 희생자로 선택해 롤백한다.

-- 세션 2에서 실행된 내용:

-- 1. 첫 번째 UPDATE 쿼리: '80f07400-d252-4130-abe3-99df408eadf1' hub의 상품 수량 감소
-- 이 시점에서 해당 행에 대한 배타적 락(X Lock)을 획득함
product.public> UPDATE public.p_stock
                SET quantity = quantity - 20, updated_at = NOW(), updated_by = 11
                WHERE hub_id = '80f07400-d252-4130-abe3-99df408eadf1'
                  AND product_id = '2bbb56d6-9961-4ece-aa1e-e3851f47569e'
[2025-03-27 22:54:03] 1 row affected in 5 ms  -- 성공적으로 1행 업데이트

-- 2. 2초 동안 대기 (이 시간 동안 세션 1이 다른 행에 락을 획득할 기회를 줌)
product.public> SELECT pg_sleep(2)
[2025-03-27 22:54:05] 1 row retrieved starting from 1 in 2 s 161 ms (execution: 2 s 11 ms, fetching: 150 ms)

-- 3. 두 번째 UPDATE 쿼리: '5e27b315-a4d4-43e2-81fd-e2f3771f46fc' hub의 상품 수량 증가
-- 이 시점에서 세션 1이 이미 이 행에 락을 보유하고 있음
-- 그리고 세션 1은 세션 2가 잠근 행에 접근을 시도하고 있음 -> 데드락 발생!
product.public> UPDATE public.p_stock
                SET quantity = quantity + 15, updated_at = NOW(), updated_by = 11
                WHERE hub_id = '5e27b315-a4d4-43e2-81fd-e2f3771f46fc'
                  AND product_id IN ('8a47ecaa-141f-4ebf-885c-dcc0bcbc644c', 'f000f6f5-a4ad-47be-9b11-5c8e060eb5e4')
[2025-03-27 22:54:05] 2 rows affected in 5 ms  -- 이 쿼리는 성공했지만 데드락 감지로 트랜잭션이 이미 롤백됨

-- 4. COMMIT 시도: 하지만 데드락으로 인해 이미 트랜잭션이 자동 롤백됨
product.public> COMMIT
[2025-03-27 22:54:05] [25P01] there is no transaction in progress  -- 에러: 진행 중인 트랜잭션 없음
[2025-03-27 22:54:05] completed in 4 ms

로그레벨 조정하여 더 상세한 정보 보기

ALTER SYSTEM SET log_min_messages = 'DEBUG5';
-- 데드락 관련 로그 활성화
ALTER SYSTEM SET log_lock_waits = on;
-- 변경사항 적용
SELECT pg_reload_conf();
[2025-03-27 23:03:17] [40P01] ERROR: deadlock detected
[2025-03-27 23:03:17] Detail: Process 57417 waits for ShareLock on transaction 275838; blocked by process 58881.
[2025-03-27 23:03:17] Process 58881 waits for ShareLock on transaction 275837; blocked by process 57417.
[2025-03-27 23:03:17] Hint: See server log for query details.
[2025-03-27 23:03:17] Where: while updating tuple (0,45) in relation "p_stock"
  1. 에러 코드: 40P01 - PostgreSQL의 데드락 감지 코드

  2. 상호 차단 구조:

    • 프로세스 57417은 트랜잭션 275838에 대한 ShareLock을 대기 중
    • 프로세스 58881은 이를 차단 중
    • 반대로 프로세스 58881은 트랜잭션 275837에 대한 ShareLock을 대기 중
    • 프로세스 57417은 이를 차단 중
  3. 발생 지점: p_stock 테이블의 튜플(0,45) 업데이트 과정

이런 상황은 서로 다른 행에 락을 잡은 두 트랜잭션이 각자 상대방이 잠근 행에 접근하려 할 때 발생한다. PostgreSQL의 데드락 감지기는 이 상황을 자동으로 감지하고 둘 중 하나의 트랜잭션을 희생자로 선택해 롤백시킨다.

  1. 각 프로세스가 서로 다른 행에 배타적 락을 획득
  2. 각 프로세스가 서로의 락을 요청하며 대기
  3. 데드락 감지기가 순환 대기 상태 감지
  4. 비용이 적은 트랜잭션을 자동 롤백
  5. 롤백된 트랜잭션은 "deadlock detected" 에러 반환
  6. 다른 트랜잭션은 정상 진행

결론

PostgreSQL의 자동 데드락 감지 및 해결 메커니즘은 데이터 일관성을 유지하면서 시스템이 계속 작동할 수 있게 해주는 중요한 기능이다. 그러나 데드락 감지 자체는 이미 발생한 문제를 해결하는 방법(탐지 및 복구)이므로, 애플리케이션 설계 단계에서 데드락을 예방(예방 및 회피)하는 전략을 함께、사용하는 것이 바람직하겠다.

profile
기록

2개의 댓글

comment-user-thumbnail
2025년 3월 27일

매일 다른 기술 블로그 대단하세요! 잘 보고 있습니다!!

1개의 답글