ИТ развој

System Architecture: Embrace Emergent Behavior for Resilient Software

Системска архитектура: Прифатете го појавувачкото однесување за отпорен софтвер

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

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

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

Архитектурата е збир на интеракции, а не дијаграм на стек

Типична PHP услуга може да изгледа едноставно: HTTP барање стигнува до апликација, апликацијата повикува API, чита или запишува во база на податоци и враќа одговор. Сепак, секој чекор има сопствено време, начин на откажување и политика за повторни обиди. Додадете Docker оркестрација, кеш, работник во заднина и повеќе инстанци на апликацијата, и локалните одлуки почнуваат да влијаат на целиот систем.

На пример, клиентот истекува со време додека барањето за плаќање сè уште се обработува. Клиентот прави повторен обид. Балансер на оптоварување го испраќа повторниот обид до друг PHP контејнер. Двете барања стигнуваат до истата граница на трансакција во базата на податоци во малку различни моменти. Ако крајната точка не е идемпотентна, едно корисничко дејство може да создаде две наплати, две е-пораки или две нарачки.

Ниту една поединечна компонента не мора нужно да е расипана. Дуплираниот исход произлегува од разумно однесување на секој слој. Решението е архитектонско: направете ја операцијата безбедно повторлива и дајте му на секој слој доволно контекст за да го препознае истото логичко барање.

Дизајнирајте за безбедно повторување

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

API може да прифати клуч за идемпотентност обезбеден од повикувачот и да го зачува завршениот резултат со тој клуч. Клучот мора да биде заштитен со единствено ограничување во базата на податоци; само проверката за постоечки ред во кодот на апликацијата е ранлива на истовремени барања.

CREATE TABLE api_requests (
    id BIGINT PRIMARY KEY,
    idempotency_key VARCHAR(255) NOT NULL,
    response_code INT NULL,
    response_body JSON NULL,
    UNIQUE KEY api_requests_idempotency_key (idempotency_key)
);

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

Чувајте ги споредните ефекти зад трајна граница

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

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

Ова не е непотребна формалност. Тоа претвора несигурен временски проблем во експлицитна, проверлива машина на состојби.

Направете ги начините на откажување видливи во API договорот

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

За работа што може да биде бавна или да зависи од ненадежни услуги, вратете прифатено барање со траен идентификатор на операцијата. Дозволете им на клиентите да го проверуваат неговиот статус или да добијат повратен повик каде што тој модел е соодветен. Важната разлика е помеѓу „барањето беше примено“ и „бараниот деловен исход е завршен“.

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

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

Нека базата на податоци ги носи инваријантите

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

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

UPDATE inventory
SET available = available - 1
WHERE product_id = :product_id
  AND available > 0;

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

Контејнерите ги менуваат границите на откажување, а не самото откажување

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

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

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

Набљудувајте го однесувањето што очекувате да се појави

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

Структурираните логови со идентификатори на барања и корелација овозможуваат следење на една операција низ PHP-FPM или долготрајни работници, потрошувачи на редици и надворешни повици. Логовите без контекст стануваат збирка анегдоти; трагите без значајни имиња на операции стануваат скап лавиринт.

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

Отпорноста доаѓа од контролирани последици

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

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

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

Mihajlo

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