Отдел микросервисов Netflix обратился ко мне за помощью с загадочной проблемой. Они заявили, что у них на сервере файловая система ZFS потребляет 30% CPU. Я рассказывал об этом на конференции Kernel Recipes 2017 — да, это старая история… но её стоит рассказать ещё раз.

Постановка задачи


Этот микросервис предназначен для сбора метрик. Недавно ребята обновились на новый образ ОС (BaseAMI) — и после этого заявили, что ZFS начала потреблять более 30% ресурса CPU. Первым делом я подумал, что они ошиблись: я лично разрабатывал системные компоненты ZFS в компании Sun Microsystems — и файловая система ну никак не могла настолько загружать процессор.

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

Мониторинг


Для начала я запустил инструмент облачного мониторинга Atlas, чтобы посмотреть показатели CPU в целом, включая разбивку процессорного времени в процентах на работу системы ('sys') и пользовательскую нагрузку ('usr').

К своему удивлению я обнаружил, что 38% процессорного времени действительно приходится на sys. Это очень необычно для облачных рабочих нагрузок Netflix — и вроде бы подтверждает заявление парней, что ZFS съедает CPU… но как такое возможно? Наверняка тут ошибка — и процессор нагружает некий другой процесс в ядре, а не ZFS.

Дальнейшие действия


Для более глубокого анализа я обычно подключаюсь к инстансам по SSH, так можно запустить mpstat(1) и ещё раз посмотреть разбивку usr/sys, а также perf(1) для первоначального профилирования и проверки, какие конкретно пути кода нагружают ядра CPU. Однако у Netflix есть очень удобные инструменты для построения flame-графики прямо из облачного UI (раньше это был Vector, теперь FlameCommander). Так что я сразу перешёл к делу. Для наглядности на КДПВ показан пользовательский интерфейс Vector и типичный flame-график для облачной нагрузки.

Обратите внимание, что в этом образце преобладает Java (показана зелёным). Вот увеличенный фрагмент:



Flame-график


Посмотрим график нагрузки на CPU одного из проблемных инстансов:



Сразу бросаются в глаза системные процессы. Это две оранжевые башни слева и справа. Жёлтый — это C++, а красный — прочий код пользовательского уровня.

Итак, посмотрим поближе на левую башню с нагрузкой от ядра:



Да это же поток arc_reclaim_thread! С этим кодом я колупался ещё в Sun. Так что здесь действительно ZFS, они правы!

Adapative Replacement Cache (ARC) в ZFS — это основной кэш памяти для файловой системы. Поток arc_reclaim_thread выполняет операцию arc_adjust() для удаления памяти из кэша, чтобы тот не слишком разрастался и чтобы освободить память для приложений. Процесс выполняется по расписанию или по вызову, в условиях нехватки памяти. Вообще-то я помню, что поток arc_reclaim_thread действительно может нагружать CPU в определённых условиях: если в файловой системе используется крошечный размер записи (например, 512 байт), что создаёт миллионы маленьких буферов. Но это по сути некорректная конфигурация ZFS. Размер записи по умолчанию составляет 128 КБ, и ни при каких условиях не следует указывать размер меньше 8 КБ.

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

Но главный вопрос: почему вообще используется ZFS?

Конфигурация


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

Я выложил ребятам свою теорию, уверенный, что нахожусь на правильном пути. Но они ответили: «Мы не используем контейнеры». Хорошо, тогда как вы используете ZFS? Я не ожидал такого ответа:

— Мы не используем ZFS.

Что!? Конечно используете. Я же вижу поток arc_reclaim_thread на flame-графике. Он не запускается просто так! Он может появиться на CPU только по одной причине: когда нужно удалить страницы памяти из кэша ARC. Если вы не используете ZFS, то в ARC не будет никаких страниц, поэтому нечего очищать, и этот процесс просто не запустится.

Но они были уверены, что вообще не используют ZFS. В таком случае график невозможно логично объяснить. Я должен был выяснить, каким образом всё-таки запускается ZFS, и показать им это.

cd и ls


Для ответов на все вопросы достаточно команд cd и ls(1). Первая обращается к файловой системе, а вторая показывает, что там находится. Имена файлов во многом объяснят, почему запускается эта файловая система.

Сначала посмотрим, где смонтированы системы ZFS:

df -h
mount
zfs list

Но результат нулевой! В данный момент не смонтировано ни одной файловой системы ZFS. Я попробовал другой инстанс — то же самое. Что за ерунда?

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

arcstats


Точно, мне помогут arcstats! Счётчики работают на уровне ядра и отслеживают статистику ZFS, включая попадания и промахи кэша ARC. Посмотрим на них:

# cat /proc/spl/kstat/zfs/arcstats
name                            type data
hits                            4    0
misses                          4    0
demand_data_hits                4    0
demand_data_misses              4    0
demand_metadata_hits            4    0
demand_metadata_misses          4    0
prefetch_data_hits              4    0
prefetch_data_misses            4    0
prefetch_metadata_hits          4    0
prefetch_metadata_misses        4    0
mru_hits                        4    0
mru_ghost_hits                  4    0
mfu_hits                        4    0
mfu_ghost_hits                  4    0
deleted                         4    0
mutex_miss                      4    0
evict_skip                      4    0
evict_not_enough                4    0
evict_l2_cached                 4    0
evict_l2_eligible               4    0
[...]

Невероятно! Все счётчики по нулям! ZFS действительно не использовалась, вообще ни разу! Но в то же время она съедала более 30% вычислительных ресурсов! Как???

Клиент, который впервые заметил проблему, оказался прав. ZFS прямо-таки пожирала процессор, причём без всякой причины.

Как файловая система, которая вообще не используется, может потреблять 38% CPU? Я такого никогда не видел… Загадка.

