Русский
Русский
English
Статистика
Реклама

Оркестрация

Перевод Пять промахов при развертывании первого приложения на Kubernetes

23.09.2020 18:06:49 | Автор: admin
Fail by Aris-Dreamer

Многие считают, что достаточно перенести приложение на Kubernetes (либо с помощью Helm, либо вручную) и будет счастье. Но не всё так просто.

Команда Mail.ru Cloud Solutions перевела статью DevOps-инженера Джулиана Гинди. Он рассказывает, с какими подводными камнями его компания столкнулась в процессе миграции, чтобы вы не наступали на те же грабли.

Шаг первый: настройка запросов пода и лимитов


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

Запросы пода (pod requests) это основное значение, используемое планировщиком для оптимального размещения пода.

Из документации Kubernetes: на шаге фильтрации определяется набор узлов, где можно запланировать под. Например, фильтр PodFitsResources проверяет, достаточно ли на узле ресурсов для удовлетворения конкретных запросов пода на ресурсы.

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

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

Лимиты пода (pod limits) это более четкое ограничение для пода. Оно представляет собой максимальный объем ресурсов, который кластер выделит контейнеру.

Опять же, из официальной документации: если для контейнера установлен лимит памяти 4 ГиБ, то kubelet (и среда выполнения контейнера) введет его принудительно. Среда выполнения не позволяет контейнеру использовать больше заданного лимита ресурсов. Например, когда процесс в контейнере пытается использовать больше допустимого объема памяти, ядро системы завершает этот процесс с ошибкой out of memory (OOM).

Контейнер всегда может использовать больше ресурсов, чем указано в запросе на ресурсы, но никогда не может использовать больше, чем указано в ограничении. Это значение сложно установить правильно, но оно очень важно.

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

К сожалению, я не могу дать конкретные указания, какие значения устанавливать, но мы сами придерживаемся следующих правил:

  1. Используя инструмент нагрузочного тестирования, моделируем базовый уровень трафика и наблюдаем за использованием ресурсов пода (памяти и процессора).
  2. Устанавливаем запросы пода на произвольно низкое значение (с ограничением ресурсов примерно в 5 раз больше значения запросов) и наблюдаем. Когда запросы на слишком низком уровне, процесс не может начаться, что часто вызывает загадочные ошибки времени выполнения Go.

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

Представьте ситуацию, когда у вас легковесный веб-сервер с очень высоким ограничением ресурсов, например 4 ГБ памяти. Вероятно, этот процесс придется масштабировать горизонтально и каждый новый модуль придется планировать на узле с доступным объемом памяти не менее 4 ГБ. Если такого узла не существует, кластер должен ввести новый узел для обработки этого пода, что может занять некоторое время. Важно добиться минимальной разницы между запросами ресурсов и лимитами, чтобы обеспечить быстрое и плавное масштабирование.

Шаг второй: настройка тестов Liveness и Readiness


Это еще одна тонкая тема, которая часто обсуждается в сообществе Kubernetes. Важно хорошо разбираться в тестах жизнеспособности (Liveness) и готовности (Readiness), поскольку они обеспечивают механизм устойчивой работы программного обеспечения и минимизируют время простоя. Однако они могут нанести серьезный удар по производительности вашего приложения, если не настроены правильно. Ниже приводится краткое изложение, что из себя представляют обе пробы.

Liveness показывает, работает ли контейнер. Если она выходит из строя, kubelet убивает контейнер, и для него включается политика перезапуска. Если контейнер не оснащен Liveness-пробой, то состоянием по умолчанию будет успех так говорится в документации Kubernetes.

Пробы Liveness должны быть дешевыми, то есть не потреблять много ресурсов, потому что они запускаются часто и должны информировать Kubernetes, что приложение запущено.

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

У нас в компании тесты Liveness проверяют основные компоненты приложения, даже если данные (например, из удаленной базы данных или кэша) не полностью доступны.

Мы настроили в приложениях конечную точку работоспособности, которая просто возвращает код ответа 200. Это показатель того, что процесс запущен и способен обрабатывать запросы (но еще не трафик).

