Yazan Mert Dönmezler10 dk okuma

Veri Sözleşmeleri ve İdempotanlık: Veri Hatları İçin İki Güvenilirlik İlkesi

Önlenebilir veri hattı olaylarının çoğu iki eksikten çıkar: üretici sınırında zorunlu kılınan bir sözleşme ve tüketici tarafında idempotan bir yazma. İkisinin pratikte nasıl kurulduğu.

  • data-engineering
  • reliability
  • data-contracts
  • research
  • idempotency
  • pipelines
  • schema-evolution

Veri hatlarında yaşanan olayların arkasında genellikle egzotik dağıtık sistem arızaları yoktur. Çoğu, iki ilkenin eksik olmasından çıkar. Bu yazıda savunduğumuz şu: üretici sınırında zorunlu kılınan bir sözleşme ile tüketici sınırında idempotan bir yazma, önlenebilir veri olaylarının büyük kısmını birlikte açıklar. Schema-on-read doğrulama maliyetini yalnızca sonraya bırakır. Tam-bir-kez (exactly-once) teslim de pratikte ancak idempotan işlemlerin üzerine kurulan etkin-bir-kez (effectively-once) semantiği olarak elde edilebilir. Sonraki bölümlerde bir sözleşmeyi çalıştırılabilir bir yapı olarak yazıyor, uyumluluk yönünü bilerek seçiyor ve belirli bir yazma yolu için idempotanlık anahtarını belirliyoruz. Geriye dönük yüklemeyi (backfill) de tasarım aşamasında planlıyoruz, böylece bir gün acil duruma dönüşmüyor.

Üretici–tüketici bağlaşım sorunu

Yazılı olsun ya da olmasın, her veri hattının bir sözleşmesi vardır. Bir servis bir olay ya da tablo yayımladığında ve bir iş bunu okuduğunda, okuyan taraf ister istemez bazı varsayımları koda gömer: bir alanın var olduğu, zaman damgasının UTC olduğu, tutarın para biriminin alt biriminde tutulduğu, bir satırın sipariş satırını değil siparişi temsil ettiği. Bu varsayımlar neredeyse hiçbir yerde yazılmaz. Tüketicinin kodu yazıldığı gün bakılan bir veri örneğinden çıkarılır.

Hyrum Wright'a atfedilen Hyrum Yasası da bu mekanizmayı anlatır: yeterince tüketici olduğunda, bir sistemin gözlemlenebilir her davranışına, ne vaat edilmiş olursa olsun, birileri bağımlı hale gelir. Veri platformlarında gözlemlenebilir yüzey alışılmadık derecede geniştir. Kolon sırası, metin dolgusu, bir dosyanın tesadüfi sıralaması, boş olabilen bir kolonun bugüne kadar hiç boş gelmemiş olması, bunların hepsi dışarıdan görülebilir. Birileri er geç bunlara dayanır ve bunlar fiilen arayüzün parçası olur.

Sağlamlık ilkesi, yani "gönderirken tutucu, kabul ederken hoşgörülü ol" (Postel, 1981), taşıma katmanında internet protokollerinin işine yaradı, çünkü orada tek alternatif birlikte çalışamamaktı. Bir veri platformunun içinde ise ilkenin hoşgörülü yarısı doğrudan zarar verir. Hatalı biçimli bir kaydı sessizce kabul eden tüketici, gürültülü, yerel ve teşhisi kolay bir üretici hatasını alır ve onu haftalar sonra bir panoda ortaya çıkan, sessiz ve dağınık bir veri kalitesi sorununa çevirir. İlişkisel modelin (Codd, 1970) kalıcı katkılarından biri de şudur: yapı bir kez tanımlanır ve onu okuyan programlardan bağımsız olarak zorunlu kılınır. Her okuyucunun yapıyı yeniden çıkarması gerekmez.

Bu yüzden doğrulamanın yeri sınırdır, yani üreticiye bakan taraf. Gerekçe ekonomiktir. Sınırda yakalanan bir ihlalin tek bir sahibi, tek bir etki alanı ve makul bir çözümü vardır. Aynı ihlal aşağı akışa geçtiğinde n ayrı sahibi olur ve hiçbirinin elinde makul bir çözüm kalmaz.

Schema-on-read maliyeti neden ortadan kaldırmaz, erteler

Schema-on-read'in lehindeki argüman boş değildir: veri alımı ucuzlar, üreticilerin önü açılır ve yorumlama bir kullanım senaryosu çıkana kadar bekletilir. Argümanın atladığı şey, yorumlamanın bir kez ertelenmediğidir. Her okuyucu için, tekrar tekrar ve süresiz olarak ertelenir. Ham bir akışın zorunlu bir şeması yoksa, k tüketicinin her biri bir alanın anlamını kendi başına çözmek, kendi tür dönüşümünü yazmak ve alan eksik geldiğinde ne yapacağına kendisi karar vermek zorundadır. Toplam doğrulama işi tüketici sayısı k ile birlikte büyür ve k ayrı uygulama zamanla birbirinden ayrışır. Aynı akıştan, boş değerleri farklı işleyerek "aktif mekân" sayısını hesaplayan iki pano görünür bir hata üretmez. Ortaya iki farklı sayı çıkar, bunun için bir toplantı yapılır ve platforma duyulan güven azalır. Bu kayıp, tasarımın kaçınmak istediği veri alımı sürtünmesinden çok daha pahalıdır. Schema-on-read gerçek keşif çalışmalarında ve ileride nasıl kullanılacağı bilinmeyen verinin arşivlenmesinde savunulabilir, ama birden fazla üretim tüketicisi olan bir akış için doğru seçim değildir.

Çalıştırılabilir yapı olarak sözleşmeler

Bir wiki sayfasında duran sözleşme dokümantasyon olarak kalır. Çalıştırılabilir bir yapı olması için makinenin okuyabileceği biçimde yazılması, sürüm kontrolünde tutulması ve kodu yayına taşıyan hattın içinde zorunlu kılınması gerekir. Bu, Fowler'ın uzun süredir savunduğu sürekli tümleştirme disiplininin veriye uygulanmış halidir. Böyle bir yapıya üç tür yükümlülük girer.

Yapısal yükümlülükler

Alan adları, fiziksel türler, boş değer alabilirlik, kardinalite, birincil ve yabancı anahtarlar, izin verilen aralıklar ve sabit değer kümeleri. Denetlemesi en ucuz ve test üretmesi en kolay yükümlülükler bunlardır. Bir şema kaydına karşı statik olarak, canlı veri örneklerine karşı dinamik olarak doğrulanabilirler.

Anlamsal yükümlülükler

Yapısal denetimlerin ifade edemediği yükümlülükler: sayısal bir alanın birimi, bir zaman damgasının saat dilimi, bir satırın granülerliği, "bilinmiyor" anlamındaki boş değerle "uygulanamaz" anlamındaki boş değer arasındaki fark ve monoton bir tanımlayıcının monotonluğunun garanti mi yoksa sadece alışkanlık mı olduğu. Anlamsal kayma en yıkıcı arıza sınıfıdır, çünkü her tür denetiminden geçer.

Operasyonel yükümlülükler

Adı belli bir sahip ekip, bir tazelik hedefi, belgelenmiş bir kullanımdan kaldırma süresi ve bozucu bir değişikliğin nasıl önerileceğini, duyurulacağını ve devreye alınacağını anlatan bir değişiklik protokolü. Sahibi olmayan bir sözleşmeyi uygulatacak kimse olmaz.

Kendi işlerimizde bilinçli olarak tasarlanmış bir sözleşme yüzeyinin en net örneği, geliştirme aşamasındaki mekân işletim sistemi BrewX. Veri modeli 46 tablodan oluşan bir şema ve istemciler bu tablolara hiç doğrudan erişmiyor. Tüm yazmalar ve okumaların büyük kısmı 80'in üzerinde uzak yordam üzerinden geçiyor. Sözleşmeyi yordam imzaları oluşturuyor. Bu dolaylılığın gerçek bir emek maliyeti var: her yeni yetenek için bir sorgu yetmiyor, bir yordam yazmak gerekiyor. Karşılığında iki şey kazanıyoruz. İç şemadaki değişiklikler kendiliğinden istemcinin göreceği bozucu değişikliklere dönüşmüyor. Doğrulama, yetkilendirme ve idempotanlık mantığı da her çağıranda yeniden yazılmıyor, zorunlu kılınabildiği tek bir yerde duruyor.

Şema evrimi ve uyumluluk yönleri

