company_banner

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


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

    Пару недель назад я столкнулся с вопросом, почему 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-провайдера?
    Ждем вас в комментариях.

    Selectel
    ИТ-инфраструктура для бизнеса

    Похожие публикации

    Комментарии 42

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

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

          +3
          Некоторую?)
            +13

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

        +2
        Тут дело не в интерфейсах, а в работе NAND части. Вот 3 пункта, почему серверное решение быстрее:
        1. Количество физических каналов и микросхем памяти на серверных решениях обычно больше, чем на клиентских — результат распараллеливание операций записи;
        2. Скорость работы серверных дисков постоянна (почти), в независимости от нагрузки. Клиентские же последние записи хранят в SLC буфере, и в момент простоя перемещают данные в MLC/TLC область. По этому на многих графиках виден резкий провал производительности при длительной нагрузке на запись;
        3. Конденсаторы. Откройте серверный SSD и посмотрите — треть, а то и больше площади платы усыпаны конденсаторами, что дает достаточно времени и энергии для сохранения состояний так называемой L2P таблицы, клиент же лишен этой привилегии и должен чаще скидывать в nand изменения, на что тратится дополнительное время.
          +1
          1) не всегда
          2) не всегда — в том числе не на всех клиентских есть SLC кэши — в том числе на части SLC кэш можно даже отключить конфигурацией прошивки и перепрошивкой — лично такое делал. и наоборот, на каких-то «серверных» SLC кэш может быть.
          3) а вот конденсаторы — это стопроцентная тема, мы уже в цефочате про них года 3 рассказываем всем. вот, наконец и досюда долетело) правда про L2P опять не совсем верно, как серверные, так и клиентские диски кэшируют всё подряд, а не только маппинги. просто серверные могут вообще ничего не сбрасывать во флеш при fsync — ни данные, ни метаданные. а, и да, на самых дешёвых «серверных» Intel DC конденсаторов нет.
            0
            1. Согласен, не всегда.
            2. Согласен, не всегда.
            3. За дешевые не скажу, может и нет. Да, серверные тоже всегда свое состояние сбрасывает, но может это делать реже, тут как FW написано и сколько «кондеров» не пожалели. Ну и в клиенте на DRAM могут сэкономить, некоторые вообще без него работают, что плохо сказывается на IOPs/s…
            Кстати, про FW, возможно для клиента уклон делается в сторону ускорения чтения, а запись не сильно оптимизируют, так как на пользовательских ПК записи, по большому счету, не так много, а что есть — ограничена уже другими вещами…
            0

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

            0

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

              +3
              Мебибайт (wikipedia).
                +3
                О… Стыдно то как!!! Спасибо!
              +2

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

                0

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

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

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

                      0

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

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

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

                        +1

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

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

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

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

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


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


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

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

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


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

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


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

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

                                +1

                                Если вас интересует производительность накопителя, то вам нужен 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 будет явно быстрее чем если будет использоваться обычный ввод-вывод (по крайней мере в моём случае).

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

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


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

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


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

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

                                    Зависит от базы, но да, тут может быть тормоз. Правда, пишется он всё же обычно не маленькими блоками, и даже тут 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.

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

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


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

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

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

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


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

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


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

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

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


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

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


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


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

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


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

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

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

                                            Не скажите, зависит от приложения. Несколько сотен 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, то внутри он может быстрее их обработать, если раскидает их по разным чипам, хотя, конечно, гарантии нет.

                                              0
                                              на моей боевой прирост был около 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");
                                              }

                                              читаем 16 ГиБ в N потоков, если N=1, то блоками по 4 МиБ, если N=4, то блоками по 1 МиБ, и т. п.
                                              как и ожидалось, с ростом числа потоков время чтения увеличивается.


                                              если кто захочет повторить у себя:


                                              • подразумевается linux и наличие компилятора C;
                                              • собирается gcc -O2 -o my-super-fio my-super-fio.c (gcc можно заменить по вкусу на clang/что-то-ещё);
                                              • запускается sudo time ./my-super-fio /dev/nvme0n1 4 (последний параметр — число потоков).
                                                0
                                                замена на 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).

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

                                                  за счёт того, что 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 мкс/оп

                                                  смешивается выполнение fork() с операциями чтения, хоть и немного но может повлиять на обработку — в идеале это должны быть либо threads либо AIO, причём в первом случае лучше начинать их одновременно (по семафору, к примеру);

                                                  ну так это и есть быстрая прикидка «на коленке», со всякими многопоточностями возиться пришлось бы дольше.


                                                  К сожалению, время на чтение даже одного мегабайта уж очень мало в случае NVMe, так что сделал его 1 GiB и проверил

                                                  если я вас правильно понял, это вы уже совсем другое измеряете, многопоточное чтение крупными блоками. понятное дело, что оно будет быстрее (и это проще с помощью fio проверить).
                                                  речь же изначально шла про «если он получит 8 команд на 8 блоков по 128K вместо одной на 1M» (ну разве что я себе позволил 1М на 4М заменить, но это не критично).


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

                                                  нет-нет, задумано так: все процессы запускаются de-facto одноврменно, и начинают параллельно читать кусок в 16 ГиБ именно так, как вы сказали: делаются N запросов к первому блоку на 4 Миб, потом N ко второму, и т. д.


                                                  К сожалению, время на чтение даже одного мегабайта уж очень мало в случае NVMe

                                                  так потому я и сделал 16 гигов. притом читать одно и то же по кругу не совсем честно (может сработать кэширование в контроллере), а вот последовательное чтение в несколько потоков как раз реализует вашу первоначальную идею.


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


                                                  ваше же изначальное предложение в переносе на seq scan было примерно таково: запускаем 8 воркеров, первый читает первую запись, второй вторую, …, потом первый девятую, второй десятую, …


                                                  с fork() я не совсем уверен что система реально отправляет запросы к устройству параллельно

                                                  ну это вы зря )


                                                  к тому же я читал файл а не устройство (но это вряд-ли существенно)

                                                  не должно особо влиять, но иногда файловые системы вносят странные искажения.
                                                  а почему не стали читать устройство? оно гарантировано ничего не пишет; плюс в начале диска обычно есть какие-то данные, так что проблемы сверхбрыстрого чтения после trim не должно быть.


                                                  P. S. мне интересно было бы увидеть результаты моего варианта теста.

                                                    0
                                                    за счёт того, что 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

                                                    речь же изначально шла про «если он получит 8 команд на 8 блоков по 128K вместо одной на 1M» (ну разве что я себе позволил 1М на 4М заменить, но это не критично).

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


                                                    ваше же изначальное предложение в переносе на seq scan было примерно таково: запускаем 8 воркеров, первый читает первую запись, второй вторую

                                                    Примерно.


                                                    Так что я вернулся в вашему коду и кажется нашёл в чём проблема...

                                                    Каюсь, я был настолько "возмущен" наличем там for() что даже не стал анализировать pread(), оказалось — зря:


                                                        for (int i=0; i<COUNT; i++) {
                                                            pread(fd, buf, bs+offset, i*BS+offset);
                                                        }

                                                    Вы в каждом блоке читаете bs + offset байт, я думаю это не совсем то что нужно, если учесть что offset сильно зависит от номера потока и становится очень немаленьким, т.е. вы не читаете каждый кусок последовательно блоками одного размера — что и приводит к ужасной производительности, т.к. offset ненулевой для числа потоков > 1.


                                                    Если убрать offset из размера буфера, чтение в один поток медленнее (~ 0.64s) чем в более чем один (~ 0.54s).


                                                    Но если вас интересуют "оригинальные" результаты (которые, на мой взгляд, лишены смысла из-за переменного размера блока зависящего от номера потока) — то получается всё те же ~ 0.64s для одного потока (ясный пень) но уже ощутимо медленнее для 2 (~ 0.81s) и уж тем более 8 (аж 2.40s).


                                                    а почему не стали читать устройство?

                                                    Почитал (в начале, где с вероятностью 99% нет пустышек) — результаты не изменились.

                                                      0
                                                              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 уже не будет узким местом.

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

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


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

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

                                                          0

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


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

                                                            +2

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


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

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

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое