Это первое сообщение из серии, состоящей из двух частей.
В этом году мы с товарищами по команде решили проблему с дросселированием ЦП, которая затрагивает почти все оркестраторы контейнеров с жесткими ограничениями, включая Kubernetes, Docker и Mesos. При этом мы снизили задержку ответа в худшем случае в одном из приложений Indeed с более чем двух секунд до 30 миллисекунд. В этой серии из двух частей я расскажу о нашем пути к поиску первопричины и о том, как мы в конечном итоге пришли к решению.
Проблема возникла в прошлом году, вскоре после выпуска ядра Linux v4.18. Мы увидели увеличение времени отклика для наших веб-приложений, но когда мы посмотрели на использование ЦП, все выглядело нормально. При дальнейшем исследовании стало ясно, что частота высокого времени отклика напрямую коррелирует с периодами сильного троттлинга ЦП. Что-то было не так. Нормальное использование ЦП и сильное троттлинг недопустимы. В конце концов мы нашли виновника, но сначала нам нужно было понять механизмы работы.
Предыстория: как работают ограничения ЦП контейнера
Почти все оркестраторы контейнеров полагаются на механизмы группы управления ядром (cgroup) для управления ограничениями ресурсов. Когда жесткие ограничения ЦП установлены в оркестраторе контейнеров, ядро использует Полностью справедливый планировщик (CFS) контроль пропускной способности Cgroup для обеспечения соблюдения этих ограничений. Механизм управления пропускной способностью CFS-Cgroup управляет распределением ЦП с использованием двух параметров: квоты и периода. Когда приложение использовало выделенную квоту ЦП в течение определенного периода, оно будет ограничено до следующего периода.
Все показатели ЦП для контрольной группы находятся в /sys/fs/cgroup/cpu,cpuacct/<container>
. Настройки квоты и периода находятся в cpu.cfs_quota_us
и cpu.cfs_period_us
.
Вы также можете просмотреть показатели регулирования в cpu.stat
. Внутри cpu.stat
вы найдете:
nr_periods
- количество периодов, в течение которых любой поток в контрольной группе был запущенnr_throttled
- количество периодов выполнения, в течение которых приложение использовало всю свою квоту и было задушеноthrottled_time
- общее количество времени, в течение которого отдельные потоки в контрольной группе были ограничены
Во время нашего исследования регрессии времени отклика один инженер заметил, что приложения с медленным временем отклика подвергались чрезмерному регулированию периодов (nr_throttled
). Мы разделили nr_throttled
на nr_periods
, чтобы найти критически важный показатель для выявления чрезмерно ограниченных приложений. Мы называем эту метрику «задушенный процент». Нам не понравилось использовать throttled_time
для этой цели, потому что он может сильно различаться между приложениями в зависимости от степени использования потоков.
Концептуальная модель ограничений ЦП
Чтобы увидеть, как работают ограничения ЦП, рассмотрим пример. Однопоточное приложение выполняется на ЦП с ограничениями контрольной группы. Этому приложению требуется 200 миллисекунд времени обработки для выполнения запроса. Без каких-либо ограничений график его ответа будет выглядеть примерно так.
Теперь предположим, что мы назначаем приложению лимит ЦП в 0,4 ЦП. Это означает, что приложение получает 40 миллисекунд времени выполнения за каждые 100 миллисекунд - даже если у ЦП нет другой работы. Запрос 200 мс теперь занимает 440 мсек.
Если мы собираем метрики за время 1000 мс, статистика для нашего примера будет следующей:
nr_periods
- 5; от 440 мс до 1000 мс приложение не имеет никакого отношения к работе и поэтому не запускаетсяnr_throttled
- 4; приложение не ограничивается в пятом периоде, потому что оно больше не работает.throttled_time
- 240 мс; для каждых 100 мсек приложение может работать только 40 мсек и ограничивается на 60 мсек. Он был снижен на 4 периода, поэтому 4, умноженные на 60, равняются 240 мс.throttled_percentage
- 80%; 4nr_throttled
разделить на 5nr_periods
.
Но это на высоком уровне, а не в реальной жизни. У этой концептуальной модели есть несколько проблем. Во-первых, мы живем в мире многоядерных, многопоточных приложений. Во-вторых, если бы все это было полностью правдой, наше проблемное приложение не должно было подвергнуться дросселированию до того, как исчерпала свою квоту ЦП.
Воспроизведение проблемы
Мы знали, что сжатый воспроизводящий тестовый пример поможет убедить сообщество разработчиков ядра в том, что проблема действительно существует и ее необходимо исправить. Мы попробовали несколько стресс-тестов и сценариев Bash, но изо всех сил пытались надежно воспроизвести поведение.
Наш прорыв произошел после того, как мы посчитали, что многие веб-приложения используют асинхронные рабочие потоки. В этой потоковой модели каждому рабочему дается небольшая задача. Например, эти рабочие могут выполнять операции ввода-вывода или другой небольшой объем работы. Чтобы воспроизвести этот тип нагрузки, мы создали небольшой репродуктор на языке C под названием Fibtest. Вместо использования непредсказуемого ввода-вывода мы использовали комбинацию последовательности Фибоначчи и сна, чтобы имитировать поведение этих рабочих потоков. Мы разделяем их между быстрыми потоками и медленными рабочими потоками. Быстрые потоки проходят через максимально возможное количество итераций последовательности Фибоначчи. Медленные потоки выполняют 100 итераций, а затем спят в течение 10 мс.
Для планировщика эти медленные потоки действуют так же, как асинхронные рабочие потоки, поскольку они выполняют небольшой объем работы, а затем блокируются. Помните, наша цель не состояла в том, чтобы произвести наибольшее количество итераций Фибоначчи. Вместо этого нам нужен был тестовый пример, который мог бы надежно воспроизвести большое количество троттлинга при одновременной низкой загрузке ЦП. Прикрепив эти быстрые и медленные потоки каждый к своему собственному процессору, мы наконец получили тестовый пример, который мог воспроизвести поведение троттлинга процессора.
Первое исправление / регресс дросселирования
Нашим следующим шагом было использование Fibtest в качестве условия для запуска git bisect в ядре. Используя эту технику, мы смогли быстро обнаружить фиксацию, которая привела к чрезмерному дросселированию: 512ac999d275« sched / fair: Fix condition drift clock таймера полосы пропускания ». Это изменение было внесено в ядро 4.18. Тестирование ядра после удаления этой фиксации решило нашу проблему низкой загрузки ЦП с высоким троттлингом. Однако, когда мы проанализировали фиксацию и связанные с ней источники, исправление выглядело вполне корректным. И что еще более сбивает с толку, эта фиксация также была введена для исправления непреднамеренного удушения.
Проблема, исправленная этой фиксацией, была проиллюстрирована регулированием, которое, по-видимому, не коррелировало с фактическим использованием ЦП. Это произошло из-за разницы в тактовой частоте между ядрами, что привело к преждевременному истечению квоты ядром на определенный период.
К счастью, эта проблема возникала гораздо реже, поскольку на большинстве наших узлов были запущены ядра, в которых уже было исправление. Однако одно неудачное приложение столкнулось с этой проблемой. Это приложение в основном простаивало, и ему было выделено 4,1 ЦП. Полученные в результате графики использования ЦП и процента дроссельной заслонки выглядели следующим образом.
Commit 512ac999d275 устранил проблему и был перенесен на многие стабильные деревья Linux. Фиксация была применена к большинству основных ядер дистрибутивов, включая RHEL, CentOS и Ubuntu. В результате некоторые пользователи, вероятно, заметили улучшения регулирования. Однако многие другие, вероятно, видят проблему, которая положила начало этому расследованию.
На этом этапе нашего пути мы обнаружили серьезную проблему, создали репродуктор и определили причинную фиксацию. Эта фиксация выглядела полностью правильной, но имела некоторые отрицательные побочные эффекты. Во второй части этой серии я объясню основную причину, обновлю концептуальную модель, чтобы объяснить, как на самом деле работают ограничения ЦП CFS-Cgroup, и опишу решение, которое мы в конечном итоге внедрили в ядро.
Размещено на сайте Indeed Engineering Blog.