На начало декабря 2020 у нас во «Фланте» было около 150 кластеров на Kubernetes 1.16. Это кластеры с разной степенью загруженности: как находящиеся под высоким трафиком production-кластеры, так и использующиеся для разработки и демонстрации новых возможностей. Кластеры работают поверх различной инфраструктуры, начиная с облачных провайдеров, таких как AWS, Azure, GCP, Яндекс.Облако, различных инсталляций OpenStack и vSphere, и заканчивая железками.
Все эти кластеры находятся под управлением Deckhouse, который является нашей внутренней разработкой и позволяет не только иметь один инструмент для создания кластеров, но и общий интерфейс для управления всеми компонентами кластера на всех поддерживаемых типах инфраструктуры. Для этого Deckhouse состоит из различных подсистем. Например, есть подсистема candi (cluster and infrastructure), которая наиболее интересна в рамках данной статьи, поскольку отвечает за управление control-plane Kubernetes и настройку узлов, предоставляет готовый к работе, актуальный кластер.
(Оценить плюсы Deckhouse пока можно в рамках нашего Managed Kubernetes, а уже в ближайшее время мы планируем сделать первый Open Source-релиз проекта для всех желающих посмотреть на него изнутри и развернуть для своих задач. Присоединиться к числу ожидающих можно в Telegram-канале.)
Итак, почему мы вообще застряли на версии 1.16, когда уже достаточно давно вышли 1.17, 1.18 и даже выпустили патч версии для 1.19? Дело в том, что у нас затянулось предыдущее обновление кластеров — с 1.15 на 1.16. Оно было очень тяжелым и вот почему:
В версии 1.16 окончательно удалялись устаревшие версии некоторых API. Наиболее болезненным стал отказ от старых версий API контроллеров: DaemonSet (
extensions/v1beta1
иapps/v1beta2
), Deployment (extensions/v1beta1
,apps/v1beta1
иapps/v1beta2
) и StatefulSet (apps/v1beta1
иapps/v1beta2
). Перед обновлением на 1.16 необходимо было, чтобы в прикладных Helm-чартах были обновлены версии API, а иначе после обновлений ломался выкат Helm-релиза. Так как в кластеры деплоятся клиентские приложения и написанием Helm-чартов занимаются как наши команды DevOps, так и сами клиенты, то необходимо было в первую очередь оповестить о проблеме. Для этого мы сделали модуль в Deckhouse, который проверяет все последние установленные Helm-релизы в кластере на предмет использования старых версий API и отдаёт метрики с этой информацией. Далее уже Prometheus и Alertmanager зажигают алерты. (Подробнее о том, как во «Фланте» устроена работа с инцидентами, мы рассказывали здесь.)Обновление с 1.15 на 1.16 приводило к рестарту контейнеров на узле, поэтому приходилось выполнять drain узла, что в случае большого количества stateful-приложений приводило к необходимости выполнять работы в согласованные окна и требовало особого внимания инженеров.
Эти 2 фактора сильно тормозили процесс обновления на 1.16. С одной стороны необходимо было добиваться от клиентов обновления чартов и выпуска обновлений, а с другой — аккуратно обновлять кластеры в наполовину ручном режиме. Была проделана большая работа со всеми пользователями кластеров, потому что требовалось донести, что существует реальная необходимость обновления, а подход «работает — не трогай» в данном случае может сыграть злую шутку… Попробую убедить в этом и вас.
Зачем вообще обновлять Kubernetes?
Одной из самых очевидных вещей, которой проще всего объяснить обновление, является устаревание программного обеспечения. В проекте Kubernetes поддерживаются только 3 последние минорные версии. Таким образом, на момент выхода 1.19 наша текущая версия 1.16 должна была покинуть список поддерживаемых. Хотя так совпало, что именно в этом релизе (1.19) разработчики Kubernetes пошли навстречу сообществу и увеличили окно поддержки до 1 года. Это изменение стало ответом на грустную статистику, которая показала, как медленно обновляются инсталляции K8s в реальной жизни (по результатам опроса в начале 2019 года лишь 50-60% кластеров K8s имели поддерживаемую версию).
Примечание. Тема обновлений релизов Kubernetes вообще широко обсуждается в сообществе инженеров эксплуатации. Например, опросы Platform9 от 2019-го года (№1, №2) показали, что обновление — в топ-3 главных вызовов при обслуживании Kubernetes. Легко найти в интернете и связанные с обновлениями failure stories, вебинары и т.д.
Но вернёмся к релизу 1.16. С ним также был ряд проблем, которые мы вынужденно затыкали «костылями». Вполне возможно, что большинство из читателей не сталкивались с описанным ниже, однако у нас на поддержке большое количество кластеров (с тысячами узлов), поэтому даже редкие ошибки регулярно генерировали нам дополнительную работу. К декабрю мы обросли различными забавными компонентами как в самом Kubernetes, так и в системных юнитах:
Достаточно регулярно стали приходить алерты о том, что случайные узлы кластеров находятся в состоянии
NotReady
. Это связано с issue, которую исправили только в Kubernetes 1.19 и без перспектив бэкпортирования:Чтобы не просыпаться по ночам, на все узлы кластеров мы выкатили systemd-юнит, который назвали…
kubelet-face-slapper
. Он просто следил за логами kubelet’a и в случае возникновения ошибкиuse of closed network connection
перезапускал kubelet. Если посмотреть в долгую историю issue, то можно увидеть, что похожее решение применяли и другие.Периодически случались странные проблемы с kube-scheduler. В наших инсталляциях метрики собираются исключительно через HTTPS с использованием клиентского сертификата для Prometheus, а он в случайный момент переставал получать метрики планировщика по причине того, что kube-scheduler не мог корректно обработать данные из клиентского сертификата. Каких-то конкретных issue не было обнаружено (вероятно, сбор метрик через HTTPS — это не столь распространённая практика), но между версиями 1.16 и 1.19 был большой рефакторинг и пачка исправлений в соответствующей кодовой базе, из-за чего мы были уверены, что обновление закроет и эту проблему. Как временное решение мы запускали в каждом кластере компонент на мастер-узлах, который имитировал обращение Prometheus за метриками и в случае ошибки перезапускал kube-scheduler. По аналогии с предыдущим решением данный компонент получил название
kube-scheduler-face-slapper
.Иногда случалось совсем страшное. В кластерах можно было получить целую пачку проблем с узлами, потому что зависали kube-proxy на обращении к kube-apiserver. Причиной всему являлось отсутствие health check для HTTP/2-соединений для всех клиентов, использующихся в Kubernetes. При зависшем kube-proxy по понятным причинам начинались сетевые проблемы на узлах, что могло приводить к простою. Исправление было выпущено для 1.20 и сделан backport только в 1.19. Кстати, этот же фикс решил проблемы с зависанием kubelet. Были также проблемы, что периодически мог зависать kubectl на каких-то длительных операциях и приходилось всегда помнить, что следует использовать timeout.
Во всех кластерах мы используем Docker в качестве Container Runtime, что тоже приносило нам регулярные проблемы в виде зависших в воздухе Pod’ов в статусе
Terminating
. Это происходило из-за известного бага.
Были также и другие раздражающие проблемы — например, как c ошибками mount’ов при рестарте контейнеров, если используются subpath
. Мы не стали пилить очередной whatever-face-slapper, а окончательно для себя решили, что хватит страдать и пора обновляться на 1.19. Тем более, что к этому моменту у нас уже почти 100% кластеров были готовы к обновлению.
Как устроен процесс обновления Kubernetes
В начале статьи уже кратко упоминался Deckhouse и подсистема candi, которая отвечает в том числе и за процесс обновления control-plane и узлов кластера. Если не погружаться в нюансы, то внутри имеется немного допиленный kubeadm, поэтому структурно процесс обновления совпадает с документацией Kubernetes по обновлению кластеров под управлением kubeadm.
Этапы обновления с 1.16 на 1.19 выглядят следующим образом:
Обновление версии control-plane до версии 1.17;
Обновление версии kubelet на узлах до версии 1.17;
Обновление версии control-plane до версии 1.18;
Обновление версии kubelet на узлах до версии 1.18;
Обновление версии control-plane до версии 1.19;
Обновление версии kubelet на узлах до версии 1.19.
В Deckhouse эти этапы выполняются автоматически. Для этого в каждом кластере есть Secret, в котором описана конфигурация кластера в YAML’е следующего формата:
apiVersion: deckhouse.io/v1alpha1
cloud:
prefix: k-aksenov
provider: OpenStack
clusterDomain: cluster.local
clusterType: Cloud
kind: ClusterConfiguration
kubernetesVersion: "1.16"
podSubnetCIDR: 10.111.0.0/16
podSubnetNodeCIDRPrefix: "24"
serviceSubnetCIDR: 10.222.0.0/16
Для обновления достаточно изменить значение kubernetesVersion
на желаемое (можно менять сразу с 1.16 на 1.19). Внутри подсистемы candi есть два модуля, которые отвечают за управление control-plane и управление узлами.
В сontrol-plane-manager автоматически отслеживаются изменения в данном YAML.
Вычисляется текущая версия Kubernetes в кластере на основании версии control-plane и узлов кластера. Например, если kubelet на всех узлах версии 1.16 и все control-plane компоненты версии 1.16, то можно запускать обновление на следующую версию 1.17 — и так далее, пока версия не будет той, что указана в конфигурации.
Также control-plane-manager следит, чтобы обновление компонентов control-plane выполнялось по очереди на каждом из мастеров: для этого реализован алгоритм запроса и выдачи разрешения на обновление через специальный диспетчер.
Node-manager отвечает за управление узлами, в том числе и за обновление kubelet’а:
Каждый узел в кластере принадлежит к одной из NodeGroup. Как только node-manager определяет, что версия control-plane на всех узлах обновилась, то он приступает к обновлению версии kubelet. Если обновление узла не приводит к простою, оно считается безопасным и выполняется в автоматическом режиме. В данном случае обновление kubelet больше не приводит к перезапуску контейнеров, что позволило безопасно выкатывать его.
В рамках node-manager также реализован механизм автоматической выдачи разрешений на обновление узла, что гарантирует одновременное обновление только одного узла в рамках NodeGroup. При этом обновление узлов в NodeGroup выполняется, только когда требуемое количество узлов равно текущему количеству узлов в состоянии
Ready
, то есть нет узлов в процессе заказа. (Это касается, конечно же, только облачных кластеров, где есть автоматический заказ новых виртуальных машин.)
Наш опыт обновления Kubernetes на 1.19
Итак, мы решили обновляться сразу на 1.19, фактически минуя версии 1.17 и 1.18, по нескольким причинам.
Основная причина была в том, что не хотелось затягивать процесс обновления. Каждый цикл обновления требует различных согласований с пользователями кластеров, инженерного времени на контроль процесса обновления. Таким образом, мы снова продолжали бы рисковать отставать от upstream’а на несколько версий. Мы хотели, чтобы к февралю 2021 все наши кластеры были версии 1.19. Поэтому было твёрдое желание одним волевым усилием получить максимально свежую версию Kubernetes в наших кластерах — тем более, уже на подходе был 1.20 (его релиз состоялся 8 декабря 2020).
Как уже написано выше, у нас есть кластеры, которые работают поверх различных облачных провайдеров. Чтобы реализовать это требуется использование нескольких компонентов, специфичных для каждого провайдера. Для управления дисками используется Container Storage Interface, а для взаимодействия с API облачных провайдеров — Cloud Controller Manager. Тестирование работоспособности этих компонентов для каждой версии Kubernetes требует достаточно большого количества ресурсов. В данном случае нет потребности тратить столько времени впустую на «промежуточные» версии.
Процесс
Было проведено полное тестирование совместимости компонентов с версией Kubernetes 1.19 и принято решение обновляться сразу на 3 версии вперёд. Так как стандартный процесс обновления выполняется последовательно, то на момент, когда версия control-plane в облачных кластерах соответствовала 1.17 и 1.18, были отключены компоненты, описанные выше.
Фактическое время обновления зависело от числа worker-узлов и узлов control-plane, занимало от 20 до 40 минут. На это время в облачных кластерах не работал заказ и удаление узлов, любые операции с дисками. При этом уже работающие в кластере узлы и примонтированные диски продолжали работать корректно. Это был единственный очевидный минус запланированного обновления, с которым пришлось смириться на старте. Только из-за этого было принято решение проводить работы по обновлению большинства кластеров в ночное время, когда нет высокой нагрузки.
Сам процесс обновления мы сначала прогнали несколько раз на внутренних dev-кластерах, после чего начали проводить на клиентских. Естественно, тоже по нарастающей, стартовав с dev-кластеров, которые можно было обновить даже с небольшим простоем.
Первые апгрейды и первое препятствие
На этапе обновления кластеров без нагрузки всё выглядело хорошо, но в какой-то момент при обновлении кластеров с нагрузкой столкнулись с проблемой, что в кластерах продолжал работать Cluster Autoscaler, который хотел заказать дополнительные узлы. Для заказа узлов в Deckhouse используется Machine Controller Manager: поскольку инженеры «Фланта» активно контрибьютят в этот проект, мы были уверены в его совместимости со всеми версиями Kubernetes и не отключали его. Но при выключенном Cloud Controller Manager узлы не могли перейти в состояние Ready
. Почему?
Как упоминалось ранее, в модуле node-manager есть механизм защиты: узлы обновляются только по одному и только при условии, что все узлы в NodeGroup находятся в состоянии Ready
. Получается, что мы сами себе выстрелили в ногу, заблокировав возможность обновления узлов, когда кластер начинает масштабироваться.
Кластеры, в которых заблокировался процесс обновления, пришлось вручную «проталкивать» за счёт изменения количества узлов в NodeGroup и удаления новых заказанных узлов, которые не могли перейти в Ready
. А глобальное решение, конечно, пришло быстро: необходимо было выключить Machine Controller Manager и Cluster Autoscaler вместе с другими компонентами, которые уже были отключены в облачных кластерах.
Массовые обновления
К концу декабря 2020 года мы имели около 50 кластеров Kubernetes, обновленных до 1.19. До сих пор выполнялось параллельное обновление по несколько (2-3, максимум — 5) кластеров. Но так как уверенность в стабильности и корректности обновления росла, отлично отметив 2021-й Новый Год, мы решили запустить обновление около 100 оставшихся кластеров одновременно, чтобы к концу января иметь только парочку последних кластеров на 1.16. Кластеры были разбиты на две группы, в каждой — около 50 кластеров.
В процессе обновления первой группы кластеров столкнулись только с одной проблемой. Один из узлов кластера была постоянно в NotReady
, а в kubelet сыпались ошибки:
Failed to initialize CSINodeInfo: error updating CSINode annotation
Отлаживать, к сожалению, времени не было: так как кластер находился на железе, которое почти всегда полностью загружено, выход одного узла из строя уже начинал сказываться на работоспособности приложения. Было найдено быстрое решение, которое, надеюсь, поможет и вам, если столкнётесь с похожей проблемой. Однако повторюсь, данная проблема в итоге возникла только на одном(!) из 150 кластеров в процессе обновления, поэтому вероятность получить такую же проблему минимальна.
Чтобы успеть в сроки, которые мы сами себе обозначили, на 28 января в 00:00 было запланировано обновление второй группы кластеров, в которой остались самые нагруженные producton’ы: простой в них приводит к написанию постмортемов, нарушению SLA и штрафам. Процесс обновления контролировал лично наш CTO — @distol… И всё прошло как по маслу: совсем ничего примечательно во время обновления не случилось, никакое ручное вмешательство не потребовалось. В нашем Telegram-канале появилось радостное сообщение:
Последняя боль
Но позже выяснилось, что нам досталась одна проблема, которая произошла из-за обновления, но последствия её проявились только через пару дней (и как обычно в одном из самых нагруженных и ответственных проектов…).
Во время обновления несколько раз перезапускается kube-apiserver. Исторически сложилось, что в части кластеров под управлением Deckhouse активно используется flannel, где всё это время была упоминавшаяся ранее в статье проблема с зависанием Go-клиента из-за отсутствия health check для HTTP/2-соединений при обращении к API Kubernetes. В итоге, на части узлов в кластере в логах flannel появились ошибки:
E0202 21:52:35.791600 1 reflector.go:201] github.com/coreos/flannel/subnet/kube/kube.go:310: Failed to list *v1.Node: Get https://192.168.0.1:443/api/v1/nodes?resourceVersion=0: dial tcp 192.168.0.1:443: connect: connection refused
Это привело к тому, что на этих узлах некорректно работал CNI и проскакивали 500-е ошибки при обращении к сервисам. Рестарт flannel исправил проблему, а для глобального решения необходимо обновлять flannel.
К счастью, относительно недавно (15 декабря 2020) появился pull request, который обновляет версию client-go. На момент написания данной статьи это исправление доступно в релизе v0.13.1-rc2, и мы планируем глобально обновить версию flannel во всех кластерах под управлением Deckhouse.
В чем же удовольствие?
Сейчас у нас не осталось ни одного кластера на 1.16: все 150+ кластеров — на версии Kubernetes 1.19. Обновление прошло достаточно гладко — было очень приятно наблюдать, как автоматика решила все вопросы и как на тысячах узлов в течение буквально часа почти параллельно меняется значение Version
в выводе команды kubectl get nodes
. Пожалуй, это было завораживающее зрелище — мы действительно получили большое удовольствие от того, как проходило обновление Kubernetes.
У нас уже готова и поддержка версии Kubernetes 1.20, так что в скором времени мы обновим кластеры на неё по аналогичной схеме. А уж если столкнёмся с какими-то интересными особенностями — обязательно об этом расскажем.
P.S.
Читайте также в нашем блоге: