Fəsil 11 · 9 dəq oxu

Reliability pattern-ləri: failure-u dizayn etmək

Reliability pattern-ləri: failure-u dizayn etmək

Ch07 "sistem nə qədər etibarlı olmalıdır" sualına cavab verdi: SLO-lar, , . Bu fəsil daha aşağı səviyyəli, amma gündəlik sualla məşğul olur: dependency xarab olanda sizin kodunuz nə edir? daxilində funksiya çağırışı ya işləyir, ya da exception atır. Şəbəkə üzərindən edilən çağırışın üçüncü, ən pis variantı da var: cavab gəlmir və siz bunun səbəbini bilmirsiniz.

Bu fəsil istisna kimi yox, normal iş rejimi kimi qəbul edir və ona qarşı istifadə olunan pattern-ləri izah edir. Ch02-nin dili ilə desək, bunların hamısı reliability və üçün tactic-lərdir. Hər birinin isə səhv konfiqurasiya olunanda özünəməxsus failure-u var.

Partial failure normal vəziyyətdir

1990-cı illərdə Sun Microsystems-də "distributed computing-in yanılmaları" adlı siyahı tərtib olunub: proqramçıların şəbəkə haqqında fərz etdiyi, amma doğru olmayan şeylər. Səkkiz bənddən üçü bu fəsil üçün xüsusilə vacibdir:

  • Şəbəkə etibarlıdır. Deyil. Paket-lər itir, connection-lar qırılır, instance-ı rotasiyadan çıxarır.
  • sıfırdır. Hər çağırış vaxt aparır və bu vaxt sabit deyil. Orta 20 ms olan service-in 2 saniyə ola bilər.
  • Topology dəyişmir. Instance-lar hər , hər autoscaling hadisəsində dəyişir.

Buradan çıxan nəticə: distributed sistemdə nəsə həmişə xarabdır. Sual bütün sistemin işləyib-işləməməsi deyil, hansı hissənin hazırda pis olması və bunun yayılıb-yayılmamasıdır.

Ən təhlükəli failure komponentin ölməsi yox, yavaşlamasıdır. Ölü service dərhal "connection refused" qaytarır və çağıran tərəf başqa yol seçə bilir. Yavaş service isə thread-ləri, connection-ları və memory-ni tutub saxlayır. Onu çağıran service cavab gözləyərkən özü də yavaşlayır və beləcə cascading failure zəncir boyunca yuxarı qalxır. Yavaş, ölüdən pisdir. Bu fəsildəki pattern-lərin çoxu yavaşlığı sürətli və idarə olunan failure-a çevirmək üçündür.

Timeout: gözləməyin sərhədi

Hər şəbəkə çağırışının olmalıdır. Bu, istisnasız qaydadır. Problem ondadır ki, bir çox kitabxananın default-u ya çox böyükdür, ya da ümumiyyətlə yoxdur: bəzi HTTP client-lər və database driver-ləri default olaraq sonsuz gözləyir.

təxminlə yox, dependency-nin real paylanmasına baxaraq seçmək lazımdır. Orta aldadıcıdır, , yəni və ya əsaslanmalıdır. Dependency-nin 300 ms-dirsə, 400–500 ms-lik normal request-lərin demək olar ki, hamısını keçirir və anormal olanları kəsir.

İkinci qayda: timeout-lar zəncir boyunca uyğunlaşdırılmalıdır. İstifadəçi 2 saniyədən sonra imtina edirsə, arxada 10 saniyə işləyən request boş yerə resurs yandırır. Bunun həlli deadline propagation-dır: ilk nöqtədə ümumi büdcə təyin olunur (məsələn, 2 saniyə) və hər növbəti çağırışa qalan vaxt ötürülür. bunu hazır verir, HTTP-də isə header ilə edilir. Büdcə bitibsə, service işə başlamadan imtina edir.

: çox qısa sağlam, amma bir az yavaş cavabları failure-a çevirir və lazımsız retry-lar yaradır. Çox uzun isə yavaşlığın yayılmasına imkan verir.

Retry, backoff və jitter

keçici xətalara qarşı ən sadə müdafiədir: connection qırıldı, instance restart oldu, leader seçimi gedir. Amma yalnız iki şərt ödənəndə təhlükəsizdir:

  1. Xəta keçicidir. 503, connection reset, keçici ola bilər. 400, 401 və validation xətaları isə yox: onları təkrarlamaq eyni nəticəni verəcək.
  2. Əməliyyat . alan ödəniş request-i server-də artıq icra olunmuş ola bilər. olmadan onu təkrarlamaq müştəridən iki dəfə pul çıxmaq deməkdir ( API dizaynı fəslində baxırıq).

dərhal, ardıcıl etmək xarab service-ə ən çətin anında daha çox yük gətirir. Ona görə istifadə olunur: hər cəhddən əvvəl gözləmə müddəti ikiqat artır (100 ms, 200 ms, 400 ms...). Amma min client eyni anda xəta alıbsa, eyni anda da edəcək. , yəni gözləmə müddətinə təsadüfi komponent əlavə etmək, bu sinxron dalğaları zamana yayır. yarımçıq həlldir.

