PostgreSQL 18 비동기 I/O(AIO): 콜드 캐시 순차 스캔 3배 빠른 이유와 벤치마크
데이터베이스 성능 튜닝을 하다 보면 언제나 I/O 병목 앞에서 막히는 순간이 옵니다. shared_buffers 올리고, effective_io_concurrency 조정해보고, SSD로 교체도 해봤는데 여전히 대용량 테이블 풀 스캔이 느린 그 상황 말입니다. 솔직히 저도 그 벽 앞에서 "PostgreSQL이 원래 그런 거 아닌가"라고 체념했던 적이 있었거든요.
PostgreSQL 18(2025년 출시)은 그 벽을 꽤 근본적인 방식으로 허물었습니다. 비동기 I/O(Asynchronous I/O, AIO) 서브시스템을 공식 도입해, I/O가 진짜 병목인 환경에서 업그레이드만으로 눈에 띄는 성능 향상을 가능하게 했습니다. 이 글에서는 AIO가 내부적으로 어떻게 동작하는지, 실제 측정 결과는 어떻게 나오는지, 그리고 어떤 상황에서 의미 있는 효과를 기대할 수 있는지를 같이 살펴보겠습니다.
이 글의 실습 예시는 PostgreSQL을 직접 운영하거나 운영해본 적 있는 백엔드 개발자를 대상으로 합니다. postgresql.conf를 직접 수정하거나 ALTER SYSTEM을 사용할 수 있는 슈퍼유저 권한이 있다고 가정하고 설명합니다.
핵심 개념
기존 PostgreSQL이 I/O를 처리하던 방식
PG17까지 PostgreSQL은 디스크 I/O를 동기(synchronous) 방식으로 처리했습니다. 백엔드 프로세스가 블록을 읽어달라는 요청을 OS에 보내면, 데이터가 올 때까지 그 프로세스는 아무것도 안 하고 기다리는 구조입니다.
동기 I/O(Synchronous I/O): 읽기 요청을 보낸 프로세스가 응답이 올 때까지 블로킹 상태로 대기하는 방식. CPU 자원이 낭비되고, I/O 대역폭도 충분히 활용되지 않는다.
로컬 NVMe SSD처럼 레이턴시가 낮은 환경에서는 큰 문제가 아니지만, AWS EBS 같은 네트워크 블록 스토리지에서는 요청 하나당 수 밀리초씩 기다리는 시간이 쌓이면서 쿼리 실행 시간 대부분이 "그냥 기다리는 시간"이 됩니다. 순차 스캔처럼 수백만 개의 블록을 읽는 작업에서는 이게 심각한 문제가 되는 거죠.
여기서 자연스럽게 의문이 생깁니다. "그럼 여러 블록을 미리 요청해두면 되지 않나?" PG17에서 effective_io_concurrency와 prefetch 관련 설정이 바로 그 시도였는데, 이건 OS에 힌트를 주는 수준이었고 근본 구조는 동기 방식 그대로였습니다.
PG18 AIO의 세 가지 모드
PG18은 io_method 파라미터 하나로 I/O 처리 방식을 선택할 수 있게 됩니다.
| 모드 | 동작 방식 | 지원 환경 |
|---|---|---|
sync |
PG17과 동일한 동기 블로킹 (posix_fadvise 활용) | 전체 |
worker |
전용 백그라운드 I/O 워커 프로세스가 요청을 대신 처리 | 전체 (기본값) |
io_uring |
Linux 커널 io_uring 인터페이스로 시스템 콜 오버헤드 최소화 | Linux 5.1+ 전용 |
worker 모드가 기본값으로 설정됐다는 게 핵심입니다. PG18로 업그레이드하는 순간, 설정 건드리지 않아도 비동기 I/O가 활성화됩니다.
worker 모드가 내부적으로 어떻게 동작하는지도 짚어보면 좋겠습니다. 백엔드 프로세스가 직접 디스크를 읽는 대신, I/O 요청을 전용 백그라운드 워커 프로세스의 큐에 넣습니다. 워커가 실제 읽기를 처리하는 동안 백엔드는 다음 블록 요청을 계속 큐에 쌓을 수 있습니다. 결과가 준비되면 워커가 백엔드에 신호를 보내고, 백엔드는 그때 데이터를 가져가는 방식입니다. "한 번에 하나씩 기다리는" 구조에서 "여러 요청을 동시에 날리고 준비된 것부터 처리하는" 구조로 바뀌는 셈이죠.
io_uring은 한 단계 더 나아갑니다. Linux 커널 5.1에서 도입된 io_uring은 유저스페이스와 커널 사이에 공유 링 버퍼를 두어 시스템 콜 수 자체를 획기적으로 줄입니다. 워커 프로세스 간 컨텍스트 전환 오버헤드도 없앨 수 있어서 이론상으로는 더 효율적입니다. 다만 Linux 전용이라는 제약이 있어서, macOS나 Windows 환경, 그리고 일부 클라우드 관리형 서비스(RDS 등)에서는 선택할 수 없습니다.
io_uring: Linux 커널 5.1(2019)에서 도입된 고성능 비동기 I/O 인터페이스. 유저스페이스와 커널 간 공유 링 버퍼로 시스템 콜 수를 최소화한다.
liburing라이브러리로 접근 가능.
정리하면, 클라우드나 크로스 플랫폼 환경이라면 worker 모드로 먼저 시작하고, Linux 온프레미스나 EC2 직접 운영 환경이라면 io_uring을 테스트해보시면 좋습니다.
AIO가 적용되는 작업 범위
PG18 AIO가 효과를 내는 작업은 현재 세 가지입니다.
- 순차 스캔(Sequential Scan): 대용량 테이블 풀 스캔
- 비트맵 힙 스캔(Bitmap Heap Scan): 인덱스 조건 후 힙 블록 랜덤 접근
- VACUUM: 테이블 전체를 훑는 유지 관리 작업
반대로 쓰기(Write)와 WAL 작업은 PG18에서도 여전히 동기 방식입니다. 인덱스 스캔(Index Scan, Index-Only Scan)도 아직 AIO 적용 범위 밖이고요. 그래서 쓰기 중심 OLTP 워크로드에서는 효과가 제한적입니다. 이 부분은 향후 개선 과제로 커뮤니티에서 꾸준히 논의되고 있습니다.
주요 파라미터
-- PG17 → PG18 기본값 변화 확인
SHOW effective_io_concurrency; -- PG17: 1, PG18: 16
-- 현재 io_method 확인
SHOW io_method; -- 기본값: worker
-- 현재 진행 중인 비동기 I/O 요청 조회 (PG18 신규)
SELECT * FROM pg_aios;effective_io_concurrency가 1에서 16으로 올라간 것도 눈여겨볼 만합니다. 이전에는 "이 값을 높이면 prefetch가 된다"는 걸 알고 있어도 직접 손봐야 했는데, 이제 기본 동작 자체가 달라진 거거든요.
실전 적용
예시 1: 콜드 캐시 환경에서 대용량 테이블 스캔 측정
가장 극적인 효과를 볼 수 있는 시나리오입니다. OS 페이지 캐시가 비워진 상태에서 대용량 테이블에 순차 스캔을 실행하는 상황인데, 실무에서는 야간 배치가 처음 실행될 때나 DB 서버 재시작 직후가 이 상황과 유사합니다.
설정을 바꾸기 전에 알아두실 중요한 점이 하나 있습니다. io_method는 postmaster 컨텍스트 파라미터입니다. 세션 단위 SET이 아닌 postgresql.conf 수정 또는 ALTER SYSTEM 명령 후 PostgreSQL 재시작이 필요합니다. pg_reload_conf()만으로는 적용되지 않습니다.
-- 1단계: io_method 변경 (재시작 필요)
ALTER SYSTEM SET io_method = 'io_uring'; -- Linux 5.1+ 환경
-- ALTER SYSTEM SET io_method = 'worker'; -- 크로스 플랫폼 또는 RDS
-- PostgreSQL 재시작 후 적용 확인
-- (Linux 직접 운영 시: systemctl restart postgresql)
SHOW io_method;
-- 2단계: 동시 I/O 수 조정 (재시작 없이 reload 가능)
ALTER SYSTEM SET effective_io_concurrency = 200; -- NVMe SSD 기준
-- ALTER SYSTEM SET effective_io_concurrency = 64; -- 클라우드 EBS 기준
ALTER SYSTEM SET io_max_concurrency = 32;
SELECT pg_reload_conf();
-- 3단계: 콜드 캐시 상태 만들기
-- OS 캐시 클리어 (루트 권한 필요, 테스트 전용 환경에서만 사용):
-- sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
--
-- pg_prewarm 익스텐션 설치 여부 확인:
SELECT * FROM pg_extension WHERE extname = 'pg_prewarm';
-- 4단계: 실행 계획과 시간 확인
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT COUNT(*) FROM large_table;캐시 클리어 주의:
echo 3 > /proc/sys/vm/drop_caches는 OS의 페이지 캐시를 모두 비웁니다. 루트 권한이 필요하고, 운영 중인 시스템에서 실행하면 PostgreSQL뿐 아니라 다른 프로세스 성능에도 영향을 줍니다. 반드시 테스트 전용 환경에서만 사용하는 것이 좋습니다.
실측 결과를 정리하면 다음과 같습니다. 2억 행 규모 테이블, OS 캐시 클리어 후 SELECT COUNT(*) 기준입니다.
io_method |
상대 성능 | 특이사항 |
|---|---|---|
sync (PG17 기준) |
1× | 베이스라인 |
worker |
약 2~2.5× | 크로스 플랫폼, 기본값 |
io_uring |
약 2.5~3× | Linux 5.1+ 필요 |
예상과 달랐던 점이 하나 있었습니다. 고대역폭 순차 스캔 특정 케이스에서 worker가 io_uring과 비슷하거나 더 나은 결과를 보인 사례도 보고되어 있거든요. io_uring이 이론적으로 더 효율적이지만, 워크로드 특성에 따라 실제 결과가 달라질 수 있습니다. 두 모드를 직접 비교 측정해보는 게 중요한 이유입니다.
단, 캐시가 워밍된 상태(hot cache)에서는 세 모드 간 차이가 거의 없습니다. shared_buffers 히트율이 99% 이상인 환경이라면 이 설정을 바꿔도 체감 변화가 적습니다.
예시 2: AWS RDS에서 sync vs. worker 실측
클라우드 환경은 비동기 I/O의 효과가 더 두드러지는 편입니다. EBS 같은 네트워크 블록 스토리지는 요청당 레이턴시가 로컬 NVMe보다 높기 때문에, "기다리는 동안 다음 요청을 미리 보내는" AIO의 핵심 가치가 훨씬 잘 발휘되거든요.
Classmethod의 RDS PostgreSQL 18 실측에서는 worker 모드가 sync 대비 순차 스캔 처리량 약 26% 향상을 기록했습니다. 단일 수치로는 극적이지 않아 보일 수 있지만, 야간 보고서나 데이터 마이그레이션처럼 대용량 스캔을 반복하는 작업에서는 누적 효과가 큽니다.
-- RDS 환경에서 사용 가능한 모드 확인
-- (RDS는 io_uring 미지원, worker만 선택 가능)
SELECT name, setting, context
FROM pg_settings
WHERE name IN ('io_method', 'effective_io_concurrency', 'io_max_concurrency');
-- AIO 활동 모니터링
SELECT pid, mode, offset, nbytes, already_done
FROM pg_aios;RDS에서는 Parameter Group을 통해 io_method와 effective_io_concurrency를 설정할 수 있습니다. 파라미터 타입에 따라 즉시 적용 여부가 다르니, 콘솔에서 "Requires Restart" 항목을 먼저 확인해보시면 좋습니다.
예시 3: 야간 배치 분석 쿼리 최적화
야간 배치는 AIO가 가장 잘 맞는 케이스입니다. 이유가 몇 가지 있는데요. 배치 시작 시점은 대부분 콜드 캐시 상태이고, 여러 쿼리가 순차적으로 실행되며 각각 순차 스캔을 하기 때문에 비동기 prefetch 효과가 계속 발생합니다. 거기에 쓰기가 거의 없는 읽기 전용 패턴이라 AIO 미적용 영역인 쓰기 경로의 한계도 없는 셈입니다.
-- 배치 시작 전 현재 I/O 설정 확인
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN (
'io_method',
'effective_io_concurrency',
'io_max_concurrency'
);
-- 대용량 집계 쿼리 예시
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT
date_trunc('day', created_at) AS day,
COUNT(*) AS order_count,
SUM(total_amount) AS revenue
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY 1
ORDER BY 1;
-- 쿼리 실행 중 AIO 활동 확인 (별도 세션에서)
SELECT pid, mode, nbytes, already_done
FROM pg_aios;EXPLAIN (ANALYZE, BUFFERS) 출력에서 Buffers: shared read=X의 X 값은 AIO 적용 전후로 달라지지 않습니다. AIO는 "얼마나 읽었냐"가 아니라 "얼마나 빨리 읽었냐"를 바꾸는 기능이라서, 차이는 Execution Time에서 확인할 수 있습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 읽기 처리량 향상 | 콜드 캐시 순차 스캔에서 최대 2~3× 성능 향상 |
| Tail 레이턴시 개선 | P99 쿼리 시간 30~50% 감소 보고 (적절히 설정된 시스템 기준) |
| 무설정 업그레이드 효과 | worker 기본값으로 PG17 대비 즉시 성능 개선 |
| 클라우드 스토리지 친화적 | 네트워크 블록 스토리지에서 효과 극대화 |
| VACUUM 성능 향상 | 대용량 테이블 유지 관리 속도 개선으로 운영 부담 감소 |
Tail 레이턴시(P99 레이턴시): 전체 요청의 99번째 백분위수 응답 시간. 평균이 빠르더라도 느린 1%의 요청이 사용자 경험에 큰 영향을 줄 수 있어 별도로 측정한다.
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 쓰기 경로 미지원 | Write I/O, WAL은 여전히 동기 방식 | 쓰기 중심 OLTP라면 기대치 조정 |
| Hot cache 환경 무효 | shared_buffers 히트율 99%+ 환경은 효과 미미 |
히트율 먼저 확인 후 적용 검토 |
| io_uring Linux 전용 | 커널 5.1+ 필요, macOS·Windows 미지원 | 크로스 플랫폼 환경은 worker 사용 |
| 인덱스 스캔 미지원 | Index Scan, Index-Only Scan은 AIO 미적용 | 인덱스 활용 쿼리는 효과 없음 |
| io_method 변경 시 재시작 필요 | postmaster 컨텍스트 파라미터라 reload 불가 |
유지보수 창 확보 후 적용 |
| 연결 풀러 상호작용 | PgBouncer 등과 함께 쓸 때 초기 성능 저하 사례 있음 | 설정 변경 후 스테이징에서 검증 |
| 클라우드 서비스 제약 | RDS 등 일부 환경에서 io_uring 선택 불가 |
worker 모드로 대체 |
이 중에서 실무에서 가장 자주 당하는 함정은 첫 번째와 네 번째입니다. Hot cache 환경에서 기대치를 높게 잡았다가 실망하는 경우, 그리고 io_method를 바꾸려다 재시작이 필요하다는 걸 뒤늦게 알게 되는 경우가 꽤 흔하거든요.
실무에서 가장 흔한 실수
-
Hot cache 환경에서 기대치를 과도하게 잡는 것:
shared_buffers가 충분히 크고 동일한 쿼리가 반복 실행되는 OLTP 서비스라면 AIO 설정을 바꿔도 체감 변화가 거의 없습니다.pg_stat_user_tables에서heap_blks_hit/heap_blks_read비율을 먼저 확인해보시면 AIO가 효과를 낼 상황인지 가늠할 수 있습니다. -
io_uring을 무조건 최선으로 가정하는 것: 고대역폭 순차 스캔에서worker모드가io_uring과 비슷하거나 나은 결과를 보인 케이스도 있습니다. 워크로드 특성에 따라 두 모드를 직접 비교해보시는 게 좋습니다. -
effective_io_concurrency를 너무 높게 설정하는 것: 클라우드 EBS 기준 64~128, 로컬 NVMe 기준 200 정도가 실측 권장 범위입니다. 무한정 올린다고 비례해서 좋아지지 않으며,io_max_concurrency와 함께 워크로드 특성에 맞게 조정해보시면 좋습니다.
마치며
PostgreSQL 18의 비동기 I/O는 설정 없이 업그레이드만 해도 동작하는 드문 무료 성능 향상이며, 특히 클라우드 환경이나 야간 배치처럼 콜드 캐시 읽기가 많은 워크로드에서 즉각적인 효과를 기대할 수 있습니다.
지금 바로 시작해볼 수 있는 3단계:
-
캐시 히트율과 I/O 대기 시간 측정:
pg_stat_user_tables에서heap_blks_hit/heap_blks_read비율을 확인하고,pg_stat_activity에서wait_event_type = 'IO'비중을 파악해보시면 AIO가 효과를 낼 환경인지 판단할 수 있습니다. -
스테이징에서 모드별 비교 측정:
io_method를worker→io_uring순으로 변경(매번 재시작 필요)하면서 동일 쿼리의 실행 시간을 비교해볼 수 있습니다. OS 페이지 캐시를 클리어한 콜드 상태에서EXPLAIN (ANALYZE, BUFFERS)로 측정하고,pg_aios뷰로 비동기 I/O 요청이 실제로 발생하는지 확인해보시면 좋습니다. -
운영 환경 적용 및 모니터링: 검증이 끝나면
postgresql.conf에io_method,effective_io_concurrency,io_max_concurrency를 설정합니다. PgBouncer 같은 연결 풀러를 사용 중이라면 풀 크기와 설정 호환성도 함께 점검해보시면 좋습니다.
참고 자료
- PostgreSQL 18 Released! | PostgreSQL 공식
- Waiting for Postgres 18: Accelerating Disk Reads with Async I/O | pganalyze
- PostgreSQL 18: Better I/O performance with AIO | CYBERTEC
- PostgreSQL 18 and beyond: From AIO to Direct IO? | CYBERTEC
- PostgreSQL 18 Asynchronous I/O: A Complete Guide | Better Stack
- I tested PostgreSQL 18's async I/O performance on RDS | Classmethod DevelopersIO
- Async I/O in Postgres 18: 3× Faster Seq Scans with io_uring | Medium
- Exploring why PostgreSQL 18 put async I/O in your database | Aiven
- PostgreSQL 18 Async I/O in Production: Real-World Benchmarks | PostgresSQL HTX
- Benchmarking Postgres 17 vs 18 | PlanetScale
- Tuning PostgreSQL 18's AIO for Performance | ChatDBA
- PostgreSQL 18 Asynchronous I/O | Neon Documentation
- Get Excited About Postgres 18 | Crunchy Data Blog