Антипаттерны#
Собрано из разделов «Trade-offs и режимы отказа» всех карточек. Формат: симптом → почему это ломается → что вместо.
Список отвечает на вопрос «что обычно идёт не так», поэтому годится как второй чек-лист при design review — после основного.
Оглавление#
Процесс · API · Данные · Кэш · Асинхронность · Распределённость · Надёжность · Безопасность · Эксплуатация · Производительность и стоимость
Процесс проектирования#
Компоненты вперёд требований. Схема из квадратов рисуется до разговора о том, что система должна делать и по каким числам это проверяется. Дизайн выглядит убедительно и не проверяем. → Сначала W0 и W1, потом схема.
Нефункциональные требования без чисел. «Быстро, надёжно, масштабируемо» не позволяет принять ни одного решения и не позволяет узнать, достигнута ли цель. → Число и способ измерения на каждое требование.
Оценка по среднему. Ёмкость посчитана по среднему трафику, система живёт в пике. → Коэффициент пика 3–10, для событийных нагрузок больше.
Решение из чужого блога. Архитектура компании с другими числами перенесена целиком. Инженерные блоги описывают итог, а не путь, и молчат о промежуточных решениях, работавших годами. → Сначала контекст и ограничения, потом решение (CASE-13).
Отсутствие «вне рамок». Список исключений не зафиксирован, объём растёт молча. → Явный список того, чего не делаем.
API и контракты#
200 OK с ошибкой в теле. Ломает всё, что между клиентом и сервером: балансировщик считает ответ успешным, кэш кэширует, мониторинг зелёный, клиент не повторяет. → Осмысленные коды (NET-05).
Неидемпотентная небезопасная операция. Клиент получил таймаут и не знает, выполнилась ли операция. Любой повтор — риск двойного эффекта. → Ключ идемпотентности (NET-16).
API от таблиц БД. Схема хранения протекает в публичный контракт, миграция становится ломающим изменением. → Контракт от сценариев использования.
Пагинация по offset на растущей коллекции. Задержка растёт с номером страницы, вставки сдвигают выдачу, пользователь видит дубли и пропуски. → Курсорная пагинация (NET-14).
Клиент падает на неизвестном поле. Добавить поле в ответ становится ломающим изменением, эволюция API останавливается. → Игнорировать неизвестные поля — правило с первого дня (NET-17).
Сериализация доменной модели в ответ. Внутренние поля утекают наружу, а переименование поля ломает клиентов. → Явное преобразование в ответ (SEC-06).
Массовый эндпоинт без ограничения размера. Один запрос исчерпывает память сервиса. → Лимиты по числу элементов и объёму тела (NET-15).
Данные#
Выбор БД до формулировки паттернов доступа. Хранилище выбрано по привычке, а нужный запрос оказывается невыразимым или неприемлемо медленным. → Список запросов с частотой и требованиями, потом выбор (DATA-03).
«Транзакция есть — значит гонок нет». Уровень изоляции по умолчанию допускает потерянные обновления и write skew (перекос записи). Баги тихие и воспроизводятся раз в тысячу операций. → Разобрать конкретные аномалии (DATA-08).
Индексы «на всякий случай». Каждый индекс — дополнительная запись при каждом изменении; замедление записи объясняют «ростом нагрузки». → Индекс под конкретный частый и селективный запрос (DATA-05).
Ключ шардирования, выбранный до понимания запросов. Запросы без этого ключа превращаются в обход всех шардов; переиграть почти невозможно. → Ключ выводится из паттернов доступа (DATA-10).
Раннее шардирование. Сложность распределённых данных получена до того, как исчерпаны вертикальный рост, кэш, реплики и асинхронность. → Порядок из W5.
Аналитика на БД прода. Тяжёлый запрос вытесняет данные из буферного пула и удерживает старые версии строк, деградируя пользовательские операции. → Разделение контуров (DATA-15).
Деньги в числе с плавающей точкой. Ошибки округления накапливаются и обнаруживаются сверкой через месяцы. → Целое в минимальных единицах или десятичный тип (DATA-04).
Будущее событие сохранено в UTC. Изменение правил часового пояса сдвигает встречу. → Локальное время плюс идентификатор зоны.
Отключённые ограничения БД «ради производительности». Внешние ключи и уникальные индексы — самый дешёвый эшелон целостности; без них расхождения появляются тихо. → Оставить ограничения, оптимизировать иначе (DATA-16).
Миграция одним изменением. Во время выкатки работают обе версии кода; удаление или переименование колонки ломает старую. → expand → backfill → переключение → contract (DATA-17).
Забытый шаг contract. Двойная запись и старая колонка остаются навсегда, система живёт в переходном состоянии. → Планировать удаление сразу, с датой.
Кэш#
Кэш поверх неоптимального запроса. Проблема скрыта до первого массового промаха, после которого БД получает стократную нагрузку. → Сначала оптимизировать запрос (PERF-05).
Обновление кэша после записи. Два процесса записывают в кэш в порядке, обратном порядку записи в БД. → Удаление ключа, а лучше версионирование ключа (CACHE-03).
Одинаковый TTL у всех ключей. Ключи, созданные одновременно, истекают одновременно — БД получает залп. → Jitter (случайный разброс) ±10–20 % к TTL.
Отсутствие защиты от stampede (лавины запросов) на популярном ключе. Тысяча параллельных запросов идёт в БД за одним значением. → Блокировка на пересчёт, request coalescing (объединение запросов), probabilistic early expiration (вероятностное раннее обновление) (CACHE-04).
Нет negative caching (кэширования отсутствия). Запросы за несуществующими данными каждый раз доходят до БД — простой способ её перегрузить. → Кэшировать сам факт отсутствия с коротким TTL.
Инвалидация из кода приложения вне транзакции. Транзакция откатилась, а в кэше уже новое значение — данные, которых нет в БД. → Инвалидация по журналу изменений БД.
Асинхронность#
Двойная запись: в БД и в брокер. Публикация упала после коммита — событие потеряно навсегда. Откатилась транзакция после публикации — подписчики узнали о том, чего не было. → Transactional outbox или CDC (change data capture, MSG-07).
Событие в повелительном наклонении. ОтправитьУведомление вместо ЗаказОплачен возвращает связность: издатель снова знает, кто и что должен сделать. → Событие — факт в прошедшем времени (MSG-01).
Потребитель без идемпотентности. Доставка at-least-once — норма, значит дубликаты гарантированы. → Отметка об обработке в одной транзакции с эффектом (MSG-06).
Бесконечный повтор сбойного сообщения. Poison message («ядовитое» сообщение) в упорядоченном логе останавливает партицию навсегда. → Классификация ошибок, ограниченное число попыток, DLQ (dead letter queue, очередь недоставленных сообщений) (MSG-08).
DLQ без мониторинга. Очередь молча накапливает потерянные бизнес-операции — самый распространённый способ «потерять» платежи. → Алерт на любое ненулевое значение для критичных потоков.
Неограниченная очередь в памяти. Разница между темпом производителя и потребителя копится до исчерпания памяти. → Ограниченные буферы и backpressure (MSG-09).
Обещание exactly-once. Свойство приписывается брокеру, хотя относится ко всей цепочке. → At-least-once плюс дедупликация.
Распределённость#
Распределённая транзакция там, где нужна одна. Сущности, меняющиеся только вместе, разнесены по сервисам — признак неверной границы. → Пересмотреть границы, а не строить saga (ARCH-02).
Saga без учёта отсутствия изоляции. Промежуточные состояния видны другим, и на них принимаются решения. → Явные статусы и семантические блокировки (DIST-08).
Компенсация, которая не может упасть. Компенсирующее действие считается безотказным, хотя это обычный сетевой вызов. → Идемпотентная повторяемая компенсация.
Блокировка на TTL без fencing token. Держатель «проснулся» после паузы и продолжает работу, считая себя владельцем. → Fencing token, отвергающий устаревшие операции (DIST-07).
Разрешение конфликтов по метке времени. Часы узлов расходятся; «последняя» запись определяется неверно, данные теряются молча. → Логические версии, разрешение в приложении или CRDT (conflict-free replicated data types) (DIST-10).
Случайный UUID как первичный ключ большой таблицы. Вставки идут в случайные места индекса, страницы фрагментируются, запись деградирует. → Время-упорядоченные идентификаторы (DIST-09).
Корректность, опирающаяся только на детектор отказов. Ложное объявление лидера мёртвым даёт двух лидеров. → Кворум и fencing token как страховка (DIST-06).
Надёжность#
Сетевой вызов без таймаута. Один зависший вызов занимает поток до бесконечности; сотня таких исчерпывает пул, и отказ зависимости становится отказом сервиса. → Таймаут на каждом вызове, из бюджета задержки (REL-01).
Повторы без jitter и без бюджета. Клиенты синхронизируются и добивают восстанавливающуюся систему. → Exponential backoff с full jitter плюс ограничение доли повторов.
Повторы на каждом уровне. Три уровня по три попытки дают 27 запросов вместо одного. → Повторяет один слой.
Health check, зависящий от общей БД. При кратковременном сбое БД все экземпляры объявляют себя нездоровыми, и система падает полностью вместо частичной деградации. → Liveness проверяет только процесс (REL-05).
Отсутствие приоритетов при load shedding (сбросе нагрузки). Отказ бьёт случайно и с равной вероятностью попадает по оформлению заказа и по рекомендациям. → Классы приоритета (REL-03).
Резервный регион, на который ни разу не переключались. Конфигурация разъехалась, квоты не выданы, образы устарели — узнаётся в момент аварии. → Регулярные учения (REL-10).
Бэкап без проверки восстановлением. Файлы есть, восстановиться нельзя — обнаруживается в худший момент. → Автоматический прогон восстановления (REL-09).
Механики устойчивости без observability (наблюдаемости). Circuit breaker, деградация и повторы есть, но не видно, срабатывают ли они. → Состояние каждого механизма как метрика.
Безопасность#
Проверка прав только по факту аутентификации. Токен валиден — доступ дан; смена идентификатора в адресе открывает чужие данные. Самая частая уязвимость. → Авторизация на уровне объекта в слое доступа к данным (SEC-04).
Фильтрация вместо разделения данных и кода. Чёрные списки и экранирование «опасных» символов обходятся; защита строится не там. → Параметризованные запросы, контекстное экранирование при выводе, белые списки адресов (SEC-07).
Доверие к полю алгоритма в токене. Проверяющая сторона берёт алгоритм из самого токена. → Ожидаемый алгоритм задаётся конфигурацией (SEC-02).
Долгоживущий токен без возможности отзыва. Утечка действует до истечения срока, а он год. → Короткий access token плюс отзываемый refresh token.
Секреты в репозитории или в переменных образа. Секрет, попавший в историю системы контроля версий, считается скомпрометированным. → Хранилище секретов и ротация (SEC-08).
Персональные данные в логах и трассировках. Утечка через систему, которую не считают хранилищем. → Явные правила того, что не логируется (SEC-12).
Плавающие версии зависимостей. В прод попадает то, чего никто не проверял; компрометация пакета доходит автоматически. → Lock-файл, проверка уязвимостей, внутреннее зеркало (SEC-10).
CORS как защита сервера. Разрешения ослабляются в расчёте на то, что браузер что-то охраняет; curl про CORS не знает. → Авторизация на сервере независимо от CORS (NET-19).
Эксплуатация#
Алерт на каждую аномалию. Дежурного будят из-за всплеска, который прошёл сам; через месяц уведомления не читают, и настоящая авария теряется. → Алерты по симптомам и по расходу бюджета ошибок (OPS-10).
Метка с идентификатором пользователя в метрике. Кардинальность умножается на миллионы, система мониторинга падает. → Метки с ограниченным набором значений; детали в логи и трассировку (OPS-08).
Логи без идентификатора запроса. Распределённый запрос не собирается в единую картину. → Сквозной контекст во всех трёх сигналах (OPS-07).
Нестабильные тесты (flaky tests). После нескольких ложных падений красную сборку перезапускают не глядя — конвейер перестаёт что-либо гарантировать. → Чинить или удалять (OPS-12).
Ручные правки поверх кода инфраструктуры. Следующее применение внезапно откатывает чужие изменения. → Только через код, регулярная проверка расхождения (OPS-06).
Пересборка артефакта для прода. Тестировали не то, что выкатили. → Одна сборка на все окружения, различия только в конфигурации.
Задача по расписанию без контроля запуска. Пропущенный запуск не вызывает ошибок и потому невидим. → Dead man's switch («сторожевой» мониторинг) по факту завершения (ARCH-08).
Kubernetes ради Kubernetes. Три сервиса и редкие выкатки не окупают сложности кластера. → Соразмерять инструмент задаче (OPS-05).
Производительность и стоимость#
Оптимизация без измерения. Недели уходят на участок, дающий два процента. → Профилирование до оптимизации (PERF-09).
Среднее время ответа как показатель. Среднее 80 мс совместимо с тем, что каждый двадцатый запрос идёт две секунды. → Перцентили по сценариям (PERF-02).
Усреднение перцентилей между узлами. Среднее из p99 десяти серверов — не p99 системы, и цифра на дашборде не значит ничего. → Агрегировать гистограммы.
N+1 запросов. Сто запросов вместо одного; на тестовых данных незаметно. → Счётчик запросов на сценарий как метрика (PERF-06).
Огромный пул соединений. Лишние соединения не ускоряют БД, а создают конкуренцию и ухудшают время ответа для всех. → Размер подбирается измерением, обычно десятки.
Планирование ёмкости под 100 % загрузки. Задержка растёт нелинейно и деградация наступает задолго до исчерпания ресурсов. → Целевая загрузка 60–70 % (PERF-07).
Нагрузочный тест с падающим темпом. Генератор ждёт ответа и сам снижает нагрузку — измеренная задержка занижена как раз тогда, когда система работает хуже всего. → Постоянный темп независимо от ответов (PERF-08).
Автомасштабирование без верхнего предела. Всплеск трафика или цикл повторов превращается в счёт на порядок больше. → Верхний предел числа экземпляров (OPS-11).
Стоимость считается только по инфраструктуре. Observability (наблюдаемость), исходящий трафик и труд на эксплуатацию часто больше счёта за серверы. → TCO (полная стоимость владения) и unit-экономика (PERF-11).
Числа и матрицы выбора — в 90-cheatsheets.md. Чек-лист ревизии дизайна — в 00-workflow.md, шаг W11.