본문으로 건너뛰기

"autovacuum" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

모든 태그 보기

PG19 Beta 핵심 변화 4가지

· 약 11분

Big 4를 한눈에

Christophe Pettus가 The Build에서 정리한 PostgreSQL 19 Beta Big 4에는 공통점이 있어요. 넷 다 피처 노트로 끝나지 않고 운영 시나리오를 바꾼다는 점이에요. 신규 syntax 한 줄에 그치지 않고, DBA의 메모리 계산식, 장애 시나리오, 튜닝 기본값을 다시 그리게 만들어요.

업데이트 (2026-06-04): PostgreSQL 19 Beta 1이 공식 릴리스됐습니다. 이 글이 예고한 Big 4는 모두 Beta 1에 그대로 담겼습니다: 64bit MultiXact members(Make multixid members 64-bit), 병렬 autovacuum worker(autovacuum_max_parallel_workers), UPDATE/DELETE FOR PORTION OF, JIT 기본 비활성화입니다. 정식 출시는 9월~10월 예정이며, 그사이 release candidate가 한 차례 이상 나옵니다.

PostgreSQL 19는 9월 정식 출시 예정입니다. 이번 글은 Pettus의 Big 4 큐레이션을 출발점으로 삼아, 거기에 DBA 점검 항목, 메모리 계산식, SQL 예시, 업그레이드 체크리스트를 보탠 글입니다. 네 가지 선정은 Pettus의 시각이고, 운영 점검, 메커니즘 해설, 업그레이드 절차는 기존 PG19 시리즈 5편이 다루지 않은 부분을 채우는 보완/확장입니다.

왜 이 Big 4인가

PostgreSQL 19의 변경 목록은 CommitFest 5회분의 수백 건입니다. 그중 DBA가 실제로 체감하는 변화는 보통 둘 중 하나입니다.

  • 장애 회피: 지금까지 운영 매뉴얼에 "이거 터지면 끝"으로 적혀 있던 시나리오가 사라집니다.
  • 튜닝 기본값 변경: 업그레이드 직후 워크로드 성격에 따라 plan 시간, 메모리, 비상 상황이 달라집니다.

Pettus가 꼽은 Big 4는 정확히 그 두 축에 들어갑니다. 새 syntax나 새 알고리즘이라기보다, 기존 운영 매뉴얼을 손보게 만드는 변경들입니다.

변경영향
64bit MultiXact members장애 회피"긴급 vacuum"이라는 운영 시나리오가 사라집니다
병렬 autovacuum 인덱스 worker튜닝 기본값 변경최악 메모리 사용량 식이 달라집니다
UPDATE/DELETE FOR PORTION OF신규 운영 영역row trigger/cascading FK 동작에 새 사각지대
jit = off (기본)튜닝 기본값 변경OLAP는 명시 활성화하지 않으면 회귀합니다

1. 64bit MultiXact members가 없앤 긴급 vacuum 시나리오

무엇이 바뀌었나

MultiXact 멤버 카운터가 32bit에서 64bit로 확장됐습니다. 4 billion(약 40억) 멤버 공간 고갈 시나리오가 사실상 사라집니다.

DBA가 왜 체감하나

PostgreSQL에는 DBA 사이에서 "이게 터지면 끝"으로 통하는 시나리오가 몇 개 있습니다. MultiXact wraparound는 거기 한참 머물러 있었습니다.

SELECT ... FOR SHARE, foreign-key check 같은 공유 row lock이 같은 행에 여러 트랜잭션에서 동시에 걸리면, PostgreSQL은 그 잠금 정보를 묶어서 MultiXact라는 별도 구조에 기록합니다. 각 MultiXact는 멤버 슬롯을 소비합니다. 그 멤버 카운터의 자료형이 PostgreSQL 19에서 처음으로 확장됐습니다.

구분타입최대값
기존 (PG 18 이하)uint32 (32비트)약 42억 9천만
변경 (PG 19~)uint64 (64비트)약 1844경

42억은 운영 부하가 큰 워크로드(공유 lock + FK check 누적)에서 실제로 도달할 수 있는 수치입니다. 1844경은 사실상 도달이 불가능한 영역으로 넘어갑니다.

주의: XID/mxid wraparound는 그대로다

DBA가 흔히 "wraparound"라고 묶어 부르는 경우는 사실 셋입니다. 이번 변경은 그중 하나에 한정됩니다.

wraparound 종류PG 19에서 변경운영 영향
MultiXact members 카운터uint32uint64사실상 해결
MultiXact ID (mxid)변경 없음 (32bit)autovacuum 의존, anti-wraparound vacuum 그대로
Transaction ID (xid)변경 없음 (32bit)autovacuum의 freeze 책임 그대로

PostgreSQL의 오래된 숙제인 XID wraparound와 mxid wraparound는 PostgreSQL 19에서도 살아 있습니다. autovacuum이 주기적으로 freeze를 수행해야 하는 이유가 정확히 이것입니다. 이번 글이 다루는 "해결된 wraparound"는 MultiXact members 카운터 하나뿐임을 분명히 해 둡니다.

64bit XID 확장은 2018년부터 PostgreSQL hackers 메일링 리스트에서 논의돼 왔습니다. Postgres Pro가 PostgreSQL 15 시기에 실험적 패치 시리즈로 동작 확인까지 갔지만, tuple 헤더 크기 증가, 모든 index format 재설계, WAL format 변경 같은 광범위한 ripple effect 때문에 main 브랜치 commit으로 이어지지 않았습니다. PostgreSQL 19에서도 같은 상태입니다. PostgreSQL의 가장 오래된 숙제 중 하나가 한 라운드 더 미뤄졌고, 그 사이 DBA는 autovacuum freeze 튜닝과 pg_visibility/pg_class.relfrozenxid 모니터링을 손에서 놓을 수 없습니다.

운영 영향은 이렇게 정리됩니다.

항목PG 18까지PG 19 Beta
MultiXact members 고갈 시새 트랜잭션 거부 + offline emergency VACUUM사실상 도달 불가
XID/mxid wraparoundautovacuum freeze로 방어그대로이며 모니터링을 유지합니다
운영 매뉴얼 항목MultiXact members 항목은 "이거 터지면 끝" 목록의 한 줄members 항목만 삭제 후보. XID/mxid는 유지

42억 한계에 도달하면 DBA가 마주하는 그림은 단순했습니다. 새 트랜잭션이 거부되고, 복구 경로는 offline emergency VACUUM 하나. Pettus의 한 줄이 이를 짚습니다.

"When exhausted, the system would refuse new transactions, and the only recovery path was an emergency VACUUM."

이론적으로 wraparound 수학은 2^64에서도 여전히 존재합니다. 다만 같은 워크로드를 우주의 나이만큼 굴려도 도달이 어렵습니다. 32bit 공간 도달 사례는 운영 사고 보고서들에 종종 등장했지만, 64bit는 실무 카탈로그에서 제외할 수 있습니다.

DBA가 할 일

  • 운영 매뉴얼/런북에서 "MultiXact wraparound" 비상 절차 항목을 "PG 19부터는 도달 불가"로 갱신
  • 단, PostgreSQL 18 이하 운영 중인 클러스터는 그대로 이 시나리오가 살아 있으니 모니터링 대시보드를 유지

Beta 1에는 이 부분을 보강하는 변경도 함께 들어왔습니다. multixact 활동을 보여 주는 pg_get_multixact_stats() 함수가 추가됐고, xid/multixact wraparound 경고 임계값이 기존 4천만에서 1억 건 남았을 때로 올라갔습니다(클라이언트와 서버 로그 양쪽에 경고). 여전히 살아 있는 xid/mxid wraparound를 그만큼 더 일찍 알아챌 수 있습니다.

2. 병렬 autovacuum 인덱스 worker와 메모리 계산식

무엇이 바뀌었나

새 GUC autovacuum_max_parallel_workers가 추가됩니다. autovacuum이 단일 테이블의 인덱스 정리를 여러 worker로 병렬 처리합니다. PostgreSQL 17에서 도입된 manual VACUUM의 병렬 인덱스 정리가 autovacuum 경로까지 확장됐습니다.

