TL;DR

  • Jens Axboe가 차단 가능성이 있는 io_uring 작업을 제출 스레드에서 먼저 처리하고 실제 차단이 발생할 때만 스레드 정체성을 교환하는 RFC 패치 세트를 제안함.
  • 스케줄러에 PF_IO_HANDOFF 플래그와 io_uring_task_sleeping() 호출을 추가해 커널 어디서 발생하든 스레드 차단을 감지하는 방식임.
  • 작업이 차단되면 작업자 스레드가 제출 스레드의 정체성을 이어받고, 원래 스레드는 작업자 스레드의 정체성을 가진 채 차단 후 작업을 마무리하는 구조임.
  • task_struct 참조가 남아 있을 수 있는 조건에서는 정체성 교환을 금지하며, 추적·성능 이벤트·실시간 스케줄러·섀도 스택 등이 제약 요인임.
  • tmpfs의 fsync() 벤치마크에서 거의 700% 성능 향상이 나타난 반면, 항상 차단되는 작업과 높은 큐 깊이에서는 성능 저하가 발생할 수 있음.

비차단 실행을 위한 기존 비용

  • io_uring은 비동기 실행을 중심으로 설계돼 애플리케이션은 명시적으로 요청하지 않는 한 호출이 차단되지 않기를 기대함. 그러나 커널의 많은 경로는 비동기 실행을 염두에 두고 설계되지 않아 이 보장을 지키기 어려우며, 기존 우회 방식은 성능 비용이 큼.
  • 사용자 공간 프로세스는 제출 링의 제출 큐 항목(SQE)에 작업을 기술하고 io_uring_enter()를 호출해 하나 이상의 작업을 제출함. 커널은 링에 들어 있는 항목을 처리하며, 가능하면 io_uring_enter() 호출 안에서 바로 실행함.
  • 예를 들어 읽기 요청의 데이터가 페이지 캐시에 있고 사용자 공간 버퍼가 램에 상주하면 차단 없이 데이터를 즉시 복사할 수 있음.
  • 차단이 필요한 경우 커널의 일부 입출력 경로는 비동기 처리를 지원해 요청을 가능한 지점까지 진행한 뒤 차단 작업이 끝나면 커널 내 다른 곳에서 완료함. 하지만 fdatasync(), statx(), 일부 openat() 경로 등은 이러한 지원이 없음. O_NONBLOCK 플래그도 일부 openat() 경로의 차단을 막지 못함.
  • 현재 커널은 io_uring_enter()를 차단할 수 있는 작업을 별도 작업자 스레드로 넘김. 제출 스레드는 계속 진행할 수 있지만, 대기 중인 작업자 스레드를 깨우고 컨텍스트 전환을 수행하는 등의 비용이 발생함.
  • 작업이 실제로 차단되면 이 비용이 전체 비용에서 큰 비중을 차지하지 않을 수 있지만, 차단 없이 끝나는 경우에는 요청 실행 비용에서 상당한 비중을 차지함. 성능 향상을 위해 io_uring을 사용하는 애플리케이션에는 불필요한 차단 회피 비용이 부담임.

스레드 차단 감지와 정체성 교환

  • 커널의 모든 시스템 호출 경로를 비차단 방식으로 바꾸는 방안은 일부 경로에서 이미 적용됐지만, 작업이 쉽거나 빠르지 않음. Jens Axboe가 선택한 대안은 차단 가능성이 있는 작업을 우선 진행하고, 실제 차단이 생기더라도 제출 스레드는 차단되지 않도록 처리하는 방식임.
  • 차단 여부를 감지하기 위해 스케줄러를 수정함. 스레드를 나타내는 task_struct의 플래그 필드에 PF_IO_HANDOFF를 추가하고, 해당 플래그가 설정된 스레드가 어떤 이유로든 차단 직전에 있으면 스케줄러가 io_uring_task_sleeping()을 호출함.
  • 스케줄러에 연결하면 관련 코드 경로 자체를 수정하지 않고도 커널 어디에서 발생한 차단이든 io_uring에 알릴 수 있음.
  • 작업이 차단될 상황임을 알게 된 뒤, 이미 진행한 작업을 되돌리고 작업자 스레드에서 다시 시작하는 방안은 사실상 불가능함. 차단이 커널 어디에서나 발생할 수 있기 때문이며, 차단을 피하도록 설계되지 않은 커널 경로를 이용해 작업을 시작한 경우 일반적으로 완료까지 진행해야 함.
  • Axboe의 해법은 ‘스레드 정체성 교환(thread identity handoff)’임. 차단 알림을 받은 io_uring은 스레드 풀에서 작업자 스레드를 고르고, 진행 중인 작업을 넘기는 대신 두 스레드의 정체성을 교환함.
  • 작업자 스레드는 스레드 ID와 신호 처리 설정 등을 포함해 제출 스레드처럼 보이도록 바뀌며, 제출 링 처리를 계속한 뒤 사용자 공간으로 돌아감. io_uring_enter()에서 돌아오는 스레드는 호출을 시작한 스레드와 다른 task_struct를 가지지만, 그 밖의 모든 것이 같아 보이도록 하는 것이 목표임.
  • 차단 직전이던 원래 스레드는 작업자 스레드의 정체성을 이어받아 정상적으로 차단됨. 깨어난 뒤 작업을 완료하고 작업자 스레드 풀에 합류함. 이에 따라 작업이 차단 없이 끝나면 제출 스레드에서 전부 처리되고, 실제 차단이 발생할 때만 작업자 스레드를 끌어오는 비용이 발생함.

