ИТ развој

Beyond the Framework: Architecting API Resilience From the Ground Up

Надвор од рамката: Архитектирање на отпорноста на API од темел нагоре

Повеќето неуспеси на API не се предизвикани од недостиг на функционалност во рамката. Тие произлегуваат од претпоставки: дека зависностите ќе одговорат, клиентите ќе се однесуваат правилно, повторните обиди ќе помогнат, базите на податоци ќе останат достапни и распоредувањата ќе поминат без проблеми. Рамката може да направи една крајна точка да биде пријатна за изработка. Отпорноста бара одлуки што се протегаат далеку надвор од контролерот.

За PHP backend тимовите, таа разлика е важна. Чиста дефиниција на рута и уреден service container се корисни, но не одговараат на потешките прашања: што се случува кога давател на услуги за плаќање ќе истече по обработката на барање? Што се случува кога клиент ќе повтори барање за запишување? Што се случува кога пулот за конекции со базата на податоци ќе се исцрпи на средина од нагол пораст на сообраќајот?

Отпорните API се дизајнираат околу тие непријатни патеки уште од почетокот.

Почнете со границите на неуспехот, а не со крајните точки

Пред да изберете middleware или да пишувате правила за валидација, мапирајте ги зависностите зад секоја операција. Крајна точка што чита еден индексиран ред од база на податоци има многу поинаков профил на ризик од онаа што запишува во повеќе табели, емитува настан, повикува надворешна услуга и враќа потврда.

За секоја зависност, дефинирајте три работи: временското ограничување, резервното однесување и одговорноста за опоравување. Барањето не треба да чека неограничено затоа што надворешната услуга е бавна. Ниту секој слој треба независно да се обидува повторно; насобраните повторни обиди можат краткото забавување да го претворат во прекин предизвикан од самиот систем.

Практично правило е на секое барање да му доделите временски буџет. Ако клиентот може да чека пет секунди, намерно поделете го тој буџет меѓу работата со базата на податоци, повиците кон зависни услуги и мала резерва за серијализација и мрежно доцнење. Кога зависноста не може да заврши во рамките на својот дел, неуспехот нека биде предвидлив.

$response = $httpClient->request('POST', $url, [
    'timeout' => 2.0,
    'json' => $payload,
]);

if ($response->getStatusCode() >= 500) {
    throw new UpstreamUnavailableException();
}

Точната библиотека за клиентот не е суштината. Важниот дел е дека постои временско ограничување и дека апликацијата го преведува неуспехот на зависноста во значаен исход. Клиентот може да добие серверска грешка за која може да се обиде повторно, одговор дека барањето е ставено во редица или јасно одбивање. Не треба да добие случајна генеричка грешка по долго, необјаснето чекање.

Направете ги операциите за запишување безбедни за повторување

Мрежите се доволно ненадежни за клиентите да се обидуваат повторно. Мобилните врски прекинуваат. Балансерите на оптоварување може да изгубат одговор откако апликацијата веќе ја завршила работата. Корисник може двапати да кликне на копче. Ако POST крајна точка создава нарачка, наплатува картичка или резервира залиха, „клиентот не треба да се обидува повторно“ не е стратегија за отпорност.

Користете идемпотентност за надворешно видливите операции за запишување. Клиентот доставува единствен клуч за идемпотентност, а серверот ги евидентира и клучот и конечниот резултат. Повтореното барање со истиот клуч го враќа оригиналниот исход наместо повторно да ја изврши дејството.

Записот мора да се создаде атомски со деловната операција каде што е можно. Во спротивно, две истовремени барања може и двете да утврдат дека клучот недостига и секое да го изврши запишувањето. Ограничување за единственост во базата на податоци е посигурно од проверка на постоење само на ниво на апликација.

CREATE TABLE idempotency_keys (
    key_value VARCHAR(255) PRIMARY KEY,
    response_status INTEGER NOT NULL,
    response_body TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL
);

Идемпотентноста исто така наметнува корисни одлуки за производот. Колку долго треба да се чуваат клучевите? Што треба да се случи ако истиот клуч пристигне со различен payload? Обично, одбивањето на таквото несовпаѓање е побезбедно од тивко враќање резултат за неповрзано барање.

Користете ја базата на податоци како алатка за конзистентност

Backend кодот треба да ги изразува деловните правила, но базата на податоци треба да ги спроведува инваријантите што никогаш не смеат да бидат прекршени. Ограничувањата за единственост, странските клучеви, ограничувањата за проверка каде што е соодветно и трансакциите не се остатоци од постара архитектура. Тие се последната сигурна линија на одбрана кога истовремени барања или workers ќе се судрат.

