Distributed sistemlərin problemləri
Distributed sistemlərin problemləri
Əvvəlki fəsillər distributed data-nın alətlərini verdi: replication, partitioning, transaction-lar. Hamısı bir fərziyyəyə söykənirdi: şəbəkə, saat və proseslər, bəlkə bir az ləngiməklə, işləyir. Bu fəsil həmin fərziyyəni sökür. Tək kompüterdə proqram ya işləyir, ya da düşür: yarım işləyən vəziyyət yoxdur, hardware xətasında bütün sistem dayanır və determinizm pozulmur. Distributed sistemdə isə partial failure var: sistemin bir hissəsi düşür, qalanı işləyir və ən pisi, hansı hissənin düşdüyünü dəqiq bilmək mümkün olmur.
Bu fəsildə heç bir həll vermirik. Məqsəd nəyin pozula biləcəyini sistematik şəkildə tanımaqdır, çünki Fəsil 9-dakı consensus alqoritmlərinin niyə belə mürəkkəb olduğunu yalnız bu problemlər izah edir. Pessimizm burada peşəkarlıqdır: kifayət qədər maşın olanda nadir hadisələr adi hadisəyə çevrilir.
Partial failure və iki fəlsəfə
Böyük maşınlar qurmağın iki yanaşması var. Supercomputing fəlsəfəsi partial failure-u total failure-a çevirir: bir komponent sıradan çıxanda bütün job dayandırılır, checkpoint-dən bərpa olunur. Bu, tək maşın modelinə yaxındır. Cloud computing fəlsəfəsi isə əksinədir: minlərlə ucuz maşın, hər gün bir neçəsi mütləq sıradan çıxır, sistem bunu normal rejim kimi qəbul edib işləməyə davam etməlidir. İnternet xidmətləri ikinci yolu seçməyə məcburdur, çünki "təmir üçün sistemi dayandırırıq" onlar üçün mümkün deyil.
Buradan əsas prinsip gəlir: etibarsız komponentlərdən etibarlı sistem qurmaq. Bu, yeni fikir deyil: TCP etibarsız IP paketləri üzərində etibarlı axın yaradır, error-correcting kodlar səs-küylü kanalda düzgün bit verir. Lakin abstraksiyanın həddi var: TCP paket itkisini gizlədir, amma gecikməni gizlədə bilmir. Eyni şəkildə, distributed sistem bəzi xətaları gizlədir, bəzilərini isə application-a qaldırmağa məcburdur.
Etibarsız şəbəkə
Bu kitabdakı sistemlər shared-nothing sistemlərdir: maşınlar yalnız şəbəkə ilə əlaqə saxlayır. İnternet və datacenter-lərin çoxu asynchronous packet network-dür: paket göndərəndə çatacağına, nə vaxt çatacağına və cavabın gələcəyinə heç bir zəmanət yoxdur. Sorğu göndərib cavab almayanda bunlardan hər hansı biri baş vermiş ola bilər:
- Sorğu itdi: kabel çıxarılıb, switch yanlış konfiqurasiya olunub.
- Sorğu növbədə gözləyir və sonra çatdırılacaq.
- Qarşı node çöküb.
- Qarşı node müvəqqəti dayanıb, məsələn GC pause-dadır, sonra davam edəcək.
- Qarşı node sorğunu emal edib, cavab itdi.
- Cavab gecikir və sonra çatacaq.
Göndərən tərəf bunları bir-birindən ayıra bilmir: hamısı eyni görünür, cavab yoxdur. Yeganə alət timeout-dur və timeout "node ölüb" demir, "cavab almadım" deyir.
Şəbəkə problemləri nəzəri deyil. Orta datacenter-də ayda onlarla şəbəkə xətası qeydə alınır; köpəkbalıqlarının dənizaltı kabeli dişləməsi, switch firmware-inin bütün paketləri bir istiqamətdə ötürüb əks istiqamətdə atması kimi ekzotik hallar da sənədləşib. Sistem şəbəkə xətasına cavab verməyə məcbur deyil, amma cavabı müəyyən etməlidir: ən azı xəta mesajı göstərib data-nı korlamamaq.
Xətanı aşkarlamaq və timeout-u seçmək
Bəzi hallarda şəbəkə dəqiq siqnal verir: port-da proses yoxdursa, OS RST paketi qaytarır; proses çöküb amma OS sağdırsa, script bunu bildirə bilər; switch idarəetmə interfeysi link-in düşdüyünü göstərir. Amma bunlar fakültativdir və ən pis hal, yəni node-un sorğunu alıb emal etməməsi, bu siqnalların heç birini vermir. Ümumi halda yalnız timeout qalır.
Timeout nə qədər olmalıdır? Uzun timeout ölü node-u elan etməyi gecikdirir və istifadəçi gözləyir. Qısa timeout isə sadəcə yavaş node-u ölü elan edir. Bu təhlükəlidir: node əslində yarım işi görüb və onun vəzifəsini başqasına vermək əməliyyatı iki dəfə icra edir; əlavə olaraq, sistem overload-dadırsa, yavaş node-ları "ölü" elan edib onların yükünü digərlərinə köçürmək kaskadlı failure yaradır.
Problem gecikmənin unbounded olmasıdır. Gecikmənin əsas mənbəyi növbələrdir:
- Switch-də bir neçə port eyni çıxışa eyni anda paket göndərəndə növbə yaranır, dolanda paketlər atılır.
- Paket maşına çatanda bütün CPU nüvələri məşğuldursa, OS-in növbəsində gözləyir.
- Virtual maşın mühitində hypervisor VM-i dayandırıb başqasına CPU verir; bu arada gələn paketlər VM monitor-un buferində yığılır.
- TCP flow control göndərəni ləngidir, itirilmiş paketlər retransmit timeout gözləyir.
Nəticə: eyni şəbəkədə gecikmə bəzən 1 ms, bəzən 1 s ola bilər. Sabit timeout əvəzinə sistemlər round-trip time-ın paylanmasını davamlı ölçüb timeout-u dinamik hesablaya bilər; Phi Accrual failure detector və TCP-nin retransmit timeout-u bu prinsiplə işləyir.
Niyə sinxron şəbəkə qurmuruq?
Telefon şəbəkəsi fərqlidir: zəng başlayanda hər switch-də sabit bant genişliyi ayrılır, bu circuit-dir və söhbət bitənə qədər heç kim onu tuta bilmir. Gecikmə sabitdir, növbə yoxdur. Datacenter şəbəkələri niyə belə deyil? Çünki circuit sabit bit sürəti üçün optimaldır, kompüter trafiki isə bursty-dir: fayl köçürməsi şəbəkəni tam yükləyir, sonra dəqiqələrlə susur. Sabit bant genişliyi ayırsaq, ya həddindən artıq ehtiyat saxlamalı, ya da sürəti süni azaltmalıyıq. Paket şəbəkəsi mövcud bant genişliyini dinamik bölüşdürür: yüksək utilization-un qiyməti dəyişkən gecikmədir. Bu, mühəndislik trade-off-udur, texniki məhdudiyyət deyil və hazırda sənaye utilization-u seçib.
Etibarsız saatlar
Distributed sistemdə vaxt iki məqsədlə lazımdır: müddətləri ölçmək (timeout, latency) və nöqtələri qeyd etmək (event nə vaxt baş verib, cache nə vaxt expire olur). Hər maşının öz quartz kristalı var və onlar fərqli sürətlə gedir. Ona görə iki növ saat fərqləndirilir:
Time-of-day clock epoch-dan keçən vaxtı qaytarır, NTP ilə sinxronlaşır və buna görə geri çəkilə bilər: maşın NTP server-dən çox irəli gedibsə, saat qəfil geri atılır. Müddət ölçmək üçün yararsızdır. Leap second-lar da buraya əlavə problem gətirir: bəzi sistemlər 23:59:60 saniyəsini emal edə bilməyib çöküb.
Monotonic clock həmişə irəli gedir, mütləq dəyərinin mənası yoxdur, yalnız iki oxuma arasındakı fərq mənalıdır. Timeout və latency ölçmək üçün məhz bu işlədilir. NTP onu geri çəkmir, yalnız sürətini bir az tənzimləyə bilər.
NTP-nin dəqiqliyi də məhduddur: quartz drift-i gündə onlarla millisaniyə ola bilər, NTP sorğusunun özü şəbəkə gecikməsinə məruz qalır, firewall NTP-ni bloklaya bilər və müxtəlif kernel-lərdə smearing fərqli implementasiya olunur. Sinxronlaşmaya ehtiyacı olan sistem drift-i mütləq monitor etməlidir, çünki saatın səhv olması səssiz baş verir.
Sinxron saata güvənmənin təhlükəsi
Event-ləri saat vaxtına görə sıralamaq cazibədar və təhlükəlidir. Fəsil 5-dəki last-write-wins strategiyasını götürək: iki node eyni key-ə yazır, hər biri öz timestamp-ini qoyur, böyük timestamp qalib gəlir. Node A-nın saatı node B-dən 3 ms irəlidirsə, B-nin əslində sonra olan yazması "köhnə" sayılıb atılır. Xəta mesajı yoxdur, data sadəcə itir. Üstəlik eyni timestamp-lə iki yazma gələndə qalib təsadüfidir.
Problem budur: timestamp ordering causality-ni əks etdirmir. Daha düzgün yol Fəsil 9-dakı logical clock-lardır, onlar vaxtı deyil, event-lərin nisbi sırasını izləyir.
Saatı işlədən sistem onun confidence interval-ını bilməlidir: "indi 12:00:00.000-dir" əvəzinə "indi 11:59:59.990 ilə 12:00:00.010 arasındadır". Google-un Spanner-dəki TrueTime API-si məhz bu intervalı qaytarır və transaction commit-ini intervalın sonuna qədər gözlədir ki, iki transaction-ın intervalları kəsişməsin; bu, snapshot isolation-ı datacenter-lər arasında global timestamp ilə mümkün edir. Amma bunun üçün hər datacenter-də GPS və atom saatı var; adi NTP ilə eyni fəndi etmək olmur.
Proses pauzaları
Saatlarla bağlı ən gözlənilməz problem node-un özündə yaranır. Fərz edək ki, leader hər 10 saniyədə lease alıb yeniləyir və yazmadan əvvəl yoxlayır:
while (true):
if lease.expiry - now() < 1s:
lease = renew()
if now() < lease.expiry:
write() // bu sətrə qədər 15 saniyə keçə bilər
now() oxumaqla write() arasında thread 15 saniyə dayanırsa, lease çoxdan bitib, başqa node leader olub və iki leader paralel yazır. Niyə dayanmalıdır? Stop-the-world garbage collection; hypervisor-un VM-i suspend edib köçürməsi; laptop-un qapağının bağlanması; CPU-nun başqa thread-ə context switch etməsi; yaddaş swap-a gedib geri gəlməsi; SIGSTOP. Bunların heç birini kod görmür: proses üçün vaxt sadəcə sıçrayır. Ona görə distributed sistemdə heç bir node "ötən 100 ms-də dünya dəyişməyib" fərz edə bilməz.
Real-time sistemlər bu problemi həll edir: RTOS, hər addımın ən pis halda nə qədər çəkəcəyinin sübutu, GC-siz dillər. Amma bu, bahadır və ümumi server proqramlaşdırması üçün yararsızdır. Praktik kompromislər: GC-ni qısa, tez-tez pause-larla konfiqurasiya etmək; GC-yə girməzdən əvvəl node-un trafikini digərlərinə yönəltmək; uzun ömürlü obyektlər üçün prosesi vaxtaşırı restart etmək.
Bilik, həqiqət və yalan
Node heç nəyi təkbaşına bilə bilmir: yalnız mesaj alıb göndərir. Node-un özü "mən sağam" düşünsə belə, digərləri ona 30 saniyə çata bilmirsə, sistem onu ölü elan edir və bu qərar düzgündür. Buradan prinsip çıxır: həqiqəti çoxluq müəyyən edir. Alqoritmlər tək node-a deyil, quorum-a, yəni node-ların çoxluğunun səsinə güvənir. Beş node-dan üçü "leader öldü" deyirsə, leader ölüb, hətta sağ olsa da.
Fencing token
Bu, leader və lock üçün konkret problem yaradır. Client distributed lock service-dən lock alır, sonra GC pause-a düşür, lock expire olur, başqa client lock alır və storage-ə yazır. Birinci client oyanıb hələ də lock-un onda olduğunu düşünür və yazır: data korlanır. Çarə fencing token-dir: lock service hər lock verəndə artan nömrə qaytarır və storage yalnız gördüyü ən böyük token-dən kiçik olmayan yazmaları qəbul edir:
client 1: lock al → token 33 → pause...
client 2: lock al → token 34 → write(token=34) ✓
client 1: oyanır → write(token=33) ✗ rədd edilir, 34 < 33 deyil
Yoxlamanı storage tərəfində etmək vacibdir: client-in özünün "mənim lock-um var" deməsinə güvənmək olmaz.
Byzantine xətalar
İndiyədək node-ların səhv etdiyini, amma yalan danışmadığını fərz etdik. Node qəsdən yalan mesaj göndərirsə, bu Byzantine fault-dur. Aerokosmik sistemlər radiasiyanın yaddaşı korlamasına, blockchain-lər isə qarşılıqlı güvənsiz iştirakçılara görə Byzantine fault tolerance tələb edir. Lakin bu kitabdakı sistemlər üçün, yəni bir təşkilatın öz datacenter-indəki server-lər üçün, bu fərziyyə artıqdır: alqoritmlər qat-qat mürəkkəb və yavaşdır, həm də node-ların üçdə birindən çoxu pozulanda onsuz da işləmir.
Zəif "yalan" formalarına qarşı isə sadə müdafiə kifayətdir: şəbəkə checksum-ları hardware xətalarını tutmur, ona görə application səviyyəsində checksum; istifadəçi input-unun validasiyası; NTP üçün bir neçə server, ki bir server-in səhv vaxtı rədd edilsin.
Sistem modelləri
Alqoritmin düzgünlüyünü sübut etmək üçün hansı xətaları gözlədiyimizi formal yazmaq lazımdır. Zamanla bağlı üç model var:
| Model | Fərziyyə | Realist? |
|---|---|---|
| Synchronous | Şəbəkə gecikməsi, proses pauzası və saat xətası məlum həddi aşmır | Xeyr, unbounded gecikmə var |
| Partially synchronous | Çox vaxt synchronous kimi davranır, bəzən həddi aşır | Bəli, əksər sistemlər üçün |
| Asynchronous | Heç bir zaman fərziyyəsi yoxdur, saat belə yoxdur | Çox məhdudlaşdırıcı, az alqoritm işləyir |
Node xətaları üçün də üç model: crash-stop, yəni node düşəndə bir daha qayıtmır; crash-recovery, yəni node düşüb qayıda bilər və stabil yaddaşdakı data qalır, RAM itir; Byzantine. Praktikada ən faydalı birləşmə partially synchronous və crash-recovery-dir.
Alqoritmin xüsusiyyətlərini iki sinfə bölmək faydalıdır. Safety "pis heç nə baş vermir" deməkdir: unikallıq, monotonluq. Pozulanda konkret an göstərmək olar və geri qaytarmaq mümkün deyil. Liveness "yaxşı nəsə nəhayət baş verir" deməkdir: sorğu nəhayət cavab alır. Distributed alqoritmlərdən tələb budur: safety hər şəraitdə, hətta bütün node-lar düşəndə də qorunmalıdır; liveness isə yalnız müəyyən şərtlər altında, məsələn node-ların çoxluğu sağ olanda və şəbəkə nəhayət bərpa olunanda.
Son xəbərdarlıq: model abstraksiyadır. Fəsil 7-dəki durability fərziyyəsi, yəni fsync-dən sonra data-nın qalması, disk firmware bug-ı ilə pozula bilər; "crash-recovery" modelindəki stabil yaddaş da korlana bilər. Sübut edilmiş alqoritm real sistemdə düzgün işləyəcəyinə zəmanət deyil, amma rasional başlanğıcdır. Növbəti fəsil bu modellər daxilində nəyin mümkün olduğunu göstərir.
Tapşırıqlar
- Service-iniz downstream API-ni 500 ms timeout ilə çağırır. Son həftənin latency paylanmasını götürün və p99.9-u hesablayın. Timeout-u p99.9-un altına qoysanız nə olur, çox yuxarı qoysanız nə olur? Adaptiv timeout üçün sadə alqoritm təklif edin.
- Bir maşında time-of-day və monotonic clock-u paralel oxuyan skript yazın, sonra sistem saatını əl ilə 10 saniyə geri çəkin. Hər iki oxumada nə dəyişdi? Hansı kod yerlərində hansı saatı işlətmək lazım olduğunu üç nümunə ilə göstərin.
- Komandanız Redis üzərində
SET key value NX PX 10000ilə distributed lock qurub. Fencing token olmadan bu lock-un data-nı korlaya biləcəyi ssenarini addım-addım yazın. Sonra storage tərəfində token yoxlamasını pseudo-code ilə əlavə edin.