Ən gizli təhlükə qatlar üzrə vurulmasıdır:

Request client → → service A → service B yolu ilə gedir. Hər qat xəta alanda 3 cəhd edir (1 əsas + 2 ). Service B yıxılanda A ona 3 dəfə müraciət edir. Gateway A-ya 3 dəfə müraciət edir və hər biri B-yə 3 çağırış yaradır. Client də gateway-ə 3 dəfə müraciət edir. Nəticədə bir istifadəçi hərəkəti B-yə 3 × 3 × 3 = 27 request göndərir. Saniyədə 1 000 request alan B indi 27 000 request alır, özü də məhz bərpa olunmağa çalışdığı anda. Bu, storm-dur və uzadan klassik mexanizmdir.

Həll yolu yalnız bir qatda etmək (adətən xətanın mənbəyinə ən yaxın olanda), digər qatlarda isə xətanı dərhal yuxarı ötürməkdir. Əlavə qoruma budget-dir: client sayını ümumi request-lərin müəyyən faizi ilə (məsələn, 10%) məhdudlaşdırır. Dependency sağlamdırsa, nadir retry-lar sərbəst keçir; hamı xəta alırsa, budget tez bitir.

Circuit breaker və bulkhead

"bir də cəhd et" deyir. Bəzən düzgün cavab "cəhd etmə" olur. dependency-yə edilən çağırışları izləyir və üç vəziyyətdən birində olur:

  • Closed: normal rejim. Çağırışlar keçir, xətalar sayılır.
  • Open: xəta faizi həddi keçib (məsələn, son 10 saniyədə 50%). Çağırışlar dependency-yə ümumiyyətlə getmir, dərhal xəta qaytarılır.
  • Half-open: müəyyən vaxtdan sonra az sayda sınaq request-i buraxılır. Uğurlu olsalar, breaker bağlanır, olmasalar, yenidən açılır.

əsas faydası fail fast-dır: çağıran service gözləmək əvəzinə millisaniyələr içində cavab alır və resurslarını tutub saxlamır. Amma açıq breaker yalnız ilə birlikdə həqiqətən faydalıdır: xəta əvəzinə nə qaytaracağınızı bilməlisiniz. breaker sadəcə daha sürətli xətadır.

: hədlər səhv seçiləndə breaker ya heç vaxt açılmır, ya da kiçik dalğalanmada açılıb sağlam dependency-ni kəsir. Az traffic olan endpoint-lərdə statistika etibarsızdır: 2 request-dən biri xəta verdisə, bu artıq 50%-dir.

gəmi dizaynından gəlir: gövdə su keçirməyən bölmələrə bölünür ki, bir deşik bütün gəmini batırmasın. Software-də bu, resurs izolyasiyası deməkdir. Bütün dependency-lər bir ortaq thread pool və ya connection pool istifadə edirsə, yavaş recommendation service bütün thread-ləri tutur və checkout da cavab verə bilmir. Hər dependency-yə ayrıca pool və ya concurrency limiti versəniz, recommendation-lar üçün ayrılmış 20 thread dolanda yalnız recommendation-lar xəta verir.

qiyməti səmərəlilikdir: bir bölməyə ayrılmış resurs boş qalanda başqası onu istifadə edə bilmir. Böyük miqyasda eyni ideya ch07-dəki cell-lərdir.

Yük altında: rate limiting, load shedding və graceful degradation

client-in müəyyən müddətdə göndərə biləcəyi request sayını məhdudlaşdırır. Onun iki rolu var: reliability (bir client-in və ya səhv yazılmış cron job-un hamının resursunu yeməsinin qarşısını almaq) və abuse-dan qorunma (brute force, scraping, DoS). Ən çox yayılmış iki alqoritm:

  • : bucket sabit sürətlə token-larla dolur, hər request bir xərcləyir. Qısa burst-lara icazə verir, orta sürəti məhdudlaşdırır.
  • : request-lər queue-ya girir və sabit sürətlə emal olunur. Çıxışı hamarlayır, burst-lara icazə vermir.

Limit aşılanda düzgün cavab 429 Too Many Requests status-u və Retry-After header-idir. Bu, təsadüfi idarə olunan gözləməyə çevirir.

client səviyyəsində işləyir. isə server-in özünü qorumasıdır: ümumi yük tutumu aşanda service bəzi request-ləri dərhal rədd edir ki, qalanlarına normal xidmət göstərə bilsin. Bütün request-lərə yavaş cavab vermək 80%-ə vaxtında cavab verib 20%-i rədd etməkdən pisdir. Əsas sual nəyi atmaqdır. Checkout request-ləri analytics event-lərindən, istifadəçi request-ləri isə background job-lardan əvvəl qorunmalıdır.

