Əsas məzmuna keç
Resurslara qayıt
SYSTEM DESIGN

Paylanmış sistemlərin üç qızıl qaydası

Latency, partial failure və məhdud resurslar niyə hər bir arxitektura qərarına təsir etməlidir?

10 dəq. oxu
Bir-birinə işıq yolları ilə bağlanan üç server: gecikmə, itən cavab və məhdud queue tutumunun konseptual təsviri.

Bir checkout, üç fərqli risk

Müş­tə­ri on­layn ma­ğa­za­da sifarişini təsdiqləyir və checkout service payment provider-ə sorğu gön­də­rir. Ödə­niş uğur­la ta­mam­la­nır, amma cavab checkout service-ə çat­mır. Bir neçə sa­ni­yə göz­lə­dik­dən sonra tət­biq xəta gös­tə­rir, müş­tə­ri isə “Ye­ni­dən cəhd et” düy­mə­si­ni sıxır.

Checkout service ödə­ni­şin nəticəsini bilmədiyi üçün tək­rar sorğu eyni əmə­liy­ya­tın ye­ni­dən icrasına səbəb ola bilər. Bu müddətdə gecikən sor­ğu­lar re­sursla­rı məş­ğul saxlaya, av­to­ma­tik retry-lar isə sis­te­min yükünü artıra bilər. Beləliklə, itən bir cavab həm ödə­ni­şin və­ziy­yə­ti, həm də sis­te­min işləkliyi ilə bağlı prob­lem ya­ra­dır.

Bu ssenarini anlamaq üçün pay­lan­mış sis­tem­lə­rin üç məh­du­diy­yə­ti­ni birlikdə nəzərə almaq la­zım­dır:

  1. Mə­lu­ma­tın ötü­rül­mə­si vaxt tələb edir.
  2. Asılı ol­du­ğu­muz sis­tem­lər bəzən əl­çat­maz olur.
  3. Re­surslar məh­dud­dur.

Məqalədə həmin checkout nümunəsi üzə­rin­dən bu məhdudiyyətlərin timeout, retry, mə­lu­ma­tın ak­tu­al­lı­ğı və yükün idarə edilməsi ilə bağlı qərarlara necə təsir etdiyinə baxacağıq.

01 / LATENCY

Məlumatın ötürülməsi vaxt tələb edir

Kodda remote call adi metod çağırışı kimi gö­rü­nə bilər:

inventoryClient.reserve(productId, quantity);

Bu çağırışın ye­ri­nə yetirilməsi üçün mə­lu­mat serialize edil­mə­li, şə­bə­kə ilə ötü­rül­mə­li və qarşı tə­rəf­də emal olunmalıdır. Remote service işi tamamladıqdan sonra cavab da geri göndərilir. Connection qu­rul­ma­sı, şə­bə­kə­də sıx­lıq və queue-da göz­lə­mə bu müddəti uzada bilər. HTTP client həmin addımların çoxunu kod­dan gizlətsə də, on­la­rın tələb et­di­yi vaxt qalır.

Ge­cik­mə mə­lu­ma­tın digər komponentlərə nə vaxt çatmasına da təsir edir. Mə­sə­lən, müş­tə­ri an­bar­da qalan son məh­su­lu aldıqda inventory service öz database-ini ye­ni­lə­yir, search index və product cache isə bu dəyişikliyi asin­xron qəbul edir. Yenilənmə onlara çatana qədər eyni məh­sul haqqında fərqli mə­lu­mat gö­rü­nə bilər:

  • Inventory service məh­su­lun möv­cud ol­ma­dı­ğı­nı bil­di­rir.
  • Search nə­ti­cə­lə­ri məh­su­lu hələ də möv­cud gös­tə­rir.
  • Cache-dəki məh­sul sə­hi­fə­sin­də hələ də “Stok­da var” ya­zı­lır.

Komponentlər həmin ana qədər aldıqları məlumatla işləyirlər. Buna görə dizaynda köh­nəl­miş mə­lu­ma­tın, yəni stale data-nın hansı əmə­liy­yat­lar­da qəbul edilə bi­lə­cə­yi aydın ol­ma­lı­dır. Ax­ta­rış nə­ti­cə­si­nin qısa müd­dət köhnə qalması məq­bul sayıla bilər; ödənişə keçərkən isə məh­su­lun həqiqətən möv­cud olduğuna əmin olmaq la­zım­dır.

Bu iki axının tələbləri fərqlidir. Məhsula baxış sə­hi­fə­si sü­rət­li cavab üçün cache-dən oxuya bilər. Checkout isə inventory service-in əsas məlumatına söykənməli və son məhsula gələn paralel sifarişləri düz­gün idarə edən re­zerv əməliyyatından is­ti­fa­də et­mə­li­dir.

01 / MƏLUMATIN YOLU

Eyni məhsulun, fərqli anda görünən vəziyyəti

Inventory serviceAuthoritative state1 məhsul
Product cacheAsinxron yenilənirStokda var
Checkout qərarı cache-ə deyil, paralel rezervasiyaları düzgün idarə edən inventory əməliyyatına əsaslanmalıdır.

Hər çağırışın vaxt büdcəsində yeri var

Hər remote call sor­ğu­nun latency budget-indən, yəni cavab üçün ay­rıl­mış ümumi vaxtdan pay gö­tü­rür. Ar­dı­cıl ça­ğı­rış­la­rın ge­cik­mə­lə­ri top­la­nır; bir dependency-nin ya­vaş­la­ma­sı digər addımlara ayrılan vaxtı da azal­dır. Buna görə yeni service call əlavə edər­kən onun cavabı üçün nə qədər gözləyə biləcəyimizi və timeout baş verdikdə nə edəcəyimizi müəyyənləşdirməliyik.

Bu qərarı vermək üçün mə­lu­ma­tın aktuallığına olan tələbi dəqiqləşdirmək faydalıdır:

Bu əmə­liy­ya­ta məhz indi remote service-dəki ən son mə­lu­mat la­zım­dır, yoxsa lokal ola­raq möv­cud mə­lu­mat­dan is­ti­fa­də etmək olar?

Lokal mə­lu­mat kifayət edir­sə, əlavə şə­bə­kə çağırışına ehtiyac qalmaya bilər. Ən son və­ziy­yət vacibdirsə, həmin dependency-yə müraciət üçün vaxt ümumi deadline daxilində ayrılmalıdır.

02 / PARTIAL FAILURE

Dependency əlçatmaz, əməliyyatın nəticəsi isə naməlum ola bilər

Server na­saz­lı­ğı, şə­bə­kə bağlantısının kəsilməsi və ya kon­fi­qu­ra­si­ya dəyişikliyi asılı ol­du­ğu­muz service-i əl­çat­maz edə bilər. Sorğu gön­də­rən tət­biq üçün çə­tin­lik təkcə cavab ala bilməmək deyil: bəzən əmə­liy­ya­tın qarşı tə­rəf­də icra olunub-olunmadığı da bilinmir.

Mə­sə­lən, ödə­niş sorğusu timeout ilə bitdikdə aşağıdakı hallardan hər hansı biri baş ver­miş ola bilər:

  • Sorğu payment provider-ə ümu­miy­yət­lə çat­ma­yıb.
  • Provider sor­ğu­nu qəbul edib, amma emalı hələ ta­mam­la­ma­yıb.
  • Ödə­niş ta­mam­la­nıb, amma cavab itib.

Caller yal­nız gözlədiyi ca­va­bın gəlmədiyini görür və bu halları bir-bi­rin­dən ayıra bilməyə bilər. Timeout caller-in göz­lə­mə­yi da­yan­dır­dı­ğı­nı bil­di­rir. Əmə­liy­ya­tın uğur­suz ol­du­ğu­nu sübut etmir. Bu fərq retry qərarında va­cib­dir, çünki tək­rar sorğu artıq tamamlanmış ödə­ni­şi ye­ni­dən icra edə bilər.

Bunun qar­şı­sı­nı almaq üçün eyni ödə­ni­şin retry-larında əmə­liy­yat identifikatoru də­yiş­mə­mə­li­dir. Idempotency key bu funksi­ya­nı dəs­tək­lə­yən provider-ə tək­rar sor­ğu­nu ta­nı­ma­ğa və müş­tə­ri­dən ikinci dəfə ödə­niş tu­tul­ma­sı­nın qar­şı­sı­nı al­ma­ğa imkan verir.

Nə­ti­cə hələ məlum deyilsə, tət­biq ödə­ni­şi pending sta­tu­sun­da saxlayıb provider-dəki və­ziy­yə­ti yoxlaya bilər. Reconciliation prosesi lokal qeydlə provider-in nəticəsini tutuşdurmağa kömək edir. Belə halda istifadəçiyə ödə­ni­şin yoxlanıldığını gös­tər­mək, onu dər­hal uğur­suz elan edib yeni ödənişə yönləndirməkdən daha uyğundur.

02 / ADDIM-ADDIM SSENARİ

