Привет! Я Алексей, старший системный администратор ЮMoney. Так уж вышло, что я — главный по Куберу в компании. Поэтому когда меня попросили рассказать, как мы создавали сервис Kubernetes и что у нас в итоге получилось, уговаривать меня долго не пришлось.

Зачем компании оркестрация контейнеров?

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

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

Когда-то давно у нас так и было: приложений было пара десятков, и жили они все на нескольких железных серверах без какой-либо контейнеризации. По мере роста компании и распиливания монолитов количество приложений стало расти, а сами они начали перемещаться в LXC/LXD-контейнеры. Но поскольку их всё ещё было немного, ручное управление размещением контейнеров, выделением ресурсов и сетями не было проблемой. Пока в конце концов мы не пришли к тому, что приложений у нас уже больше 160, в год добавляется ещё по 20, а живут они на 800+ LXC/LXD-контейнеров.

Кубер v1

Для начала небольшое погружение в историю. Ещё четыре года назад, в конце 2017 – начале 2018 года, когда приложений было поменьше, около 60, и даже не все они ещё были в LXC, стало понятно, что нам нужна какая-то оркестрация контейнеров. На тот момент наряду с Kubernetes рассматривали ещё Swarm и Nomad, а последний даже подняли для нужд одного из наших подразделений (и он до сих пор жив).

Тогда-то один из Linux-админов компании и поднял тестовый кластер куба для экспериментов. От этого кластера ведут свою родословную все кластеры, живущие в нашей компании.

Первый тикет в Jira с тегом K8s, который мне удалось найти
Первый тикет в Jira с тегом K8s, который мне удалось найти

Кластер был инициативой снизу и делался одним сотрудником фактически на голом энтузиазме. От первой заявки на сети до появления работающего кластера прошло около года экспериментов. В этот кластер заехал созданный тем же сотрудником релизный бот, про которого отдельно подробно рассказал @dvorob в своём докладе, и... всё, больше ничего заехать не успело. Угадайте почему? Потому что закончилось всё ровно тем, чем всегда заканчиваются истории с bus_factor == 1. Конечно же, тот самый единственный и неповторимый сотрудник в один прекрасный день покинул компанию.

Кубер v2

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

Втроём ребята проводили эксперименты и пытались нащупать подходящую нам архитектуру системы, которая должна была, насколько это возможно, соответствовать следующим требованиям:

  • отказоустойчивость

  • минимальные переделки приложений со стороны разработки

  • совместимость с существующей инфраструктурой: система не должна стоять особняком

  • масштабируемость

  • тиражируемость: система должна быть воспроизводима

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

К лету 2019-го появилась первая версия технического решения, а в сентябре разработка архитектуры кластера в основном была завершена. Было решено поднять четыре кластера K8s, по два на дата-центр. Каждый кластер состоял из трёх мастер-нод, на которых крутился control-plain и etcd, двух нод-балансировщиков, на которых поднимался ingress-nginx, и произвольного числа нод для рабочих нагрузок. Единым service mesh для всех приложений компании должен был стать Consul Connect.

За сентябрь-октябрь в новые кластеры начали заезжать приложения. Вообще, бо́льшая часть платёжной системы ЮMoney написана на Java и Node.js, которые деплоятся в LXD-контейнеры. Но для начала брали приложения, которые не были связаны с ядром платёжной системы, жили на отдельных виртуалках в VmWare vCenter и не проходили через стандартный CI/CD пайплайн. Это был десяток небольших РНР-приложений, которые стояли особняком и могли быть безболезненно мигрированы в новую инфраструктуру.

Кроме PHP-приложений для заезда в кластеры куба было выбрано одно небольшое новое Node.js-приложение, которое готовилось к выезду в продакшн и не было критичным в случае простоя. Нам было важно проверить работу на одном из стандартных приложений и убедиться, что всё работает как задумано.

Примерно на этом этапе к команде, занимавшейся Кубером, присоединился я: не хватало рабочих рук, и меня с ещё одним коллегой добавили для усиления. На тот момент у меня был некоторый опыт работы с докером, но ни с Kubernetes, ни с аналогами я дела не имел.

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

На какое-то время о развитии пришлось забыть. Мы забрали в команду ещё одного перспективного сотрудника, но экспертизы катастрофически не хватало. Втроём мы с трудом успевали решать текущие проблемы в довольно сыром сервисе.

Проблемы с Consul Connect

Особенно много проблем доставлял Consul Connect, который постоянно терял связность между своими «дата-центрами» (не нашими физическими дата-центрами, а внутренними логическими сущностями Консула).

Проблемы с Consul Connect, вероятно из-за недостатка экспертизы
Проблемы с Consul Connect, вероятно из-за недостатка экспертизы

Каждый кластер куба был отдельным «дата-центром» и отваливался от основного ДЦ по любому чиху. Следствием была потеря связности между приложением Node.js внутри кластера и компонентами снаружи. Очень подозреваю, что мы просто не сумели правильно приготовить connect, экспертизы по Consul у нас на тот момент было ещё меньше, чем по Куберу. 

В конечном итоге спустя месяц терзаний мы вернули Node.js-приложение на наши стандартные LXD-рельсы, выпилили Consul Connect, более-менее стабилизировали работу кластеров куба и задумались над новой архитектурой кластеров.

Кубер v3

Поскольку у предыдущей архитектуры оказался фатальный недостаток, надо было придумать на её основе что-то новое, но без коннекта. И тут совпали два события. Во-первых, я изучил возможности сетевого плагина Calico в плане обмена сетевыми маршрутами к подам через BGP и получил от наших сетевиков принципиальное ОК на возможность осуществить пиринг с их оборудованием. А во-вторых, в недрах команды DevOps родился проект по созданию service discovery на основе HAProxy+Consul.

Локальный HAProxy на каждой ноде приложения и без того использовался в компании для балансировки запросов приложения к другим частям платёжной системы. Фактически на нём был построен такой прото-service mesh, управлявшийся через Ansible. Для превращения в полноценный service mesh не хватало как раз обнаружения сервисов, которое и решили сделать на основе Consul.

Из этих двух фактов родилась новая концепция архитектуры кластера, в первую очередь – сетевой архитектуры. В «новом прекрасном Кубере будущего» мы отказывались от Ingress-контроллеров, добавляли к каждому поду sidecar с Consul-агентом и направляли трафик снаружи кластера напрямую в поды приложений. Кроме sidecar с агентом, для взаимодействия с внешними относительно кластера сервисами в поды приложений добавился sidecar с HAProxy.

На сетевом уровне поды каждого приложения оказывались в своей /24 сети для возможности управления сетевыми доступами снаружи кластера. Это важное соответствие существующей инфраструктуре, так как в её основе лежит сегментация на сетевом уровне.

На реализацию этих идей в проде ушёл почти год: кластеры на новой сетевой архитектуре запустились в дев-среде в начале 2021-го. И дело не только в переходе на новый service discovery. Пандемия, локдаун и переход на удалёнку в то время имели максимальный приоритет, так что задачи по BGP-пирингу между сетевой инфраструктурой и кластерами K8s были отложены до лучших времён. Вишенкой на торте стал процесс смены названия компании в середине года, затянувший все свободные ресурсы.

За прошедший год концепция успела немного мутировать. Было решено оставить только два кластера в продакшн – по одному на ДЦ. В прошлом такое решение не было бы отказоустойчивым, поскольку входящий трафик терминировался исключительно в том же ДЦ, куда прилетал изначально. Мы решили эту проблему, и количество кластеров стало возможно уменьшить без ущерба для отказоустойчивости: при проблемах с кластером в одном ДЦ запросы полетят во второй.

К концу февраля я вывел из эксплуатации один из старых кластеров и пересоздал его на новой сетевой архитектуре. Замаячила финишная прямая...

Внешний аудит. Внедрение Istio

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

Ребята практически под микроскопом рассмотрели нашу архитектуру и процессы и показали себя настоящими профессионалами. Пользуясь случаем, хочу лично поблагодарить @distolи @jamboза прекрасные часы в Zoom и фирменное «Сложилось!».

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

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

На RnD, тесты и интеграцию с уже существующим service discovery на HAProxy+Consul ушло несколько месяцев. Поначалу была идея затащить в Istio service mesh вообще все наши сервисы как в кубе, так и снаружи, но мы встретили непреодолимые проблемы при попытке включения в mesh хостов на Windows. К тому же, само решение по добавлению в mesh внешних относительно кластера хостов было явно сыровато. И раз уж всё равно придётся изобретать механизм для связи с хостами на Windows, решили, что точно такой же механизм отлично сработает и с Linux-хостами.

Итоговое решение

Таким образом у нас получилось два service discovery. Один снаружи куба – на Consul, другой внутри – на CoreDNS. При этом фактически у нас получился единый гибридный service mesh: приложения могут прозрачно ходить друг в друга независимо от того, где они расположены.

Вот как это устроено.

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

application:
  name: app-name
  endpoints:
    - name: endpoint-name
      app: other-app
      port:
        prod: https

Внутри кластера K8s:

  • Поды CoreDNS мы расположили на мастерах и настроили на них hostNetwork. Так DNS кластера оказался доступен снаружи.

  • Для приложений, работающих в кластере, создаём Headless-сервисы в дополнение к обычным ClusterIP, чтобы иметь возможность получать IP-адреса подов по DNS.

  • Каждое приложение в кластере K8s живёт в собственном namespace.

  • Если у приложения в description.yml указаны внешние относительно кластера сервисы, при деплое такого приложения в его namespace создаются ServiceEntry, endpoint’ы для которых берутся из Consul и выглядят примерно так:

apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: entry-name
  namespace: app-namespace
spec:
  endpoints:
    - address: port-name.app-name.service.consul.domain
  exportTo:
    - .
  hosts:
    - app-name.virtual-namespace.svc
  location: MESH_EXTERNAL
  ports:
    - name: https-app-name
      number: 1234
      protocol: HTTPS
  resolution: DNS

Имя хоста для ServiceEntry внешних сервисов задаётся такое же, как если бы они были внутри кластера: service.namespace.svc. Когда (если) этот сервис переедет в Кубер, достаточно будет удалить ServiceEntry, чтобы перенаправить трафик.

Для приложений, живущих в LXC/LXD-контейнерах или виртуалках:

  • на хосте устанавливается Consul agent, который регистрирует его в консуле и делает частью service mesh,

  • также устанавливается HAProxy

  • при деплое приложения для HAProxy генерируется конфигурация, исходя из значений в description.yml,

  • HAProxy динамически отслеживает изменения благодаря использованию директивы server-template.

listen app_name_port_name
    bind 127.0.0.1:1234 tfo
    mode tcp
    option tcp-check
    default-server inter 2000 fall 2 rise 2
    server-template app-name-port-name-active 32 _app-name._port-name.service.dc1.consul.domain check init-addr none
    server-template app-name-port-name-backup 32 _app-name._port-name.service.dc2.consul.domain check init-addr none backup
    server-template k8s-app-name-port-name-active 10 _port-name._tcp.headless-service.namespace.svc.cluster-in-same-dc.domain check resolve-opts init-addr none
    server-template k8s-app-name-port-name-backup 10 _port-name._tcp.headless-service.namespace.svc.cluster-in-other-dc.domain check resolve-opts init-addr none backup

На Nginx+ балансировщиках для приложений, живущих в Kubernetes, поднимаются апстримы, смотрящие на SRV-записи в DNS кластеров куба, примерно такого вида:

upstream k8s-app-name-http {
    ...
    server headless-service.namespace.svc.cluster-in-same-dc.domain max_fails=0 fail_timeout=10s service=_port-name._tcp resolve;
    server headless-service.namespace.svc.cluster-in-other-dc.domain max_fails=0 fail_timeout=10s service=_port-name._tcp resolve backup;
}

Итого в августе 2021-го наконец-то была окончательно закончена разработка архитектуры кластера. А в сентябре приложения, уже жившие в K8s, переехали из старых кластеров в новые. Сегодня из LXC в кластер куба наконец-то начали переезжать компоненты на Java.

В конце концов Кубер в ЮMoney стал выглядеть так:

Архитектура кластера сейчас
Архитектура кластера сейчас

Выводы

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

Возможно, в других случаях было бы эффективнее развернуть новую инфраструктуру на K8s с нуля, но на наших объёмах это почти невозможно. Поэтому мы вложились в то, чтобы предусмотреть все нюансы взаимодействий существующих приложений и подготовку рабочих механизмов, вместо того, чтобы поднять новый красивый Кубер с дефолтными настройками и сказать разработке: «Заезжайте!»

Сейчас мы на пороге великой миграции из LXC в Kubernetes. Ждите продолжения через год-другой.

Комментарии (9)


  1. sse
    24.11.2021 15:35
    +5

    Офигенная статья для тех, кто умеет читать, спасибо большое за честность!


  1. muturgan
    25.11.2021 07:43
    +1

    Подзаголовки круто было бы добавить.


    1. AlexRay Автор
      25.11.2021 11:04
      +2

      Спасибо за идею. Добавил заголовков для удобства чтения.


      1. muturgan
        25.11.2021 17:35
        +2

        Я наверно был не в себе. Не помню как писал этот комментарий) Пора завязывать полуношничать.
        Теперь круто, спасибо :)


  1. misterkramer
    25.11.2021 19:11
    +2

    Дорогу осилит идущий)

    Как тут в комментариях прозвучало - спасибо за честность @AlexRay!)

    У меня такой вопрос - а как решаете вопрос с обслуживанием сертификатов TLS для сервисов внутри куба? Используйте ли cert-manager или какая-либо другая схема - поставки/обновления сертов?


    1. AlexRay Автор
      26.11.2021 12:43
      +1

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


  1. Nikita_Danilov
    28.11.2021 21:24

    Спасибо большое за статью, особенно за схему, сильно же усложнилась изменилась инфраструктура за последние года. :)

    В конце концов Кубер в ЮMoney стал выглядеть так:

    Можете подсказать, пожалуйста, по паре моментов:

    1. Есть ли принципиальные причины почему сверху использован Nginx+, а снизу HAProxy?

    2. Аналогично, есть ли принципиальные причины почему использован Envoy, а не HAProxy, например? И почему прямоугольничек Envoy нарисован именно внутри App?


    1. AlexRay Автор
      28.11.2021 22:28
      +2

      1. На Nginx+ работают балансировщики для входящего трафика, их всего несколько штук на датацентр. HAProxy обеспечивает меш и установлен по экземпляру возле каждого приложения. Не то чтобы их невозможно было поменять местами, но зачем? К тому же надо понимать, что Nginx+ — платный продукт.

      2. Envoy использован потому, что его использует Istio, альтернативы там особо никакой нет. Он нарисован внутри App потому, что App на схеме - под приложения, а envoy - один из сайдкаров внутри пода.


      1. Nikita_Danilov
        29.11.2021 10:57

        Спасибо! Первый вопрос скорее не про "поменять местами", а про использование чего-то одного. Всё равно ведь чем Уже список инструментов, тем проще с ними управляться, лучше накапливается экспертиза.