Как стать автором
Обновить
Флант
DevOps-as-a-Service, Kubernetes, обслуживание 24×7

Глубокое погружение в запросы, лимиты и специфику использования CPU в Kubernetes

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров3.4K
Автор оригинала: Джон Такер (John Tucker)

Перевели статью, посвящённую разъяснению концепции запросов/лимитов на ресурсы контейнеров в K8s, вокруг которых существует множество заблуждений.

Процессор, потоки и квантование времени

Сначала разберёмся с базовыми понятиями.

Центральный процессор (ЦП; также центральное процессорное устройство — ЦПУ; англ. central processing unit, CPU) — электронный блок либо интегральная схема, исполняющая машинные инструкции (код программ), главная часть аппаратного обеспечения компьютера или программируемого логического контроллера.

Процесс — это совокупность взаимосвязанных и взаимодействующих действий, которые преобразуют входящие данные в исходящие. Компьютерная программа сама по себе — лишь набор последовательных инструкций. В то время как процесс — непосредственное выполнение этих инструкций.

Поток выполнения (тред; от англ. thread — нить) — наименьшая единица обработки, исполнение которой может быть назначено ядром операционной системы.

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

А теперь небольшая диаграмма, которая поможет разобраться с квантами времени:

Здесь изображён компьютер с одним CPU и двумя процессами с одинаковым приоритетом. У одного процесса три потока: два с инструкциями, ожидающими выполнения, а третий заблокирован. У другого процесса один поток с инструкциями, ожидающими выполнения.

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

Потоки с ожидающими выполнения инструкциями планируются в CPU для выполнения в течение соответствующего временного среза (здесь 100 мс). В случае процессов с одинаковым приоритетом временной срез поровну делится между ними. Потоки с инструкциями, ожидающими выполнения, в первом процессе затем поровну делят время, отведённое этому процессу. После чего цикл повторяется.

Примечание
В Linux обычно используются временные срезы по 100 мс. См. /proc/sys/kernel/sched_rr_timeslice_ms.

Несколько важных моментов:

  • Компьютер с несколькими процессорами работает по схожему принципу. Потоки выполняются в течение выделенных им временных интервалов во временном срезе на одном из процессоров. В определённом временном срезе конкретный поток может работать только на одном процессоре.

  • В нашем примере после завершения временного среза у всех трёх неблокированных потоков остаются невыполненные инструкции. Они будут запланированы для выполнения в следующем временном срезе.

Базовый пример

В этой статье я использую тестовое приложение, написанное на Go, с HTTP-эндпойнтом на порте 8080. Оно нагружает процессор в течение 50 мс, после чего возвращает ответ. Само приложение упаковано в контейнер и работает в поде в кластере Google Kubernetes Engine (GKE).

В качестве инструмента нагрузочного тестирования используется wrk, который запускается на отдельном инстансе и генерирует HTTP-трафик на тестовое приложение.

В этом базовом примере:

  • тестовому приложению выделены 2 CPU;

  • оно настроено на использование двух потоков;

  • нагрузочное приложение генерирует некоторый объём HTTP-трафика (один поток с одним соединением в течение 5 минут, или 20 запросов в секунду).

Ниже показано скользящее одноминутное среднее значение потребления CPU (синяя линия), которое значительно ниже двух доступных CPU (красная линия).

Средняя задержка составляет 50,7 мс, максимальная — 55,99 мс (практически совпадает с 50 мс, которые тестовое приложение «работает», прежде чем отправить ответ).

Диаграмма ниже иллюстрирует этот пример: компьютер с 2 CPU, одним процессом с 2 потоками (каждый с ожидающими выполнения инструкциями):

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

Примечание

Наши диаграммы не показывают, когда конкретно выполняются инструкции во временном срезе.

Слишком мало потоков

В этом примере у приложения слишком мало потоков по сравнению с имеющимся количеством CPU:

  • тестовому приложению выделены 2 CPU;

  • в этот раз тестовое приложение будет использовать только один поток (единственное отличие от базового примера);

  • нагрузочное приложение будет генерировать приемлемый объём HTTP-трафика (один поток с одним соединением в течение 5 минут, или 20 запросов в секунду).

Ниже показано скользящее одноминутное среднее значение использования CPU (синяя линия), которое значительно ниже 2 доступных CPU (красная линия):

Видно, что при нагрузке средняя задержка составила 62,44 мс, максимальная — 99,30 мс, то есть производительность значительно снизилась по сравнению с базовым примером.

Диаграмма ниже иллюстрирует этот пример: компьютер с 2 CPU, одним процессом с одним потоком с инструкциями, ожидающими выполнения:

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

Общая идея состоит в том, что потоков должно быть не меньше, чем CPU, выделенных приложению. Избыток потоков не так сильно сказывается на производительности, но есть примеры (например, некоторые рабочие нагрузки, написанные на Go), когда влияние всё же заметно.

Запросы и лимиты CPU в Kubernetes

Теперь пришло время поговорить ещё о паре концепций.

Когда вы указываете запрос на ресурсы для контейнеров в поде, kube-scheduler использует эту информацию, чтобы решить, на какой узел запланировать под. В свою очередь, если для контейнера указан лимит ресурсов, kubelet следит за тем, чтобы контейнер не потреблял больше ресурсов, чем установленный пользователем лимит. Кроме того, kubelet резервирует запрашиваемый объём того или иного системного ресурса для соответствующего контейнера.

и

Если узел, на котором запущен под, располагает достаточным количеством ресурсов, контейнер может (это не запрещено) использовать больше ресурса, чем указано в его запросе на этот ресурс.

и

Лимиты на ресурсы CPU обеспечиваются дросселированием процессора. Когда контейнер приближается к лимиту на ресурсы CPU, ядро ограничивает для него доступ к процессору. Таким образом, лимит на ресурсы CPU — это жёсткое ограничение, накладываемое ядром. Контейнеры не могут использовать больше ресурсов процессора, чем указано в их лимите.

Управление ресурсами подов и контейнеров

Примечание
В предыдущих примерах запрос контейнера на ресурсы процессора (и по совместительству лимит) равнялся 2 CPU.

Кроме того (хотя это может быть и очевидным), отсутствие лимита на ресурсы процессора эквивалентно установке бесконечно высокого лимита CPU.

Высокий уровень использования CPU (на помощь приходят высокие лимиты)

В этом примере нагрузка велика, но высокий лимит CPU предотвращает проблемы с производительностью. Мы:

  • запрашиваем (request) 2 CPU для тестового приложения;

  • ставим лимит на 4 CPU;

  • настраиваем тестовое приложение на использование четырёх потоков (то есть оно может использовать все 4 процессора)

  • настраиваем нагрузочное приложение на генерирование большого объёма HTTP-трафика (один поток с тремя подключениями в течение 5 минут, или 60 запросов в секунду).

Ниже показано скользящее одноминутное среднее значение использования CPU (синяя линия), которое выше двух запрошенных CPU (зелёная линия), но значительно ниже лимита в 4 CPU (красная линия):

В данном случае средняя задержка составила 51,21 мс, максимальная — 68,88 мс (опять практически совпадает с 50 мс, которые тестовое приложение ждёт, прежде чем вернуть ответ).

Диаграмма ниже иллюстрирует этот пример: компьютер с 4 CPU, одним процессом с четырьмя потоками (каждый с ожидающими выполнения инструкциями).

Примечание
Здесь иллюстрируется ситуация, когда на компьютере (фактически на узле Kubernetes) нет других процессов, потребляющих значительные ресурсы процессора.

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

Высокая загрузка процессора (высокие лимиты на CPU не помогают)

В этом примере нагрузка велика, при этом высокий лимит CPU не способен предотвратить проблемы с производительностью. Мы:

  • запрашиваем (request) 2 CPU для тестового приложения;

  • ставим лимит на 4 CPU;

  • настраиваем тестовое приложение на использование четырёх потоков (то есть оно может использовать все 4 процессора)

  • настраиваем нагрузочное приложение на генерирование большого объёма HTTP-трафика (один поток с тремя подключениями в течение 5 минут, или 60 запросов в секунду).

После этого запускаем на той же машине второе приложение, которое: 

  • получает 1660m CPU (запрос) и такой же лимит (на узле с 4 CPU);

  • работает в два потока (то есть нагружает два CPU);

  • настроено на генерирование большого объёма HTTP-трафика (один поток с двумя подключениями на 30 минут, или 40 запросов в секунду).

Ниже показано скользящее одноминутное среднее значение использования CPU (синяя линия), которое чуть выше двух запрошенных CPU (зелёная линия), но значительно ниже лимита в 4 CPU (красная линия):

В то же время второе приложение использует около 1,6 CPU, что соответствует его запросу и лимиту CPU:

В данном случае средняя задержка составила 66,11 мс, максимальная — 94,39 мс (значительно выше 50 мс, которые тестовое приложение ждёт, прежде чем вернуть ответ).

Диаграмма ниже иллюстрирует этот пример: компьютер с 4 CPU и двумя процессами. Первый — с четырьмя активными потоками, второй — с двумя активными потоками:

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

Ключевой вывод из последних двух примеров заключается в том, что установка высокого лимита CPU имеет смысл, когда на компьютере (или узле Kubernetes) нет других процессов, активно использующих ресурсы CPU.

Прерывистая нагрузка на процессор (низкие лимиты процессора в помощь)

В этом сценарии установка низкого лимита CPU (по сути, равного запросу) решает проблему. Мы:

  • запрашиваем (request) 2 CPU для тестового приложения;

  • ставим лимит на 2 CPU;

  • настраиваем его на использование двух потоков;

  • настраиваем нагрузочное приложение на периодическое генерирование большого объёма HTTP-трафика (один поток с четырьмя подключениями, или 80 запросов в секунду). Оно будет генерировать трафик в течение 10 секунд, затем ждать 50 секунд. Всё это на протяжении 5 минут.

Ниже показано скользящее одноминутное среднее значение использования CPU (синяя линия), которое значительно ниже двух доступных запросов/лимитов CPU (зелёная/красная линия):

Средняя задержка составила 82,40 мс, максимальная — 122,02 мс (значительно выше 50 мс, которые тестовое приложение ждёт, прежде чем вернуть ответ).

Диаграмма ниже иллюстрирует этот пример: компьютер с четырьмя CPU, одним процессом с двумя активными потоками (для 10 секунд, в течение которых поступал трафик):

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

Однако в течение следующих 50 секунд трафика нет:

Кажущееся противоречие заключается в том, что график потребления ресурсов процессора говорит об отсутствии проблемы, но задержки определённо на неё намекают. Ключевым моментом для понимания этого является то, что метрика использования CPU представляет собой скользящее одноминутное среднее значение потребления ресурсов процессора. То есть 10 секунд высокой нагрузки плюсуются к 50 секундам нулевой — среднее значение получается низким.

Поскольку был установлен низкий лимит (равный запросу), когда нагрузка исчерпала отведённые ей ресурсы процессора, она также достигла и лимита. Как мы помним:

Лимиты на ресурсы CPU обеспечиваются дросселированием процессора.

То есть в данном случае стоит учитывать уже две метрики контейнера:

container_cpu_cfs_periods_total — количество «периодов принудительного исполнения» (enforcement periods).

container_cpu_cfs_throttled_periods_total — количество периодов, в течение которых происходил троттлинг.

Отношение этих метрик (за минуту) даёт процент временных срезов, в которых у потоков контейнера (процесса) оставались невыполненные инструкции, которые пришлось планировать на следующий временной срез.

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

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

Заключение

Из этой довольно длинной статьи можно сделать некоторые выводы:

  • Важно, чтобы число потоков было не меньше, чем количество запросов CPU.

  • Хорошей практикой является приравнивание лимитов контейнера к его запросам.

Второй вывод подтверждает и автор другой статьи:

Если запросы контейнеров на ресурсы CPU занижены и они намного меньше лимитов (или лимиты вообще не установлены), производительность приложения будет зависеть от того, насколько велика конкуренция за ресурсы CPU на узле в каждый момент времени. Если приложение стабильно потребляет больше ресурсов CPU, чем значится в его запросе, всё будет хорошо до тех пор, пока этих ресурсов в избытке. Однако его производительность начнёт страдать, если на тот же узел будет запланирован набор новых подов с более высокими потребностями в ресурсах СPU, и доступные на узле вычислительные мощности сократятся.

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

Лимиты и запросы CPU в Kubernetes: глубокое погружение

P. S.

Читайте также в нашем блоге:

Теги:
Хабы:
+16
Комментарии3

Публикации

Информация

Сайт
flant.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Александр Лукьянов