Почему мой NVMe медленнее SSD? +25





В данной статье мы рассмотрим некоторые нюансы подсистемы ввода-вывода и их влияние на производительность.

Пару недель назад я столкнулся с вопросом, почему NVMe на одном сервере медленнее, чем SATA на другом. Посмотрел в характеристики серверов и понял, что это был вопрос с подвохом: NVMe был из пользовательского сегмента, а SSD — из серверного.

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

Что такое fsync и где он используется


Для ускорения работы с накопителями данные буферизируются, то есть сохраняются в энергозависимой памяти до тех пор, пока не представится удобный случай для сохранения содержимого буфера на накопитель. Критерии «удобного случая» определяются операционной системой и характеристиками накопителя. В случае исчезновения питания все данные в буфере будут утеряны.

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

Продемонстрируем влияние буферов искусственным примером в виде короткой программы на языке C.

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(void) {
    /* Открываем файл answer.txt на запись, если его нет -- создаём */
    int fd = open("answer.txt", O_WRONLY | O_CREAT);
    /* Записываем первый набор данных */
    write(fd, "Answer to the Ultimate Question of Life, The Universe, and Everything: ", 71);
    /* Делаем вид, что проводим вычисления в течение 10 секунд */
    sleep(10);
    /* Записываем результат вычислений */
    write(fd, "42\n", 3); 

    return 0;
}

Комментарии хорошо объясняют последовательность действий в программе. Текст «ответ на главный вопрос жизни, Вселенной и всего такого» будет буферизирован операционной системой, и если перезагрузить сервер нажатием на кнопку Reset во время «вычислений», то файл окажется пуст. В нашем примере потеря текста не является проблемой, поэтому fsync не нужен. Базы данных такого оптимизма не разделяют.

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

На что влияет частое использование fsync


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

Продемонстрируем влияние использования fsync на конкретном примере. В качестве испытуемых у нас следующие твердотельные накопители:

  • Intel® DC SSD S4500 480 GB, подключен по SATA 3.2, 6 Гбит/с;
  • Samsung 970 EVO Plus 500GB, подключен по PCIe 3.0 x4, ~31 Гбит/с.

Тесты проводятся на Intel® Xeon® W-2255 под управлением ОС Ubuntu 20.04. Для тестирования дисков используется sysbench 1.0.18. На дисках создан один раздел, отформатированный как ext4. Подготовка к тесту заключается в создании файлов объемом в 100 ГБ:

sysbench --test=fileio --file-total-size=100G prepare

Запуск тестов:

# Без fsync
sysbench --num-threads=16 --test=fileio --file-test-mode=rndrw --file-fsync-freq=0 run

# С fsync после каждой записи
sysbench --num-threads=16 --test=fileio --file-test-mode=rndrw --file-fsync-freq=1 run

Результаты тестов представлены в таблице.
Тест Intel® S4500 Samsung 970 EVO+
Чтение без fsync, МиБ/с 5734.89 9028.86
Запись без fsync, МиБ/с 3823.26 6019.24
Чтение с fsync, МиБ/с 37.76 3.27
Запись с fsync, МиБ/с 25.17 2.18
Нетрудно заметить, что NVMe из клиентского сегмента уверенно лидирует, когда операционная система сама решает, как работать с дисками, и проигрывает, когда используется fsync. Отсюда возникает два вопроса:

  1. Почему в тесте без fsync скорость чтения превышает физическую пропускную способность канала?
  2. Почему SSD из серверного сегмента лучше обрабатывает большое количество запросов fsync?

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

Если ставить под сомнение все результаты sysbench, то можно воспользоваться fio.

# Без fsync
fio --name=test1 --blocksize=16k --rw=randrw --iodepth=16 --runtime=60 --rwmixread=60 --fsync=0 --filename=/dev/sdb

# С fsync после каждой записи
fio --name=test1 --blocksize=16k --rw=randrw --iodepth=16 --runtime=60 --rwmixread=60 --fsync=1 --filename=/dev/sdb
Тест Intel® S4500 Samsung 970 EVO+
Чтение без fsync, МиБ/с 45.5 178
Запись без fsync, МиБ/с 30.4 119
Чтение с fsync, МиБ/с 32.6 20.9
Запись с fsync, МиБ/с 21.7 13.9
Тенденция к просадке производительности у NVMe при использовании fsync хорошо заметна. Можно переходить к ответу на второй вопрос.

Оптимизация или блеф


Ранее мы говорили, что данные хранятся в буфере, но не уточняли в каком именно, так как это было не принципиально. Мы и сейчас не будем углубляться в тонкости операционных систем и выделим два общих вида буферов:

  • программный;
  • аппаратный.

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

Так как SSD показывает лучшие результаты, то можно сделать два предположения:

  • диск спроектирован под нагрузку подобного плана;
  • диск «блефует» и игнорирует команду.

Нечестное поведение накопителя можно заметить, если провести тест с исчезновением питания. Проверить это можно скриптом diskchecker.pl, который был создан в 2005 году.

Данный скрипт требует две физические машины — «сервер» и «клиент». Клиент записывает на тестируемый диск небольшой объем данных, вызывает fsync и отправляет серверу информацию о том, что было записано.

# Запускается на сервере
./diskchecker.pl -l [port]

# Запускается на клиенте
./diskchecker.pl -s <server[:port]> create <file> <size_in_MB>

После запуска скрипта необходимо обесточить «клиента» и не возвращать питание в течение нескольких минут. Важно именно отключить тестируемого от электричества, а не просто выполнить жесткое выключение. По прошествии некоторого времени сервер можно подключать и загружать в ОС. После загрузки ОС необходимо снова запустить diskchecker.pl, но с аргументом verify.

./diskchecker.pl -s <server[:port]> verify <file>

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

Наш S4500 не показал ошибок при потере питания, то есть можно утверждать, что он готов к нагрузкам с большим количеством вызовов fsync.

Заключение


При выборе дисков или целых готовых конфигураций следует помнить о специфике задач, которые необходимо решить. На первый взгляд кажется очевидным, что NVMe, то есть SSD с PCIe-интерфейсом, быстрее «классического» SATA SSD. Однако, как мы поняли сегодня, в специфических условиях и с определенными задачами это может быть не так.

А как вы тестируете комплектующие cерверов при аренде у IaaS-провайдера?
Ждем вас в комментариях.

Теги:




