Примечание переводчика: в сообществе тепло приняли свежую статью инженеров 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 зон обслуживания, и во время технического обслуживания отдельная зона изолируется целиком. Серверы можно безопасно отключить в любой из таких зон, поскольку шарды приложений распределены сразу по всем зонам.

Рисунок 1. Пример запроса и ответа между оператором и ACM
Рисунок 1. Пример запроса и ответа между оператором и ACM

На диаграмме выше показано, как оператор и ACM планируют операции в кластерах со stateful-приложениями. В ACM отправляются четыре типа запросов: развёртывание, приостановка работы (disruption), масштабирование и переезд (swap). Операции масштабирования добавляют или удаляют поды из кластера, операции приостановки работы временно удаляют поды, сохраняя их данные, а операции переезда перемещают поды с одного узла на другой, сохраняя как их данные, так и ID. Затем ACM оценивает безопасность применения этих операций к кластеру. Если операция безопасна, ACM отвечает подтверждением, позволяя оператору продолжить работу. Если подтверждения нет, операция не выполняется. Такое взаимодействие обеспечивает стабильность и безопасность кластеров со stateful-приложениями.

Давайте рассмотрим приведённый выше пример, в котором в очереди стоят следующие операции: развёртывание, приостановка работы, масштабирование и переезд. После выполнения проверок, привязанных к конкретному приложению, ACM отправляет подтверждения. Например, одобряет развёртывание пода 1, добавление пода 5 и переезд пода 7 на другой узел. Получив эти подтверждения, оператор выполняет соответствующие действия — развёртывает новую версию пода 1, запускает под 5 и перемещает под 7 на новый узел.

ACM значительно упрощает управление жизненным циклом и координацию технического обслуживания stateful-приложений. Если бы команды разрабатывали свои собственные операторы, для каждого приложения пришлось бы создавать машины состояний для развёртывания и интегрироваться с IaaS-слоем и Kubernetes. Всё это потребовало бы глубоких знаний о внутреннем устройстве базовых систем, что увеличило бы нагрузку на команды. Управляя всей оркестрацией и интеграцией централизованно, ACM снимает эти проблемы, позволяя отдельным приложениям сосредоточиться на логике, необходимой для обеспечения безопасности во время операций жизненного цикла кластера и хоста, в то время как оператор решает все задачи, связанные с IaaS и Kubernetes.

Архитектура stateful-оператора

Рисунок 2. Высокоуровневая архитектура нашего stateful-оператора
Рисунок 2. Высокоуровневая архитектура нашего 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 пользователем — от начальной настройки до создания пода:

  1. Пользовательская настройка: пользователь конфигурирует и применяет YAML-файл LiStatefulSet в Kubernetes.

  2. Создание ревизии: создаётся новая ревизия для отслеживания версии масштабируемого PodTemplate.

  3. Создание индекса подов: генерируется объект PodIndex, который содержит подробную информацию обо всех подах, которые необходимо развернуть.

  4. Генерация операции: на базе PodIndex формируется операция масштабирования, в которой указывается, какие поды должны быть развёрнуты.

  5. Взаимодействие с ACM: операция отправляется в ACM через строго определённый gRPC-интерфейс; ACM постоянно опрашивается — так оператор узнаёт об одобрении дальнейших действий с подами.

  6. Создание подов: как только ACM сигнализирует о готовности определённых подов, Kubernetes получает команду на их создание.

  7. Планирование подов: Kubernetes планирует эти поды и развёртывает их на соответствующих узлах.

Автоматическое исправление: самовосстанавливающаяся платформа

Рисунок 3. Высокоуровневое логическое представление того, как stateful-оператор согласовывает различия между объявленным и реальным состоянием
Рисунок 3. Высокоуровневое логическое представление того, как stateful-оператор согласовывает различия между объявленным и реальным состоянием

Наша система постоянно отслеживает изменения состояния подов в кластере Kubernetes и сравнивает их с желаемым состоянием, указанным пользователем в LiStatefulSet. Она рассчитывает разницу между фактическим и желаемым состоянием, а затем генерирует необходимые операции для устранения этих различий. Эти операции делятся на пять категорий: масштабирование, обратное масштабирование (то есть сокращение числа подов), переезд, временное прекращение работы и развёртывание.

Например, если пользователь указал четыре пода [a, b, c, d], но в кластере работают только два пода [a, b], будет сгенерирована операция масштабирования для подов [c, d].

Для обеспечения единообразия в ходе повторяющихся циклов согласования в системе используется механизм дедупликации, позволяющий избежать создания дублирующих операций. Если существующая операция уже включает затронутый под и совпадает с типом операции, новая операция для этого пода создаваться не будет.

Координация технического обслуживания: бесперебойное управление жизненным циклом узлов

Основной целью при разработке нашего оператора было упрощение обслуживания узлов для владельцев приложений. В наших центрах обработки данных мы часто обновляем ОС, встроенное ПО и аппаратное обеспечение на узлах, и все это требует эвакуации рабочих нагрузок. Наш подход освобождает владельцев приложений от необходимости эвакуировать узлы, следить за их состоянием и обеспечивать замену неисправного оборудования. Вместо этого ACM получает события прерывания работы от нашего стека обслуживания, который информирует владельцев приложений о временной или постоянной потере узла. Затем ACM уведомляет нас, когда соответствующий под можно временно отключить или переместить на новый узел.

Приостановка работы узла

Рисунок 4. Пример высокоуровневого сценария с временным прерыванием работы, например при замене образа. Процесс начинается на уровне IaaS, а затем передаётся ACM на одобрение. После одобрения оператор останавливает под и разрешает перерыв в работе на уровне IaaS
Рисунок 4. Пример высокоуровневого сценария с временным прерыванием работы, например при замене образа. Процесс начинается на уровне IaaS, а затем передаётся ACM на одобрение. После одобрения оператор останавливает под и разрешает перерыв в работе на уровне IaaS

Когда узлу требуется переустановка системного образа или его необходимо обновить/обслужить без потери данных, stateful-оператор передаёт запрос от уровня IaaS к Application Cluster Manager. После одобрения под удаляется, но Persistent Volume Claims сохраняется вместе с данными. Затем с узлом производятся операции на уровне IaaS. После их завершения узел возвращается в кластер, оператор обнаруживает его и поднимает под на узле, завершая операцию временного прерывания работы.

Постоянная потеря узла

Рисунок 5. Пример высокоуровневого сценария в случае постоянной потери узла, например при выводе узла из эксплуатации. Процесс начинается на уровне IaaS, а затем передаётся ACM на одобрение. После одобрения оператор запускает новый под, удаляет старый под с целевого узла и утверждает вывод из эксплуатации на уровне IaaS
Рисунок 5. Пример высокоуровневого сценария в случае постоянной потери узла, например при выводе узла из эксплуатации. Процесс начинается на уровне IaaS, а затем передаётся ACM на одобрение. После одобрения оператор запускает новый под, удаляет старый под с целевого узла и утверждает вывод из эксплуатации на уровне 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.

Читайте также в нашем блоге:

Комментарии (0)