지난주에 우리 팀 리포팅 DB — Postgres 18로 올린 지 두 달째다 — 에 async I/O를 본격적으로 켜보자고 결심했다. Postgres 18에서 들어온 AIO가 sequential scan, bitmap heap scan, vacuum 같은 read-heavy 작업에서 최대 2~3배까지 빨라진다는 얘기가 릴리스 노트부터 pganalyze 벤치까지 여기저기 도배돼 있었고, 우리 리포팅 워크로드는 딱 그 형태였다. NVMe 붙은 i4i 계열, 대형 seq scan이 하루에도 수백 번 도는 그런 곳.
결과부터 말하면 결국 io_method = worker로 되돌렸고, io_uring은 우리 환경에서 못 썼다. 왜 그렇게 됐는지 시간 순서대로 남겨둔다.
첫 번째 삽질: `io_method=io_uring`이 안 켜졌다
문서만 보면 간단해 보인다. postgresql.conf에 이렇게 넣고 재시작.
io_method = io_uring
io_workers = 3
effective_io_concurrency = 16
재시작. 그런데 로그에 이런 라인이 뜬다.
FATAL: io_method "io_uring" is not supported by this build
빌드 옵션에서 io_uring이 안 켜져 있던 거였다. 우리는 RDS가 아니고 EC2에 커뮤니티 패키지(pgdg)를 그대로 올려서 쓰는데, pgdg 배포판은 io_uring 지원을 기본적으로 컴파일해서 넣어주긴 한다. 문제는 우리가 쓰던 Amazon Linux 2 이미지가 커널 5.10대라, io_uring이 있긴 하지만 IORING_SETUP_SQPOLL이나 최근 유즈케이스에 필요한 몇몇 op가 없거나 불안정하다는 거였다.
Postgres 18의 io_uring 백엔드는 커널 5.15 이상을 사실상 요구하고, 안정적으로 쓰려면 6.x가 권장된다. 그런데 우리 RDS Custom도 아니고 그냥 셀프 매니지드라, 커널 올리려면 AMI를 통째로 바꿔야 했다. 그 순간 "아 이거 오늘 안에 못 끝내겠구나" 싶었다.
그래서 이단 io_method = worker로 우회했다. 이건 커널 요구사항이 없고, 별도 프로세스가 read I/O를 대신 발사해준다.
두 번째 삽질: worker 방식은 켰는데 별 차이가 없었다
io_method = worker, io_workers = 3으로 켜고, 평소 P99가 1.4초쯤 되던 리포팅 쿼리를 돌렸다. 결과는... 1.35초. 오차 범위 안이었다.
솔직히 좀 멘탈이 흔들렸다. 벤치들은 다 20~35% 개선 얘기하던데?
pg_stat_io 뷰를 꺼내봤다. 이게 Postgres 18에서 확장된 뷰인데, backend type별로 read/write, hit/miss가 다 나온다.
SELECT backend_type, object, context,
reads, read_time, hits
FROM pg_stat_io
WHERE reads > 0
ORDER BY reads DESC;
보니까 io worker backend의 reads 카운트가 이상하게 낮았다. 대부분 여전히 client backend에서 sync read가 나가고 있었다. 왜?
원인은 effective_io_concurrency였다. 이 값이 우리 세팅에는 1로 잡혀 있었다. Postgres 17까지는 이 파라미터가 posix_fadvise 기반의 read-ahead 힌트에만 영향을 줬는데, 18부터는 얘가 AIO subsystem이 얼마나 많은 read를 미리 발사할지 결정하는 핵심 노브가 됐다. 1이면 사실상 AIO의 이점이 없다.
effective_io_concurrency = 16으로 올리고 다시 돌렸다. 이제야 P99가 1.05초. 25% 개선. 벤치에서 얘기하던 수치가 나오기 시작했다.
세 번째 삽질: vacuum이 갑자기 느려졌다
여기서 끝났으면 좋았을 텐데, 다음날 아침에 autovacuum이 이상하게 오래 걸린다는 슬랙이 왔다. 평소 20분에 끝나던 큰 테이블 vacuum이 40분을 넘겼다.
pg_stat_progress_vacuum으로 상태 보면서 top을 같이 봤는데, io worker 프로세스 3개가 CPU 100% 근처에서 계속 붙어 있었다. 그리고 그 시점에 리포팅 워커도 동시에 돌고 있어서, io worker 3개를 두 워크로드가 나눠 쓰고 있는 형국이었다.
Postgres 18의 io worker는 인스턴스 전역 리소스다. NodeLocal cache같은 거 없고, 워크로드별로 격리 안 된다. 그러니까 vacuum이 read 폭탄을 쏘면 리포팅 쿼리의 read도 큐에서 대기해야 한다. 워커가 3개면 병목이 확실히 보인다.
io_workers를 8로 올리고, maintenance_io_concurrency도 명시적으로 32로 잡았다. 이제 vacuum이 다시 22분 안쪽으로 돌아왔다. 근데 이 값들이 진짜 최적인지는 아직 모르겠다. 워커를 너무 많이 띄우면 컨텍스트 스위칭 오버헤드가 커진다는 얘기도 있어서.
지금 우리 팀 세팅
# Postgres 18 AIO 관련
io_method = worker
io_workers = 8
effective_io_concurrency = 16
maintenance_io_concurrency = 32
# 참고로 이건 원래 있던 값
shared_buffers = 32GB
work_mem = 64MB
io_uring은 커널 6.1 AMI로 올린 다음에 스테이징에서 다시 시도할 예정이다. 벤치상 io_uring이 worker보다 15~20% 더 빠르다고 하니, 커널만 맞으면 갈 이유는 있다.
정리하며
정리하고 보니 결국 우리가 얻은 건 25% 정도의 P99 개선인데, 그거 얻으려고 반나절을 날렸다. 다음에 다른 팀이 Postgres 18 AIO 켤 때 참고할 만한 것들:
첫째, io_uring 쓸 거면 커널 버전부터 확인해라. 5.10대는 그냥 포기하자. 둘째, effective_io_concurrency를 반드시 같이 올려라. 이거 안 올리면 io_method만 바꿔봤자 아무것도 안 바뀐다. 셋째, vacuum과 쿼리가 io worker를 공유한다는 걸 잊지 마라. 특히 read-heavy 시간대에 autovacuum 창이 겹치면 병목이 눈에 띄다.
그리고 진짜로 시간이 없다면 그냥 Postgres 17에 머물러도 괜찮다. AIO는 훌륭한 기뉥이지만 튜닝 없이 켜다고 자동으로 빨라지진 않더라.
혹시 io_uring을 6.x 커널에서 돌려보신 분 있으면 워커랑 얼마나 차이 나는지 댓글로 좀 알려주세요. 특히 write는 여전히 sync라는 점을 감안했을 때 실제 워크로드에서 얼마나 체감되는지 궁금합니다.
'IT > DB 운영' 카테고리의 다른 글
| PostgreSQL VACUUM, 내부에서 무슨 일이 벌어지는가 - autovacuum 튜닝 전에 (0) | 2026.08.25 |
|---|---|
| PgBouncer transaction pool에서 prepared statement 쓰기, 이젠 진짜 된다 (0) | 2026.08.18 |
| PostgreSQL 논리 복제 슬롯, failover에서 살리는 법 (17 이후 실전 가이드) (0) | 2026.08.09 |
| PgBouncer vs pgcat vs Supavisor, 2026년에 뭘 쓸까 (0) | 2026.08.06 |
| Redis vs Valkey, 지금 갈아탈 때인가 (1) | 2026.08.02 |