Переезд Тиндера в Кубернетес

Автор: Крис О'Брайен, технический директор | Крис Томас, технический директор | Jinyong Lee, старший инженер-программист | Под редакцией: Купер Джексон, инженер-программист

Почему

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

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

Это было нелегко. Во время нашей миграции в начале 2019 года мы достигли критической массы в нашем кластере Kubernetes и начали сталкиваться с различными проблемами из-за объема трафика, размера кластера и DNS. Мы решили интересные задачи по миграции 200 сервисов и запуску кластера Kubernetes в масштабе, насчитывающем 1000 узлов, 15 000 модулей и 48 000 работающих контейнеров.

Как

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

Создание изображений для Kubernetes

Существует более 30 хранилищ исходного кода для микросервисов, работающих в кластере Kubernetes. Код в этих репозиториях написан на разных языках (например, Node.js, Java, Scala, Go) с несколькими средами выполнения для одного и того же языка.

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

Рисунок 1–1 Стандартизированный процесс сборки через контейнер Builder

Для достижения максимальной согласованности между средами выполнения во время фазы разработки и тестирования используется один и тот же процесс сборки. Это поставило уникальную задачу, когда нам нужно было разработать способ гарантировать согласованную среду сборки на всей платформе. В результате все процессы сборки выполняются внутри специального контейнера «Builder».

Реализация контейнера Builder потребовала нескольких продвинутых технологий Docker. Этот контейнер Builder наследует локальный идентификатор пользователя и секреты (например, ключ SSH, учетные данные AWS и т. Д.), Необходимые для доступа к частным репозиториям Tinder. Он монтирует локальные каталоги, содержащие исходный код, чтобы иметь естественный способ хранения артефактов сборки. Такой подход повышает производительность, поскольку исключает копирование встроенных артефактов между контейнером Builder и хост-машиной. Хранимые артефакты сборки будут повторно использованы в следующий раз без дальнейшей настройки.

Для определенных сервисов нам нужно было создать другой контейнер в Builder, чтобы сопоставить среду времени компиляции со средой времени выполнения (например, при установке библиотеки Node.js bcrypt генерируются специфичные для платформы двоичные артефакты). Требования времени компиляции могут различаться для разных сервисов, и окончательный Dockerfile создается на лету.

Архитектура и миграция кластера Kubernetes

Размер кластера

Мы решили использовать kube-aws для автоматической подготовки кластеров на экземплярах Amazon EC2. Ранее мы запускали все в одном общем пуле узлов. Мы быстро определили необходимость разделения рабочих нагрузок на экземпляры разных размеров и типов для более эффективного использования ресурсов. Причиной было то, что использование меньшего количества многопоточных модулей дало нам более предсказуемые результаты, чем позволяло им сосуществовать с большим количеством однопоточных модулей.

Мы остановились на:

  • m5.4xlarge для мониторинга (Прометей)
  • c5.4xlarge для рабочей нагрузки Node.js (однопоточная рабочая нагрузка)
  • c5.2xlarge для Java и Go (многопоточная рабочая нагрузка)
  • c5.4xlarge для плоскости управления (3 узла)

миграция

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

Эти конечные точки были созданы с использованием взвешенных наборов записей DNS, в которых CNAME указывал на каждый новый ELB. Для переключения мы добавили новую запись, указывающую на новый ELB службы Kubernetes, с весом 0. Затем мы установили время жизни (TTL) для записи, установленной на 0. Затем старые и новые веса были медленно скорректированы до в конечном итоге 100% на новом сервере. После завершения переключения TTL был настроен на что-то более разумное.

Наши модули Java поддерживали низкий DNS TTL, а наши Node-приложения - нет. Один из наших инженеров переписал часть кода пула соединений, чтобы обернуть его в диспетчере, который обновляет пулы каждые 60-е годы. Это сработало очень хорошо для нас без заметного снижения производительности.

Усвоение

Пределы сети Fabric

В ранние утренние часы 8 января 2019 года платформа Tinder подверглась постоянному отключению. В ответ на несвязанное увеличение задержки платформы ранее тем утром количество кластеров и узлов было масштабировано в кластере. Это привело к исчерпанию кэша ARP на всех наших узлах.

Есть три значения Linux, относящиеся к ARP-кешу:

кредит

gc_thresh3 - жесткая кепка. Если вы получаете записи журнала «Переполнение соседней таблицы», это означает, что даже после синхронной сборки мусора (GC) в кэше ARP не хватало места для хранения соседней записи. В этом случае ядро ​​просто отбрасывает пакет полностью.

Мы используем Flannel в качестве нашей сетевой структуры в Kubernetes. Пакеты пересылаются через VXLAN. VXLAN - это оверлейная схема уровня 2 в сети уровня 3. Он использует инкапсуляцию протокола MAC-адресов в пользовательских дейтаграммах (MAC-in-UDP), чтобы обеспечить средства для расширения сегментов сети уровня 2. Транспортный протокол в сети физического центра обработки данных - это IP плюс UDP.

Рисунок 2–1 Фланелевая диаграмма (кредит)

Рисунок 2–2 Пакет VXLAN (кредит)

Каждый рабочий узел Kubernetes выделяет свое собственное / 24 виртуального адресного пространства из большего блока / 9. Для каждого узла это приводит к 1 записи таблицы маршрутов, 1 записи таблицы ARP (на интерфейсе flannel.1) и 1 записи базы данных пересылки (FDB). Они добавляются при первом запуске рабочего узла или при обнаружении каждого нового узла.

Кроме того, связь между узлами и модулями в конечном итоге протекает через интерфейс eth0 (как показано на диаграмме Flannel выше). Это приведет к появлению дополнительной записи в таблице ARP для каждого соответствующего источника и назначения узла.

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

На момент сбоя в кластере было 605 узлов. По причинам, изложенным выше, этого было достаточно, чтобы затмить значение gc_thresh3 по умолчанию. Как только это происходит, не только пакеты отбрасываются, но и все виртуальные адресные пространства Flannel / 24 отсутствуют в таблице ARP. Узел для связи и поиска DNS не удается. (DNS размещен в кластере, как будет объяснено более подробно позже в этой статье.)

Для разрешения значения gc_thresh1, gc_thresh2 и gc_thresh3 повышаются, и Flannel необходимо перезапустить для перерегистрации отсутствующих сетей.

Неожиданно работающий DNS в масштабе

Чтобы приспособить нашу миграцию, мы активно использовали DNS для облегчения формирования трафика и постепенного перехода от устаревших к Kubernetes для наших услуг. Мы устанавливаем относительно низкие значения TTL для связанных наборов записей Route53. Когда мы запускали нашу устаревшую инфраструктуру на экземплярах EC2, наша конфигурация распознавателя указывала на DNS Amazon. Мы воспринимали это как должное, и стоимость относительно низкого TTL для наших сервисов и сервисов Amazon (например, DynamoDB) осталась в основном незамеченной.

По мере того, как мы загружали все больше и больше услуг в Kubernetes, мы обнаружили, что запускаем службу DNS, которая отвечает на 250 000 запросов в секунду. Мы столкнулись с прерывистыми и эффективными тайм-аутами поиска DNS в наших приложениях. Это произошло несмотря на исчерпывающие усилия по настройке и переключение провайдера DNS на развертывание CoreDNS, которое в свое время достигло максимума в 1000 модулей, потребляющих 120 ядер.

Исследуя другие возможные причины и решения, мы нашли статью, описывающую состояние гонки, влияющее на сетевой фильтр фильтрации пакетов Linux. Время ожидания DNS, которое мы видели, вместе с увеличивающимся счетчиком insert_failed на интерфейсе Flannel, соответствовало выводам статьи.

Эта проблема возникает во время трансляции сетевых адресов источника и назначения (SNAT и DNAT) и последующей вставки в таблицу conntrack. Одним из обходных путей, который обсуждался внутри организации и предлагался сообществом, было перенести DNS на сам рабочий узел. В этом случае:

  • SNAT не является необходимым, потому что трафик находится локально на узле. Его не нужно передавать через интерфейс eth0.
  • DNAT не требуется, поскольку IP-адрес назначения является локальным для узла, а не случайно выбранным модулем в соответствии с правилами iptables.

Мы решили двигаться вперед с этим подходом. CoreDNS был развернут как DaemonSet в Kubernetes, и мы внедрили локальный DNS-сервер узла в resolv.conf каждого модуля, настроив флаг команды kubelet - cluster-dns. Обходной путь был эффективен для тайм-аутов DNS.

Однако мы по-прежнему видим потерянные пакеты и увеличение счетчика insert_failed интерфейса Flannel. Это будет сохраняться даже после вышеупомянутого обходного пути, потому что мы избегали только SNAT и / или DNAT для трафика DNS. Состояние гонки будет по-прежнему иметь место для других типов трафика. К счастью, большинство наших пакетов являются TCP, и когда возникает условие, пакеты будут успешно повторно переданы. Долгосрочное исправление для всех типов трафика - это то, что мы все еще обсуждаем.

Использование Envoy для достижения лучшей балансировки нагрузки

Когда мы перенесли наши внутренние сервисы в Kubernetes, мы начали страдать от несбалансированной нагрузки между модулями. Мы обнаружили, что благодаря HTTP Keepalive соединения ELB привязывались к первым готовым модулям каждого развертывания, поэтому большая часть трафика проходила через небольшой процент доступных модулей. Одним из первых смягчений, которые мы попробовали, было использование 100% MaxSurge в новых развертываниях для худших нарушителей. Это было незначительно эффективным и не устойчивым в долгосрочной перспективе с некоторыми из крупных развертываний.

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

Мы внутренне искали, чтобы оценить посланника. Это дало нам шанс развернуть его очень ограниченным образом и получить немедленные выгоды. Envoy - это высокопроизводительный прокси-сервер уровня 7 с открытым исходным кодом, разработанный для крупных сервис-ориентированных архитектур. Он способен реализовывать передовые методы балансировки нагрузки, включая автоматические повторные попытки, разрыв цепи и глобальное ограничение скорости.

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

Фронт-посланники службы затем использовали этот механизм обнаружения службы с одним восходящим кластером и маршрутом. Мы настроили разумные тайм-ауты, увеличили все настройки автоматического выключателя, а затем установили минимальную конфигурацию повторных попыток, чтобы помочь с переходными сбоями и плавным развертыванием. Мы предоставили каждому из этих фронтальных сервисов Envoy TCP ELB. Даже если keepalive из нашего основного переднего прокси-слоя был закреплен на некоторых модулях Envoy, они намного лучше справлялись с нагрузкой и были настроены на балансирование с помощью наименьшего количества запросов к бэкэнду.

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

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

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

Рис. 3–1 Процесс конвергенции одного сервиса во время переключения между посланником

Конечный результат

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

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

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