dependency xarab olanda funksionallığın bir hissəsini itirib əsasını saxlamaq deməkdir. Tipik :

  • köhnə data ("qiymətlər 5 dəqiqə əvvəlki vəziyyətə görədir");
  • default cavab (personal tövsiyələr əvəzinə populyar məhsullar);
  • feature-u gizlətmək (rəylər bloku sadəcə görünmür).

Ch07-də dediyimiz kimi, tövsiyələr bloku xarab olanda səhifə onsuz render olunursa, onun hədəfi checkout-unkundan aşağı ola bilər. Amma özü də kod yoludur: nadir işləyir, nadir test olunur və ən lazım olan anda səhv çıxa bilər. Onu chaos testləri və ya vasitəsilə müntəzəm işə salmaq lazımdır. başqa bir zəif dependency-yə müraciət edirsə, o deyil, ikinci failure nöqtəsidir.

Queue-lar: backpressure və dead letter queue

Asinxron sistemlərdə failure fərqli görünür. Consumer yavaşlayanda xəta yox, böyüyən queue görürsünüz. Sonsuz queue problemi gizlədir: memory dolana qədər hər şey qaydasında görünür, sonra isə saatlarla ölçülür. aşağı axının yuxarı axına "yavaşla" deməsidir: bounded queue dolanda producer bloklanır, xəta alır və ya sürətini azaldır. Bu, problemi gizlətmək əvəzinə görünən edir. Queue-ya yuxarı hədd qoymaq, qoymaq kimi, qaydadır.

İkinci asinxron problem -dır: heç vaxt uğurla emal oluna bilməyən message (səhv format, mövcud olmayan entity-yə istinad, consumer-dəki bug). zamanı broker onu yenidən çatdırır, consumer yenidən yıxılır və queue-nun qalan hissəsi bu tək message-in arxasında ilişib qalır. Həll: müəyyən sayda cəhddən sonra message-i -ya () köçürmək. Əsas axın davam edir, problemli message isə itmir.

zibil qutusu deyil. Onun iki öhdəliyi var: monitoring ( message sayı sıfırdan fərqlidirsə, kimsə xəbər tutmalıdır) və replay (bug düzəldildikdən sonra message-ləri əsas queue-ya qaytarmaq üçün alət). Heç kimin baxmadığı səssiz data itkisidir.

Pattern-lər birlikdə

Pattern-lər tək-tək yox, request yolu boyunca qatlar şəklində işləyir:

client → [timeout, retry + backoff + jitter]
  → API gateway [rate limiting, load shedding]
    → service A [deadline, circuit breaker, bulkhead, fallback]
      → service B [load shedding]
      → queue [backpressure] → consumer [retry, DLQ]

Hər pattern bir problemi həll edir və səhv konfiqurasiya olunanda öz problemini yaradır:

PatternQoruduğu problemSəhv konfiqurasiya olunanda
Sonsuz gözləmə, resursların tutulmasıÇox qısa: sağlam request-lər kəsilir. Çox uzun: yavaşlıq yayılır
+ Keçici xətalarHər qatda : storm. olmayan əməliyyatda: dublikat
Xarab dependency-yə israf olunan çağırışlarHəssas hədd: sağlam dependency kəsilir. : sadəcə daha sürətli xəta
Bir dependency-nin ortaq resursları tutmasıÇox kiçik pool: normal yükdə xəta. Ortaq pool: izolyasiya yoxdur
Bir client-in hamını boğması, abuseÇox aşağı limit: legitim istifadəçilər bloklanır
Overload zamanı hamıya yavaş cavabPrioritet yoxdur: vacib traffic atılır
Queue-ların sonsuz böyüməsiLimitsiz queue: problem saatlarla gizli qalır
axını bloklayırMonitor olunmur: səssiz data itkisi
Kiçik dependency-nin bütün feature-u yıxmasıTest olunmayan : ən lazım anda işləmir

Bu pattern-lərin çoxunu service mesh və ya səviyyəsində kod yazmadan konfiqurasiya etmək olar. Bu rahatdır, amma riskli də ola bilər: retry-lar həm kitabxanada, həm də mesh-də aktiv olanda yuxarıdakı 27 qat amplification siz bilmədən yaranır. Hansı qatın nəyə cavabdeh olduğu ch05-dəki mənada layiq qərardır. Pattern-lərin həqiqətən işlədiyini isə yalnız ölçəndə biləcəksiniz; buna fəslində qayıdacağıq.

Hazırkı sisteminiz haqqında suallar

  1. Ən vacib request yolunuzdakı hər şəbəkə çağırışının nə qədərdir? Bunu konfiqurasiyada göstərə bilərsinizmi, yoxsa kitabxananın default-una güvənirsiniz?
  2. hansı qatlarda baş verir: client-də, gateway-də, service mesh-də, kodda? Ən pis halda bir istifadəçi hərəkəti ən dərindəki dependency-yə neçə request yaradır?
  3. Ən az vacib dependency-niz (tövsiyələr, analytics) 10 saniyə cavab verməsə, checkout-a nə olur? Onlar ortaq thread və ya connection pool istifadə edirmi?
  4. hazırda neçə message var və bunu kim bilir?