Home » Исследование чтения с фантомного диска Linux

Исследование чтения с фантомного диска Linux

Не так давно один из наших пользователей обратился со случаем странного использования оборудования. Они использовали наш ILP (протокол линии InfluxDB)
клиент вставлять строки в свою базу данных QuestDB, но наряду с записью на диск они также наблюдали значительное количество операций чтения с диска. Это определенно не ожидалось от рабочей нагрузки только для записи, поэтому нам пришлось добраться до сути этой проблемы. Сегодня мы делимся этой историей, полной взлетов и падений, а также магии ядра Linux.

#

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

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

После нескольких попыток нам удалось воспроизвести проблему. Симптомы выглядели следующим образом iostat выход:

вывод утилиты iostat с большим количеством операций чтения с диска

Здесь у нас есть 10-70 МБ/с для записи на диск, а чтение достигает 40-50 МБ/с. Использование чтения должно быть близко к нулю в сценариях только приема. Таким образом, большой объем чтения является очень неожиданным.

#

Первое, что мы хотели понять, это точные файлы, которые были прочитаны базой данных. К счастью, в Linux нет ничего невозможного. blktrace полезность в сочетании с blkparse можно использовать для сбора всех операций чтения на данном диске:

Приведенная выше команда выводит все события чтения с диска, происходящие в системе. Вывод выглядит примерно так:

Каждая строка здесь обозначает отдельное событие. Для простоты рассмотрим первую строку:

Соответствующие части следующие:

  • 425548 – это событие было сгенерировано с pid 425548.
  • Q РА – это событие обозначает запрос на чтение диска, добавленный (поставленный в очередь) в очередь ввода-вывода. Суффикс «A» плохо документирован, но он означает потенциальное опережающее чтение. Что такое readahead, мы узнаем чуть позже.
  • 536514808 + 8 – эта операция чтения начинается с блока 536514808 и имеет размер 8 блоков.
  • [questdb-ilpwrit] – операция запущена потоком записи ILP QuestDB.

Вооружившись этими знаниями, мы можем использовать debugfs чтобы следовать номеру блока, чтобы найти соответствующий индекс:

Наконец, мы можем проверить, что означает индексный дескриптор 8270377:

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

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

Кто может читать файлы? Может ОС?

#

Как и многие другие базы данных, QuestDB использует буферизованный ввод-вывод, например
mmap,
readи
write, чтобы иметь дело с дисковыми операциями. Это означает, что всякий раз, когда мы что-то записываем в файл, ядро ​​записывает измененные данные на несколько страниц в кэше страниц и помечает их как грязные.

Кэш страниц — это специальный прозрачный кэш в памяти, используемый Linux для хранения недавно прочитанных данных с диска и недавно измененных данных для записи на диск. Кэшированные данные организованы в виде страниц размером 4 КБ в большинстве дистрибутивов и архитектур ЦП.

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

По умолчанию
cairo.commit.mode значение, QuestDB не делает явным fsync/msync вызовы для сброса данных файлов столбцов на диск, поэтому он полностью зависит от ядра для сброса грязных страниц. Следовательно, нам нужно лучше понять, чего ожидать от ядра, прежде чем выдвигать гипотезу о нашем случае «фантомного чтения».

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

Есть несколько pdflush параметры, доступные для настройки. Вот наиболее значимые из них:

  • dirty_background_ratio – хотя процент грязных страниц меньше этого параметра, грязные страницы остаются в памяти до тех пор, пока они не станут достаточно старыми. Как только количество грязных страниц превышает это соотношение, ядро ​​активизируется.
    pdflush. В Ubuntu и других дистрибутивах Linux этот параметр по умолчанию равен 10
    (10%).
  • dirty_ratio – когда процент грязных страниц превышает это соотношение, записи перестают быть асинхронными. Это означает, что процесс записи (в нашем случае процесс базы данных) будет синхронно записывать страницы на диск. Когда это происходит, соответствующий поток переводится в состояние «непрерываемого сна» (D код состояния в top полезность). Этот параметр обычно по умолчанию равен
    20 (20%).
  • dirty_expire_centisecs – это определяет возраст в сантисекундах для грязных страниц, чтобы быть достаточно старыми для обратной записи. Этот параметр обычно по умолчанию равен
    3000 (30 секунд).
  • dirty_writeback_centisecs – определяет интервал для пробуждения процесса pdflush. Этот параметр обычно по умолчанию равен 500 (5 секунд).

