Перейти к основному содержимому
megapromotingДавайте обсудим

Экспертиза · Сложные архитектуры

Несколько систем, которые должны работать вместе, в том числе когда одна из них падает.

Мы проектируем и эксплуатируем платформы из нескольких сервисов, с шиной сообщений, ограниченными повторными попытками, ключами идемпотентности, circuit breaker-ами и публикацией с автоматическим возвратом. Каждый шаблон ниже работает в собственной системе, а не на диаграмме.

Уже построеноМы эксплуатируем четыре собственные платформы с разными архитектурами и можем открыть их все. Цифры измерены сегодня командами, а не взяты из документации: aichat имеет 28 служебных директорий, 235 моделей данных на четырёх схемах и шину сообщений с 297 очередями и 17.525 связями; Kallina имеет 472 edge-функции, 559 миграций и 28 запланированных задач в базе; MEGA CRM имеет backend из 9.516 строк с 99 маршрутами и 179 политиками доступа в строку; Taskin запускает 21 запланированную задачу без единого Docker. Оговорка, которую мы говорим: в двух местах собственная документация отстала от кода — утверждение о числе строк было устаревшим на 16%, а схема инфраструктуры описывала сервер, на котором мы уже ничего не запускаем. Мы использовали измерение, а не документ.

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

Наша работа начинается с разделения обязанностей и заканчивается тем, что видно, когда что-то выходит из строя. В одной из собственных платформ сообщения не идут напрямую между сервисами, а проходят через шину с 297 очередями, 9 exchange и 17.525 связями, из которых 278 очередей генерируются на клиента, а не пишутся вручную. Восемь очередей имеют dead-letter exchange, и у восьми время жизни сообщения составляет пять минут — то есть сообщение, которое не может быть обработано, попадает туда, где его можно увидеть, а не исчезает.

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

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

Что входит

Работа, по составляющим

Мы разделяем сервисы по тому, что может сломаться независимо

В одной собственной платформе 28 служебных директорий, из которых 16 имеют собственную точку входа, а в продакшене под менеджером процессов работают пять процессов. Каждый канал связи имеет собственный сервис и собственный порт, именно чтобы сбой в одном не остановил остальные. Данные моделируются на четырёх отдельных схемах, всего 235 моделей — разделение не только на уровне процесса, но и на уровне схемы.

Мы выстраиваем шину сообщений, а не цепочку синхронных вызовов

297 очередей, 9 exchange, 17.525 связей. Название очередей задаётся для каждого клиента и, в некоторых случаях, для каждого треда — то есть топология генерируется, а не пишется вручную. Есть также exchange с отложенной доставкой для вещей, которые должны произойти позже, а не сейчас. Восемь очередей имеют настроенный dead-letter exchange, а у восьми время жизни сообщения — пять минут.

Повторные попытки с лимитом, а не бесконечно

Три разных лимита для трёх разных ситуаций, все записаны в коде: пять повторных попыток для запланированных сообщений, с сохранением счётчика, а не в памяти; три повторные попытки на уровне очереди, считаемые в заголовке сообщения; и три повторные попытки с экспоненциальным увеличением паузы для отправки e-mail, по двум путям поставщика. Повторная попытка без лимита — это не отказоустойчивость, это цикл.

Ключи идемпотентности, чтобы повторная доставка ничего не сломала

Для событий от третьих сторон мы используем вставку, которая завершается с ошибкой, если событие уже было замечено: нарушение уникальности базы данных трактуется как «duplicate», а не как ошибка, вместе с проверкой свежести момента. Для платежей ключ — это метка на credit transaction, поэтому повторная отправка того же webhook не кредитует дважды. Для сообщений дедупликация выполняется по идентификатору, со временем жизни и очисткой.

Circuit breaker для хрупкой интеграции

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

Порог потребления перед дорогостоящими действиями

На одной из платформ любое платное действие проходит через четырёхступенчатую функцию проверки, каждая ступень возвращает в явном виде собственную причину: аккаунт приостановлен, месячный лимит разговоров достигнут, дневной потолок расходов превышен, недостаточный баланс. При исключении возвращает «не разрешено», а не «разрешено» — ворота закрываются, а не открываются, когда что-то не работает. Сзади стоит реестр событий потребления со стоимостью за событие, с разбивкой по поставщику, модели и модальности.

Ограничение скорости, переживающее перезапуск

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

Наблюдаемость, которая различает «ответил» и «сработал»

Ответ 200 не означает, что задача выполнена успешно. Обёртка, с которой мы запускаем запланированные задачи, рекурсивно проходит ответ в поиске списков сбоев или ошибок и обрабатывает «200 со сбоями» как отдельный результат. У оповещений есть порог повторения — десять минут в одной системе, два часа в другой — а метка стирается при первом успехе, чтобы повторно возникшая неисправность могла сразу снова вызвать оповещение.

Публикация с проверкой и автоматическим откатом

