ADR-0002 · Границы: перенос линии вокруг лент#
Статус: принят · 2026-09-16
Метод: D-02 v3.0 · шаблон ADR v1.2
Участники: команды «лента», «посты», «платформа»
Учебная запись: часть прогона E-02, система вымышлена. Форма и рассуждение настоящие.
Контекст#
Вход — 00-context.md. Хранилище лент выбрано в ADR-0001.
Что есть сейчас — два сервиса, разделённые исторически, а не по решению:
| Сервис | Что внутри | Владелец |
|---|---|---|
web |
API, сборка ленты, профили, граф подписок | Команда «лента» |
posts |
Создание и хранение постов, медиа | Команда «посты» |
Сборка ленты живёт в web, посты — в posts, поэтому каждое открытие ленты требует обращения через границу. Отсюда 347 тысяч межсервисных обращений в секунду [расчёт W1] и деградация p99.
Семь вопросов D-02 v3.0 — впервые все с ответами#
| Вопрос | Ответ | Пометка |
|---|---|---|
| Сколько изменений за полгода затронуло больше одного сервиса? | 32 % из 1 400 задач. В одной границе укладывается 68 % | [история 6 мес] |
| Какие операции обязаны выполняться атомарно? | Ни одной | [требования] |
| У каких сущностей больше одного пишущего? | Одна: users.last_seen пишут и web при открытии ленты, и профили при заходе |
[схема] |
| Где одно слово означает разное? | «Лента»: экран у продукта, список идентификаторов у бэкенда, поток событий у аналитики. «Пост»: запись в posts, идентификатор в ленте |
[орг] |
| Сколько команд и кто чем владеет? | Три: лента, посты, платформа. Команда «лента» владеет web, где лежат ещё профили и подписки — не её язык |
[орг] |
| У каких частей профиль нагрузки различается на порядок? | Чтение ленты 12 000/с против публикации 12/с — три порядка | [расчёт W1] |
| Что по плану переписывается в год? | Ранжирование ленты — сядет на ленту, не на посты | [орг] |
68 % — ниже порога. Здоровой считается доля около 80 %; треть задач, пересекающих границу, означает, что линия проведена не там. Это тот самый случай, ради которого документ и существует: история изменений опровергла существующий разрез, и никаким рассуждением этого получить было нельзя.
Заодно выяснилось, что нынешняя граница частично идёт по техническим слоям: web — это «всё, что отвечает на запросы», posts — «то, где лежат данные постов». D-02 называет такой разрез анти-формой, и 32 % сквозных задач — ровно его симптом.
Дисквалификаторы#
Сработал один из шести — против трёх в E-01, где они отсекли саму идею разреза.
| Условие | Здесь |
|---|---|
| Разрыв атомарной операции в основном сценарии | Не срабатывает: атомарности нет нигде |
| У сущности больше одного пишущего | Срабатывает для варианта «оставить как есть»: users.last_seen остаётся с двумя писателями |
| Разрез по техническим слоям | Не срабатывает для кандидатов; срабатывает для текущего состояния, см. выше |
| У сервиса нет владельца | Не срабатывает: три команды, дежурство выделенное |
| Нет наблюдаемости, трассировки, автовыката | Не срабатывает: всё построено |
| Сервисов больше, чем команд | Не срабатывает: три и три |
Рассмотренные варианты#
| Ось | Что записывать | Оставить как есть | A — выделить ленту третьим | B — перенести линию по языку |
|---|---|---|---|---|
| Связность изменений | Доля задач за период в одной границе | 68 % [история 6 мес] |
74 % — часть сквозных задач остаётся [история 6 мес] |
89 % [история 6 мес] |
| Владение данными | Сколько сущностей с несколькими писателями | 1 — users.last_seen [схема] |
1 — та же [схема] |
0 — подписки уезжают к ленте вместе с записью [схема] |
| Языковая чистота | Сколько терминов с двойным значением внутри границы | 2 — «лента» и «пост» [орг] |
1 — «пост» [орг] |
0 [орг] |
| Организационное соответствие | Команды и границы один к одному | Нет: «лента» владеет профилями и подписками [орг] |
Частично [орг] |
Полное [орг] |
| Глубина синхронной цепочки | Сетевых вызовов в основном сценарии | 1, но 347 тыс./с [расчёт W1] |
1, редкий — досбор 1 % [расчёт] |
1, редкий [расчёт] |
| Стоимость перехода | Шагов миграции и что ломается | 0 | 2 шага, контракт web↔лента |
4 шага: перенос подписок, strangler fig на API [схема] |
| Обратимость | Что переделывать при ложной границе | — | Недели | Недели плюс миграция графа подписок |
Ось транзакционных разрывов вычеркнута. Правило мастер-шаблона v4.0: ось, по которой варианты не различаются, убирается, а не заполняется нулями. Здесь атомарность не требуется нигде, ось одинакова у всех трёх кандидатов и создаёт только видимость тщательности. В E-01 эта же ось была решающей.
Решение#
Вариант B: перенести линию по языку.
| Сервис | Что внутри | Владелец |
|---|---|---|
лента |
Материализованные ленты, раздача, граф подписок, чтение ленты | Команда «лента» |
посты |
Создание, хранение, удаление постов, медиа | Команда «посты» |
| Шлюз | Маршрутизация, аутентификация, профили | Платформа |
Граф подписок переезжает к ленте: он нужен раздаче на каждую публикацию и не нужен больше никому. Профили уходят к платформе вместе со шлюзом. web как сервис растворяется.
Переход — четырьмя шагами через strangler fig (ARCH-13), с переносом подписок последним.
Обоснование#
Решила связность изменений — единственная ось, которую нельзя было оценить на глаз. 89 % против 74 % и 68 %: разница не в пределах погрешности, и она посчитана по 1 400 задачам, а не выведена из соображений.
Вариант «оставить как есть» дисквалифицирован по двум писателям у одной сущности. Строго говоря, users.last_seen — мелочь, которую можно починить и не трогая границ; но вместе с 68 % связности и разрезом по слоям он складывается в одну картину: линия проведена не там, и её надо двигать, а не латать.
Вариант A дешевле на два шага миграции и хуже по трём осям. Разница в стоимости перехода — недели; разница в связности изменений — постоянная величина, действующая на каждой задаче. При выборе между разовой ценой и постоянной выбрана разовая.
Самая дорогая будущая граница#
Между лентой и постами. Она остаётся и после переноса: каждое открытие ленты требует досбора постов по идентификаторам. Сейчас это дёшево, потому что материализация свела обращения к одному проценту открытий (ADR-0001). Но если ранжирование ленты, стоящее в роадмапе, потребует содержимого постов, а не только идентификаторов, — эта граница станет узким местом снова, и денормализация постов в ленту сделает удаление поста дорогим. Записано здесь, потому что через год связь между ранжированием и стоимостью удаления будет неочевидной.
Последствия#
- Миграция графа подписок — самый рискованный шаг. 3 млрд рёбер, 48 ГБ; переносить придётся с двойной записью и сверкой, а не переключением.
- До завершения перехода живут два пути чтения ленты — старый и новый. Срок сосуществования надо ограничить датой, иначе он станет постоянным.
- Команда «платформа» получает профили, то есть продуктовый код. Это отклонение от её роли, и его стоит пересмотреть, когда появится четвёртая команда.
- Три сервиса на три команды — граница, за которой дробление придётся останавливать: следующий сервис окажется без владельца.
users.last_seenчинится по дороге: писать будет только шлюз.
Условия пересмотра#
- Доля задач внутри одной границы снова упала ниже 80 % — теперь это измеримо и должно попасть в регулярный отчёт.
- Появилась четвёртая команда.
- Ранжирование потребовало содержимого постов в ленте — см. «самая дорогая будущая граница».
- Профиль нагрузки частей разошёлся так, что мешает масштабировать их порознь.
- Атомарность потребовалась там, где её сейчас нет.
Связанные ADR#
Зависит от: ADR-0001. Зависимые: ADR-0003 (раздача поста идёт через границу «посты → лента»).