Текущие значения вышеуказанных настроек можно проверить с помощью /proc виртуальная файловая система:

Также важно отметить, что приведенные выше проценты рассчитываются на основе общего объема восстанавливаемой памяти, а не общего объема ОЗУ, доступного на компьютере. Если ваше приложение не создает много грязных страниц и имеется много оперативной памяти, все записи на диск выполняются асинхронно pdflush.

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

Настройка этих значений не дала нам многого. Помните, что пользователь писал в большое количество столбцов? Это означает, что база данных должна была выделить некоторую память для каждого столбца, чтобы иметь дело с
вышел из строя
(O3) записывает, оставляя меньше памяти доступной для кэша страниц. Сначала мы это проверили, и действительно, большая часть оперативной памяти использовалась процессом базы данных. Настройка
cairo.o3.column.memory.size с 16 МБ по умолчанию до 256 КБ помогло значительно снизить скорость чтения с диска, так что действительно проблема была как-то связана с нехваткой памяти. Не волнуйтесь, если вы не понимаете этот абзац в деталях. Вот самый важный момент: уменьшение использования памяти базы данных уменьшило количество операций чтения. Это полезная подсказка.

Но какова была точная причина чтения диска?

Чтобы ответить на этот вопрос, нам нужно лучше понять часть страничного кеша, читающую диск. Для простоты сосредоточимся на mmapввод-вывод. как только ты mmap файл, ядро ​​выделяет записи таблицы страниц (PTE) для виртуальной памяти, чтобы зарезервировать диапазон адресов для вашего файла, но в этот момент оно не читает содержимое файла. Фактические данные считываются на страницу, когда вы обращаетесь к выделенной памяти, т.е. начинаете чтение (LOAD инструкция в x86) или запись (STORE инструкция в x86) памяти.

Когда это происходит впервые,
блок управления памятью
(MMU) вашего ЦП сигнализирует о специальном событии, называемом «ошибка страницы». Ошибка страницы означает, что доступ к памяти принадлежит PTE без выделенной физической памяти. Ядро обрабатывает ошибку страницы двумя способами:

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

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

На концептуальном уровне упреждающее чтение отвечает за чтение данных, которые приложение явно не запрашивало. Когда вы впервые получаете доступ (чтение или запись) к нескольким байтам только что открытого файла, происходит ошибка основной страницы, и ОС считывает данные, соответствующие запрошенной странице, а также несколько страниц до и после файла. Это называется «перечитывать». Если вы продолжаете обращаться к последующим страницам файла, ядро ​​распознает шаблон последовательного доступа и начинает упреждающее чтение, пытаясь заблаговременно прочитать последующие страницы пакетами.

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

Запомните событие чтения диска из blktrace выход? Суффикс «A» в типе операции «RA» предполагал, что чтение с диска было частью упреждающего чтения. Однако мы знали, что наша рабочая нагрузка связана только с записью и работой с большим количеством файлов. Проблема была гораздо более заметной, когда для кэширования страниц оставалось мало памяти.

Что, если страницы были исключены из кэша страниц слишком рано, что привело к избыточному упреждающему чтению при последующем доступе к странице?

Мы можем проверить эту гипотезу, отключив упреждающее чтение. Это так же просто, как сделать madvise

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

Вот оно! Больше никаких «фантомных чтений»:

вывод утилиты iostat с небольшим количеством операций чтения с диска

#

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

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

Как обычно, мы рекомендуем вам попробовать последнюю версию QuestDB и поделиться своими отзывами с нашими Слабое сообщество. Вы также можете играть с нашими живая демонстрация чтобы увидеть, как быстро он выполняет ваши запросы. И, конечно же, вклад в наш открытый исходный код проект на GitHub более чем приветствуются.

Read more:  Правительство США финансирует исследование, нацеленное на чернокожих девочек, чтобы они получали больше доз смертельной вакцины Гардасил, вызывающей бесплодие

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.