Автоскейлинг StarRocks в Kubernetes: как я довел его до предела

от автора

Всем, привет! Это не глубокая техническая статья и не исследование. Тема просто не влезла в формат поста в моём 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.

Покой: один CN, CPU у нуля, потолок коридора 12 нод. Мониторинг Prometheus и Grafana

Покой: один CN, CPU у нуля, потолок коридора 12 нод. Мониторинг Prometheus и Grafana

Сам тест

Генерация нагрузки состояло из двух частей.

  1. 64 параллельных дашборд-запроса по coffee.sales (выручка по городам, топ позиций, средний чек) без пауз.

  2. 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, а разрыв между ними и есть потолок.

Рост current 1→6, desired дорос до 12, шесть нод в Pending, CPU застрял выше цели 60%, QPS до ~500/с.

Рост current 1→6, desired дорос до 12, шесть нод в Pending, CPU застрял выше цели 60%, QPS до ~500/с.

Что стало с запросами?

Я взял 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 так и стоит выше цели.

Важно, КАК это проявляется. Не падениями и не ошибками. Успешность запросов держится. Проявляется ростом задержки у тяжелых запросов: они встают в очередь за свободным узлом, а обычные запросы при этом отвечают как раньше.

Отсюда три практических вывода:

  1. Автоскейл не отменяет сайзинг — на фиксированном кластере maxReplicas и ресурсы узлов надо ставить так, чтобы потолок был выше реального пика, иначе пик превратится в очередь на тяжелых запросах.

  2. По-настоящему бесконечная эластичность появляется, только когда эластичны и сами ноды связка HPA и Cluster Autoscaler в облаке добавляет виртуалки под Pending-поды; на своем железе автоскейл экономит ресурсы между пиками, отдавая их соседям по кластеру, но не создает их сверх имеющихся.

  3. Все что видите на графиках дашборда лучше перепроверить другими средствами.

Чтобы не пропустить:

🔹Подписывайтесь на этот блог на Habr

🔹Между статьями — новости StarRocks в Telegram-канале @starrocks_selena (https://t.me/starrocks_selena)

ссылка на оригинал статьи https://habr.com/ru/articles/1041278/