Direct I / O пишет: лучший способ улучшить свой кредитный рейтинг.

Я недавно писал о том, как серьезные изменения в технологии хранения меняют традиционные знания о том, как обращаться с вводом-выводом при хранении. Главный тезис статьи был прост: по мере того, как устройства NVMe становятся обычным явлением, влияние программного уровня становится все сильнее. Следует пересмотреть старые идеи и API, разработанные для того времени, когда доступ к хранилищу осуществлялся за сотни миллисекунд.

В частности, я исследовал идею о том, что буферный ввод-вывод, при котором операционная система кэширует страницы данных от имени пользователя, всегда должен быть лучше, чем прямой ввод-вывод, где такого кеширования не происходит. Когда мы используем современные API, это просто не так. Фактически, в примере с использованием асинхронного исполнителя Glommio io_uring для Rust чтение прямого ввода-вывода в большинстве случаев выполнялось лучше, чем буферизованный ввод-вывод.

А как насчет записи? В этой статье мы рассмотрим проблему записи, ее отличия от чтения и покажем, что, как и задолженность по кредитной карте, буферизованные операции ввода-вывода создают только иллюзию богатства за счет дешевых денег. В какой-то момент вам все равно придется платить по счетам. С другой стороны, настоящее богатство приносит прямой ввод-вывод.

Чем отличаются чтение и запись?

Тот факт, что операции чтения и записи различаются по своим характеристикам, никого не должен удивлять: это обычное дело в информатике, и это то, что лежит в основе большей части тенденции к неизменяемым структурам данных в последние годы.

Тем не менее, есть один секрет, касающийся, в частности, устройств хранения данных, который является не чем иным, как умопомрачительным:

Выполнить атомарную запись на запоминающее устройство просто невозможно. По крайней мере, на практике. Статья stackoverflow хорошо описывает ситуацию, и я также рекомендую эту статью LWN.net, в которой рассказывается об изменениях, которые некоторые разработчики файловой системы Linux обсуждают для улучшения ситуации.

Для SSD ситуация совершенно беспомощная. Для NVMe это немного лучше: в спецификации есть положение для атомарной записи, но даже если все устройства реализуют это (а они этого не делают), все еще существует большой контингент устройств, на которых должно работать программное обеспечение, на котором это просто недоступен.

По этой причине записи в середину файла очень редко в приложениях, и даже когда они случаются, они, как правило, сопровождаются журналом, который носит последовательный характер.

У этого есть два непосредственных последствия:

  1. Структуры данных только с добавлением преобладают над записью в хранилище. Большинство современных хранилищ, оптимизированных для записи, построены на основе LSM-деревьев, и даже рабочие нагрузки, использующие более традиционные структуры данных, такие как B-деревья, будут иметь журнал и / или другие методы, обеспечивающие надежную запись данных.
  2. Обычно существует буфер памяти, который используется для накопления записей перед их передачей в файл: это гарантирует некоторый уровень контроля над состоянием файла, когда происходит запись. Если бы мы, например, записывали непосредственно в файл mmap'd, сбросы могли бы происходить в любое время, и мы просто не знали бы, в каком состоянии находится файл. Хотя это правда, что мы можем принудительно установить максимум время для синхронизации со специализированными системными вызовами, такими как msync, операционной системе, возможно, придется принудительно выполнить сброс из-за нехватки памяти в любой момент до этого.

Это означает, что объединение, которое является обычным преимуществом буферизации, не применяется для записи. Для большинства современных структур данных нет особых причин держать буфер в памяти в ожидании следующего доступа: вероятно, то, что было отправлено в файл, больше никогда не будет затронуто, за исключением будущих чтений. И тут применимы расчеты из моей прочитанной статьи. Следующая запись вероятна для следующей позиции в файле.

Это еще больше склоняет чашу весов в пользу Direct I / O. В ожидании использования недавно записанных страниц в будущем буферизованный ввод-вывод может использовать огромный объем памяти в кэше страниц операционной системы. И хотя это правда, что это кэшированная память, эта память должна быть сначала записана на устройство, прежде чем ее можно будет выбросить. Если устройство недостаточно быстрое, нам легко может не хватить памяти. Об этой проблеме я писал в прошлом.

