company_banner

3 года с Kubernetes в production: вот что мы поняли

Автор оригинала: Komal Venkatesh Ganesan
  • Перевод
Прим. перев.: в очередной статье из категории «lessons learned» DevOps-инженер австралийской компании делится главными выводами по итогам продолжительного использования Kubernetes в production для нагруженных сервисов. Автор затрагивает вопросы Java, CI/CD, сетей, а также сложности K8s в целом.

Свой первый кластер Kubernetes мы начали создавать в 2017 году (с версии K8s 1.9.4). У нас было два кластера. Один работал на bare metal, на виртуальных машинах RHEL, другой — в облаке AWS EC2.

На сегодняшний день наша инфраструктура насчитывает более 400 виртуальных машин, разбросанных по нескольким дата-центрам. Платформа выступает базой для высокодоступных критически важных приложений и систем, которые управляют огромной сетью, включающей почти 4 миллиона активных устройств.

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

Вот ключевые уроки, которые мы вынесли из опыта использования Kubernetes в production на протяжении трех лет.

1. Занимательная история с Java-приложениями


Когда речь заходит о микросервисах и контейнеризации, инженеры, как правило, избегают использования Java, прежде всего из-за ее печально известного несовершенного управления памятью. Впрочем, сегодня ситуация обстоит иначе и совместимость Java с контейнерами в последние годы улучшилась. В конце концов, даже такие популярные системы, как Apache Kafka и Elasticsearch, работают на Java.

В 2017—2018 годах некоторые наши приложения работали на Java восьмой версии. Частенько они отказывались функционировать в контейнерных средах вроде Docker и падали из-за проблем с heap-памятью и неадекватной работы сборщиков мусора. Как оказалось, эти проблемы были вызваны неспособностью JVM работать с механизмами контейнеризации Linux (cgroups и namespaces).

С тех пор Oracle приложила значительные усилия, чтобы повысить совместимость Java с миром контейнеров. Уже в 8-ой версии Java появились экспериментальные флаги JVM для решения этих проблем: XX:+UnlockExperimentalVMOptions и XX:+UseCGroupMemoryLimitForHeap.

Но, несмотря на все улучшения, никто не будет спорить, что у Java по-прежнему плохая репутация из-за чрезмерного потребления памяти и медленного запуска по сравнению с Python или Go. В первую очередь это связано со спецификой управления памятью в JVM и ClassLoader'ом.

Сегодня, если нам приходится работать с Java, мы по крайней мере стараемся использовать версию 11 или выше. И наши лимиты на память в Kubernetes на 1 Гб выше, чем ограничение на максимальный объем heap-памяти в JVM (-Xmx) (на всякий случай). То есть, если JVM использует 8 Гб под heap-память, лимит в Kubernetes на память для приложения будет установлен на 9 Гб. Благодаря этим мерам и улучшениям жизнь стала чуточку легче.

2. Обновления, связанные с жизненным циклом Kubernetes


Управление жизненным циклом Kubernetes (обновления, дополнения) — вещь громоздкая и непростая, особенно если кластер базируется на bare metal или виртуальных машинах. Оказалось, что для перехода на новую версию гораздо проще поднять новый кластер и потом перенести в него рабочие нагрузки. Обновление существующих узлов попросту нецелесообразно, поскольку связано со значительными усилиями и тщательным планированием.

Дело в том, что в Kubernetes слишком много «движущихся» частей, которые необходимо учитывать при проведении обновлений. Для того, чтобы кластер мог работать, приходится собирать все эти компоненты вместе — начиная с Docker и заканчивая CNI-плагинами вроде Calico или Flannel. Такие проекты, как Kubespray, KubeOne, kops и kube-aws, несколько упрощают процесс, однако все они не лишены недостатков.

Свои кластеры мы разворачивали в виртуальных машинах RHEL с помощью Kubespray. Он отлично себя зарекомендовал. В Kubespray были сценарии для создания, добавления или удаления узлов, обновления версии и почти все, что необходимо для работы с Kubernetes в production. При этом сценарий обновления сопровождался предостережением о том, что нельзя пропускать даже второстепенные (minor) версии. Другими словами, чтобы добраться до нужной версии, пользователю приходилось устанавливать все промежуточные.

