Топология микросервисов: gateway, discovery, mesh
Четыре урока подряд мы смотрели на связи между сервисами по одной: вызов и его таймаут, событие и его доставка, очередь и её DLQ. Пора подняться на высоту, с которой видна вся карта: как запрос с телефона находит нужный из тридцати сервисов, кто проверяет его токен, кто повторяет упавший вызов, и как потом восстановить маршрут запроса по журналу. Это урок про инфраструктурный этаж микросервисов: API gateway и BFF на входе, service discovery внутри, sidecar и mesh как способ вынести сетевую логику из кода, и distributed tracing как зрение во всём этом хозяйстве. Урок обзорный: каждая из четырёх тем руками собирается в Go-практикуме раздела 24 и в уроках про инфраструктуру, здесь же мы строим карту, чтобы было куда класть детали.
Service discovery: как сервисы находят друг друга
Начнём с вопроса, который в монолите не существовал: по какому адресу
звать соседа? Пока сервис был модулем, ответом был импорт. Теперь
tickets живёт в трёх экземплярах, которые пересоздаются при каждом
деплое и автоскейле, и у каждого новый IP. Конфиг с прописанными
адресами мёртв с рождения: он устаревает быстрее, чем доезжает до
прода.
Ответ индустрии: адрес это не конфигурация, а ответ на запрос к живому источнику. Источник называется service registry, и вокруг него два решения, которые надо принять.
Первое: кто регистрирует. Либо сервис сам сообщает о себе при старте и шлёт heartbeat (self-registration, так работают клиенты Consul и Eureka), либо это делает платформа, которая и так знает, что и где запустила (Kubernetes регистрирует поды сама). Второй вариант снимает заботу с кода, и это один из главных бытовых аргументов за K8s.
Второе: кто выбирает инстанс. Здесь классическая развилка:
- Client-side discovery. Клиент сам спрашивает реестр, получает список живых инстансов и сам выбирает, к кому пойти. Плюс: балансировка в клиенте, можно умную (меньше всего висящих запросов, ближайшая зона). Минус: логика discovery повторяется в каждом языке и каждом сервисе, и её версии расползаются.
- Server-side discovery. Клиент ходит на стабильное имя, а реестр спрятан за балансировщиком или DNS. Плюс: клиент туп и прост. Минус: ещё один прыжок и ещё один компонент на пути.
Сегодняшний дефолт выглядит обыденно: Kubernetes Service. Сервис
tickets доступен по DNS-имени tickets.default.svc, kube-proxy
раскидывает соединения по живым подам, состав реестра определяется
health check-ами. Это server-side discovery из коробки, и для
большинства систем вопрос закрыт до того, как был задан. Имена
Consul (полноценный registry с health-чеками и key-value) и Eureka
(исторический client-side registry из стека Netflix) стоит знать,
чтобы читать архитектурные статьи, но тянуть их в систему поверх K8s
нужно редко.
Деталь, которая связывает главу с уроком 18: health check как условие членства в реестре. Вспомни медленный сервис рекомендаций: процесс жив, порт открыт, health check зелёный, пользы ноль. Хороший check проверяет способность работать, а не способность ответить «я жив»: глубина очереди, доступность базы, доля ошибок. Иначе discovery честно балансирует трафик между тремя инстансами, два из которых в коме.
API Gateway и BFF
Снаружи системы тот же вопрос звучит острее: телефону пользователя нельзя выдать список из тридцати сервисов и пожелать удачи. Клиент не должен знать топологию вовсе: она меняется каждый квартал, а приложение в сторах живёт годами.
Точка входа, которая прячет топологию, называется API gateway. Кроме маршрутизации на ней селятся все сквозные заботы входа: TLS, аутентификация (проверить токен один раз на входе, внутрь передать проверенную identity), rate limiting, версии API, кеш. Знакомая логика: это фасад из урока 17, только постоянный, а не миграционный, и от reverse proxy или балансировщика его отличает именно осведомлённость об API: proxy гоняет байты, gateway знает про маршруты, токены и контракты.
У gateway есть собственный гравитационный колодец: в него протекает бизнес-логика. Сначала «давайте склеим два ответа», потом «и отфильтруем по правам», потом «и посчитаем скидку», и вот на входе системы стоит новый монолит, который меняется при каждой фиче и принадлежит всем сразу, то есть никому. Правило гигиены: gateway занимается транспортом и доступом, доменная логика живёт в сервисах.
Отдельная история начинается, когда клиентов становится много и они разные. Вебу нужна страница сеанса одним толстым ответом, мобильному тот же экран в три раза тоньше (трафик и батарея), смарт-ТВ нужен другой формат картинок, а партнёрскому API вообще другая модель. Один gateway на всех превращается в поле боя: команды клиентов конкурируют за его роадмап и ломают друг другу форматы. Решение Сэма Ньюмана: BFF. У каждого фронтенда свой тонкий бэкенд, которым владеет команда этого фронтенда: мобильная команда сама решает, какой формат нужен её экранам, и не согласовывает это ни с кем. Это снова закон Конвея из урока 17, применённый к точке входа: граница по команде.
Главная работа BFF: агрегация. Экран сеанса собирается из трёх сервисов, и делать это лучше в одном месте за один сетевой прыжок от клиента, чем тремя запросами с телефона через мобильную сеть. Набросок на Fastify, с дисциплиной из урока 18:
app.get('/bff/sessions/:id', async (request) => {
const { id } = request.params;
// бюджет всего экрана: 800 мс, дальше клиент уже страдает
const signal = AbortSignal.any([
AbortSignal.timeout(800),
request.raw.signal,
]);
const [session, seats, recs] = await Promise.all([
fetchJson(`${CATALOG}/sessions/${id}`, { signal }),
fetchJson(`${TICKETS}/sessions/${id}/availability`, { signal }),
// рекомендации не стоят того, чтобы ронять экран: деградируем
fetchJson(`${RECS}/for-session/${id}`, { signal }).catch(() => []),
]);
return { session, seats, recommendations: recs };
});
Три вызова едут параллельно (латентность экрана равна худшему из трёх, а не сумме), у всех общий дедлайн, а необязательный блок рекомендаций деградирует в пустой список вместо ошибки. Где агрегировать, на BFF или на фронте, мы уже обсуждали со стороны клиента в уроке про состояние и формы; ответ BFF «экран одним запросом» особенно хорош для мобильных сетей с их дорогими раундтрипами.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.