На начало декабря 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 поддерживаются только 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% кластеров были готовы к обновлению. В начале статьи уже кратко упоминался 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’е следующего формата: Для обновления достаточно изменить значение 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, то есть нет узлов в процессе заказа. (Это касается, конечно же, только облачных кластеров, где есть автоматический заказ новых виртуальных машин.) Итак, мы решили обновляться сразу на 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 появились ошибки: Это привело к тому, что на этих узлах некорректно работал 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, так что в скором времени мы обновим кластеры на неё по аналогичной схеме. А уж если столкнёмся с какими-то интересными особенностями — обязательно об этом расскажем. Читайте также в нашем блоге: «3 года с Kubernetes в production: вот что мы поняли»; «Kubernetes 1.19: обзор основных новшеств»; «Наш опыт с Kubernetes в небольших проектах» (обзор и видео доклада);