Всем, привет! Это не глубокая техническая статья и не исследование. Тема просто не влезла в формат поста в моём Telegram-канале https://t.me/starrocks_selena, поэтому мы с вами тут. Это пятничный лёгкий длиннопост про автоскейлинг StarRocks. Без глубокого погружения в архитектуру, с мЭмами и несколькими наблюдениями из практики.

Проблематика
Представьте, 28 дней в месяц аналитический кластер/БД потребляет ресурсы равномерно. Регламентные отчеты, привычные дашборды, пересчеты витрин. В среднем около 60% ресурсов. И вот наступают самые тяжелые три дня месяца. Закрытие периода, массовые выгрузки, аналитики разом открывают отчеты. В этот момент кластеру нужно в разы больше мощности, чем обычно.
Держать тройной запас постоянно дорого, потому что большую часть месяца он простаивает. Без запаса пик упирается в потолок и запросы встают в очередь.
Долгое время эту задачу решали через сайзинг под пиковую нагрузку. Если расчет делал опытный архитектор, инженер или DBA, пики просто закладывались в конфигурацию. То, что большую часть времени серверы работают с низкой загрузкой, считалось платой за спокойный отчетный период. Особенно заметно это было с Exadata и похожими ПАК. Конфигурации фиксированы, шаг масштабирования крупный, поэтому нередко приходилось покупать больше ресурсов, чем требуется для повседневной нагрузки.
Режим Shared-data в StarRocks отчасти появился как раз для того чтобы эту проблему решить, полностью или частично.
StarRocks и K8s спешат на помощь

Классический режим StarRocks, shared-nothing, хранит данные на локальных дисках BE-узлов: каждый узел и хранит, и считает. Добавить узел — значит перенести на него tablets и выполнить rebalance. Это небыстрая операция.
Shared-data — это режим развертывания StarRocks, при котором у вас данные лежат на едином удаленном хранилище (S3-хранилище или HDFS), а вычислительные компоненты развернуты отдельно от хранения.
Вычислительные компоненты в Shared-data режиме называются Compute Node (CN). Сами по себе они stateless, так как предназначены только для вычислений. У CN есть локальный кэш, обычно на SSD или NVMe. В него попадают данные, прочитанные из S3-хранилища, поэтому повторные запросы выполняются быстрее, так как читать из S3-хранилища — это всегда доп. нагрузка на сеть и, соответственно, лишняя latency (больше/меньше зависит от пропускной способности сети и профиля запросов).
Работать StarRocks в K8s помогает StarRocks Operator. Это контроллер, который живет внутри Kubernetes и управляет StarRocks сам. Он вводит свой тип объекта StarRocksCluster: в нем описан весь кластер — роли (FE, BE, CN), число узлов, образы, ресурсы, хранилище, настройки. По этому описанию оператор разворачивает компоненты как StatefulSet, поднимает сервисы и конфигурации, раскатывает обновления и постоянно приводит кластер к заданному виду. Масштабировать руками не нужно, достаточно поменять описание. Для CN оператор берет на себя и автоскейл: по секции autoScalingPolicy он создает HPA, а при сокращении сначала корректно выводит ноду из кластера и только потом останавливает под. Но дальше, я подробней опишу механику.
И здесь две вещи складываются вместе. CN ничего не хранят, pod с CN поднимается и гасится за секунды, переносить на него нечего. А Kubernetes как раз умеет менять число подов под нагрузку. Stateless-вычисления над общим хранилищем плюс оркестратор, который добавляет и убирает поды. На этой связке и стоит автоскейлинг StarRocks.
StarRocks ничего не решает

Решает когда мастштабироваться, а когда нет не сам StarRocks. Решает Kubernetes, а точнее его встроенный автоскейлер, HPA (Horizontal Pod Autoscaler). Разворачивает и настраивает кластер оператор: служебная программа, которая ставит StarRocks и держит его в заданном виде. В настройках оператора мы один раз задаем параметры и границы: нод CN от 1 до 12, ориентир по загрузке 60%. Дальше оператор сам поднимает автоскейлер и привязывает его к группе CN, руками ничего трогать не нужно.
Работает все на простом цикле. Рядом крутится metrics-server (в k3s он есть сразу) и каждые 15 секунд снимает загрузку CPU с каждой ноды. Автоскейлер берет эти цифры, усредняет по всем CN и сравнивает с ориентиром 60%.
Если среднее ушло выше 60%, то нод не хватает, и он добавляет новые, по паре за раз, чтобы быстро снять пик. Сколько добавить, считается так: текущее число нод умножается на то, во сколько раз загрузка выше верхней планки. Загрузка под 200% при цели 60% — значит, нужно примерно втрое больше нод, чтобы та же работа размазалась до спокойных 60% на каждую.
Если упало среднее надолго ниже 60%, то лишние ноды убираются по одной. Тут автоскейлер действует осмотрительней: сначала ждет около минуты и только потом начинает сворачивать. Без этой паузы кластер скакал бы числом нод на каждом коротком затишье.
Включается это в одном блоке описания кластера, секция CN (добавил описание каждого параметра):
starRocksCnSpec: # секция compute-нод (CN) в описании кластера # replicas здесь НЕ указываем: фиксированное число спорит с автоскейлом — количеством нод рулит HPA requests: cpu: 1 # 1 ядро в брони; именно от requests считается % загрузки memory: 3Gi # 3 GiB памяти в брони limits: cpu: 2 # максимум 2 ядра на под memory: 6Gi # максимум 6 GiB памяти autoScalingPolicy: minReplicas: 1 # ниже одной ноды не опускаемся maxReplicas: 5 # выше пяти не поднимаемся — верх коридора hpaPolicy: metrics: # показатель, по которому скейлим - type: Resource # ресурсная метрика самого пода (CPU/память) resource: name: cpu # смотрим на CPU target: type: Utilization # считаем в процентах от requests averageUtilization: 60 # цель: средний CPU по всем CN держать около 60% behavior: # скорость и осторожность реакции scaleUp: # как растем под нагрузкой policies: - type: Pods # шаг считаем в подах value: 2 # добавляем по 2 пода periodSeconds: 15 # каждые 15с, пока CPU выше цели — быстро снять пик scaleDown: # как сворачиваемся на спаде stabilizationWindowSeconds: 60 # ждем минуту низкой нагрузки, чтобы не мигать числом нод policies: - type: Pods # шаг в подах value: 1 # убираем по 1 поду periodSeconds: 30 # не чаще раза в 30с
А в чем тогда экономия?

Вопрос всплывает почти сразу: какая экономия, если под расширение все равно надо держать ресурсы зарезервированными? Идея пришла из облаков, и там все сходится: оплата по факту, pay-as-you-go. Если поды растут, то облако поднимает виртуалки, нагрузка ушла виртуалки гасятся, в счете остаются только часы, что машины реально работали.
На локальной инфраструктуре у заказчика все зависит от того, что еще живет в кластере. Если на k8s крутится один только StarRocks, экономия действительно сомнительная: машины под пик из пяти CN все равно должны стоять, иначе новым подам некуда разворачиваться, и как говорится в простое они просто простаивают.
Другое дело, если общий кластер компании, где рядом живут десятки систем, разнесенных по namespace со своими квотами. Железо там берут под совокупный пик, и он меньше суммы отдельных пиков: разные нагрузки взлетают в разное время. StarRocks занял пять CN на свои три дня в месяц, а в остальные дни эти ресурсы забирают соседи: ночные батчи, отчетность другого отдела, dev-стенды. Автоскейл экономит здесь заметнее: одно и то же железо успевает поработать на разных проектах.
А работает ли автоскейлинг у StarRocks?
Да, работает. Быстро покажу, что видно при настроенном автоскейлинге:
Шаг 1. До запуска запросов:

Шаг 2. Нормальная работа, работает один CN:

Шаг 3. Автоскейлинг, до 5 CN:

Шаг 4: Остановка и полный цикл:

Мне показалось, что будет круто исследовать пограничный случай и посмотреть, что будет с запросами в БД, когда автоскейлинг съест все доступные ресурсы.
Стенд и данные
k3s, 3 ноды, суммарно ~24 vCPU и ~93 GB.
Selena 2.0.7 (это наш дистрибутив, взял его, потому что он под k8s-инфраструктуру уже готов). Данные — coffee.sales: 2 097 152 строки продаж кофе по шести городам. Это мой старый датасет, я его часто показываю в разных демосценариях.
Перед прогоном я командой cordon запретил Kubernetes ставить новые поды на control-plane-ноду, поэтому новые CN поднимаются только на двух рабочих нодах. Так я, во-первых, не нагружаю управляющий слой кластера (apiserver и etcd), во-вторых, заранее знаю предел: два воркера — это примерно 16 ядер, и автоскейл упрется именно в них.
Автоскейл CN включается прямо в описании кластера: requests 2 ядра / 4 GiB, limits 3 ядра / 6 GiB, коридор 1–12 нод, цель по CPU — 60%. Оператор по этому описанию сам создал HPA и подключил его к группе CN.
Сам тест
Генерация нагрузки состояло из двух частей.
-
64 параллельных дашборд-запроса по coffee.sales (выручка по городам, топ позиций, средний чек) без пауз.
-
80 CPU-тяжелых запросов с математикой (pow/sin/cos/sqrt/ln) по всем двум миллионам строк.
Пока есть запас, скейлинг работает
Под первой нагрузкой CN рос ступенями: 1 → 3 → 6. И латентность при этом падала — примерно с 1000 до 350 мс. Больше нод, та же работа размазывается по ним, каждый запрос отвечает быстрее. Ровно то, ради чего автоскейл и нужен.
Потолок
Дальше пошла тяжелая нагрузка. CPU уперся в ~117% при цели 60%, и HPA поднял число нод до 12 (максимума). Но на два воркера поместилось только шесть CN, по ~2.5 ядра каждый. Остальные шесть повисли в Pending с понятной причиной:
FailedScheduling: 0/3 nodes are available: 1 node(s) were unschedulable, 2 Insufficient cpu.
Автоскейлер хочет двенадцать нод, а получает шесть. Расширяться некуда: свободные ядра кончились, новые поды не на что поставить. На графике это видно одним кадром — current упирается в 6, desired уходит к 12, а разрыв между ними и есть потолок.
Что стало с запросами?
Я взял audit-лог: в нем записана длительность каждого запроса.
Картина оказалась очень интересной. Обычный запрос остался быстрым: и до пика, и на потолке половина запросов отвечала примерно за 130–150 мс. Замедлились только самые тяжелые. До пика даже редкие медленные укладывались в ~0.8 с, а на потолке растянулись до ~2 с (самый долгий 2.6 с); средняя по всем запросам выросла с 0.24 до 0.63 с.
Причина тут простая: новых нод нет, процессора на всех разом не хватает. Легкие запросы проскакивают, тяжелые встают в очередь и ждут свободный CN. Мой probe гонял как раз тяжелый запрос, поэтому и показывал 1.5–2.5 с, он попадал в эту медленную группу.
Ошибок при этом почти не было ,а тут кстати дашборд меня чуть не обманул. Счетчик ошибок рос до ~170 в секунду (видно на графике), и сначала я решил, что запросы отваливаются. В логе оказалось другое: почти все это select $$ это служебный пинг, который mysql-клиент шлет на каждом подключении, а StarRocks его не понимает и отклоняет. Больше соединений под нагрузкой больше таких отказов, вот счетчик и реагировал на нагрузку.
А как только я снял нагрузку, probe снова отвечал за ~30 мс, CPU упал к нулю, и кластер свернул лишние CN обратно к одному. Замедление держалось ровно пока не хватало нод, и ушло вместе с нагрузкой.
Вывод по тесту
Автоскейл CN эластичен ровно до предела самого кластера. Пока на узлах есть свободные ядра, он добавляет ноды и держит latancy в норме. Кончились ядра (у нас было два воркера, 16 ядер) — задержка упирается в потолок: HPA продолжает просить ноды (хочет 12), а ставить их некуда, поды висят в Pending, и CPU так и стоит выше цели.
Важно, КАК это проявляется. Не падениями и не ошибками. Успешность запросов держится. Проявляется ростом задержки у тяжелых запросов: они встают в очередь за свободным узлом, а обычные запросы при этом отвечают как раньше.
Отсюда три практических вывода:
-
Автоскейл не отменяет сайзинг — на фиксированном кластере maxReplicas и ресурсы узлов надо ставить так, чтобы потолок был выше реального пика, иначе пик превратится в очередь на тяжелых запросах.
-
По-настоящему бесконечная эластичность появляется, только когда эластичны и сами ноды связка HPA и Cluster Autoscaler в облаке добавляет виртуалки под Pending-поды; на своем железе автоскейл экономит ресурсы между пиками, отдавая их соседям по кластеру, но не создает их сверх имеющихся.
-
Все что видите на графиках дашборда лучше перепроверить другими средствами.
Чтобы не пропустить:
🔹Подписывайтесь на этот блог на Habr
🔹Между статьями — новости StarRocks в Telegram-канале @starrocks_selena (https://t.me/starrocks_selena)
ссылка на оригинал статьи https://habr.com/ru/articles/1041278/