Контейнери, мікросервіси та сервіс-міші

В інтернеті купа статей о сервіс-мішах (Service mesh), і ось ще одна. Ура! Але навіщо? Тому, що я хочу викласти свою думку, краще б сервіс-міші з'явилися 10 років тому, до появи контейнерних платформ, таких як Docker і Kubernetes. Я не стверджую, що моя точка зору краща чи гірша за інших, але оскільки сервіс-міші — досить складні тварини, множинність точок зору допоможе краще їх зрозуміти.

Я розповім про платформу dotCloud, яка була побудована на більш ніж сотні мікросервісів і підтримувала тисячі програм у контейнерах. Я поясню проблеми, з якими ми зіткнулися при її розробці та запуску, і як сервіс-міші могли б допомогти (або не могли).

Історія dotCloud

Я вже писав про історію dotCloud та вибір архітектури для цієї платформи, але мало розповідав про мережевий рівень. Якщо не хочете занурюватись у читання минулої статті про dotCloud, то коротко ось суть: це платформа-як-сервіс PaaS, що дозволяє клієнтам запускати широкий спектр додатків (Java, PHP, Python…), з підтримкою широкого спектра служб даних (MongoDB, MySQL, Redis…) та робочим процесом як у Heroku: ви завантажуєте свій код на платформу, вона будує образи контейнерів та розгортає їх.

Я розповім, як прямував трафік на платформу dotCloud. Не тому, що це було особливо здорово (хоча для свого часу система працювала непогано!), але насамперед тому, що за допомогою сучасних інструментів такий дизайн легко може реалізувати за короткий час скромна команда, якщо їм потрібен спосіб маршрутизації трафіку між купою мікросервісів або купою додатків. Таким чином, можна порівняти варіанти: що виходить, якщо розробити все самим або використовувати існуючий сервіс-міш. Стандартний вибір: зробити самим чи купити.

Маршрутизація трафіку для hosted-додатків

Програми на dotCloud можуть надавати кінцеві точки HTTP та TCP.

Кінцеві точки HTTP динамічно додаються до конфігурації кластера балансувальників навантаження Hipache. Це схоже на те, що сьогодні роблять ресурси. Вхід в Kubernetes і балансувальник навантаження на зразок Traefik.

Клієнти підключаються до кінцевих точок HTTP через відповідні домени за умови, що доменне ім'я вказує на балансувальники навантаження dotCloud. Нічого особливого.

Кінцеві точки TCP пов'язані з номером порту, який потім передається всім контейнерам цього стека через змінні середовища.

Клієнти можуть підключатися до кінцевих точок TCP, використовуючи відповідне ім'я хоста (щось на зразок gateway-X.dotcloud.com) та номер порту.

Це ім'я хоста резолвується на кластер серверів “nats“ (не має відношення до НАТИ), які будуть маршрутизувати вхідні TCP-з'єднання до правильного контейнера (або, у разі служб з балансуванням навантаження, до правильних контейнерів).

Якщо ви знайомі з Kubernetes, ймовірно, це нагадає вам служби Порт вузла.

На платформі dotCloud не було еквіваленту служб ClusterIP: для простоти доступ до служб відбувався однаково як зсередини, так і зовні платформи.

Все було організовано досить просто: початкові реалізації мереж маршрутизації HTTP і TCP, ймовірно, лише кілька сотень рядків Python. Прості (я б сказав, наївні) алгоритми, які допрацьовувалися із зростанням платформи та появою додаткових вимог.

Великий рефакторинг існуючого коду не був потрібний. Зокрема, 12-факторні програми можуть безпосередньо використовувати адресу, отриману через змінні оточення.

Чим це відрізняється від сучасного сервісу?

Обмежена оглядовість. У нас взагалі не було жодних метриків для сітки маршрутизації TCP. Що стосується маршрутизації HTTP, то в пізніших версіях з'явилися докладні HTTP-метрики з кодами помилок та часом відгуку, але сучасні сервіс-міші йдуть ще далі, забезпечуючи інтеграцію із системами збору метрик, як Prometheus, наприклад.

