Data at scale
Data at scale
Kodu yenidən yazmaq olar. Service-i bir həftəyə başqa dilə köçürmək, framework-ü dəyişmək, monolith-i hissələrə bölmək mümkündür. Data isə qalır. Terabaytlarla data-nı yeni storage-ə köçürmək aylarla dual write, backfill, reconciliation və "köhnə sistemi nə vaxt söndürürük?" sualı deməkdir. Ch01-dəki testlə desək: geri qaytarılması ən baha olan qərarlar data qatında verilir.
Bu fəsil həmin qərarları sıralayır: hansı storage, hansı index, data-nı necə çoxaltmalı və necə bölməli, istifadəçi hansı consistency-ni görəcək. Məqsəd hər texnologiyanı öyrətmək deyil, hər birinin nə verdiyini və nəyə başa gəldiyini aydın görməkdir.
Storage seçimi access pattern-dən başlayır
"SQL, yoxsa NoSQL?" sualı yanlış başlanğıcdır. Doğru sual budur: data necə oxunacaq və necə yazılacaq? Hansı key ilə axtarılacaq, hansı sorğular hər saniyə, hansıları gündə bir dəfə işləyəcək, hansı əməliyyatlar atomik olmalıdır?
| Access pattern | Uyğun storage növü | Nə verir | Nəyə başa gəlir |
|---|---|---|---|
| Ad-hoc sorğular, join-lər, çox cədvəlli transaction-lar | Relational (Postgres, MySQL) | Çevik sorğular, güclü constraint-lər, ACID | Horizontal scaling çətindir |
| Key ilə tək obyekt oxumaq/yazmaq, session, counter | Key-value (Redis, DynamoDB) | Çox aşağı latency, sadə scaling | Key-dən başqa heç nə ilə axtarmaq olmur |
| Bütöv obyekti bir dəfəyə oxumaq, strukturu dəyişkən sənədlər | Document (MongoDB) | Obyekt modelinə yaxın, nested data | Sənədlər arası join və transaction zəifdir |
| Böyük həcmli yazma, zaman sırası, partition key ilə range sorğusu | Wide-column (Cassandra, Bigtable) | Nəhəng write throughput | Sorğular əvvəlcədən bilinməlidir; data sorğuya görə modelləşdirilir |
| Əlaqələr üzərində çox addımlı keçid (dost-dostu, fraud halqası) | Graph (Neo4j) | Dərin traversal sürətlidir | Niş alət, ümumi məqsədli deyil |
| Böyük fayllar: şəkil, video, backup | Object storage (S3) | Ucuz, praktiki olaraq limitsiz | Sorğu yoxdur, yalnız key ilə oxuma |
Cədvəldən iki dərs çıxır.
Birincisi, "NoSQL-də schema yoxdur" mifdir. Schema həmişə var; sual onun harada yoxlanmasıdır. Relational database-də o, yazma anında yoxlanır (schema-on-write). Document store-da isə onu oxuyan kod gözləyir (schema-on-read): üç il əvvəl yazılmış sənəddə address field-i string idi, indi object-dir və bunu hər reader bilməlidir. Schema yox olmur, sadəcə database-dən application koduna köçür.
İkincisi, Postgres ilə çox uzağa getmək olar. JSONB sütunları document ehtiyacını, yaxşı index-lər key-value ehtiyacının çoxunu, read replica-lar isə oxuma yükünü qarşılayır. NoSQL database-ə keçid real ehtiyacdan doğmalıdır: tək node-un write limitinə çatmaq, multi-region yazma, konkret access pattern üçün ölçülmüş üstünlük. "Gələcəkdə scale lazım ola bilər" bəhanəsi isə bu gün join-ləri və transaction-ları itirmək deməkdir.
Index-lər: oxumanı alıb yazmadan ödəmək
Database index cədvəlin üzərində saxlanan əlavə data strukturudur və müəyyən sütunla axtarışı tam cədvəli skan etmədən etməyə imkan verir. Bütün storage engine-lər iki ailədən birinə yaxındır:
- B-tree. Data sıralı ağacda saxlanır, yerində yenilənir. Oxuma sürətli və proqnozlaşdırılandır. Relational database-lərin standartıdır.
- LSM-tree. Yazmalar əvvəl yaddaşa, sonra diskə ardıcıl fayllar kimi yazılır və fon rejimində birləşdirilir (compaction). Yazma çox ucuzdur, oxuma isə bir neçə faylı yoxlamalı ola bilər. Cassandra, RocksDB və bir çox wide-column store bunun üzərində qurulub.
Seçim yenə access pattern-dən asılıdır: oxuma ağır yük B-tree-yə, yazma ağır yük (log-lar, metrikalar, event-lər) LSM-tree-yə meyl edir.
Unudulan hissə: hər index hər yazmanı yavaşladır. Cədvəldə yeddi index varsa, bir insert yeddi strukturu yeniləyir. Index-lər həm də disk və yaddaş tutur. Buna görə hər index-in arxasında konkret sorğu olmalıdır, "bəlkə lazım olar" yox.
Composite index-də sütunların sırası vacibdir. (tenant_id, created_at) index-i "bu tenant-ın son order-ləri" sorğusuna kömək edir, amma yalnız created_at üzrə axtarışa kömək etmir. Telefon kitabçası soyada görə sıralanıb; adla axtarmaq üçün yenə hamısını vərəqləməlisiniz.
Replication: eyni data, bir neçə yerdə
Replication eyni data-nın bir neçə node-da saxlanmasıdır. O, üç tactic-i birdən verir: availability (bir node düşəndə digəri var), oxuma scalability-si (read-ləri replica-lara paylamaq) və latency (data istifadəçiyə yaxın region-da). Üç əsas model var:
- Leader-follower. Bütün yazmalar bir leader-ə gedir, follower-lər onun dəyişikliklərini təkrarlayır. Ən sadə və ən geniş yayılmış modeldir. Leader düşəndə follower-lərdən biri failover ilə leader olur (ch07).
- Multi-leader. Hər region-un öz leader-i var və yazmaları qəbul edir. Multi-region yazmanı mümkün edir, amma eyni sətrin iki yerdə eyni anda dəyişməsi conflict yaradır və onun həlli sizin probleminizdir.
- Leaderless. Client bir neçə node-a yazır və bir neçəsindən oxuyur. Quorum qaydası: N replica varsa, W yazma və R oxuma təsdiqi üçün W + R > N olanda oxuma ən azı bir təzə nüsxəyə rast gəlir. Cassandra və DynamoDB bu ailədəndir.
İkinci seçim replication-ın synchronous, yoxsa asynchronous olmasıdır. Synchronous replication-da leader follower-in təsdiqini gözləyir: data itmir, amma hər yazma ən yavaş replica qədər yavaşdır və o replica düşəndə yazmalar dayanır. Asynchronous-da yazma dərhal təsdiqlənir, amma leader təsdiqdən dərhal sonra düşsə, son yazmalar itə bilər. Praktikada çox vaxt arada qalırlar: bir follower sync, qalanları async.
Async replication-ın gündəlik qiyməti replication lag-dır: follower leader-dən bir neçə millisaniyə, yük altında isə saniyələrlə geri qala bilər. İstifadəçi bunu belə görür:
İstifadəçi profilində adını dəyişir və "Yadda saxla" düyməsini basır. Yazma leader-ə gedir və uğurla bitir. Səhifə yenilənir, oxuma isə load balancer vasitəsilə hələ dəyişikliyi almamış replica-ya düşür. Ekranda köhnə ad görünür. İstifadəçi düşünür ki, saxlanmadı, və yenidən cəhd edir. Support-a "sayt işləmir" müraciəti gəlir. Sistemdə heç bir xəta yoxdur.
Bu, read-your-writes zəmanətinin pozulmasıdır. Tipik həllər: istifadəçinin öz data-sını son bir neçə saniyə ərzində leader-dən oxumaq, client-də son yazmanın versiyasını saxlayıb replica-dan ən azı o versiyanı tələb etmək, və ya UI-da dəyişikliyi optimistic göstərmək.
Partitioning və sharding: son çarə
Sharding data-nı hissələrə bölüb hər hissəni ayrı node-da saxlamaqdır. Replication eyni data-nı çoxaldır; sharding isə fərqli data-nı paylayır. O, tək node-un yaza bildiyindən çox yazmağın və tək diskə sığmayan data-nı saxlamağın yeganə yoludur.
Amma o, ən baha scaling addımıdır. Əvvəl ucuz addımları tükəndirin:
- Vertical scaling. Daha böyük server. Bu gün tək bir database server-i terabaytlarla RAM ilə gəlir.
- Read replica-lar. Yük oxuma ağırdırsa, problemin çoxu burada həll olunur.
- Cache. Ən çox oxunan data-nı database-ə heç çatdırmamaq.
- Köhnə data-nı arxivləşdirmək. Çox vaxt cədvəlin 90%-i heç kimin oxumadığı tarixçədir.
Bunlar kifayət etməyəndə əsas qərar partition key-dir: data hansı dəyərə görə bölünəcək. Bölmə üsulu iki cürdür:
- Range partitioning. Key aralıqlarına görə (A–F bir shard-da, G–M digərində; və ya tarixə görə). Range sorğuları effektivdir, amma ardıcıl key-lər (məsələn timestamp) bütün yeni yazmaları bir shard-a yığır.
- Hash partitioning. Key-in hash-inə görə. Yük bərabər paylanır, amma range sorğusu bütün shard-lara getməlidir.
Sadə hash(key) mod N sxeminin problemi N dəyişəndə üzə çıxır: bir shard əlavə etmək demək olar ki, bütün key-lərin yerini dəyişir. Consistent hashing key-ləri və node-ları eyni halqa üzərində yerləşdirir ki, node əlavə edəndə key-lərin yalnız kiçik hissəsi köçsün.
Hash yükü bərabər paylayır, amma key-lər bərabər deyil. Bir məşhur istifadəçinin profili, bir viral post və ya bir nəhəng tenant bir shard-ı yandırır, digərləri isə boş dayanır. Bu, hot partition (və ya "celebrity problem") adlanır. Həlli adətən həmin key-ə xüsusi münasibətdir: əlavə suffix ilə bölmək, ayrıca cache, və ya ayrıca shard.
Sharding-in davamlı qiymətləri:
- Cross-shard sorğular. Partition key-i daxil etməyən hər sorğu bütün shard-lara gedir (scatter-gather) və ən yavaş shard qədər yavaşdır.
- Cross-shard transaction-lar. İki shard-a atomik yazmaq two-phase commit və ya saga tələb edir. Sonuncuya distributed data pattern-ləri fəslində qayıdacağıq.
- Resharding. Partition key səhv seçiləndə onu dəyişmək bütün data-nı canlı sistemdə yenidən paylamaq deməkdir. Bu, aylarla çəkən layihədir.
Partition key məhz ADR-ə layiq qərardır:
# ADR-0014: Order data-sının tenant_id ilə shard edilməsi
## Status
Accepted (2026-10-28).
## Context
Multi-tenant SaaS. Orders cədvəli 4 TB-dır və ayda 8% böyüyür; tək
Postgres primary-nin write CPU-su peak-də 85%-dir. Read replica-lar və
arxivləşdirmə tətbiq edilib. Sorğuların 97%-i bir tenant daxilindədir.
Ən böyük tenant ümumi yükün 6%-ni yaradır.
## Decision
Orders data-sı tenant_id ilə shard edilir. Tenant → shard xəritəsi
ayrıca directory cədvəlində saxlanır (hash yox), ki, tenant-ları
fərdi olaraq köçürmək mümkün olsun. Başlanğıcda 16 shard.
## Consequences
+ Tenant daxilindəki sorğu və transaction-lar tək shard-da qalır.
+ Böyük tenant-ı ayrıca shard-a köçürmək mümkündür (hot partition üçün çıxış yolu).
+ Tenant-ın data-sını silmək və ya region-a köçürmək sadələşir.
- Bütün tenant-lar üzrə hesabatlar scatter-gather tələb edir; onlar
data warehouse-a köçürülür.
- Directory cədvəli hər request-in yolundadır; cache-lənməlidir.
- Bir tenant tək shard-ın tutumunu aşarsa, bu sxem kifayət etməyəcək.
## Alternatives considered
- order_id ilə hash sharding: yük daha bərabər, amma tenant daxilindəki
hər siyahı sorğusu bütün shard-lara gedir. Rədd edilib.
- Distributed SQL (CockroachDB və s.): sharding-i şəffaf edir; komanda
onu operate etməyib, latency profili ölçülməyib. 12 aydan sonra yenidən baxılsın.
- Daha böyük instance: 6–9 ay qazandırır; aralıq addım kimi tətbiq edilib.
CAP-dan PACELC-ə
CAP teoremi (ch02-də qısaca toxunduq) deyir ki, network partition zamanı consistency ilə availability arasında seçim etməlisiniz. Faydalı, amma natamamdır: partition-lar nadirdir, sistem isə vaxtın 99.9%-ini normal rejimdə keçirir.
PACELC boşluğu doldurur: Partition olanda Availability və Consistency arasında seçim edirsiniz; Else (normal vaxtda) isə Latency və Consistency arasında. Güclü consistency hər yazmada replica-lar arasında koordinasiya deməkdir. Replica-lar fərqli region-lardadırsa, bu, hər yazmaya on, bəlkə yüz millisaniyə əlavə edir.
Bu çərçivə database-ləri daha dürüst təsvir edir. Leader-dən sync oxuyan klassik relational setup PC/EC-dir: hər iki halda consistency seçir. Default konfiqurasiyada Cassandra və DynamoDB PA/EL-dir: hər iki halda sürət və cavab seçir. Bir çoxu isə bunu sorğu səviyyəsində tənzimləməyə icazə verir. Bu, yaxşı xəbərdir: seçimi bütün database üçün yox, hər əməliyyat üçün etmək olar.
Consistency modelləri istifadəçinin gözü ilə
"Strong" və "eventual" arasında bir neçə praktik pillə var. Onları texniki tərifdən yox, istifadəçinin nə gördüyündən başlayaraq seçmək daha asandır:
- Strong (linearizable). Sistem özünü data-nın tək nüsxəsi varmış kimi aparır: yazma bitən kimi hər kəs onu görür. Lazımdır: hesab balansı, son bilet, unikal username. Qiyməti: koordinasiya, latency və partition zamanı availability.
- Read-your-writes. İstifadəçi öz dəyişikliyini dərhal görür, başqaları bir az sonra. Lazımdır: profil, şərh, parametrlər. Yuxarıdakı ad nümunəsi məhz bunun pozulmasıdır.
- Monotonic reads. İstifadəçi zamanda geriyə getmir. Pozulanda belə görünür: şərh bir yeniləmədə var, növbətidə yoxdur, sonra yenə peyda olur, çünki oxumalar fərqli lag-ı olan replica-lara düşür. Adi həll istifadəçini eyni replica-ya bağlamaqdır.
- Eventual. Yazmalar dayansa, nüsxələr zamanla eyniləşəcək. Lazımdır: like sayı, baxış sayı, tövsiyələr. Ən ucuz və ən sürətli variantdır.
Ch07-dəki availability qaydası burada da işləyir: consistency bütün sistem üçün bir seçim deyil. Bir səhifədə balans strong, şərhlər read-your-writes, like sayı isə eventual ola bilər. Hər data set üçün soruşun: köhnə dəyər görmək burada nəyə başa gəlir?
Unstructured data: object storage
Şəkillər, video-lar, PDF-lər, backup-lar database-də yer almır. Onları BLOB sütununda saxlamaq database-i şişirdir, backup-ı yavaşladır və bahalı storage-i ucuz data ilə doldurur. Doğru yer object storage-dir (S3 və analoqları): ucuz, praktiki olaraq limitsiz və çox yüksək durability ilə.
Standart pattern blob ilə metadata-nın ayrılmasıdır. Faylın özü object storage-də key ilə saxlanır. Onun haqqında olan hər şey (sahibi, ölçüsü, yüklənmə tarixi, giriş icazələri, object key) database-də saxlanır. Axtarış, icazə və əlaqələr database-də, bayt-lar isə object storage-də qalır.
Faylları application server-dən keçirmək də lazım deyil. Presigned URL ilə server client-ə konkret object üçün qısa müddətli, imzalanmış link verir və client faylı birbaşa storage-ə yükləyir və ya oradan endirir. Server yalnız icazəni verir; gigabaytlar onun üzərindən keçmir. Oxuma tərəfində bunun önünə adətən CDN qoyulur.
Hazırkı sisteminiz haqqında suallar
- Ən böyük üç cədvəlinizin əsas access pattern-lərini yazın. Storage seçiminiz onlara uyğundurmu, yoxsa sadəcə komandanın bildiyi alətdir?
- Replica-dan oxuduğunuz yerləri tapın. İstifadəçi öz yazdığını görməyəndə nə baş verir və bunu bu gün test edirsinizmi?
- Sharding-ə ehtiyacınız olduğunu düşünürsünüzsə: vertical scaling, read replica, cache və arxivləşdirmə tükənibmi? Partition key nə olardı və hansı sorğular onu daxil etmir?
- Hansı data set-iniz üçün consistency səviyyəsini explicit seçmisiniz, hansı üçün isə database-in default-u sizin yerinizə seçib?