Переезд монолита в k8s. Делаем каршеринг cloud native
Приветствую всех! Меня зовут Максим Шаленко, я старший системный администратор в каршеринге Ситидрайв, и сегодня я хочу поделиться опытом компании по становлению на светлую сторону Силы — переезду в облако.
Kubernetes. Зачем он нужен?
Я пришёл в Ситидрайв 2 года назад и столкнулся с особенностью, которая часто встречается у молодых компаний и стартапов — root-доступ у разработчиков на серверах с приложением. NodeJS-код жил в PM2, разработчик заходил на сервер, делал git pull и запускал новую версию приложения. Следовательно, никакой автоматизации доставки релизов не было, а риск накосячить всегда присутствовал.
Дабы автоматизировать деплой, минимизировать риски и человеческий фактор во время выкаток, я написал gitlab-ci, который запускал Ansible, а тот в свою очередь доставлял код на серверы. В среднем релиз длился 30-40 минут.
Имея большой опыт работы с k8s и CI/CD процессами, я, конечно же, сказал: «Давайте в кубер. Там будет и ускорение релизов, и бесшовный деплой, и self-healing сервиса, и масштабирование и т.д.». Но не тут-то было. После небольшого ресёрча стало ясно, что сервис не готов к переезду в облачную инфраструктуру.
Из основных особенностей, с которыми предстояло поработать:
сервис сохранял файлы и картинки прямо «под ноги»;
graceful shutdown отсутствовал;
метрики и трассировки отсутствовали, ориентировались только на метрики балансировщиков nginx и на логи;
у логов не было единого формата;
отсутствовали хелсчеки;
…и это далеко не конец.
Поэтому первое, что нужно было сделать — составить регламент, задать стандарт, которому должен соответствовать сервис.
Регламент
Мы составили минимальные требования к разработке сервисов:
Stateless
Мы должны были уметь масштабироваться, и один из нюансов, который прям «торчал» — проблема работы с файлами. Как я говорил ранее, сервис попросту сохранял их на файловую систему.
Командами разработки и эксплуатации была проведена большая работа по внедрению файлового сервиса и S3 MinIO, благодаря чему теперь вся статика хранилась в отказоустойчивом хранилище. После этого мы могли смело указать readOnlyRootFilesystem: true в securityContext нашего деплоймента и спать спокойно.
Environment variables
Было очень неудобно управлять конфигурацией приложения через js файлы, поэтому приняли решение стандартизировать настройку через переменные окружения.
Хранилища данных
Любые подключения к хранилищам данных должны быть конфигурируемыми, начиная от банальных username и password и заканчивая конкретными параметрами определённого компонента. Например:
PostgreSQL. Если сервис работает с реляционной базой данных, он должен отправлять запросы на запись в мастер, а на чтение из реплик. Исключения могут быть только в случае, если требуются самые актуальные данные, например, если бизнес логика связана с биллингом и оплатами.
Redis. Работа с редиской как напрямую, так и через redis sentinels для большей отказоустойчивости сервиса.
Kafka. В случае использования кафки, должна быть возможность конфигурировать security_protocol, sasl_mechanism, sasl_username, sasl_password, ssl_ca_pem. Помимо параметров авторизации нужно уметь настраивать топики: их названия, кол-во партиций, replication factor, retention time и т.д.
Health checks
Сервис должен включать в себя хелсчеки для проверки его работоспособности. Это позволит обеспечить так называемый self-healing, благодаря которому приложение станет более доступным несмотря на какие-то баги в коде.
Graceful shutdown
Чтобы приложение корректно перезагружалось или выключалось, оно должно уметь обрабатывать сигнал SIGTERM. Это так называемый graceful shutdown. Это одно из главных требований к микросервису на сегодняшний день, т.к. если сервис выполняет какой-то важный функционал и, как пример, сохраняет картинки в s3, то в таком случае главное, чтобы мы не потеряли изображение во время его загрузки, соответственно, после получения сигнала мы должны загрузить файл до конца, перестать принимать новые соединения, и только после этого завершать процесс.
Метрики
Сервис должен отдавать по запросу GET /metrics метрики в prometheus формате. Они должны включать в себя не только технические, но и критически важные бизнес-метрики, такие как: кол-во машин в аренде; кол-во машин, которые выпали из сети; проблемы с открытием и закрытием дверей и т.д.
Трассировки
Сервис должен уметь слать трассировки в Jaeger. Это дополнительный слой мониторинга, который помогает увидеть, на каких этапах «тормозим», будь то запросы в БД или взаимодействие с какой-то внешней интеграцией. Очень полезный инструмент.
Логи
Как единый формат логов для всех сервисов был выбран json. Бонусом является то, что по полям в json очень удобно строить индексы в системе агрегации и логирования Grafana Loki, которую мы используем в инфраструктуре.
Security
Сервис не должен запускаться под пользователем root, в привилегированном режиме, иметь какие-либо capabilities кроме требуемых и т.д.
Resources
Приложение не может запускаться без resources. Мы должны знать, сколько мы потребляем ресурсов и видеть проблему заранее, если вдруг сервис «течёт».
Что касается 9 и 10 пункта, валидацию деплойментов выполняет OPA Gatekeeper.
Архитектура и инструментарий
Чтобы доставить сервис в production и заставить эту карусель крутиться, была выстроена определённая архитектура и использованы соответствующие инструменты, которыми вкратце хочу поделиться:
За CI/CD (Continuous Integration and Continuous Delivery/Continuous Deployment) отвечает Gitlab.
Секреты храним в Vault Hashicorp.
Хранилищем артефактов является Nexus.
Для деплоя используем helm и универсальный chart, написанный специально под нужды компании. Он подходит для доставки всех сервисов в production-окружение.
Мы также имеем опыт canary выкаток и помогает нам в этом Argo Rollouts — простое решение с достаточно гибким инструментарием. Чего только стоит metric providers. Благодаря этому функционалу можно гибко управлять canary релизами на основе prometheus метрик (и не только) приложения.
Как я упоминал ранее, манифесты в k8s валидирует OPA Gatekeeper. Это позволяет предотвращать выкатки, которые не соответствуют регламенту.
За входящий трафик в k8s отвечает nginx ingress controller. Сделали этот выбор, т.к контроллер полностью соответствует всем техническим потребностям компании, а именно: поддержка HTTP/HTTPS, HTTP2, gRPC, TCP, TCP+TLS, Websockets. Из бенефитов также то, что контроллер имеет привычную для нас конфигурацию и большое комьюнити.
Мониторинг:
В качестве основной системы мониторинга используем TSDB решение Victoria Metrics.
Grafana Loki + Promtail для сбора и отображения логов.
Трассировки отправляем в Jaeger.
Хронология релиза
А теперь самое интересное — релиз монолита в k8s. Был анонсирован код-фриз в компании. Переключение происходило в несколько этапов и длилось несколько дней. За это время нам нужно было отловить все те баги, которые по тем или иным причинам не могли быть воспроизведены в dev-окружении.
Главная цель — выкатить сервис-монолит в кубер и перевести на него трафик с минимальными потерями для бизнеса и клиента.
Эпизод I: Новая надежда.
Для начала мы нарезали несколько worker-нод из резерва в k8s специально для этого сервиса, что позволило нам гарантировать выделенные ресурсы и не аффектить другие сервисы.
Как я уже упоминал ранее, для приёма и балансировки внешнего трафика мы используем nginx. С помощью директивы split_clients мы могли направлять клиентский трафик в тот или иной upstream.
Итак, начинаем. 5% трафика в k8s.
Эпизод II: Баги наносят ответный удар.
Фикс багов будем воспринимать как должное, т.к. без них, увы, не обошлось:
Одним из неприятных моментов, который не учли и не протестировали при переезде — функционал генерации промокодов. Как оказалось, промокоды генерировались… угадаете как и куда? Прямиком в CSV файлик “под ноги”. Как workaround мы просто смонтировали emptyDir volume в Pod’ы приложения и релиз продолжился. Костыль жил недолго, т.к наша доблестная команда разработки в этот момент параллельно работала над новым сервисом промокодов. Таким образом мы отрезали очередной кусочек от монолита и вывезли функционал в отдельный микросервис.
Была кратковременная недоступность авторизации с помощью SberID из-за косяка в коде, который быстро пофиксили, и всё нормализовалось.
Задели авторизацию с помощью SMS. Правда, на этот раз из-за того, что не прокинули в нужном месте X-Real-IP header в конфигурации nginx.
Но не будем заострять на этом внимание, т.к статья всё-таки не об ошибках в коде.
Учитывая, что наши железные ресурсы были ограничены на тот момент, мы не могли себе позволить просто так пойти и заказать x8 серверов/виртуальных машин по 32 CPU и 128GB RAM. Поэтому пришлось параллельно высвобождать старые серверы с приложениями и добавлять их как worker-ноды в k8s. Этот процесс повторялся вплоть до конца переезда, пока мы не перевели 100% клиентов.
На следующий день с интервалами в несколько часов мы пустили 10%, 25%, 50%.
Эпизод III: Пробуждение силы.
Наконец, финальный, третий, день — это 75%, 100% трафика. Аренды в норме, а значит это был успех.
Итоги
Оправдались ли наши ожидания? Несомненно.
Прежде всего мы стали на 100% cloud native.
Мы задали стандарт, по которому теперь разрабатываются все новые микросервисы в компании.
На протяжении 2-х лет монолитный сервис был разбит на десятки микросервисов, и мы продолжаем двигаться в этом направлении.
Мы создали уникальный CI/CD инструмент, тем самым ускорив time-to-market в 4 раза (вместо 30-40 минут стало 8-10).
Внедрили canary релизы.
Безопасно храним секреты.
Можем быстро и эффективно создавать новые стенды в dev-окружении.
Мы стали попросту стабильнее как сервис, т.к. избавились от тех. долга, а это значит, что конечный пользователь будет доволен стабильностью приложения.
За масштабирование теперь отвечает horizontal pod autoscaler, который скейлит кол-во реплик приложения в зависимости от нагрузки на сервис.
Обеспечили отказоустойчивость и self-healing сервиса благодаря инструментам k8s.
Настроили инструменты мониторинга. Теперь мы о многих проблемах можем знать заранее, а траблшутинг ошибок или багов стал намного легче и прозрачнее — без гаданий на кофейной гуще.
Пожалуй, это всё на сегодня, если говорить вкратце и по сути :)
Я бы хотел сказать спасибо каждому, кто внёс свой вклад в развитие этого проекта! Мы переезжали из одного датацентра в другой, мы шатали прод по ночам, выкатывали новые микросервисы, переписывали старые, за это время наш парк машин вырос в 4 раза. И всё это за экстремальные 2 года! Всем респект и низкий поклон.
Надеюсь, данная статья оказалась для вас полезной, и да пребудет с вами Сила!