Огляд важлива не тільки з оперативної точки зору (щоб допомагати в усуненні проблем), але і при випуску нових функцій. Мова про безпечне синьо-зелене деплое и деплое канарок.

Ефективність маршрутизації теж обмежена. У сітці маршрутизації dotCloud весь трафік мав проходити через кластер виділених вузлів маршрутизації. Це означало потенційне перетинання кількох меж AZ (зони доступності) та значне збільшення затримки. Пам'ятаю, як усував проблеми з кодом, який робив більше сотні SQL-запитів на сторінку і відкривав нове з'єднання з SQL-сервером для кожного запиту. При локальному запуску сторінка завантажується миттєво, але в dotCloud завантаження займає кілька секунд, тому що для кожного з'єднання TCP (і наступного SQL-запиту) потрібно десятки мілісекунд. У цьому випадку проблему вирішили постійні з'єднання.

Сучасні сервіс-міші краще справляються з такими проблемами. Насамперед, вони перевіряють, що з'єднання маршрутизуються в джерелі. Логічний потік той самий: клиент → меш → сервис, але тепер меш працює локально, а не на віддалених вузлах, тому з'єднання клиент → меш є локальним і дуже швидким (мікросекунди замість мілісекунд).

Сучасні сервіс-міші також реалізують розумніші алгоритми балансування навантаження. Контролюючи працездатність бекендів, вони можуть відправляти більше трафіку на швидші бекенди, що призводить до підвищення загальної продуктивності.

Безпека теж краще. Сітка маршрутизації dotCloud працювала повністю на EC2 Classic і не шифрувала трафік (виходячи з припущення, що якщо комусь вдалося поставити сніффер на мережевий трафік EC2, у вас вже є великі проблеми). Сучасні сервіс-міші прозоро захищають весь наш трафік, наприклад, із взаємною TLS-автентифікацією та подальшим шифруванням.

Маршрутизація трафіку для служб платформи

Добре, ми обговорили трафік між програмами, але як щодо самої платформи dotCloud?

Сама платформа складалася приблизно з сотні мікросервісів, які відповідають різні функції. Одні приймали запити від інших, а деякі були фоновими воркерами, які підключалися до інших служб, але не приймали з'єднань. У будь-якому випадку кожна служба повинна знати кінцеві точки адрес, до яких необхідно підключитися.

Багато послуг високого рівня можуть використовувати сітку маршрутизації, описану вище. Насправді багато з більш ніж сотні мікросервісів dotCloud були розгорнуті як звичайні програми на самій платформі dotCloud. Але невелика кількість низькорівневих сервісів (зокрема, які реалізують цю сітку маршрутизації) потребувала чимось простішого, з меншими залежностями (оскільки для роботи вони не могли залежати від самих себе — стара добра проблема курки та яйця).

Ці низькорівневі важливі служби були розгорнуті шляхом запуску контейнерів безпосередньо на кількох ключових вузлах. При цьому не були задіяні стандартні служби платформи: компонувальник, планувальник і runner. Якщо хочете порівняти із сучасними контейнерними платформами, це схоже на запуск площини управління з docker run безпосередньо на вузлах замість делегування завдання Kubernetes. Це досить схоже на концепцію статичних модулів (подов), які використовує kubeadm або bootkube під час завантаження автономного кластера.

Ці служби експонувалися простим та грубим способом: у файлі YAML були перераховані їхні імена та адреси; а кожен клієнт мав для деплоя взяти копію цього YAML-файлу.

З одного боку, це надзвичайно надійно, тому що не потребує підтримки зовнішнього сховища ключів/значень, таких як Zookeeper (не забувайте, тоді ще не існувало etcd або Consul). З іншого боку, це ускладнювало переміщення служб. Щоразу під час переміщення всі клієнти мали отримати оновлений файл YAML (і потенційно перезавантажитися). Не дуже зручно!

Згодом ми почали впроваджувати нову схему, де кожен клієнт підключався до локального проксі-сервера. Замість адреси та порту йому достатньо знати тільки номер порту служби, та підключатися через localhost. Локальний проксі-сервер обробляє це з'єднання та спрямовує його на фактичний сервер. Тепер при переміщенні бекенда на іншу машину або масштабуванні замість оновлення всіх клієнтів потрібно оновити лише всі ці локальні проксі; і перезавантаження більше не потрібне.