Проба Readiness указывает, готов ли контейнер к обслуживанию запросов. Если проба готовности выходит из строя, контроллер конечных точек удаляет IP-адрес пода из конечных точек всех служб, соответствующих поду. Это также говорится в документации Kubernetes.

Пробы Readiness потребляют больше ресурсов, так как они должны попадать в бэкенд таким образом, чтобы показать готовность приложения к приему запросов.

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

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

SELECT small_item FROM table LIMIT 1

Вот пример, как мы настраиваем эти два значения в Kubernetes:

livenessProbe:  httpGet:      path: /api/liveness       port: http readinessProbe:   httpGet:       path: /api/readiness       port: http  periodSeconds: 2

Можно добавить некоторые дополнительные параметры конфигурации:

  • initialDelaySeconds сколько секунд пройдет между запуском контейнера и началом запуска проб.
  • periodSeconds интервал ожидания между запусками проб.
  • timeoutSeconds количество секунд, по истечении которых под считается аварийным. Обычный тайм-аут.
  • failureThreshold количество отказов тестов, прежде чем в под будет отправлен сигнал перезапуска.
  • successThreshold количество успешных проб, прежде чем под переходит в состояние готовности (после сбоя, когда под запускается или восстанавливается).

Шаг третий: настройка дефолтных сетевых политик пода


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

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

Например, ниже приведена простая политика, которая запрещает весь входящий трафик для конкретного пространства имен:

---apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata:   name: default-deny-ingressspec:   podSelector: {}   policyTypes:     - Ingress

Визуализация этой конфигурации:


(http://personeltest.ru/aways/miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Более подробно здесь.

Шаг четвертый: нестандартное поведение с помощью хуков и init-контейнеров


Одной наших из главных задач было обеспечение деплоев в Kubernetes без простоя для разработчиков. Это трудно из-за того, что существует множество вариантов завершения работы приложений и освобождения ими использованных ресурсов.

Особые трудности возникли с Nginx. Мы заметили, что при последовательном развертывании этих подов активные соединения прерывались до успешного завершения.

После обширных изысканий в интернете выяснилось, что Kubernetes не ждет, пока соединения Nginx исчерпают себя, прежде чем завершить работу пода. С помощью pre-stop хука мы внедрили такую функциональность и полностью избавились от даунтайма:

lifecycle:  preStop:   exec:     command: ["/usr/local/bin/nginx-killer.sh"]

А вот nginx-killer.sh:

#!/bin/bashsleep 3PID=$(cat /run/nginx.pid)nginx -s quitwhile [ -d /proc/$PID ]; do   echo "Waiting while shutting down nginx..."   sleep 10done

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

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

Как обычно, цитата из документации: init-контейнеры безопасно запускают пользовательский код или утилиты, которые иначе снизят безопасность образа контейнера приложения. Храня отдельно ненужные инструменты, вы ограничиваете поверхность атаки образа контейнера приложения.

Шаг пятый: настройка ядра


Напоследок расскажем о более продвинутой технике.

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

Однако Kubernetes позволяет запустить привилегированный контейнер, который изменяет параметры ядра только для конкретного пода. Вот что мы использовали для изменения максимального количества открытых соединений:

initContainers:  - name: sysctl     image: alpine:3.10     securityContext:         privileged: true      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

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

В заключение


Хотя Kubernetes может показаться готовым решением из коробки, для бесперебойной работы приложений необходимо предпринять несколько ключевых шагов.

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

Реалистично оцените ожидаемый трафик и попробуйте выйти за его предел, чтобы посмотреть, какие компоненты сломаются первыми. С таким итерационным подходом для достижения успеха может хватить лишь нескольких из перечисленных рекомендаций. Либо может потребоваться более глубокая настройка.

Всегда задавайте себе такие вопросы:

  1. Сколько ресурсов потребляют приложения и как изменится этот объем?
  2. Каковы реальные требования к масштабированию? Сколько трафика в среднем будет обрабатывать приложение? А как насчет пикового трафика?
  3. Как часто сервису потребуется горизонтальное масштабирование? Как быстро нужно вводить в строй новые поды, чтобы принимать трафик?
  4. Насколько корректно завершается работа подов? Нужно ли это вообще? Можно ли добиться развертывания без даунтайма?
  5. Как минимизировать риски для безопасности и ограничить ущерб от любых скомпрометированных подов? Есть ли у каких-то сервисов разрешения или доступы, которые им не требуются?

Kubernetes предоставляет невероятную платформу, которая позволяет использовать лучшие практики для развертывания тысяч сервисов в кластере. Тем не менее все приложения разные. Иногда внедрение требует немного больше работы.

К счастью, Kubernetes предоставляет необходимые настройки для достижения всех технических целей. Используя комбинацию запросов ресурсов и лимитов, проб Liveness и Readiness, init-контейнеров, сетевых политик и нестандартной настройки ядра, вы можете добиться высокой производительности наряду с отказоустойчивостью и быстрой масштабируемостью.

Что еще почитать:

  1. Лучшие практики и рекомендации для запуска контейнеров и Kubernetes в производственных средах.
  2. 90+ полезных инструментов для Kubernetes: развертывание, управление, мониторинг, безопасность и не только.
  3. Наш канал Вокруг Kubernetes в Телеграме.
Подробнее..

Self-Hosted, или Kubernetes для богатых почему самостоятельное развертывание кластера не всегда способ сэкономить

02.06.2021 18:06:54 | Автор: admin


Идея самостоятельно развернуть кластер Kubernetes на собственных серверах или в облаке выглядит привлекательной: кажется, что это дешевле, чем платить за Managed-решение от провайдера. На самом деле все не так однозначно: на практике можно обнаружить скрытые расходы и подводные камни.


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


Я Дмитрий Лазаренко, директор по продукту облачной платформы Mail.ru Cloud Solutions (MCS). В статье расскажу, в чем особенности развертывания Self-Hosted-кластера Kubernetes и о чем нужно знать перед запуском.


Для старта понадобятся время, деньги и администраторы, разбирающиеся в Kubernetes


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


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


В реальности развернуть кластер только половина дела. В таком виде он будет работать до первой проблемы, которая неизбежно возникнет через неделю или месяц. Например, перестанут создаваться поды из-за неверной конфигурации ресурсов на controller-manager. Или кластер начнет работать нестабильно из-за проблем с дисками у etcd. Или запущенные СronJob из-за ошибок controller-manager начнут бесконечно плодить новые поды. Или в кластере будут возникать сетевые ошибки из-за неправильного выбора конфигурации DNS.


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


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


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


Конечно, если запускать Kubernetes только ради деплоя контейнеров, то можно не разбираться и не развивать кластер. Но тогда возникает вопрос: зачем вам Kubernetes? Можно взять более простой в настройке и поддержке инструмент, тот же Docker Swarm. Если вы хотите от Kubernetes что-то простое, просто его не используйте. Нет смысла тратить время на развертывание кластера лишь ради запуска простого кода. Эта технология предназначена для проектов, где постоянно идет разработка, часто запускаются новые релизы и нужно выдерживать требования HighLoad.

По этой причине Self-Hosted Kubernetes в большинстве случаев могут успешно запустить только крупные компании, где есть возможность выделить сотрудников для обслуживания кластера и нет потребности экономить ресурсы.


Кроме того, самостоятельное развертывание кластера дело небыстрое. Если понадобится запустить кластер в короткие сроки для проекта или тестовых сред, то на Self-Hosted это не выйдет: развертывание займет несколько часов, а то и недель. К этому стоит быть готовыми. Для сравнения: в облаке вы запустите кластер KaaS за 10 минут и сможете сразу его использовать, но это получается потому, что над инфраструктурной частью уже заранее поработали специалисты провайдера.


Kubernetes требует прокачки: он не работает сам по себе


Как я уже говорил выше, Kubernetes отдельная экосистема, которой нужно заниматься и подключать к ней дополнительные инструменты. Если брать Self-Hosted, то все это придется делать самостоятельно.


Все инструменты, дополняющие Kubernetes, Open Source-решения, которые нужно настраивать. В кластер потребуется установить систему мониторинга, реализовать балансировку нагрузки, сбор и хранение логов, настройки безопасности и авторизации пользователей, сети и многое другое.


Например, понадобится мониторить и сам кластер, и приложения в нем. Причем стандартного мониторинга через Zabbix вам не хватит, потребуется специфический Prometheus или Telegraph.


С логами аналогичная ситуация: из коробки вы получите только историю логов для уже запущенных приложений, при передеплое она исчезнет. Вручную собирать логи с Kubernetes не получится, нужно подключать сборщики логов вроде Fluentd и систему хранения, например Elasticsearch или Loki. Отдельно придется заниматься балансировкой нагрузки: понадобится отказоустойчивый балансер вроде MetalLB.


Системы хранения для Self-Hosted Kubernetes еще одна головная боль


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


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


Для нормальной работы приложения без изменения его логики понадобятся Persistent Volumes хранилища, связанные с подами. Они подключаются внутрь контейнеров как локальные директории, позволяя приложению хранить данные под собой. Среди рабочих вариантов CephFS, Glusterfs, FC (Fiber Channel), полный список СХД можно посмотреть в официальной документации.


Интеграция Kubernetes c Persistent Volumes нетривиальная задача. Чтобы развернуть тот же Ceph, недостаточно взять мануал с Хабра и выполнить ряд команд. Плюс в дальнейшем СХД должен кто-то заниматься опять нужен отдельный инженер, а то и несколько.


Если же Self-Hosted-кластер развернут не на железе, а на виртуальных машинах в облаке, то все немного проще собственный кластер Ceph поднимать не нужно. Можно взять кластер хранилища у провайдера и научить его работать с кластером K8s, если провайдер готов предоставить вам API к своей системе хранения данных, что есть не везде. Писать интеграцию при этом придется самостоятельно.


Правда, у провайдеров, предоставляющих IaaS, можно арендовать объектное хранилище или облачную СУБД, но только если логика приложения позволяет их использовать. А в Managed-решениях Kubernetes уже из коробки есть интегрированные Persistent Volumes.


Отказоустойчивость кластера отдельная проблема


С Kubernetes проще обеспечить отказоустойчивость приложений, однако потребуется еще и реализовать отказоустойчивость кластера.


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


Это создает сложности для малого бизнеса: раньше для всех приложений требовалось всего два сервера, а с Kubernetes только ради отказоустойчивости нужно три дополнительных сервера.


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


Резерв зависит от того, какое количество вышедших из строя нод вероятно в вашем случае:


  • Если у вас одна стойка с серверами в одном дата-центре, то одномоментно, скорее всего, выйдет из строя максимум одна нода на одном сервере, например из-за ошибок ОС. Значит, нужен резерв на одну ноду. Конечно, может сломаться стойка, но тут уже нужно резервирование не средствами Kubernetes.
  • Если у вас несколько стоек с серверами, то есть вероятность потери одной стойки, например из-за проблем со свичем, когда все серверы в ней станут недоступны. Значит, нужен резерв в размере количества серверов в одной стойке.
  • Если у вас несколько дата-центров, то в каждом нужно держать резерв по размеру другого дата-центра, чтобы приложения работали в случае его выхода из строя.

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


При этом лучше, если ноды в кластере небольшие по объему, но их много. Допустим, у вас есть пул ресурсов 100 ГБ оперативной памяти и 100 ядер CPU. Такой объем позволяет запустить 10 виртуалок и 10 нод кластера Kubernetes. И в случае выхода из строя одной ноды вы теряете только 10% кластера.

На железных серверах такую конфигурацию не создашь. Например, используя 300 ГБ оперативной памяти и 50 ядер CPU, вы развернете всего 23 ноды кластера. И в случае выхода из строя одной ноды рискуете сразу потерять 3050% кластера.

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


Автомасштабирование кластера нетривиальная задача


Чтобы кластер всегда был готов к любой нагрузке и новые ноды подключались и отключались по необходимости, нужно реализовать автомасштабирование. То есть сделать так, чтобы ваши приложения автоматически получали нужные ресурсы в необходимом объеме.


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


Плюс если мы говорим о Self-Hosted на Bare Metal, то все серверы, необходимые для работы приложений на случай нагрузки, придется держать в рабочем состоянии и постоянно за них платить.


Если Self-Hosted-кластер развернут на IaaS, то схема похожая: инженер добавляет новую виртуальную машину и вносит ее в кластер. Другой вариант взять API провайдера, если он его предоставляет, подключить через него кластер Kubernetes, научить его запускать для себя новые серверы и так реализовать автомасштабирование. Но потребуется разрабатывать отдельное решение это сложная задача, предполагающая высокий уровень экспертности в Kubernetes и облаках.


Кроме того, для быстрого масштабирования Self-Hosted-кластера на IaaS придется резервировать нужное количество ресурсов провайдера и создавать из них новые виртуальные машины по мере надобности. И за эти зарезервированные ресурсы придется платить: практика брать плату за выключенные ресурсы бывает у реселлеров VMware. На нашей платформе в случае отключенных ВМ вы не платите за ресурсы, только за диски. В некоторых Managed-решениях автоскейлинг включается по кнопке, уточните эту возможность у вашего провайдера.


Подводные камни Self-Hosted Kubernetes


  1. Для самостоятельной эксплуатации кластера нужен специалист на фултайм, который хорошо знает технологию и понимает, как все работает внутри Kubernetes.
  2. В кластере потребуется настроить мониторинг, сбор логов, балансировку нагрузки и многое другое.
  3. Отдельная проблема развернуть и интегрировать с кластером систему хранения данных.
  4. Чтобы обеспечить отказоустойчивость кластера, потребуются дополнительные серверы или виртуалки это дополнительные затраты.
  5. Для масштабирования кластера под нагрузкой нужен запас серверов или виртуалок это еще одна статья дополнительных расходов.

Рассчитывайте ваши возможности при старте проекта. То, какие ресурсы есть у вашей компании, ваш бэкграунд, навыки и другие детали сильно влияют на выбор решения, насколько вам будет выгодно разворачивать Kubernetes самостоятельно или лучше это сделать в облаке с помощью готового сервиса. И не забываем главный вопрос всего Kubernetes: нужна ли вообще эта технология на вашем проекте, как и для чего вы собираетесь ее использовать?


Тут можно почитать, как устроен наш Kubernetes aaS на платформе Mail.ru Cloud Solutions: что у него под капотом и что в него еще входит, кроме собственно Kubernetes.
Подробнее..

1000 и 1 способ сесть на мель в Spring WebFlux при написании высоконагруженного сервиса

29.04.2021 10:23:13 | Автор: admin

Источник изображения: Shutterstock.com/photowind

Добрый день, меня зовут Тараканов Анатолий, я senior java разработчик SberDevices. 2.5 года программирую на Java, до этого 6 лет писал на C# и 1 год на Scala. Хочу поделиться опытом создания сервиса-оркестратора Voice Processing Service. Он является точкой входа для пользователей семейства виртуальных ассистентов Салют. Через него также проходит часть трафика приложений SmartMarket, где любой разработчик может написать навык для наших виртуальных ассистентов Салют. Одним словом, на сервис приходится немалая нагрузка. Давайте посмотрим, какие проблемы при его создании возникли и как мы их решали, а также сколько времени ушло на поиск причин. И всё это в контексте реактивного фреймворка Spring WebFlux.

Немного о сервисе


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

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


Как видно, смежных систем немало. API части из них доступны по REST-у запрос-ответ, другие по Socket-у потоковая передача данных.

Сервис хостится в нескольких ЦОДах, в том числе в SberCloud, горизонтально масштабируется в OpenShift. Для передачи, поиска и хранения логов используется ELK-стек, для трассировки Jaeger, для сбора метрик Prometheus, а для их отображения Grafana.



Каждый инстанс в секунду держит нагрузку примерно в 7000 пакетов (средний размер пакета 3000 байт). Это эквивалентно активности 400 пользователей, которые без перерыва обращаются к виртуальному ассистенту. С учётом взаимодействия нашего сервиса со смежными число пакетов увеличивается втрое до 21 000.
Каждая виртуалка имеет 3 ядра и 8 Gb оперативной памяти.

Сервис создавался в реалиях стартапа, а значит неопределенности. Были такие вводные:

  • поддержка TLS/mTLS;
  • WebSocket с клиентом;
  • текстовый, голосовой стриминг;
  • отказоустойчивость 99.99;
  • высокая нагрузка;
  • масса смежных систем в перспективе и необходимость в гибком формате контракта.

В этих реалиях мы выбрали такие технологии:

  • Java 11 с Gradle;
  • JSON/Protobuf на транспортном уровне.

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

А ещё мы использовали Junit 5 и Mokito для тестирования и несколько библиотек Nimbus JOSE + JWT, Google Guava, Lombok, vavr.io для удобства в виде синтаксического сахара и автогенерации кода.

Оценив требования, мы решили втащить в наш технологический стек Spring WebFlux с Reactor и Netty под капотом.

Итак, поговорим о нюансах использования этого реактивного фреймворка.

Кастомизация Netty-сервера


Сразу отмечу, что не все настройки Netty-сервера доступны через спринговые проперти или аргументы командной строки. А ещё иногда необходимо повесить логику на события самого сервера и подключения, заменить стандартный обработчик события в рамках подключения и добавить свой регистратор метрик.

Так вот, всё это можно сделать в компоненте, имплементирующем WebServerFactoryCustomizer. В его методе доступны как HttpServer, так и каждое клиентское подключение.



Пропущу этап создания сервиса и сразу перейду к его сдаче на стенд нагрузочного тестирования. В тот же день мы получили фидбэк о том, что с течением времени число незакрытых соединений растёт. Стали искать причину анализировали tcp-дампы с помощью WireShark. Выяснилось, что клиент не присылал сигнал о закрытии соединения, а также, что в реакторе по умолчанию не инициализируются обработчики таймаутов на входящие/исходящие пакеты. Для исправления ситуации в вышеуказанный компонент был добавлен такой обработчик.



На просмотр логов, анализ ситуации со смежниками, сбор дампов и их анализ, исправление и тестирование у нас ушло 2 дня. Немало.

Следующей проявившейся под нагрузкой проблемой было то, что спустя порядка 30 минут после начала теста смежные сервисы, доступные по RESTу, стали иногда отвечать на запросы ошибкой Сonnection reset by peer. Мы снова отправились смотреть логи, дампы. Оказалось, дело было в том, что при инициализации HttpClient-а фабричным методом .create(), размер пула соединений по умолчанию будет равен 16 или числу процессоров, умноженному на два. Со своей логикой выселения, ожидания свободного соединения и многим другим. И это всё на каждый тип протокола.



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

Но всего этого не было, поэтому на время при взаимодействии с такими клиентами мы стали применять ConnectionProvider с отключенным пулом.



Поиск причины такого поведения съел 3 дня, это больно.

Мы развивали наш сервис дальше, накручивали логику, сценарии становились всё сложнее и вот, в один прекрасный день с нагрузочного тестирования пришла печальная весть: мы перестали держать ожидаемую нагрузку. Что обычно делают в таком случае берут в руку JFR и профилируют. Так мы и поступили. Результат не заставил себя долго ждать. Мы обнаружили, что при написании fluent-цепочек вызовов методов Flux-ов о декомпозиции логики в функциональном стиле стоит забыть.



В приведенном фрагменте кода замеряется работа флакса из 100_000 элементов с 1 реактивным методом, во втором с 6 методами. Тест проверяет, что первый метод работает вдвое быстрее второго, причем число итераций проверок не играет роли.

Почему так? Потому что на каждом этапе вызова методов .map/.filter/.flatmap/.switchOnFirst/.window и других создается Publisher, Subscriber и другие специфичные каждому из этих методов объекты. В момент подписки происходит вызов Publisher и Subscriber вверх по fluent-цепочке. Все эти накладные расходы можно наглядно увидеть в стектрейсах. Эту проблему решали 3 дня, такого рода рефакторинг недешёвое удовольствие.

Итак, мы отрефачили код, прошли тестирование. Ещё несколько месяцев обрастали новыми смежными сервисами и сценариями потоковой обработки. Как вы думаете, какие грабли пришли в движение следующими? Те, что находятся в ядре самого WebFlux-а. В логике сервиса было с десяток мест с методами .groupBy и .flatMap. В связи с особенностями контракта и бизнес-логики они порождали множество очередей, к тому же при некоторых стечениях обстоятельств группы не финализировались. Приклад не падал, но процессинг останавливался, а перед этим происходила утечка памяти.

Кстати, на гитхабе много вопросов по этой теме. Если отвечать коротко, то стоит заглядывать вглубь каждого метода. Там может быть много интересного: от ограничений по размеру внутренней очереди, volatile чтений/записей, до порождения потенциально бесконечного числа очередей, которые сами собой не зафиналятся. Подробнее здесь: github.com/reactor/reactor-core/issues/596.



Вот, собственно, простой тест с замиранием процессинга.



Как видно, последняя запись в логе 255 элемент. Если заглянуть в документацию, то причина такого поведения станет очевидна, но кто её читает?) Особенно когда методы имеют такие говорящие и всем привычные названия.