Вообичаена замка е координирањето запишување во база на податоци и објавување настан во едно барање. Ако трансакцијата со базата на податоци се потврди, но објавувањето не успее, системот се променил без да ги извести зависните потрошувачи. Ако објавувањето успее, но трансакцијата се поништи, потрошувачите дознаваат за нешто што никогаш не се случило.

Шаблонот outbox го решава ова со зачувување на деловната промена и необјавен настан во истата трансакција. Посебен worker ги чита и објавува записите од outbox што чекаат. Тој worker мора да толерира двојна испорака, бидејќи пад по објавувањето, но пред означувањето на настанот како завршен, може да предизвика повторно испраќање. Затоа, и потрошувачите треба да имаат идемпотентно постапување.

Ова не е непотребна формалност. Тоа е експлицитен избор да се зачува исправноста кога работата преминува преку граници меѓу процеси или услуги.

Одвојте ги синхроните ветувања од работата во заднина

API барањето треба да ја заврши само работата потребна за на повикувачот да му даде вистинит одговор. Испраќањето е-пошта, генерирањето извози, повторното пресметување индекси за пребарување и известувањето на неколку надворешни системи често е подобро да се обработуваат асинхроно.

Редиците ја подобруваат одзивноста, но не го отстрануваат неуспехот. Задачите имаат потреба од ограничени повторни обиди, корисно известување за грешки и одредиште за работа што не може да успее по разумен број обиди. Политиката за повторни обиди треба да разликува привремени состојби од трајни. Повторен обид со недостапна услуга може да биде разумен; повторувањето на невалиден влез обично е залудно.

  • Вклучете стабилен идентификатор на задачата во логовите и одговорите за грешки каде што е соодветно.
  • Поставете максимален број обиди и намерно одредете го доцнењето меѓу обидите.
  • Осигурете се дека workers можат безбедно да обработат задача повеќе од еднаш.
  • Набљудувајте ја староста на редицата, не само нејзината должина; старата работа често е позначаен сигнал.
  • Обезбедете оперативен начин за прегледување и повторно пуштање неуспешни задачи.

Дизајнирајте ги грешките како дел од договорот

Клиентите не можат да изградат сигурно однесување околу нејасни неуспеси. Дефинирајте конзистентна форма на грешка, внимателно користете ги HTTP статусните кодови и вклучете стабилен машински читлив код за грешка. Пораката може да му помогне на човек, но кодот е она според кое клиентот треба да постапува.

{
  "error": {
    "code": "inventory_unavailable",
    "message": "Бараната количина моментално не е достапна."
  }
}

Не изложувајте stack traces, SQL изјави, ингеренции или внатрешна топологија. Евидентирајте ги тие детали интерно со идентификатор на барањето или корелациски идентификатор. Истиот идентификатор може да му се врати на повикувачот како референца за поддршка, без внатрешната дијагностика да се претвори во функционалност на API.

Направете го распоредувањето прашање на отпорност

Многу неуспеси во продукција се воведуваат при инаку рутински изданија. Промените на шемата и изданијата на апликацијата мора да бидат компатибилни за време на преминот, особено кога повеќе инстанци на апликацијата накратко може да извршуваат различни верзии.

Претпочитајте миграции expand-and-contract: прво додајте nullable колона или нова табела, распоредете код што може да работи со двете форми, пополнете ги податоците ако е потребно, а потоа отстранете ја старата патека во подоцнежно издание. Избегнувајте распоредување код што претпоставува дека миграцијата е завршена додека старите инстанци сè уште зависат од претходната шема.

Docker помага апликацијата да се пакува конзистентно, но контејнерот не е автоматски здрав само затоа што неговиот процес се стартувал. Подготвеноста треба да одразува дали апликацијата може безбедно да прифаќа сообраќај. Живоста треба да одговори на потесно прашање: дали процесот треба да се замени. Мешањето на двете може да предизвика здравите, но чекачки инстанци постојано да се рестартираат при прекин на надворешна услуга.

Отпорноста е архитектонска навика

Најсилните API не се оние што ветуваат дека никогаш нема да откажат. Тие се оние што откажуваат во познати граници, го зачувуваат интегритетот на податоците, јасно комуницираат и се опоравуваат без да бараат херојство.

Рамките остануваат вредни забрзувачи, но тие не се архитектурата. Архитектурата се наоѓа во временските ограничувања, трансакциите, клучевите за идемпотентност, семантиката на редиците, стратегијата за миграции, набљудливоста и одлуките што се носат кога сè не оди според планот. Вградете ги тие одлуки во системот рано, и API ќе стане полесен за менување токму затоа што е потешко да се скрши.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.