Операционные системы

Продвинутый I/O

Nginx обрабатывает 100,000 соединений на одном сервере. PostgreSQL выполняет миллионы транзакций в секунду. Redis возвращает данные с latency <1ms. Секрет? Продвинутые техники I/O: async I/O, zero-copy, direct I/O. Это разница между приложением, которое захлёбывается на 1000 RPS, и тем, которое масштабируется до миллионов.

  • **High-performance серверы:** io_uring в Nginx, lighttpd - до 3x меньше CPU на тот же throughput. sendfile() для статических файлов - основа CDN и web-серверов.
  • **Базы данных:** PostgreSQL, MySQL InnoDB используют O_DIRECT для WAL и data files. Это избегает double buffering (page cache + buffer pool) и даёт предсказуемую latency.
  • **Streaming и Big Data:** Kafka, ClickHouse используют zero-copy (sendfile) для передачи данных между партициями и клиентами. Экономия: до 70% CPU при высоких нагрузках.
  • **Cloud Storage:** MinIO, Ceph используют Direct I/O + io_uring для максимальной утилизации NVMe SSD. 1M IOPS на один сервер - результат правильного I/O.

Цели урока

  • Различать select/poll/epoll/kqueue и их сложность по числу FD
  • io_uring (Linux 5.1, 2019): shared ring buffer, batching, zero syscall в горячем пути
  • Zero-copy: sendfile, splice, MSG_ZEROCOPY для сетевой передачи
  • Direct I/O (O_DIRECT): обход page cache, alignment 512B/4KB, кейсы для БД
  • Применять async для high-throughput серверов (Nginx, Redis, ScyllaDB)

Асинхронный I/O

**Асинхронный I/O** (async I/O) позволяет приложению продолжать выполнение, пока операция ввода-вывода обрабатывается в фоне. В отличие от блокирующего I/O, поток не застревает в ожидании завершения операции.

**Три модели I/O:** • **Blocking I/O** - поток блокируется до завершения операции • **Non-blocking I/O** - операция сразу возвращает управление, нужно polling • **Async I/O** - операция выполняется в фоне, приложение получает уведомление о завершении

**Linux AIO (Asynchronous I/O)** - старый механизм асинхронного I/O в Linux. Работает через системные вызовы `io_submit()`, `io_getevents()`. Имеет ограничения: работает только с O_DIRECT, сложный API, не все операции действительно асинхронны.

Зачем async I/O

**Web-сервер на 10000 соединений:** • Blocking I/O: нужно 10000 потоков → огромный overhead на context switch • Non-blocking + epoll: один поток обрабатывает все соединения, но приходится делать много системных вызовов • Async I/O (io_uring): подаём batch запросов, получаем batch результатов - минимум системных вызовов

**Проблема Linux AIO:** операции с буферным кешем (page cache) всё равно блокируют. Асинхронность гарантируется только для Direct I/O (O_DIRECT), что неудобно для многих приложений.

**Применение async I/O:** • **Базы данных** - параллельные запросы к диску • **Web-серверы** - обработка тысяч соединений одним потоком • **Файловые серверы** - одновременная обработка множества файловых операций • **High-frequency trading** - минимизация latency критична

В чём ключевое преимущество async I/O перед non-blocking I/O с epoll?

io_uring - Революция в Linux I/O

**io_uring** - современный механизм асинхронного I/O в Linux (с ядра 5.1, 2019). Это полная переработка подхода к I/O: вместо отдельных системных вызовов используются **кольцевые буферы** (ring buffers) в shared memory между ядром и приложением.

**Революционные возможности io_uring:** • **Zero системных вызовов** в горячем пути - приложение и ядро общаются через shared memory • **Batch операций** - подаём сразу много запросов, получаем много результатов • **Любые операции асинхронны** - не только I/O, но и accept, connect, fsync, даже openat • **Полированная цепочка операций** - можно связывать операции без возврата в userspace

