※ PostgreSQL: io_combine_limit.
안녕하세요. 듀스트림입니다.
오늘 포스팅은 PostgreSQL 17버전에서 도입된 GUC 설정인 io_combine_limit에 관한 내용입니다.
1. io_combine_limit ?
- PostgreSQL 17에서 추가된 GUC 설정으로 preadv()를 활용한 병합 I/O 시 최대 바이트 수를 조절합니다.
- io_combine_limit는 러닝 타임에 변경할 수 있습니다.
- 이를 통해 여러 데이터 페이지를 한 번에 읽어 시스템 호출 횟수와 디스크 오버헤드를 줄여 성능을 향상시킬 수 있습니다.
- io_combin_limit의 기본값은 '128kB'입니다.
- io_combin_limit 값은 'io_max_combine_limit' 값을 초과할 수 없습니다.
io_max_combine_limit ?
→ 18 버전에서 도입된 GUC입니다.
→ 변경 시 서버 재시작이 필요하며 사용자가 수정할 수 없습니다.
→ io_combine_limit의 설정 값은 이 파라미터 값을 초과할 수 없습니다.
2. 벡터 I/O와 preadv() 개념
- preadv()는 POSIX API로 여러 버퍼를 한 번에 읽는 벡터화 I/O 방식입니다.
- iovec[] 배열을 통해 scatter-gather 방식으로 데이터를 동시에 처리하며, PostgreSQL 17에서는 이를 적극 활용해 연속 블록들을 묶어 읽도록 설계되었습니다.
3. 소스 코드 구현
3.1 preadv() 호출 위치
- read_stream.c의 핵심 함수인 StartReadBuffers() 내부에서 stream->pending_read_nblocks 값을 기준으로 실제 preadv() 호출이 이루어집니다. (이때 stream → io_combine_limit 값 이하로 묶인 블록만 읽도록 제어합니다.)
- 관련 소스 코드: src/backend/storage/aio/read_stream.c
- Doxygen 문서에서 관련 호출 흐름 확인 가능합니다.
- 함수 흐름
read_stream_begin_impl()
→ read_stream_look_ahead()
→ read_stream_start_pending_read()
→ StartReadBuffers() { preadv(...) 호출 } → 내부적으로 preadv() 사용 (벡터화된 병합 I/O)
→ read_stream_next_buffer()
→ read_stream_end()
이 로직은 preadv() 기반 병합 I/O를 제어하며 io_combine_limit은 해당 스트림의 초기 생성 시점에 stream → io_combine_limit으로 저장되어 이후에는 고정적으로 사용됩니다.
(GUC 변경에도 영향을 받지 않도록 설계되어 있습니다.)
PostgreSQL Source Code: src/backend/storage/aio/read_stream.c File Reference
Go to the source code of this file. static void * get_per_buffer_data (ReadStream *stream, int16 buffer_index) BlockNumber block_range_read_stream_cb (ReadStream *stream, void *callback_private_data, void *per_buffer_data) static BlockNumber read_
doxygen.postgresql.org
4. 테스트
4.1 테스트 환경
- 테이블: 약 10GB io_test (1,730만 행)
- 실행: SELECT count(*) FROM io_test; (병렬 스캔 2 workers)
- 환경: Rocky 8.10, PostgreSQL 17.4, SSD, Linux 파라미터는 기본값
- 측정 항목:
- EXPLAIN (ANALYZE, BUFFERS, TIMING)
- strace -e preadv
4.2 테스트
- 캐시 초기화 후 32kB~256kB 수행 반복
▸ 캐시 초기화
sudo sh -c "echo 3 > /proc/sys/vm/drop_caches"
▸ 파라미터 변경, 확인
-- 설정 변경
SET io_combine_limit = 'n';
-- 설정 확인
SHOW io_combine_limit;
▸ PID 확인
SELECT pg_backend_pid();
▸ 시스템 콜 추적 (preadv()만 필터링)
strace -e preadv -p <PID> -s 0 -f
▸ 실행 계획 SQL
EXPLAIN (ANALYZE, BUFFERS, TIMING) SELECT count(*) FROM io_test.io_test;
▸ 32kB


▸ 64kB


▸ 128kB(기본값)


▸ 256kB


▸ 요약 표
| 값 | 실행 시간 | Seq Scan 시간 | shared_read 블록 | preadv 병합 패턴 |
| 32kB | 5,150 ms | 3,992 ms | 632,989 | ~4page |
| 64kB | 4,048 ms | 2,891 ms | 632,954 | ~8page |
| 128kB (기본값) | 3,914 ms | 2,850 ms | 632,945 | ~16page |
| 256kB | 3,460 ms | 2,396 ms | 674,764 | ~32page |
병합 패턴 페이지 계산식: 추적 값 Byte ÷ 8192(1page = 8kB)
+ io_combine_limit 값이 높다고해서 반드시 성능이 잘 나오는 것은 아닙니다. 스토리지 상태 및 I/O 스케줄링 등에 따라 오히려 병목이 발생할 수 있습니다.
아래는 io_combine_limit = 256kB이고, 스토리지 환경이 달라졌을 때의 결과입니다.

최적화가 잘 된다면 Disk I/O 성능을 증가시켜 SeqScan 시 성능 향상을 기대할 수 있습니다.
하지만 높게 설정하다고 반드시 좋은 건 아닙니다. 반드시 해당 워크로드에서 테스트 후 적용이 필요합니다.
• shared_buffers가 높고 SSD 캐시 효율이 좋다면 낮추는 게 성능상 유리할 수도 있습니다.
• 랜덤 I/O 비율이 높은 워크로드(OLTP) 환경에서도 낮추는 게 성능상 유리할 수도 있습니다.
+ 관련된 OS 파라미터
| 파라미터명 | 설명 | 확인 방법 | 설정 방법 | 기본값 |
| fs.aio-max-nr | 시스템 전체에서 허용되는 AIO(비동기 IO) 요청 수 한도 | cat /proc/sys/fs/aio-max-nr | sysctl -w fs.aio-max-nr=1048576 /etc/sysctl.conf에 추가 | 65536 |
| max_iov_len (UIO_MAXIOV) | preadv() 호출당 가능한 최대 iovec 배열 수 (= 병합 가능한 최대 페이지 수) | 소스 코드 (/usr/include/linux/uio.h에서 UIO_MAXIOV) | 커널 컴파일 시 설정됨 (수정 불가) |
보통 1024 |
| /sys/block/*/queue/read_ahead_kb | 디스크 장치 read-ahead 크기 (미리 읽는 범위로 페이지 캐시 효율과 관련) |
cat /sys/block/sda/queue/read_ahead_kb | echo 8192 > /sys/block/sda/queue/read_ahead_kb | 4096 |
| /sys/block/*/queue/scheduler | 디스크 I/O 스케줄러 설정 (I/O 병합 정책에 영향) | cat /sys/block/sdX/queue/scheduler | echo mq-deadline > /sys/block/sdX/queue/scheduler | 버전에 따라 다름 |
| vm.dirty_ratio | 전체 메모리 중 dirty page가 차지할 수 있는 최대 비율 (백그라운드 write 타이밍에 영향) | sysctl vm.dirty_ratio | sysctl -w vm.dirty_ratio=10 /etc/sysctl.conf | 30 |
| vm.dirty_background_ratio | 백그라운드로 디스크에 쓰기를 시작하는 dirty page 비율 | sysctl vm.dirty_background_ratio | sysctl -w vm.dirty_background_ratio=5 /etc/sysctl.conf | 10 |
++ ulimit의 open files가 io_combine_limit 값과 간접적으로 왜 관련이 있는지에 대한 내용입니다.
| 항목 | 설명 |
| preadv()는 파일 디스크립터(fd)를 통해 호출됨 | PostgreSQL이 데이터 파일을 읽을 때 내부적으로 fd를 사용 |
| 병렬 워커가 많아지면 동시에 열린 fd도 증가 | 병렬 SeqScan, basebackup, wal sender 등에서 fd 다수 사용 |
| IO 병합이 커질수록 한 번에 열리는 relation segment 수 증가 가능성이 있음 | 1 relation = 다수의 1GB segment → 병합 시 여러 segment 접근 |
| shared_buffers 캐시 미스가 많을수록 open/close 횟수 많아짐 | 병합 효과 작을 때 IOPS 증가 = 더 많은 fd 활동 |
오늘은 여기까지~
'PostgreSQL' 카테고리의 다른 글
| PostgreSQL: pg_store_plans (4) | 2025.07.10 |
|---|---|
| PostgreSQL: Logical Replication (0) | 2025.07.02 |
| PostgreSQL: Partition (4) | 2025.06.26 |
| PostgreSQL: Lock (0) | 2025.06.24 |
| PostgreSQL: credcheck (0) | 2025.06.17 |