(Також планувалося інкапсулювати трафік у TLS-з'єднаннях і поставити ще один проксі-сервер на стороні, що приймає, а також перевіряти сертифікати TLS без участі приймаючої служби, яка налаштована на прийом з'єднань тільки на localhost. Про це згодом).

Це дуже схоже на SmartStack від Airbnb, але суттєва різниця в тому, що SmartStack реалізований і розгорнутий у продакшн, тоді як внутрішню систему маршрутизації dotCloud прибрали в ящик, коли dotCloud перетворився на Docker.

Я особисто вважаю SmartStack одним із попередників таких систем, як Istio, Linkerd і Consul Connect, тому що всі вони слідують одному шаблону:

  • Запуск проксі кожного вузла.
  • Клієнти підключаються до проксі.
  • Площина керування оновлює конфігурацію проксі-сервера під час зміни бекендів.
  • … Профіт!

Сучасна реалізація сервіс-міша

Якщо нам потрібно реалізувати подібну сітку сьогодні, то ми можемо використовувати аналогічні принципи. Наприклад, налаштувати внутрішню зону DNS, зіставляючи імена служб адресам у просторі 127.0.0.0/8. Потім запустити HAProxy на кожному вузлі кластера, приймаючи з'єднання за кожною адресою служби (у цій підмережі 127.0.0.0/8) та перенаправляючи/балансуючи навантаження на відповідні бекенди. Конфігурація HAProxy може керуватися конф, дозволяючи зберігати інформацію про бекенд в etcd або Consul і автоматично пушити оновлену конфігурацію на HAProxy, коли це необхідно.

Приблизно так працює Istio! Але з деякими відмінностями:

  • Використовує Посланець проксі замість HAProxy.
  • Зберігає конфігурацію бекенду через Kubernetes API замість etcd або Consul.
  • Службам виділяються адреси у внутрішній підмережі (адреси Kubernetes ClusterIP) замість 127.0.0.0/8.
  • Має додатковий компонент (Citadel) для додавання взаємної автентифікації TLS між клієнтом і серверами.
  • Підтримує нові функції, таких як розрив ланцюга (circuit breaking), розподілене трасування, деплой канарок та ін.

Давайте коротко розглянемо деякі відмінності.

Посланець проксі

Envoy Proxy написала компанія Lyft [конкурент Uber на ринку таксі - прим. пров.]. Він багато в чому схожий на інші проксі (наприклад, HAProxy, Nginx, Traefik ...), але Lyft написала свій, тому що їм були потрібні функції, відсутні в інших проксі, і розумніше видалося зробити новий, ніж розширювати існуючий.

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

Але Envoy також здатний працювати як площина даних (data plane) для сервіс-міша. Це означає, що тепер для цього сервіс-міша Envoy налаштовується площиною управління (control plane).

Площина керування

У площині управління Istio покладається Kubernetes API. Це не дуже відрізняється від використання confdякий покладається на etcd або Consul для перегляду набору ключів у сховищі даних. Istio через Kubernetes API переглядає набір ресурсів Kubernetes.

Між іншим: особисто мені здалося корисним це опис Kubernetes API, Що говорить:

Сервер Kubernetes API – це «дурний сервер», який пропонує зберігання, керування версіями, перевірку, оновлення та семантику ресурсів API.

Istio розроблено для роботи з Kubernetes; і якщо ви хочете використовувати його за межами Kubernetes, вам потрібно запустити екземпляр сервера Kubernetes API (і допоміжної служби etcd).

Адреси служб

Istio покладається на адреси ClusterIP, які виділяє Kubernetes, тому служби Istio отримують внутрішню адресу (не в діапазоні 127.0.0.0/8).

Трафік на адресу ClusterIP для конкретної служби в кластері Kubernetes без Istio перехоплюється kube-proxy та відправляється на серверну частину цього проксі. Якщо вас цікавлять технічні деталі, kube-proxy встановлює правила iptables (або балансувальники навантаження IPVS, залежно від того, як його налаштували), щоб переписати IP-адреси призначення з'єднань, що йдуть за адресою ClusterIP.