**SQPOLL режим** - ядро выделяет отдельный поток, который постоянно проверяет Submission Queue. Приложению вообще не нужны системные вызовы для submit - просто пишем в shared memory, и ядерный поток сразу обрабатывает.

Реальный пример

**Nginx с io_uring (экспериментальная поддержка):** Бенчмарк показал прирост производительности до 30-40% на высоких нагрузках (100k+ req/s) за счёт: • Меньше context switches • Batch обработка запросов • Меньше системных вызовов (с ~4 на запрос до ~1)

**Linked operations** - можно связывать операции в цепочку: открыть файл → прочитать → закрыть. Всё выполняется в ядре без возврата в userspace между операциями.

**io_uring сегодня:** • **RocksDB** - использует io_uring для ускорения compaction • **ScyllaDB** - база данных, изначально спроектированная под io_uring • **QEMU** - виртуализация с io_uring для disk I/O • **liburing** - официальная библиотека от автора io_uring (Jens Axboe)

Почему io_uring может работать вообще без системных вызовов в hot path?

Zero-Copy I/O

**Zero-copy** - техники передачи данных между файлами, сокетами и приложением без копирования в userspace. Обычный путь: disk → kernel buffer → user buffer → kernel socket buffer → network. Zero-copy: disk → kernel buffer → network.

**sendfile()** - системный вызов для zero-copy передачи файла в сокет. Данные передаются внутри ядра, минуя userspace. Идеально для web-серверов, отдающих статические файлы.

Web-серверы

**Nginx: статический файл 1 MB** • Без sendfile: read() + write() → ~1000 системных вызовов, ~2-3 MB копирования • С sendfile: 1 вызов, 0 копирований в userspace Прирост производительности: до 70% при высоких нагрузках.

**splice()** - более гибкий zero-copy механизм. Передаёт данные между file descriptor и pipe, или между двумя pipes. Можно строить сложные data pipelines внутри ядра.

**mmap() + write()** - альтернативный подход к zero-copy. Отображаем файл в память процесса (memory-mapped I/O), затем пишем в сокет. Данные копируются только один раз: page cache → socket buffer.

**Scatter-Gather I/O (vectored I/O):** readv()/writev() позволяют читать/писать в несколько буферов одним вызовом. В комбинации с zero-copy можно построить эффективные pipelines без лишних копирований.

Файловые серверы

**Samba file server:** При передаче файлов Windows-клиентам использует sendfile() для zero-copy. На файлах >1GB разница в CPU usage: ~80% → ~20%. Освобождённые ресурсы можно потратить на обработку большего числа клиентов.

В чём основное преимущество sendfile() перед классическим read() + write()?

Direct I/O и обход кеша

**Direct I/O (O_DIRECT)** - режим работы с файлами, при котором данные передаются напрямую между приложением и диском, минуя page cache ядра. Это даёт приложению полный контроль над кешированием.

**Когда нужен O_DIRECT:** • **Базы данных** - свой buffer pool, page cache только мешает (double buffering) • **Streaming** - данные читаются один раз, кешировать бессмысленно • **Измерение производительности диска** - page cache искажает результаты • **Управление приоритетами** - приложение само решает, что кешировать

**PostgreSQL и O_DIRECT:** PostgreSQL может работать в режиме O_DIRECT для WAL (Write-Ahead Log). Это критично для durability: данные сразу идут на диск, минуя page cache, который может быть потерян при падении.

Реальный кейс

**MySQL InnoDB:** По умолчанию использует O_DIRECT для data files: ```ini innodb_flush_method = O_DIRECT ``` Причины: • InnoDB имеет свой buffer pool (кеш) • page cache только расходует память и создаёт overhead • С O_DIRECT: 100GB buffer pool + 0 page cache vs Buffered I/O: 100GB buffer pool + 50GB page cache (двойное расходование RAM)