8. Анализ кода


Я внимательнее посмотрел график, а именно задействованные пути кода. И обратил внимание, что они ведут к get_random_bytes() и extract_entropy(). Это что-то новенькое, я такого не писал. Посмотрев исходный код и историю изменений, всё стало понятно.

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

Криптографически безопасный рандом.

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

В 2017 году я зафиксировал проблему в тикете #6531. Полагаю, что первым делом они внесли изменения в код arc_reclaim_thread, чтобы процесс не занимался выбором списка, когда ZFS не используется, а завершал работу. С тех пор код ARC изменяли многократно, и я больше не слышал об этой проблеме.



Комментарии (25)


  1. Junecat
    20.10.2021 09:38
    +7

    Обалденный перевод. читается круче любого детектива. и такая неожиданная развязка...


    1. A1EF
      21.10.2021 00:51

      Да, блог Брендана Грегга всегда интересно читать. Один легендарный пост про подсчёт load average чего стоит.


  1. gred
    20.10.2021 09:56
    +3

    хм. а теперь кто-нибудь объясните мне, зачем ZFS вообще была включена в ядре, если она не использовалась?


    1. tangro
      20.10.2021 11:14
      +5

      Как-будто вот все пересобирают под себя постоянно ядро, включая и выключая необходимые фичи. Тем более что фиг угадаешь какая фича будет необходима через полгода. Была себе там поддержка zfs - ну, пусть себе будет. Пока мы её не используем - она ведь не должна никак влиять, правда? Баг был именно в zfs, которая заставляла "платить" за себя даже того, кто её не использовал.


      1. Self_Perfection
        20.10.2021 12:20
        +1

        Да, возражений к тому, что бага в ZFS была — нет. Но всё же кажется её можно было обойти проще: rmmod zfs


        1. Nova_Logic
          20.10.2021 13:19

          лучше в блэклист добавить


          1. gred
            20.10.2021 13:21

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


            1. SergeyMax
              29.10.2021 11:38

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

              Потушены с оговоркой "на продакшене" -> гарантированно ловим баги, не воспроизводящиеся на "непродакшене". Не надо так.


    1. event1
      20.10.2021 18:30
      +2

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

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


  1. UrsusDominatus
    20.10.2021 10:37
    +2


  1. v1000
    20.10.2021 11:02

    Еще бы узнать, что вызывает на винде System Interrupts загрузку проца в 100%, когда система не используется. Не удивлюсь если там что-то похожее.


    1. easyman
      20.10.2021 12:58
      +1

      У меня после того как залил ноутбук помогает отключить контроллер USB в device manager


      1. v1000
        20.10.2021 18:05

        Ну это один из вариантов, который обычно предлагают, только вот как при этом работать на компе с USB клавиатурой и мышкой?


    1. vesper-bot
      20.10.2021 14:14

      Руткит с майнером?


      1. dartraiden
        20.10.2021 16:04
        +2

        Смех смехом, а недавно в одной железячной конфе был вопрос «помогите, у меня 3 из 4 ядер загружены на 100%, но как только открываю диспетчер задач, то всё по нулям». В итоге, человек нашёл у себя в системе сразу несколько майнеров.


        1. vesper-bot
          20.10.2021 19:35

          А это и не было смехом, кстати.


      1. v1000
        20.10.2021 18:04

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


        1. vesper-bot
          20.10.2021 19:37
          +1

          Тогда кривые/непатченные драйвера какого-то оборудования, Т.е. мало данных.


    1. mvv-rus
      20.10.2021 21:51
      +7

      Тут неплохо было бы уточнить, что такое System Interrupts.
      В Windows прерывания обычно обрабатываются в 2 стадии.
      Первая — это первоначальная обработка прерывания, на уровне IRQL устройства, блокирующая менее приоритетные прерывания. Руковдства рекомендуют делать ее максмально краткой, и она обычно ограничивается фиксацией состояния устройства и, если дальнешая обработка необходима — постановкой элемента для этого в очередь отложенной обработки (DPC). Процент времени процессора, приходящийся на эту стадию показывает счетчик монитора производительности %Interrupt time.
      Вторая — эта самая отложенная обработка, которая выполняет основную работу по обработке прерывания. Она производится на уровне IRQL DPC/Dispatch (на том же, на котором выполняется планировщик) при разрешенных прерываниях. И процент времени процессора, приходящийся на эту стадию показывает другой счетчик монитора производительности — %DPC time. Так что в системе есть два разных счетчика, но некоторые утилиты отображения информации могут их свести в один.
      Если велик именно %Interrupt Time, то дело — в слишком часто приходящих прерываниях (для скорости их прихода, кстати, тоже сеть счетчик). Это может быть неисправное устройство или, к примеру, сетевая карта в неизбирательном (promiscuous) режиме в хорошо загруженной сети.
      Как искать такоеустройство без отладчика (или последовательго отключения подозрительных устройств) я даже и не знаю.
      DPC же могут порождать не только прерывания, но и другие механизмы ядра. Там думать надо по месту.


      1. v1000
        21.10.2021 09:11

        А насколько эти события реально "грузят" процессор? То есть это виртуальная загрузка процессора или реальная? Потому что на моей системе вроде вполне реальная, потому что процессор греется также, как при реальной полной загрузке.


        1. mvv-rus
          21.10.2021 16:26

          Это вполне реальная загрузка: процессор в это время исполняет код.


        1. SergeyMax
          29.10.2021 11:52

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


  1. mvv-rus
    20.10.2021 21:54
    +3

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

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


    1. korus
      21.10.2021 07:55

      Потому что код криптографически безопасного генератора уже был под рукой


    1. anticyclope
      28.10.2021 11:00

      почему вообще надо что-то выбирать? может просто всегда первый?