grig-teo:~$
light
← ко всем статьям

Как проектировать бэкенд, который выживает

Сначала состояния, потом экраны, повторяемая запись, байты вне API и расчёт на одну машину.

Сначала состояния, потом экраны

Большинство бэкенд-проблем, которые я унаследовал, были не про производительность. Они были про разногласия о том, чем запись вообще может быть.

Прежде чем писать фичу, я теперь выписываю состояния, через которые проходит запись, и переходы между ними. В Touchstone тестовая задача проходит через offered, claimed, submitted, reviewed, paid. Когда список написан первым, API становится очевидным, а сложные случаи видны: что будет, если тестировщик бросит взятую задачу, если двое отправят одну и ту же задачу, если платёж упадёт после одобрения.

Экраны это следствие списка. А схема, спроектированная от экранов, обрастает nullable-колонками, в которых закодированы недостающие решения.

Запись должна быть повторяемой

Джобы загрузки падают и перезапускаются. Если ретрай дублирует данные, все числа дальше по цепочке неверны, и неделями никто этого не замечает.

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

Это стоит одного уникального индекса и пары строк кода. Зато снимает класс багов, на которые уходит день поиска и которые подрывают доверие к данным.

Уберите байты из API

Файлы-доказательства это вторая повторяющаяся ошибка. Отчёт с записью экрана не должен загружаться через сервер приложения, и хранить его в базе тоже не надо.

В Touchstone агент запрашивает presigned-загрузку, кладёт файл напрямую в объектное хранилище и отправляет ссылку. API не трогает тело файла, воркер читает его из хранилища, когда нужно проанализировать, а запись отдаётся по подписанной URL, у которой есть срок жизни.

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

Исходите из одной машины

Моя платформа живёт на одном VPS: Next.js, NestJS, Postgres, MinIO, кэш на nginx и локальная модель для фолбэков. Это ограничение полезно. Когда память конечна, находишь запрос, который материализует миллион строк, а когда конечен диск, замечаешь, что логи никто никогда не чистил.

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

Деплой должен быть скучным

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

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

Как проектировать бэкенд, который выживает | grig-teo