Перехід Tinder на Kubernetes

Прим. перев.: Співробітники всесвітньо відомого сервісу Tinder нещодавно поділилися деякими технічними деталями міграції своєї інфраструктури на Kubernetes Процес зайняв майже два роки і вилився в запуск на K8s дуже масштабної платформи, що складається з 200 сервісів, розміщених на 48 тисяч контейнерів. З якими цікавими труднощами зіткнулися інженери Tinder і яких результатів дійшли — читайте в цьому перекладі.

Перехід Tinder на Kubernetes

Навіщо?

Майже два роки тому Tinder вирішив перевести свою платформу на Kubernetes. Kubernetes дозволив би команді Tinder провести контейнеризацію та перейти на експлуатацію з мінімальними зусиллями за допомогою незмінного розгортання (Immutable deployment). У цьому випадку складання додатків, їх деплою та сама інфраструктура були б однозначно визначені кодом.

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

Процес виявився непростим. Під час нашої міграції на початку 2019-го кластер Kubernetes досяг критичної маси і ми почали стикатися з різними проблемами через обсяг трафіку, розмір кластера та DNS. На шляху ми вирішили безліч цікавих завдань, пов'язаних з перенесенням 200 сервісів та обслуговуванням кластера Kubernetes, що складається з 1000 вузлів, 15000 pod'ів та 48000 працюючих контейнерів.

Як?

Починаючи з січня 2018 року, ми пройшли через різні етапи міграції. Ми почали з контейнеризації всіх наших сервісів та їхнього розгортання у тестових хмарних оточеннях Kubernetes. Починаючи з жовтня ми почали методично переносити всі існуючі сервіси в Kubernetes. До березня наступного року ми закінчили «переселення» і тепер платформа Tinder працює виключно на Kubernetes.

Складання образів для Kubernetes

Ми маємо понад 30 репозиторіїв вихідного коду для мікросервісів, що працюють у кластері Kubernetes. Код у цих репозиторіях написаний різними мовами (наприклад, Node.js, Java, Scala, Go) з безліччю runtime-оточень для однієї й тієї ж мови.

Система складання розроблена таким чином, щоб забезпечувати повністю налаштований «контекст складання» для кожного мікросервісу. Зазвичай він складається з Dockerfile та списку shell-команд. Їх вміст повністю настроюється, і в той же час всі ці контексти складання написані відповідно до стандартизованого формату. Стандартизація контекстів збирання дозволяє одній єдиній системі збирання обробляти всі мікросервіси.

Перехід Tinder на Kubernetes
Малюнок 1-1. Стандартизований процес складання через контейнер-складальник (Builder)

Для досягнення максимальної узгодженості між середовищами виконання (runtime environments) один і той же процес збирання використовується під час розробки та тестування. Ми зіткнулися з дуже цікавим завданням: довелося розробити спосіб, що гарантуватиме узгодженість складального середовища по всій платформі. Для цього всі складальні процеси проводяться всередині спеціального контейнера. Будівельник.

Його реалізація контейнера вимагає просунутих прийомів роботи з Docker. Builder успадковує локальний ID користувача та секрети (наприклад, ключ SSH, облікові дані AWS тощо), потрібні для доступу до закритих репозиторій Tinder. Він монтує локальні директорії, що містять вихідні джерела, щоб природним чином зберігати артефакти складання. Подібний підхід підвищує продуктивність, оскільки усуває потребу в копіюванні артефактів збирання між контейнером Builder та хостом. Артефакти збірки, що зберігаються, можна використовувати повторно без додаткового налаштування.

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

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

Управління розміром кластера

Ми вирішили використати kube-aws для автоматизованого розгортання кластера на екземплярах EC2 від Amazon. На початку все працювало в одному загальному пулі вузлів. Ми швидко усвідомили необхідність поділу робочих навантажень за розмірами та типами екземплярів для більш ефективного використання ресурсів. Логіка була в тому, що запуск кількох навантажених багатопоточних pod'ів виявлявся більш передбачуваним за продуктивністю, ніж їх співіснування з більшим числом однопоточних pod'ів.