Публикация проверяет не только то, что адрес отвечает 200, но и то, что обслуживаемая страница содержит точный идентификатор только что опубликованного пакета; иначе она возвращает предыдущую версию. Для фонового сервиса публикация сравнивает дерево по контрольным суммам и пропускает перезапуск, если ничего не изменилось — ненужный перезапуск убивает работающий цикл.

Как это выглядит

Путь, шаг за шагом.

01

Мы рисуем карту режимов отказа, а не коробок

Для каждой связи между двумя системами: что происходит, если другая сторона медленная, если она упала, если отвечает дважды, если отвечает неверно. Мы поставляем: список связей с ожидаемым поведением в каждой из четырёх ситуаций, плюс решение синхронно/асинхронно для каждой.

02

Мы настраиваем шину и контракты сообщений

Очереди, обмены, мёртвые письма, время жизни сообщения и ключ идемпотентности для каждого типа события. Мы поставляем: топологию, контракты сообщений и задокументированное поведение при повторной отправке.

03

Мы добавляем ворота: rate limit, consumption, circuit breaker

Ограничение скорости по скользящему окну в базе, ворота потребления перед дорогими действиями и circuit breaker для интеграций вне нашего контроля. Мы поставляем: настроенные пороги, возвращаемые понятные причины и тесты для каждого уровня отказа.

04

Мы строим публикацию и откат

Публикация с проверкой содержимого, а не только кода ответа, сохранением предыдущей версии рядом и автоматическим откатом. Мы поставляем: процедуру публикации, процедуру отката, отработанную как минимум один раз, и проверки здоровья — одну поверхностную для «жив», одну глубокую для «действительно работает».

05

Мы сдаём с документацией, сверенной с кодом

Документ сверяется с измерением перед сдачей, потому что документация, которая отстаёт, опаснее, чем её отсутствие. Мы поставляем: диаграмму, процедуры и явный список мест, где документ и код были найдены в расхождении, с тем, что мы исправили.

DocumenteРазговорыИсточникиСинтезДействиеКомандаКонтекстуализируйАвторизованные источники. Проверенные действия.
Системы не вызывают друг друга по цепочке: между ними стоит шина с очередями на клиента, отдельными обменниками по типу трафика и мёртвыми письмами для того, что нельзя обработать. Вокруг неё — шлюзы: ограничение частоты, шлюз потребления, прерыватель цепи — и пробы здоровья, одна поверхностная и одна глубокая.

Данные

К чему прикасаемся, где оно лежит и сколько остаётся

Вопросы, которые задаёт всякий, у кого есть ответственный за защиту данных, — заданы здесь раньше, чем их задаст он.

Где хранятся данные, по платформам
Нет единого ответа, и это хорошо: одна платформа использует MySQL на собственном сервере, две используют PostgreSQL через Supabase — одна размещена, одна установлена на нашей инфраструктуре — а одна хранит контент как статические файлы, без базы данных вообще. Выбор делается по требованиям, а не по привычке.
Доступ применяется в базе, а не только в приложении
Во внутренней системе безопасности на уровне строк включена на 43 таблицах, через 90 инструкций, с 179 написанными политиками. Правило, которого мы придерживаемся: если вызов обходит приложение и напрямую бьёт в базу, он всё равно не должен видеть чужие строки.
Реестр потребления
Одна строка на событие с типом, поставщиком, количествами по модальности — секунды разговора, токены входа, токены выхода, символы — плюс стоимость в валюте с шестью знаками после запятой и списанные кредиты. Из него получаются дневной лимит расходов, стоимость на вызов и прогноз потребления.
Миграции — это история, а не документация
559 миграций на одной платформе, 80 на другой. Схема меняется через версионированные миграции, поэтому состояние базы можно реконструировать и читать хронологически. Когда документ и миграция не совпадают, права миграция.
Плановые задачи хранятся в базе или в cron, а не в процессе
28 запланированных задач работают внутри базы данных в одной платформе; в другой 21 задача работает через системный cron. Причина записана в файле: таймеры внутри сервиса сбрасываются при каждой перезагрузке, поэтому трёхчасовой цикл в сервисе, который деплоится повторно, никогда не срабатывает.

Случай

Топология сообщений, которая генерируется сама для каждого клиента

Ситуация

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

Что мы построили

Топология генерируется по клиенту, а не пишется вручную: из 297 очередей 278 — типа «уведомления для определённого аккаунта», а некоторые опускаются до уровня одной ветки обсуждения. Поверх них девять exchange разделяют типы трафика — уведомления, токены, webhook-ы, каналы — и есть exchange с отложенной доставкой для того, что должно произойти позже. Восемь очередей имеют dead-letter exchange, у восьми время жизни сообщения составляет пять минут. Правила оркестрации можно подгружать на лету, через канал публикации, без перезапуска потребителей.

Что получилось