Timeout oldu. Pul tutulubmu?

Idempotency key payment-order-1042Bütün cəhdlərdə eynidir
Checkout serviceCavab gözləyir
Payment providerEmal edir

Hansı mexanizm hansı problemi həll edir?

Timeout, retry və digər resilience mexanizmlərinin vəzifələri fərqlidir. Onları seçərkən hansı na­saz­lı­ğı idarə etdiklərini ayırmaq la­zım­dır:

Me­xa­nizmMəq­səd
TimeoutCaller-in nə qədər göz­lə­yə­cə­yi­ni məh­dud­laş­dı­rır
Retry with backoff and jitterCəhdlər ara­sın­da artan və tə­sa­dü­fi də­yi­şən fa­si­lə­lər ya­ra­da­raq mü­vəq­qə­ti na­saz­lıq­dan sonra ye­ni­dən yox­la­ma­ğa imkan verir
Circuit breakerAr­dı­cıl xə­ta­lar verən dependency-yə ça­ğı­rış­la­rı mü­vəq­qə­ti azal­dır
BulkheadBir dependency-nin və ya workload-un ortaq re­surslar­dan nə qədər is­ti­fa­də edə bi­lə­cə­yi­ni məh­dud­laş­dı­rır
IdempotencyDəs­tək­lə­nən əmə­liy­yat­lar­da tək­rar cəhdlə­ri təh­lü­kə­siz edir

Bu mexanizmlərin li­mit­lə­ri bir-bi­ri­nə uyğun seçilməlidir: hər retry üçün ayrılan göz­lə­mə müddəti və cəhdlər arasındakı fa­si­lə­lər sor­ğu­nun ümumi deadline-ını aşmamalıdır. Fallback də əmə­liy­ya­tın mənasına uyğun ol­ma­lı­dır. Mə­sə­lən, məh­sul haqqında cache-dəki mə­lu­ma­tı gös­tər­mək qəbul edilə bilər, amma provider-dən təs­diq al­ma­dan ödə­ni­şi uğur­lu gös­tər­mək olmaz.

03 / CAPACITY

Resurslar məhduddur

Sis­te­min qəbul edə bi­lə­cə­yi yük CPU, memory, network bandwidth, database connection-ları, worker thread-lər və queue tu­tu­mu ilə məhdudlaşır. Cloud in­fra­struk­tu­ru bu imkanların bir his­sə­si­ni ge­niş­lən­di­rə bilər, amma scaling vaxt tələb edir; xərc, kvota və downstream sis­tem­lə­rin li­mit­lə­ri də nəzərə alınmalıdır.

Sor­ğu­lar yavaş tamamlandıqda eyni anda aktiv qalan işlərin sayı artır. Sabit axına yaxın şəraitdə bu əlaqəni təx­mi­nən belə ifadə etmək olar:

Orta in-flight sorğu sayı ≈ sor­ğu­la­rın da­xil­ol­ma sü­rə­ti × sis­tem­də ke­çi­ri­lən orta vaxt.

Service sa­ni­yə­də 200 sorğu qəbul edir və hər sorğu orta he­sab­la 100 millisaniyəyə tamamlanırsa, eyni anda təx­mi­nən 20 sorğu aktiv olur. Da­xil­ol­ma sü­rə­ti də­yiş­mə­dən orta müd­dət iki sa­ni­yə­yə qalxdıqda bu say təx­mi­nən 400-ə çatır. Yəni daha çox tra­fik gəlməsə belə, təkcə gecikmənin artması sis­tem­də saxlanılan işi xeyli çoxalda bilər.

Bu hesab 400 thread və ya database connection tələb olunduğu demək deyil; konkret re­surs is­ti­fa­də­si implementation-dan ası­lı­dır. Bununla belə, gözləyən sor­ğu­lar connection, memory və digər məh­dud re­sursla­rı tutursa, yeni işi qəbul etmək üçün yer azalır.

Retry-lar da eyni re­surslar­dan is­ti­fa­də edir. Yavaşlayan dependency-yə əlavə cəhdlər göndərildikcə yük arta, queue isə getdikcə böyüyə bilər. Li­mit­siz queue prob­le­mi sonraya ötürür: memory sərfi artır və bəzi sor­ğu­lar emala çatana qədər aktuallığını itirir.

03 / ÖZÜN DƏYİŞ VƏ MÜQAYİSƏ ET

Trafik eynidir. Bəs yük?

Daxilolma sürəti200 sorğu/s
Orta in-flight sorğu≈ 20
Hər blok təxminən 10 aktiv sorğunu göstərir.
100 ms → 20 sorğu2 000 ms → 400 sorğu

