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

Усередзінне працаванне службы паведамленняў з калькольніках аптэкароў: чаргі, альтернатывныя способы, доверлівасць

Практычны ўзгляд на дыяжнаўскі процес надзеянага сервіса паведамленняў: зберагчык атрыбутаў апраўніцы, запасны варыянт прадаўцы, павторныя спробы BullMQ і DLQ-ы, справядлівасць для корыстнікаў і перакананне падпіса webhook-а.

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

Крыніцы: Building NotifyKit: The Engineering Behind Reliable Notifications

Адрасаванне электранайтовага або SMS выглядае як простая задача: запытваецца API-сервісу падаючага прадавцы і ўсё. У рэальных умовах гэты адзін запыт send() таямніча ператвараецца на проблему дыстрыбюіраваных систем, таму што прадавцы выконваюць тайма-аут, абмежваюць частоту запытоў, выключаюцца, доставляюць паведамленні не за порядкам, а перапрыбуткі можу стварыць дублікаты. У гэтым артыкуле разбіраецца архітектура адкрытага, мнагатэнарного сервісу паведамленняў, створанага на базе Node.js, BullMQ і Redis, і ў яму рассмотрваюцца семь аспектаў: секрэты, неудачы прадавцоў, дизайн кялі, порядак доставкі, можлівасць доставкі, справядлівасць па адносу да тэнараў і верыфікацыя webhook-аў. Для кожнага з іх паказаныя рашэння, прычыны яго і ліміты, якія ўсвядомлена прыймаюцца, тады вы зможаце перасвайсці гэтыя прычыны у своіх сопрацоўных системах.

Два віды секрэтов, две стратэгіі зберагчання

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

Ключы прадаўцоў: зашыфраваць, таму што іх неабходна вярнуць

Кліенты выкарыстоўваюць савеянскія API-клучы своих прадаўцоў электронной пошты, і платформа должна представіць гэтыя клучы прадаўцу ў кожны момент адправкі. Гэта выключае варыянт хэшавання; даны должны быць можлівыя да вярнення. У проекта спачатку пераканальваецца кожны клуч у аднасоўчыні з яго прадаўцам, а потым ён зашыфраваны за дапамогою AES-256-GCM пры ях збераганні. GCM — это режым з аутентыкацыяй, таму, кроме конфідэнцыйнасці, ён таксама выяўляе пазначкі: зашыфраваны тэкст, які быў зменены, не можа быть расшыфраваны, у працоўнасці не будучы правдападобным, але некоректным ліцэнзыйным дакументам.

Чысты тэкст ніколі не прабывае ў базе дадзеных, логах чырахоўках. Ён дэкрыйптуецца толькі ў двух месцах: для атрымання замаскаванага прывіту на панелі керування, і ў процесе адправкі пакета прымэроўкаю, яшчэ перад адправкай запыту да зовнішняго прадаўца. Обмежэнне дэкрыйптавання да калькоў чыста адзінаковых кодавых шляхоў значна спрыяе аудыту ў разы, калі выканаеся випадковая вытэчка.

Клучы API, выданыя платформай: хэш, таму што іх трэба толькі пераканацца

Клучы API, якія платформа выдает своім кліентам, працуюць за адваротной логікай. Системе ніколі не трэба ўскрыць іх, толькі паказаць, чы рэальны клуч, які быў представлен. Таму клуч паказваецца адной раз пры стварэнні, і сохраняецца толькі його хэш SHA-256. Пры кожным запыте прыбутны клуч хэшуецца і паўставляецца з сохраненым хэшам. Загублены клуч немагчыма вярнуць, і яго трэба перзаснавіць.

У такім случае падзейны хэш, як SHA-256, ўсунецься, таму што клучы — цэлкава довгія, вялканастыя строкі з высокай энтропіяй. Такой выбор будзе некальканы для пароў людзей, якім патрэбны спецыяльна медленныя алгорытмы, такія як bcrypt, scrypt чыста Argon2, каб запобiec адгадванню.

Секрэты падпісаў webhook: чыста ў середзіне

Секрэт, які викорыстоўваецца для падпісаў выходзячых webhook, генеруецца платформай, падобна да API-клуча, але яго неасвечаная значэнне патрэбна ў кожны раз, калі падпісваецца webhook, падобна да клуча прадаўцы. Таму ён шифруецца за дапамогою AES-256-GCM у стані спакою, дэкодуецца толькі за час кожнай операцыі падпісаў і застаецца толькі ў памяці пад час выкарыстоўвання.

Адпрацоўкі ненадзеяных прадаўцоў за дапамогою арранжаванага запаснага варыянта

Кожная система, прыяжаная да адзінаго прадаста, спадзяецца яго перыванням, обмежэннем працы і тайма-аутамі. Служба зменшае гэты рызык за дапамою спіса прыоритэтаў па кожнаму кліянту. Кліент можа зарэўстраваць калькі прадастоў, напрыклад SendGrid, Resend і Postmark, і парадкаваць іх. Калі запит на адправку не называе прадаста, спіс прыемлівых варыянтаў ствараецца на адной з гэтых пазіцый, пачынаючы з найвышэйшага прыоритэта.

Служба перайшоўць па спісе. Якщо прадаст не можа выканаць задачу чераз перыванне, обмежэнне працы чы занадта медленную працу, гэты факт фіксуецца, і пробуюцца наступны прадаст. Першы успех завершае процэс, і кліент ніколі не знае пра альтернатыўны варыянт, як толькі ўсе прадасты не можу выканаць задачу.

Кліянты таксама можу прыкрепіць паведамленне да аднаго прадаста, за жаданням з адным назначаным альтернатыўным варыянтом; у такім случае пробуецца толькі гэтая пара. Не кожная памылка адносіцца да сябе на аднаковы лад:

  • Пастаяяяе неудачы, такія як адмоўленыя кранталізацыі або неправільныя запиты, выклікаюць неудачу адразу, таму што пракаты ўспрыяць іх вылечэнню не можаць.
  • Тымчасовыя неудачы, такія як тайма-аўтаматы, ліміты частоты і адпаведзі провайдера з кодам 5xx, пракатуюцца з адступамі.

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

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

Архітектура калеў

Адрасаванне паведамлення ўнутры прыказу API, яным гэта было запускаецца, спаявае час адпаведзьбы вызываючага з наймедленнейшым прадастальнікам. Медлівы або недоступны прадастальнік будзе утримваць HTTP-запыт кліента ў стане очаквання, пакуль той нарэшце не успее або не выйдзе за час. Сэрвіс ухілваецца ад гэтага, раздзеляючы прыемку і адправку.

Раз'ядруванне прыказу ад адправки

API толькі пераканальвае прыказ, стварае рэкорд Job у базе дадзеных і додае заданне ў чергу. BullMQ на Redis адпавядае за планаванне, пры чым існуеячыя чергі падзеленыя па типах заданняў:

  • notifications-email — для электранаўтляў кліентаў
  • notifications-webhook — для webhooks кліентаў
  • platform-email — для внутраніх апавешчэнняў самага сэрвісу
  • notifications-failed — як черга для заданняў, якія выкараналі всі спробы

Адзінаковыя чаты означаюць, што адны тип заданняў можа застацца у затрымцы або быць налаштаваны без паўлінняяна на іншыя. Для болей шырокага аднараджэння пра проектаванне заданняў у BullMQ, адзірніце стварэнне надзеяных систем заданняў у фоне з BullMQ і Redis.

Колькасць праказоў лічыцца за цэлыя праходзы, а не за прадастальнікаў

Кожна задача мае тры прабавы з экспансіяным адкладэнням, і важлівы момент — што значыць «прабава». Це не адна прабава на кожнага паспрэдавальніка. Одна прабава — це повны прыем для паспрэдавальнікаў кляйента за прыоритетам, калі працэс зупіняецца пасля першага успеху. Калі всі паспрэдавальнікі працэсу не дадзе рэзультата, задача выкарыстоўвае адну прабаву, і наступная спроба пачынаецца знову з паспрэдавальніка з найвышым прыоритетам. Ляч толькі пасля трох невыкараных працэсаў задача, включаючы паказанніе адзіны, копіруецца ў очакальную чату для паводзяных паведамленняў, а выначальная задача пазначаецца як FAILED.

База дадзеных ёсць адказным источнікам, нават калі сама очэрга не працуе. Запуск кліента Job або запис PlatformEmailLog для внутраніх паведамленняў запісваецца з статусам PENDING прытымкі да таго, як ён будзе дадзен у очэргу. Якщо до Redis немагчыма дасягнуць або падчас дадзення запуску выйшла памылка, яна фіксуецца, пра нея паведамляецца, а рэкорд у базе дадзеных апডейтуецца на FAILED з пазначэнням прычыны. Жаданне не зникае непазначальна між API і очэргай.

Для чаго патрабуецца очэрга для непрацюючых запускаў