Поскольку мы можем записать целый файл размером в терабайт, сохраняя при этом всего пару килобайт в памяти, прямой ввод-вывод является бесспорным способом записи в файлы.

Но сколько стоит прямой ввод-вывод?

Как и при чтении, вам нужно убедиться, что вы измеряете правильный результат, чтобы понять преимущества прямого ввода-вывода. А как это сделать - далеко не очевидно.

Недавно один из наших пользователей открыл проблему на нашей странице Github, в которой он отметил, что, несмотря на то, что мы рекламируем, операции прямого ввода-вывода потребляют намного больше ресурсов ЦП, чем буферизованные записи. Так почему это так?

Причина в том, что буферизованная запись похожа на ссуду: вы можете получить свой актив по дешевке сейчас, но затем вам придется вернуть его в будущем с процентами. Когда вы выполняете запись с прямым вводом-выводом, вы оплачиваете большую часть затрат, связанных с транзакцией, сразу и в ЦП, который отправил ввод-вывод, что вполне предсказуемо. Ситуация иная для буферизованного ввода-вывода: единственные затраты, которые необходимо немедленно оплатить, - это очень дешевая запись в память.

Фактическая работа по сохранению данных выполняется в потоках ядра. Эти потоки ядра могут запускаться на других процессорах, поэтому в простой системе, которая далека от точки насыщения, это может дать пользователю иллюзию более дешевого доступа.

Как и в случае ссуды, в некоторых случаях это может сработать в вашу пользу. Однако на практике это произойдет в непредсказуемое и потенциально неудобное время в будущем.

Помимо этой непредсказуемости, чтобы принять правильное решение, нужно хотя бы осознавать тот факт, что общая стоимость ссуды может быть выше. Чаще всего бывает так, что при насыщении или близком к нему все ваши процессоры заняты, и в этом случае общая стоимость более важна.

Если мы воспользуемся командой time для измерения версии прямого ввода-вывода по сравнению с буферизованной версией того же кода, предоставленного пользователем, и сосредоточимся на системном и пользовательском времени, мы получим:

Прямой ввод / вывод:

user 0m7.401s
sys 0m7.118s

И буферизованный ввод / вывод:

user	0m3.771s
sys	0m11.102s

Итак, у нас есть это: все, что делала версия с буферизованным вводом-выводом, - это переключение пользовательского времени на системное время. А поскольку это системное время расходуется потоками ядра, что может быть труднее увидеть, мы можем получить иллюзию, что буферизованная запись потребляет меньше ресурсов ЦП.

Но если мы просуммируем пользовательское и системное время, мы ясно увидим, что на самом деле мы в конечном итоге платим проценты по нашей ссуде: буферизованная запись использует на 1,7% больше ЦП, чем запись прямого ввода-вывода. На самом деле это не очень далеко от текущих ежемесячных процентных ставок по моей кредитной карте. Если это шокирующее совпадение или большой заговор, решать вам, читатель.

Но что быстрее?

Многие пользователи были бы счастливы заплатить некоторый процент процессорного времени, чтобы получить более быстрые результаты. Но если мы посмотрим на реальное время в приведенных выше примерах, Direct I / O не только дешевле, но и быстрее.

В примере кода вы заметите, что пользователь правильно выполнил вызов close.. По умолчанию закрытие потока Glommio подразумевает синхронизацию. Но не только это можно отключить, в большинстве случаев в других языках и фреймворках это не так. В частности, для Posix close не подразумевает синхронизацию.

Это означает, что даже после того, как вы запишете все свои буферы и закроете файл, ваши данные могут по-прежнему небезопасно присутствовать на носителе устройства! Однако может вызывать удивление то, что данные не хранятся безопасно даже при использовании Direct I / O! Это связано с тем, что Direct I / O записывает данные немедленно на устройство, а устройства хранения имеют собственные внутренние кеши. И в случае потери питания данные все равно могут быть потеряны, если эти кеши не сохраняются.

Здесь справедливо спросить: если синхронизация необходима как для буферизованной записи, так и для прямого ввода-вывода, действительно ли есть преимущества у прямого ввода-вывода? Чтобы исследовать это поведение, мы можем использовать пример тестирования хранилища Glommio.

