Перейти к содержанию

Антипаттерны#

Собрано из разделов «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.