DBA가 왜 체감하나

인덱스가 많이 붙은 wide table을 운영해 본 사람은 압니다. autovacuum이 돌면 heap 정리는 빠른데 인덱스 정리 단계에서 한참 멈춰 있습니다. 단일 worker가 인덱스 N개를 순차로 도는 구조였기 때문입니다.

PostgreSQL 19에서는 같은 단계에 worker가 동시에 들어갑니다. wide-and-many-indexes 테이블의 autovacuum 시간이 줄어들고, vacuum 누적 부담이 풀립니다.

-- 새 GUC (값은 예시)
ALTER SYSTEM SET autovacuum_max_parallel_workers = 4;
SELECT pg_reload_conf();

주의: 최악 메모리 식이 달라진다

같은 변경이 운영 부담을 새로 만드는 지점도 있습니다. 각 병렬 worker는 자기 몫의 maintenance_work_mem을 따로 잡습니다.

항목PG 18까지PG 19 Beta
autovacuum 최악 메모리autovacuum_max_workers × maintenance_work_memautovacuum_max_workers × autovacuum_max_parallel_workers × maintenance_work_mem

autovacuum_max_workers=3, autovacuum_max_parallel_workers=4, maintenance_work_mem=1GB인 환경에서 최악 메모리 사용량은 3 × 4 × 1GB = 12GB가 됩니다. PostgreSQL 18까지의 3GB와 4배 차입니다.

DBA는 PostgreSQL 19 업그레이드 전에 세 가지를 확인합니다.

  1. maintenance_work_mem 현재 값
  2. autovacuum_max_workers 현재 값
  3. 위 두 곱에 새 GUC 곱을 더한 값이 시스템 메모리에 맞는가

maintenance_work_mem을 1GB 이상으로 키워 놓은 운영 환경(분석 워크로드/큰 인덱스 재정렬 빈도가 잦은 곳)은 특히 다시 계산해야 합니다.

"명백한 이득"이 보이는 경우는 한정적입니다. serial 인덱스 정리가 vacuum의 병목이던 wide table에서 가장 큰 폭으로 줄어듭니다.

DBA가 할 일

  • 업그레이드 전 메모리 식 재계산. RAM이 빠듯하면 autovacuum_max_parallel_workers를 기본보다 낮춰 시작
  • wide table/인덱스 다수 테이블에서 autovacuum 지속 시간 모니터링 (pg_stat_progress_vacuum)
  • 한꺼번에 너무 많은 worker가 깨어나 I/O 포화되지 않도록 autovacuum_vacuum_cost_limit 검토

3. UPDATE ... FOR PORTION OF, SQL:2011 시간 범위가 PostgreSQL로

무엇이 바뀌었나

시간 범위(period) 컬럼을 가진 테이블에서 부분 범위만 UPDATE 또는 DELETE할 수 있게 됐습니다. SQL:2011 표준 syntax입니다.

-- 가격 이력 테이블 (period 컬럼 사용 가정)
CREATE TABLE price_history (
product_id int,
price numeric,
valid_period daterange,
PERIOD FOR valid_period (valid_from, valid_to)
);

-- 2026-06-01 ~ 2026-06-15 구간만 가격 변경
UPDATE price_history
FOR PORTION OF valid_period
FROM DATE '2026-06-01' TO DATE '2026-06-15'
SET price = price * 0.9
WHERE product_id = 42;

이전에는 DBA가 직접 row를 자르고 새 row를 끼워 넣는 SQL을 손으로 썼습니다. PostgreSQL 19부터는 한 문장으로 끝납니다. 건드리지 않은 범위의 row는 자동으로 보존됩니다.

DBA가 왜 체감하나

가격 이력, 직원 직책 이력, 권한 이력, 보험료 이력처럼 시간 범위 컬럼을 두는 모델은 흔합니다. 매번 손으로 자르는 SQL을 짜는 대신 syntax 한 줄로 정리되는 경우가 늘어납니다.

다만 새 운영 사각지대도 함께 생깁니다.

