Философия границ: линзы · часть 2
Продолжаем перебирать оси, по которым режут системы. В первой части были внутренние линзы: язык, команды, данные, скорость изменения, отказы. Здесь линзы, которые приходят извне (регуляторы, нагрузка, безопасность) и изнутри процесса (деплой, план замены). Финал серии: convergence, методика оценки шва числом совпавших линз, и практикум на трёх кейсах.
Линза 6 · Compliance
Особенность этой линзы: она не советует, а обязывает, и часто требует границу там, где все остальные линзы молчат.
- PCI-DSS устроен экономически коварно: аудиту подлежит всё, что касается карточных данных. Если номер карты проходит через монолит, в scope аудита весь монолит: каждый его сервер, каждый разработчик с доступом, каждая зависимость. Поэтому карточные данные изолируют в минимальный сервис (или вообще отдают провайдеру с токенизацией): граница здесь уменьшает площадь аудита и его стоимость на порядки.
- GDPR со своим правом на забвение задаёт неприятный вопрос: «удали все данные этого человека». Если PII разбросан по десяти сервисам, пяти бэкапам и трём аналитическим выгрузкам, право на забвение стоит как отдельный проект. Практика: персональные данные живут в одном модуле с узким API, остальная система оперирует анонимными id, и «забыть человека» означает удалить записи в одном месте (или уничтожить ключ шифрования его данных, приём crypto-shredding).
- Та же логика у любых регуляторных периметров: медицинские данные, финансовая отчётность, локализация данных по странам.
Вывод линзы: scope регулятора это граница, навязанная снаружи, и чем раньше она проведена, тем дешевле жизнь. Дешевле всего она в день старта, дороже всего в день первого аудита.
Линза 7 · Профиль нагрузки
Вопрос линзы: какие у кусков системы требования к ресурсам и задержке? Если они конфликтуют, куски плохо живут в одном процессе.
Классический конфликт: REST API с SLO «p99 меньше 100 мс» и ночной batch, перемалывающий гигабайты. В одном процессе batch отъедает CPU, раздувает heap и устраивает GC-паузы ровно поперёк latency-bucket-ов API (механика пауз знакома из урока про перформанс Node). В одной БД batch выжирает пул соединений и прогоняет рабочий набор кеша. Сосед по процессу всегда делит судьбу.
Язык этой линзы дала SRE-практика: SLO. Эвристика формулируется через него: разные SLO на одном дереве кода это аргумент за разрез. Куску с четырьмя девятками и куску «доедет за ночь» нужны разные стратегии деплоя, разное масштабирование (API скейлится репликами, batch воркерами очереди) и разные бюджеты риска.
Тот же критерий на уровне данных разводит OLTP и OLAP: транзакционная нагрузка и аналитические сканы конкурируют за кеш и блокировки, поэтому аналитика уезжает в реплику или колоночное хранилище (подробно в уроке про архитектуру данных).
Линза 8 · Доверие
Вопрос линзы: где меняется уровень доверия к данным и вызывающему? Граница доверия это термин из threat modeling: рисуешь поток данных и отмечаешь точки, где вызывающему верить нельзя.
Типовые ступени доверия в продукте: анонимный интернет, залогиненный пользователь, пользователь другого tenant-а, внутренний сервис, админ. Каждая пара соседних ступеней это граница, на которой обязаны жить authn, authz, валидация и санитизация. А раз эти проверки всё равно обязаны существовать в конкретном месте, это место естественный кандидат на архитектурный шов: проверки доверия любят жить на явной границе (gateway, BFF, middleware-периметр), а не быть размазанными if-ами по всем хэндлерам.
Анти-паттерн линзы: admin-функции в том же сервисе и том же роутинге,
что и пользовательские, отличающиеся одним if (user.isAdmin). Проверку
легко забыть в новом эндпоинте, а компрометация пользовательской
поверхности сразу отдаёт админскую. Явный шов (отдельный поддомен,
отдельный процесс, отдельная сеть) превращает «забыли if» из инцидента
в невозможность. Аналогично с мульти-tenancy: изоляция арендаторов
это граница доверия, и её стоит держать как можно ниже (RLS из
урока про прагматику
ровно про это).
Глубже про модели авторизации в уроке про auth, про threat modeling целиком в уроке про моделирование угроз.
Линза 9 · Каденция деплоя
Вопрос линзы самый бинарный из всех: можно ли выкатить A без B?
- Если нет и никогда, перед тобой один модуль. Сетевая граница между кусками, которые релизятся только вместе, добавляет бойлерплейта (две пайплайны, согласование версий), не добавляя свободы. Это диагностический признак distributed monolith из первой части: формально сервисы, фактически один релизный пакет.
- Если да, то независимый деплой обязан работать в обе стороны: старая A с новой B и новая A со старой B. Из этого следуют backward-compatible контракты (поля только добавляем, как в уроке про REST и про protobuf), feature flags для включения фич отдельно от выкатки кода и миграции БД в стиле expand/contract (сначала добавить новое, переключить код, потом убрать старое).
Сэм Ньюман называет independent deployability главным свойством, ради которого вообще стоит резать на сервисы: если его нет, остальные обещания микросервисов не исполняются. Отсюда рабочий тест для любого предложенного шва: опиши сценарий, в котором одна сторона релизится десять раз за день, а вторая не релизится месяц. Если сценарий невообразим, шов декоративный.
Эта линза часто конфликтует с линзой данных: выкатить независимо можно только то, что не делит схему БД. Конфликт решается в пользу данных: сначала развести владение данными, потом обещать независимый деплой.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.