Сначала мы напишем файл размером меньше памяти и не будем выполнять синхронизацию. Легко создать впечатление, что буферизованный ввод-вывод быстрее. Если мы запишем файл размером 4 ГБ на сервере с 64 ГБ памяти DRAM, мы увидим следующее:

Buffered I/O: Wrote 4.29 GB in 1.9s, 2.25 GB/s
Direct   I/O: Wrote 4.29 GB in 4.4s, 968.72 MB/s

Буферизованный ввод-вывод более чем в два раза быстрее! Это потому, что, поскольку файл настолько мал по сравнению с размером памяти, он может просто находиться в памяти все время. Однако на этом этапе ваши данные вообще не хранятся безопасно. Если мы учтем время, необходимое для обеспечения безопасности до тех пор, пока наш вызов синхронизации не вернется, затраты на установку, отсутствие параллелизма, сопоставления и другие затраты, обсуждаемые при анализе чтения, начнут показывать:

Buffered I/O: Wrote 4.29 GB in 1.9s, 2.25 GB/s
Buffered I/O: Closed in 4.7s, Amortized total 642.54 MB/s
Direct   I/O: Wrote 4.29 GB in 4.4s, 968.72 MB/s
Direct   I/O: Closed in 34.9ms, Amortized total 961.14 MB/s

Как мы видим, ссуды с буферизацией ввода-вывода создавали иллюзию богатства. Как только нам пришлось платить по счету, Direct I / O стал быстрее, а мы стали богаче. Как отмечалось ранее, синхронизация файла прямого ввода-вывода не является бесплатной: но спустя 35 мс мы можем предсказуемо гарантировать, что он будет безопасно сохранен. Сравните это с более чем 4 с для буферизованного ввода-вывода.

Все начинает меняться по мере того, как файл становится больше. Это связано с тем, что виртуальная память операционной системы находится под большим давлением. По мере увеличения размера файла операционная система больше не может позволить себе роскошь ждать до конца, чтобы выполнить сброс. Если мы теперь напишем файл размером 16 ГБ, 32 ГБ и файл 64 ГБ, мы увидим, что даже иллюзорная разница между буферизованным и прямым вводом-выводом начинает исчезать.

Buffered I/O: Wrote 17.18 GB in 10.4s, 1.64 GB/s
Buffered I/O: Closed in 11.8s, Amortized total 769.58 MB/s
Buffered I/O: Wrote 34.36 GB in 29.9s, 1.15 GB/s
Buffered I/O: Closed in 12.2s, Amortized total 814.85 MB/s
Buffered I/O: Wrote 68.72 GB in 69.4s, 989.7 MB/s
Buffered I/O: Closed in 12.3s, Amortized total 840.59 MB/s

Во всех вышеупомянутых случаях Direct I / O продолжал писать со скоростью около 960 МБ / с, что является максимальной пропускной способностью этого конкретного устройства.

Когда размер файла превышает размер памяти, больше не нужно притворяться: прямой ввод-вывод работает быстрее, с какого бы угла мы на него ни смотрели.

Buffered I/O: Wrote 107.37 GB in 113.3s, 947.17 MB/s
Buffered I/O: Closed in 12.2s, Amortized total 855.03 MB/s
Direct   I/O: Wrote 107.37 GB in 112.1s, 957.26 MB/s
Direct   I/O: Closed in 43.5ms, Amortized total 956.89 MB/s

Заключение

Доступ к кредиту - это неплохо. Во многих случаях это имеет решающее значение для накопления богатства. Однако нам нужно обращать внимание на общие затраты, удостовериться, что процентные ставки разумны, чтобы быть уверенным, что мы создаем реальное, а не иллюзорное богатство.

То же самое и при записи в файлы на современном хранилище. Мы можем сначала выписать их по дешевке, но мы обязаны заплатить реальную стоимость - потом с процентами. Хорошо это или нет, конечно, зависит от ситуации. Но с высокими процентными ставками и потенциалом выхода памяти из-под контроля, если вы пишете быстрее, чем может прожевать устройство, буферизованный ввод-вывод может легко стать субстандартным. Прямой ввод-вывод с фиксированным использованием памяти и более низкой стоимостью ЦП - это AAA.

Я надеюсь, что эта статья даст вам возможность сделать лучший выбор, чтобы вы могли реально увеличить объем хранилища.