Комментарии (42):

  1. lelik363
    /#22126590 / +1

    Имелось ввиду: накопители с интерфейсом NVMe уже больше не SSD?

    • ildarz
      /#22126644

      NVMe — протокол доступа, SSD — технология энергонезависимой памяти. Так что таки да, NVMe не SSD. :) Хотя автор тоже некоторую вольность себе позволил.

      • c_kotik
        /#22126648

        Некоторую?)

        • ildarz
          /#22126682 / +4

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

  2. jakushev
    /#22126734

    Тут дело не в интерфейсах, а в работе NAND части. Вот 3 пункта, почему серверное решение быстрее:
    1. Количество физических каналов и микросхем памяти на серверных решениях обычно больше, чем на клиентских — результат распараллеливание операций записи;
    2. Скорость работы серверных дисков постоянна (почти), в независимости от нагрузки. Клиентские же последние записи хранят в SLC буфере, и в момент простоя перемещают данные в MLC/TLC область. По этому на многих графиках виден резкий провал производительности при длительной нагрузке на запись;
    3. Конденсаторы. Откройте серверный SSD и посмотрите — треть, а то и больше площади платы усыпаны конденсаторами, что дает достаточно времени и энергии для сохранения состояний так называемой L2P таблицы, клиент же лишен этой привилегии и должен чаще скидывать в nand изменения, на что тратится дополнительное время.

    • vitalif
      /#22126788

      1) не всегда
      2) не всегда — в том числе не на всех клиентских есть SLC кэши — в том числе на части SLC кэш можно даже отключить конфигурацией прошивки и перепрошивкой — лично такое делал. и наоборот, на каких-то «серверных» SLC кэш может быть.
      3) а вот конденсаторы — это стопроцентная тема, мы уже в цефочате про них года 3 рассказываем всем. вот, наконец и досюда долетело) правда про L2P опять не совсем верно, как серверные, так и клиентские диски кэшируют всё подряд, а не только маппинги. просто серверные могут вообще ничего не сбрасывать во флеш при fsync — ни данные, ни метаданные. а, и да, на самых дешёвых «серверных» Intel DC конденсаторов нет.

      • jakushev
        /#22126884

        1. Согласен, не всегда.
        2. Согласен, не всегда.
        3. За дешевые не скажу, может и нет. Да, серверные тоже всегда свое состояние сбрасывает, но может это делать реже, тут как FW написано и сколько «кондеров» не пожалели. Ну и в клиенте на DRAM могут сэкономить, некоторые вообще без него работают, что плохо сказывается на IOPs/s…
        Кстати, про FW, возможно для клиента уклон делается в сторону ускорения чтения, а запись не сильно оптимизируют, так как на пользовательских ПК записи, по большому счету, не так много, а что есть — ограничена уже другими вещами…

    • edo1h
      /#22127784

      так статья как раз про пункт три

  3. optemist
    /#22126772

    Что то я возможно туплю, но
    Что за единицы "МиБ/с"?
    Если это Мегабайты в секунду почему Samsung доходит до 9000 МБ/с? У него паспортные 3500.

  4. dion
    /#22126838

    Ответ тут очень простой… Нормальные серверные диски умеют FUA, а обычные 'домашние' — нет. Из-за этого fsync на домашних дисках дорогой (эмулируется обычным cache flush)

    • edo1h
      /#22127800

      нет, ответ был дан выше — дело в конденсаторах/ионисторах, которые позволяют более агрессивно кэшировать запись

  5. eldog
    /#22127050

    Интересно, а команда создать файл нулевой длины немедленно выполняется или тоже как-то кешируется? Возможна ли ситуация, когда последовательность «создать временный файл — записать в него — прочитать из него — удалить файл» выполнится целиком в памяти, вообще не трогая диск?

    • CaptainFlint
      /#22127178

      В операционках существует поддержка «временных файлов». Если файл создать с таким атрибутом, то да, он может всю свою жизнь просидеть только в оперативке.

    • alex-khv
      /#22128516

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

      • pansa
        /#22128918

        Если файл будет удалён до сбрасывания грязных данных из кэша, то вполне может и не придётся ничего писать. Именно потому, что диск это блочное устройство. Конечно, если речь о более-менее обычных ОС и дефолтных режимах i/o.

  6. picul
    /#22127318

    В программе на C хорошо бы закрыть файловый дескриптор перед выходом…

    • alex-khv
      /#22128518

      Кто считает что после завершения процесса это не обязательно? Также как и освобождение памяти ?

      • edo1h
        /#22128910

        вам пофлеймить или как? )

      • AllexIn
        /#22140212

        Это как с поворотником: показывать надо всегда. Чтобы это был рефлекс и не надо было думать — показывать или нет.
        Также и с дескрипторами: закрывать надо всегда, даже если иногда можно не закрывать.

  7. Tangeman
    /#22128446

    Для тестов собственно накопителя лучше использовать --direct=1 в fio, fsync покажет результаты которые замылены системой, да и на чтение он не влияет (в вашем случае разница есть потому что это r/w тест, а не только чтение). И добавить --numjobs=4 (или сколько у вас там ядер) не помешает тоже.

    • edo1h
      /#22128908

      1. тестировать запись без fsync с включенным directio бессмысленно — drectio сам по себе синхронный;
      2. numjobs увеливает глубину очереди, иногда хочется посмотреть на однопоточную производительность накопителя.

      • Tangeman
        /#22128944

        Смысл как раз в том чтобы заменить fsync на directio, иначе вы тестируете не сам накопитель а все слои над ним, включая буферизацию.


        Если хочется однопоточной производительности, ставьте уж тогда iodepth=1, зачем там 16?


        Правда, не совсем понятно какой смысл тестировать однопоточную синхронную производительность если это не основной сценарий использования — устройство может плохо показать себя с QD1 но отлично — с QD4-32 (особенно NVMe), разница может быть на порядки.

        • edo1h
          /#22128984

          Смысл как раз в том чтобы заменить fsync на directio, иначе вы тестируете не сам накопитель а все слои над ним, включая буферизацию

          речь про то, что с --directio=1 мы никак не проверим производительность несинхронной записи на диск.


          Если хочется однопоточной производительности, ставьте уж тогда iodepth=1, зачем там 16?

          упс, просмотрел, что в статье тестируется с глубиной очереди 16. в этом случае согласен, что что-то вроде --numjobs=4 --iodepth=4 имеет смысл.


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

          можете привести правдоподобный пример синхронной записи с глубиной очереди >>1 на продолжительное время? мне сходу ничего не приходит в голову.
          примеров же однопоточной синхронной записи сколько угодно, начиная с почти любой базы данных.

          • Tangeman
            /#22129038

            Если вас интересует производительность накопителя, то вам нужен directio. Без него чтение (и запись без fsync) показывает производительность памяти минус оверхед на буферизацию прочие слои ввода-вывода (LVM, crypt etc). Запись с fsync почти соответствует directio только если fsync делает после каждой операции записи, хотя может быть добавлен оверхед на буфера и прочее.


            Правдоподобный пример асинхронной многопоточной записи (не синхронной) — это собственно работа самой ОС и файловой системы — все приложения которые лезут на диск в рано или поздно потребуют обращения к накопителю, в зависимости от конкретного пути к нему, буферизации и т.п. всё скорее всего преобразуется в серию несвязанных запросов на операции которые свалятся накопителю в очередь, а он уже их попытается оптимизировать как может.


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


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


            Грубо говоря, если он даёт в одном потоке 100 MiB/s, то не факт что 10 клиентов получат каждый по 10 MiB — вполне может оказаться что каждый получит по 100 MiB/s или чуть меньше (но не в 10 раз).


            Судя по своему опыту, паспортные показатели SSD (SATA, SAS и NVMe) накопители выдают либо на однопоточном синхронном доступе с большим размером блока (обычно 128-256K или выше), либо при многопоточном асинхронном с блоками небольшого размера (4-16K).


            Вот пример с моего домашнего Pny CS3030 1T

            , размер блока 4k, iodepth=8 numjobs=4, случайное чтение, directio:


            read: IOPS=486k, BW=1899MiB/s (1991MB/s)(18.5GiB/10001msec)

            Теперь те же 4k но с iodepth/numbjobs 1:


            read: IOPS=27.0k, BW=106MiB/s (111MB/s)(1056MiB/10001msec)

            Какие-то несчастные 100 MiB/s… А вот с блоком в 1M, iodepth/numjobs 1:


            read: IOPS=1909, BW=1910MiB/s (2002MB/s)(18.6GiB/10001msec)

            Т.е. мы получили те же почти 2 GiB/s что и 4k iodepth=8 numjobs=4. Теперь повторим 1M блок на iodepth=8 и numjobs=4:


            read: IOPS=2963, BW=2964MiB/s (3108MB/s)(28.0GiB/10011msec)

            Это уже почти близко к максимуму (по паспорту 3500 MB/s, я не уверен что имеется в виду MiB). Повторим с блоками по 4k, iodepth=16 numjobs=8:


            read: IOPS=790k, BW=3087MiB/s (3237MB/s)(30.2GiB/10004msec)

            Примерно столько же. Увы, на паспортный показатель выйти не удается с любыми параметрами (хотя система ни чем не загружена), разве что там MB а MiB. Причём что примечательно, если из последнего теста убрать directio и сбросить кэш (3 > drop_caches), то получим не очень приятные результаты:


            read: IOPS=151k, BW=588MiB/s (616MB/s)(5880MiB/10001msec)

            Сразу видны накладные расходы на буферизацию и всё с ней связанное (сначала всё идёт в буфера и только потом в приложение, практически двойное копирование).


            Как видите, прямой связи с производительностью накопителя нет, в том смысле что реальная производительность будет сильно зависеть от конкретных условий, размера памяти, политики буферизации и обработки очередей и ещё кучи факторов, но вот работа базы которая использует directio будет явно быстрее чем если будет использоваться обычный ввод-вывод (по крайней мере в моём случае).

            • edo1h
              /#22129116

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

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


              Какие-то несчастные 100 MiB/s

              время доступа около 30 мкс? слишком хороший результат, чтобы быть правдой.
              подозреваю, что вы тестировали на «пустой» области (на которую не было записано данных после trim).


              read latency существенно меньше 100 мкс я видел только на optane.

              • Tangeman
                /#22131344

                обычно база имеет общий журнал, куда пишется однопоточно и синхронно

                Зависит от базы, но да, тут может быть тормоз. Правда, пишется он всё же обычно не маленькими блоками, и даже тут directio помогает. Но всё зависит от конкретных условий, да и при чтении выигрыш будет всё равно, а при наличии другой активности (вне базы) — и по записи. В случае если накопитель использует на хосте виртуалок — уж точно будет не очень плохо.


                read latency существенно меньше 100 мкс я видел только на optane.

                Для синхронной однопоточной или вообще? Потому что для асинхронной многопоточной он вполне выдает около 30 мкс, о чём говорят другие тесты — например тут или тут — в обоих около 30 мкс, и там сначала пишут потом читают.


                Я повторил тест на большом файле который гарантированно без дыр (ISO c CentOS), получается около 100 мкс при iodepth=1 numjobs=1 bs=4k, но поскольку тут уже чуть-чуть замешана файловая система (даже при directio), не уверен что это совсем "чистый" результат:


                read: IOPS=10.2k, BW=39.8MiB/s (41.7MB/s)(398MiB/10001msec)

                Не 30 мкс конечно, но всё же около 100.

                • edo1h
                  /#22132818

                  Правда, пишется он всё же обычно не маленькими блоками, и даже тут directio помогает

                  у постгреса 8 Кб, у остальных ЕМНИП цифры того же порядка


                  Потому что для асинхронной многопоточной он вполне выдает около 30 мкс, о чём говорят другие тесты — например тут или тут — в обоих около 30 мкс

                  по вашим же ссылкам crystaldiskmark выдаёт 40 мегабайт в секунду в один поток, всё те же 100 мкс.
                  там, где получилось меньше, читают с пустого диска (то есть NAND не участвует в тесте вообще), такие ляпы чуть ли не в каждом втором тесте

                  • Tangeman
                    /#22133074

                    у постгреса 8 Кб, у остальных ЕМНИП цифры того же порядка

                    Ну он же не пишет каждый блок отдельно с fsync после каждого — а сразу пачкой, которая накапливается. Т.е. если транзакция на пару мегабайт, они и уйдут одним блоком (скорее всего, в любом случае не по одному с 8K каждый), а если commit_delay не особо низкий то и несколько транзакций из разных сессий упакует сразу, если окажутся рядом.


                    по вашим же ссылкам crystaldiskmark выдаёт 40 мегабайт в секунду в один поток

                    В один поток да, факт, но десяток потоков (которые пишут/читают в разные места) получат с высокой вероятностью по 100 мкс каждый.


                    У меня даже есть подозрение, что если взять условный блок в 1 MiB в приложении, разбить его на 8 по 128k каждый и отправить эти части асинхронно — то скорость получится быстрее чем если отправить его целиком, то есть можно обойти ограничение однопоточной производительности, если накопитель позволит.

                    • edo1h
                      /#22133112

                      Т.е. если транзакция на пару мегабайт, они и уйдут одним блоком

                      транзакция на пару мегабайт — это исключение всё-таки


                      а если commit_delay не особо низкий то и несколько транзакций из разных сессий упакует сразу, если окажутся рядом.

                      commit_delay на боевых базах вообще не включаю. ибо раньше были контроллеры с bbwc, сейчас dc ssd, и то, и другое делает синхронную запись фактически равной обычной.


                      а на тестовых базах можно и вообще fsync отключить )


                      В один поток да, факт, но десяток потоков (которые пишут/читают в разные места) получат с высокой вероятностью по 100 мкс каждый.

                      нет (взяли бы, да и проверили, недолго же и недеструктивно)


                      У меня даже есть подозрение, что если взять условный блок в 1 MiB в приложении, разбить его на 8 по 128k каждый и отправить эти части асинхронно — то скорость получится быстрее чем если отправить его целиком, то есть можно обойти ограничение однопоточной производительности, если накопитель позволит.

                      тоже нет, производительность последовательного чтения и так заметно выше, чем случайного (распараллеливание по чипам NAND контроллер сам делает, ничем вы ему тут не поможете)

                      • Tangeman
                        /#22133362

                        транзакция на пару мегабайт — это исключение всё-таки

                        Не скажите, зависит от приложения. Несколько сотен update/insert и уже накапливается, а в зависимости от размеров полей и больше может. Даже один апдейт по сотне тысяч строк легко наберёт мегабайты, если вспомнить что внутри Postgres update = delete/insert.


                        commit_delay на боевых базах вообще не включаю

                        Почему нет? Чем плохо совместить запись в журнал нескольких параллельных транзакций с одним fsync? Сто пудов два fsync на два блока по 1 MiB будут чуть-чуть медленней чем один на блок 2 MiB, а если блоки поменьше то выигрыш может быть существенным — на моей боевой прирост был около 30%, всего-то поднял delay до 100ms (там много параллельных insert/update). Шанс что она слетит весьма невысок (всего пару случает за последние 5 лет), если слетит — приложение её повторит (ибо будет знать об ошибке).


                        взяли бы, да и проверили, недолго же и недеструктивно

                        Проверяем:


                        bs=4k iodepth=1 numjobs=1:   read: IOPS=10.2k, BW=39.8MiB/s (41.7MB/s)(398MiB/10001msec)
                        
                        bs=4k iodepth=1 numjobs=8:   read: IOPS=70.3k, BW=274MiB/s (288MB/s)(2745MiB/10001msec)
                        

                        Не пустой изошник вышеупомянутой CenOS, в один поток — ~100 мкс, в восемь потоков — сюрприз, всего чуть-чуть выше — ~114 мкс. Или я где-то ошибся в расчётах? Проверьте у себя.


                        распараллеливание по чипам NAND контроллер сам делает, ничем вы ему тут не поможете

                        Вообще-то помогу, если отправлю сразу несколько команд на запись/чтение вместо одной, т.е. если он получит 8 команд на 8 блоков по 128K вместо одной на 1M, то внутри он может быстрее их обработать, если раскидает их по разным чипам, хотя, конечно, гарантии нет.

                        • edo1h
                          /#22138000

                          на моей боевой прирост был около 30%, всего-то поднял delay до 100ms (там много параллельных insert/update).

                          диски DC? если нет, то замена на DC должна кардинально изменить ситуацию.


                          в один поток — ~100 мкс, в восемь потоков — сюрприз, всего чуть-чуть выше — ~114 мкс. Или я где-то ошибся в расчётах? Проверьте у себя.

                          я проверял на нескольких накопителях, у меня таких результатов (в 8 потоков ?7 от производительности одного потока) нет, видимо, ваш накопитель в этом плане хорош.


                          Вообще-то помогу, если отправлю сразу несколько команд на запись/чтение вместо одной, т.е. если он получит 8 команд на 8 блоков по 128K вместо одной на 1M

                          не поленился, накатал quick and dirty тест
                          #define _GNU_SOURCE
                          #include <sys/stat.h>
                          #include <sys/types.h>
                          #include <sys/wait.h>
                          #include <fcntl.h>
                          #include <stdio.h>
                          #include <unistd.h>
                          
                          #define BS (4*1024*1024LL)
                          #define COUNT 4096
                          
                          #define ALIGN 4096
                          
                          int forks;
                          int fd;
                          
                          char buf[BS] __attribute__ ((__aligned__ (ALIGN)));
                          
                          void do_test(int n)
                          {
                              const size_t bs = ((BS/forks+ALIGN-1)/ALIGN)*ALIGN;
                              const size_t offset = (((BS*n)/forks)/ALIGN)*ALIGN;
                          
                              for (int i=0; i<COUNT; i++) {
                                  pread(fd, buf, bs+offset, i*BS+offset);
                              }
                          }
                          
                          int main(int argc, char **argv)
                          {
                          
                              switch (argc) {
                              case 2:
                                  forks=1;
                                  break;
                              case 3:
                                  sscanf(argv[2], "%d", &forks);
                                  break;
                              default:
                                  printf("Usage: %s filename [count]\n", argv[0]);
                                  return 1;
                              }
                          
                              if (forks<1 || forks>1000) {
                                  printf("Wrong count %d\n", forks);
                                  return 1;
                              }
                          
                              fd=open(argv[1], O_RDONLY | O_DIRECT);
                          
                              if (fd<0) {
                                  printf("Cannot open %s\n", argv[1]);
                                  return 1;
                              }
                          
                              for (int i=0; i<forks; i++) {
                                  switch (fork()) {
                                  case 0:
                                      do_test(i);
                                      return 0;
                                  }
                              }
                          
                              printf("Reading, please wait...");
                              while(wait(NULL)>0);
                              printf("done\n");
                          }

                          • Tangeman
                            /#22138068

                            замена на DC должна кардинально изменить ситуацию

                            Не могу представить за счёт чего. Если у меня есть 10 параллельных транзакций, то запись из по очереди с fsync будет по определению медленней чем всех сразу с одним fsync, на любом диске который изобретён, потому что записать много маленьких блоков всегда дороже чем один большой.


                            Выигрыша может не быть разве что на сравнительно маленьких транзакциях, до 1-2 мегабайт, тогда всё в кэш дисков валится, но у меня они бывают и в несколько десятков мегабайт.


                            А тут я решил чуть-чуть поэкспериментировать...

                            Насчёт вашего super-fio есть пара замечаний:


                            • смешивается выполнение fork() с операциями чтения, хоть и немного но может повлиять на обработку — в идеале это должны быть либо threads либо AIO, причём в первом случае лучше начинать их одновременно (по семафору, к примеру);
                            • каждый процесс читает в цикле N блоков (и каждое чтение — запрос к накопителю), хотя всего-то нужно разбить один большой на много частей и читать каждую независимо — т.е. for() там совершенно лишний и pread() должен выглядеть как pread(fd, buf, bs, offset); — и всё.

                            К сожалению, время на чтение даже одного мегабайта уж очень мало в случае NVMe, так что сделал его 1 GiB и проверил — получилось 0.37s/0.24s (минимальное время) при одном потоке, и 0.35s/0.32s при четырех, но что примечательно — при одном потоке максимальное время было и около 1 секунды, при 4 и больше — оно стабильно 0.35s плюс-минус 0.02s из полусотни прогонов.


                            Итого, из 50 прогонов, (общее время всех прогонов, real time, система ничем не занята):


                            • 1 поток: 27s (бывало и 23, но редко)
                            • 2 потока: 18s (ощутимая разница, однако)
                            • 4 потока: 17s (чуть лучше)
                            • 8 потоков: 17s (без изменений)
                            • 16 потоков: 16s (видимо мы на пределе)
                            • 32 потока: 16s (таки да)

                            Я повторил эксперимент несколько раз, время отличается только для одного потока, остальные стабильно на показанном выше уровне. Хоть это и не кратное ускорение, но и условия не совсем честные (fork() вместо threads/aio), к тому же я читал файл а не устройство (но это вряд-ли существенно). Если использовать threads или aio, вероятно результаты станут лучше, с fork() я не совсем уверен что система реально отправляет запросы к устройству параллельно (а лезть в ebpf мне лень).


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


                            Правильный do_test():


                            void do_test(int n)
                            {
                                const size_t bs = ((BS/forks+ALIGN-1)/ALIGN)*ALIGN;
                                const size_t offset = (((BS*n)/forks)/ALIGN)*ALIGN;
                            
                                pread(fd, buf, bs, offset);
                            }

                            И как я уже сказал раньше, блок у меня был в 1 GiB, при 4 MiB не хватало разрешения таймера (чтение идёт со скоростью около 2 GiB/s).

                            • edo1h
                              /#22138084

                              Не могу представить за счёт чего.

                              за счёт того, что fsync на DC дисках намного быстрее, в результате оно уже может перестать быть узким местом.


                              раз уж мы говорим про постгрес, сколько у вас pg_test_fsync показывает? (в debian лежит в /usr/lib/postgresql/xx/bin/)


                              на моём стареньком s3510
                              Сравнение методов синхронизации файлов при однократной записи 8 КБ:
                              (в порядке предпочтения для wal_sync_method, без учёта наибольшего предпочтения fdatasync в Linux)
                                      open_datasync                      4796,401 оп/с        208 мкс/оп
                                      fdatasync                          4795,636 оп/с        209 мкс/оп
                                      fsync                              4590,151 оп/с        218 мкс/оп
                                      fsync_writethrough                            н/д
                                      open_sync                          4493,085 оп/с        223 мкс/оп
                              
                              Сравнение методов синхронизации файлов при двухкратной записи 8 КБ:
                              (в порядке предпочтения для wal_sync_method, без учёта наибольшего предпочтения fdatasync в Linux)
                                      open_datasync                      3028,778 оп/с        330 мкс/оп
                                      fdatasync                          4256,953 оп/с        235 мкс/оп
                                      fsync                              4111,496 оп/с        243 мкс/оп
                                      fsync_writethrough                            н/д
                                      open_sync                          2842,767 оп/с        352 мкс/оп

                              • Tangeman
                                /#22139976

                                за счёт того, что fsync на DC дисках намного быстрее, в результате оно уже может перестать быть узким местом.

                                Независимо от скорости — два обращения к накопителю дороже одного, хотя бы потому что ядру нужно дождаться выполнения каждого по очереди (если пишет один процесс — а в postgres только один пишет в журнал, ибо всё должно быть последовательно). Насколько дороже — зависит от размера блока и их количества.


                                сколько у вас pg_test_fsync показывает?

                                Вполне ожидаемое замедление
                                5 seconds per test
                                O_DIRECT supported on this platform for open_datasync and open_sync.
                                
                                Compare file sync methods using one 8kB write:
                                (in wal_sync_method preference order, except fdatasync is Linux's default)
                                        open_datasync                       643,253 ops/sec    1555 usecs/op
                                        fdatasync                           664,511 ops/sec    1505 usecs/op
                                        fsync                               438,914 ops/sec    2278 usecs/op
                                        fsync_writethrough                              n/a
                                        open_sync                           430,532 ops/sec    2323 usecs/op
                                
                                Compare file sync methods using two 8kB writes:
                                (in wal_sync_method preference order, except fdatasync is Linux's default)
                                        open_datasync                       320,054 ops/sec    3124 usecs/op
                                        fdatasync                           653,392 ops/sec    1530 usecs/op
                                        fsync                               400,675 ops/sec    2496 usecs/op
                                        fsync_writethrough                              n/a
                                        open_sync                           203,359 ops/sec    4917 usecs/op
                                
                                Compare open_sync with different write sizes:
                                (This is designed to compare the cost of writing 16kB in different write
                                open_sync sizes.)
                                         1 * 16kB open_sync write           413,544 ops/sec    2418 usecs/op
                                         2 *  8kB open_sync writes          197,709 ops/sec    5058 usecs/op
                                         4 *  4kB open_sync writes           99,996 ops/sec   10000 usecs/op
                                         8 *  2kB open_sync writes           46,859 ops/sec   21341 usecs/op
                                        16 *  1kB open_sync writes           23,062 ops/sec   43361 usecs/op
                                
                                Test if fsync on non-write file descriptor is honored:
                                (If the times are similar, fsync() can sync data written on a different
                                descriptor.)
                                        write, fsync, close                 430,991 ops/sec    2320 usecs/op
                                        write, close, fsync                 401,975 ops/sec    2488 usecs/op
                                
                                Non-sync'ed 8kB writes:
                                        write                            183465,059 ops/sec       5 usecs/op

                                • edo1h
                                  /#22140164

                                          pread(fd, buf, bs+offset, i*BS+offset);

                                  уфф, по ночам спать надо, а не тесты писать.
                                  конечно, имелось в виду


                                          pread(fd, buf+offset, bs, i*BS+offset);

                                  проверил на паре накопителей — действительно, на одном с ростом числа потоков скорость растёт (на втором интереснее — на диапазоне 2-32 потока производительность растёт, потом падает, но в 1 поток быстрее, чем в 32).


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


                                  Независимо от скорости — два обращения к накопителю дороже одного, хотя бы потому что ядру нужно дождаться выполнения каждого по очереди

                                  да, формально так.
                                  но вы видите же, что fsync на DC диске на порядок быстрее (на самом деле на порядки, сейчас протестировал нормальный DC NVME — 30-40 мкс на fsync против ваших полутора тысяч), так что на сопоставимых нагрузках fsync уже не будет узким местом.

                                  • Tangeman
                                    /#22140214

                                    скорее всего, особого смысла в этой оптимизации нет

                                    Вероятно, для ряда задач всё же он есть, но не в общем.


                                    сейчас протестировал нормальный DC NVME — 30-40 мкс на fsync

                                    Если мы перенесём эксперименты на HDD (не SSD) — разница проявится в полную силу. К сожалению, не везде и не всегда можно впихнуть SSD, к тому же не всегда они DC.

                                    • edo1h
                                      /#22140448

                                      Кому в 2020 может придти в главу мысль размещать нагруженную БД на hdd? Вот лет десять назад это было актуально (но и тогда проблемы задержек fsync решались, тогда это был bbwc на рейд-контроллере, ну или полноценная схд).


                                      Да и десктопные ssd особого смысла использовать нет, «серверные» зачастую стоят примерно столько же, при этом нет проблем вроде супер-медленного fsync (иногда и того лучше — быстрого, но ничего не делающего) и «ой, у меня slc-буфер переполнился, дальше будем писать со скоростью 60МБ/с».

                                      • Tangeman
                                        /#22140880 / +2

                                        Мысль приходит когда у вас 10+ терабайт данных — на RAID с SSD вы просто разоритесь, в то время как RAID на HDD вполне справляется с нагрузкой за более разумные деньги — бюджет не всегда безразмерен, особенно когда из своего кармана.


                                        Чтобы буфер переполнился нужно загнать туда 30-60 гб одним куском — это уж очень нагруженная база получается (а вот тут можно вспомнить про commit_delay и размеры транзакций), хотя даже мой CS3030 пишет 500 MiB/s после заполнения кэша, что всё равно в два раза быстрее DC HDD.

  8. edo1h
    /#22129112

    del

  9. Delhin
    /#22130296

    В даташитах на ССД искать наличие «data loss protection».
    Но! Например, для micron 1100 серии вероятно уже маркетинговый обман, т.к. «power-loss protection for data-at-rest» — это с вероятностью 99,99% не то что нужно для серверных решений.
    Похоже и сюда маркетологи добрались :(