Калі запуск выкарыстоўвае ўсе тры сваія спробы, копія падае ў notifications-failed, якая містіць усю інфармацыю, памылку і час запісу, тады як рэкорд у базе дадзеных ставіцца на FAILED з той самай прычыной. Двойная фіксацыя неудачы ўмышленая, таму што кожны рэкорд падходзіць для разных аудытарыяў:

  • Рэкорд базы дадзейна назначан для кліента і падтрымвае GET /jobs/:id.
  • Элемент DLQ назначан для аператара, яўляецца тымчасовай копіяю, прызначанай для монітарынгу і дальнейшых дзеянняў.

DLQ мае значэнне толькі тады, калі яго старанна адстэйкаюць. Кожны прыбываючы заданні запускае апавешчэнне ў Slack, пераўтараючы шлях апавешчэння, які ўжо створыўся для адмов у роботе электроннай пошты платформы. эта ператварае чергу на сігнал ранньага апавешчэння: раптовы падвышэнне колькасці заданняў у статусе «мертвых лістоў» за короткі час зазвычай вказывае на якуюсь системную проблему, такую як адмова прадаўцы чы неякі некоректны запуск.

Нічога не выходзіць з DLQ аўтаматычна. Заданні з статусам «мертвае» прабуецца запустыць занова толькі тады, калі кліент выканае запит POST /jobs/:id/retry, які перакладае яго у статус PENDING і пачынае процес занова.

Прыорітэт і канкуранцыя

Заданні маюць адзін з трохоў прыоритэтаў: CRITICAL, NORMAL або LOW; гэтыя прыоритэты супарадніць з адпаведным падтрымкам прыоритэтаў у BullMQ. Трафік па безкоштовнам плане завжды прызначаецца як LOW, ўсёляка не дазволяючы яму конкуруваць з трафікам за плату. Электранаўпы платформы абходзяць гэтую логіку цалкам: яны завжды дадаюцца у чергу як CRITICAL і викорыстоўваюць меншы час адкладэння виканання.

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

Распараджэнне: намераваны разлік

Сэрвіс не гарантуе адчыненне замовленняў па кожным кліянту. Канкуранцыя і прыоритэты налаштаваны для падвышэння працэздатнасці, і за дапамогою пяці паралельных працоўнікаў плюс перапрыбуткі, два засланыя ў ряду паведамленні можна лёгка доставіць у зворачным порядку. Для большасці паведамленняў гэта не шкодзіць, тым болей што яны незалежныя. Гэта стае проблемай толькі для пар, якія зв’язаны прычыно-наследку, дзе порядак мае значэнне, напрыклад, калі паведамлення должна логічна выйсць пасля іншага.

Існуюць два загальныя рашэння, кожна з яых мае свою цену:

  • Серыязаваць паведамленні па кліянтах. Направляць задачы кліента через адну траекторыю, ўнаследке чаго адчыненне замовлення гарантаванае, але цэе коштуе кліенту можлівасці паралельнай обработкі.
  • Зробіць паведамленні стойкімі да порядку. Даўаць часовыя меткі і дазволіць прыёмніку самаму адначытаць порядак, чым перакладаецца адпаведальнасць на спосаб, якім кліянты чытаюць паведамленні.

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

Можлівасць доставкі: запобежэнне абыяковасцям да і пасля адправкі

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

Да адправкі: верыфікацыя домэна

Калі кляўэнт хоча падаць паведамлення з свайго сабскрыптыўнага домэна, сервіс рэгіструе гэты домэн у выбраным кляўэнтам прадаўце і атрымвае DNS-запісы, якія падтверджаюць його прыналежнасць. Гэтыя запісы разніцаюцся залежна ад прадаўцы, таму яны паказваны кляўэнту для дадаць іх у свой DNS-хост. Падача паведамлення з гэтага домэна застаецца заблокаванай пакуль не будзе атрымана падтверджэння.

Пасля адправкі: дзеяння, вызваныя прадаўцам