Главный вывод здесь в том, что если вы планируете использовать или уже используете Kubernetes, продумайте свои шаги, связанные с жизненным циклом K8s и то, как он вписывается в ваше решение. Создать и запустить кластер часто оказывается проще, чем поддерживать его в актуальном состоянии.

3. Сборка и деплой


Будьте готовы к тому, что придется пересмотреть пайплайны сборки и деплоя. При переходе на Kubernetes у нас прошла радикальная трансформация этих процессов. Мы не только реструктурировали пайплайны Jenkins, но с помощью инструментов, таких как Helm, разработали новые стратегии сборки и работы с Git'ом, тегирования Docker-образов и версионирования Helm-чартов.

Вам понадобится единая стратегия для поддержки кода, файлов с deployment’ами Kubernetes, Dockerfiles, образов Docker'а, Helm-чартов, а также способ связать все это вместе.

После нескольких итераций мы остановились на следующей схеме:

  • Код приложения и его Helm-чарты находятся в разных репозиториях. Это позволяет нам версионировать их независимо друг от друга (семантическое версионирование).
  • Затем мы сохраняем карту с данными о том, какая версия чарта к какой версии приложения привязана, и используем ее для отслеживания релиза. Так, например, app-1.2.0 развертывается с charts-1.1.0. Если меняется только файл с параметрами (values) для Helm, то меняется только patch-составляющая версии (например, с 1.1.0 на 1.1.1). Все требования к версиям описываются в примечаниях к релизу (RELEASE.txt) в каждом репозитории.
  • К системным приложениям, таким как Apache Kafka или Redis (чей код мы не собирали и не модифицировали), у нас иной подход. Нам не были нужны два репозитория, поскольку Docker-тег был просто частью версионирования Helm-чартов. Меняя Docker-тег для обновления, мы просто увеличиваем основную версию в теге чарта.

(Прим. перев.: сложно пройти мимо такой трансформации и не указать на нашу Open Source-утилиту для сборки и доставки приложений в Kubernetes — werf — как один из способов упростить решение тех проблем, с которыми столкнулись авторы статьи.)

4. Тесты Liveness и Readiness (обоюдоострый меч)


Проверки работоспособности (liveness) и готовности (readiness) Kubernetes отлично подходят для автономной борьбы с системными проблемами. Они могут перезапускать контейнеры при сбоях и перенаправлять трафик с «нездоровых» экземпляров. Но в некоторых условиях эти проверки могут превратиться в обоюдоострый меч и повлиять на запуск и восстановление приложения (это особенно актуально для stateful-приложений, таких как платформы обмена сообщениями или базы данных).

Наш Kafka стал их жертвой. У нас был stateful set из 3 Broker'ов и 3 Zookeeper'ов с replicationFactor = 3 и minInSyncReplica = 2. Проблема возникала при перезапуске Kafka после случайных сбоев или падений. Во время старта Kafka запускал дополнительные скрипты для исправления поврежденных индексов, что занимало от 10 до 30 минут в зависимости от серьезности проблемы. Такая задержка приводила к тому, что liveness-тесты постоянно завершались неудачей, из-за чего Kubernetes «убивал» и перезапускал Kafka. В результате Kafka не мог не только исправить индексы, но даже стартовать.

Единственным решением на тот момент виделась настройка параметра initialDelaySeconds в настройках liveness-тестов, чтобы проверки проводились только после запуска контейнера. Главная сложность, конечно, в том, чтобы решить, какую именно задержку установить. Отдельные запуски после сбоя могут занимать до часа времени, и это необходимо учитывать. С другой стороны, чем больше initialDelaySeconds, тем медленнее Kubernetes будет реагировать на сбои во время запуска контейнеров.

В данном случае золотой серединой является значение initialDelaySeconds, которое лучше всего удовлетворяет вашим требованиям к устойчивости и в то же время дает приложению достаточно времени для успешного запуска во всех сбойных ситуациях (отказы диска, проблемы с сетью, системные сбои и т.д.)

Обновление: в свежих версиях Kubernetes появился третий тип тестов под названием startup probe. Он доступен как альфа-версия, начиная с релиза 1.16, и как бета-версия с 1.18.

Startup probe позволяет решить вышеописанную проблему, отключая readiness и liveness-проверки до тех пор, пока контейнер не запустится, тем самым позволяя приложению нормально стартовать.

5. Работа с внешними IP


Как оказалось, использование статических внешних IP для доступа к сервисам оказывает серьезное давление на механизм отслеживания соединений системного ядра. Если его не продумать тщательно, он может «сломаться».

В своем кластере мы используем Calico как CNI и BGP в качестве протокола маршрутизации, а также для взаимодействия с пограничными маршрутизаторами. В Kube-proxy задействован режим iptables. Доступ к нашему очень загруженному сервису в Kubernetes (ежедневно он обрабатывает миллионы подключений) открываем через внешний IP. Из-за SNAT и маскировки, проистекающих от программно-определяемых сетей, Kubernetes нуждается в механизме отслеживания всех этих логических потоков. Для этого K8s задействует такие инструменты ядра, как сonntrack и netfilter. С их помощью он управляет внешними подключениями к статическому IP, который затем преобразуется во внутренний IP сервиса и, наконец, в IP-адрес pod'а. И все это делается с помощью таблицы conntrack и iptables.

Однако возможности таблицы conntrack небезграничны. При достижении лимита кластер Kubernetes (точнее, ядро ОС в его основе) больше не сможет принимать новые соединения. В RHEL этот предел можно проверить следующим образом:

$  sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_maxnet.netfilter.nf_conntrack_count = 167012
net.netfilter.nf_conntrack_max = 262144

Один из способов обойти это ограничение — объединить несколько узлов с пограничными маршрутизаторами, чтобы входящие соединения на статический IP распределялись по всему кластеру. В случае, если у вас в кластере большой парк машин, такой подход позволяет значительно увеличить размер таблицы conntrack для обработки очень большого числа входящих соединений.

Это полностью сбило нас с толку, когда мы только начинали в 2017 году. Однако сравнительно недавно (в апреле 2019-го) проект Calico опубликовал подробное исследование под метким названием «Why conntrack is no longer your friend» (есть такой её перевод на русский язык — прим. перев.).

Действительно ли вам нужен Kubernetes?


Прошло три года, но мы до сих пор продолжаем открывать/узнавать что-то новое каждый день. Kubernetes — сложная платформа со своим набором вызовов, особенно в области запуска окружения и поддержания его в рабочем состоянии. Она изменит ваше мышление, архитектуру, отношение к проектированию. Вам придется заниматься расширением масштабов деятельности команд и повышением их квалификации.

С другой стороны, работа в облаке и возможность использовать Kubernetes как услугу избавит вас от большинства забот, связанных с обслуживанием платформы (вроде расширения CIDR внутренней сети и обновления Kubernetes).

Сегодня мы пришли к пониманию того, что главный вопрос, который следует задать себе — действительно ли вам нужен Kubernetes? Он поможет оценить, насколько глобальна имеющаяся проблема и поможет ли с ней справиться Kubernetes.

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

Помните, что технология исключительно ради технологии бессмысленна.

P.S. от переводчика


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

Флант
DevOps-as-a-Service, Kubernetes, обслуживание 24×7

