Таблыцы «Выходныя пакеты» і «Вхідныя пакеты»: проектаванне Webhooks, якія вытрымаюць неудачы
Дазвольце даклэ научыцца, як транзакцыйны выходны рядок, ідэмпатыўны вхідны рядок, адкладэнне з урахоўванням статусу і калонкі для мертвых лістоў забезпечваюць надзеяваную доставку webhook-аў у AWS, Azure і GCP.
Webhooks здаюцяся самай простым спосабам інтэграціі: адна сторона выканае запыт HTTP POST, а іншая яго обрабоцвае. У практыцы ж яны несу ўсія рызыкі даступнай системы, таму што сеть между двума сервісамі може паспрабаваць адхіліць запыты, завершыць ўзаемадзейство на паўпацёку, доставіць той самы пакет дадзеных два разы або пераранжаваць запуск падзеяў. Якшо ставіцца да webhook-а як да звычнага запыту CRUD, то з часам можна апынуцца без уведамленняў, два разы запускаць побачныя эфекты, і ў канцэ наступае ситуацыя, калі два системы не пагоджуюцца ў тым, што саме адбылася.
У гэтым кялічыку паспрабаванаецца разглядзець дыяграму, якая застойваеся пад такімі умовамі. Вы пазірзеце, чым вызначаецца неяксамотнае падходжэнне, як транзакцыйны выхідны бокс робіць выходзячыя webhooks надзеянымі, як ідэмпатыўны вхідны бокс робіць прыходзячыя webhooks безпечнымі для павторнага выканання, як справляцца з адбыткамі, якія прыходзяць у неправильнай парады, як кварантінацыяя навантажэння, якія ніколі не можуць завершыцца успехам, а таксама якія кераваныя службы ў AWS, Azure і Google Cloud падходзяць кожнай часткі.
Чаму звычайная рэалізацыя втрачае данні
Разглядзім SaaS-бэкенд, який керуе значымі зменамі стану, напрыклад, выпаленнем ардэру чыя падпіска стаюць актыўнымі. Партнер вызывае ваш API, каб падтвердзіць дзеянне, і тады ваша служба мае два заведамення:
- Зберагчы новы стан, напрыклад, задаючы статус аб’екта на
Active. - Паведаміць службу, якая знаходзіцца нижэй па ланцоўке, што аб’ект готавы, вышлівшы яйу webhook.
Інтуітивны код запісвае даныя ў базу дадзеных, а пасля, на наступнай лініі, выканае HTTP-запит. Цэе ўсьмо двойны запіс: два незалежных системы апдэйтуюцца адна за іншай, без жадных зв’язкаў між ямі.
З гэтага безпосередна вынікаюць два варыянты неудач:
- Процес завершваецца між двумя крокамі. База дадзеных тады паказвае, што ентыт актыўны, але запит так і не быў адправлены. Вашы записы правільныя, служба, якая працуе пасля, нічога не ведае, і ніхто не з’являецца, пакуль кліент не прасіць пра дапамогу.
- Запит адправляецца, але транзакцыя не выйшла. Служба, якая працуе пасля, апранутая про тое, што ентыт актыўны, але ваша база дадзеных відмовілася ад змян і яшчэ вважае операцыю неудачной.
Ні адаптаванне спосабу выконання, ні іншыя змены не рашуюць проблему. Якщо запускаць HTTP-запыт першым, вы павінны страждзець ад другой нявыпадковасці; якщо — пасляльнім, — ад першай. Асалёвая прычына заключаецца у тым, што апвярджэнне дадзеных у базе і сетевы запыт не можна выконваць атомічна разам, таму будзь-яя нявыпадковасць у промежутку заставляе обе стороны застацца несінхроннымі.
Надзеяна апрабоўка webhooks за дапамогою транзакцыйнага механізма
Модель outbox усунеяе двойную запісь, так как або ўзагалі не выконваецца HTTP-запыт з маршруту запиту. У замене намер апрабавкі webhook стае дадзенням, якое запісваецца ў той жа транзакцыйны момент, калі выконваецца змена ў бізнес-системе. Або якія-небудзь з эўрабоўкаў будуць апвярджаны, або ніхто.
Чатыры крокі процесу outbox
- Ачыніце транзакцыю. Бізнес-операцыя запускае звычайную транзакцыю ў базе дадзеных.
entities (напрыклад, ставячы статус у Active) і дадае рэкорд у outbox_events, які мае точна інфармацыя, яку павінен прымець наступны сервіс.outbox, якія ўсё ўтолькі не былі адправлены. Для кожнага з іх ён выканае запыт HTTP POST, а пасля пазначае рэкорд як оброблены.Таблица outbox
У наведанай выкладцы зберагаецца по аднам рэядам на кожную чакаючую паведамленне. aggregate_type і aggregate_id паказваюць, пры яком бізнес-об’екте відбуваецца падыянне, event_type называе тое, што сталася, payload містыць тэкст, який трэба дасты, а processed_at застаецца порожнім пакуль релея не падтвердзіць доставку. Запыт на рэяды, дзе processed_at равна null, дае релею яго список задаў. Заўважыце, што внутрашнія каментары выкарыстоўваюць адзін галочку; у PostgreSQL для каментараў патрэбны две (--), таму трэба гэта пасправіць перад запускам запыту.
CREATE TABLE outbox_events (
id UUID PRIMARY KEY,
aggregate_type VARCHAR(50), - e.g., 'Order' or 'User'
aggregate_id UUID, - e.g., Entity ID
event_type VARCHAR(100), - e.g., 'order.activated'
payload JSONB NOT NULL, - The exact webhook payload
created_at TIMESTAMP DEFAULT NOW(),
processed_at TIMESTAMP - Null until successfully sent
);
Што гарантуюе выкладка з паведамленнямі, і чаго не гарантуюе
Якщо сервер зупніўся пасля адправкі, нічага не згубяецца: рэкорд застаецца ў таблыце, і релея знайдзе яго пад час наступнага перагляду. Якщо кінцевы пункт не доступны, релея проста спробуе знову, жадаўна з экспансійным адкладэнням, каб не навантажваць прыемнік, які мае труднасці. Змяны стану і намер паведаміць больш не можаюць розходзіцца.
Адзін з недагодзей — адазважанне, якое вырабляецца тады, калі доставка вяршыцца жоўткі раз. Релею можа ўспэшна адправіць запит, а пасля зламацца прычынай таго, што не падканае значэнне processed_at; у такім случыку той жа запуск знову адправляе тое ж заданне. Гэта прыемна толькі тады, калі прымучыя элементы адсортаваюць дублікаты, што самэўсёльва робіцца за дапамогою моделі папкі для спрамоў з іншай стороны. Уключэнне значэння id рядка з папкі для адправы ў навантажэнне або у загалоўку дае прымучыям стабільны ключ для адсортавання дублікатоў. Якщо вы запускаеце калькі рэлейных экземпляраў, будзьце вярны таму, што два працоўнікі не могу прызначыць сабе той самы рядок адночасова; у PostgreSQL стандартны спосаб для гэтага — выбіранне рядкаў за дапамогою параметра FOR UPDATE SKIP LOCKED.
Безпечнае прыманне вебхуков за дапамогою ідэмпатнай папкі для спрамоў
Тепер пераключымся на вебхукі, якія ваша служба прымае ад партнероў чы верхняярэчных систем.
Падзейце, калі ваш адрабатчык выкарыстоўвае пяць секунд, таму што выканае складныы вычысленні чыста чакае на блокаванне, якое трывае ў іншай службе. Кліент HTTP апаведчальніка можа здарыцца здацца раней, чым вы адпавісіце, і заключыць, што вы ніколі не прымелі заўважэння, і апавесціць яго знову. Тады тое ж заўважэння прыходзіць два разы. Якщо ваш адрабатчык з кожнага запуску вышле электронную пашту чыста створыць запис, кліент атрымае два пісьмы, а вы — дублікат рэкорда.
Шаблон папкі для пашты раздзеляе прыему webhook-а і дзействія з яго.
Чатыры крокі прабегу ў папцы для пашты
- Прымкі і пераканацца. Як толькі прыходзіць запит, пераканацца ў його падпісе HMAC, каб з’ясаваць, чы ён справды прыйшоў ад партнера і не быў сфабрыкаваны чыста зменены.
webhook_inbox, параграфавануюя яе унікальным ідэнтыфікатаром падзеi партнера і захоўваючы яе за дапамой абмежэння ўнікальнасці базы дадзеных.200 OK, прычым раней, чым запускаецца які-леба бізнес-логіка.Таблыца inbox
У гэтай структуры кожны рядок фіксуе таго, хто адправіў запуск (partner_name), ідэнтыфікатор адправальніка (partner_event_id), даныя, якія адправляюцца, чый падпис быў перакананы, а таксама status, які можа быць PENDING, PROCESSED або QUARANTINED. Важлівайшым элементам є комплексная абмежэння UNIQUE(partner_name, partner_event_id): саме яна ператварае дуплікаты на безнэпасныя, неактывацыйныя запускі. Каляк і ў таблы з выходными запускамі, коментары, напісаныя з апострабою, павінны стаць --, каб PostgreSQL могла прыняць запіс.
CREATE TABLE webhook_inbox (
id UUID PRIMARY KEY,
partner_name VARCHAR(50), - e.g., 'Stripe' or 'GitHub'
partner_event_id VARCHAR(100), - The unique ID from the sender
payload JSONB NOT NULL,
signature_verified BOOLEAN,
status VARCHAR(20), - 'PENDING', 'PROCESSED', 'QUARANTINED'
received_at TIMESTAMP DEFAULT NOW(),
processed_at TIMESTAMP,
UNIQUE(partner_name, partner_event_id) - Prevents duplicate inserts
);
Чаму абмежэння виконвае галоўную роботу
Пакалькі, што працоўнік толькі пераканальвае, дадае і вяртае даны, ён рэагуе быстра, і адправальнік рэдка калі страждае на працяг чакання. Калі адправальнік зноў прабуе, нават дзесяць разоў паспяльна, абмежэнне ўнікальнасці дазволяе, каб успялася толькі адна практыка дадзення. Ваш працоўнік павінен спрыймать выйшлую памылку нарушэння унікальнасці (או рэзултат ON CONFLICT DO NOTHING) як успех і ўсё раве вярнуць 200 OK; інакш адправальнік будзе продаваць прабавы ўрэгуляваць запіс, які вялікі ўжо існуе. Пакалькі існуе толькі адна строка, працоўнік выканае побачныя эфекты толькі аднажды.
Есць два моменты, якія варта правільна адрабатаваць. Першы — усуненне дуплікатаў залежыць ад таго, чы рэпартнер будзе суплікаваць стабільны ідэнтыфікатор запуску; большасць прадаўцаў webhook-аў включае такі ідэнтыфікатор, але трэба паказвацца ў кожнай інтеграцыі. Другі — працоўнік можа зламацца пасля выкарыстоўвання побачных эфектаў, але перш чы зазначыць, што рэкорд адобрабаваны, таму, дзе толькі можна, трэба выкарыстоўваць змены ў бізнес-логіцы і апдэйты статусу ў одной транзакцыі, а таксама робіць пабочныя эфекты зовнішняго характеру ідэмпотентнымі. Чыба прачытаць болей дакладна аб усуненні дуплікатаў запытоў за дапамогою ключоў, адвярніцеся да ключоў ідэмпотентнасці ў POST-канцах Node.js.
Адрабатаванне запускаў, якія прыходзяць не па порядку
Нават калі дублікаты контролююцца, няма гарантii, што запісі будуць прыходзіць у той порядку, у яком былі створены. Ваша служба можа прымець entity.completed раней, чым entity.started. Адпрацоўчык, який слепа прыменяе кожны запіс, пасля гэтага пакушацца перавести ентыт з стану draft безпосередна ў стан completed, чыяму або будзе зіпсаваны яго стан, або выйдзе памылка на кшталт 409 Conflict.
Перакананне кожнай пераходу па машыне станоў
Рэшэнь — перастаць спрыятаць запісам як командам на змěну стану і начаць спрыятаць іх як прыпускаемымі пераходамі, якія павінны быць падтверджаны. Гэта інодзе называецца механізам разыявлення станоў, у духу метода event sourcing: працоўнік поручывае прыходзячы запіс з нынешнім станам ентыту і вяршыць, чы рэалізацыя пераходу є дазволена.
Ніжныя падаюць зображэння паказуюць гэтае рашэнне. Якщо падае запускаючыя падзея, калі об’ект яшчэ ў стадіі чернавіка, праблемная умова яшчэ не выпалася, таму функцыя адзвяртаеся з падзеёй як з відкладанай, у замест на ёе выкарыстоўванне. У прымэтках наводзяцца два спосабы распрацоўкі відкладання: залишыць рядок у папцы з непрацаванымі задачамі і спробаваць знову пазней, альбо зафіксаваць прыбліжны стан і чакаць на нехватную падзею. Падзея запуску для об’екта у стадіі чернавіка ўважаецца правямым пераходам і выконваецца. Спрыйміце гэта як псевдакод: return status: 'DEFERRED'; — гэта не правямы JavaScript, і яго трэба пісаць як return { status: 'DEFERRED' };, а рэальная реалізацыя таксама будзе распрацоўвала засталыя комбінаціі падзеі і стану.
function processWebhookEvent(event, currentEntityState) {
if (event.type === 'entity.completed' && currentEntityState === 'draft') {
// The 'started' event hasn't arrived yet!
// We cannot transition from 'draft' directly to 'completed'.
// Option A: Leave it in the inbox and retry in 5 minutes.
// Option B: Store a "Projected State" and wait for the missing piece.
return status: 'DEFERRED';
}
if (event.type === 'entity.started' && currentEntityState === 'draft') {
return transitionTo('started');
}
}
Відкладанне як самовылечваючыяся цикл
З’явіцца замовлення, у якому падеж „shipped“ будзе прыбыць раней, чым падеж „paid“. Якшо негайна аплікацыя падежу „shipped“ прыведзе замовлення у стан, які ваша модель не дазволяе. За дапамою працэйніка, який ведае станы, последовасць стане такой:
- Прыбывае падеж „Shipped“, алгорытм бачыць, што платежа няма, і падэж адкладаецца.
- Прыбывае падеж „Paid“, ён валідны, і замовленне апдэйтуецца.
- Адкладзены падеж „shipped“ спробуецца зноў, тепер ён бачыць, што його прабалансаванне выпанавана, і ён прыменяецца.
Задзейнаваныя зачыны можаць знаходзіцца ў спецыяльнай адрабоцкай лісты, напрыклад, Amazon SQS або лісты на базе Redis, і фонавы працоўнік періядычна прабуе ўжо раз запрацаваць іх. У рэзультате стварываецца процес, які адхоўчае некоректныя переходы, але ў канцэ настаёна да правільнага стану, не прыменшаючы колькасці зачынаў. Трэба ж адначасова встановіць меры ўскоранення таго, як дуго зачын можа застацца у стане задзейнавання: якщо неабходныя умовы так і не будуць выпанены, зачын у канцэ павінен быць прызначаны як неудача, а не прабываць у процесе адрабоцкіх практык без канца, што прыводзіць да наступнага раздзела.
Ізоляцыя проблемных зачынаў за дапамогою адрабоцкіх практык і лісты з некоректнымі зачынамі
Дзеянняякі ніколі не будуць выкарыстоўваны, незалежна ад таго, як часта вы іх перапрыямляеце: некоректны пакет дадзеных або апыл да ID, якога няма ў вашай базе дадзеных. Їх называюць «трыючыя таблеткі». Неаспакоўваны працоўнік будзе іх перапрыямляць без канца, і якщо черга обрабоўваецца па порядку, адна неякісная паведамленне можа заблокаваць усі правільныя дзеяння, якія знаходзяцца пасля ёй.
Стандартны спосаб захавання — це політыка перапрыямлення з абмежаннем і падыходжучымі затрымкамі, за якою следуе черга для необрабоўваных паведамленняў (DLQ). Тыповы графік выглядае так:
- Першая спроба не ўспехоўвае; чакайце адзін мінут.
- Другая спроба не ўспехоўвае; чакайце пяць мінут.
- Трэцяя спроба не ўспехоўвае; чакайце падзесят мінут.
- Чатвёрта спроба не ўспехоўвае; перакладзіце дзеяння ў DLQ.
DLQ можа быть таблой у вашай сябе базе дадзеных або функцыяю кераванайчага зьборкі. Галоўнае — што будзе далей: запісы з DLQ павінны апыляцца на внутранім адміністрацыйскім апвізе і спрычыняць апавешчэнне высокага прыорітету, адколі кожны з іх прадставляе данні, якія ваша система не змогла обрабаваць. Інжынер дакладва аналіз, вылечвае бяг у супарабатцы або некалькісныя данні, а пасля перазапускае запіс, каб той працаваў па звычнаму маршруту обрабаткі. Такую дзеяньне перазапуску трэба стварыць якомога раней; без яго вярненне да нормальнага стану пасля прыемкі запіса з DLQ ператвараецца на ручную правку базы дадзеных пад тыччую.
Супарабатка дизайну з AWS, Azure і Google Cloud
Корзіны для выдачы і прыемкі данней знаходзяцца ў вашай реляцыйной базе дадзеных, але супаўязаныя элементы (прыемка данней, зьборкі, працоўнікі, DLQ-ы) чыста падходзяць для кераваных хмарных служб, што скарочвае значную частку аперацыйнага навантажэння. Структура залишаецца адной і той жа у всіх прадастальніках; зменяюцься толькі назвы продуктав.
AWS
- Вхідныя даны: Amazon API Gateway прымае вхідныя webhooks, пры чым апарат для автарызаціі Lambda пераканальвае падпис HMAC прытаму, прычым запит не дасягае бэкенду.
- База дадзейнаў: Amazon Aurora PostgreSQL зберагае бізнес-табелі, а таксама
webhook_inboxіoutbox_events, таму дыяжнаюць гарантіі транзакцый. - Чэргі і DLQ: Стандартная чэрга SQS адпрацоўвае задачы асінхронна, а настроеныя чэргі для мертвых лістоў SQS прымаюць паведамленні, калі ўжо пераканальваецца максымальная колькасць ўсвайомленых паведамленняў. Стандартныя чэргі забезпечваюць прынятнае адно разу выкананне задач, але не зберагаюць порядак, што ўжо адной з прычын, чаму важная ідэмпотентнасць і пераканальвання статусу.
processed_at.Azure
- Прыём дадзеных: Azure API Management прымае webhooks, перакантролюе падписі і перадае запыткі да бэкенду.
- База дадзеных: Azure Database for PostgreSQL Flexible Server храніць стан прыкладнай програмы, а таксама табелі для вхідных і выходных дадзеных.
- Чаргі та DLQ: Azure Service Bus координуе перадачу паведамленняў і мае вбудованую систему обработкі непрацэсаваных паведамленняў, якая автаматычна перакладае паведамленне у спецыяльную групу пасля настаўленага колькісця спроб ўпрымкнення.
- Рабочыя процесы: Azure Functions з трыгерамі Service Bus обрабоцвают пакеты дадзейна з папкі вхідных паведамленняў. Процес выдачы паведамленняў працюе як фоновы цикл у Azure Container Apps, або як Kubernetes CronJob, якщо вы викорыстоўваеце AKS; ён запытвае PostgreSQL аб непадарожаных паведамленнях і доставляе іх через HTTP.
Google Cloud
- Вхідныя паведамленні: Google Cloud API Gateway керуе прыходзячыми HTTP-вебхукамі та процесамі аутантыкацыі.
- База дадзейнаў: Cloud SQL для PostgreSQL зберагае реляцыйныя даны, уключаючы обе табліцы.
- Чаргі та DLQ: Pub/Sub асінхронна направляе паведамленні. Галоўная падпіска обрабоцвае паведамленні, а тэма dead letter збірае тыя паведамленні, якія так і не былі падтверджаны пасля настаўленага максимальнага колькісця спроб доставкі.
- Рабочыя элементы: Сэрвісы Cloud Run, якія можаць зменшыцца да нуля экземпляраў між спалахамі, прымоць падачу дадзенняў через Pub/Sub, каб обрабацаваць паведамленні. Рэлей для выходных дадзенняў — это або заданне Cloud Run, або сэрвіс Cloud Run, які запускаецца за раскладам па меры працы Cloud Scheduler, які пераглядае дадзеныя з Cloud SQL і адправляе чакаючыя запісы.
Для адносна большых прыкладаў способаў з’яеднання сэрвісаў, такіх як OAuth і надзеяныя вызовы API, адзірніце шасць шаблонаў інтэграцыі для надзеяного з’яеднання сэрвісаў Node.js.
Ключовыя выводы
- Надзеяная система webhook — это канал обработкі запісаў, а не пара HTTP-канцэнтраў.
- Ніколы не адчыняйце базу дадзенняў і не вызывайце аддалённы сэрвіс як два незалежныя кроки; запішыце рядок у выходную частку базы дадзенняў у той жа транзакцыі і дазвольце рэлею адправіць яго.