Biri bir olaya zorunlu bir customer_id alanı ekledi ve üreticiyi deploy etti. Tüketicilerin yarısı henüz bu değişikliği shiplememişti. Yirmi dakika boyunca, eski tüketicilerin bir yıldır sorunsuz işlediği her mesaj birden validation’dan kalmaya başladı — ta ki rollout tamamlanana kadar. Hiçbir kod “yanlış” değildi. Bir sözleşme, ona bağlı olanların altından çekilip değiştirildi.

Bir mesajın data şekli bir API’dir. Response body yerine queue’da JSON olması onu daha az sözleşme yapmaz — daha zor bir sözleşme yapar, çünkü iki tarafı aynı anda değiştiremezsin.

Dağıtık bir sözleşmeyi atomik değiştiremezsin

HTTP API’sinin kontrol ettiğin tek bir sunucusu vardır. Yayınlanan bir mesajın ise bağımsız deploy edilen, kendi takvimlerinde olan birçok tüketicisi vardır — ve at-least-once teslimatla, eski şekille üretilmiş yoldaki mesajlar, sen yenisini yayarken hâlâ geliyordur. “Herkesin yeni şemada olduğu” bir an yoktur. Yani tek güvenli değişiklikler, eski ve yeninin bir arada var olabildiği değişikliklerdir.

Bu tek kısıt sana tüm kural setini verir.

Ne güvenli, ne değil

Bir değişiklik, eski şema altında geçerli olan data yeni şema altında da geçerliyse backward-compatible’dır — böylece erken yükselten bir tüketici, henüz yükseltmemiş bir üreticinin yaydığı mesajları hâlâ kabul eder.

  • Opsiyonel bir alan ekle — güvenli. Eski data onsuz olur, olur.
  • Bir required kısıtını kaldır (alanı opsiyonel yap) — güvenli. Eski data daha gevşek kuralı zaten karşılar.
  • Bir enum’ı genişlet, bir minimum’u gevşet — güvenli.

Ve ısıranlar:

  • Zorunlu bir alan ekle ya da opsiyonel bir alanı zorunlu yap — kırıcı. Eski data onu içermez.
  • Bir alanı kaldır, yeniden adlandır ya da tipini değiştir — kırıcı. Yeniden adlandırma, bir kaldırma artı bir eklemedir.
  • Kuralları sıkılaştır — bir enum değerini düşür, bir minimum’u yükselt, additionalProperties’i kapat — kırıcı.

Asimetri asıl mesele: gevşetmek güvenli, sıkılaştırmak değil. Bir tüketici, beklediğinden daha hoşgörülü bir data’yı, artık talep ettiği bir şeyi eksik bir data’ya göre çok daha kolay tolere eder.

Kırıcıysa, kimliği sürümle — değiştirme

Kırıcı bir değişiklikte içgüdü “şemayı güncelleyiver” demek. Deme. Mevcut bir kimliğin arkasındaki şekli değiştirmek, yukarıdaki rollout’u kıran şeyin ta kendisi. Bunun yerine yeni bir kimlik üret — yeni bir mesaj URN’i (orders.created.v2), yeni bir topic, yeni bir olay adı — ve ikisini paralel koştur:

  1. Üreticiler v1 yaymaya devam eder; sen v2’yi yanında yayınlarsın.
  2. Tüketiciler kendi takvimlerinde v2’ye göç eder.
  3. v1’in tüketicisi kalmayınca onu emekliye ayır.

Her adımın altındaki kural: önce tüketiciler yükseltir. Hiçbir deploy edilmiş tüketicinin anlamadığı bir sürümü asla yayma. (Bu, bir mükerrer teslimat no-op olacak şekilde handler tasarlamanın aynı şekli — sırayı kontrol etmediğin için, değişikliği her sırada uygulanabilir kılarsın.)

Kuralı mekanik hâle getir

Bunların hiçbiri, cuma akşamı 5’te bir code review’da yeniden türetmek isteyeceğin bir yargı değil. Yukarıdaki uyumluluk kuralları deterministik — eski ve yeni şema verildiğinde, bir araç sana “additive, ship et” ya da “kırıcı, yeni sürüm üret” diyebilir, hiçbir görüş katmadan. O yüzden ben bunu kapıya koyuyorum: sınırda iki şemayı diff’leyip kırıcı değişiklikte build’i kıran bir kontrol — aşağı tarafa ulaşmadan önce. Orada yakalamak bir PR yorumuna mal olur; production’da yakalamak yukarıdakine benzer bir rollout penceresine.

Bu, “darboğaz aşağı kaydı” tezinin sıkıcı-altyapı ucu: şema ucuza değiştirilir ama yanlış değiştirmek pahalıdır, o yüzden bir parça araçla o ikinci maliyeti masadan kaldırırsın.

Bu ne zaman önemsizleşir?

Bir mesajın tam olarak tek bir üreticisi ve birlikte deploy olan tek bir tüketicisi varsa — tek bir servisin iç queue’su — sözleşme aslında dağıtık değildir; iki tarafı aynı anda değiştirebilirsin. O zaman bu tören fazlalıktır. Kurallar, ikinci, bağımsız deploy edilen bir tüketici var olduğu an hak ettiğini ödemeye başlar. İlk ayını aşan çoğu olay o noktaya ulaşır.


Bir şema sahip olduğun bir struct değildir; başka servislerin etrafında plan yaptığı bir sözdür. Onu, geri alamayacağın herhangi bir sözü nasıl evrimleştirirsen öyle evrimleştir — additive olarak ya da yeni bir ad altında. Asla eskisini sessizce yeniden tanımlayarak değil.


İlgili: BabelQueue schema-validation spec’i bu uyumluluk kurallarını yazıya döker; babelqueue-registry URN-başına şemaların yaşadığı yerdir — onun bqschema aracı (ve paketlenmiş Action’ı) kırıcı değişiklikte build’i kıran sınır kontrolüdür.