Галоўная / Артыкулы

Контроль паралельнай роботы Node.js: як узбегчы апостачкі API за дапамою p-map і Bottleneck

Безмежны выкарыстоўванне Promise.all можа зіпсаваць API-я. Спалучыце p-map і Bottleneck у Node.js, каб абмежыць канкурантнасць, сарахтацца да лімітаў частоты і захаваць работоспроможнасць систем, якія працуюць пасля.

Выдавец: REACTAPP.TOP5 хв чытання1240 слоў

Крыніцы: Stop Melting Your APIs: Smart Concurrency Control in Node.js with p-map & Bottleneck

Большасць прыкладоў практычнага выкарыстання Node.js не зламваюцца чераз неправильную логіку. Яны зламваюцца таму, што выкананыя задачі адбываюцца занадта агрэсыўна, усе адразу.

Уявіце сабе такую ситуацыю: каманда выпускае функцыяльнасць, якая за калькі секунд выканае прыблізна 10 000 запытоў API. На локальным комп’ютере все працуе гладка і швыта, а рэзультаты вялікія.

Калі той самы код прайшоў у працэйную среду, картына змінілася:

  • Паўсюду з’являюцца памылкі 429
  • API трэціх сторон начынае адхіляць запыты
  • Спрацоўванне памяці растае стрэмгава
  • Логіка павторных спроб толькі пагаршае ситуацыю

Настоямы проблемай ніколі не быў сам внешній API. Це была канкуранцыя задач, якая працавала без жадных лімітаў.

Пастка, якая хаваецца ўнутрь Promise.all

Багато разработчыкаў спачатку вяртаюцца да гэтага патэрану:

await Promise.all(urls.map(fetchData));

Ён чытаецца проста і, здаецца, работае эфектыва.

Але паглядзіце атрымальней на тое, што ён на самай працо запускае:

  • Кожны запит выконваецца адночасова
  • Няма меры ў тым, скількі запітов можа выконввацца адночасова
  • Няма механізму контролю навантажэння
  • Ліміты частоты, заданыя API, абсалютна ігнаруюцца

У практыцы такі падход працуе як генератор раптовых сплескаў навантажэння.

Promise.all не дае можлівасці кантролю паралельнасці. Ён спрычыняе експлозію паралельнасці.

Першы крок: обмежыць паралельнасць за дапамою p-map

Якщо ваша мета — абмежыць колькі операцый выконваецца паралельна, p-map — гэты інструмент, створаны для гэтай задачы.

import pMap from 'p-map';

const result = await pMap(
  urls,
  async (url) => {
    const res = await fetch(url);
    return res.json();
  },
  { concurrency: 5 }
);

