Dayanıklı Veri Depolama ve Linux Dosya API'leri

Bulut sistemlerinde veri depolamanın kararlılığını araştıran ben, temel şeyleri anladığımdan emin olmak için kendimi test etmeye karar verdim. BEN NVMe teknik özelliklerini okuyarak başladı veri kalıcılığı ile ilgili hangi garantilerin (yani, bir sistem arızasından sonra verilerin kullanılabilir olacağına dair garantilerin) bize NMVe disklerini verdiğini anlamak için. Aşağıdaki ana sonuçları çıkardım: veri yazma komutunun verildiği andan ve depolama ortamına yazıldığı ana kadar zarar görmüş verileri dikkate almanız gerekir. Bununla birlikte, çoğu programda, sistem çağrıları veri yazmak için oldukça güvenli bir şekilde kullanılır.

Bu makalede, Linux dosya API'leri tarafından sağlanan kalıcılık mekanizmalarını keşfediyorum. Görünüşe göre burada her şey basit olmalı: program komutu çağırıyor write(), ve bu komutun işlemi tamamlandıktan sonra veriler güvenli bir şekilde diskte saklanacaktır. Ancak write() uygulama verilerini yalnızca RAM'de bulunan çekirdek önbelleğine kopyalar. Sistemi diske veri yazmaya zorlamak için bazı ek mekanizmaların kullanılması gerekir.

Dayanıklı Veri Depolama ve Linux Dosya API'leri

Genel olarak, bu materyal ilgimi çeken bir konuda öğrendiklerime ilişkin bir dizi nottur. En önemlisi hakkında çok kısaca konuşursak, sürdürülebilir veri depolamayı organize etmek için komutu kullanmanız gerektiği ortaya çıkıyor. fdatasync() veya bayraklı dosyaları aç O_DSYNC. Koddan diske giden yolda verilere ne olduğu hakkında daha fazla bilgi edinmek istiyorsanız, şuna bir göz atın: bu makale.

write() işlevini kullanmanın özellikleri

Sistem çağrısı write() standartta tanımlanmış IEEE POSIX bir dosya tanıtıcıya veri yazma girişimi olarak. İşin başarıyla tamamlanmasının ardından write() veri okuma işlemleri, verilere başka işlemlerden veya iş parçacıklarından erişiliyor olsa bile tam olarak önceden yazılmış baytları döndürmelidir (burada POSIX standardının ilgili bölümü). öyle, iş parçacıklarının normal dosya işlemleriyle etkileşimi bölümünde, iki iş parçacığının her biri bu işlevleri çağırırsa, her çağrının ya diğer çağrının yürütülmesinin yol açtığı belirtilen tüm sonuçları görmesi gerektiğini söyleyen bir not vardır. hiçbir sonuç görmemek. Bu, tüm dosya G/Ç işlemlerinin üzerinde çalışılan kaynak üzerinde bir kilit tutması gerektiği sonucuna götürür.

Bu, operasyon anlamına mı geliyor? write() atomik mi Teknik açıdan, evet. Veri okuma işlemleri, ile yazılanların tamamını veya hiçbirini döndürmemelidir. write(). Ama operasyon write(), standarda uygun olarak, yazması istenen her şeyi yazarak bitirmek zorunda değildir. Verilerin sadece bir kısmının yazılmasına izin verilir. Örneğin, aynı dosya tanıtıcı tarafından tanımlanan bir dosyaya her biri 1024 bayt ekleyen iki akışımız olabilir. Standart açısından, yazma işlemlerinin her biri dosyaya yalnızca bir bayt ekleyebildiğinde sonuç kabul edilebilir olacaktır. Bu işlemler atomik kalacaktır, ancak tamamlandıktan sonra dosyaya yazdıkları veriler karmakarışık olacaktır. Burada Stack Overflow'ta bu konuyla ilgili çok ilginç bir tartışma.

fsync() ve fdatasync() işlevleri

