Про лимиты на CPU в kubernetes
Поговорим о лимитах на CPU в kubernetes: ка к работают, какие выставлять, нужны/не нужны и так далее.
Проблема
Мы с нашими клиентами часто сталкивались с ситуацией, когда контейнеру нужно значительно повышать лимиты на CPU, чтобы приложению в нём нормально работало и не теряло в производительности. И под "значительно повышать", я имею ввиду выставлять лимиты, в несколько раз превосходящие реквесты.
Интересное для пользователей/администраторов OpenShift | OKD кластеров
У вас есть приложение в контейнере, которое по графикам потребляет 50m CPU. При том, что лимит у него стоит в 100m, работает оно хреново. На графиках вы не видите, чтобы приложение упиралось в свой лимит по CPU и из-за этого не понимаете, в чем дело. Вы долго думаете, что делать, и решаете повысить лимит в 2 раза. О чудо! Производительность увеличилась в два раза! Но по графикам контейнер продолжает потреблять 50m CPU и не упирается в лимит. Как так?
Это происходит, потому что метрика (pod:container_cpu_usage:sum
), которую вам предоставляет OpenShift | OKD, сглаживает резкие скачки и падения потребления CPU. Метрика pod:container_cpu_usage:sum
это на самом деле агрегированный через prometheus rule запрос sum(rate(container_cpu_usage_seconds_total{container="",pod!=""}[5m])) BY (pod, namespace)
.
Ниже на скриншоте приведён пример потребления контейнера, в котором я запускал программу, которая сразу после запуска начинает потреблять все доступные ей ресурсы в рамках одного ядра. Запустил я её в ~13:08 и вместо того, чтобы увидеть максимальное потребление (в нашем случае 0.2CPU) в это время, я вижу, как потребление постепенно растёт, пока не достигает реального значения. Тоже самое происходит и после "убийства" процесса в ~13:23 – вместо того, чтобы моментально упасть до нуля, потребление плавно снижается.
Из-за этого на графиках в консоли OpenShift вы никогда не увидите краткосрочных скачков потребления. Например, если процесс на несколько секунд упрётся в лимит контейнера.
Дробные лимиты на CPU
Причина у нашей проблемы одна – дробные лимиты на CPU. О том, что это такое, как возникает и как лечить – расскажу ниже.
I. У приложения один поток
Давайте представим, что у вас в контейнере запущено приложение, которое под капотом имеет всего один поток. В таком случае, если вы этому контейнеру установите лимит меньше одного ядра, вы будете всегда получать задержки, и соответственно просадки в производительности приложения.
Почему это происхо дит? Смотрите, когда поток становится исполняемым, то есть подошла его очередь на выполнение в процессоре, оно получает период выполнения, равный 100мс, в рамках которого процесс внутри потока должен выполниться. В случае, если этого не происходит, поток возвращается обратно в очередь и ждет следующего периода. И так он будет получать эти периоды, пока процесс в потоке не завершится.
Длина периода – это настраиваемый параметр. По дефолту – 100мс.
Как работает лимит? Допустим, вы указали контейнеру лимит в 400m. В таком случае поток приложения в контейнере сможет потратить 40мс из 100мс своих периодов. Что мы после этого получаем? Ниже у меня приведён пример процесса в потоке, контейнеру которого выставлен лимит в 400m CPU. По факту на выполнение этому процессу требуется всего 120мс, но из-за установленного лимита он не может потреблять всё время своих периодов. После того, как он проработал свой максимум (40мс), ему приходится ждать своего следующего периода. Поэтому на картинке мы видим паузы (троттлинг) в работе процесса, из-за которых 120мс превратились в 235мс.
II. У приложения много потоков
Давайте теперь рассмотрим чуть более сложный вариант – когда у приложения несколько потоков. В таком случае лимит, который вы указали для контейнера, будет делиться поровну между активными потоками приложения.
Под "активными" потоками я имею ввиду те, что в данный момент выполняются в процессоре.
А если у вас лимит делится между активными, то существует большая вероятность того, что каждый из этих активных потоков по итогу получит дробный лимит (меньше одного).
Например, у вас есть приложение с двумя потоками, вы контейнеру этого приложения задаёте лимит в одно ядро. В случае, если работает только один поток, он заберёт себе лимит в одно ядро полностью и соответственно будет иметь доступ к своим 100мс периодам (то есть по сути лимита для него не будет).
В случае же, если будут одновременно работать сразу два потока, то лимит в одно ядро поделится между ними и каждый получит по 500m CPU. А это в свою очередь означает, что каждый сможет использовать только половину своих периодов, что приведёт к троттлингу.
Итого
Давайте подведём небольшие итоги по дробным лимитам.
- Лимит меньше одного ядра приложений с одним потоком всегда будет приводить к задержкам и, соответственно, к потерям по производительности. Просто потому, что приложение не сможет целиком использовать свой период.
- Для приложений с двумя и более потоками – анализируйте их загруженность. Например, есть приложение с двумя потоками, и эти потоки у него загруженны равномерно. Тут для получения максимальной производительности контейнеру нужно будет задавать лимит в два ядра, чтобы они поровну поделились между потоками и каждый получил по одному целому ядру. В случае же, если из этих двух потоков 90% времени работает только один, а второй выполняет фоновую активность, то контейнеру есть смысл ставить лимит в одно ядро, потому что большую часть времени работает из двух потоков только один.