Были проблемы и с методом .windowWhile. Вот ссылка на найденный нами в этом методе баг. Отмена подписки на его источник данных останавливала работу оператора.

С фреймворком Spring WebFlux нужно быть очень аккуратным. В том числе нужно следить за тем, какой паблишер возвращается методом. На какие-то можно подписываться неограниченное число раз (FluxReplay), но они имеют нюанс с размером буфера, другие возвращают один и тот же элемент каждому новому подписчику (MonoDefer).

Несколько эффективных и дешёвых оптимизаций


  • Переход на Z Garbage Collector сильно улучшил производительность, а интервалы простоя приложения во время сборки мусора сократились с 200 мс до 20 мс.

  • С той же версией приложения и под той же нагрузкой G1 давал пилу с большими зубьями по таймингам, Major GC вообще шёл вразнос, так как не хватало CPU на I/O-операции. В то же время ZGC / Shenandoah GC сократили пилу раз в 10.

  • Если ваш сервис занимается передачей тяжеловесных данных (голоса или видео) стоит внимательно посмотреть на io.netty.buffer и пользоваться его возможностями. Профилирование показало, что его использование позволило вдвое уменьшить основную категорию мусора в памяти.

  • Использование метрик Reactor Netty вместе с профилированием показали, что на криптографию уходила уйма времени, поэтому мы перешли с JDK SSL на Open SSL. Это в 2 раза ускорило приклад.