Verileri diske aktarmanın en kolay yolu, işlevi çağırmaktır. fsync (). Bu işlev, işletim sisteminden değiştirilmiş tüm blokları önbellekten diske taşımasını ister. Bu, dosyanın tüm meta verilerini içerir (erişim zamanı, dosya değiştirme zamanı vb.). Bu meta verilere nadiren ihtiyaç duyulduğuna inanıyorum, bu nedenle sizin için önemli olmadığını biliyorsanız işlevi kullanabilirsiniz. fdatasync(). Yardım üzerinde fdatasync() bu işlevin çalışması sırasında, "aşağıdaki veri okuma işlemlerinin doğru yürütülmesi için gerekli olan" bu kadar miktarda meta verinin diske kaydedildiğini söylüyor. Ve çoğu uygulamanın önemsediği şey de tam olarak budur.

Burada ortaya çıkabilecek bir sorun, bu mekanizmaların olası bir arızadan sonra dosyanın bulunabileceğini garanti etmemesidir. Özellikle yeni bir dosya oluşturulduğunda çağrılmalıdır. fsync() onu içeren dizin için. Aksi takdirde, bir çökmeden sonra bu dosyanın olmadığı ortaya çıkabilir. Bunun nedeni, UNIX altında, sabit bağlantıların kullanılması nedeniyle, bir dosyanın birden fazla dizinde bulunabilmesidir. Bu nedenle, arama yaparken fsync() bir dosyanın hangi dizin verilerinin de diske boşaltılması gerektiğini bilmesinin bir yolu yoktur (burada bununla ilgili daha fazla bilgi edinebilirsiniz). Görünüşe göre ext4 dosya sistemi şunları yapabilir: otomatik olarak uygulamak fsync() karşılık gelen dosyaları içeren dizinlere, ancak diğer dosya sistemlerinde durum böyle olmayabilir.

Bu mekanizma, farklı dosya sistemlerinde farklı şekilde uygulanabilir. kullandım blktrace ext4 ve XFS dosya sistemlerinde hangi disk işlemlerinin kullanıldığını öğrenmek için. Her ikisi de, hem dosyaların içeriği hem de dosya sistemi günlüğü için diske olağan yazma komutlarını verir, önbelleği boşaltır ve günlüğe bir FUA (Birim Erişimini Zorla, verileri doğrudan diske yazma, önbelleği atlayarak) yazarak çıkar. Muhtemelen işlemin gerçekliğini doğrulamak için tam da bunu yapıyorlar. FUA'yı desteklemeyen sürücülerde bu, iki önbellek boşaltmaya neden olur. Deneylerim bunu gösterdi fdatasync() biraz daha hızlı fsync(). Yarar blktrace belirtir fdatasync() genellikle diske daha az veri yazar (ext4'te fsync() 20 KiB yazıyor ve fdatasync() - 16 KiB). Ayrıca, XFS'nin ext4'ten biraz daha hızlı olduğunu öğrendim. Ve burada yardımla blktrace bunu öğrenebildi fdatasync() diske daha az veri boşaltır (XFS'de 4 KiB).

fsync() kullanılırken belirsiz durumlar

ile ilgili üç belirsiz durum düşünebilirim. fsync()ki pratikte karşılaştım.

Bu tür ilk olay 2008'de meydana geldi. O zamanlar, Firefox 3 arayüzü “donmuştu”, eğer çok sayıda dosya diske yazılıyordu. Sorun, arayüzün uygulanmasının, durumu hakkında bilgi depolamak için bir SQLite veritabanı kullanmasıydı. Arayüzde meydana gelen her değişiklikten sonra fonksiyon çağrıldı. fsync(), kararlı veri depolama için iyi garantiler verdi. Daha sonra kullanılan ext3 dosya sisteminde, işlev fsync() yalnızca ilgili dosyayla ilgili olanları değil, sistemdeki tüm "kirli" sayfaları diske attı. Bu, Firefox'ta bir düğmenin tıklanmasının megabaytlarca verinin manyetik bir diske yazılmasına neden olabileceği ve bunun birkaç saniye sürebileceği anlamına geliyordu. sorunun çözümü anladığım kadarıyla o malzeme, veritabanı ile çalışmayı eşzamansız arka plan görevlerine taşımaktı. Bu, Firefox'un gerçekten gerekenden daha sıkı depolama kalıcılığı gereksinimleri uyguladığı ve ext3 dosya sistemi özelliklerinin bu sorunu yalnızca şiddetlendirdiği anlamına gelir.

İkinci sorun 2009'da oldu. Ardından, bir sistem çökmesinden sonra, yeni ext4 dosya sisteminin kullanıcıları, yeni oluşturulan birçok dosyanın sıfır uzunlukta olduğunu fark ettiler, ancak bu eski ext3 dosya sisteminde olmadı. Önceki paragrafta, ext3'ün diske çok fazla veri döktüğünden ve bunun işleri çok yavaşlattığından bahsetmiştim. fsync(). Durumu iyileştirmek için ext4 yalnızca belirli bir dosyayla ilgili "kirli" sayfaları temizler. Ve diğer dosyaların verileri, ext3'ten çok daha uzun süre bellekte kalır. Bu, performansı artırmak için yapıldı (varsayılan olarak, veriler 30 saniye boyunca bu durumda kalır, bunu kullanarak yapılandırabilirsiniz. Dirty_expire_centisecs; burada bununla ilgili daha fazla bilgi bulabilirsiniz). Bu, bir çökmeden sonra büyük miktarda verinin geri alınamayacak şekilde kaybolabileceği anlamına gelir. Bu sorunun çözümü kullanmaktır fsync() İstikrarlı veri depolaması sağlaması ve bunları arızaların sonuçlarından mümkün olduğunca koruması gereken uygulamalarda. İşlev fsync() ext4 ile ext3'ten çok daha verimli çalışır. Bu yaklaşımın dezavantajı, daha önce olduğu gibi kullanımının program yüklemek gibi bazı işlemleri yavaşlatmasıdır. Bununla ilgili ayrıntıları görün burada и burada.

ilgili üçüncü sorun fsync(), 2018'de ortaya çıktı. Daha sonra, PostgreSQL projesi çerçevesinde, eğer fonksiyonun fsync() bir hatayla karşılaşırsa "kirli" sayfaları "temiz" olarak işaretler. Sonuç olarak, aşağıdaki aramalar fsync() bu tür sayfalarla hiçbir şey yapmayın. Bu nedenle değiştirilen sayfalar bellekte saklanır ve asla diske yazılmaz. Bu gerçek bir felaket çünkü uygulama bazı verilerin diske yazıldığını düşünecek ama aslında öyle olmayacak. Bu tür başarısızlıklar fsync() nadirdir, bu tür durumlarda uygulama sorunla mücadele etmek için neredeyse hiçbir şey yapamaz. Bu günlerde, bu olduğunda, PostgreSQL ve diğer uygulamalar çöküyor. öyle, "Uygulamalar fsync Hatalarından Kurtulabilir mi?" makalesinde bu sorun ayrıntılı olarak incelenmiştir. Şu anda bu soruna en iyi çözüm, bayrakla Doğrudan G/Ç kullanmaktır. O_SYNC veya bir bayrakla O_DSYNC. Bu yaklaşımla, sistem belirli veri yazma işlemlerini gerçekleştirirken oluşabilecek hataları rapor edecektir, ancak bu yaklaşım, uygulamanın arabellekleri kendisinin yönetmesini gerektirir. Bu konuda daha fazlasını okuyun burada и burada.

O_SYNC ve O_DSYNC bayraklarını kullanarak dosyaları açma

Kalıcı veri depolama sağlayan Linux mekanizmaları tartışmasına geri dönelim. Yani bayrağın kullanımından bahsediyoruz. O_SYNC veya bayrak O_DSYNC sistem çağrısını kullanarak dosyaları açarken açık(). Bu yaklaşımla, her veri yazma işlemi, her komuttan sonraymış gibi gerçekleştirilir. write() sistem sırasıyla komutlar verilir fsync() и fdatasync(). POSIX özellikleri buna "Eşzamanlı G/Ç Dosya Bütünlüğü Tamamlama" ve "Veri Bütünlük Tamamlama" adı verilir. Bu yaklaşımın ana avantajı, veri bütünlüğünü sağlamak için iki değil, yalnızca bir sistem çağrısının yürütülmesi gerekmesidir (örneğin - write() и fdatasync()). Bu yaklaşımın ana dezavantajı, ilgili dosya tanıtıcıyı kullanan tüm yazma işlemlerinin senkronize edilmesidir, bu da uygulama kodunu yapılandırma yeteneğini sınırlayabilir.

Doğrudan G/Ç'yi O_DIRECT bayrağıyla kullanma

Sistem çağrısı open() bayrağı destekler O_DIRECTişletim sistemi önbelleğini atlamak, doğrudan diskle etkileşime girerek G / Ç işlemlerini gerçekleştirmek için tasarlanmış. Bu, çoğu durumda, program tarafından verilen yazma komutlarının doğrudan diskle çalışmayı amaçlayan komutlara çevrileceği anlamına gelir. Ancak, genel olarak, bu mekanizma, işlevlerin yerine geçmez. fsync() veya fdatasync(). Gerçek şu ki, diskin kendisi gecikme veya önbellek veri yazmak için uygun komutlar. Ve daha da kötüsü, bazı özel durumlarda bayrak kullanılırken gerçekleştirilen G/Ç işlemleri O_DIRECT, yayın geleneksel ara belleğe alınmış işlemlere dönüştürür. Bu sorunu çözmenin en kolay yolu, dosyaları açmak için bayrağı kullanmaktır. O_DSYNC, bu, her yazma işleminin ardından bir çağrı geleceği anlamına gelir fdatasync().

XFS dosya sisteminin yakın zamanda bir "hızlı yol" eklediği ortaya çıktı. O_DIRECT|O_DSYNC-veri kayıtları. Bloğun üzerine yazılırsa O_DIRECT|O_DSYNC, ardından XFS, önbelleği temizlemek yerine, cihaz destekliyorsa FUA yazma komutunu yürütür. Bunu yardımcı programı kullanarak doğruladım blktrace Linux 5.4/Ubuntu 20.04 sisteminde. Bu yaklaşım daha verimli olmalıdır, çünkü diske minimum miktarda veri yazar ve iki değil tek bir işlem kullanır (yazma ve önbelleği temizleme). bir bağlantı buldum yama Bu mekanizmayı uygulayan 2018 çekirdeği. Bu optimizasyonu diğer dosya sistemlerine uygulamakla ilgili bazı tartışmalar var, ancak bildiğim kadarıyla, XFS şu ana kadar onu destekleyen tek dosya sistemi.

sync_file_range() işlevi

Linux'un bir sistem çağrısı var senkronizasyon_dosyası_aralığı(), bu da dosyanın tamamını değil, yalnızca bir kısmını diske aktarmanıza olanak tanır. Bu çağrı, zaman uyumsuz bir temizleme başlatır ve tamamlanmasını beklemez. Ama referansta sync_file_range() bu komutun "çok tehlikeli" olduğu söyleniyor. Kullanılması tavsiye edilmez. Özellikler ve tehlikeler sync_file_range() içinde çok iyi tarif Bu malzeme. Özellikle, bu çağrı, çekirdeğin "kirli" verileri diske ne zaman boşaltacağını kontrol etmek için RocksDB'yi kullanıyor gibi görünüyor. Ancak aynı zamanda orada, istikrarlı veri depolamayı sağlamak için de kullanılır. fdatasync(). kod RocksDB'nin bu konuda bazı ilginç yorumları var. Örneğin, arama gibi görünüyor sync_file_range() ZFS kullanırken verileri diske aktarmaz. Deneyimler, nadiren kullanılan kodun hatalar içerebileceğini söylüyor. Bu nedenle, kesinlikle gerekli olmadıkça bu sistem çağrısını kullanmamanızı tavsiye ederim.

Veri sürekliliğini sağlamaya yardımcı olmak için sistem çağrıları

Kalıcı G/Ç işlemlerini gerçekleştirmek için kullanılabilecek üç yaklaşım olduğu sonucuna vardım. Hepsi bir işlev çağrısı gerektirir fsync() dosyanın oluşturulduğu dizin için. Bunlar yaklaşımlardır:

  1. işlev çağrısı fdatasync() veya fsync() fonksiyondan sonra write() (kullanmak daha iyi fdatasync()).
  2. Bayrakla açılan bir dosya tanıtıcıyla çalışma O_DSYNC veya O_SYNC (daha iyi - bir bayrakla O_DSYNC).
  3. Komut kullanımı pwritev2() bayraklı RWF_DSYNC veya RWF_SYNC (tercihen bayraklı RWF_DSYNC).

Performans Notları

Araştırdığım çeşitli mekanizmaların performansını dikkatli bir şekilde ölçmedim. Çalışma hızlarında fark ettiğim farklar çok küçük. Bu, yanılıyor olabileceğim ve başka koşullarda aynı şeyin farklı sonuçlar gösterebileceği anlamına gelir. Öncelikle performansı nelerin daha çok etkilediğinden, sonra neyin performansı daha az etkilediğinden bahsedeceğim.

  1. Dosya verilerinin üzerine yazmak, bir dosyaya veri eklemekten daha hızlıdır (performans kazancı %2-100 olabilir). Bir dosyaya veri eklemek, sistem çağrısından sonra bile dosyanın meta verilerinde ek değişiklikler yapılmasını gerektirir. fallocate(), ancak bu etkinin büyüklüğü değişebilir. En iyi performans için aramanızı tavsiye ederim fallocate() gerekli alanı önceden tahsis etmek için. O zaman bu boşluk açıkça sıfırlarla doldurulmalı ve çağrılmalıdır. fsync(). Bu, dosya sistemindeki karşılık gelen blokların "ayrılmamış" yerine "ayrılmış" olarak işaretlenmesine neden olacaktır. Bu, küçük (yaklaşık %2) bir performans artışı sağlar. Ayrıca, bazı diskler diğerlerinden daha yavaş bir ilk blok erişim işlemine sahip olabilir. Bu, boşluğu sıfırlarla doldurmanın önemli bir (yaklaşık %100) performans iyileştirmesine yol açabileceği anlamına gelir. Özellikle, bu disklerde olabilir. AWS EBS'si (bu resmi olmayan veridir, teyit edemedim). Aynı şey depolama için de geçerli. GCP Kalıcı Diski (ve bu zaten testlerle onaylanan resmi bilgidir). Diğer uzmanlar da aynısını yaptı gözlemlerfarklı disklerle ilgili.
  2. Ne kadar az sistem çağrısı olursa, performans o kadar yüksek olur (kazanç yaklaşık %5 olabilir). Bir çağrı gibi görünüyor open() bayraklı O_DSYNC veya ara pwritev2() bayraklı RWF_SYNC daha hızlı arama fdatasync(). Buradaki noktanın, bu yaklaşımda, aynı görevi çözmek için daha az sistem çağrısının yapılması gerekmesinin (iki yerine bir çağrı) rol oynadığından şüpheleniyorum. Ancak performans farkı çok küçüktür, bu nedenle kolayca görmezden gelebilir ve uygulamada mantığının karmaşıklığına yol açmayacak bir şey kullanabilirsiniz.

Sürdürülebilir veri depolama konusuyla ilgileniyorsanız, işte bazı faydalı materyaller:

  • G/Ç Erişim yöntemleri — girdi / çıktı mekanizmalarının temellerine genel bir bakış.
  • Verilerin diske ulaşmasını sağlama - uygulamadan diske giderken verilere ne olduğu hakkında bir hikaye.
  • İçeren dizini ne zaman fsync yapmalısınız? - ne zaman başvurulur sorusunun cevabı fsync() dizinler için. Özetle, yeni bir dosya oluştururken bunu yapmanız gerektiği ortaya çıkıyor ve bu önerinin nedeni, Linux'ta aynı dosyaya birçok referans olabilmesidir.
  • Linux'ta SQL Server: FUA Dahilileri - Linux platformunda SQL Server'da kalıcı veri depolamanın nasıl uygulandığının açıklaması buradadır. Burada Windows ve Linux sistem çağrıları arasında bazı ilginç karşılaştırmalar var. XFS'nin FUA optimizasyonunu bu materyal sayesinde öğrendiğime neredeyse eminim.

Diskte güvenli bir şekilde saklandığını düşündüğünüz verileri hiç kaybettiniz mi?

Dayanıklı Veri Depolama ve Linux Dosya API'leri

Dayanıklı Veri Depolama ve Linux Dosya API'leri

Kaynak: habr.com