Після встановлення Istio в кластері Kubernetes нічого не змінюється, поки він не буде явно включений для даного споживача або навіть всього простору імен шляхом введення контейнера sidecar в кастомні поди. Цей контейнер запустить екземпляр Envoy і встановить ряд правил iptables для перехоплення трафіку, що йде в інші служби, і перенаправлення цього трафіку на Envoy.

При інтеграції з Kubernetes DNS це означає, що наш код може підключатися на ім'я служби, і все «просто працює». Іншими словами, наш код видає запити типу http://api/v1/users/4242, тоді api резолвіт запит на 10.97.105.48, правила iptables перехоплюють з'єднання з 10.97.105.48 і перенаправляють їх на локальний проксі Envoy, а цей локальний проксі направить запит на фактичний бекенд API. Фух!

Додаткові рюшечки

Istio також забезпечує наскрізне шифрування та аутентифікацію через mTLS (mutual TLS). За це відповідає компонент під назвою Цитадель.

Також є компонент Змішувач, який Envoy може запросити для кожного запиту, щоб прийняти спеціальне рішення про цей запит залежно від різних факторів, таких як заголовки, завантаження бекенда і т. д… (не хвилюйтеся: є багато засобів забезпечити працездатність Mixer, і навіть якщо він злетить, Envoy продовжить нормально працювати як проксі) .

І, звичайно, ми згадали оглядовість: Envoy збирає величезну кількість метрик, забезпечуючи при цьому розподілене трасування. В архітектурі мікросервісів, якщо один запит API повинен пройти через мікросервіси A, B, C і D, то при вході в систему розподілене трасування додасть до запиту унікальний ідентифікатор і збереже даний ідентифікатор через підзапити до всіх цих мікросервісів, дозволяючи фіксувати всі пов'язані виклики, їх затримки і т.д.

Розробляти чи купити

Istio має репутацію складної системи. Навпаки, побудова сітки маршрутизації, яку я описав на початку цієї посади, відносно просто за допомогою існуючих інструментів. Отже, чи є сенс натомість створювати власний сервіс-міш?

Якщо у нас скромні потреби (не потрібна оглядовість, переривник ланцюга та інші тонкощі), то приходять думки про створення власного інструменту. Але якщо ми використовуємо Kubernetes, він може навіть не знадобитися, тому що Kubernetes вже надає базові інструменти для виявлення служб та балансування навантаження.

Але якщо у нас просунуті вимоги, то «купівля» сервіс-міша є набагато кращим варіантом. (Це не завжди саме "купівля", оскільки Istio поставляється з відкритим вихідним кодом, але нам все одно потрібно інвестувати інженерний час, щоб розібратися в його роботі, задеплоїти та керувати ним).

Що вибрати: Istio, Linkerd чи Consul Connect?

Поки що ми говорили тільки про Istio, але це не єдиний сервіс-міш. Популярна альтернатива Linkerd, а є ще Consul Connect.

Що вибрати?

Щиро кажучи, не знаю. На даний момент я не вважаю себе достатньо компетентним для відповіді на це запитання. є декілька цікавих статей з порівнянням цих інструментів і навіть бенчмарки.

Один із багатообіцяючих підходів — використовувати інструмент начебто SuperGloo. Він реалізує шар абстракції для спрощення та уніфікації API, що надаються сервіс-мішами. Замість того, щоб вивчати конкретні (і, на мій погляд, відносно складні) API різних сервіс-мішей, ми можемо використовувати простіші конструкції SuperGloo — і легко перемикатися з одного на інший, немов у нас проміжний формат конфігурації, що описує HTTP-інтерфейси та бекенди, здатний генерувати фактичну конфігурацію для Nginx, HAProxy, Traefik, Apache...

Я трохи побалувався з Istio і SuperGloo, а в наступній статті хочу показати, як додати Istio або Linkerd в існуючий кластер за допомогою SuperGloo, і наскільки останній впорається зі своєю роботою, тобто дозволяє перемикатися з одного сервісу на інший без перезапису конфігурацій.

Джерело: habr.com

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