Обычный путь read(file) → write(socket):
- DMA копирует данные с диска в page cache ядра;
read копирует из page cache в буфер приложения (CPU);write копирует из буфера приложения в буфер сокета (CPU);- DMA копирует из буфера сокета в сетевую карту.
Плюс два системных вызова и четыре переключения между пользовательским режимом и ядром. Приложение здесь просто перекладывает байты, ничего с ними не делая.
sendfile(socket, file, offset, count) — ядро передаёт данные из page cache прямо в сокет. С поддержкой сетевой карты (scatter-gather DMA) не копирует даже в буфер сокета — только дескрипторы. Итог: нет копирований через CPU в пространство пользователя, один системный вызов, кэши процессора не засоряются. Используется в nginx (sendfile on), Kafka, FileChannel.transferTo в Java, os.sendfile в Python.
Почему Kafka быстрая — набор решений вокруг этого:
- журнал с последовательной записью — только дописывание в конец файла сегмента; последовательный доступ быстр и на HDD, и на SSD;
- page cache вместо собственного кэша — свежие сообщения обычно читаются из памяти ядра; нет двойного кэширования и пауз GC от кэша в куче JVM;
- zero-copy — сообщения хранятся на диске в том же формате, что идут по сети, поэтому брокер отдаёт их потребителю через
sendfile, не разбирая; - пакеты — продюсер копит сообщения (
linger.ms, batch.size), сжимает пачку целиком, брокер пишет её как есть; - партиции — параллелизм по брокерам и потребителям.
Ограничения zero-copy:
- данные нельзя менять по пути: если брокер должен перекодировать формат для старого клиента — копирование возвращается;
- TLS шифрует в пространстве пользователя —
sendfile неприменим, пока не включён kTLS (шифрование в ядре); - выгода видна на больших объёмах; для мелких ответов важнее число системных вызовов.
Родственные механизмы: mmap (файл как память, без копии при чтении), splice/tee (перекладка между дескрипторами через канал), MSG_ZEROCOPY для отправки из пользовательских буферов, io_uring с зарегистрированными буферами, RDMA между машинами.