Клиент с большим объёмом не задерживает остальных клиентов, а сообщение, которое нельзя обработать, попадает туда, где его можно увидеть и повторно запустить, вместо того чтобы исчезнуть или блокировать очередь. Количество связей в шине — 17.525 — точно показывает, почему топология должна генерироваться: никто не обслуживает вручную что-то подобное.

Чего этот случай не говорит

Цена операционная: шина такого размера нуждается в собственной мониторинговой системе и в плане для брошенных очередей, иначе она растёт бесконечно. А генерация по клиенту означает, что удаление клиента удаляет и его топологию — если этот шаг отсутствует, накапливаются мёртвые очереди.

Вопросы

О чём нас спрашивают перед тем, как позвонить

Откуда я знаю, что вы не продаёте мне сложность, которая мне не нужна?

Потому что первая рекомендация, которую мы часто даём, — не разделять. Каждый новый сервис — это новый режим отказа, а режимы отказа множатся быстрее, чем функциональности. Пример из наших собственных систем: backend одного из них — это один файл на 9.516 строк с 99 маршрутами. Это не элегантно, и мы это говорим, но он публикуется в один шаг и отлаживается в одном месте. Разделение делается тогда, когда есть измеримая причина — сервис, который нужно масштабировать отдельно, отдельная команда, отдельный ритм выпуска.

Что происходит, когда система в цепочке не отвечает?

Это зависит от того, что мы вместе решили на этапе карты, и в этом суть. В наших системах: сообщение попадает в очередь и повторяется ограниченное число раз, затем попадает в dead-letter exchange, где его можно увидеть; нестабильная интеграция имеет выключатель, который после десяти последовательных сбоев выключается на две минуты вместо того, чтобы держать запросы в ожидании; а действия, которые стоят денег, останавливаются воротами, которые при исключении отказывают, а не разрешают.

Как вы избегаете повторной обработки одного и того же события?

С помощью ключа идемпотентности, а не проверки «я это уже видел?», в которой есть гонка данных. Конкретно: мы вставляем строку с идентификатором события и, если база отказывает из-за нарушения уникальности, трактуем этот код ошибки как сигнал о дубликате, а не как сбой. Для платежей метка стоит на кредитной транзакции, поэтому повторная отправка того же webhook-а не кредитует дважды.

Вы используете Docker или нет?

И то и другое, и выбор мы каждый раз обосновываем. Одна система работает в контейнере, но без смонтированных volume, а это значит, что публикация — это копирование в контейнер плюс перезапуск; `docker rm` там теряет состояние, и это записано в эксплуатационном документе именно так. Другая платформа не имеет никакого Docker: публикация — это синхронизация файлов, а скрипты публикации установлены вручную с правами администратора и доступны автоматическому потоку ровно через два разрешённых пути. Причина записана в коде: скрипт, который поток может переписать, — это скрипт, через который поток может эскалировать.

Как вы знаете, что запланированная задача действительно отработала?

Из плохого опыта. Один из наших ежедневных отчётов был мёртв одиннадцать дней, с 7 по 17 августа, в то время как запланированная задача запускалась каждый день — используемая команда молча выходила при ошибке и ничего не писала. С тех пор каждая задача работает в оболочке, которая проходит по ответу по спискам сбоев, трактует «200 с ошибками» как отдельный результат, имеет максимальное время выполнения и пишет структурированную строку на каждый запуск. Оповещения имеют порог повторения, а метка удаляется при первом успехе.

Насколько важна абстракция по поставщику?

Очень, если поставщик находится в быстро меняющейся области. Для голоса у нас есть три отдельные мостовые — по одной для каждого поставщика speech в реальном времени — которые реализуют один и тот же интерфейс. Мостовая делает преобразование аудио между телефонной станцией и поставщиком, на выделенном порту, а health server связан только с локальным интерфейсом, не выставлен наружу. Смена поставщика становится решением, а не переписыванием.

Ваша документация в актуальном состоянии?

Не везде, и мы предпочитаем говорить где. При подготовке этой страницы мы измерили две системы и нашли собственный документ с отставанием: одно утверждение о размере файла было примерно на 16% меньше реальности, а одна диаграмма инфраструктуры описывала сервер, с которого мы уже переехали. Правило, которое мы применяем и которого требуем в проектах: когда документ и измерение не совпадают, право за измерением, а документ исправляется в тот же шаг.

Чего вы не делаете?

Мы не обещаем цели по доступности без измерения — процент вроде «99,9%» требует полевых данных за период, и там, где их нет, мы этого не утверждаем. Мы не проектируем системы, которые не можем эксплуатировать или передать: если результат — архитектура, которую ваша команда не может поддерживать, это неправильная архитектура. И мы не делаем сертификаты соответствия — мы можем построить контроли, мы не можем выдать сертификат.

На чём основаны утверждения выше (23 источников)

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

Что вы хотели бы наладить?

Расскажите о своём процессе. Вместе определим, что стоит построить, что можно подключить и как проверим результат.

Давайте обсудим