У результаті ми зупинилися на:

  • м5.4хвеликий - для моніторингу (Prometheus);
  • c5.4xlarge - для робочого навантаження Node.js (однопоточне робоче навантаження);
  • c5.2xlarge - для Java і Go (багатопоточне робоче навантаження);
  • c5.4xlarge - для контрольної панелі (3 вузли).

міграція

Одним із підготовчих кроків для міграції зі старої інфраструктури на Kubernetes стало перенаправлення існуючої прямої взаємодії між сервісами до нових балансувальників навантаження (ELastic Load Balancers, ELB). Вони були створені в певній підмережі віртуальної приватної хмари (VPC). Ця підмережа була підключена до VPC Kubernetes. Це дозволило нам переносити модулі поступово, враховуючи конкретний порядок залежностей від сервісів.

Ці endpoints були створені за допомогою зважених наборів DNS-записів, у яких CNAME вказували на кожен новий ELB. Для перемикання ми додавали новий запис, що вказує на новий ELB служби Kubernetes з вагою, що дорівнює 0. Потім встановлювали Time To Live (TTL) набору записів на 0. Після цього старі та нові вагові коефіцієнти повільно коригувалися, і зрештою 100% навантаження прямували на новий сервер. Після завершення перемикання значення TTL поверталося більш адекватний рівень.

Наявні у нас Java-модулі справлялися з низьким TTL DNS, а Node-додатки - ні. Один з інженерів переписав частину коду пулу з'єднань і обернув його в менеджера, який оновлював пули кожні 60 секунд. Вибраний підхід спрацював дуже добре та без помітного зниження продуктивності.

Уроки

Межі мережевої фабрики

Рано-вранці 8 січня 2019 року платформа Tinder несподівано «впала». У відповідь на незв'язане збільшення часу очікування платформи раніше того ж ранку в кластері зросла кількість pod'ів та вузлів. Це призвело до вичерпання кешу ARP на всіх наших вузлах.

Є три параметри Linux, пов'язані з кешем ARP:

Перехід Tinder на Kubernetes
(джерело)

gc_thresh3 - Це жорстка межа. Поява в лозі записів виду "neighbor table overflow" означало, що навіть після синхронного збору сміття (GC) в кеші ARP виявлялося недостатньо місця для зберігання сусіднього запису. І тут ядро ​​просто повністю відкидало пакет.

Ми використовуємо Фланель як мережева фабрика (network fabric) в Kubernetes. Пакети надсилаються через VXLAN. VXLAN являє собою L2-тунель, піднятий поверх L3-мережі. Технологія використовує інкапсуляцію MAC-in-UDP (MAC Address-in-User Datagram Protocol) та дозволяє розширювати мережні сегменти 2-го рівня. Транспортний протокол у фізичній мережі центру обробки даних – IP плюс UDP.

Перехід Tinder на Kubernetes
Малюнок 2-1. Діаграма Flannel (джерело)

Перехід Tinder на Kubernetes
Малюнок 2-2. Пакет VXLAN (джерело)

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

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

У нашому середовищі такий тип зв'язку дуже поширений. Для об'єктів типу сервіс Kubernetes створюється ELB і Kubernetes реєструє кожен вузол в ELB. ELB нічого не знає про pod'ах і обраний вузол може бути кінцевим пунктом призначення пакета. Справа в тому, що коли вузол отримує пакет від ELB, він розглядає його з урахуванням правил Iptables для конкретного сервісу та випадковим чином вибирає pod на іншому вузлі.

На момент збою у кластері було 605 вузлів. З причин, викладених вище, цього виявилося достатньо, щоб подолати значення gc_thresh3за замовчуванням. Коли таке відбувається, не лише пакети починають відкидатися, а й весь віртуальний адресний простір Flannel з маскою /24 пропадає з таблиці ARP. Зв'язок вузол-pod та DNS-запити перериваються (DNS розміщений у кластері; подробиці читайте далі у цій статті).

