Problem: bir yavaş asılılıq hər şeyi çökdürür

Order servisi sifariş səhifəsini bəzəmək üçün recommendation servisini çağırır. Recommendation servisi ölmür, yavaşlayır: hər çağırış əlli millisaniyə əvəzinə otuz saniyə çəkir. Order servisində 200 sorğu thread-lik pool var. Hər gələn sorğu yavaş çağırışı gözləyir, pool dolur və tezliklə recommendation-a ehtiyacı belə olmayan sifariş səhifəsi də heç kimə xidmət edə bilmir. Kritik olmayan asılılıq tam outage-a çevrildi və o, uğursuzluq səssiz yox, yavaş olduğuna görə yayıldı.

Distributed sistemdə şəbəkə mesajı itirə, gecikdirə və ya təkrarlaya bilər, uzaq proses yavaş, ölü və ya yarımçıq işləyən ola bilər və çağıran tərəfdən bunların hamısı eyni görünür: hələ cavab yoxdur. Uğursuzluq istənilən anda hansısa komponentin normal vəziyyətidir. Dizayn sualı belədir: bir uğursuzluğun nə qədər uzağa getməsinə icazə verilir.

Hər yerdə timeout

Prosesinizi tərk edən hər çağırışın timeout-u olmalıdır: HTTP client-lər, baza driver-ləri, message producer-lər, lock əldə etmə, connection pool-dan götürmə. Bir çox kitabxana default olaraq "sonsuz gözlə" və ya istifadəçiyə baxan sorğu üçün çox uzun dəyərlərdən istifadə edir.

  • Connect timeout (serverə çata bilirəmmi?) ilə read/response timeout-u (cavabı nə qədər gözləyirəm?) ayırın.
  • Timeout-ları çağıranın öz büdcəsi ilə uyğunlaşdırın. İstifadəçi sorğusu iki saniyədə bitməlidirsə, içindəki üç downstream çağırışın hər birinə on saniyə vermək olmaz. Deadline-ı çağırış zəncirində aşağı ötürmək yaxşı təcrübədir.
  • Dəyərləri təxminlərə yox, asılılığın müşahidə olunan gecikmə percentile-larına və bir az ehtiyata əsaslandırın. Ölçməyi observability məqaləsində görün.
RestClient client = RestClient.builder()    .requestFactory(new JdkClientHttpRequestFactory(        HttpClient.newBuilder()            .connectTimeout(Duration.ofMillis(300)).build()))    .build();// plus a read timeout on the request factory, set per dependency

Timeout uzaq əməliyyatın baş verib-vermədiyini demir. Server sorğunu emal etmiş, cavab isə itmiş ola bilər. Buna görə retry-lar idempotency tələb edir; bu, Idempotency nümunələri məqaləsində izah olunur.

Exponential backoff və jitter ilə retry

Retry bağlantının qopması və ya qısa yüklənmə kimi keçici xətalar üçün faydalıdır. Diqqətsiz edildikdə təhlükəlidir: asılılıq çətinlik çəkirsə və hər client dərhal üç dəfə yenidən cəhd edirsə, artıq çökməkdə olan sistemin yükünü dörd qat artırmısınız. Buna retry storm deyilir.

Yaxşı retry qaydaları:

  1. Yalnız keçici ola biləcək xətaları (timeout, connection reset, 503) və təkrarlanması təhlükəsiz əməliyyatları retry edin. 400-ü və ya validasiya xətasını retry etməyin.
  2. Cəhd sayını kiçik rəqəmlə məhdudlaşdırın.
  3. Exponential backoff istifadə edin: hər uğursuzluqdan sonra daha çox gözləyin.
  4. Jitter əlavə edin: gözləmə vaxtını təsadüfiləşdirin ki, minlərlə client eyni anda təkrar etməsin.
  5. Retry-ı yalnız bir qatda edin. Üç qatın hər biri üç dəfə retry edirsə, bir uğursuzluq iyirmi yeddiyə qədər çağırışa çevrilir.
resilience4j:  retry:    instances:      inventory:        maxAttempts: 3        waitDuration:         enableExponentialBackoff: true        exponentialBackoffMultiplier: 2        enableRandomizedWait: true        retryExceptions:          - java.io.IOException          - java.util.concurrent.TimeoutException

İstifadə etdiyiniz kitabxana versiyası üçün property adlarını yoxlayın. Dəqiq açarlardan çox anlayışlar vacibdir.

Circuit breaker

Circuit breaker asılılığa çağırışlara baxır və uğursuzluqlar həddi keçəndə açılır: uzaq servisə cəhd etmədən çağırışları dərhal uğursuz edir. Gözləmə müddətindən sonra half-open vəziyyətə keçir, bir neçə sınaq çağırışına icazə verir və onlar uğurlu olarsa yenidən bağlanır.

  CLOSED --failure rate too high--> OPEN
     ^                               |
     |                          wait elapsed
     +--trial calls succeed-- HALF-OPEN
                  |--trial fails--> OPEN

İki fayda var: çağıranlar cavab verə bilməyən asılılıq üzərində thread-ləri bağlamaq əvəzinə tez uğursuz olur, çətinlik çəkən servis isə bərpa üçün nəfəs alır. Qiymətləndirmədən əvvəl minimum çağırış sayını, uğursuzluq nisbəti həddini, yavaş çağırış həddini (yavaşlıq da uğursuzluq sayılır) və ağlabatan gözləmə müddətini konfiqurasiya edin. Breaker-in vəziyyətini metrik kimi açın və açılanda alert qurun.

Bulkhead

