Хабрахабр

[Перевод] Еще одна причина, почему тормозят Docker контейнеры

Сегодня хотелось бы продолжить разговор о короткой, но от того не менее интересной истории отладки, которая произошла совсем недавно. В последнем посте я рассказывал о Kubernetes, о том, как ThoughtSpot использует его для собственных нужд по поддержке разработки. К тому же наглядно показывается, как контейнеризированные процессы конкурируют за ресурсы даже при оптимальных ограничениях по cgroup и высокой производительности машины.
image Статья базируется на том, что containerization != virtualization.

Все бы ничего, да при запуске «докеризированного» приложения неожиданно сильно падала производительность. Ранее мы запускали серии операций, связанных с разработкой b CI/CD, во внутренем кластре Kubernetes. На виртуальной машине с такими параметрами все наши запросы из крошечного набора данных (10 Кб) для тестов летали бы. Мы не скупились: в каждом из контейнеров стояли ограничения по вычислительной мощности и памяти (5 CPU / 30 ГБ RAM), заданные через конфигурацию Pod. Запросы, которые раньше завершались за пару миллисекунд, теперь висели по 1–2 секунде, и это вызывало всевозможные сбои в CI-конвейере задач. Однако в Docker & Kubernetes на 72 CPU / 512 ГБ RAM мы успевали запустить 3–4 копии продукта, а потом начинались тормоза. Пришлось вплотную заняться отладкой.

Однако мы не нашли ничего, что могло бы вызвать хоть какое-либо замедление (если сравнивать с установками на голом железе или виртуальных машинах). Как правило, под подозрением — всевозможные ошибки конфигурации при упаковке приложения в Docker. Далее мы опробовали всевозможные тесты из пакета Sysbench. С виду все правильно. Некоторые сервисы нашего продукта хранят подробную информацию обо всех действиях: ее потом можно использовать в профилировании производительности. Проверили производительность ЦП, диска, памяти — все было таким же, как и на голом железе. Однако в данном случае ничего такого не произошло. Как правило, при нехватке какого-либо ресурса (ЦП, оперативной памяти, диска, сети) в некоторых вызовах отмечается значительный провал во времени — так мы обнаруживаем, что именно тормозит и где. Ничто не указывало на настоящий источник проблемы. Временные пропорции не отличались от исправной конфигурации — с той лишь разницей, что каждый вызов был значительно медленнее, чем на голом железе. Мы уже были готовы сдаться, как вдруг нашли вот это: https://sysdig.com/blog/container-isolation-gone-wrong/.

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

  1. Основная причина крылась в самом ядре Linux. Из-за структуры кэша-объектов dentry в ядре, поведение одного процесса сильно тормозило вызов ядра __d_lookup_loop, что прямым образом сказывалось на производительности другого.
  2. Автор использовал perf для обнаружения ошибки в ядре. Прекрасное средство отладки, которым мы никогда раньше не пользовались (а жаль!).

6. perf (иногда его называют perf_events или perf-инструменты; ранее был известен как Performance Counters for Linux, PCL) — это инструмент анализа производительности в Linux, доступный с версии ядра 2. Утилита управления пользовательским пространством, perf, доступна с командной строки и представляет собой набор подкоманд. 31.

Данное средство поддерживает счетчики производительности аппаратной и программной (например, hrtimer) платформы, точки трассировки и динамические пробы (скажем, kprobes или uprobes). Она осуществляет статистическое профилирование целой системы (ядра и пространства пользователя). В 2012 году два инженера IBM признали perf (наряду с OProfile) одним из двух наиболее используемых инструментов профилирования счетчиков производительности в Linux.

Мы же запускали сотни различных процессов в контейнерах, и во всех было одно и то же ядро. Вот мы и подумали: может, и у нас то же самое? Вооружившись perf, повторили отладку, и в итоге нас ждало преинтереснейшее открытие. Мы чуяли, что напали на след!

Ниже приведены записи perf первых 10 секунд ThoughtSpot, работающего на здоровой (быстрой) машине (слева) и внутри контейнера (справа).
image

Время, в основном, расходуется на пространство ядра, тогда как слева — большая часть времени идет на собственные процессы, выполняемые в пространстве пользователя. Сразу видно, что справа первые 5 вызовов связаны с ядром. Но самое интересное, что все время занимает вызов posix_fadvise.

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

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

Мы пользовались ей для проекта. Это сторонняя библиотека логирования под названием glog. Она вызывается для всех событий «запись лога в файл» (log to file), а многие экземпляры нашего продукта пишут лог довольно часто. Конкретно эта строка (в LogFileObject::Write), наверное, самый критический путь всей библиотеки. Беглый взгляд на исходный код подсказывает, что часть fadvise можно отключить, установив параметр --drop_log_memory=false:

if (file_length_ >= logging::kPageSize)
}

что мы, конечно же, сделали и… в яблочко!
image

Немножко погуглив, мы нашли вот что: https://issues.apache.org/jira/browse/MESOS-920 и еще это: https://github.com/google/glog/pull/145, что в очередной раз подтвердило нашу догадку об истинной причине торможения. То, что раньше отнималло пару секунд, теперь выполняется за 8 (восемь!) миллисекунд. Увеличив процессы логирования в 3–4 раза и выделив им одно общее ядро, мы увидели, что это действительно застопорило fadvise. Скорее всего, то же самое происходило и на виртуальной машине/голом железе, но так как у нас было по 1 копии процесса на каждую машину/ядро, то интенсивность вызова fadvise была значительно ниже, чем и объяснялось отсутствие дополнительного потребления ресурсов.

И в заключение:

А поскольку ядро — это архисложная структура, то сбои могут происходить где угодно (как, например, в __d_lookup_loop из статьи Sysdig). Информация эта не нова, но многие почему-то забывают главное: в случаях с контейнерами «изолированные» процессы конкурируют за все ресурсы ядра, а не только за ЦП, оперативную память, дисковое пространство и сеть. Они — отличный инструмент, решающий свои задачи. Это, правда, не говорит о том, что контейнеры хуже или лучше традиционной виртуализации. Кроме того, такие конфликты — отличная возможность для злоумышленников прорваться через «истонченную» изоляцию и создать скрытые каналы между контейнерами. Просто помните: ядро — это общий ресурс, и готовьтесь к отладке неожиданных конфликтов в пространстве ядра. Если планируете запускать высоконагруженные приложения в Docker, то обязательно выделите время на изучение perf. И, наконец, есть perf — отличное средство, которое покажет, что происходит в системе, и поможет отладить любые проблемы с производительностью.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть