Примечание переводчика: в сообществе тепло приняли свежую статью инженеров LinkedIn, где они рассказали про свой Stateful Workload Operator. С проблемами StatefulSet сталкиваются многие, и тема развёртывания в кластерах Kubernetes приложений с сохранением состояния породила немало мемов. Команда LinkedIn нашла решение в разработке собственных кастомных ресурсов и оператора, который на них базируется. Мы перевели текст для тех, кому комфортнее читать на русском.
Более десяти лет LinkedIn эксплуатирует stateful-системы, охватывающие сотни тысяч машин. Эти системы обеспечивают работу таких важных сервисов, как Kafka, Zookeeper, Liquid и Espresso. Все они очень чувствительны к задержкам и поэтому в значительной степени полагаются на локальные диски для быстрого и надёжного обслуживания запросов.
С самого начала у LinkedIn был собственный планировщик, который распределял bare-metal-серверы по приватным пулам для команд, работающих со stateful-сервисами. Благодаря ему операторы stateful-систем получали стабильность и предсказуемость, однако командам приходилось самостоятельно следить за работоспособностью оборудования, обновлять операционную систему, обслуживать и обновлять аппаратное обеспечение, а также выполнять другие задачи на уровне парка машин. К сожалению, вместе с его расширением росла и трудоёмкость поддержки stateful-систем.
Было необходимо решить эту проблему. Поэтому при переходе от нашего собственного планировщика к Kubernetes мы уделили особое внимание stateful-приложениям и обслуживанию жизненного цикла хостов. Многие скажут, что stateful-приложения и Kubernetes несовместимы. Однако учитывая изобилие решений, адаптированных под конкретные приложения, мы всё же решили попробовать. Stateful-приложения в Kubernetes традиционно полагаются на специально разработанные операторы. Те управляют их жизненным циклом, а подстроенные под конкретные задачи (domain-specific) кастомные ресурсы (CRD) позволяют проводить тонкий контроль.
Сегодня мы представляем наш Stateful Workload Operator — альтернативу традиционному подходу. Теперь все stateful-приложения используют общий оператор с единым кастомным ресурсом, а подстройка конкретных приложений осуществляется с помощью подключаемых внешних политик. В LinkedIn мы перевернули традиционную модель операторов stateful-приложений, предоставив их владельцам базовый строительный блок и централизованную точку для управления хранилищем, внешними интеграциями, инструментами и другими функциями. Цель в том, чтобы команды сосредоточились исключительно на логике, необходимой для поддержания работоспособности приложений во время развёртывания, обслуживания и масштабирования, в то время как оркестрация этих процессов проводилась бы централизованно, обеспечивая единообразие и снижая операционные издержки.
Почему StatefulSet не подходит
StatefulSet в Kubernetes предоставляет удобный способ управления persistent volume claims и оркестрации упорядоченного выката изменений. Однако мы столкнулись с несколькими ключевыми ограничениями, которые сделали его менее подходящим для наших нужд:
StatefulSet не знает о политике шардинга stateful-приложений, поэтому нам всё равно пришлось бы создавать отдельный слой поверх StatefulSet для управления шардингом приложений.
StatefulSet поддерживает только увеличение/сокращение масштаба и развёртывание подов. Он не умеет работать с запланированным или незапланированным обслуживанием хостов (например, не может временно приглушать все поды на определённом узле или переселять их с одного узла на другой).
StatefulSet не позволял нам запускать несколько canary-версий на одном и том же наборе подов.
Хотя технически было возможно обойти эти ограничения с помощью хуков и кастомной логики, такой подход больше походил на сборище разношёрстных решений, чем на чистую, удобную для обслуживания систему. Преимущества StatefulSet были минимальными. Даже если бы он и уменьшил общую сложность управления, то лишь немного. Поэтому мы разработали собственные кастомные ресурсы. Они помогли нам преодолеть эти трудности и добиться необходимой гибкости, а также гораздо лучше подошли под наши конкретные требования.
Доносим информацию о шардах до Kubernetes с помощью менеджеров кластеров приложений
Stateful-приложения отличаются богатством архитектур, причём каждая архитектура предъявляет свои требования к управлению жизненным циклом. Например, в приложении базы данных, где существует только один живой инстанс раздела, выход этого инстанса из строя может привести к потере данных или значительному простою. Однако Kubernetes ничего не знает о разделах, а значит, ему нужен механизм, который бы сигнализировал, что приложение готово потерять под и это не приведёт к критическому сбою.
Чтобы справиться с этими трудностями, был внедрён специальный менеджер (Application Cluster Manager, ACM). Он работал в кластере и координировал свои действия с нашим оператором посредством «кооперативного планирования». ACM определяет, может ли оператор продолжить развёртывание или обслуживание, гарантируя, что кластер останется работоспособным.
Каждая реализация ACM включает логику, адаптированную под конкретное stateful-приложение, что обеспечивает безопасность всех операций. Это может быть гарантия достаточной репликации шардов перед разрешением временного простоя пода, обеспечение порядка развёртывания в зонах обслуживания или предотвращение ненужного перевыбора лидеров во время обновлений. Наши центры обработки данных разбиты на 20 зон обслуживания, и во время технического обслуживания отдельная зона изолируется целиком. Серверы можно безопасно отключить в любой из таких зон, поскольку шарды приложений распределены сразу по всем зонам.
На диаграмме выше показано, как оператор и ACM планируют операции в кластерах со stateful-приложениями. В ACM отправляются четыре типа запросов: развёртывание, приостановка работы (disruption), масштабирование и переезд (swap). Операции масштабирования добавляют или удаляют поды из кластера, операции приостановки работы временно удаляют поды, сохраняя их данные, а операции переезда перемещают поды с одного узла на другой, сохраняя как их данные, так и ID. Затем ACM оценивает безопасность применения этих операций к кластеру. Если операция безопасна, ACM отвечает подтверждением, позволяя оператору продолжить работу. Если подтверждения нет, операция не выполняется. Такое взаимодействие обеспечивает стабильность и безопасность кластеров со stateful-приложениями.
Давайте рассмотрим приведённый выше пример, в котором в очереди стоят следующие операции: развёртывание, приостановка работы, масштабирование и переезд. После выполнения проверок, привязанных к конкретному приложению, ACM отправляет подтверждения. Например, одобряет развёртывание пода 1, добавление пода 5 и переезд пода 7 на другой узел. Получив эти подтверждения, оператор выполняет соответствующие действия — развёртывает новую версию пода 1, запускает под 5 и перемещает под 7 на новый узел.
ACM значительно упрощает управление жизненным циклом и координацию технического обслуживания stateful-приложений. Если бы команды разрабатывали свои собственные операторы, для каждого приложения пришлось бы создавать машины состояний для развёртывания и интегрироваться с IaaS-слоем и Kubernetes. Всё это потребовало бы глубоких знаний о внутреннем устройстве базовых систем, что увеличило бы нагрузку на команды. Управляя всей оркестрацией и интеграцией централизованно, ACM снимает эти проблемы, позволяя отдельным приложениям сосредоточиться на логике, необходимой для обеспечения безопасности во время операций жизненного цикла кластера и хоста, в то время как оператор решает все задачи, связанные с IaaS и Kubernetes.
Архитектура stateful-оператора
Паттерн операторов Kubernetes расширяет функциональность K8s, автоматизируя управление сложными приложениями. Операторы инкапсулируют опыт эксплуатации в нативные контроллеры Kubernetes, автоматизируя такие задачи, как развёртывание, обновление, масштабирование и восстановление. Обычно они взаимодействуют с CRD, которые определяют новые типы ресурсов в API Kubernetes. Отслеживая эти ресурсы, оператор реагирует на изменения состояния и настраивает компоненты приложения, чтобы те соответствовали желаемому состоянию.
Наш Kubernetes-оператор Stateful Workload Operator базируется на пяти CRD: LiStatefulSet, Revision, PodIndex, Operation и StatefulPod. У каждого CRD своё предназначение в жизненном цикле оператора. CRD помогают нам разбить зоны ответственности по различным уровням.
LiStatefulSet CRD — обращенный к пользователю API, с помощью которого настраиваются параметры развёртывания приложений, включая информацию о контейнерах, количестве подов, поддержке томов и проверке работоспособности.
Revision CRD отслеживает историю версий LiStatefulSets, причём каждая ревизия представляет собой неизменяемый PodTemplateSpec, подобно встроенному в Kubernetes API ControllerRevision.
PodIndex CRD служит в качестве промежуточного (staging) объекта для предлагаемых изменений в подах, позволяя оператору сравнивать текущее состояние с желаемым и генерировать операции соответствующим образом. Например, если в LiStatefulSet указано 4 пода, PodIndex может выглядеть так: podIndex: { running: [a, b, c, d] }.
Operation CRD определяет типы операций — развёртывание, масштабирование, приостановка работы и переезд, — указывая, какие поды должны подвергнуться определённым изменениям. Например, operation[type:deployment, instances:[a, b, c, d]] предписывает Kubernetes развернуть новые версии подов a, b, c и d.
StatefulPod CRD управляет подами и Persistent Volume Claims, обеспечивая корректную работу с данными и конфигурациями подов в процессе жизненного цикла.
Пример высокоуровневого рабочего сценария
Пример высокоуровневого сценария конфигурирования LiStatefulSet пользователем — от начальной настройки до создания пода:
Пользовательская настройка: пользователь конфигурирует и применяет YAML-файл LiStatefulSet в Kubernetes.
Создание ревизии: создаётся новая ревизия для отслеживания версии масштабируемого PodTemplate.
Создание индекса подов: генерируется объект PodIndex, который содержит подробную информацию обо всех подах, которые необходимо развернуть.
Генерация операции: на базе PodIndex формируется операция масштабирования, в которой указывается, какие поды должны быть развёрнуты.
Взаимодействие с ACM: операция отправляется в ACM через строго определённый gRPC-интерфейс; ACM постоянно опрашивается — так оператор узнаёт об одобрении дальнейших действий с подами.
Создание подов: как только ACM сигнализирует о готовности определённых подов, Kubernetes получает команду на их создание.
Планирование подов: Kubernetes планирует эти поды и развёртывает их на соответствующих узлах.
Автоматическое исправление: самовосстанавливающаяся платформа
Наша система постоянно отслеживает изменения состояния подов в кластере Kubernetes и сравнивает их с желаемым состоянием, указанным пользователем в LiStatefulSet. Она рассчитывает разницу между фактическим и желаемым состоянием, а затем генерирует необходимые операции для устранения этих различий. Эти операции делятся на пять категорий: масштабирование, обратное масштабирование (то есть сокращение числа подов), переезд, временное прекращение работы и развёртывание.
Например, если пользователь указал четыре пода [a, b, c, d], но в кластере работают только два пода [a, b], будет сгенерирована операция масштабирования для подов [c, d].
Для обеспечения единообразия в ходе повторяющихся циклов согласования в системе используется механизм дедупликации, позволяющий избежать создания дублирующих операций. Если существующая операция уже включает затронутый под и совпадает с типом операции, новая операция для этого пода создаваться не будет.
Координация технического обслуживания: бесперебойное управление жизненным циклом узлов
Основной целью при разработке нашего оператора было упрощение обслуживания узлов для владельцев приложений. В наших центрах обработки данных мы часто обновляем ОС, встроенное ПО и аппаратное обеспечение на узлах, и все это требует эвакуации рабочих нагрузок. Наш подход освобождает владельцев приложений от необходимости эвакуировать узлы, следить за их состоянием и обеспечивать замену неисправного оборудования. Вместо этого ACM получает события прерывания работы от нашего стека обслуживания, который информирует владельцев приложений о временной или постоянной потере узла. Затем ACM уведомляет нас, когда соответствующий под можно временно отключить или переместить на новый узел.
Приостановка работы узла
Когда узлу требуется переустановка системного образа или его необходимо обновить/обслужить без потери данных, stateful-оператор передаёт запрос от уровня IaaS к Application Cluster Manager. После одобрения под удаляется, но Persistent Volume Claims сохраняется вместе с данными. Затем с узлом производятся операции на уровне IaaS. После их завершения узел возвращается в кластер, оператор обнаруживает его и поднимает под на узле, завершая операцию временного прерывания работы.
Постоянная потеря узла
Когда планируется вывод узла из эксплуатации или слой IaaS обнаруживает критические проблемы со здоровьем узла, он направляет запрос на постоянное вытеснение (eviction) узла. В ответ на это stateful-operator генерирует запросы на переезд, завершая работу подов на целевом узле и запуская их на других узлах, сохраняя при этом ID.
Общий паттерн состоит в том, что ACM сначала подтверждает добавление нового пода перед тем, как дать разрешение на удаление старого пода. В этом случае ACM будет ждать, пока новый под не будет полностью запущен и готов принимать трафик, прежде чем подтвердить удаление старого пода. После подтверждения удаления старый под и связанные с ним ресурсы удаляются, о чём извещается IaaS-слой, и узел навсегда удаляется из кластера.
Наработки и усовершенствования, извлечённые из legacy-стека
Сокращение трудозатрат за счёт совместного планирования
Мы уже упоминали о том, что Application Cluster Manager позволяет владельцам stateful-приложений легко интегрировать их с Kubernetes и IaaS-слоем. Такая интеграция позволяет пользователям фокусироваться на логике конкретных приложений, не вникая в особенности базовой инфраструктуры.
Применяя этот подход, команды могут отказаться от громоздкой домашней автоматизации или ручных операций, — таких как распределение узлов, балансировка зон обслуживания и замена нездоровых хостов, — которые влекут за собой значительные эксплуатационные расходы. Теперь, когда Stateful Workload Operator управляет их машинами, владельцы stateful-систем могут сосредоточиться на управлении системами, не думая о сложностях эксплуатации.
Приоритизация развёртывания и обслуживания упрощает стек
Наш Stateful Workload Operator централизует операции развёртывания и обслуживания как приложений, так и хостов. Раньше операции часто занимали больше времени, чем требовалось, потому что разные участники, отвечающие за развёртывание, обновление, перезагрузку и так далее, конкурировали за контроль над одними и теми же ресурсами. Рассматривая развёртывание и обслуживание как первостепенные задачи, можно последовательно выполнять все «опасные» операции. Это позволяет избежать многих проблем вроде условий гонки, которые возникали из-за архитектуры старого стека. Кроме того, разработчикам проще тестировать и понимать все последствия каждой операции, особенно когда несколько операций происходят одновременно, поскольку вся машина состояний находится в одном репозитории.
Эффективные методы разработки программного обеспечения
Скорость и успех нашей разработки за последний год доказали преимущества стандартных принципов разработки программного обеспечения, о которых мы кратко расскажем ниже:
Разделение ответственности — мы придерживались принципов SOLID при разработке наших CRD и ACM API, стремясь к тому, чтобы каждый компонент имел простую и понятную бизнес-цель. В результате удалось добиться значительного распараллеливания разработки, поскольку компоненты оказались относительно изолированными друг от друга.
Постоянные инвестиции в тестовую инфраструктуру — девиз нашей команды: «Затестировать всё до смерти, а потом протестировать ещё разок». Каждый компонент подвергался глубокому локальному тестированию, а для каждого коммита проводились строгие модульные и интеграционные тесты. Когда тесты становились медленными и ненадёжными, мы в приоритетном порядке занимались их улучшением, чтобы повысить производительность.
Частый рефакторинг — первоначальный дизайн оказался не совсем правильным; мы не учли всех особенностей. Со временем возникали новые требования или обнаруживались новые проблемы, которые неизбежно добавляли сложности в код. Мы предпочитали проводить рефакторинг сразу же, как только код начинал выглядеть запутанно. Так удалось сохранить его чистым и лёгким для понимания. Благодаря этой практике кодовая база оставалась простой и понятной, что позволяло любому члену команды легко в ней ориентироваться.
Придерживаясь высоких стандартов разработки программного обеспечения, мы смогли сохранить продуктивность в условиях роста команды и возникновения новых требований, в то же время поставляя пользователям качественное программное обеспечение. Этот проект служит напоминанием о том, что стандарты существуют не зря и что они работают.
Наш опыт с Kubernetes
Год назад мы начали с идеи. Сегодня у нас куча кластеров со stateful-системами. Они полностью переведены и работают на Stateful Workload Operator в едином production-регионе, успешно выполняя все задачи системы развёртывания (несмотря на её сложность — система развивалась и дорабатывалась последние десять лет!). Кроме того, оператор управляет многими функциями, которых не было в старой системе, например хранилищами/томами для каждого приложения.
Такие темпы развития стали возможны благодаря богатой экосистеме Kubernetes, включающей встроенные ресурсы (например, Persistent Volumes/Persistent Volume Claims) и широкие возможности кастомизации с помощью CRD и операторов. Однако мы столкнулись и с некоторыми проблемами, такими как преодоление тонкостей декларативного API Kubernetes для решения фундаментально императивных задач (например, перезапуск пода через публичный API), адаптация kube-scheduler'а под требования к распределению зон обслуживания и достижение полной наблюдаемости проблем при согласовании (reconciliation) ресурсов.
Несмотря на все эти трудности, мы по-прежнему считаем, что Kubernetes — правильное решение. Мы и дальше намерены активно задействовать его богатые функциональные возможности и максимально использовать поддержку сообщества разработчиков.
Благодарности
В этой заметке освещается лишь часть сложного механизма, необходимого для того, чтобы всё это работало. Хотим выразить признательность крутым командам в LinkedIn за то, что сделали это возможным.
P. S.
Читайте также в нашем блоге: