12 основних правил для надійних API Node.js
Практичні правила надійності API Node.js: таймаути, ідемпотентність, транзакції, анулювання кешу, логування та затримки у залежностях — перед сторінкою о 3 годині ранку.
Деякі уроки надійності закарбовуються лише після збою. Дванадцять наведених нижче проблем трапляються достатньо часто у різних сервісах на Node.js, тож їх чітке формулювання дешевше, ніж знову шукати рішення під тиском негайних дій. Вважайте їх стандартами під час проектування обробників, а не додатковими опціями після запуску.
1. Запити, які можна повторно надіслати, будуть пересилані знову.
Не „можливо“, а обов’язково. Клієнти вичерпують тайм-аут, з’єднання розриваються під час відповіді, а користувачі постійно натискають кнопку надсилання. Балансувальники навантаження та мобільні мережі створюють ще більше дублікатів. Будь-який обробник, який створює чи змінює стан, потребує чіткої відповіді на питання „що робити, якщо цей ідентичний пакет знову надійде“, адже за достатньо довгий проміжок часу це вже не окремий випадок — це неминуче. Проектуйте як успішний, так і сценарій дублікації одночасно.
2. Встановіть тайм-аут для кожного зовнішнього виклику.
Запити до баз даних, HTTP від сторонніх сервісів, платіжні шлюзи — якщо ви не встановите термін, проблемний компонент може залишати сокет відкритим незмінно. Простий fetch(url) не є „терплячим на деякий час“; це необмежене очікування, а необмежене очікування дозволяє одному повільному залежному компоненту заморозити всю систему. Краще використовувати стандартні налаштування бібліотек, які встановлюють обмеження, та при помилках активувати повторні спроби чи показувати помилки, видимі користувачеві, замість тихого зависання.
3. Перевірки в браузері — це частина користувацького інтерфейсу, а не захисний бар’єр.
Користувачі можуть проігнорувати ваш SPA та звернутися до API з довільним JSON за допомогою curl, Postman чи компрометованого скрипту. Правила, які мають значення — правильно сформована електронна пошта, ненегативна ціна, обов’язкові поля — мають бути знову перевірені на сервері з такою ж суворістю, як і щодо ворожого трафіку. Перевірка з боку клієнта допомагає лише співпрацюючим користувачам, які все одно не порушили б умови.
4. Кеш без механізму скасування є відкладеною помилкою.
Кешування створює другу копію даних, і ці копії починають відрізнятися. Коли ви впроваджуєте кеш, необхідно вирішити, яка операція запису, webhook чи подія TTL буде його очищувати, та задокументувати це рішення поруч із ключем кешу. Покладання на те, що «він закінчиться через певний час», часто означає «довше, ніж клієнти зможуть терпіти неправильний баланс», особливо коли TTL вимірюється годинами заради «продуктивності».
5. Складені операції запису потребують транзакції, а не оптимізму.
await update1(); await update2(); здається атомарною, але насправді це не так. Між цими операціями збої, перерви чи відмови можуть призвести до того, що буде застосовано лише одну з операцій, що й спричиняє розбіжності між інвентарем та рахунками. Лише справжня базова даних транзакція або чітко визначена сага з компенсуючими діями може гарантувати правильність результату.
6. Інструмент для інциденту, якого ви ще не бачили.
Коли щось ламається, вже занадто пізно додавати рядок журналу, який міг би це пояснити. Структуровані журнали, ідентифікатори кореляції, які передаються між сервісами, та достатньо контексту для відтворення запиту — надісланого до виникнення помилки — краще за найрозумнішу сесію відладки після інциденту. Дані журналу, які можна безпечно зберігати, результати та затримки у залежностях під час нормальної роботи системи.
7. Пам’ять процесу — це кеш, а не система зберігання даних.
Значення, яке існує лише в процесі Node, зникає під час перезапуску, є невидимим для інших екземплярів та ніколи не є стабільним. Флаги функцій, які зберігаються в RAM, лічильники чи „тимчасові“ мапи — усі вони мають таку саму долю. Якщо дані мають вижити після розгортання, збою чи балансування навантаження без sticky-session, зберігайте їх у базі даних чи іншому стабільному сховищі, а не в випадковому слоті RAM.
8. Плануйте на випадок, коли кожен мережевий запит зрештою зазнає невдачі.
Корисне запитання — не „Чи працює це зазвичай?“ а „Що відбувається у рідкісних випадках невдачі?“ Таймаути, часткові відповіді та сплески коду 5xx також мають значення. Якщо чесна відповідь — „незрозуміло“, усуньте цю прогалину за допомогою безпечних спроб повторення, механізмів переривання обробки чи режимів зі зниженою продуктивністю, перш ніж це виявиться у продакшені.
9. Очередь лише переносить завдання; вона їх не видаляє.
Додавання завдання до очереді здається ознакою його виконання. Роботник все одно виконає ту саму роботу пізніше, з тими самими можливостями збоїв, просто в інший час. Якщо роботники працюють повільніше, кількість невиконаних завдань зростає, хоча API залишається реактивним — це краще, ніж блокування запитів, але робота залишається складною. Налаштовуйте об’єми споживачів та шляхи обробки невдалих повідомлень так само ретельно, як і рівень HTTP.
10. Давайте назви для читачів через шість місяців, а не для сьогоднішнього спрощення.
data, temp та handleStuff залишаються точними лише доти, доки ще три поняття також не будуть класифіковані як “data”. Назви, пов’язані з бізнес-ідеєю, залишаються зрозумілими навіть для користувачів без контексту — включаючи ваше майбутнє “я” на незнайомому сервісі. Краще використовувати іменники, що позначають сутність, замість механічних дієслів, які описують перші деталі реалізації.
11. Творців буде викликано двічі.
Нестабільні мережі, агресивні клієнти для повторних спроб, нетерплячі дубльовані дії — будь-що з цього. Операції створення типу POST потребують елегантного оброблення дублікатів, зазвичай через ключ ідемпотентності, який зберігається разом із результатуючим ресурсом; інакше подвійні оплати та дубльовані замовлення стануть проблемою, пов’язаною із часом виконання. Функція “вже створено” має повертати початковий результат, а не нові побічні ефекти.
12. Затримка кінцевої точки залежить від найповільнішої залежності.
Елегантний механізм обробки, який виконує запит, що не індексований, або віддалений API без таймауту, все одно працює повільно. Аналіз власних кодових рядків, ігноруючи об’єкти, які викликаються, дає неправильну відповідь. Вимірюйте показники p95 та p99 для всього дерева викликів, а потім виправляйте проблеми з індексами, групуванням операцій та таймаутами там, де насправді витрачається час.
Жоден з цих порад не є винахідливим, і саме в цьому суть: сторінки, які потрібно створювати о третій ночі, рідко бувають винахідливими. Зазвичай це одна з дванадцяти подібних ситуацій, які залишаються без уваги доти, доки проблема більше не здається терміновою. Створюйте ці нудні захисні механізми, поки сервіс ще достатньо малий, щоб його можна було змінити.