Reliable, scalable və maintainable sistemlər
Reliable, scalable və maintainable sistemlər
Bu gün yazılan application-ların çoxu compute-intensive deyil, data-intensive-dir. CPU onların darboğazı deyil; problem data-nın həcmi, mürəkkəbliyi və dəyişmə sürətidir. Tipik bir məhsul eyni anda bir neçə sual verir: istifadəçinin data-sını haradasa saxlamaq və sonra tapmaq, bahalı əməliyyatların nəticəsini yadda saxlayıb oxumanı sürətləndirmək, istifadəçiyə keyword ilə axtarış vermək, başqa process-ə mesaj göndərib asinxron emal etmək, hər gecə böyük həcmdə toplanmış data-nı emal edib hesabat çıxarmaq. Bu suallara cavab verən standart building block-lar var: database, cache, search index, message queue və stream processor, batch processor.
Amma heç bir məhsul "sadəcə database" deyil. Real tətbiq bu alətlərin bir neçəsini birləşdirir, aralarında data-nı sinxron saxlayır və nəticədə özünə məxsus, bespoke data sistemi qurur: application kodu cache-i invalidate edir, search index-i yeniləyir, queue-ya event yazır. Siz artıq data sisteminin dizayneri olursunuz və bu kitab məhz bu rolu öyrədir. Kitabın bütün fəsillərini üç sual birləşdirir: sistem nə vaxt reliable-dır, nə vaxt scalable-dır və nə vaxt maintainable-dır.
Reliability: fault ilə failure fərqi
Reliable sistem "nəsə səhv getsə də düzgün işləməyə davam edən" sistemdir. Burada dəqiq terminologiya vacibdir. Fault sistemin bir komponentinin spesifikasiyadan kənara çıxmasıdır: disk nasazlığı, process-in çökməsi, yanlış konfiqurasiya. Failure isə bütün sistemin istifadəçiyə lazım olan xidməti verə bilməməsidir. Fault-ları sıfıra endirmək mümkün deyil; məqsəd fault-un failure-a çevrilməsinin qarşısını almaqdır. Buna fault-tolerant və ya resilient sistem deyilir.
Bu baxış bir qəribə nəticə verir: fault-ları qəsdən yaratmaq faydalıdır. Təsadüfi process-ləri öldürən, şəbəkəni kəsən alətlər fault-tolerance mexanizmlərinin həqiqətən işlədiyini yoxlayır.
Hardware fault-ları
Böyük data center-də hard disk-lər daim sıradan çıxır. Disk-lərin MTTF-i, yəni orta nasazlıq müddəti, 10–50 il civarındadır. Bu az görünə bilər, amma 10 000 disk-lik cluster-də gündə təxminən bir disk ölür:
gündəlik nasazlıq ≈ disk sayı / (MTTF il · 365)
≈ 10 000 / (30 · 365) ≈ 0.9 disk/gün
Klassik cavab redundancy-dir: RAID, ikili power supply, generator. Bu, tək maşının ölməsini nadir hadisəyə çevirir. Amma data həcmi artdıqca maşın sayı artır və bütöv maşının itməsi adi hala gəlir. Buna görə müasir sistemlər hardware redundancy-ni software-də fault-tolerance ilə tamamlayır: bir node itəndə sistem işləməyə davam edir. Əlavə fayda: node-ları bir-bir restart edib downtime-sız patch tətbiq etmək olur.
Software səhvləri
Hardware fault-ları adətən müstəqildir: bir disk-in ölməsi digərinin ölmə ehtimalını artırmır. Software səhvləri isə korrelyasiyalıdır. Eyni bug bütün node-larda eyni anda tetiklənir: müəyyən bir giriş alanda çökən process, bütün server-ləri eyni vaxtda əhatə edən leap second bug-ı, bütün resursu yeyən runaway process, cavab verməyən xarici service-ə bağlı olan bütün komponentlər, kiçik xətanın böyüyə çevrildiyi cascading failure.
Bu bug-lar uzun müddət gizli qalır, çünki nadir şərtləri gözləyir. Onlara qarşı sehrli həll yoxdur; işləyən şeylər xırdadır: sistemin fərziyyələrini açıq yazmaq, hərtərəfli test, process-ləri izolyasiya etmək, çökən process-in avtomatik restart-ı və production-da davranışı ölçüb gözləniləndən kənara çıxanda xəbərdar etmək.
İnsan səhvləri
Böyük internet service-lərində outage-ların əsas səbəbi operator-un yanlış konfiqurasiyasıdır, hardware fault-ları isə siyahıda çox aşağıdadır. İnsanları etibarlı etmək mümkün deyil, amma sistemi insan səhvini çətinləşdirəcək şəkildə qurmaq olar:
- Yaxşı dizayn olunmuş abstraksiya və API-lar "düzgün yolu" asan, səhv yolu çətin edir.
- Production-dan ayrı, real data ilə işləyən sandbox mühitlər təhlükəsiz eksperiment imkanı verir.
- Unit test-dən integration test-ə qədər hər səviyyədə avtomatik test, xüsusilə normal istifadədə nadir rast gəlinən corner case-lər üçün.
- Tez və asan recovery: konfiqurasiyanı rollback etmək, yeni kodu tədricən buraxmaq, köhnə data-nı yenidən hesablamaq.
- Ətraflı monitoring və telemetry: performance metrikləri, error rate-lər. Telemetry olmadan problem baş verəndən sonra nə olduğunu bilmirsiniz.
Scalability: yükü təsvir etmək
Sistemin bu gün reliable olması sabah da belə olacağı demək deyil. Yük artanda sistem degradasiya olur. Scalability "X scalable-dır" kimi binar etiket deyil; suala cavabdır: "sistem müəyyən şəkildə böyüyəndə bununla necə baş edirik?"
Bu sualı vermək üçün əvvəlcə yükü təsvir etmək lazımdır. Yük load parameter-lərlə təsvir olunur: saniyədə request sayı, database-də oxuma-yazma nisbəti, eyni anda aktiv istifadəçi sayı, cache hit rate. Hansı parametrin vacib olması sistemə bağlıdır.
Klassik nümunə Twitter-in home timeline-ıdır. İki əsas əməliyyat var: tweet göndərmək və öz timeline-ını oxumaq. Oxuma yazmadan yüz dəfələrlə çoxdur. İki dizayn mümkündür:
| Yanaşma | Yazma | Oxuma | Problem |
|---|---|---|---|
| Fan-out on read | Tweet sadəcə global cədvələ yazılır | İstifadəçinin follow etdiyi hər kəsin tweet-lərini tapıb birləşdirir | Oxuma saniyədə yüz minlərlə sorğu yaradır; timeline yavaş açılır |
| Fan-out on write | Tweet hər follower-in timeline cache-inə kopyalanır | Hazır cache-dən bir oxuma | Milyonlarla follower-i olan istifadəçi bir tweet-lə milyonlarla yazma tetikləyir |
Fan-out on write-da əsas load parameter istifadəçi başına follower paylanmasıdır, sadəcə ümumi tweet sayı deyil. Həll hibriddir: adi istifadəçilərin tweet-ləri yazma anında fan-out olunur, məşhurların tweet-ləri isə oxuma anında ayrıca gətirilib birləşdirilir. Bu dizaynın dərsi odur ki, "doğru" yanaşma yükün paylanmasından asılıdır və bir parametrin dəyişməsi dizaynı dəyişdirə bilər.
Scalability: performansı təsvir etmək
Yükü təsvir edəndən sonra iki sual verilir: load parameter artanda resursu sabit saxlasaq performans necə dəyişir, və performansı sabit saxlamaq üçün resursu nə qədər artırmaq lazımdır?
Batch sistemlərində performans throughput-dur: saniyədə emal olunan qeyd sayı. Online sistemlərdə isə response time: istifadəçinin request göndərməsindən cavab almasına qədər keçən vaxt. Response time ilə latency fərqli şeylərdir: latency request-in emal olunmağı gözlədiyi vaxtdır, response time isə buna şəbəkə gecikməsini və emalı da əlavə edir.
Response time tək rəqəm deyil, paylanmadır. Eyni request təkrar göndərilsə də, context switch, paket itkisi, garbage collection pause, disk-in page cache-dən oxuması səbəbindən fərqli nəticə verir. Buna görə orta göstərici pis metrikdir: neçə istifadəçinin həqiqətən gecikmə yaşadığını demir. Doğru alət percentil-lərdir:
- p50, median: request-lərin yarısı bundan tez cavab alır.
- p95, p99, p999: ən yavaş 5%, 1% və 0.1%. Bunlara tail latency deyilir.
Tail latency vacibdir, çünki ən yavaş request-ləri alan istifadəçilər çox vaxt ən çox data-sı olan, yəni ən dəyərli müştərilərdir. Amazon daxili service-lər üçün p999 üzrə tələb qoyur: bir milyon request-dən min biri də olsa, sayılır.
Percentil-lər SLO və SLA-larda istifadə olunur: "p50 200 ms-dən az, p99 1 s-dən az, uptime 99.9%". Belə razılaşma müştərinin nə gözləyəcəyini dəqiqləşdirir.
Backend-də iki fenomen tail-i böyüdür. Head-of-line blocking: server-in paralel işlədə biləcəyi request sayı azdırsa, bir neçə yavaş request növbəni tutur və sonrakı sürətli request-lər də yavaş görünür. Tail latency amplification: bir istifadəçi request-i on backend çağırışı tələb edirsə, birinin yavaş olması bütün request-i yavaşladır. Backend çağırışlarının 1%-i yavaşdırsa, on çağırışlı request-lərin təxminən 10%-i yavaş olur:
P(ən azı biri yavaş) = 1 − (0.99)^10 ≈ 0.096
Percentil-ləri ölçmək üçün bütün response time-ları saxlamaq lazım deyil; HdrHistogram və ya t-digest kimi alqoritmlər yaxşı yaxınlaşma verir. Amma dashboard-da percentil-ləri ortalamaq olmaz: p99-ların orta dəyərinin mənası yoxdur, histogram-ları birləşdirmək lazımdır.
Scalability: yüklə baş etmək
Bir həcmdə yük üçün yaxşı olan architecture on dəfə böyük yük üçün yaxşı olmur. Sürətlə böyüyən sistemdə architecture-i hər böyümə pilləsində yenidən düşünmək lazımdır.
İki əsas istiqamət var. Scaling up, vertical scaling: daha güclü maşın. Scaling out, horizontal scaling: çoxlu kiçik maşın, buna shared-nothing architecture də deyilir. Tək güclü maşın sadədir, amma xətti olmayan şəkildə bahalaşır. Praktik sistemlər çox vaxt qarışıqdır: bir neçə güclü maşın çoxlu zəif virtual maşından ucuz ola bilər.
Elastic sistemlər yük artanda avtomatik resurs əlavə edir; əl ilə scale olunan sistemlər isə daha sadə və proqnozlaşdırıla biləndir. Yük qeyri-müəyyəndirsə elastiklik faydalıdır, amma öz sürprizlərini gətirir.
Stateless service-ləri çoxlu maşına yaymaq sadədir. Stateful sistemi, yəni database-i distributed etmək isə ciddi mürəkkəblik gətirir. Buna görə uzun müddət qayda belə idi: database-i tək maşında saxla, scaling tələbi məcbur edənə qədər. Bu qayda dəyişir; distributed data sistemləri üçün abstraksiyalar yaxşılaşır. Kitabın II hissəsi məhz bu mövzudur.
Vacib olan odur ki, universal scalable architecture yoxdur. Saniyədə 100 000 request, hər biri 1 KB, ilə dəqiqədə 3 request, hər biri 2 GB, eyni throughput-dur, amma tamam fərqli sistem tələb edir. Scalable architecture konkret load parameter-lər fərziyyəsi üzərində qurulur; fərziyyə səhv çıxarsa, iş boşa gedir. Sürətlə inkişaf edən məhsulda gələcəyin hipotetik yükü üçün deyil, növbəti funksionallıq üçün tez iterasiya etmək çox vaxt daha vacibdir.
Maintainability: operability
Software-in əsas xərci ilkin development deyil, davamlı baxımdır: bug-ları düzəltmək, sistemi işlək saxlamaq, failure-ları araşdırmaq, yeni platformalara uyğunlaşdırmaq, texniki borcu ödəmək. Maintainable sistem bu işi asanlaşdırır. Üç prinsip var.
Operability operations komandasının sistemi rahat işlək saxlaya bilməsidir. Yaxşı operations pis software-in çatışmazlıqlarını örtə bilir, amma yaxşı software pis operations ilə etibarlı işləmir. Operability-ni artıran şeylər: runtime davranışı və daxili vəziyyət barədə görünürlük, avtomatlaşdırma və standart alətlərlə inteqrasiya, tək maşından asılılığın olmaması, yaxşı sənədləşdirmə və aydın operational model, "default-lar yaxşıdır, amma lazım olanda override etmək mümkündür" prinsipi, özünü sağaldan amma lazım gələndə admin-ə nəzarət verən davranış, proqnozlaşdırıla bilən davranış.
Maintainability: sadəlik
Kiçik layihələr sadə kod ilə başlayır, böyüdükcə mürəkkəbləşir və anlaşılması çətinləşir. Bu mürəkkəblik baxımı yavaşladır, büdcəni və cədvəli pozur və dəyişiklik zamanı bug riski artırır. Mürəkkəbliyin əlamətləri: şişmiş state space, modullar arasında sıx coupling, qarışıq dependency-lər, ardıcıl olmayan adlandırma, bir problemi həll edən hack-lər, special-case-lər.
Burada accidental complexity anlayışı vacibdir: problemin öz mahiyyətindən gəlməyən, sadəcə implementasiyadan yaranan mürəkkəblik. Onu azaltmağın ən yaxşı aləti abstraction-dır. Yaxşı abstraksiya implementasiya detallarını təmiz fasad arxasında gizlədir və fərqli tətbiqlərdə təkrar istifadə olunur. SQL belə abstraksiyadır: disk-də data strukturlarını, konkurent sorğuları, crash-dən sonra bərpanı gizlədir. High-level proqramlaşdırma dilləri maşın kodunu gizlədir. Distributed sistemlər üçün bu səviyyədə yaxşı abstraksiyalar hələ az tapılır və kitab boyunca onları axtaracağıq.
Maintainability: evolvability
Sistemin tələbləri sabit qalmır: yeni fakt öyrənilir, biznes prioriteti dəyişir, istifadəçi yeni funksiya istəyir, platforma dəyişir, qanunvericilik yenilənir. Agile metodologiyası dəyişikliyə uyğunlaşma üçün kod səviyyəsində alətlər verir: TDD, refactoring. Böyük data sistemi səviyyəsində eyni ideyaya evolvability deyilir.
Data sistemini dəyişdirmək kodu dəyişdirməkdən çətindir, çünki data-nın özü də var: schema dəyişir, köhnə qeydlər yeni formatla oxunmalıdır, iki versiya eyni anda işləyir. Fəsil 4 bu problemi, yəni encoding və schema evolution-ı, ətraflı izah edir. Sadə və abstraksiyası yaxşı sistem dəyişdirməyə daha asandır; evolvability sadəliklə sıx bağlıdır.
Üç prinsip bir yerdə
Reliability, scalability və maintainability bir-birindən ayrı deyil. Bu üç suala cavab verərkən konkret texnologiya yox, dizayn seçimləri həlledicidir:
| Prinsip | Əsas sual | Tipik alətlər |
|---|---|---|
| Reliability | Fault baş verəndə sistem düzgün işləməyə davam edirmi? | Redundancy, replication, izolyasiya, test, telemetry, tez rollback |
| Scalability | Yük artanda performansı saxlamaq üçün nə etməliyik? | Load parameter-ləri ölçmək, percentil-lər, scaling out, partitioning |
| Maintainability | Yeni mühəndis sistemi başa düşüb dəyişə bilirmi? | Yaxşı abstraksiya, operability, sadə operational model, evolvable schema |
Qalan fəsillər bu sualları konkret komponentlər üzərində açır: əvvəlcə tək maşında data model-lər və storage engine-lər, sonra distributed data və nəhayət derived data sistemləri.
Tapşırıqlar
- Öz işlədiyiniz və ya yaxşı tanıdığınız bir sistem üçün ən azı üç load parameter yazın. Hər parametr on dəfə artsa, hansı komponent ilk olaraq darboğaz olar? Fan-out on read ilə fan-out on write nümunəsini bu sistemə tətbiq etmək mümkündürmü?
- Bir istifadəçi request-i paralel olaraq 20 backend çağırışı edir və hər çağırışın p99-u 300 ms-dir. Request-lərin təxminən neçə faizi 300 ms-dən yavaş olacaq? Bu rəqəmi aşağı salmaq üçün dizayn səviyyəsində hansı iki seçim var?
- Son bir ildə gördüyünüz production insidentlərini üç kateqoriyaya bölün: hardware, software, insan. Hər biri üçün fault-un failure-a çevrilməsinin qarşısını ala biləcək bir mexanizm təklif edin və onun qiymətini qiymətləndirin.