Щоб вирішити цю проблему, необхідно збільшити значення gc_thresh1, gc_thresh2 и gc_thresh3 та перезапустити Flannel для перереєстрації зниклих мереж.

Несподіване масштабування DNS

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

У міру перенесення сервісів Kubernetes ми виявили, що DNS обробляє по 250 тисяч запитів в секунду. В результаті програми стали відчувати постійні та серйозні timeout'и за DNS-запитами. Це сталося незважаючи на неймовірні зусилля щодо оптимізації та перемикання DNS-провайдера на CoreDNS (який на піку навантаження досяг 1000 pod'ів, що працюють на 120 ядрах).

Досліджуючи інші можливі причини та рішення, ми виявили статтю, що описує race conditions, що впливають на фреймворк фільтрації пакетів netfilter у Linux. Спостережені нами timeout'и разом з лічильником, що збільшується. insert_failed в інтерфейсі Flannel відповідали висновкам статті.

Проблема виникає на етапі Source та Destination Network Address Translation (SNAT та DNAT) та подальшого внесення до таблиці контракт. Одним із обхідних шляхів, що обговорювалося всередині компанії та запропонованим співтовариством, стало перенесення DNS на сам робочий вузол. В цьому випадку:

  • SNAT не потрібний, оскільки трафік залишається всередині вузла. Його не потрібно проводити через інтерфейс eth0.
  • DNAT не потрібен, оскільки IP адресата є локальним для вузла, а не випадково обраним pod'ом за правилами Iptables.

Ми вирішили дотримуватись цього підходу. CoreDNS був розгорнутий як DaemonSet у Kubernetes та ми впровадили локальний DNS-сервер вузла у resolve.conf кожного pod'a налаштувавши прапор -cluster-dns команди кубелет . Це рішення виявилося ефективним для timeout'ів DNS.

Однак ми, як і раніше, спостерігали втрату пакетів і збільшення лічильника insert_failed в інтерфейсі Flannel. Таке положення зберігалося і після впровадження обхідного шляху, оскільки ми зуміли виключити SNAT та/або DNAT лише для DNS-трафіку. Race conditions зберігалися інших типів трафіку. На щастя, більшість пакетів у нас TCP, і при виникненні проблеми вони просто передаються повторно. Ми досі намагаємося знайти потрібне рішення для всіх типів трафіку.

Використання Envoy для кращого балансування навантаження

У міру міграції backend-сервісів у Kubernetes ми почали страждати від незбалансованого навантаження між pod'ами. Ми виявили, що через HTTP Keepalive з'єднання ELB зависали на перших готових pod'ах кожного викочуваного deployment'а. Таким чином, основна частина трафіку йшла через невеликий відсоток доступних pod'ів. Першим рішенням, випробуваним нами, стала установка параметра MaxSurge на 100% на нових deployment'ах для найгірших випадків. Ефект виявився незначним і неперспективним у плані більших deployment'ів.

Ще одне використане нами рішення полягало в тому, щоб штучно збільшувати запити на ресурси для критично важливих сервісів. В цьому випадку у розміщених по сусідству pod'ів було б більше простору для маневру в порівнянні з іншими важкими pod'ами. У довгостроковій перспективі воно також не спрацювало б через порожню витрату ресурсів. Крім того, наші Node-додатки були однопотоковими і, відповідно, могли задіяти лише одне ядро. Єдиним реальним рішенням було використання кращого балансування навантаження.