주의: row trigger/cascading FK

Pettus가 직접 짚습니다.

"a FOR PORTION OF update can fire row triggers on rows that did not exist when the statement started."

(FOR PORTION OF UPDATE는 문이 시작될 때 존재하지 않던 row에 대해서도 row trigger를 발화시킬 수 있다.)

내부적으로 FOR PORTION OF는 원본 row를 잘라 새 row 두 개(또는 세 개)로 분리하고, 그중 하나에 UPDATE를 적용합니다. 그 결과 DELETE 트리거가 원본 row에, INSERT 트리거가 새 보존 row 둘에, UPDATE 트리거가 갱신된 부분 row에 각각 발화합니다.

cascading foreign-key 동작은 한 단계 더 복잡해집니다. parent 행의 시간 범위가 잘리면 child의 FK 동작이 어떻게 따라가는지 production에서 테스트하지 않고 올리면 사고 가능성이 큽니다. parent의 부분 UPDATE가 child를 DELETE해야 하는가, CASCADE해야 하는가, RESTRICT해야 하는가. 이런 운영 정책 결정이 SQL 한 줄 안에 묻혀 들어갑니다.

DBA가 할 일

  • FOR PORTION OF를 production에 올리기 전에 row trigger, FK, audit trigger 동작을 staging에서 전부 확인
  • 트리거가 row 단위로 카운트를 세는 코드(예: audit row 개수 검증)는 PostgreSQL 19에서 결과가 다를 수 있으니 재검토

4. jit = off 기본값, PostgreSQL 12 이후 처음의 방향 전환

무엇이 바뀌었나

Just-in-time compilation 기본값이 off로 바뀝니다. PostgreSQL 12에서 jit = on이 기본값으로 들어온 이후 처음 있는 방향 전환입니다.

DBA가 왜 체감하나

이 변경은 워크로드 성격에 따라 정반대 결과를 만듭니다.

워크로드PG 18까지PG 19 Beta
OLTP (짧은 쿼리 다수)JIT plan 오버헤드가 매 쿼리마다 누적plan 시간 단축, 응답성 개선
OLAP (긴 분석 쿼리)JIT 컴파일 후 실행 가속명시적으로 jit = on 안 켜면 회귀

Pettus는 OLAP 회귀의 크기를 한 줄로 묘사합니다.

"a six-minute report now takes nineteen."

(6분짜리 리포트가 19분 걸리게 된다.)

3배 회귀 사례를 예고하는 대목입니다. OLAP 클러스터를 PostgreSQL 19로 올리면서 jit = on을 명시 설정하지 않으면, 출시 직후 가장 먼저 받는 분기 보고서가 평소보다 한참 늦게 도착합니다.

DBA가 할 일

OLTP 클러스터는 그대로 두면 자연스럽게 plan 효율 개선합니다. 별도 설정 불필요. OLAP/분석 클러스터는 업그레이드 직후 ALTER SYSTEM SET jit = on; 실행합니다. 또는 분석 사용자의 ALTER ROLE ... SET jit = on;으로 user-level 지정. 혼합 워크로드는 pgBouncer 등의 connection pool에서 분석 트래픽 분리한 뒤 그쪽만 jit = on 적용

왜 방향이 바뀌었는가

PostgreSQL 12에서 JIT를 기본 활성화한 동기는 "분석 쿼리에서 측정 가능한 가속이 있다"였습니다. 하지만 운영 통계가 쌓이면서, OLTP가 절대 다수인 PostgreSQL 운영 현실에서 plan 시간 오버헤드 손실이 컴파일 가속 이득을 자주 초과한다는 점이 드러났습니다. 6년 만에 측정에 근거해 기본값을 되돌린 것입니다.

PostgreSQL 19 업그레이드 체크리스트 (이번 Big 4)

업그레이드 전 다음을 점검합니다.

점검 항목명령 / 확인
maintenance_work_mem 현재 값SHOW maintenance_work_mem;
autovacuum_max_workers 현재 값SHOW autovacuum_max_workers;
최악 메모리 사용량 재계산autovacuum_max_workers × autovacuum_max_parallel_workers × maintenance_work_mem
OLAP/혼합 워크로드 식별분석 ETL/BI 도구 사용 여부
시간 범위 컬럼 사용 테이블 식별\d+ 로 PERIOD 컬럼 점검
MultiXact 모니터링 운영 매뉴얼"PG 19부터 도달 불가" 주석

업그레이드 직후에는 다음을 즉시 확인합니다.

-- 1. autovacuum 동작 확인
SELECT * FROM pg_stat_progress_vacuum;

-- 2. JIT 설정 확인 (OLAP면 명시 on)
SHOW jit;

-- 3. 새 GUC 확인
SHOW autovacuum_max_parallel_workers;

정리

앞의 표에서 정리했듯 64bit MultiXact members는 최악 wraparound 시나리오를 제거하지만 PG 18 이하 클러스터 모니터링은 유지해야 하고, 병렬 autovacuum 인덱스 worker는 wide table autovacuum 시간을 단축하지만 최악 메모리 사용량 식이 달라집니다. UPDATE/DELETE FOR PORTION OF는 시간 범위 SQL을 한 줄로 정리하는 대신 row trigger/cascading FK 동작을 재검증해야 하고, 기본 jit = off는 OLTP plan 시간을 단축하지만 OLAP는 명시 활성화 안 하면 회귀합니다.

PostgreSQL 19는 화려한 신기능보다 운영 매뉴얼을 다시 쓰게 만드는 쪽에 가까운 릴리스예요. DBA 입장에서 이번 Big 4는 9월 정식 출시 전에 staging에서 한 번 돌려 보고, 메모리 식, 튜닝 기본값, 운영 매뉴얼 세 가지를 같이 갱신해야 해요.

기존 PostgreSQL 19 새 기능 총정리PostgreSQL 19 시리즈 4편이 "어떤 기능이 들어오는가"를 봤다면, 이번 글은 "어떤 운영이 달라지는가"를 봐요. 두 글을 같이 두면 PostgreSQL 19를 올리기 전 점검할 항목이 한곳에 모여요.

참고

PG18 autovacuum 슬롯

· 약 6분

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_workersautovacuum_max_workers (역할 변경)
공유 메모리 예약 GUC없음 (= max_workers가 결정)autovacuum_worker_slots
재시작 필요worker_slots 만 예 / max_workers는 SIGHUP
기본값max_workers = 3worker_slots = 16, max_workers = 3
변경 비용maintenance windowreload

기본값이 worker_slots = 16으로 잡혀 있는 점에 주목할 만합니다. "어차피 사후에 늘리지 못 하니 처음부터 넉넉히 잡아두자"는 의도된 권장입니다.

구조도

worker_slots는 부팅 때 한 번 결정되고, max_workers는 SIGHUP로 그때그때 위아래로 움직입니다.

동작 메커니즘

세 가지를 짚어두면 충분합니다.

  1. slot 예약은 postmaster 시작 시 한 번: worker_slots = 16이면 shared memory에 16개의 worker 자리가 잡힙니다. 이 크기는 실행 중 바꿀 수 없습니다.
  2. max_workers > slots는 자동 capping: worker_slots = 16인데 max_workers = 20으로 reload하면 PostgreSQL은 16으로 capping하고 서버 로그에 경고를 남깁니다. 에러가 아니어서 reload는 성공하지만, 의도대로 동작하지 않는다는 신호입니다.
  3. 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 수 변경 비용restartSIGHUP
초기 셋업 부담"충분히 크게" 1회 결정worker_slots만 크게, max_workers는 작게
워크로드 변동 대응다음 maintenance window다음 reload
운영 사고 빈도bloat 누적이 잦음즉시 대응 가능

PostgreSQL 18은 "worker 풀은 사전에 크게, 사용량은 그때그때"라는 평범한 운영 패턴을 vacuum에도 들여왔어요. 작은 변화 같지만 운영 자동화 관점에서는 큰 차이예요. 한밤중 dead tuple 누적 알림에 더 이상 maintenance window를 잡지 않아도 돼요.

참고 자료