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 값으로