Используйте JFR + JMC, именно они подсветили все эти проблемы. Во время ревью кода можно сделать неверные выводы, бенчмарк для отдельных маленьких операций можно некорректно написать и получить непоказательные результаты, но flame graph/monitor wait/thread park/GC-разделы в JMC подсветят реальные проблемы.

В качестве итогов


Reactor Netty удобен, гибок и быстр. Что касается текущей реализации Spring WebFlux, то она позволяет добиться высокой производительности, даже если сервис процессит большой объем событий в единицу времени в рамках каждого подключения и содержит витиеватую логику с нелинейной обработкой и ветвлениями.

Но придётся следовать трём правилам:

размеры очередей и буферов под капотом требуют тонкой настройки. Также нужно помнить, что использование фреймворка на каждом шагу порождает новый источник данных, который нужно закрывать;

следует избегать тяжеловесных .groupBy и .flatMap, лучше использовать .handle и .flatMapIterable, где возможно;



цепочки вызовов методов фреймворка должны быть короткими, а декомпозицией логики обработки лучше заниматься в своих методах, либо использовать Operators.lift и Sinks для создания своего реактивного оператора.



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


Источник изображения: Shutterstock.com/SEE D JAN

Отдельного рассказа заслуживают нюансы применения сборщиков мусора (GC), инструментов JFR/JMC, особенности работы с буферами и очередями в Spring WebFlux, а также тонкости настройки Netty-сервера.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru