Всем привет! Продолжаю делиться наработками по Yandex Cloud, которые мы успешно реализовали в Спортсе”. Ранее я писал о Gitlab-раннерах и fleet-плагине, а также об утилите ycqouter. В этой статье расскажу о том, как значительно сократить расходы на manage k8s путем внедрения "прерываемых" нод без потери производительности и головной боли.
Как и большинство компаний мы активно используем kubernetes, а поскольку находимся в Облаке, то для удобства перешли на management k8s. После полного переезда именно куб стал основной статьей расхода в биллинге Яндекс Облако. Поэтому стало актуально найти способ снизить стоимость без потери в производительности.
Первый подход к решению проблемы
Самое очевидное решение лежало на поверхности – использовать "прерываемые" виртуальные машины, которые стоят до 70% дешевле. Но как мы знаем, их отличает один нюанс – они обязательно выключаются раз в сутки. Эта особенность казалась не критичной, ведь в managed k8s ноды запускаются в рамках instance group, а они умеют самостоятельно запускать "прерываемые" ВМ. Казалось бы, радость близко, но все разбилось о реальность.
После первых попыток мы выяснили, что если создать instance group из прерываемых машин, то в какой-то момент они всей толпой уходят в "отключку", а это влечет за собой сбои в работе кластера целиком. Получается, что в момент, когда большая часть нод уходит в офлайн, множество подов не могут разместиться из-за нехватки ресурсов. В дополнение к этому появляется большое количество подов в статусе Unknown и Pending. Это связано с тем, что "прерываемые" виртуальные машины выключаются без предупреждения и k8s не успевает корректно пересоздать поды на других нодах.
Переход к своему оператору
Мы попробовали несколько подходов с использованием стандартных средств Облака, но ничего не давало нужной стабильности. В итоге мы решили взять на себя остановку и пересоздание "прерываемых" нод. Результатом работы стал k8s-оператор, который и позволил нам перейти на "прерываемые" виртуальные машины и не нести потери в производительности.

Общий принцип работы достаточно прост, но при этом максимально надежен.

Оператор отслеживает все ноды и собирает те, которые имеют определенные лейблы, в нашем случае
yandex.cloud/preemptible: "true"и составляет список.-
Следующим шагом выбирает ноду, которую нужно вывести из нагрузки, основываясь на трех параметрах:
Максимальное время жизни (21 час)
Минимальное время жизни (12 часов)
Окно обслуживания (с 00:00 до 07:00)В самом простом сценарии мы выводим ноду, которая живет больше 21 часа. Однако это может вылиться в то, что в рабочее время большое количество нод будут перезапускаться. Поэтому было добавлено "окно обслуживания" – с его помощью ночью мы пересоздаем большую часть кластера в относительно спокойное время.
После того как мы определились с нодой, которая должна следующей уйти из кластера, мы навешиваем на нее cordon. Таким образом на нее больше не будут планироваться новые поды. Далее оператор начинает выселять(evicted) все поды по очереди и ждать, когда нода станет совсем пустой (за исключением служебной нагрузки).
Все остальное мы отдаем на откуп Cluster autoscaling – он видит, что нода больше не используется и удаляет ее, а при необходимости создает новую, свежую ноду.
Кстати, побочным и очень приятным эффектом является уплотнение кластера. В процессе evicted подов cluster scheduler распределяет поды по нодам и только после этого создает новые.
Все это оператор делает непрерывно и понемногу обновляет все "прерываемые" ноды. Именно благодаря этому мы в один момент теряем не более одной ноды, а плавный вывод подов почти не влияет на производительность приложений в кластере.
Ограничения и рекомендации
Есть несколько важных моментов, которые стоит учитывать:
Лучше всего переносить на прерываемые ноды deployment'ы с несколькими репликами и сервисы без состояния.
Нежелательно размещать на них базы данных, очереди, stateful-сервисы – им нужна выделенная группа обычных ВМ.
Размер instance group нужно увеличить. Поскольку между cordon и удалением ноды сluster autoscaling может пройти порядка 30 минут, нам нужен запас. К примеру, если раньше у нод-группы был максимальный размер в 15 виртуальных машин, то его придется увеличить до 17-18.
Результат
И конечно всем интересно, а сколько же можно сэкономить с таким подходом. Приведем небольшую табличку до и после.
До использования оператора:
Характеристики |
Прерываемая |
Количество |
Цена |
|
|---|---|---|---|---|
NodeGroup 1 |
vCPU 8, RAM 16 |
No |
15 |
169 125 |
NodeGroup 2 |
vCPU 24, RAM 48 |
No |
1 |
33 794 |
202 919 |
После использования оператора:
Характеристики |
Прерываемая |
Количество |
Цена |
|
|---|---|---|---|---|
NodeGroup 1 |
vCPU 8, RAM 16 |
No |
3 |
33 825 |
NodeGroup 2 |
vCPU 8, RAM 16 |
Yes |
15 |
43 155 |
NodeGroup 2 |
vCPU 24, RAM 48 |
No |
1 |
33 794 |
110 774 |
Как видно, мы сократили расходы на кластер почти на 50%, что очень хорошо с учетом того, что у нас далеко не один такой кластер. Но и на этом мы останавливаться не собираемся. В планах есть улучшения, например, автоматическая перебалансировка и подбор нод (аля-karpenter). Если у вас есть похожие наработки, то поделитесь ими в комментариях, интересно сравнить подходы.