Што гэта выправляе

  • У будзь-які момент паралельна выконваецца толькі пяць запітоў
  • Рэшта зачакваюць у черзі, пакуль не з’явіцца вольная ўстановка
  • Выкарыстоўванне CPU і памяці застаецца стабільным
  • Основная ідея

    Канкурэнцыя на самай справе не стосуецца падвышэння швайна. Це стосуецца контролю над напорам на вашу систему.

    Чаму гэтага самога недастатнь

    Установка concurrency: 5 не гарантуе, што вы ухіліцеся ад лімітаў частоты запытоў.

    API не толькі фіксуюць, скількі запытоў прыходзіць адразу, але і скількі прыходзіць за певны перыяд часу.

    Это значыць, што вам трэба кераваць часам, а не толькі колькасцю паралельных запытоў.

    Другі крок: застосаванне лімітаў частоты з викорыстаннем бутлнэк

    import Bottleneck from "bottleneck";
    
    const limiter = new Bottleneck({
      maxConcurrent: 5,
      minTime: 200 // ~5 requests per second
    });
    const fetchWithLimit = limiter.wrap(async (url) => {
      const res = await fetch(url);
      return res.json();
    });
    const results = await Promise.all(urls.map(fetchWithLimit));
    

    Што гэта даўае

    • Забягае за тым, каб канкурэнцыя застаўалася павярнутай
    • Разпадзеляе запыты па часу
    • Зупіняе спалахі запытоў пры ўсуненні іх

    У замест на тое, каб атакаваць API адразу, вы тепер надаеце яму запыты па стабільным, прагнозаваным тэнпе.

    p-map протык Bottleneck

    Кожны з інструментаў рашае адзінаковую проблему:

    • p-map справляецца з ситуацыяй, калі занадта многа задач выпалююцца адразу
    • Bottleneck справляецца з ситуацыяй, калі ліміт частоты API вызывае памылкі 429
    • Калі вам патрэбны оба захавання, адзинавайце два бібліятэкі

    Шаблон, які варта выкарыстоўваць у працэсе

    import pMap from 'p-map';
    import Bottleneck from 'bottleneck';
    
    const limiter = new Bottleneck({
      minTime: 200
    });
    const task = limiter.wrap(async (url) => {
      const res = await fetch(url);
      return res.json();
    });
    await pMap(urls, task, { concurrency: 5 });
    

    Гэтая комбінацыя дае:

    • Стабільную, обмежаную канкуранцыю
    • Захаванне ад перасягнення лімітов частоты
    • Профіль навантажэння, які можна заздалегідь працягнуць

    Работа з неудачамі: павторныя спробы з адкладэнням

    Нават система, якая рэгулюецца адзінакова, часам сталкаецца з неудачамі. Тое, што адзначае стойкую систему ад кранглайвай, — гэта спосаб, якім яна реагуе на тыя неудачы.

    async function fetchWithRetry(url, retries = 3) {
      try {
        const res = await fetch(url);
        if (!res.ok) throw new Error('Failed');
        return res.json();
      } catch (err) {
        if (retries === 0) throw err;
    
    // exponential backoff
        const delay = 2 ** (3 - retries) * 100;
        await new Promise(r => setTimeout(r, delay));
        return fetchWithRetry(url, retries - 1);
      }
    }
    

    Чаму адкладэння мае значэнне тут

    • Павторныя спробы без жаднага адкладэння часта вызываюць штормы павторных спроб
  • Павтарэнне спробы з адзінчасным адпаведным паузам дазволяе системе грацыязна восстанавіцца.
  • Компрэсія между працёйнасцю і затрымкамі

    Сярод гэтага і вырашаюць справжнія пытанні дизайна системы.

    • Меньшая канкурэнцыя дае большую безпеку, але сповольняе працю
    • Вышэйшая канкурэнцыя прыбліжае выкарыстоўванне ресурсаў, але падваіць рызыкі

    Напрыклад:

    • Якшы ўстановіць канкурэнцыю на 2, ўсё застанется стабільным, хоць і сповольна
    • Якшы ўстановіць канкурэнцыю на 20, выкарыстоўванне працясніцца, але можна перасягнуць ліміты

    Не існуе універсальнага ідэяльнага значэння. Яго трэба налаштаваць па тым, што можа адрабатваць ваша система і цэльны API.

    Возможнасць аблікавання: крок, які большасць команд прахоўвае

    Не проста запускайце гэтую наладку і забывайце пра яе ў рэальных умовах.

    Стежыце за:

    • размахам рою задаўанняў
    • Серэдні час, які запыткі практычна чакаюць
    • Спявазанне колькасці успехоў і невыпадковасцей

    Без можлівасці бачыць, што на самай працоўвае ваша система, яе правильная налаштовка становіцца простаю згадкай.

    Дзе месца такім очакам, як BullMQ або Kafka?

    Это справядлівы вопыт.

    • Очакі на паведамленні забезпечваюць надзеяны, асінхронны обрабоцкі
    • p-map і Bottleneck аддаюць можлівасць кантролю ўнутар адной службы

    Вжывайце очак, калі:

    • Заданні павінны перазстаць пасля збою аб перачысці
    • Навантажэнне великае або распадзелена між калькама служб

    Вжывайце падход p-map і Bottleneck, калі:

    • Вам патрэбна шыраная, кантролюемая екзекуцыя, якая застаецца ўнутар адной службы

    Рэзультаты пасля застосавання

    Пасля адаптавання як лімітаў на паралельную роботу, так і лімітаў швайнасці, адны з праектных систем пазначыў:

    • Рэшты каштоўкаў 429 зменіліся з прыблізна 18% да менейш чым 1%
    • Значна паднялася стабільнасць падчас взаімадзеяння з зовнішнім API
    • Полны заканчык проблемы з павтаральнымі спробамі

    Логіка прыкладнага програму засталася тая ж. Зменілася толькі дисцыпліна ў експанацыі коду.

    Заключныя меркаванні

    Адрабатка асінхроннага коду — гэта базовая навык для разработчыкаў Node.js. Знанне таго, як кераваць швайнасцю ў выкананні такога коду, і є тым, што адразняе кваліфікаваных інжынераў бэкенду.

    Якщо ваша служба взаінадзейнае з:

    • зовнішнімі API
    • базамі дадзеных
    • іншымі мікросервісамі

    тады кантроль паралельной роботы перастае быць дапаможным элементам і становіцца ключовай часткай вашай архітектуры.

    Падсумак

    Якщо ваш код для працы ў продакшэне яшчэ выкарыстоўвае Promise.all без жадных лімітаў, то ад прыпынення роботы застаточна лишь незначны сплеск навантажэння.

    Практычны пачатковы пункт выглядае так:

    • Уведзіце p-map, каб обмежыць паралельную роботу
    • Уведзіце Bottleneck, каб застаўліць ліміты швайна
    • Дадзіце логіку перапрыбуткі, каб паўнейша адзычнасць

    Гэта шлях ад коду, які проста работае, да коду, який насправды можа масштабавацца.