20 шаблонаў Node.js, які запобегаюць перыядам сервераў у працэсе вырабніцтва
Выучыце 20 практычных шаблонав Node.js — адз чыгункі кэшаў падключэнняў да адрабаткі бядаў і грацыйнага завершэння роботы — якія запобегаюць збоям і не дазволяюць прыходзіць да перазапуску.
Зупінка сервера практычна ніколи не выходзіць з-пад аднаго катастрофічнага ўзбуджувача.
Зазвычай прычыной ёсць што-небудзь тихае і незначнае.
З’ўязак з базай дадзеных ніколи не вяртаецца у пул.
Выкарыстоўванне памяці поступова адбываецца, пакуль процес не досягне свайго максімуму.
Адна маршрутка, якая раней адпавядала за 200 мілісекундаў, пачынае адпавядаць за пяць секунд.
Процес развяртання перерывае з’ўязкі, якія ўсё ўжо былі актыўныя.
Асінхронная задача выкананая неудачна, без таго, каб хтось зафіксав аднуючку.
Разыгранне трафіка змушвае кожны прыходзячы запит выконваць ўтроху дорожэйшы запит да базай дадзеных.
Спачатку нічга з гэтага не выглядае абавяжлівым.
Потым час адпаведзення пачынае збільшвацца.
Потым запиты пачынаюць тайма-аутаваць.
Потым процес цэлкам зупіняецца.
І тады пачынаюцься запитанні:
„Чаму сервер не працюе?“
Тое, што гэта так разчароўвае, — гэта тое, што большасць такіх ситуацыяў была можліва адварыць.
У багацых системах праўлення выкананняяў павтарыліся некалькі способаў выправлення проблем у Node.js, якія продовжаюць адзначацца як практычна эфективныя, самэўсёле таму, што яны націлены на асляпнючую прычыну, а не проста вяртаюць сервер у робочы стан пасля його зупінкі.
У этай статыце рассказваецца пра тыя способы, якія маюць наўсёй большое значэнне.
1. Не дазволяйце адной паспялай запытке зупініць весь процес
Сярод найбольш рызыкаваных прычын у кодавой базе Node.js — гэта дазволенне таму, каб выкліканая асключэнчыя або неконтрольванае адхіленне абяцанні дасяглі самага процесу.
Возьмім такі прыклад:
app.get("/users/:id", async (req, res) => {
const user = await User.findByPk(req.params.id);
res.json(user);
});
На першы погляд усё выглядае нормальна.
Але што будзе, яксяк з’єднанне з базай дадзеных абірвется пасеред запыткі?
Або ORM выклікае асключэнчыю?
Або неспадзяваная значэнне вхідных дадзеных зламае логіку ўнутры обробніка?
Без стабільнага карантинавання памылак процес можа западзець у нестабільны стан, і залежна ад конкрэтнай памылкі та налашоўкаў часу выканання, ён можа абсолютна зупініцца.
Больш эфектыўны падход — це явна направляць памылкі, замест таго каб яны выйшлі за межы:
app.get("/users/:id", async (req, res, next) => {
try {
const user = await User.findByPk(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
});
З таго моменту все трэба направіць у аднаго цэнтрызаванага карантинавальніка:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
error: "Internal server error"
});
});
Це не пра маскаванне проблем.
Это пра тое, каб адначыта падбіраць точнае месца, дзе будуць знаходзіцца памылкі.
Замест таго, каб кожны маршрут сам ствараў свой спосаб разв’язвання проблем, весь аплікэцыя следуе аднам стабільным, прагнозаваным шляхам.
2. Не трэба спрыяць перазапуску процесу як рашэнню
Калі процес Node.js зупіняецца, інстынктыўны спосаб выправлення зазвычай ёсць проста:
npm start
Альбо, для команд, якія выкарыстоўваюць PM2:
pm2 restart app
Это дазволяе службе быстра знову запрацаваць.
Ён не рашае прычыны, якая спачатку вызвала його абяранне.
Якщо ваша прыграма зупіняецца кожныя шасць гадзін у зв’язку з вытэкамі памяці, ўскрытые яе за таким графікам не ўтвараюць стабільной системы — гэта толькі тымчасова мера.
Інструменты на кшталт PM2, Docker, Kubernetes або платформы з кераваннем часу выконання можаюць автаматычна ускрытыць зламаны процес, што ўсё ж такі є дужа корыстным.
Напрыклад, з PM2:
pm2 start app.js --name api
pm2 save
Але автаматычная ускрытыця павінна розумецца як контроль збиткаў, а не як спосаб разв’язкі фундаментальнай прычыны.
Вам все рава патрэбны адказы на такія запитанні:
Why did the process crash?
What request triggered it?
How much memory was being used?
What error occurred?
Was there an unhandled rejection?
Was the database unavailable?
Ускрытыця восстанавляе адказнасць сервіса для вашых корыстнікаў.
Толькі належная можлівасць аналізу дапамагае з’ясаваць, чым самое спачатку была втрачана адказнасць сервіса.
Обе часткі є неабходнымі.
3. Разв’язваць проблемы вытэкаў памяці перш чым яны прыведуць да збоў з-за нехваткі памяці
Node.js выкарыстоўвае механізм збору сметлівых дадзенняў, але гэта не гарантуе, што памяц будзе правільна очышвана сама по сабе.
Працэс можа застаўляцься з аб’ектамі, якія на самай працэсе вядома ўжо не патрэбны.
Ось простая ілюстрацыя:
const cache = [];
app.get("/users", async (req, res) => {
const users = await getUsers();
cache.push(users);
res.json(users);
});
Кожны прыходзячы запит дадае ўсё больш дадзенняў у cache.
Паколькі працэс застаўляеся з аб’ектамі, якія там знаходзяцца, зборчык сметлівых дадзенняў не можа іх звярнуць.
З часам ситуацыя выглядае так:
Memory usage
100 MB
200 MB
400 MB
800 MB
1.2 GB
2 GB
💥
Точныя цыфры будуць разніцявацца залежна ад вашай наладкі і заданых лімітаў, але гэты фундаментальны прынцып є важлівым.
Зверніце увагу на такія моменты:
- Глобальныя масівы, якія продовжваюць растаць
- Кэшы без ліміту паверхні
- Вялікія аб’екты, якія застаюцца ў памяці
- Прыемнікі запуску падзеяў, якія ніколі не ад’ёмляюцца
- Таймеры, якія ніколі не абнімаюцца
Якщо ваша прыемлівачка кэшуе дадзеныя ў памяці, тым кэшам трэбна чыстае правілле.
Напрыклад:
Maximum entries: 10,000
TTL: 10 minutes
Eviction: LRU
Кэш без лімітавання на самай справе не функцыонуе як кэш.
Это проблема памяці, якая жадае рашэння.
4. Установіце таймауты для зоваў да званоўных служб
Званак іншай службе без установкі таймаута — адна з найшырэйшых спосабоў заставіць ваш API у стане очаквання.
Уявіце такі званак:
const response = await fetch("https://external-service.com/api");
Што, як тая званая служба раптам пачне адпавядаць дужа медленна?
Ваш сопны званак можа застацца ў стане очаквання набагато дышэў, чым трэба.
Тепер уявіце 1 000 адночасных выкарыстоўвальнікаў, якія звертаюцца да таго ж канцэнтра.
Раптам у вас будзе сотні або тыясячы званакоў, якія застаюцца у стане очаквання на адпаведзь, якая не прыходзіць.
Гэты застой згоднае час можа таксама перавантажыць ваш сэрвер.
Заўжды включайце чыстае кераванне тайма-аўтаматамі.
Напрыклад, выкарыстоўваючы AbortController:
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 5000);
try {
const response = await fetch(
"https://external-service.com/api",
{
signal: controller.signal
}
);
return await response.json();
} finally {
clearTimeout(timeout);
}
Тады ваша аплікацыя не будзе чакаць без канца.
Засадная правіла простая:
Ніколі не дазволяйце зовнішняй залежнасці кантролюваць, як дазго ваш сэрвер готавы чакаць, без жадных лімітаў.
Гэта такі ж важлівы момент, незалежна ад таго, чы рэч ідзе пра платежную прамежу, службу даставы электранаў, звычайны HTTP-эндпоінт, Redis, ваш слой базы дадзеных, API трэцьей стороны чы іншы внутрашні мікросервіс у вашай інфраструктуре.
5. Адлагоджэнне выкарання з’ёднанняў з базай дадзеных
API на Node.js можа выглядаць абсалютна нормальна сама по сабе, хоць асэльскі бутылчасты горлач распакоўваецца на слое з’ёднанняў з базай дадзеных.
Уявіце, як ваша аплікацыя сталкаецца з такім:
500 concurrent requests
і кожны з эых запитоў прабуе стварыць сваю сабэйшую запяту на з’язак з базай дадзеных.
У певны момент база дадзеных досягае заданага ліміту запятаў.
Пасля чаго новыя запиты проста перестаюць функцыонаваць.
API можа адпавядаць з такімі памылкамі:
Too many connections
Connection refused
Connection timeout
Пул запятаў існуе самэ прычым, каб не дазволіць кожнаму запиту стварыць абсалютна новую запяту самастаі.
Візуальна ён выглядае працоўна так:
┌── Connection 1
├── Connection 2
API ── Pool ──┼── Connection 3 ── Database
├── Connection 4
└── Connection 5
Пул выступае як кантролер, який лімітуе колькість запятаў, якія можна маты да базы дадзеных у пэўны момент.
Але правильна налаштаванне габарытаў пулу таксама важна, як і сам ўжо наяўны пул.
Налаштавайце пул рэштычна.
Якщо задаць занадта маленькія габарыты:
Requests
↓
Waiting for connections
↓
High latency
Якщо задаць занадта вялікія габарыты:
Application
↓
Too many connections
↓
Database overloaded
Правильная колькасць залежыць ад таго, якую навантажэння можа фактычна выдрыжаць ваша база дадзеных, насколькі канкурэнтна ваша аплікацыя, як выглядаюць вашы запиты і як розмешчана ваша інфраструктура.
Што б вы не рабілі, не трэба реагаваць на памылкі з’яўлення з’язку проста падвайваючы максимальны размер басэна з’язка без жадных лімітаванняў.
6. Абыць ад запитаў типу N+1
Іншы часты спосаб, якім каманды прыводзяць у пастку сваю базу дадзеных, — это шаблон запитаў N+1.
Уявіце тэрмінал, які вяртае спіс пастак:
const posts = await Post.findAll();
for (const post of posts) {
post.author = await User.findByPk(post.authorId);
}
Калі ў табле 1 000 пастак, гэта ператвараецца на:
1 query → retrieve posts
1,000 queries → retrieve authors
Total: 1,001 queries
Калі пастакоў дзесяць, тэрмінал працюе мгновенна і ніхто не зазначае нічога некальканаго.
Потым колькасць дадзеных вырастае.
І той самы тэрмінал стае надзвычайна медленным.
У канцэ вы ён можа запоўніць столькі месца ў базе дадзеных, што пашкодзіць нават несвязаныя тэрміналы.
Калькі прыметных спосабаў адлагоджэння:
JOIN-ы
SELECT
posts.id,
posts.title,
users.name
FROM posts
JOIN users
ON users.id = posts.author_id;
Актыўнае завантажэнне ў ORM
Большасць ORM-аў даёная можлівасць запрашаць супакоўаныя з рэкордамі данней у адной операцыі, замест таго каб выканаць адзін запит на кожны рэкорд.
Запыты пакетам
У змену на выкананне запытак адзін за адным, напрыклад:
Get user 1
Get user 2
Get user 3
...
запрашайце всіх неабходных корыстнікаў у адным з’еднаным запытэ.
Пад час тэставання на навантажэнне цікава задаць просты діаганастычны пытанне:
Скількі адзінаковых запытак да базы дадзеных выканаецца, калі гэты эндпункт вяртае 10 000 рэкордаў?
Якша чыстасцю адпаведзь:
"10 001."
гэта яскравы сігнал, што чагосць трэба адлагодзіць.
7. Дадаць пагінацыю, перш чым база дадзеных стане вельмі большай
Гэты фрагмент — гадзінны бомба:
const users = await User.findAll();
res.json(users);
У разрабоцкім режыме ён працуе чудова, калі таблыца мае:
37 users
Чэрез шысць месцаў тая ж таблыца тепер мае:
2,400,000 users
Той жа незменны канцэнтрык тепер можа:
- З’явіць мільйоны рэядкаў з базы дадзеных
- З’явіць ресурсы базы дадзеных
- З’явіць память процэса Node.js
- Серыяваць вельмі большы JSON-пакет
- З’явіць весь пропускную спроможнасць
- Значна падняць час адпаведзення
- Стварыць рызык перапалу часу
Рашэнне — пагінацыя.
Напрыклад, прыймайце запит у такім формате:
GET /users?page=1&limit=50
і обрабоцуйце яго так:
const limit = Math.min(
Number(req.query.limit) || 50,
100
);
const page = Number(req.query.page) || 1;
const offset = (page - 1) * limit;
Не менш важліва ўстановка мяркі для таго, што кліенты можаць запрашваць.
Не дазволяйце кліенту адправляць:
?limit=100000000
і прымушайце ваш API прабаваць выканаць запыт такога размеру.
8. Паўстаць ад выканання незалежных операцыяй па спрабах
Спачатку заўважым, што канцэнтры пункту трэба збіраць тры елементы дадзейнаў:
User
Settings
Notifications
Простая рэалізацыя можа выглядаць так:
const user = await getUser();
const settings = await getSettings();
const notifications = await getNotifications();
Якщо кожны вызов займае 200 мс, чаканне на іх адны за другім дае ў суме прыблізна:
200 + 200 + 200 = 600 ms
Калі гэтыя вызовы не залежаць адзін ад другога, кращэ выканаваць іх паралельна:
const [
user,
settings,
notifications
] = await Promise.all([
getUser(),
getSettings(),
getNotifications()
]);
Тады всі тры запиты выканаюцца адночасова.
Аб’ём часу зменшыцца да роўна таму часу, які займае самы медленны запит:
max(200, 200, 200) ≈ 200 ms
Гэта стосуецца не толькі скорачання каліка мілісекундаў у адпаведзі.
У масштабных системах дадатковая затрэмленасць за кожны запит значыць, што ресурсы сервера займаюцца дольш чым трэба.
Запиты, якія завершаюцца быстрей, таксама шырэй зваліваюць ресурсы.
Пры тым не варта аўтаматычна выкарыстоваць Promise.all у кожным случае.
Якщо адзін вызов практычна патрэбуе рэсультат другога, як у наступным прыкладзе:
const user = await getUser();
const orders = await getOrders(user.id);
тады гэтыя вызовы павінны за сваёй прыродай выкананыя апошнімі.
9. Адклэянне сістэмы з грацыёй
Аднам з самых простых спосабоў стварэння непатрэбных перыядоў зупінкі пад час развяртання ёсць раптовая зупінка процэса.
Разглядзім такую последовальнасць:
User → POST /checkout
↓
Processing payment
↓
Deployment starts
↓
Process killed
У такім сцэнарые корыстнік можа пабачыць толькі:
Connection reset
нават якщо аснованая операцыя вже часткова была завершана.
Адклэянне сістэмы з грацыёй рашае гэту проблему, калі сервер перестаёт приймать новыя з’ўязкі, але дазваляе запусчаным запитам завершыцца самастоятельна.
Мінімальная версія выглядае так:
const server = app.listen(PORT);
process.on("SIGTERM", () => {
console.log("SIGTERM received");
server.close(() => {
console.log("HTTP server closed");
process.exit(0);
});
});
У прымэтной сістэме адклэянне з грацыёй часта таксама значыць закрыцце:
- Базаў скарбніц канектацый
- Канектацыі з Redis
- Працоўнікі очаквальнай ляйні паведамленняў
Абщая мета сводзіцца да наступнага:
Stop receiving new work
↓
Finish current work
↓
Close resources
↓
Exit
Процес развяртання не павінен ствараць вражэння, што хтось выдраў кабель з меры.
10. Іспользаванне пераканальніках стану і готовнасці
Цэлкам можліва ситуацыя, калі процес Node.js застаецца жывым, а прыкладна програма пазаду яго фактычна перестае працаваць.
Уявіце сабе такі стан:
Node.js process → Running
Database → DOWN
API → Practically unusable
Якщо ўжо єдым сігналам вашага балансэра навантажэння є тое, чы розпакетаваецца TCP-порт, ён можа продовжваць направляць трафік на экземпляр, який на самай працэ не можа нічога корыстнага запрацаваць.
Тэрмінал готовнасці дае вам рэальны сігнал для пераканальніка:
app.get("/ready", async (req, res) => {
try {
await db.query("SELECT 1");
res.status(200).json({
status: "ready"
});
} catch (error) {
res.status(503).json({
status: "not_ready"
});
}
});
Это дазволяе вашай інфраструктурэ розразліваць два абсалютна разныя станы:
Process is alive
протыва:
Process is ready to serve traffic
Эта разліка мае вельмі важлівэе значэнне, калі вы запускаеце новы код або вярнуецеся пасля неудачы.
11. Захавайце сервер ад разбіўчых падыходоў трафіку
Спачатку ваша служба з лёгкасцю справляецца з:
100 requests/second
Потым ваш продукт стае доступным для шырокай аудыторіі.
Раптам вы сталкватэцеся з:
2,000 requests/second
Без жадных захоўнікаў такі сплеск можа цалкам перавантажыць API.
Калькі звычных спосабаў для працэсавання разбіўчых падыходоў трафіку:
- Абмежэнне колькісці запытоў, якія кліент можа адправіць за паведамлены перыяд
- Размешчэнне зворачныго проксі перед серверамі вашага застосунку
- Разпаджэнне прыходзячага трафіку між калькіма экземплярамі
- Храненне у кэшы адпаведзей, ўбліжча не прабываць занова
- Распространэнне статычнага кантэнту через CDN
- Перадача задач у очакальную лісту заместо ўжоўчаснага ўпрацавання
Вам не патрэбны ўсе пункты з гэтага спісу.
Але кожная прыменныя система павінна маты чыстае адказ на такой запит:
Што будзе, яксяк трафік раптам зросте у дзесяць разоў?
12. Вывядзіце дорогія задачы за межы HTTP-запыту
Возьміце канчатковую точку, якая стварае пользователя і адправляе прывітальны электронны лист у тым жа запыце:
await createUser();
await sendWelcomeEmail();
res.json({ success: true });
Якшо служба электронных лістоў адпавядае за тры секунды, то ўсё гэта трое секундаў, якія паўтарае корыстувач.
І ўсё гэта не найгорша — адказнасць у этапе адправкі ліста можа спрычыніць неудачу всего запыту, нават якшо адказнасць корыстувацкага акаунта была створана ўспэшна.
Дыяграма на адміністрацыі з рангамі запобегае такой сувязі:
HTTP Request
↓
Create User
↓
Return response
↓
Queue email job
↓
Background Worker
↓
Send Email
API больш не павінен чакаць, калі повольна фонавая работа не завершыцца.
Гэты патэрн ўжо особліва корыстны ў такіх ситуацыях:
- Адправка паведамленняў пра пאверынанне або прывітанне
- Стварэнне атласаў за запатраненням
- Змена размеру або трансфармацыя заванесеных зображэнняў
- Кодаванне або транскодаванне відэа
- Адправка паведамленняў у формате push
- Стварэнне експортных дадзеных у великіх кантэйнерах
- Обработка дадзеных, прыходзячых через webhook
- Выкананне складных числовых або аналітычных вычыслаў
Як правіло, працоўнік адпаведальны за обработку HTTP-запытоў павінен выканаць толькі мінімальную колькасць работ, неабходных для стварэння адпаведзі.
13. Забезпечыце безпечную перапрыбутку фонавых заданняў
Адправка работы ў рангі адкрывае новы пытанне:
Што будзе, калі сам працоўнік зазнае неудачы ў паўпацёку?
Разглядзім такі этапы:
Worker receives payment notification
↓
Starts processing
↓
Crashes
Система очакоў, верагодна, будзе аўтаматычна прабаваць выканаць задачу знову.
Зазвычай гэта добрае.
Але якщо задача не ўзгодна з прынцыпам ідэмпотентнасці, ўскоранае выкананне можа спрычыніць тыя ж побачныя эфекты зноў.
Напрыклад:
Send payment
Send payment again
альбо:
Create order
Create duplicate order
Чырвонай прычыной гэтага ёсць тое, што фонавыя задачы павінны быць створаны з урахоўванням:
- Прабаў выканання знову
- Задзейнавання паўтарэння праз час
- Обробка некоректных звесцей
- Ідэмпотентнасць
- Максымальная колькасць прабаў
- Манітарынг
Без ідэмпотентнасці механізм прабаў можа ператворыць адну неудачу на калькі небажанных побачных эфектаў.
14. Прыпыніце логаванне чутлівай інформацыі
Працуячы над багам у працоўнай средзе, хочацца логаваць усё, што толькі можна.
Тыповы прыклад:
console.log(req.body);
У тэле запытку можа легкая ўключыць такія элементы:
Password
Access token
Credit-card information
Personal information
Калі гэта адбудзецца, ваша система логавання будзе зберагаць чутлівыя даны.
Безпечнейшы спосаб — логаваць толькі тыя поля, якія вам действительна патрэбны:
logger.info({
userId,
requestId,
endpoint: req.path
}, "Login attempt");
Не паддавайцеся на глухі календар, думаючы:
"Гэта толькі ў логах."
Логі часта застаюцца прыемнае тыясячы дзён, месцы або нават дыявольскі, і іх часта копіююць у такія месца, як:
- Системы логавання ў хмаре
- Платформы для монітарынгу
- Системы SIEM
- Сховішча для рэзервнай копіі
- Панелі керування для разработчыкаў
Адлагоджэнне проблемы ў працоўным режыме не должна завершвацца стварэнням адной занова інцидэнты безпекі.
15. Дадзіце ID запытакаў раней, чым яны патрэбны
Калі ў працоўным режыме ўсё ламаецца, неяўнасць часта распрастраняецца на калькі систем:
Client
↓
Nginx
↓
Node.js API
↓
Redis
↓
Database
↓
Payment API
Без спосабу з’єднання цых запускаў разам, відновленне таго, што сталася, можа быць повольным і складным процесам.
Рашэння — стварыць ID запуску якомога раней:
request_id = 7f4e9c21
а пасля прыўязаць яго да кожнага адпаведнага запісу лога чы рэспансу:
API log
Database-related log
Payment log
Error response
Напрыклад:
{
"requestId": "7f4e9c21",
"method": "POST",
"path": "/orders",
"status": 500
}
За такіх умоў можна стежыць за адным запускам па ўсім вашым системе.
Тады, калі кліент паведамляе:
"Мой замовленні не выйшла.
вы можете проста папросіць яго ID запуску замест таго, каб шукаць сярод мільйонав ліній лога, намагаючыся падабралі часы запускаў.
16. Запобегчы дублікацыі запускаў, якія ствараюць дублікаты дадзеных
Проблемы сеті трапляюцца постаўна, і кліенты павінны з ямі справляцца.
Уявіце сабе такую последовальнасць:
Client
↓
POST /orders
↓
Server creates order
↓
Response lost
Кліент не можа з’ясаваць, чый запит насправды быў успешны.
Таму, як і ўзрозумела, ён прабуе зноў:
POST /orders
↓
Server creates another order
Рэзультат:
Order #1001 → original request
Order #1002 → retry
Гэта самэ гэты сценарый, які павінны рашаць клучы ідэмпотентнасці.
Кліент дадае заголовак на кшталт:
Idempotency-Key: 5e7b2d91-...
Сервер запам’ятае рэзультат, звязаны з гэтым клучам. Якщо той самы клуч з’явіцца зноў, ён проста верне первы рэзультат уместо стварэння другога ресурса.
Гэта мае вялікое значэнне для такіх операцый, як:
Payments
Orders
Subscriptions
Account creation
Resource provisioning
Кожны прыменны API павінен быць спроектаваны з урахоўванням гадкі, што кліенты будуць прабаваць зноў, іноды болей разоў.
17. Іспользуйце транзакцыі, калі даныя павінны застацца консэстантнымі
Падумайце пра такую последовальнасць:
Create order
Decrease inventory
Create payment record
Тепер уявіце, што адна з гэтых ступеней завершыцца неудачаю на паўпаце:
Create order → SUCCESS
Decrease inventory → SUCCESS
Create payment record → FAIL
У вашай базе дадзеных зараз застаўся незаканчаны аператыў.
Калі калькі запісаў у базе дадзеных павінны выконвацца ўсе разам, як адна ентытная операцыя, ўкладзіце іх у транзакцыю.
Ідея выглядае працоўна так:
await db.transaction(async (transaction) => {
const order = await createOrder(transaction);
await decreaseInventory(transaction);
await createPaymentRecord(transaction);
});
А якшо хоць адна з стадзеяў выклікае памылку:
Rollback
всё будзе анульвацца, ўместа таго каб засталіся несупараднаваныя записы.
Транзакцыі не будуць рашаць усія проблэмы, з якімаі сталкнуцеся ў распадзеленай системе, але ў межах адной базы дадзеных яны ёсць аднамі з найболей надзеяных інструментаў для падтрымкі супараднаванасці дакументаў.
18. Не дазвольце аднаму медленаму запыту знішчыць базу дадзеных
Аднаму паспела напісанаму запыту можа зайняць непрапорцыйна вялікую частку магчымасцей базы дадзеных.
Паглядзіце гэты прыклад:
SELECT *
FROM orders
WHERE customer_id = ?
ORDER BY created_at DESC;
Якщо у customer_id няма індэкса, а таблыце ўтримвае мільйоны рэядкаў, гэты запыт можа змусіць базу дадзеных выканаць значна большую колькасць работ, чым трэба.
Іспользуйце
EXPLAIN
для аналізу значных запытоў.
Зверніце увагу на апавешчальныя сігналы, такія як:
Full table scan
Huge number of rows examined
Unexpected joins
Poor index usage
Expensive sorting
Потым базавайце сваю оптымаізацыю на тым, што паказваюць факты.
Не паслужывайцеся здагадкамі.
Запыт, який вяртаеся за 50 мілісэкунд на локальным вычыслальніку, можа зайняць колькісць секунд, калі ён запускаецца над даннымі у прыемных масштабах.
19. Заданне лімітаваў ресурсаў
Сервер у прыемным режыме патрабуе лімітаваў.
Без іх адзін злонамерны чы несправны запыт, або адзін неконтрольваны процес, можа выкарыстаць усі доступныя ў яго распаняжэнні ресурсы.
Падумайце пра заданне лімітаваў для такіх рэчаў, як:
Request body size
File upload size
Database connections
HTTP request duration
Cache size
Queue size
Worker concurrency
Pagination size
Memory usage
Напрыклад:
app.use(express.json({
limit: "1mb"
}));
і:
const limit = Math.min(
Number(req.query.limit) || 50,
100
);
Не існуе універсальнага числа, якое падходзіць у кожным случае.
Але асновная прынцып завжды дапраўны.
Кожны рэсурс павінен маты разумную меру.
Залішаць што-небудзь без меры рэдкая правільная выбор у працоўнай суперактывацыі.
20. Следзіце за станам да інцидэту, а не пасля яго
Вы не зможаце запобiec кожнам перывам у работе.
Тое, што вы можетэ контролаваць, — гэта шыбкасць ўвагі да проблемы і шыбкасць яе адновлення.
Як мінімум, стежыце за:
HTTP 5xx rate
Request latency
CPU
Memory
Database connections
Database latency
Event-loop health
Queue depth
External API failures
Restart count
Напрыклад:
09:00 → 0.2% errors
09:10 → 0.3%
09:20 → 0.5%
09:30 → 4.8% 🚨
Якшо ніхто не стежыць за гэтымі цифрамі, вы дазнаецеся пра проблему толькі тады, калі пачнуць скаржыцца корыстувальнікі.
Эфектывае апавешчэнне зміняе ситуацыю з:
"Кліянты паведамляюць нас, што API не працуе."
на:
«Наша шчыльнасць 5xx перасягла прагу пяць хвілін тым цяпер».
Такая розбіжнасць у часе адказу значна.
Модель, яка стоіць за большасцю гэтых рашэнняў
Якщо паглядзець на всі гэтыя рашэння, выкрываецца спакоўны мотыў.
Надзеяныя системы Node.js не працуюць на падазрэнне, што все будзе гаразд.
У замен яны ствараюцца на падазрэнне, што:
Requests will fail.
Databases will become slow.
Networks will timeout.
Clients will retry.
Dependencies will go offline.
Traffic will spike.
Deployments will restart processes.
Developers will make mistakes.
і навкола кожнага з гэтых падазрэнняў ставяцца захопы.
Рэзультатныя архітектуры часта выглядаюць так:
┌───────────────┐
│ Client │
└───────┬───────┘
│
▼
┌───────────────┐
│ Reverse Proxy │
│ Rate Limiting │
└───────┬───────┘
│
▼
┌───────────────────┐
│ Node.js API │
│ │
│ Validation │
│ Timeouts │
│ Error Handling │
│ Request IDs │
└───────┬───────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌─────────┐
│Database│ │ Redis │ │ Queue │
└────────┘ └────────┘ └────┬────┘
│
▼
┌──────────────┐
│Worker / Jobs │
└──────────────┘
Кожны слой мае чыстаю задачу.
Кожная зовнішняя залежнасць мае вядомую стратэгію выкарыстоўвання у разе абыякавасці.
Кожная дорогая операцыя обмежваецца певным лімітом.
Кожная значымая дзеянне можа быць спазірана і вымерана.
Самэ гэтая сумасць дае системе ўстойчывасць.
Найважнейшы лекцыя
Самыя значныя падышчы ў надзеямоці не завжды даюцца за рахунак дадзення большай колькасці інфраструктуры.
Інодзе рашэнням ёст толькі адна зменена лінія коду.
Інодзе — дадзенне нехваткавога індэксу.
Інодзе — абгортанне званачка да зовнішняй API ў межах таймауту.
Інодзе — ператворэнне 1,001 запытку да базы дадзеных у два.
Інодзе — абгортанне набору запісаў у транзакцыю.
Інодзе — проста падтрымка таго, каб процес завершваўся чыста.
Тое, што адзначае ненадзеямоцную API ад надзеямоцной, зазвычай вырашаецца за рахунак серыі маленькіх інжынерных выбораў.
Ні адны з іх сам по сабе не выглядае ўражаючы.
Але разам яны ёсць тым, што не дазволяе звычнам дзень у працэўным режыме ператварыцца на надзвычайную ситуацыю.
Таму, калі наступны раз ваша API на Node.js будзе працаваць абсалютна так, як і лячылася пад час розрабоцтва, не абмежвайцеся толькі запытам:
"Ці гэта працуе?"
Замест таго запытайце:
"Што будзе, калі гэта не працуе?"
Гэты запыт ёсць справжнім пачаткам надзеянасці працы системы ў прымэтных умаляваннях.
А найлепшы варіянт перыву ў роботе — гэты, якога вашы корыстувальнікі нават не зазначаюць, калі вы вылучылі та паспелі ліквідаваць нештаянье раней, чым гэта ператворылася на перыв у роботе.