정체성 교환의 안전성 제약

  • 두 스레드의 task_struct를 교환하기 전에 커널의 다른 곳에서 해당 구조체를 참조하지 않는지 확실히 확인해야 함. 참조가 남으면 잘못된 task_struct를 대상으로 작업이 수행될 수 있으며, 커널 보안 취약점(CVE) 대응 부담을 키울 위험이 있음.
  • 작업을 시작하기 전에 필요할 경우 제출 스레드가 정체성을 넘길 수 있는지 확인해야 함. 패치의 thread_handoff_allowed()와 thread_handoff_compatible()에는 교환을 막고 기존 방식으로 작업을 실행하게 하는 조건이 다수 열거됨.
  • ptrace()로 스레드를 추적하는 경우 추적자가 해당 task_struct 참조를 보유하므로 교환할 수 없음. 해당 스레드가 다른 작업을 추적하는 경우에도 그 작업들이 참조를 보유하므로 교환이 금지됨.
  • 그 밖의 제한 조건에는 성능 이벤트(perf events) 사용, 커널의 퓨텍스 소유권 추적, 실시간 스케줄러 실행, 코어 스케줄링 쿠키 보유, vfork() 수행 등이 포함됨.
  • 이 조건을 빠짐없이 판단하는 부분이 패치 세트에서 가장 우려스럽고 취약한 지점으로 보임. task_struct는 커널 전반에서 접근 가능하므로 모든 참조 가능성을 확인하기 어렵고, 향후 io_uring을 고려하지 않는 개발자가 새 참조를 추가할 가능성도 있음.

벤치마크와 성능 상충

  • 패치 설명에는 여러 벤치마크 결과가 포함됨. 빠르고 비차단인 일부 작업에서는 큰 개선이 나타났으며, tmpfs 파일 시스템의 fsync() 벤치마크에서는 거의 700% 향상됨.
  • 다른 개선 폭은 더 작고, 항상 차단되는 일부 테스트에서는 성능 저하가 나타남. 가장 큰 저하는 여러 작업을 한꺼번에 제출하는 높은 큐 깊이에서 발생하는 경향임.
  • 각 작업을 즉시 작업자 스레드에 넘기면 병렬 처리가 가능하지만, 제출 스레드에서 차단 지점까지 진행하면 작업 처리가 직렬화돼 느려질 수 있음.
  • Axboe는 이 문제를 해결할 아이디어가 있지만 추진할 가치가 있는지는 확신하지 못함. 해당 작업을 수행하는 애플리케이션은 애초에 큐 깊이가 높은 경우가 드물다는 설명임.

RFC의 향후 검토

  • Axboe는 이 패치 세트가 가까운 시일 안에 병합될 것으로 기대하기보다, 전체 접근 방식이 실현 가능한지 확인하는 데 중점을 둠. 지금까지 의견은 제한적임.
  • Peter Zijlstra는 정체성 교환을 막는 조건 중 하나인 섀도 스택 사용이 배포된 시스템 대부분에서 기능을 쓰지 못하게 할 수 있다고 지적함. Axboe는 섀도 스택도 스레드 정체성과 함께 이동할 수 있다고 봄.
  • Gabriel Krisman Bertazi는 이 패치 세트를 “정말 멋지고, 매우 위험해 보이는 것”이라고 표현함.

“정말 멋지고, 매우 위험해 보이는 것.”

  • 개발자들은 아직 패치 세트를 검토하는 단계임. 위험 요소가 해결됐다고 개발자들을 설득할 수 있다면, 여러 작업 부하에서 io_uring 성능을 크게 개선하는 결과로 이어질 가능성이 있음.