267 lines
6.8 KiB
Markdown
267 lines
6.8 KiB
Markdown
---
|
|
id: db-lock-analysis
|
|
title: Postgres Lock 분석 — Deadlock / Wait / 진단
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [database, postgres, lock, vibe-coding]
|
|
tech_stack: { language: "SQL / Postgres", applicable_to: ["Backend"] }
|
|
applied_in: []
|
|
aliases: [pg lock, deadlock, lock_timeout, statement_timeout, blocked queries, FOR UPDATE]
|
|
---
|
|
|
|
# Postgres Lock 분석
|
|
|
|
> "느린 query" 가 사실 lock wait. **`pg_stat_activity` + `pg_locks`** 로 누가 누구를 차단. **lock_timeout / statement_timeout** 으로 무한 hang 방지.
|
|
|
|
## 📖 핵심 개념
|
|
- Row-level lock: SELECT FOR UPDATE / DELETE / UPDATE.
|
|
- Table-level lock: ALTER / DROP / VACUUM FULL.
|
|
- Advisory lock: 직접 잡음.
|
|
- Deadlock: 양방 lock — Postgres 가 1개 abort.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### Locking SELECT
|
|
```sql
|
|
-- Row lock (다른 transaction 의 같은 row UPDATE 차단)
|
|
SELECT * FROM accounts WHERE id = $1 FOR UPDATE;
|
|
|
|
-- Skip locked (큐 패턴)
|
|
SELECT * FROM jobs WHERE status = 'pending' ORDER BY id LIMIT 1
|
|
FOR UPDATE SKIP LOCKED;
|
|
|
|
-- No wait (즉시 fail)
|
|
SELECT * FROM accounts WHERE id = $1 FOR UPDATE NOWAIT;
|
|
|
|
-- Share (read 차단 X but write 차단)
|
|
SELECT * FROM accounts WHERE id = $1 FOR SHARE;
|
|
```
|
|
|
|
### Lock conflicts
|
|
```
|
|
SELECT : ACCESS SHARE
|
|
SELECT FOR UPDATE / SHARE : ROW SHARE → ROW EXCLUSIVE
|
|
INSERT/UPDATE/DELETE : ROW EXCLUSIVE
|
|
CREATE INDEX : SHARE
|
|
CREATE INDEX CONCURRENTLY : SHARE UPDATE EXCLUSIVE (덜 차단)
|
|
ALTER TABLE : ACCESS EXCLUSIVE (모두 차단)
|
|
```
|
|
|
|
→ ALTER TABLE = 모든 query 차단. Concurrent 가능한 변경 사용.
|
|
|
|
### Blocked queries 진단
|
|
```sql
|
|
SELECT
|
|
blocked.pid AS blocked_pid,
|
|
blocked.usename AS blocked_user,
|
|
blocked.query AS blocked_query,
|
|
blocking.pid AS blocking_pid,
|
|
blocking.usename AS blocking_user,
|
|
blocking.query AS blocking_query,
|
|
age(NOW(), blocked.query_start) AS blocked_for
|
|
FROM pg_stat_activity blocked
|
|
JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
|
|
WHERE NOT blocked.pid = blocking.pid;
|
|
```
|
|
|
|
→ "이 query 가 저 query 를 대기 중".
|
|
|
|
### Lock timeout
|
|
```sql
|
|
SET lock_timeout = '5s';
|
|
-- 5초 안 못 잡으면 ERROR
|
|
|
|
-- Statement timeout
|
|
SET statement_timeout = '30s';
|
|
-- 30초 안 안 끝나면 cancel
|
|
```
|
|
|
|
```ts
|
|
// App 에서 — 매 connection
|
|
await pool.query("SET statement_timeout = '30s'");
|
|
await pool.query("SET lock_timeout = '5s'");
|
|
```
|
|
|
|
또는 connection string:
|
|
```
|
|
postgresql://...?options=-c%20statement_timeout=30s
|
|
```
|
|
|
|
### Deadlock
|
|
```
|
|
Tx A: UPDATE a → wait for b (held by Tx B)
|
|
Tx B: UPDATE b → wait for a (held by Tx A)
|
|
→ deadlock — Postgres 가 1개 abort
|
|
```
|
|
|
|
```sql
|
|
-- log
|
|
log_lock_waits = on
|
|
deadlock_timeout = 1s -- 1초 후 deadlock 검사
|
|
```
|
|
|
|
→ Deadlock 자주 = 코드에서 lock 순서 통일.
|
|
|
|
```ts
|
|
// ❌ 다른 순서
|
|
async function transferA(from, to) {
|
|
await update(from, ...); // lock from
|
|
await update(to, ...); // lock to
|
|
}
|
|
async function transferB(from, to) {
|
|
await update(to, ...); // lock to
|
|
await update(from, ...); // lock from — deadlock 가능
|
|
}
|
|
|
|
// ✅ 항상 ID 순
|
|
async function transfer(from, to) {
|
|
const [first, second] = [from, to].sort();
|
|
await update(first, ...);
|
|
await update(second, ...);
|
|
}
|
|
```
|
|
|
|
### Long-running transaction
|
|
```sql
|
|
-- 30분+ transaction 검출
|
|
SELECT pid, usename, state, query, age(NOW(), xact_start) AS tx_age
|
|
FROM pg_stat_activity
|
|
WHERE state != 'idle' AND age(NOW(), xact_start) > INTERVAL '30 minutes';
|
|
```
|
|
|
|
```ts
|
|
// 강제 cancel
|
|
await db.query('SELECT pg_cancel_backend($1)', [pid]);
|
|
// 또는 더 강력
|
|
await db.query('SELECT pg_terminate_backend($1)', [pid]);
|
|
```
|
|
|
|
### Idle in transaction (큰 문제)
|
|
```
|
|
App 이 BEGIN → 외부 API call → 1분 hang → 다른 트랜잭션 차단
|
|
```
|
|
|
|
```sql
|
|
-- 검출
|
|
SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction';
|
|
|
|
-- 한도
|
|
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
|
|
SELECT pg_reload_conf();
|
|
```
|
|
|
|
→ 60초 후 자동 cancel.
|
|
|
|
### Migration safety
|
|
```sql
|
|
-- ❌ Big table 에 column 추가 (default = NULL OK, default value 면 lock)
|
|
ALTER TABLE orders ADD COLUMN x INT DEFAULT 0;
|
|
-- PG 11+ = fast (metadata only). 옛 PG = full rewrite.
|
|
|
|
-- ❌ NOT NULL 추가
|
|
ALTER TABLE orders ADD COLUMN x INT NOT NULL DEFAULT 0;
|
|
-- 점진적 migration
|
|
|
|
-- ✅ Safer pattern (아래 DB_Migration_Safety 참조)
|
|
```
|
|
|
|
### Index — concurrently
|
|
```sql
|
|
-- ❌ Lock 모든 write
|
|
CREATE INDEX orders_user ON orders (user_id);
|
|
|
|
-- ✅ Lock 없이 (느리지만 안전)
|
|
CREATE INDEX CONCURRENTLY orders_user ON orders (user_id);
|
|
```
|
|
|
|
### Advisory lock (app 레벨)
|
|
```sql
|
|
-- 단일 instance 가 작업
|
|
SELECT pg_advisory_lock(42); -- bigint key
|
|
-- 작업
|
|
SELECT pg_advisory_unlock(42);
|
|
|
|
-- Transaction-scoped (자동 release)
|
|
SELECT pg_advisory_xact_lock(hashtext('process-orders'));
|
|
COMMIT;
|
|
```
|
|
|
|
→ Cron / single-leader 패턴.
|
|
|
|
### Lock 통계
|
|
```sql
|
|
SELECT mode, count(*) FROM pg_locks GROUP BY mode;
|
|
-- 어떤 종류 lock 가 많은지
|
|
|
|
-- Wait 시간
|
|
SELECT
|
|
query, wait_event_type, wait_event, count(*)
|
|
FROM pg_stat_activity
|
|
WHERE wait_event IS NOT NULL
|
|
GROUP BY 1, 2, 3 ORDER BY count DESC;
|
|
```
|
|
|
|
### App 측 transaction 짧게
|
|
```ts
|
|
// ❌ 트랜잭션 안 외부 호출
|
|
await db.transaction(async (tx) => {
|
|
const order = await tx.orders.find(id);
|
|
const result = await fetch(externalApi); // 1초+
|
|
await tx.orders.update(...);
|
|
}); // 1초 lock
|
|
|
|
// ✅
|
|
const order = await db.orders.find(id);
|
|
const result = await fetch(externalApi);
|
|
await db.transaction(async (tx) => {
|
|
await tx.orders.update(...); // ms
|
|
});
|
|
```
|
|
|
|
### Optimistic vs Pessimistic
|
|
```sql
|
|
-- Pessimistic — lock
|
|
SELECT * FROM orders WHERE id = $1 FOR UPDATE;
|
|
-- 작업
|
|
|
|
-- Optimistic — version check
|
|
UPDATE orders SET ..., version = version + 1
|
|
WHERE id = $1 AND version = $expected_version;
|
|
-- 0 row affected = 다른 process 가 변경 — retry
|
|
```
|
|
|
|
→ Lock contention 적음 = optimistic.
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 추천 |
|
|
|---|---|
|
|
| 큰 query lock 차단 | lock_timeout |
|
|
| 일반 prod | statement_timeout 30s |
|
|
| Idle in transaction | idle_in_transaction_session_timeout |
|
|
| Cron leader | Advisory lock |
|
|
| 큰 table ALTER | concurrent migration patterns |
|
|
| Update conflict | Optimistic version |
|
|
|
|
## ❌ 안티패턴
|
|
- **Lock timeout 없음**: hang. 5-30s 항상.
|
|
- **Long transaction (30분+)**: autovacuum / 다른 query 차단.
|
|
- **Idle in transaction 무한**: timeout 설정.
|
|
- **Lock 순서 다름**: deadlock.
|
|
- **ALTER TABLE prod + 큰 table**: 모두 차단.
|
|
- **CREATE INDEX (non-concurrent) prod**: write 차단.
|
|
- **App 이 외부 API 후 commit**: tx 길어짐.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- statement_timeout + lock_timeout 항상.
|
|
- pg_blocking_pids 로 진단.
|
|
- 트랜잭션 안 외부 호출 금지.
|
|
|
|
## 🔗 관련 문서
|
|
- [[DB_Vacuum_Autovacuum]]
|
|
- [[DB_Distributed_Locks]]
|
|
- [[DB_Migration_Safety]]
|