Комментарии 22

    +4
    А есть ссылка на оригинал, с которого делался перевод?
      +1
      Ой-ой, не проставил по ошибке. Исправлено! Спасибо!
      0

      Вот ответ на вопрос: почему java процессу нужно +1 GB памяти? (на самом деле может быть больше)
      Андрей Паньгин — Память Java процесса по полочкам

        +2
        Кстати, к автору оригинала в комментарии (на Medium) пришел Ruslan Synytsky (Jelastic) и предложил ему поучаствовать в «improving JVM in terms of memory usage efficiency and its elasticity». Вдруг тут ещё кому актуально присоединиться… ;-)
          +1

          Для некоторых классов приложений Xmx вообще слабо связан с реальным потреблением памяти, так как данные выносятся в off-heap. Странно что за 3 года эксплуатации явы в кубере они это не осознали )
          На самом деле проблема выглядит если не надуманной, то уж явно преувеличенной. Гоняем яву в кубере (опеншифт) на проде уже несколько лет, полёт нормальный. А вот liveness/readyness пробы при старте stateful контейнеров это боль, да.
          Вообще stateful штукам (БД, Кафка и т.п.) не место в кубере, особенно на распределённой ФС. Проблем много, выгода неочевидна.

            –1
            Для некоторых классов приложений

            Можете привести примеры таких (классов) приложений?

            данные выносятся в off-heap

            И где по вашему мнению эти данные хранятся и как определить сколько они заняли выделенных им ресурсов?

            Вообще stateful штукам (БД, Кафка и т.п.) не место в кубере

            А где им место?
              0

              Приложения где нужен быстрый доступ к памяти через Unsafe без лишней сборки мусора.

                +3
                > Можете привести примеры таких (классов) приложений?
                Почти все кэши, например Apache Ignite. В любом приложении, где надо хранить много данных в памяти, стоит посмотреть на такую возможность.

                > И где по вашему мнению эти данные хранятся и как определить сколько они заняли выделенных им ресурсов?
                Вне зависимости от «моего мнения» :-) они хранятся в оперативной памяти вне java хипа, т.е. настройка Xmx на их хранение не действует (и GC туда не ходит). Максимальный размер, который можно съесть в off heap, хранится в настройках приложения (каких именно — зависит от конкретного приложения).

                > А где им место?
                На отдельных серверах с «честной» файловой системой, вне кубера. Всё равно вы не сможете отмасштабировать тот же самый PostgreSQL просто увеличив кол-во подов.
                  0
                  Благодарю за развернутый ответ, теперь более понятно ваше мнение ;)
                –1
                Stateful прекрасно живет в кубере, если к этому готов. Тот же ElasticSearch
                +1

                Я понимаю, что Вы и так знаете то, что я опишу ниже, но здесь — самое место для моего комментария.


                +1 Гб — это магическое число, взятое с потолка.


                Мы вместо Xms/Xmx используем -XX:MaxRAM=2g, тогда JVM не вылазит за границы, очерченные лимитами контейнера. В этом случае куча (heap) приложения займет примерно 1.2g (кажется, на хип отводится 60% от MaxRAM).


                Получается примерно то же самое, что определить Xmx=1g, а в лимите прописать 2g (+1Gb как рекоммендует автор), но уже без магии.

                  0

                  Распределение памяти в этом случае задается через параметры: InitialRAMPercentage, MinRAMPercentage, MaxRAMPercentage
                  В любом случае это не панацея, а лишь другой способ определения размера heap внутри контейнера. В отдельных случаях выход за пределы выделенной памяти все еще возможен.

                    0
                    В отдельных случаях выход за пределы выделенной памяти все еще возможен.

                    В каких?

                –1
                с помощью Kubespray. Он отлично себя зарекомендовал

                очень несогласен, более глючную и тормознутую (ansible) тулзу для раскатки k8s еще поискать надо…
                  0

                  Обычно после такого комментария человек пишет хорошую альтернативу, а не просто набрасывает и уходит в тень)

                    –1

                    Их десятки и наверно ближе к сотни… перечислять не вижу смыла, выбирайте по своим требованиям.

                      0
                      Так вот я, как и автор, выбрал kubespray и доволен.
                        0
                        Ну вы хоть 1 назовите, который объективно лучше kubespray?
                          0

                          Например k3s..

                            0
                            k3s это minikube — вид сбоку.
                            Не знаю людей, которые его в здравом уме в prod затащили

                            Основное отличие Kubespray от всех других подделок — это production ready решение, говорю на основе больше 2 летнего пользования его в проде.
                              –2
                              1. Prod бывает разный
                              2. Сколько раскаток в час вы производите?
                              3. процитирую себя же: "перечислять не вижу смыла, выбирайте по своим требованиям" — предлагаете мне телепатически угадать все ваши хотелки?
                              4. извольте, но за бесплатно я не буду подбирать дистр под Ваши требования, просили пример — я привел
                                +1
                                Голословничайте дальше.
                                > более глючную и тормознутую
                                > Их десятки и наверно ближе к сотни

                                Будет какая-нибудь конкретика, можем продолжить дискус.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое