PostgreSQL 18에서 autovacuum_max_workers가 드디어 SIGHUP로 받아지는 파라미터가 됐다. 정확히는 한 파라미터가 둘로 쪼개졌다 — 시작 시 한 번 예약하는 autovacuum_worker_slots(재시작 필요)와, 그 범위 안에서 자유롭게 움직이는 autovacuum_max_workers(reload만으로 적용). 운영자가 vacuum 부하를 보다 worker 수를 늘리려고 maintenance window를 잡아야 했던 시대가 끝난다.
이 글은 The Build의 Christophe Pettus가 “All Your GUCs in a Row” 시리즈에서 다룬 autovacuum_worker_slots 편을 한국어로 풀고, 함정 한두 가지를 더 짚는다.
왜 max_workers는 재시작 파라미터였나
PostgreSQL의 autovacuum launcher는 postmaster가 띄우는 백그라운드 프로세스다. launcher가 깨운 worker는 별도 프로세스로 동작하는데, 이 worker들이 자리잡을 shared memory 구조는 postmaster가 시작될 때 한 번에 예약된다. autovacuum_max_workers가 그 크기를 결정했고, 따라서 변경하려면 재시작이 필요했다.
이게 운영에서 만든 압박은 두 가지다.
첫째, 초기 과다 provisioning. “혹시 모르니 worker 16개 정도 확보해두자"는 식의 보수적 설정이 일반적이었다. 평소엔 절반도 안 쓰면서 shared memory를 점유한다.
둘째, 워크로드 변동 대응 실패. 새 큰 테이블 N개를 한꺼번에 적재하는 야간 배치를 추가하면 vacuum 부담이 갑자기 커진다. worker를 5개에서 10개로 늘리고 싶어도 그건 다음 maintenance window에서나 가능했다. 그동안 dead tuple은 쌓이고, bloat는 자라고, query latency는 흔들린다.
“The cost of being wrong is a SIGHUP, not an outage.” — Christophe Pettus, The Build
PostgreSQL 18은 이 비용을 SIGHUP 한 번으로 줄였다.
PG18의 분리 — worker_slots와 max_workers
원래 하나였던 GUC가 둘로 쪼개진다.
autovacuum_worker_slots— postmaster가 시작 시 예약할 shared memory worker slot의 개수. 재시작 파라미터.autovacuum_max_workers— 위 slot 범위 안에서 실제로 동시에 깨어 있을 수 있는 worker의 상한. SIGHUP로 받아짐.
쉽게 말하면 worker_slots는 주차장 크기, max_workers는 지금 동시에 받을 차의 수다. 주차장은 미리 지어둬야 하지만, 진입 제한은 그때그때 바꾸면 된다.
Before / After
| 파라미터 | PostgreSQL 17 이하 | PostgreSQL 18 |
|---|---|---|
| worker 상한 GUC 이름 | autovacuum_max_workers | autovacuum_max_workers (역할 변경) |
| 공유 메모리 예약 GUC | 없음 (= max_workers 가 결정) | autovacuum_worker_slots |
| 재시작 필요 | 예 | worker_slots 만 예 / max_workers는 SIGHUP |
| 기본값 | max_workers = 3 | worker_slots = 16, max_workers = 3 |
| 변경 비용 | maintenance window | reload |
기본값이 worker_slots = 16 으로 잡혀 있는 점에 주목할 만하다. “어차피 사후에 늘리지 못 하니 처음부터 넉넉히 잡아두자"는 의도된 권장이다.
구조도
flowchart TD
PM["postmaster 시작
(재시작 필요)"]
SLOTS["autovacuum_worker_slots
= 16 (shared memory 예약)"]
MAX["autovacuum_max_workers
= 5 (현재 상한)"]
L["autovacuum launcher"]
W1["worker 1"]
W2["worker 2"]
W3["worker ..."]
SIGHUP["pg_reload_conf()
= SIGHUP"]
PM --> SLOTS
SLOTS --> L
MAX --> L
SIGHUP -.->|max_workers 변경 즉시 반영| MAX
L --> W1
L --> W2
L --> W3
worker_slots는 부팅 때 한 번 결정되고, max_workers는 SIGHUP로 그때그때 위아래로 움직인다.
동작 메커니즘
세 가지를 짚어두면 충분하다.
- slot 예약은 postmaster 시작 시 한 번 —
worker_slots = 16이면 shared memory에 16개의 worker 자리가 잡힌다. 이 크기는 실행 중 못 바꾼다. max_workers > slots는 자동 capping —worker_slots = 16인데max_workers = 20으로 reload하면 PostgreSQL은 16으로 capping하고 서버 로그에 경고를 남긴다. 에러는 아니라 reload는 성공하지만, 의도대로 안 동작한다는 신호다.max_workers는 SIGHUP로 즉시 반영 —pg_reload_conf()한 번이면 다음 worker 사이클부터 새 상한이 적용된다.
운영 적용 — maintenance window 없는 튜닝 흐름
새 흐름은 단순하다.
- 설치 시점 —
worker_slots는 “최악의 상황에서 필요할 worker 수"로 넉넉히. 16이면 대부분 충분하다. shared memory 비용은 worker 1개당 수 KB 수준이라 부담이 작다. - 평상시 —
max_workers는 보수적으로 (예: 3~5). 평소 vacuum이 늦지 않으면 그대로 유지한다. - 부하 증가 감지 — 큰 테이블 적재, partition 증가, dead tuple 누적이 보이면
postgresql.conf에서max_workers를 올리고pg_reload_conf()실행. 다음 vacuum 사이클부터 worker가 늘어난다. - 부하 진정 — 다시
max_workers를 내리고 reload. shared memory는 그대로 두면 된다.
여기서 핵심은 3번이 SIGHUP 라는 점이다. 더 이상 vacuum 부하를 더 받겠다고 데이터베이스를 내리고 다시 띄우지 않는다.
함정과 주의사항
크게 셋이다.
첫째, worker_slots 의 hard ceiling. 설치 시점에 worker_slots를 짜게 잡으면 사후에 그 위로 못 간다. 16이 적당해 보여도, partition 1만 개에 야간 배치까지 도는 환경이라면 32로 잡아두는 게 낫다. shared memory 예약 비용이 vacuum 지연 비용보다 훨씬 싸다.
둘째, worker_slots를 거꾸로 줄이고 싶을 때. 재시작이 필요하다. “지금 max_workers = 5로 줄어들었으니 slot도 8 정도면 충분하지 않을까"는 자연스러운 생각이지만, 그 효과는 다음 재시작 때만 본다. 평소에는 그냥 두고, 다른 재시작 사유가 생겼을 때 함께 조정하는 게 실무적이다.
셋째, parallel autovacuum 과의 관계. PostgreSQL 18에서는 같은 시기에 autovacuum_max_parallel_workers 도 들어왔다. 한 vacuum이 인덱스 정리를 위해 추가로 끌어쓰는 worker는 별도 카운팅이라, 동시에 도는 worker 수를 셈할 때는 둘을 함께 봐야 한다. 자세한 건 같은 저자의 “Parallel Autovacuum: It’s Not About The CPU”에 잘 정리돼 있다.
넷째, max_workers 를 올릴 때의 메모리 곱셈. worker_slots 가 차지하는 shared memory는 slot 1개당 대략 5~20KB 수준이다 — PGPROC 슬롯, PgBackendStatus 엔트리, LWLock 같은 자투리 합산. worker_slots = 16 으로 잡아도 총 수백 KB 안쪽이라 셋업 부담은 거의 없다. 하지만 slot이 깨어나서 실제 vacuum을 도는 동안에는 별도 비용이 붙는다. autovacuum_work_mem(미설정 시 maintenance_work_mem, 기본 64MB)이 worker 프로세스의 private memory로 잡힌다. max_workers = 16 으로 올린 상태에서 16개가 동시에 도는 순간 OS RSS에 약 1GB가 추가된다는 뜻이다. 정리하면 — worker_slots는 넉넉히 키워도 무방하지만, max_workers는 RAM과 maintenance_work_mem의 곱셈을 같이 보고 결정한다. “주차장은 크게, 동시 진입은 천천히”.
maintenance window라는 비용
OLTP 클러스터를 운영해 봤다면 maintenance window의 무게를 안다. 사용자에게 공지를 띄우고, 야간 새벽 시간대를 잡고, 운영자 두세 명이 대기하는 비용. autovacuum_max_workers 하나 늘리겠다고 그 비용을 다 치르기는 어렵다. 그래서 한참을 견딘다 — dead tuple이 쌓이고, 큰 테이블이 bloat로 1.5배쯤 부풀고, query plan이 흔들리기 시작한 다음에야 다음 정기 점검에 끼워 넣는다. PostgreSQL 18의 분리가 의미하는 건 이 견디는 구간이 사라진다는 거다. dead tuple 알람이 뜨면 그날 안에 worker를 두세 개 더 풀어 vacuum을 따라잡게 한 뒤, 부하가 가라앉으면 다시 줄여둔다. 운영자 입장에서는 vacuum 튜닝이 처음으로 “회의 없이 가능한 일"이 됐다.
정리
| PG17 이하 | PG18 | |
|---|---|---|
| worker 수 변경 비용 | restart | SIGHUP |
| 초기 셋업 부담 | “충분히 크게” 1회 결정 | worker_slots만 크게, max_workers는 작게 |
| 워크로드 변동 대응 | 다음 maintenance window | 다음 reload |
| 운영 사고 빈도 | bloat 누적이 잦음 | 즉시 대응 가능 |
PostgreSQL 18은 “worker 풀은 사전에 크게, 사용량은 그때그때"라는 평범한 운영 패턴을 vacuum에도 들여왔다. 작은 변화 같지만 운영 자동화 관점에서는 큰 차이다 — 한밤중 dead tuple 누적 알림에 더 이상 maintenance window를 잡지 않아도 된다.
참고 자료
- All Your GUCs in a Row: autovacuum_worker_slots — The Build
- Parallel Autovacuum: It’s Not About The CPU — The Build
- Do not change autovacuum age settings — The Build (2019)
- PostgreSQL 18 Release Notes
- Routine Vacuuming — PostgreSQL Documentation
- PostgreSQL 18의 비동기 I/O — 이 블로그
- PostgreSQL 19의 wal_level — 동적 floor 값으로