Sözleşmeler er geç değişir. Sorulması gereken, hangi uyumluluk yönünün garanti edildiğidir. Bu seçim devreye alma sırasını belirler ve yanlış yapıldığında veri hatası gibi görünen kesintilere yol açar.

YönGarantiTipik güvenli değişikliklerÖnce devreye alınan
Geriye dönükYeni okuyucular eski veriyi okurİsteğe bağlı alan ekleme, tür genişletme, sabit değer eklemeTüketici
İleriye dönükEski okuyucular yeni veriyi okurİsteğe bağlı alan ekleme, isteğe bağlı alan kaldırmaÜretici
TamHer iki yön geçerliYalnızca isteğe bağlı alan ekleme veya kaldırmaHer ikisi de
YokGaranti yokYeniden adlandırma, tür değiştirme, anlam değiştirmeEşgüdümlü göç

Buradan iki kural çıkar. Varsayılan değeri olmayan zorunlu bir alan eklemek, ekleme gibi görünse de bozucu bir değişikliktir, çünkü mevcut yazıcıların hiçbiri bu değeri göndermez. Bir alanın adını ve türünü koruyup anlamını değiştirmek ise en kötü seçenektir. Hiçbir otomatik uyumluluk denetimi bunu görmez. Ancak eski anlamı hatırlayan biri fark edebilir ve o zamana kadar genellikle hatırlayan kalmamıştır. Anlamın değişmesi gerekiyorsa yeni bir alan tanımlayın ve eskisini ilan edilmiş bir takvimle kullanımdan kaldırın. İki alanı bir çeyrek boyunca birlikte taşımak, geçmiş verinin sessizce yeniden yorumlanmasından neredeyse her zaman daha ucuzdur.

Teslim semantiği, tam olarak ifade edilmiş

Genelde üç garantiden söz edilir. En-fazla-bir-kez bir mesajı sıfır ya da bir kez teslim eder, arıza olursa veri kaybolur. En-az-bir-kez ise mesajı bir ya da daha fazla kez teslim eder ve arızada veri çoğalır. Tam-bir-kez her mesajı tam olarak bir kez teslim eder.

Birbirinden bağımsız arızalanan süreçler arasında, güvenilmez bir ağ üzerinden tam-bir-kez teslim genel durumda mümkün değildir. Nedeni basit. Gönderici mesajı iletip onay alamadığında, isteğin mi yoksa onayın mı kaybolduğunu ayırt edemez. Yeniden denemek de denememek de güvenli değildir. Olayları hangi sırayla dizerseniz dizin bu belirsizlik kalkmaz, çünkü başvurulabilecek küresel bir eşzamanlılık kavramı yoktur (Lamport, 1978). Aynı gerilim, Brewer'ın (2000) ağ bölünmesi altında tarif ettiği tutarlılık–erişilebilirlik ödünleşiminde de görülür. Yavaş bir düğümü ölü bir düğümden ayırt edemeyen sistem, neyi feda edeceğini seçmek zorundadır.

Elde edilebilen ve Kleppmann'ın (2017) bu garantinin gerçekçi ifadesi saydığı şey etkin-bir-kez semantiğidir. En-az-bir-kez teslim idempotan bir uygulama adımıyla birleştirilir, böylece tekrarlanan teslimler gözlemlenebilir durumu tek bir teslimle aynı bırakır. Biçimsel olarak uygulama fonksiyonu, her s durumu ve m mesajı için f(f(s, m), m) = f(s, m) koşulunu sağlamalıdır. Bu ayrım önemlidir, çünkü mühendislik yükünü başka bir yere taşır. Taşıma katmanının mesajı birden fazla kez getirmesine göz yumabilirsiniz, ama yazma tarafının her tekrarı doğru işlemesi gerekir. Bunun işlemsel altyapısı (atomik onay, kalıcı günlükler, kurtarma semantiği) eskidir ve iyi belgelenmiştir (Gray ve Reuter, 1993). Modern veri hatlarında değişen, yazma tarafının artık çoğu zaman çok ifadeli işlem desteği olmayan bir sistem olmasıdır. Bu da idempotanlığı veritabanından alıp uygulamanın içine taşır.

İdempotanlık anahtarları, doğal anahtarlar ve birleştirme semantiği

İdempotan bir yazma için iş biriminin kararlı bir kimliği olmalıdır. Tercih sırasıyla üç seçenek var.

