운영 중인 서비스에 조회수 카운터가 있는데, 평소엔 문제없다가 피크 트래픽 때만 이런 로그가 찍히고 트랜잭션이 늘어집니다.
ERROR: canceling statement due to lock timeout
CONTEXT: while updating tuple (42,17) in relation "counters"
쿼리는 아주 단순합니다.
UPDATE counters SET value = value + 1 WHERE key = 'page_view_home';
SELECT FOR UPDATE는 안 쓰고 있고, 단건 UPDATE 하나뿐인데 왜 lock이 쌓이는지 궁금합니다. 같은 행을 여러 워커가 동시에 업데이트하는 구조라는 건 맞습니다.
환경은 PostgreSQL 16, 커넥션 풀은 pgbouncer, 워커는 20개 정도입니다.
댓글 4
"단건 UPDATE니까 안전하다"는 오해가 핵심입니다. row-level lock은 UPDATE 문장이 트랜잭션 커밋될 때까지 유지되므로, 같은 행을 동시에 건드리면 뒤에 온 트랜잭션은 전부 대기 큐에 쌓입니다. 피크 시 워커 20개가 같은 row를 업데이트하면 직렬화되는 거예요.
해결 방법 3가지 (현실성 순서)
① 카운터만 Redis로 빼고 배치 sync
가장 검증된 방법입니다. Redis
INCR은 atomic하고 RDB에 lock을 안 만듭니다. 1분마다 배치로 Postgres에 누적치를 flush.② advisory lock으로 직렬화
Redis 도입이 부담이면, PostgreSQL에서 제공하는 advisory lock을 쓰세요. row lock보다 가볍고 hash로 키만 잡으면 됩니다.
③ 샤드된 카운터
트래픽이 아주 높다면 같은 key를 N개 행으로 쪼갭니다. 읽을 땐 SUM.
또 하나, pgbouncer transaction 모드에서는 세션 단위
SET LOCAL lock_timeout동작에 주의해야 합니다. 쿼리 단위로 명시하는 게 안전합니다.저는 카운터 성격의 워크로드는 그냥 RDB에서 빼는 게 맞다고 봅니다. 조회수 같은 수치는 1~2개 유실돼도 비즈니스적으로 큰 문제가 안 되니까 Redis
INCR→ 주기적 sync가 제일 깔끔해요. 정합성이 중요한 회계 카운터면 advisory lock + 샤드 카운터 조합을 추천합니다.advisory lock으로 긴급 조치했고 Redis 이관 작업을 스프린트에 올렸습니다. 감사합니다.
그런데 deadlock이 실제로 생기는지 어떻게 모니터링하세요?
log_lock_waits정도만 켜놓으면 되는지요?최소 세트는 이 정도를 권장합니다.
실시간으로 확인하려면
pg_stat_activity에서wait_event_type = 'Lock'세션을 잡고pg_blocking_pids()로 블로킹 체인을 추적합니다.그리고 Grafana에
pg_stat_database.deadlocks카운터를 대시보드로 붙여놓으면 추세 관찰이 편합니다.