300 MB'lık bir sunumu dizüstü bilgisayarınızda kaydediyorsunuz ve bir dakika sonra telefonunuzda açılıyor. Sonra bir slayttaki yazım hatasını düzeltiyorsunuz ve güncelleme bir saniye sürüyor, çünkü yalnızca değişen kısım yolculuk ediyor. Bir dosya senkronizasyon servisi bunu kolay gösterir, ama altında iki çok farklı veri türü arasında dikkatli bir ayrım ve kararsız ağlara, çevrimdışı düzenlemelere ve günler sonra uyanan cihazlara dayanan bir protokol vardır.
Bu yazı Dropbox tarzı bir senkronizasyon servisi tasarlıyor: gereksinimler ve rakamlar, meta veri ile içerik arasındaki ayrım, parçalama ve içerik hash'leri, yükleme ve commit protokolü, meta veri günlüğü ve cursor'lar, bildirimler, çakışmalar ve çöp toplama, bir mimari diyagramla birlikte.
Gereksinimler
- Bir cihazda kaydedilen dosyalar, kullanıcının diğer cihazlarında saniyeler ile dakikalar içinde görünür.
- Yalnızca değişen veri yüklenir ve indirilir.
- Çevrimdışı yapılan düzenlemeler cihaz yeniden bağlandığında senkronize olur ve eşzamanlı düzenlemeler birbirinin üzerine asla sessizce yazmaz.
- Paylaşılan klasörler hesaplar arasında çalışır.
- Hiçbir zaman veri kaybı olmaz; kurtarma için sürüm geçmişi vardır.
Kapasite
Her biri günde 20 değişiklik senkronize eden 10 milyon aktif cihaz ve tekilleştirmeden sonra değişiklik başına ortalama 256 KB yeni veri varsayalım:
| Büyüklük | Hesap | Sonuç |
|---|---|---|
| Günlük değişiklik | 10M × 20 | 200 milyon |
| Meta veri commit'leri | 200.000.000 / 86.400 s | Saniyede ~2.300 |
| Yeni blok verisi | 200M × 256 KB | Günde ~51 TB |
Meta veri hızı mütevazıdır; veri hacmi büyüktür ama saklaması basittir. Tasarımı bu asimetri yönlendirir.
Meta veriyi içerikten ayırın
En önemli karar, dosya içeriklerini ve dosya meta verisini iki ayrı sistem olarak ele almaktır:
- İçerik, baytlarının hash'iyle adreslenen değişmez bloklar olarak saklanır. Bloklar büyüktür, bir kez yazılır ve nesne depolamada kolayca çoğaltılır.
- Meta veri, hangi yoldaki hangi dosyanın, hangi sürümde, hangi bloklardan oluştuğunu kaydeder. Küçüktür, sürekli değişir ve işlemlere (transaction) ve katı sıralamaya ihtiyaç duyar.
Dropbox bu ayrımı streaming sync yazısında anlattı: Her dosya 4 MB'lık bloklara bölünür, her blok SHA-256 ile hash'lenir ve bir dosyanın içeriği bu hash'lerin listesiyle, yani blok listesiyle tanımlanır. Herkese açık API aynı fikri hâlâ bir içerik hash'i olarak sunar: Her 4 MB'lık bloğun SHA-256'sı, ardından birleştirilmiş blok hash'lerinin SHA-256'sı.
Parçalama: sabit mi, içeriğe göre mi
Sabit boyutlu bloklar basittir ve hesaplaması hızlıdır. Zayıflıkları, bir dosyanın başına yakın bayt eklendiğinde ortaya çıkar: Eklemeden sonraki her blok sınırı kayar ve her blok hash'i değişir.
İçeriğe göre parçalama (content-defined chunking), sınırları içerik üzerindeki kayan bir hash'in bir desene uyduğu yerlere koyar; rsync'in kayan sağlama toplamı ve FastCDC gibi daha yeni algoritmalar bunu yapar. Sınırlar içerikle birlikte hareket eder, bu yüzden bir ekleme yalnızca çevresindeki parçaları değiştirir.
| 100 MB'lık bir dosyanın başına 10 bayt ekleme | Sabit 4 MB bloklar | İçeriğe göre, ortalama ~4 MB |
|---|---|---|
| Değişen parçalar | 25'in tamamı | Yaklaşık bir |
| Yüklenecek veri | ~100 MB | ~4 MB |
| Parçalamanın CPU maliyeti | Düşük | Daha yüksek: her bayt üzerinde kayan hash |
Çoğunlukla sonuna ekleme yapılan ya da toptan değiştirilen dosyalar için sabit bloklar yeterlidir. Büyük dokümanlar, disk imajları ya da veritabanları gibi ortasından düzenlenen dosyalar için içeriğe göre parçalama çok daha fazla bant genişliği kazandırır.
Yükleme ve commit protokolü
- Masaüstü istemcisi bir değişikliği işletim sisteminin dosya izleyicisi aracılığıyla fark eder, dosyanın değişmesi durana kadar bekler, sonra onu parçalar ve hash'ler.
- Meta veri servisine bir commit gönderir: bu yol, bu yeni blok listesi, üst sürüm N temel alınarak.
- Meta veri servisi hangi hash'leri zaten sakladığını kontrol eder ve ihtiyaç duyduklarıyla yanıt verir.
- İstemci yalnızca o blokları, paralel olarak ve her birini hash'iyle doğrulayarak blok servisine yükler. Kesilen bir yükleme eksik bloklardan devam eder.
- İstemci commit'i yeniden dener. Artık her blok mevcut olduğu için meta veri servisi dosyanın yeni bir sürümünü günlüğe ekler.
Güvenlik özelliği sıralamadır: Bloklara atıfta bulunan bir sürüm, hepsi saklanana kadar asla commit edilmez. Herhangi bir noktadaki çökme ya eski sürümü ya da yenisini bırakır; eksik veriyi gösteren bir dosyayı asla bırakmaz.
İçerik adresleme tekilleştirmeyi de bedavaya getirir. Üç klasöre kopyalanan ya da önceki bir sürüme döndürülen bir dosya yeni hiçbir şey yüklemez. Hesaplar arası tekilleştirme daha fazla alan kazandırır, ama başka birinin aynı dosyayı sakladığını ortaya çıkarabilir; bu yüzden birçok tasarım yalnızca bir hesap ya da paylaşılan klasör içinde tekilleştirir.
Meta veri günlüğü ve cursor'lar
Dosyaları bir kullanıcının kendi klasör ağacı ya da tek bir paylaşılan klasör gibi namespace'lerde gruplayın. Meta veri servisi her namespace için yalnızca ekleme yapılan bir günlük (journal) tutar: Her değişiklik yol, sürüm ve blok listesiyle birlikte bir sonraki sıra numarasını alır.
Her istemci namespace başına bir cursor saklar: uyguladığı son sıra numarası. Aşağı yönde senkronizasyon bu durumda tek bir sorudur: “Bu namespace'te cursor'ımdan sonraki her şeyi ver.” Günlük bunu ucuz, sıralı ve yeniden başlatılabilir kılar. Bir hafta çevrimdışı kalmış bir cihaz aynı soruyu sorar ve bir haftalık değişikliği alır. Meta veriyi namespace'e göre shard'layın, çünkü bir namespace'teki değişikliklerin başka bir namespace ile asla işlem paylaşması gerekmez.
Bildirimler: cihazlara senkronize olmalarını söylemek
Her boştaki cihazdan günlüğü yoklamak israf olur. Bunun yerine boştaki masaüstü istemcileri bir bildirim servisine long-poll bağlantısı tutar. Bir namespace'in günlüğü ilerlediğinde servis bekleyen isteklere “değişiklik var” yanıtı verir; her istemci de cursor'ından sonraki değişiklikleri listeler ve sahip olmadığı blokları indirir. Dropbox'ın herkese açık API'si aynı deseni list_folder/longpoll ve list_folder/continue olarak sunar.
Mobil uygulamalar arka planda bağlantıları açık tutamaz; bu yüzden platformun push bildirimlerine güvenir ve uygulama açıldığında senkronize olur.
İstemci tarafı
Masaüstü istemcisi sistemin yarısıdır. Her dosyanın son senkronize durumunu içeren yerel bir veritabanı tutar: yol, sürüm, blok listesi, boyut ve değiştirilme zamanı. Üç durumu karşılaştırmak her eylemi belirler:
| Yerel dosya / son senkronize | Uzak / son senkronize | Eylem |
|---|---|---|
| Değişmiş | Değişmemiş | Yükle |
| Değişmemiş | Değişmiş | İndir |
| Değişmiş | Değişmiş | Çakışma |
| Değişmemiş | Değişmemiş | Hiçbir şey |
Yerel bir blok önbelleği, cihazın başka dosyalarda zaten tuttuğu blokları indirmeyi önler.
Çakışmalar
İki cihaz çevrimdışıyken aynı dosyayı düzenler. İkisi de sonra üst sürüm N ile commit eder. Meta veri servisi ilk commit'i kabul edip N+1 sürümünü oluşturur ve ikincisini reddeder, çünkü üst sürümü artık güncel değildir. Anlayamadığı ikili dosyaları birleştirmek yerine kaybeden istemci kendi sürümünü orijinalin yanına çakışmalı kopya olarak kaydeder ve ikisini de senkronize eder. Kararı kullanıcı verir.
Bu iyimser eşzamanlılıktır: Her commit temel aldığı sürümü belirtir. Aynı kontrol, pratikte daha zor olan dizin işlemlerini de ele alır: bir cihazda yeniden adlandırma ve diğerinde düzenleme ya da bir düzenlemeyle yarışan bir silme. Bu durumları açıkça yazın; senkronizasyon motorlarının veri kaybettiği yer onlardır.
Sürüm geçmişi ve çöp toplama
Eski sürümler blok listelerini korur; bu yüzden bir dosyayı geri yüklemek, eski bir blok listesini en yeni sürüm olarak commit etmekten ibarettir. Bloklar ancak saklama süresi içindeki hiçbir sürüm onlara atıfta bulunmadığında çöp olur. Çöp toplamayı bir bekleme süresiyle mark-and-sweep olarak çalıştırın, asla son referans kalkınca hemen silme şeklinde değil; çünkü bir istemci, sunucunun sahipsiz sandığı bir bloğu yeniden kullanan bir blok listesini commit etmek üzere olabilir.
Dosya senkronizasyonu diyagramı
Diyagram içeriği
- Laptop (writer)
- Phone (reader)
- Sync backend
- API gateway
- Metadata service
- Notification service
- Block service
- Journal per namespace
- journal events
- Blocks by SHA-256 (4 MB)
- 1, 3, 4
- 5. changes available
- 6. list after cursor, get blocks
- 1. commit (parent v7)
- 2. missing hashes
- 3. upload missing blocks
- 5. answer long polls
- 4. append v8
- namespace changed
- wake
- put by hash
Nasıl okunur:
- Soldaki laptop meta veri servisine bir blok listesi commit eder ve sunucuda eksik olan hash'leri geri alır.
- Blok yüklemeleri blok servisine gider ve hash'e göre anahtarlanmış olarak nesne depolamaya düşer.
- Meta veri servisi yeni sürümü Postgres'teki namespace günlüğüne ekler ve değişikliği yayımlar.
- Bildirim servisi telefonun long poll isteğini yanıtlar; telefon cursor'ından sonraki değişiklikleri listeler ve yalnızca eksik blokları indirir.
- Markdown bloğu altı protokol adımını sırayla listeler.
Senkronizasyon sistemlerini net çizmek
- Meta veriyi ve içeriği ayrı yollar olarak çizin. Farklı depolar, farklı ölçekleme, farklı arıza biçimleri.
- Protokol adımlarını kenarların üzerinde numaralandırın. Commit, eksik hash'ler, yükleme, yeniden commit: Doğruluk argümanı sıralamanın kendisidir.
- İkinci cihazı gösterin. Senkronizasyon yazan kadar okuyanla da ilgilidir; tek istemcili bir diyagram bildirimleri ve cursor'ları gizler.
- Bölümleme anahtarını etiketleyin: meta veri için namespace, bloklar için hash.
Genel bir yaklaşım için sistem tasarımı diyagramı örneğine bakın. Push tarafı bildirim sistemi tasarımına benzer; nesne depolamaya parçalı yüklemeler video akışı mimari diyagramında da görünür ve meta veriyi bölümlemek veritabanı sharding diyagramını izler.
Archboard'da oluşturun
- Google ya da GitHub ile giriş yapın ve bir diyagram oluşturun.
- Ağ paketinden iki Cihaz düğümü ekleyin: bir laptop ve bir telefon.
- Senkronizasyon arka ucu için bir Temel Çerçeve ekleyin ve kenarına Bulut paketinden bir API ağ geçidi koyun.
- Çerçevenin içine meta veri, blok ve bildirim servisleri için Bulut paketinden üç Hesaplama düğümü, günlük için Veri paketinden bir Postgres düğümü, bloklar için Bulut paketinden bir Nesne depolama düğümü ve günlük olayları için Veri paketinden bir Kafka düğümü ekleyin.
- Kenarları protokol sırasına göre numaralandırın: commit, eksik hash'ler, yükleme, yeniden commit, bildirim, listeleme ve indirme.
- Temel paketinden protokolü ve kapasiteyi içeren bir Markdown bloğu ekleyin.
Örnek 11 diyagram düğümü kullanıyor ve Free planın sınırları içinde kalıyor.
SSS
Senkronizasyon servisleri dosyaları neden bloklara böler? Bloklar, istemcinin yalnızca değişeni yüklemesini, kesilen aktarımlara devam etmesini ve aynı veriyi tekilleştirmesini sağlar. Bir bloğun hash'i onun kimliğidir; böylece sunucu hangi bloklara zaten sahip olduğunu hemen anlayabilir.
Sabit boyutlu mu yoksa içeriğe göre parçalama mı kullanmalıyım? Sabit boyutlu bloklar daha basittir ve hesaplaması daha ucuzdur. İçeriğe göre parçalama, bir dosyanın ortasına bayt eklendiğinde parça sınırlarını sabit tutar; bu da yerinde düzenlenen dosyalarda bant genişliği kazandırır.
Bir istemci sunucuda neyin değiştiğini nasıl bilir? Namespace başına bir cursor tutar ve meta veri servisinden ondan sonraki her günlük girdisini ister. Ne zaman soracağını bir long-poll ya da push bildirimi söyler.
Çakışan düzenlemeler nasıl ele alınır? Her commit temel aldığı sürümü belirtir. Başka bir cihaz önce commit ettiyse commit başarısız olur ve istemci, diğerinin üzerine yazmak yerine kendi düzenlemesini çakışmalı kopya olarak saklar.
Tekilleştirme kullanıcılar arasında çalışır mı? Çalışabilir, ama başka bir hesabın aynı dosyayı sakladığını ortaya çıkarabilir ve kullanıcı başına şifrelemeyle çalışmaz. Birçok servis tekilleştirmeyi tek bir hesapla ya da paylaşılan klasörle sınırlar.