Gəmilərdə su keçirməyən bölmələr olur ki, bir sızma gəmini batırmasın. Bulkhead hər asılılığın istehlak edə biləcəyi resursları məhdudlaşdırır, beləliklə birinin uğursuzluğu digərlərini aclıqdan saxlaya bilmir.

  • Semaphore bulkhead: asılılığa eyni vaxtlı çağırışları məhdudlaşdırır. Limitə çatanda sonrakı çağırışlar dərhal rədd edilir.
  • Thread pool bulkhead: asılılığa çağırışları öz növbəsi olan ayrıca pool-da işlədir.
  • Kritik və kritik olmayan baza yükləri üçün ayrı connection pool-lar eyni ideyaya əsaslanır.

Giriş hekayəsində eyni vaxtda məsələn iyirmi recommendation çağırışı limiti sifariş səhifəsi üçün 180 thread-i sərbəst saxlayardı. Virtual thread-lərlə thread-lər ucuzdur, amma downstream tutumu, connection pool-lar və yaddaş yenə də sonludur, buna görə limitlər vacib qalır.

Fallback və yumşaq degradasiya

Çağırış uğursuz olanda istifadəçinin əvəzində nə alacağına qərar verin:

  • Default dəyər: boş recommendation siyahısı.
  • Keşlənmiş və ya köhnə data: son məlum qiymət; dəqiqlik vacibdirsə, interfeysdə uyğun işarələnməklə.
  • Sadələşdirilmiş yol: ixtiyari zənginləşdirməni keçib davam etmək.
  • Aydın xəta: səhv cavabın heç cavab olmamasından pis olduğu əməliyyatlar üçün, məsələn ödənişdən əvvəl hesab qalığının yoxlanması. Fraud servisi əlçatmaz olanda "təsdiqləndi" qaytaran fallback biznes qərarıdır və səssiz default yox, açıq sahibi olmalıdır.

Fallback-lər test olunmalıdır; bunlar yalnız insidentlər zamanı işləyən kod yollarıdır və tez çürüyür.

Qismən uğursuzluq nümunələri

  • Ödəniş keçdi, cavab itdi. Client timeout alır və təkrar edir; idempotency key olmasa, müştəridən iki dəfə pul tutulur.
  • Baza yazısı commit oldu, event dərc edilmədi. Başqa servis sifarişdən heç vaxt xəbər tutmur. Bu dual-write problemi Kafka delivery semantikası məqaləsindəki outbox nümunəsi ilə həll olunur.
  • Bir yavaş replika. Yalnız "qalxıbmı?" yoxlayan load balancer-lər ona trafik göndərməyə davam edir; health check-lər və outlier detection kömək edir.
  • Saat fərqləri. Sıralama və ya lock müddəti üçün maşınlar arasında divar saatı müqayisələrindən istifadə etmək fərq olduqda yanlış nəticə verir.

Resilience4j ilə nümunələrin birləşdirilməsi

Resilience4j bu ideyaları ayrı modullar kimi təqdim edən Java kitabxanasıdır: time limiter, retry, circuit breaker, bulkhead və rate limiter. Birləşdirəndə sıra əhəmiyyətlidir. Tipik sıra xaricdən içəriyə belədir: bulkhead, time limiter, circuit breaker, retry, sonra real çağırış; və ya bunun variantı, amma onu şüurlu seçməlisiniz. Circuit breaker-in içində retry breaker-ə hər məntiqi çağırışın yekun nəticəsini göstərir; tərsinə olduqda hər cəhd ayrıca sayılır. Sadəlik üçün Spring bean üzərində @CircuitBreaker və @Retry annotasiyalarından istifadə edin, yadda saxlayın ki, onlar da @Transactional kimi proxy ilə işləyir və eyni sinif daxilindən çağırışda yan keçilir.

Tez-tez edilən səhvlər

  • Timeout yoxdur və ya hər yerdə eyni böyük timeout var.
  • İdempotent olmayan əməliyyatları retry etmək.
  • Hər qatda retry etmək.
  • Metriksiz circuit breaker; heç kim onun açıldığını bilmir.
  • Real problemi həmişəlik gizlədən fallback-lər. Degradasiya rejimi dashboard-da görünməlidir.
  • Yalnız uğurlu ssenarinin testi. Test mühitində gecikmə və xəta yeritməklə həqiqətən nə baş verdiyini görün.

Güzəştlər

Hər mexanizm konfiqurasiya, anlamalı vəziyyətlər və səhv qurma yolları əlavə edir. Monolitin iki modulu arasındakı çağırışa circuit breaker lazım deyil. Bu nümunələri şəbəkəni keçən sərhədlərdə və uğursuzluğuna dözə bilmədiyiniz asılılıqlara tətbiq edin; timeout-lardan başlayın, çünki onlar ucuzdur və ən pis kaskadların qarşısını alır. Dayanıqlılıq həm də uyğunluq və ya funksionallıq bahasınadır: degradasiya olunmuş cavab tərifinə görə ən yaxşı cavab deyil.

Yoxlama siyahısı

  • Hər xaricə çağırışın çağıranın büdcəsinə uyğun connect və read timeout-u var.
  • Retry-lar: az, backoff-lu, jitter-li, idempotent əməliyyatlarda, bir qatda.
  • Kritik uzaq asılılıqlarda metrik və alert-li circuit breaker-lər.
  • Bulkhead-lər kritik işi ixtiyari işdən ayırır.
  • Hər fallback şüurlu, test olunmuş və görünəndir.
  • Xəta yeritmə testlərin hissəsidir.

Növbəti addım