Bcache: в прод!
Опыт использования bcache в проде. О багах, тормозах и метриках.
Что это?
Bcache — это модуль ядра Linux, который позволяет объединять несколько дисков в один виртуальный диск, состоящий из кэшируемых и кэширующих дисков. Обычная история — кэширование здоровенного хдд на разделе nvme. Мы использовали такие диски под ceph-osd.
На bcache заехали не сразу, сначала в проде использовали флешкэш, со временем стало понятно, что с ним много геморроя.
Флешкэш по архитектуре — множественно-ассоциативный кэш (set associative cache). Он даёт вытесняющие промахи при нагрузке на запись во writeback-режиме. А нам как раз был нужен хороший врайтбэк.
Bcache сохраняет данные в дерево, поэтому вытесняющего промаха не может случиться по определению, а скорость врайтбэка можно регулировать. Помимо этого флешкэш устаревал, его стало трудно поддерживать. В итоге, выбрали bcache на замену. Подробнее сравнение можно почитать тут.
Ожидания
Я ожидал получить идеальный врайтбэк-кэш. Отсутствие промахов на запись, регулируемая скорость врайтбэка. Звучит многообещающе: в идеальной ситуации нагрузка на запись вообще не будет лагать и латенси будет уровня кэширующего устройства.
При этом у нас будет возможность регулировать нагрузку на медленный диск, а значит можно получить хоть немного ожидаемую задержку при кэш-промахе на чтение.
Реальность
Bcache действительно оказался топ.
Настройки
Так получилось, что bcache имеет огромное количество недокументированных настроек. В какой-то момент разработчики забили на описание новых крутилок в документации, поэтому за описанием придется идти в исходный код. Сами настройки доступны в sysfs.
Задача – свести кэш-промахи на запись к нулю. Bcache умеет прокидывать запись на кэшируемый диск во врайтбэк режиме в двух случаях. Во-первых, если идет последовательная запись превышающая порог в Мб. Во-вторых, если кэширующее устройство начало показывать задержку выше заданного лимита. Обе настройки отключаются.
Bcache ускоряет врайтбэк автоматически, после перехода количества грязных блоков через лимит. Причем рост скорости регулируется пропорционально-интегральным контроллером.
Подняв минимальную скорость врайтбэка, удаэтся уменьшить количество грязных блоков и иметь некоторый запас места до включения в работу пропорционально-интегрального контроллера. Большую часть времени нагрузка на запись на кэшируемый диск статична и аккуратно возрастает только после серьезных пиковых нагрузок.
Статистика
Вся статистика работы есть в sysfs каждого bcache-девайса. Централизованно собирать данные помогает node-exporter со встроенным bcache-коллектором.
Статистики довольно много: информация о кэш-попаданиях и промахах, количество “проброшенных” запросов на медленный диск, статистика работы btree и журнала. Я добавил статистику из файла bcache/writeback_rate_debug
. В нем все самое интересное про врайтбэк — текущий рейт, значения пропорциональных и интегральных коэффициентов, количество грязных блоков и заданный порог. По этим данным можно оценить профиль нагрузки и эффективность работы пропорционально-интегрального контроллера врайтбэка.
У нас использовалось много bcache-девайсов, иногда больше 24х на сервер. И мы столкнулись с тем, что bcache-коллектор лагает при таком количестве bcache-девайсов. Оказалось, что проблема в файле со статистикой priority_stats
. Файл читается не быстро, и в коде используется вызов cond_resched()
. Из-за того, что таких файлов много (24 диска = 24 файла), при чтении каждого из них вызывается cond_resched()
и возникает лавинообразный эффект, в результате которого мы за десяток секунд не успевали получить оттуда от коллектора. Эту проблему исправили через отключение парсинга priority_stats
по-умолчанию. Если использовать мало bcache-девайсов на сервер, то с этим не столкнуться.
Без нюансов не обошлось. Bcache округляет данные в файлах со статистикой. Например, вместо использования байтов при больших значениях переходит на мегабайты и гигабайты. В свою очередь node-exporter переводит данные обратно в байты, соответственно в итоговой статистике теряется точность. Некритично, но и не приятно.
В статистике есть момент неопределенности. Файл cache_available_percent
содержит свободный объем в кэше, не занятый грязными данными. При этом количество грязных данных из файла writeback_rate_debug
с ним не коррелирует. Дело в том, что первый использует для статистики количество грязных бакетов, а второй количество секторов. Чуть ниже я расскажу в чем разница, с этим связан один очень интересный баг.
Проброс flush
Когда на bcache во врайтбэк режиме приходит операция сброса кэша, он пробрасывает эту операцию на медленный диск.
В ядре это выглядит как запрос на запись данных с флагом
REQ_PREFLUSH
. Когда такой запрос приходит на диск, ядро генерирует запрос на сброс кэша (flush cache) устройства и отправляет его до того, как отправить запрос на запись с данными. Если в устройстве нет кэша, то ядро проигнорирует этот флаг и просто скинет данные на диск. Вызовыfsync()/fdatasync()
со стороны приложения транслируются в пустойREQ_PREFLUSH
запрос. Детали можно почитать в комментарии в коде ядра здесь.
Зачем это сделано не очень понятно. Как результат, при большом количестве синхронных операций записи, кэш-промахи обрабатываются медленнее, чем могли бы.
Баг с пробросом данных
Если поискать информацию о bcache в интернете, то периодически встречаются жалобы на то, что он начинает прокидывать запись на медленный диск, не смотря на то, что люди выкрутили все возможные крутилки, чтобы этого не происходило.
Этой проблемы сейчас нет. Она исправлена интересным фиксом. На багзилле можно почитать детали расследования.
Bcache оценивает количество грязных данных двумя способами. Внутри есть такое понятие, как бакет. Каждый бакет по-умолчанию содержит 1024 сектора, а в секторах хранятся данные. Bcache считает бакет грязным, если в нем есть хотя бы один грязный сектор. В то же время, он считает количество грязных байт, оценивая количество грязных секторов.
Так вот, при работе врайтбэка оценивается количество грязных секторов. Целевой показатель количества грязных байтов рассчитывается именно на основе количества секторов.
На врайтбэк кэш нельзя писать бесконечно с высокой скоростью. Сброс данных происходит не быстрее скорости работы медленного кэшируемого диска, поэтому кэш может переполнится грязными блоками. У bcache есть механика, предотвращающая полное заполнение кэша грязными данными. Работает просто: при достижения константного процента грязных бакетов запись начинает пробрасываться на медленный диск, минуя кэш. Обратите внимание, что здесь проверяется именно количество грязных бакетов, а не секторов.
Оказалось, что при определенном виде нагрузки грязные секторы так распространяются по бакетам, что ускорять врайтбэк рейт еще рано, потому что грязных секторов мало. Но грязных бакетов достаточно, чтоб включилась спасительная механика и запись начала пробрасываться на медленный диск. Мне так и не удалось понять, почему в этих двух механизмах используется разная статистика. Если кто-то знает — расскажите, очень интересно:)
Сейчас этот баг пофиксил разработчик из canonical. Теперь подсчитывается фрагментация грязных данных и при высоком проценте начинает увеличиваться рейт врайтбэка.
Теоретическая прблема
Есть одна проблема, описанная в этом треде списка рассылки оказалась теоретическая.
Когда приложение отправляет запрос на запись, после которого инициирует сброс кэша через fsync()/fdatasync(), bcache записывает данные в кэш и отправляет пустой запрос на сброс кэша медленному диску. Об этом я уже писал выше. А потенциальная проблема, о которой говорит автор из списка рассылки, в том, что при врайтбэке не используются флаги инициирующие сброс кэша на диске. Т.е. данные, которые приложение хочет иметь в персистентном сторадже, при врайтбэке могут оказаться в неперсистентном кэше медленного диска.
Я не сталкивался с этой проблемой, хотя масштаб инсталляции был не маленький: несколько сотен хостов постоянно использовали десятки кэшей.
В прод!
Я написал эту статью с точки зрения эксплуатации. В эксплуатации не бывает идеальных решений и я специально обратил внимание на неочевидные и проблемные нюансы.
Bcache – отличный выбор за свои деньги, если нужен персистентный врайтбэк-кэш. Он хорошо держит нагрузку, скорость врайтбэка регулируется плавно и зависит от количества грязных блоков. Драйвер отдает много статистики и ее достаточно для профилирования и отладки в большинстве ситуаций.
Проект все еще жив и пользуется популярностью, несмотря на то, что документация отстает от кода.