Varsa doğal anahtar en iyisidir. Bu, varlığı tek başına belirleyen iş özniteliklerinin birleşimidir, örneğin kaynak sistem, sipariş numarası ve satır numarası. Teslimden bağımsız olarak doğrudan veriden türediği için yeniden oynatmalarda da aynı kalır.

İkinci sırada üreticinin atadığı idempotanlık anahtarı gelir. Bu, bir kez üretilen ve her yeniden denemede aynen taşınan bir değerdir:

anahtar = hash(kaynak_sistem, dogal_anahtar, mantiksal_surum)

Burada kritik olan, anahtarın ilk denemeden önce hesaplanması ve her yeniden denemede aynısının kullanılmasıdır. Her denemede yeniden üretilmemelidir. Yeniden deneme döngüsünün içinde üretilen bir anahtar, aynı iş için yeni bir kimlik yaratır ve yazma idempotan olmaz.

Teslim tarafının atadığı bir tanımlayıcı (ofset, mesaj kimliği, dosya adı) son çaredir. Aynı teslimin yeniden denemelerini tekilleştirir ama aynı iş olayı başka bir yoldan yeniden gelirse onu tekilleştiremez. Geriye dönük yükleme de bunu yapar.

Anahtar belli olduğunda yazma, o anahtar üzerinde açık bir çatışma politikasıyla yapılan bir birleştirmeye dönüşür. Son-yazan-kazanır ancak "son" kavramı yükün içinde taşınan mantıksal bir sürümle tanımlanıyorsa kabul edilebilir. Varış ya da alım zamanıyla tanımlanmamalıdır, çünkü varış sırası nedensel olarak ilişkili olaylar üzerinde tam bir sıralama vermez. Güncellemeler kısmi olduğunda birleştirme, hangi kolonların üzerine yazabileceğini belirtmelidir. Aksi halde aynı satırın farklı alanlarını yazan iki üretici birbirinin işini sessizce siler.

ERP'den sevkiyata uzanan iş akışımız küçük ama iyi bir örnek. Sipariş verisini bir ERP'den çekiyor, kargo etiketlerini biçimlendiriyor ve sevkiyat sistemine gönderiyor. Günde 800'den fazla kez çalışıyor. Bu sıklıkta geçici arıza olağan bir durumdur ve operasyon açısından önemli olan tek soru, bir yeniden denemenin ne yaptığıdır. İş birimi her çalıştırmaya göre değil sipariş kimliğine göre anahtarlandığı için yeniden denenen bir adım ikinci bir sevkiyat açmaz, aynı etiketi tekrar üretir.

Birinci sınıf gereksinim olarak geriye dönük yükleme ve yeniden oynatma

Geriye dönük yükleme bir veri hattı tasarımının gerçek sınavıdır, çünkü bütün varsayımları aynı anda zorlar: sözleşmenin geçmiş veriyle uyumunu, standart dışı bir yoldan yeniden teslimde idempotanlığı ve geç gelen veri kurallarının doğruluğunu. Geriye dönük yükleme yapılmamış bir hattın bu varsayımları da hiç sınanmamıştır.

Yeniden oynatmaya göre tasarlamak somut gereksinimler getirir. Dönüşümler duvar saatine bakmamalı, yalnızca tanımlı girdilerinin ve parametrelerinin fonksiyonu olmalıdır. O zaman geçmiş bir pencereyi yeniden işlediğinizde o günkü cevabı alırsınız. Yazmalar yukarıda anlatıldığı gibi anahtarlanmalıdır ki canlı trafikle çakışan bir yeniden oynatma kayıtları çoğaltmasın, aynı sonuca varsın. Bölümleme alım zamanına göre değil olay zamanına göre yapılırsa, sınırlı bir yeniden oynatma da sınırlı sayıda bölüme dokunur. Hat ayrıca üretime yüklemeden önce karşılaştırma için bir gölge hedefe yükleyebilmelidir. Pratik bir ölçüt: akış canlıyken son otuz günü yeniden işlerseniz ne olacağını ekipten biri duraksamadan söyleyemiyorsa tasarım bitmemiştir.

Gözlemlenebilirlik: tazelik, hacim, dağılım, köken

Sözleşmeler bir kusur sınıfını baştan önler. Geri kalanı izlemeyle yakalanır ve bunun büyük kısmını dört sinyal ailesi kapsar. Tazelik, yani en yeni kaydın hedefe göre ne kadar eski olduğu, en yaygın ve en sessiz arıza olan durmaları yakalar. Hacim kısmi ve tekrarlanan yüklemeleri gösterir. Dağılım (boş değer oranı, kardinalite, kategori dağılımı, sayısal yüzdelikler) yapısal denetimlerden geçen anlamsal kaymayı yakalar. Köken ise her olayda eninde sonunda sorulan soruya cevap verir: aşağı akışta hangi varlıklar etkilendi?

Eşikleri hesapla belirleyin. Girdileri tamamen varsayıma dayanan bir örnek hesap yapalım. Bir tablo saatte yaklaşık 10.000 satır alıyor, saatlik hacim yaklaşık normal dağılıyor, değişim katsayısı %15 ve uyarı, hacim iki standart sapmadan fazla saptığında tetikleniyor olsun. Bu varsayımlarla simetrik iki-sigma kuralı, sırf gürültü yüzünden aralıkların yaklaşık %5'inde tetiklenir. Saatlik denetimde bu, günde kabaca bir yanlış alarm, tek bir tablo için ayda 30'a yakın demektir. Böyle on tablo, ekibin uyarıları görmezden gelmeye başlaması için yeterli gürültüyü üretir. Sayılar varsayımsal ama aynı hesap her tabloya uygulanabilir. Uyarı eşiği, yanlış pozitif oranı hesaplanabilen istatistiksel bir seçimdir. Eşiği hisle seçerseniz uyarılar bir süre sonra kimsenin bakmadığı bir gürültüye dönüşür.

Pratik karar kuralları

Verinin bir ekibin sorumluluğundan diğerine geçtiği her noktada bir sözleşme zorunlu olsun. Hatalı kaydı hoşgörüyle kabul etmek yerine doğrulayıp reddedin. Her arayüz için tek bir uyumluluk yönü seçip yazın ve devreye alma sırasını alışkanlığa göre değil, bu yöne göre belirleyin. Teslimin her yerde en az bir kez olacağını varsayın ve her yazmayı veriden hesaplanan bir anahtarla idempotan yapın. Son-yazan-kazanır kuralını varış zamanına göre tanımlamayın. Bir iş, geriye dönük yükleme provasından geçmeden bitmiş sayılmasın. İzlemeye tazelikten başlayın ve koyduğunuz her eşiğin yanlış pozitif oranını hesaplayın.

Sınırlamalar

Bu yazı mekanizmaya ve kendi projelerimize dayanan bir akıl yürütme, görgül bir çalışma değil. Genel olarak veri hatlarındaki olayların ne kadarının sözleşme ve idempotanlık eksikliğinden çıktığını ölçmedik. Bu iki ilkenin baskın olduğu iddiası, deneyimimize uyan ama sınanmamış bir hipotez. Sözleşme benimsemenin maliyeti hakkında da kanıt sunmuyoruz. Bu maliyet gerçektir ve ağırlıklı olarak üretici ekiplerin üzerine biner. Maliyetin faydayı ne zaman aştığını da göstermiyoruz, muhtemelen tek tüketicili ya da kısa ömürlü hatlarda aşar. BrewX ve ERP örnekleri tasarım kararlarını ve işletme hacimlerini anlatır, denetimli karşılaştırma sayılmaz. Bu sistemlerin başka bir tasarımla neye mal olacağını ya da nasıl arızalanacağını bilemeyiz. Gözlemlenebilirlik hesabı belirtilen varsayımlar altında yapılmış bir hesaptır, gözlemlenmiş bir yanlış pozitif oranı değildir. Son olarak yazı örgütsel soruya girmiyor: üreticinin tüketiciye hizmet etmek için bir nedeni yokken sözleşme sahipliğini ona nasıl kabul ettirirsiniz? Pratikte bu yaklaşımın benimsenip benimsenmeyeceğini de çoğu zaman bu soru belirler.

Kaynaklar

  • Brewer, E. (2000). Towards robust distributed systems.
  • Codd, E. F. (1970). A relational model of data for large shared data banks.
  • Fowler, M. (1999). Refactoring: improving the design of existing code.
  • Gray, J., & Reuter, A. (1993). Transaction processing: concepts and techniques.
  • Kleppmann, M. (2017). Designing data-intensive applications.
  • Lamport, L. (1978). Time, clocks, and the ordering of events in a distributed system.
  • Postel, J. (1981). Transmission Control Protocol.