200 sorğu/s × 0,1 s ≈ 20 aktiv sorğu

Bu hesabı necə oxumalı?Little’s Law əsasında sabit orta axın üçün sadələşdirilmiş hesab. Zolaq 400 sorğuya nisbəti göstərir, real capacity limitini deyil. Bu rəqəm thread və ya connection sayı demək deyil; limitsiz böyüyən queue üçün sabit vəziyyət fərziyyəsi ödənmir.

Capacity bitəndə nə etməli?

Sis­tem tutumuna yaxınlaşdıqda məq­səd qəbul edilmiş işi tamamlamaq üçün kifayət qədər re­surs saxlamaqdır. Bunun üçün limitlər və limitə çatdıqda tət­biq ediləcək davranış əv­vəl­cə­dən müəyyənləşdirilməlidir:

  • Queue öl­çü­sü­nü və eyni anda iş­lə­yən əməliyyatların sayını məh­dud­laş­dı­rın.
  • Retry sa­yı­na və ümumi retry müd­də­ti­nə limit qoyun.
  • Uyğun hal­lar­da rate limiting və backpressure tət­biq edin.
  • Əlavə işi qəbul etmək sis­te­min ümumi availability-sini təh­lü­kə­yə ata­caq­sa, onu erkən rədd edin.
  • CPU is­ti­fa­də­si ilə ya­na­şı göz­lə­mə müd­də­ti­ni və resource saturation sə­viy­yə­si­ni də iz­lə­yin.

Yeni sor­ğu­nu erkən rədd etmək is­ti­fa­də­çi üçün xoş nə­ti­cə olmasa da, bütün sor­ğu­la­rın uzun müd­dət gözləyib timeout almasının qar­şı­sı­nı ala bilər. Hansı işin qəbul ediləcəyi və hansının təxirə salınacağı xidmətin tələblərinə uyğun seçilməlidir.

Bu üç qayda bir-birinin təsirini necə gücləndirir?

Checkout ssenarisinə qayıdaq: payment provider yavaşladıqda sor­ğu­lar daha uzun müd­dət aktiv qalır və ortaq re­surslar dolmağa baş­la­yır. Timeout alan caller-lər retry etdikcə əlavə yük ya­ra­nır. Beləcə, bir dependency-dəki ge­cik­mə tət­bi­qin nor­mal iş­lə­yən digər his­sə­lə­ri­nə də yayıla bilər.

Bu axında hər mexanizmin ayrıca işi var. Timeout caller-in gözləməsini məh­dud­laş­dı­rır, concurrency li­mi­ti isə aktiv işlərin re­sursla­rı tam tükətməsinə mane olur. Retry budget tək­rar cəhdlərin yükünü nə­za­rət­də sax­la­yır; idempotency eyni biz­nes əməliyyatının tək­rar icrasını idarə edir. Bu me­xa­nizmlə­ri birlikdə seçmək la­zım­dır, çünki yal­nız timeout qoymaq əlavə retry yükünü və ya tək­rar ödə­niş riskini həll etmir.

Dizaynı yoxlamaq üçün üç sual

Service-lər arasındakı hər mühüm əmə­liy­yat üçün bu üç sualın konkret cavabı ol­ma­lı­dır:

  1. Mə­lu­mat ge­ci­kər­kən nə baş verir?
  2. Dependency əl­çat­maz ol­duq­da və ya əmə­liy­ya­tın nə­ti­cə­si məlum ol­ma­dıq­da nə baş verir?
  3. Möv­cud capacity tü­kən­dik­də nə baş verir?

Checkout üçün bu cavablar praktik davranışa çevrilir: nə­ti­cə­si məlum olmayan ödə­niş yoxlanılır, retry eyni idempotency key ilə göndərilir və sis­tem tutumunu aşan sor­ğu­lar üçün aydın cavab qaytarılır. İs­ti­fa­də­çi də ödənişinin uğur­suz ol­du­ğu­nu deyil, hələ yoxlanıldığını görür.

Ar­xi­tek­tu­ra qərarlarını nə­zər­dən ke­çi­rər­kən nor­mal axınla ya­na­şı bu halları da izləmək faydalıdır. Hansı mə­lu­ma­tın köhnə qala bi­lə­cə­yi, hansı əmə­liy­ya­tın təkrarlana bi­lə­cə­yi və yük artanda hansı işin dayandırılacağı aydın ol­duq­da həm implementasiya, həm də nasazlıq za­ma­nı qərar vermək asanlaşır.