12 основных правил для надежных API на Node.js
Практические правила надежности API Node.js: таймауты, идемпотентность, транзакции, аннулирование кэша, логирование и задержки в зависимостях — перед страницей в 3 часа ночи.
Некоторые уроки надежности усваиваются только после сбоев. Двенадцать приведенных ниже принципов встречаются достаточно часто в различных сервисах на Node.js, поэтому их четкое изложение дешевле, чем повторное открытие их в условиях срочных запросов. Рассматривайте их как стандартные правила при проектировании обработчиков, а не как дополнительные улучшения после запуска.
1. Запросы, которые можно повторить, будут отправлены заново.
Не «возможно». У клиентов истекает время ожидания, соединения прерываются в процессе ответа, а пользователи постоянно нажимают кнопку отправки. Балансировщики нагрузки и мобильные сети также добавляют дублирующиеся передачи данных. Любой обработчик, создающий или изменяющий состояние, должен иметь четкий ответ на вопрос «что, если такой же пакет данных придет снова», потому что со временем это становится не изолированным случаем, а неизбежностью. Проектируйте как успешный сценарий, так и сценарий дублирования одновременно.
2. Установите время ожидания для каждого внешнего вызова.
Запросы к базе данных, внешние HTTP-соединения, платежные шлюзы — если не установить крайний срок, проблемный клиент может держать соединение открытым бесконечно. Простая функция fetch(url) не способна «терпеливо ждать некоторое время»; она ведет к бесконечному ожиданию, что позволяет одной медленной зависимости заблокировать всю работу выше по цепочке. Лучше использовать стандартные настройки библиотек, которые устанавливают лимиты, и при сбоях выводить явные сообщения об ошибках или автоматически перепробовать действия, вместо того чтобы происходило бесшумное зависание.
3. Проверки в браузере — это элемент пользовательского интерфейса, а не защитный барьер.
Злоумышленники могут обойти вашу SPA и напрямую обращаться к API с произвольными данными в формате JSON с помощью curl, Postman или скомпрометированного скрипта. Важные правила — корректный формат электронной почты, ненегативная цена, обязательные поля — должны снова проверяться на сервере с той же строгостью, что и в отношении враждебного трафика. Проверка на стороне клиента помогает только сотрудничающим пользователям, которые и так не нарушили бы условия использования.
4. Кэш без механизма аннулирования представляет собой отложенную ошибку.
Использование кэширования создает вторую копию данных, и эти копии могут отклоняться друг от друга. При внедрении кэша необходимо определить, какое действие — запись данных, сигнал webhook или истечение времени жизни — приведет к его очистке, и задокументировать это решение рядом с ключом кэша. Опора на принцип «он автоматически истекает со временем» часто означает «дольше, чем клиенты готовы терпеть неверные суммы», особенно когда время жизни кэша указано в часах ради «повышения производительности».
5. Для совместных записей требуется транзакция, а не оптимизм.
await update1(); await update2(); кажется атомарным действием, но на самом деле им не является. Между этими вызовами сбой, перебои или отклонение могут привести к тому, что будет выполнена только одна из записей, в результате чего уровень запасов и счета начинают отклоняться друг от друга. Только настоящая транзакция базы данных или явная схема обработки с компенсирующими действиями позволяет гарантировать корректность операций.
6. Инструмент для инцидентов, которые вы ещё не видели.
Когда что-то ломается, уже слишком поздно добавлять записи в журнал, которые могли бы это объяснить. Структурированные журналы, идентификаторы корреляции, передающиеся между сервисами, и достаточный контекст для воссоздания запроса до возникновения сбоя эффективнее самых изощрённых сеансов отладки после инцидента. Записи журнала, которые можно безопасно сохранять, результаты работы системы и задержки в зависимостях при её нормальной работе — всё это полезно.
7. Память процесса — это кэш, а не система хранения данных.
Значение, существующее только в процессе Node, исчезает при перезагрузке, остаётся невидимым для других экземпляров процесса и не является постоянным. Флаги функций, хранящиеся в ОЗУ, счётчики или «временные» структуры данных Map имеют такую же судьбу. Если данные должны сохраняться после развертывания, сбоя или при балансировке нагрузки без использования sticky sessions, их следует сохранять в базе данных или другом надёжном хранилище, а не в случайной области памяти.
8. Планируйте на случай, когда каждый сетевой запрос в конечном итоге потерпит неудачу.
Полезный вопрос — не «работает ли это обычно?», а «что происходит в редких случаях сбоя?» К таким ситуациям относятся таймауты, частичные ответы и всплески ошибок 5xx. Если честный ответ — «неясно», устраните этот риск с помощью безопасных попыток повтора, механизмов прерывания работы или режимов сниженной производительности до того, как проблема станет очевидной в производственных условиях.
9. Очередь переназначает задачи; она не удаляет их.
Добавление задачи в очередь создает иллюзию её выполнения. Рабочий процесс всё равно будет выполнять ту же работу позже, с теми же возможностями сбоя, просто в другое время. Если рабочие процессы работают медленнее, объём незавершённых задач растёт, хотя API остаётся отзывчивым — это лучше, чем блокировка запросов, но работа остаётся такой же трудоёмкой. Так же тщательно настраивайте размер потребителей задач и механизмы обработки некорректных сообщений, как и уровень HTTP-сервиса.
10. Давайте названия так, чтобы их понимали читатели через шесть месяцев, а не ради сегодняшней удобства.
data, temp и handleStuff остаются точными только до тех пор, пока ещё три элемента также не будут отнесены к категории «data». Названия, связанные с бизнес-идеей, остаются понятными даже при отсутствии контекста — включая будущего пользователя на незнакомом сервисе. Лучше использовать существительные, описывающие сферу деятельности, вместо глаголов, описывающих первые детали реализации.
11. Создатели будут вызываться дважды.
Нестабильные сети, агрессивные клиенты для повторных попыток, нетерпеливые действия пользователя — любой из этих факторов может привести к проблемам. Запросы типа POST для создания ресурсов требуют корректной обработки дубликатов, обычно с использованием ключа идемпотентности, хранящегося вместе с созданным ресурсом; иначе могут возникнуть двойные списания средств и дубликаты заказов из-за различий в времени выполнения операций. При ситуации «уже создано» необходимо возвращать первоначальный результат, а не какой-либо новый побочный эффект.
12. Задержка ответа от конечной точки зависит от самой медленной зависимости.
Даже элегантный механизм обработки запросов, выполняющий запросы к незиндексированным источникам или удаленным API без ограничения по времени, все равно работает медленно. Анализ собственного кода при игнорировании поведения вызываемых функций приводит к неверным выводам. Необходимо измерять показатели p95 и p99 по всей структуре вызовов, а затем устранять проблемы с индексами, группировкой операций и ограничениями по времени там, где действительно теряется время.
Ни один из этих советов не является особенно гениальным, и в этом-то и суть: документация, созданная поздно ночью, редко бывает хорошей. Обычно это один из двенадцати типов такой документации, которая остается без внимания до тех пор, пока проблема не перестает казаться срочной. Необходимо внедрять простые меры защиты, пока сервис еще достаточно мал, чтобы его можно было изменить.