Dosya İşlemede Atomik Yayınlama
Geçici dosya ve atomik rename yaklaşımını atomiklik, görünürlük ve kalıcılık garantileri açısından inceler. NFS cache davranışı, fsync, manifest, çökme senaryoları ve çoklu yazıcı koordinasyonu ele alınır.
Canlı veri işlemede dosyasını geçici adla yazıp ardından hedef ada taşımak, okuyucunun yarım içerik görmesini önleyen yaygın bir yayın yöntemidir:
veriyi oluştur geçici dosyaya yaz dosyayı kapat geçici dosyayı hedef ada taşı
Yerel dosya sisteminde bu işlem çoğu zaman yeterli görünür. Aynı düzen NFS üzerinde kullanıldığında ise üç ayrı garanti birbirine karışabilir:
atomiklik görünürlük kalıcılık
Atomiklik, ad değişiminin ara durum göstermeden gerçekleşmesini ifade eder. Görünürlük, diğer istemcilerin yeni dosyayı ne zaman gördüğüyle ilgilidir. Kalıcılık ise sunucu veya istemci çöktüğünde verinin depolama üzerinde korunup korunmadığını belirler.
Bir "rename" işleminin atomik olması, dosyanın bütün NFS istemcilerinde aynı anda görünür hale geldiğini veya elektrik kesintisinden sonra kesin olarak korunacağını göstermez. Güvenilir dosya yayını bu garantileri ayrı ayrı kurmalıdır.
Geçici dosyadan hedef dosyaya geçiş
Doğrudan hedef dosyaya yazılan bir süreç şu ara durumları dışarı açar:
0 baytlık dosya kısmen yazılmış JSON tamamlanmamış UTF-8 dizisi kapanmamış JSON nesnesi eski dosyanın üzerine kısmi yazım
Okuyucu dosyayı yazım sürerken açarsa geçerli bir dizin girdisi bulur, fakat içerik henüz tamamlanmamıştır. Dosya boyutu denetimi de yeterli değildir. Yazım ilerledikçe boyut değişebilir ve belirli bir anda makul görünen içerik yine eksik olabilir.
Geçici dosya kullanıldığında hedef ad yalnızca içerik tamamlandıktan sonra yayımlanır:
report.json.tmp ↓ report.json
Linux "rename" işlemi başarılı olduğunda hedef adın değiştirilmesini atomik olarak gerçekleştirir. Hedef zaten varsa işlem sırasında başka bir sürecin hedef adı bulamadığı bir boşluk oluşmaz. Kaynak ve hedef farklı bağlı dosya sistemlerindeyse işlem "EXDEV" hatası verebilir. Bu nedenle geçici dosyanın hedef dosyayla aynı dizinde oluşturulması, aynı dosya sistemi üzerinde kalmayı sağlayan en sade yöntemdir.
Java tarafında karşılığı şöyledir:
final Path temporary = target.resolveSibling(target.getFileName() + ".tmp");write(temporary, content);
Files.move( temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING );
"ATOMIC_MOVE", taşımanın dosya sistemi tarafından tek bir atomik işlem olarak yapılmasını ister. Sağlayıcı bunu desteklemiyorsa "AtomicMoveNotSupportedException" üretir. Atomik seçenek kullanılmadan yapılan taşıma hata verirse kaynak ve hedef dosyanın son durumu tanımlı olmayabilir. Java API bu nedenle atomik taşımanın sessizce varsayılmasına izin vermez.
Güvenilir bir uygulama "ATOMIC_MOVE" başarısız olduğunda doğrudan normal taşımaya geçmemelidir. Böyle bir geri dönüş, hedef dosyanın tamamlanmış çıktı anlamına geldiği sözleşmeyi bozar. Atomik taşıma desteklenmiyorsa işlem başarısız sayılabilir veya ayrı bir commit protokolü kullanılabilir.
Atomiklik kalıcılık değildir
Geçici dosyanın hedef ada atomik biçimde taşınması, hedef adın işlem sırasında tutarlı görünmesini sağlar. Verinin fiziksel depolamaya ulaştığını tek başına garanti etmez.
İşletim sistemi yazılan veriyi önce sayfa önbelleğinde tutabilir:
uygulama → işletim sistemi önbelleği → dosya sistemi → depolama denetleyicisi → kalıcı ortam
"write" çağrısının tamamlanması, bu zincirin sonuna ulaşıldığı anlamına gelmez. "close" da her ortamda kalıcı depolama garantisi olarak yorumlanmamalıdır.
Linux "fsync", dosyanın değiştirilen verisini ve ilişkili metadata bilgisini depolama aygıtına aktarmayı ister. Ancak dosyaya uygulanan "fsync", dosyanın dizin girdisinin de kalıcı hale geldiğini garanti etmez. Yeni dosya oluşturma veya "rename" sonrasında dizin girdisinin korunması için üst dizinin de ayrıca senkronize edilmesi gerekir.
Yerel bir dosya sistemi için güçlü yayın sırası şu şekilde kurulabilir:
- Geçici dosyayı oluştur
- İçeriği yaz
- Geçici dosyayı fsync et
- Geçici dosyayı hedef ada atomik taşı
- Üst dizini fsync et
Üçüncü adım dosya içeriğini korur. Beşinci adım yeni hedef adın veya değişen dizin girdisinin çökme sonrasında kaybolma riskini azaltır.
Java "FileChannel.force(true)" dosya içeriği ve metadata için senkronizasyon isteği gönderebilir. Dizin senkronizasyonu ise standart Java API içinde bütün platformlarda aynı davranışı garanti eden taşınabilir bir işlem değildir. Linux üzerinde dizini "FileChannel" ile açma gibi yöntemler kullanılabilse de bunların geçerliliği dosya sistemi sağlayıcısına ve işletim sistemine bağlıdır. Uygulamanın gerektirdiği garanti, yalnızca Java kodundan değil, hedef platformdaki gerçek dosya sistemi davranışından doğrulanmalıdır.
NFS üzerinde kalıcılık daha fazla katman içerir. NFS protokolü istemci ve sunucu arasındaki yazma işlemlerinde verinin kararlı depolamaya ulaşıp ulaşmadığını ayrıca ele alır. NFSv3, "UNSTABLE" yazımların daha sonra "COMMIT" işlemiyle kararlı depolamaya alınmasına izin verir. NFSv4 tarafında "FILE_SYNC4", sunucunun veri ile gerekli metadata bilgisini kararlı depolamaya aktarmadan başarılı cevap vermemesini gerektirir.
Uygulama çoğu zaman bu protokol ayrıntılarını doğrudan yönetmez. NFS istemcisi, çekirdek, mount seçenekleri ve sunucu birlikte davranır. "fsync" çağrısı bu katmanlara kalıcılık talebini iletir, fakat gerçek garanti sunucu uygulamasına ve depolama altyapısına da bağlıdır.
Sunucunun yazma önbelleği, RAID denetleyicisi veya sanal depolama katmanı veriyi kalıcı kabul edip gerçekte uçucu bellekte tutuyorsa uygulama düzeyindeki çağrılar tek başına elektrik kesintisine karşı koruma sağlayamaz. Kalıcılık sözleşmesi altyapı seviyesinde de doğrulanmalıdır.
NFS istemcilerinde görünürlük
Bir istemci hedef dosyayı yayımladıktan sonra başka bir istemcinin dosyayı ne zaman göreceği, yalnızca "rename" atomikliğine bağlı değildir. NFS istemcileri veri ve dosya özniteliklerini yerel olarak önbelleğe alır.
Linux NFS istemcisinin varsayılan davranışı close-to-open consistency olarak adlandırılır. Yazıcı dosyayı kapatırken bekleyen değişiklikleri sunucuya gönderir. Başka bir istemci dosyayı açarken önbelleğindeki özniteliklerin güncelliğini denetleme fırsatı bulur. Bu model, bir istemcinin kapattığı dosyanın sonraki açılışta diğer istemci tarafından görülmesini hedefler. Yine de istemcilerdeki öznitelik önbellekleri tam anlamıyla eş zamanlı değildir ve bazı koşullarda eski veri görülebilir.
NFSv4, "RENAME" işleminin istemci açısından atomik olmasını ister. Okuyucu eski adı veya yeni adı görebilir, fakat protokol düzeyinde yarım bir ad değişimi gözlemlememelidir. Bu garanti, farklı istemcilerin dizin önbelleklerini aynı anda yenileyeceği anlamına gelmez.
Bir okuyucu dosyayı sürekli açık tutuyorsa yeni hedef dosyanın içeriğine otomatik olarak geçmesi de beklenmemelidir. "rename", mevcut dosya adını yeni inode veya dosya nesnesiyle ilişkilendirir. Önceden açılmış dosya tanıtıcısı eski nesneyi okumayı sürdürebilir. Yeni sürümü görmek isteyen okuyucu dosyayı yeniden açmalıdır.
Güvenli tüketici akışı şu biçimde kurulabilir:
dizin veya manifest değişikliğini algıla hedef dosyayı yeniden aç içeriği baştan sona oku dosyayı kapat
Dosyayı sürekli açık tutup aynı tanıtıcı üzerinden yeni yayınları izlemek, atomik değiştirme modeline uygun değildir.
Dizin listeleme de ayrı bir önbellek davranışı gösterebilir. Bir istemci kısa süre önce bulunmayan bir dosyayı aradıysa negatif lookup sonucu önbellekte kalabilir. Attribute cache sürelerini azaltmak görünürlüğü hızlandırabilir, fakat sunucuya gönderilen metadata çağrılarını artırır. "noac" veya "actimeo=0" gibi seçenekler performans maliyeti taşır ve bütün tutarlılık sorunlarını genel biçimde çözmez. Linux NFS belgeleri, öznitelik önbelleğinin performans ile güncellik arasında denge kurduğunu açıkça belirtir.
Bu nedenle mount ayarını değiştirmeden önce uygulama protokolünü düzeltmek daha güvenlidir. Okuyucu, tamamlanmış dosyanın biraz gecikmeli görünmesini tolere edebiliyorsa varsayılan cache davranışı daha yüksek throughput sağlar. Milisaniye düzeyinde anlık çapraz istemci görünürlüğü gerekiyorsa NFS dizin taraması uygun bildirim mekanizması olmayabilir.
Commit dosyası ve manifest kullanımı
Tek veri dosyasının varlığı tamamlanmış yayın anlamına gelecekse atomik taşıma yeterli bir commit noktası olabilir:
daily.json.tmp → daily.json
Bir yayın birden fazla dosyadan oluşuyorsa sorun değişir:
schema.json data.json summary.json
Bu dosyalar ayrı ayrı atomik taşınsa bile okuyucu farklı sürümleri birlikte görebilir:
yeni schema.json eski data.json yeni summary.json
Dosya başına atomiklik, dosya kümesi için transaction sağlamaz.
Çözüm, sürümlü dizin veya manifest kullanmaktır:
/releases/104/schema.json /releases/104/data.json /releases/104/summary.json /current.json
Yazar önce "104" sürümüne ait bütün dosyaları oluşturur. Dosyalar tamamlandıktan sonra küçük bir manifest yayımlar:
{ "version": 104, "path": "/releases/104" }
Okuyucu önce "current.json" dosyasını açar, ardından belirtilen sürüm dizinindeki dosyaları okur. Commit noktası yalnızca manifestin atomik değişimidir.
Bu model iki yarar sağlar. Birincisi, çoklu dosya yayını tek sürüm anahtarıyla tutarlı hale gelir. İkincisi, eski sürüm hemen silinmek zorunda kalmaz. Manifesti daha önce okuyan istemciler eski dizinden okumayı tamamlayabilir.
Eski sürümlerin temizlenmesi için güvenli bir bekleme süresi veya okuyucu referans modeli gerekir:
yayınla manifesti değiştir okuyucuların geçiş süresini bekle eski sürümü temizle
Ayrı ".commit" dosyası da kullanılabilir:
data.json data.json.commit
Okuyucu yalnızca iki dosya da varsa veriyi tamamlanmış sayar. Fakat commit dosyasının oluşturulması da aynı atomiklik ve görünürlük kurallarına tabidir. Çoklu dosya yapısında manifest, her dosya için ayrı commit işareti kullanmaktan daha açık bir sürüm sözleşmesi oluşturur.
Çökme senaryoları
Yayın protokolü normal akıştan çok, her adımda oluşabilecek kesintiler üzerinden değerlendirilmelidir.
Geçici dosya yazılırken süreç kapanırsa hedef dosya değişmez. Kalan ".tmp" dosyası sonraki başlangıçta temizlenebilir veya üzerine yazılabilir.
Geçici dosya tamamlandıktan sonra taşıma öncesinde süreç kapanırsa yine eski hedef geçerlidir. Yeni veri yayımlanmamıştır.
Atomik taşıma tamamlandıktan sonra süreç kapanırsa okuyucular yeni hedefi görür. Ancak dosya ve dizin kalıcılığı sağlanmadıysa sunucu çökmesi sonrasında yeni adın veya içeriğin kaybolma ihtimali kullanılan dosya sistemi ve NFS sunucusunun davranışına bağlıdır.
Manifest değiştikten sonra veri dosyaları silinirse yayın bozulur. Bu nedenle yeni sürümdeki bütün dosyalar manifestten önce hazırlanmalı, eski sürüm ise manifest değişiminden hemen sonra kaldırılmamalıdır.
Aynı hedefe birden fazla yazıcı yayın yapıyorsa atomik "rename" veri yarışını ortadan kaldırmaz. Son taşıma yapan yazıcı hedef adı kazanır:
writer A → version 104 writer B → version 105 writer A rename writer B rename
Sonuç "105" olabilir. Zamanlama ters dönerse eski "104" sürümü yeni sürümün üzerine yazılabilir. Atomiklik yalnızca her ad değişiminin bölünmez olduğunu garanti eder. Sürüm sırasını garanti etmez.
Birden fazla yazıcı varsa yayın şu yöntemlerden biriyle tekilleştirilmelidir:
- Tek aktif üretici
- Dış kilit veya lease
- Artan sürüm ve compare-and-set benzeri manifest güncellemesi
- Her üreticinin ayrı sürüm dizinine yazması ve merkezi yayın kararının tek noktadan verilmesi
NFS dosya kilitleri kullanılabilir, fakat kilit kaybı, istemci çökmesi ve ağ bölünmesi senaryoları ayrıca ele alınmalıdır. Yalnızca hedef dosyayı atomik taşımak çoklu yazıcı koordinasyonu sağlamaz.
Uygulanabilir yayın sözleşmesi
NFS üzerinde günlük JSON veya istatistik dosyası yayımlayan tek yazıcılı bir servis için sade sözleşme şu şekilde kurulabilir:
- Geçici dosyayı hedefle aynı dizinde oluştur
- İçeriği tam olarak yaz
- Yazma ve kapatma hatalarını kontrol et
- Gereken kalıcılık düzeyi için dosyayı senkronize et
- ATOMIC_MOVE ile hedef ada taşı
- Atomik taşıma desteklenmiyorsa yayını başarısız say
- Okuyucuların dosyayı her sürümde yeniden açmasını sağla
- Çoklu dosya yayınında sürümlü manifest kullan
- Eski sürümleri gecikmeli temizle
- Geçici dosyaları yeniden başlangıçta uzlaştır
Dosyanın varlığı işlenmiş bölüm göstergesi olarak kullanılacaksa hedef ad yalnızca başarılı commit sonrasında oluşmalıdır. ".tmp" dosyasının bulunması tamamlanmış çıktı sayılmamalıdır.
Okuyucu tarafında parse hatası görülürse bunu otomatik olarak yazarın kısmi dosya yayımladığı şeklinde yorumlamak da doğru değildir. Eski istemci cache'i, yanlış sürüm manifesti, hatalı karakter kodlaması veya bozuk kaynak veri aynı belirtiyi üretebilir. Yayın sürümü, dosya boyutu ve içerik özeti kayıt altına alınırsa hata kaynağı daha kolay ayrılır:
version size sha256 publishedAt schemaVersion
Hash değeri güvenlik doğrulaması için zorunlu olmayabilir. Yine de aktarım veya depolama bozulmasını tespit eden sabit bir bütünlük ölçüsü sağlar.
NFS üzerinde atomik dosya yayını tek bir "Files.move" çağrısından ibaret değildir. "rename" okuyucuya yarım dosya göstermeme problemini çözer. "fsync" ve NFS commit semantiği çökme sonrası kalıcılıkla ilgilenir. Cache politikası ise başka istemcilerin yeni sürümü ne zaman göreceğini belirler.
Bu üç garanti ayrı ayrı tanımlandığında dosya sistemi güvenilir bir yayın arayüzüne dönüşür. Tanımlanmadığında aynı dosya bir istemci için tamamlanmış, başka bir istemci için görünmez, sunucu çökmesi sonrasında ise kayıp olabilir.