**O_SYNC vs O_DIRECT vs fdatasync():** • **O_SYNC** - каждый write() ждёт физической записи на диск (медленно) • **O_DIRECT** - обходит page cache, но не гарантирует durability (может быть в disk cache) • **fdatasync()** - сбрасывает данные из page cache на диск, но не метаданные • **fsync()** - сбрасывает всё: данные + метаданные

**Alignment требования O_DIRECT:** • **Offset** - кратен logical block size (обычно 512 или 4096 байт) • **Size** - кратен logical block size • **Buffer address** - выровнен на boundary (512/4096 байт) Используйте `posix_memalign()` или `aligned_alloc()` для выделения буферов.

Durability vs Performance

**Redis persistence:** Redis AOF (Append-Only File) может использовать fsync() после каждой записи для максимальной durability: ``` appendfsync always → fsync() после каждой команды (медленно, но надёжно) appendfsync everysec → fsync() раз в секунду (баланс) appendfsync no → ОС сама решает (быстро, но риск потери данных) ``` Для critical данных используют `always` + O_DIRECT в некоторых сценариях.

O_DIRECT делает операции быстрее, поэтому его нужно использовать везде

O_DIRECT полезен только когда приложение само управляет кешированием. Для обычных случаев buffered I/O эффективнее

page cache ядра содержит множество оптимизаций: read-ahead, write coalescing, lazy write-back. O_DIRECT отключает всё это. Для БД с собственным buffer pool это плюс (нет дублирования), но для обычного приложения - минус (потеря оптимизаций). Неправильное использование O_DIRECT может привести к падению производительности в 10x из-за потери read-ahead и мелких несвыравненных запросов.

Почему базы данных часто используют O_DIRECT вместо buffered I/O?

Ключевые идеи

  • **Async I/O** (io_uring) позволяет приложению продолжать работу во время I/O операций. Вместо системных вызовов используются ring buffers в shared memory - до 0 syscalls в hot path.
  • **io_uring** - революция в Linux I/O (kernel 5.1+). Submission Queue (SQ) для запросов, Completion Queue (CQ) для результатов. Batch операций, SQPOLL режим, linked operations. Используется в RocksDB, ScyllaDB, QEMU.
  • **Zero-copy** (sendfile, splice, mmap) передаёт данные внутри ядра без копирования в userspace. Критично для web-серверов (статика), файловых серверов, streaming. Экономия: до 70% CPU на high throughput.
  • **Direct I/O (O_DIRECT)** обходит page cache, давая контроль приложению. Базы данных используют для избежания double buffering. Требует alignment буферов и офсетов (512/4096 байт). Комбинация O_DIRECT + io_uring = максимальная производительность.

Связанные темы

Продвинутый I/O - часть экосистемы производительности и надёжности систем:

  • I/O Scheduling — Планировщики I/O (CFQ, Deadline, mq-deadline, Kyber) определяют порядок выполнения запросов к диску. io_uring работает поверх I/O scheduler.
  • File Systems — ФС (ext4, XFS, Btrfs) влияют на производительность Direct I/O. Journaling, COW, extent allocation - всё это взаимодействует с O_DIRECT.
  • Memory Management — page cache - часть подсистемы управления памятью. Direct I/O требует понимания DMA, pinned pages, TLB.
  • Concurrency — Async I/O позволяет строить высокопараллельные системы. Event loops (epoll, io_uring) - основа для async/await, futures, coroutines.

Вопросы для размышления

  • Почему io_uring с SQPOLL режимом может работать вообще без системных вызовов? Какой trade-off у этого подхода?
  • Когда sendfile() неэффективен? Приведите пример сценария, где классический read() + write() будет быстрее.
  • Базе данных нужно писать 1000 записей по 4KB. Что эффективнее: 1000 операций с O_DIRECT или один большой write() с buffered I/O? Почему?
  • Как io_uring linked operations могут помочь в реализации HTTP сервера? Какие операции можно связать?

Связанные уроки

  • db-12-query-tuning
  • net-36-websocket
  • net-53-distributed-intro
  • rt-01-what-is-realtime
Продвинутый I/O

0

1

Войти