- PVSM.RU - https://www.pvsm.ru -
Прим. перев.: в очередной статье из категории «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 на протяжении трех лет.
Когда речь заходит о микросервисах и контейнеризации, инженеры, как правило, избегают использования Java, прежде всего из-за ее печально известного несовершенного управления памятью. Впрочем, сегодня ситуация обстоит иначе и совместимость Java с контейнерами в последние годы улучшилась. В конце концов, даже такие популярные системы, как Apache Kafka и Elasticsearch, работают на Java.
В 2017—2018 годах некоторые наши приложения работали на Java восьмой версии. Частенько они отказывались функционировать в контейнерных средах вроде Docker и падали из-за проблем с heap-памятью и неадекватной работы сборщиков мусора. Как оказалось, эти проблемы были вызваны неспособностью JVM [2] работать с механизмами контейнеризации 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 Гб. Благодаря этим мерам и улучшениям жизнь стала чуточку легче.
Управление жизненным циклом Kubernetes (обновления, дополнения) — вещь громоздкая и непростая, особенно если кластер базируется на bare metal или виртуальных машинах [3]. Оказалось, что для перехода на новую версию гораздо проще поднять новый кластер и потом перенести в него рабочие нагрузки. Обновление существующих узлов попросту нецелесообразно, поскольку связано со значительными усилиями и тщательным планированием.
Дело в том, что в Kubernetes слишком много «движущихся» частей, которые необходимо учитывать при проведении обновлений. Для того, чтобы кластер мог работать, приходится собирать все эти компоненты вместе — начиная с Docker и заканчивая CNI-плагинами вроде Calico или Flannel. Такие проекты, как Kubespray, KubeOne, kops и kube-aws, несколько упрощают процесс, однако все они не лишены недостатков.
Свои кластеры мы разворачивали в виртуальных машинах RHEL с помощью Kubespray. Он отлично себя зарекомендовал. В Kubespray были сценарии для создания, добавления или удаления узлов, обновления версии и почти все, что необходимо для работы с Kubernetes в production. При этом сценарий обновления сопровождался предостережением о том, что нельзя пропускать даже второстепенные (minor) версии. Другими словами, чтобы добраться до нужной версии, пользователю приходилось устанавливать все промежуточные.
Главный вывод здесь в том, что если вы планируете использовать или уже используете Kubernetes, продумайте свои шаги, связанные с жизненным циклом K8s и то, как он вписывается в ваше решение. Создать и запустить кластер часто оказывается проще, чем поддерживать его в актуальном состоянии.
Будьте готовы к тому, что придется пересмотреть пайплайны сборки и деплоя. При переходе на Kubernetes у нас прошла радикальная трансформация этих процессов. Мы не только реструктурировали пайплайны Jenkins, но с помощью инструментов, таких как Helm, разработали новые стратегии сборки и работы с Git'ом, тегирования Docker-образов и версионирования Helm-чартов.
Вам понадобится единая стратегия для поддержки кода, файлов с deployment’ами Kubernetes, Dockerfiles, образов Docker'а, Helm-чартов, а также способ связать все это вместе.
После нескольких итераций мы остановились на следующей схеме:
app-1.2.0
развертывается с charts-1.1.0
. Если меняется только файл с параметрами (values
) для Helm, то меняется только patch-составляющая версии (например, с 1.1.0
на 1.1.1
). Все требования к версиям описываются в примечаниях к релизу (RELEASE.txt
) в каждом репозитории.(Прим. перев.: сложно пройти мимо такой трансформации и не указать на нашу Open Source-утилиту для сборки и доставки приложений в Kubernetes — werf [5] — как один из способов упростить решение тех проблем, с которыми столкнулись авторы статьи.)
Проверки работоспособности (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 появился [6] третий тип тестов под названием startup probe. Он доступен как альфа-версия, начиная с релиза 1.16 [7], и как бета-версия с 1.18.
Startup probe позволяет решить вышеописанную проблему, отключая readiness и liveness-проверки до тех пор, пока контейнер не запустится, тем самым позволяя приложению нормально стартовать.
Как оказалось, использование статических внешних 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 [8]» (есть такой её перевод [9] на русский язык — прим. перев.).
Прошло три года, но мы до сих пор продолжаем открывать/узнавать что-то новое каждый день. Kubernetes — сложная платформа со своим набором вызовов, особенно в области запуска окружения и поддержания его в рабочем состоянии. Она изменит ваше
С другой стороны, работа в облаке и возможность использовать Kubernetes как услугу [10] избавит вас от большинства забот, связанных с обслуживанием платформы (вроде расширения CIDR внутренней сети и обновления Kubernetes).
Сегодня мы пришли к пониманию того, что главный вопрос, который следует задать себе — действительно ли вам нужен Kubernetes? Он поможет оценить, насколько глобальна имеющаяся проблема и поможет ли с ней справиться Kubernetes.
Дело в том, что переход на Kubernetes обходится дорого. Поэтому плюсы от вашего сценария использования (и то, насколько и как он задействует платформу) должны оправдывать цену, которую вы заплатите. Если это так, то Kubernetes может существенно повысить вашу производительность.
Помните, что технология исключительно ради технологии бессмысленна.
Читайте также в нашем блоге:
Автор: Дмитрий Шурупов
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/357179
Ссылки в тексте:
[1] мышления: http://www.braintools.ru
[2] вызваны неспособностью JVM: https://developers.redhat.com/blog/2017/03/14/java-inside-docker/
[3] bare metal или виртуальных машинах: https://platform9.com/blog/where-to-install-kubernetes-bare-metal-vs-vms-vs-cloud/
[4] семантическое версионирование: https://semver.org/
[5] werf: https://ru.werf.io/
[6] появился: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
[7] релиза 1.16: https://habr.com/ru/company/flant/blog/467477/
[8] Why conntrack is no longer your friend: https://www.projectcalico.org/when-linux-conntrack-is-no-longer-your-friend/
[9] такой её перевод: https://habr.com/ru/company/nixys/blog/492686/
[10] Kubernetes как услугу: https://flant.ru/services/managed-kubernetes-as-a-service
[11] 10 типичных ошибок при использовании Kubernetes: https://habr.com/ru/company/flant/blog/504396/
[12] Liveness probes в Kubernetes могут быть опасны: https://habr.com/ru/company/flant/blog/470958/
[13] Истории успеха Kubernetes в production. Часть 10: Reddit: https://habr.com/ru/company/flant/blog/441754/
[14] Источник: https://habr.com/ru/post/519962/?utm_source=habrahabr&utm_medium=rss&utm_campaign=519962
Нажмите здесь для печати.