Ми давно хотіли повною мірою оцінити Посланець. Ситуація, що склалася, дозволила нам розгорнути його вкрай обмеженим чином і отримати негайні результати. Envoy - це високопродуктивний проксі сьомого рівня з відкритим вихідним кодом, розроблений для великих SOA-додатків. Він вміє застосовувати передові методи балансування навантаження, включаючи автоматичні повтори, circuit breakers та глобальне обмеження швидкості. (Прим. перев.: Докладніше про це можна почитати в цієї статті про Istio, в основі якого використовується Envoy.

Ми придумали наступну конфігурацію: мати по Envoy sidecar'у для кожного pod'а і єдиний маршрут, а кластер - підключати до контейнера локально портом. Щоб звести до мінімуму потенційне каскадування та зберегти невеликий радіус «ураження», ми використовували парк front-proxy pod'ів Envoy, по одному на кожну зону доступності (Availability Zone, AZ) для кожного сервісу. Вони зверталися до простого механізму виявлення сервісів, написаного одним із наших інженерів, який просто повертав список pod'ів у кожній AZ для даного сервісу.

Потім сервісні front-Envoy'і використовували цей механізм виявлення сервісів з одним upstream-кластером та маршрутом. Ми задали адекватні timeout'и, збільшили всі налаштування circuit breaker'а та додали мінімальну конфігурацію повторів, щоб допомогти з одиночними збоями та забезпечити безперешкодні розгортання. Перед кожним із цих сервісних front-Envoy'ів ми розташували TCP ELB. Навіть якщо keepalive з нашого основного проксі-шару зависав на деяких Envoy pod'ах, вони все ж таки могли набагато краще справлятися з навантаженням і були налаштовані на балансування через least_request в backend.

Для deployment'ів ми використовували хук preStop як на pod'ах додатків, так і на pod'ах sidecar'ов. Хук ініціював помилку в перевірці стану у адмінського endpoint'а, розташованого на sidecar-контейнері, і засинав на деякий час для того, щоб дати можливість завершитися активним з'єднанням.

Одна з причин, через яку ми змогли так швидко просунутися, пов'язана із докладними метриками, які ми змогли легко інтегрувати у звичайну установку Prometheus. Це дозволило нам точно бачити, що відбувається, поки ми підбирали параметри конфігурації та перерозподіляли трафік.

Результати були негайними та очевидними. Ми почали з найбільш незбалансованих сервісів, а зараз він функціонує вже перед 12 найважливішими сервісами в кластері. Цього року ми плануємо перехід на повноцінний service mesh з більш просунутим виявленням сервісів, circuit breaking'ом, виявленням викидів, обмеженням швидкості та трасуванням.

Перехід Tinder на Kubernetes
Малюнок 3-1. Конвергенція CPU одного сервісу під час переходу на Envoy

Перехід Tinder на Kubernetes

Перехід Tinder на Kubernetes

Кінцевий результат

Завдяки отриманому досвіду та додатковим дослідженням ми створили сильну команду з інфраструктури з гарними навичками щодо проектування, розгортання та експлуатації великих кластерів Kubernetes. Тепер усі інженери Tinder мають знання та досвід про те, як упаковувати контейнери та розгортати програми в Kubernetes.

Коли на старій інфраструктурі виникала потреба у додаткових потужностях, нам доводилося за кілька хвилин чекати запуску нових екземплярів EC2. Тепер контейнери починають запускатися і починають робити трафік протягом декількох секунд замість хвилин. Планування роботи кількох контейнерів на одному екземплярі EC2 також забезпечує покращену горизонтальну концентрацію. У результаті в 2019 році ми прогнозуємо значне зниження витрат на EC2 порівняно з минулим роком.

На міграцію пішло майже два роки, але ми завершили її у березні 2019-го. В даний час платформа Tinder працює виключно на кластері Kubernetes, що складається з 200 сервісів, 1000 вузлів, 15 000 pod'ів та 48 000 працюючих контейнерів. Інфраструктура більше не є долею виключно команд з експлуатації. Всі наші інженери поділяють цю відповідальність і контролюють процес складання та розгортання своїх додатків лише за допомогою коду.

PS від перекладача

Читайте також у нашому блозі цикл статей:

Джерело: habr.com

Додати коментар або відгук