대용량 분석이나 검색 시스템에서 동시 쿼리가 늘자 p95와 p99가 급격히 악화되면, mmap을 io_uring으로 바꾸자는 제안이 빠르게 나옵니다. mmap은 파일을 가상 메모리에 연결하므로, 데이터 접근 중 페이지 폴트가 발생할 수 있기 때문입니다.
하지만 여기서 API의 세대만 비교하면 판단이 흔들립니다. io_uring이 mmap보다 새롭다는 사실은 운영 부하에서 더 빠르다는 증거가 아닙니다. 공개된 Rust 기반 쿼리 엔진의 조사에서도 mmap을 io_uring으로 교체한 뒤 성능이 느려졌다는 결과가 제시됐습니다. 이 사례가 모든 시스템에 mmap 유지를 권하는 것은 아닙니다. 다만 I/O API 교체 자체를 성능 개선책으로 간주하는 접근이 위험하다는 점은 분명히 보여줍니다.
CTO와 개발 리드는 먼저 다음 질문에 답해야 합니다. 현재 지연 시간은 파일을 읽는 방식 때문에 나빠졌는가, 아니면 여러 쿼리가 같은 메모리와 페이지 캐시를 소모하면서 서로를 밀어내기 때문에 나빠졌는가입니다.
장애는 mmap 호출이 아니라 캐시 축출의 순환에서 커질 수 있다
mmap은 파일 데이터를 애플리케이션 주소 공간에 매핑합니다. 애플리케이션은 별도 읽기 호출 결과를 버퍼로 받는 대신, 메모리를 읽는 방식으로 파일의 필요한 위치를 접근할 수 있습니다. Arrow IPC처럼 디스크 배치와 메모리 표현이 가까운 포맷에서는 이 방식이 특히 자연스럽습니다.
다만 가상 주소가 곧 물리 메모리 상주를 뜻하지는 않습니다. 애플리케이션이 아직 메모리에 올라오지 않은 파일 페이지를 건드리면 커널이 페이지 폴트를 처리합니다. 필요한 데이터가 페이지 캐시에 남아 있으면 minor fault로 처리될 수 있지만, 페이지가 축출됐다면 저장 장치에서 다시 읽는 major fault가 발생합니다.
문제는 이 현상이 동시성, 파일 범위, 메모리 사용량과 결합할 때입니다. 여러 쿼리가 각기 다른 대용량 파일과 컬럼을 넓게 읽고, 같은 호스트의 포드가 private memory를 크게 사용하면 파일 데이터를 위한 페이지 캐시 공간이 줄어듭니다. 그 뒤에는 다음과 같은 악순환이 생길 수 있습니다.
- 여러 쿼리가 서로 다른 파일 페이지를 캐시에 올립니다.
- 메모리가 부족해지면서 아직 필요한 페이지도 축출됩니다.
- 실행 중인 쿼리가 축출된 페이지를 다시 건드리고 major fault가 늘어납니다.
- 커널은 페이지를 다시 캐시에 넣는 동안 메모리 회수와 스케줄링 작업을 반복합니다.
- 워커는 분석 코드보다 폴트 처리, 대기, 재스케줄링에 더 많은 시간을 씁니다.
이 상태에서는 NVMe 대역폭이 충분해도 지연 시간이 안정되지 않을 수 있습니다. 공개된 조사에서는 스트레스 구간에서 실제 디스크 I/O 대기보다 futex 대기와 선점, 페이지 캐시에 페이지를 추가하는 커널 경로가 더 큰 비중을 보였습니다. 스토리지를 늘렸는데도 꼬리 지연이 유지되거나, 포드를 추가했는데 총 처리량이 떨어진다면 디스크만 교체하기 전에 이 경로를 확인해야 합니다.
조기 신호는 평균 지연 시간이 아니라 부하 곡선에 나타난다
mmap이 문제인지 보려면 단일 쿼리의 평균 실행 시간만으로는 부족합니다. 캐시가 충분한 상태에서는 mmap 경로가 잘 작동할 수 있고, 장애는 대개 메모리 한계 근처에서 나타납니다.
다음 신호가 한 시점에 함께 나타나는지 살펴볼 만합니다.
- 동시 쿼리를 늘릴수록 p95와 p99가 예상보다 가파르게 증가한다.
- 같은 CPU 자원에서 스캔한 행 수나 처리 바이트가 눈에 띄게 감소한다.
- RSS가 물리 메모리 한계에 가까워진 직후 감소하고, major fault가 뒤따라 증가한다.
- warm-cache 구간에서는 애플리케이션 계산 코드가 CPU 시간을 쓰지만, 압박 구간에서는 파일 페이지를 캐시에 넣는 커널 경로가 두드러진다.
- 컨텍스트 스위치와 futex 대기가 크게 늘며, 워커를 추가해도 처리량이 회복되지 않는다.
- 같은 호스트에서 포드 수를 늘렸을 때 격리 효과 대신 쿼리 지연이 악화된다.
공개된 조사에서는 14일 범위 쿼리에서 하나의 포드가 네 개 포드보다 최대 41% 빨랐고, p95에서도 20% 이상 앞섰습니다. 이 결과를 그대로 다른 환경에 적용할 수는 없습니다. 다만 포드 증설이 항상 병렬성 향상으로 이어지지 않으며, 호스트 단위 페이지 캐시를 여러 프로세스가 경쟁할 수 있다는 점은 운영 설계에 반영할 필요가 있습니다.
io_uring 전환은 해답이 아니라 별도 가설이다
io_uring은 비동기 I/O 요청의 제출과 완료 처리를 다루는 Linux 인터페이스입니다. 그러나 io_uring을 도입한다고 페이지 캐시 압박, 메모리 부족, 쿼리 과다 동시성, 비효율적인 파일 접근이 자동으로 사라지지는 않습니다.
특히 다음 두 문장을 같은 뜻으로 다루면 안 됩니다.
- mmap의 페이지 폴트가 병목이다.
- 따라서 io_uring이 더 빠르다.
첫 문장은 측정으로 검증할 수 있습니다. major fault 증가, 커널 CPU 비중, 캐시 축출, 대기 시간 변화가 근거가 됩니다. 반면 둘째 문장은 별도 구현과 별도 부하 시험이 필요한 가설입니다. 파일 포맷이 mmap 기반 zero-copy 접근에 맞춰져 있다면, 전환 후 데이터 전달 방식과 메모리 사용 형태가 바뀌면서 예상하지 못한 비용이 생길 수도 있습니다. 공개 사례의 “교체 후 저하”는 바로 이 간극을 경계하게 합니다.
따라서 “io_uring으로 가면 페이지 캐시 경쟁을 해결할 수 있는가”보다 “현재 병목을 재현한 조건에서 새 경로가 p99와 자원 경쟁을 줄이는가”를 물어야 합니다. API 이름이 아니라 장애 조건을 기준으로 비교해야 합니다.
유지, 전환, 혼합 운영을 고르는 순서
먼저 mmap을 전면 교체하지 말고, 현재 장애를 만드는 쿼리 집합을 분리하십시오. 짧은 기간을 반복 조회하는 요청, 넓은 기간을 스캔하는 요청, 백필·재처리 작업, 테넌트별 대량 조회가 같은 자원을 두고 경쟁하고 있는지 확인하는 단계입니다.
그 다음에는 운영과 가까운 조건에서 mmap 경로를 측정합니다. 단일 쿼리와 목표 동시성, 캐시가 충분한 상태와 메모리 압박 상태, 단일 프로세스와 실제 포드 배치 수를 나눠 비교해야 합니다. 이때 평균 시간만 보지 말고 p95·p99, major·minor fault, RSS 변화, 컨텍스트 스위치, 커널과 애플리케이션의 CPU 비중을 함께 기록하는 편이 낫습니다.
이 결과에 따라 판단이 달라집니다.
- 메모리 압박 없이도 지연이 높다면 mmap 교체보다 쿼리 계획, 파일 구조, CPU 병목을 먼저 조사해야 합니다.
- 메모리 한계와 major fault 급증이 꼬리 지연과 함께 반복된다면, 포드 수·동시 쿼리 수·private memory 사용량을 조정하는 방안부터 검토할 수 있습니다.
- 특정 작업 유형만 캐시를 대량으로 축출한다면, 그 작업을 운영 조회와 분리하거나 별도 I/O 경로를 시험할 후보로 삼을 수 있습니다.
- io_uring 구현이 필요하다고 판단되면, 기존 mmap 경로와 같은 데이터·같은 동시성·같은 메모리 조건에서 비교해야 합니다. 빠른 단일 실행 결과만으로 전환을 승인해서는 안 됩니다.
혼합 운영도 가능성일 뿐 기본 정답은 아닙니다. 워크로드별로 다른 경로를 둔다면, 어떤 요청을 어느 경로로 보낼지와 두 경로가 같은 호스트 자원을 어떻게 공유하는지까지 운영 규칙으로 정해야 합니다. 그렇지 않으면 I/O 모델을 둘로 늘렸지만 캐시 경쟁은 그대로 남는 상황이 생길 수 있습니다.
I/O 전환 논의의 출발점은 “무엇이 더 현대적인가”가 아닙니다. 다음 장애 구간에서 어떤 쿼리가 페이지 캐시를 채우고, 어떤 작업이 그 페이지를 밀어내며, 그때 워커가 어디에서 멈추는지부터 확인해야 합니다. 그 측정이 있어야 mmap 유지와 io_uring 실험, 워크로드 분리 중 하나를 기술 취향이 아닌 운영 근거로 선택할 수 있습니다.