Прадаўцы павядомляюць пра доставку, адкрыванне, клікі, непрынятныя паведамленні і скаргі на спам через webhooks. Сервіс зберагае гэтыя паданні і рэагуе ў залежнасці ад их типу:

  • Жорсткая непрынятнасць (адрэса не існуе або ёй стала перманентна відмова) автаматычна блакуе абоўязковага прымальніка, а пасляэтапныя спробы адправкі на гэту адрэсу таксама заблакуюцься, прычым прадаўца не будзе вызваны.
  • Скарга на спам таксама выклікае блакуванне.
  • Мягкі адрэсны збоі, такія як заповнены ляцочны ящок або тымчасова недоступная сервера, фіксуюцца толькі тады, калі проблема, як правило, ўскорай зникне.
  • Правілам є тое, што толькі сігналы, які указваюць на стойкую проблему, ведуць да блакавання. Гэта не рашае прыбутковасці доставкі повнаста, адказна репутацыя зменяецца пад вплывам фактараў, якія выходзяць за межы адной-едзінай паведамлення, але гэта не дазволяе системе раз за разам падаць на адрэсы, пра якія вядома, што яны не функцыонуюць, а гэта сама по сабе ўскладнюе стварэнне репутацыі.

    Забава аб’ектаў ад взаімнага нанесення шкоды

    Усі кліянты выкарыстоўваюць адныя і тыя ж чергі, пулы працоўнікаў і экземпляр Redis; нічога не ўскладнена фізычна. Справядлівасць забезпечваецца трыма мерамі, якія прыменяюцца на разных этапах.

    Першы бар’ер: ліміты частоты для кожнага кліента ў API

    Перш чым запит стане задам, ён праходзіць перакрантную перакантнае обмежэнне, падтрымванае Redis, на адного кліента за 60 секунд, якое залежыць ад плана:

    • FREE: 5 запитоў на хвіліну
    • INDIE: 50 запитоў на хвіліну
    • STARTUP: 200 запитоў на хвіліну

    Разыграны спад з аднаго рахунку адхоўваецца на перакранцы і ніколі не запалюе очакальнай лісты.

    Другі перакрант: прыоритэт пасля адзначэння

    Адзначаныя зады обрабоўваюцца за прыоритэтам, і зады безкоштовнага плану завжды маюць LOW прыоритэт. Рабоўнікі завжды спачатку беруць зады з вышэйшым прыоритэтам, таму нават якщо у безкоштовнага кліента ўтварыцца застой, упавядомленні платных кліяў будуць обрабоўвацца раней.

    Трэці перакрант: месячныя квоты

    У дэльтавай перспективе квоты обмежваюць колькасць. Безкоштовны план дазваляе 100 сумарных адправак электраняў і webhook-аў на месяц. Платныя планы обмежваюць толькі webhook-ы; электраняў для іх няма ліміту.

    Што ўсё ж неяўнае

    Не існуе строгай аізоляцыі рэсурсаў: няма спецыяльных працоўнікаў для кожнага кліента. Два кліенты на адным рангу прыоритэта дзеляць аднойчыны магчымасці, і ў межах гэтага рангу задачы обрабоўваюцца прыблізна за прынцыпам «хто першы, той і абрабоўваецца». Празараз на такім розмаху гэта не мяў значэння, адколі навантажэнне застаецца ў межах таго, што можа выканаць спільная інфраструктура, і жадны кліент значна не спамчвае іншага. Пры большым масштабах стане неабходным абавесці аізоляцыю кожнага кліента або справядлівы план распадзелу задач (напрыклад, за прынцыпам «рулетка» між кліентамі ў межах аднаго рангу прыоритэта).

    Перакананне у правільнасці работы webhooks у обох направленнях

    Кожны інтеграцыйны прадаўцы адправляе ўсі зместы праз webhooks: падтверджэння аддачы, запісы пра немагчымасць доставкі, адказы пра спам і змены статусу платежа. Кожны канец-пункт прыемкі ўособлівае публічнае URL. Без пераканання ў адправцы, кожны можа праказаць фальшывы змест пра „аддачу“, сфабрыкаць запіс пра немагчымасць доставкі, каб замаскуваць чужы адрес, альбо, што ўжо найбольш шкодзіце, сфабрыкаць фальшывы змест пра платеж, каб паказаць, што прадукт або падпіска заплаценыя.

    Уваходжанне дадзеных: адна схема на прадаўцу

    Не існуе аднойчынага стандарта, таму кожная інтеграцыя ўособлівае элемент абароны:

    • SendGrid падписвае дадзеныя за дапамогою ECDSA, і падпис пераканваецца працоўна з публічным ключам SendGrid перш чым дадзеныя будуць прыйнятыя як правядзібныя.
    • Resend адправляе webhooks через Svix, які выкарыстоўвае схему HMAC з пазначэнням часу, якая заходзіць ад парадкавання і повторнай адправкі дадзеных.
  • Postmark не падрэсава вебхукі, таму яго канцэнтры залежаць ад HTTP Basic Auth з секрэтом для кожнага кліента.
  • Paystack падтверджае вебхукі платэжаў за дапамою HMAC з выкарыстоўваннем SHA-512.
  • Polar падтверджае вебхукі платэжаў за дапамою бібліятэкі standardwebhooks.
  • У разе Postmark існуе лёгкая для працэйкання пастка: секрэт неабходна пораўнаваць у сталям часе, напрыклад, за дапамою crypto.timingSafeEqual у Node, а не за дапамою звычайнага пераканалення роўнасці строк. Звычайнае пераканаленне можа завершыцца як толькі з’явіцца адна-едзина разніца ў символах, і гэтыя часовыя разніцы могу дазволіць злочынніку вярнуць секрэт по аднаму символу, замест таго каб працэўваць сіловым способам над усім значэннем.

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

    Выходны трафік: стварэнне довер'я кліентам да платформы

    Той жа проблема існуе ў зворачным направленні, калі сервіс дастае webhook-ы на канцы кліентаў. Якщо кліент настроўвае секрэт для падпіса, кожная даставка включае заголовак X-Webhook-Timestamp і заголовак X-Webhook-Signature. Падпіс — гэта HMAC-SHA256, вырачаны над строкай timestamp.payload, який прыўязвае тэла пакета да конкрэтнага момента. Прыймальнік перысчытае падпіс і таксама адхоўвае тымчасовыя пазначкі, якія занадта старыя, што не дазволяе перзапускаць захопленыя запросы пазней.

    Кліянты маюць пашчотную контроль над своім секрэтным кодам: яны можаць яго стварыць, пераканаліцца ў його статусе, змяніць яго або выдаліць. Без настаўленага секрэтнага коду вебхукі все равно будуць доставляцца, проста без падпісу. эта спрыяе адваротнай сумеснасці, але таксама значыць, што прыймальнікі должны самі выбраць верыфікацыю, каб захавацца. Для дапаможнага адгледзення з боку прыймальніка, адзірніце Розумэнне ключоў ідэмпотэнцыі у канцах POST у Node.js.

    Мінімальны fallback і праверка подпісу

    deliver ідзе па правайдэрах па парадку і вяртае першага, хто прыняў паведамленне. Паўтор усёй задачы застаецца за чаргой.

    type Provider = {
      name: string;
      send: (to: string, body: string) => Promise<void>;
    };
    
    const deliver = async (providers: Provider[], to: string, body: string) => {
      const errors: string[] = [];
      for (const provider of providers) {
        try {
          await provider.send(to, body);
          return provider.name;
        } catch (err) {
          errors.push(`${provider.name}: ${(err as Error).message}`);
        }
      }
      throw new Error(`all providers failed\n${errors.join('\n')}`);
    };

    Подпіс пакрывае timestamp.payload. Атрымальнік адхіляе няправільны MAC і метку старэйшую за пяць хвілін.

    import { createHmac, timingSafeEqual } from 'node:crypto';
    
    const sign = (secret: string, timestamp: string, payload: string) =>
      createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex');
    
    const verify = (
      secret: string,
      timestamp: string,
      payload: string,
      signature: string,
    ) => {
      const ageMs = Math.abs(Date.now() - Number(timestamp));
      if (!Number.isFinite(ageMs) || ageMs > 5 * 60 * 1000) return false;
      const expected = Buffer.from(sign(secret, timestamp, payload));
      const received = Buffer.from(signature);
      if (expected.length !== received.length) return false;
      return timingSafeEqual(expected, received);
    };

    Ключовыя выводы

    • Рашыце, чы хацяце викорыстоўваць шифраванне чы хэшаванне, запыталівшыся, чы калі-небудзь трэба вярнуць первісную значэнне.
    • Расследжвайце альтернатыўныя способы вядчыка і практыкі перапрыбутку з рэя як околачныя слоі, і чыста апісайце, што значыць „спроба“.
    • Запісуйце стойкі запис у базе дадзеных пры ўключэнні ў рэй, каб перывод базы дадзеных не пазбавіў запиту.
  • Рэйт карточакоў з мертвымі паведамленнямі працуе толькі тады, калі на яго надходзяць апавешчэнняі і існуе чыста дорага для праказы.
  • Закрыць яго можна толькі па стойкіх сігналах, такіх як абмовы на розпашчэнне паведамленняў і скаргі на спам.
  • Застосаваць контролы справядлівасці (ліміты частоты, прыоритэт, квоты) і чытачна паказаць, калія ізоляцыі вы яшчэ не прыменяеце.
  • Пераканацца ў кожным уваходзячым webhook-е па схеме самога прадастальніка, выкорыстоўваць порэванні за сталяга часу, а таксама падпісваць выходзячыя webhook-е часовым пазначкам, каб запобiec ўжыванню іх знову.
  • Назваць вядомыя абмежэння, такія як порядак обробкі паведамленняў, і адклаць дорогія рашэнні, пакуль даныя не пакажуць, калія компроміс будзе наяўны.