Olay güdümlü her servis eninde sonunda şöyle bir kod yazar: siparişi kaydet, sonra OrderPlaced yayınla. Testte çalışır. Üretimde süreç iki satırın arasında çöker ve sipariş, olayı olmadan var olur. Ya da yayın başarılı olur, veritabanı işlemi geri alınır ve olay var olmayan bir siparişi duyurur.
Bu çift yazma (dual-write) sorunudur ve transactional outbox deseni bunun standart çözümüdür. Bu yazı sorunu, deseni, aktarıcıyı kurmanın iki yolunu ve desenin gerçekten işe yarayıp yaramayacağını belirleyen ayrıntıları anlatıyor: sıralama, tekrarlar ve temizlik.
Çift yazma sorunu
Bir veritabanı ile bir mesaj aracısı (broker), ortak işlemi olmayan iki sistemdir. İkisine birden yazan her kodda, yazmalardan birinin başarılı olup diğerinin olmadığı bir zaman aralığı vardır:
| İşlem sırası | Ne ters gidebilir |
|---|---|
| Önce commit, sonra yayın | Commit sonrası çökme: olay sonsuza kadar kaybolur |
| Önce yayın, sonra commit | Commit başarısız olur: tüketiciler hiç var olmamış bir sipariş için harekete geçer |
| İşlemin içinde yayın | Broker çağrısı başarılı olur, işlem yine de geri alınır |
Yeniden denemeler bu aralığı kapatmaz, yalnızca yerini değiştirir. Commit sonrası bir hook'tan yayınlamak gibi sıralama hileleri aralığı yalnızca küçültür: commit ile hook arasındaki bir çökme yine olayı kaybettirir. Veritabanı ile broker arasında dağıtık işlemler nadiren mümkündür, mümkün olduğunda da yavaştır.
Desen
Olayı iş verisiyle aynı işlemde veritabanına yazın ve daha sonra oradan yayınlayın:
- Servis, sipariş satırını ve bir outbox satırını (olay kimliği, aggregate kimliği, tür, yük) tek bir yerel işlemde ekler. İkisi birlikte commit edilir ya da hiçbiri edilmez.
- Ayrı bir aktarıcı (relay) yeni outbox satırlarını okur ve broker'a yayınlar.
- Aktarıcı, broker onay verdiğinde satırları yayınlandı olarak işaretler ya da siler.
Artık garanti basittir: sipariş varsa olayı en az bir kez yayınlanacaktır. Chris Richardson'ın desen açıklaması da aynı yapıyı kullanır.
CREATE TABLE outbox (
id uuid PRIMARY KEY,
aggregate_id bigint NOT NULL,
type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
Aktarıcıyı kurmanın iki yolu
Polling ile yayınlayıcı. Bir döngü yayınlanmamış satırları gruplar halinde seçer, yayınlar ve siler. Postgres'te SELECT ... FOR UPDATE SKIP LOCKED, birden fazla aktarıcı örneğinin birbirini engellemeden işi paylaşmasını sağlar; kilitleme ifadesi belgelerine bakın. Kurması ve anlaması kolaydır. Bedelleri polling gecikmesi ve yük ile ince bir sıralama ayrıntısıdır: düşük kimlikli satırlar, yüksek kimlikli satırlardan sonra commit edilebilir; bu yüzden kimliğe göre sıralayan bir poller henüz görünür olmayan bir satırı atlayabilir.
Log takibi (change data capture). Bir CDC aracı veritabanının işlem logunu okur ve commit edilmiş outbox eklemelerini mesajlara dönüştürür. Debezium'un outbox event router'ı outbox tabloları için tam olarak bunu yapar ve her satırı sütunlarına göre bir konuya yönlendirir. Olaylar commit sırasıyla, düşük gecikmeyle ve polling sorguları olmadan çıkar; bedeli CDC hattını çalıştırmak ve işletmektir.
| Polling | CDC | |
|---|---|---|
| Gecikme | Polling aralığı | Commit anına yakın |
| Sıralama | Dikkat ister | Commit sırası |
| Hareketli parçalar | Bir işçi | Bir connector ve log yapılandırması |
| Uygun olduğu durum | Orta hacimler, küçük ekipler | Yüksek hacim, çok sayıda outbox |
En az bir kez, tekrarlar demektir
Aktarıcı bir satırı yayınlayıp yayınlandı olarak işaretlemeden önce çökebilir. Yeniden başladığında satırı tekrar yayınlar. Bu kaçınılmazdır, bu yüzden tüketiciler idempotent olmalıdır:
- Her olay benzersiz bir kimlik taşır (outbox satırının kimliği).
- Tüketiciler işledikleri kimlikleri, tercihen işlemenin etkisiyle aynı işlemde (bir inbox tablosu) kaydeder ve gördükleri kimlikleri atlar.
- Ya da etkiyi doğal olarak idempotent yapın; örneğin sipariş kimliğine göre bir upsert.
Sıralama
Tüketiciler genellikle aggregate başına sıraya önem verir: aynı sipariş için OrderPlaced, OrderCancelled'dan önce gelmelidir. Bir siparişin tüm olayları aynı bölüme (partition) gitsin diye mesaj anahtarı olarak aggregate kimliğini kullanın ve commit sırasıyla yayınlayın (CDC bunu yapar; bir poller commit anında atanan bir sıra numarasına göre sıralamalı ya da bir aggregate'in olaylarını seri olarak işlemelidir). Tüm siparişler genelinde küresel sıralama nadiren gerekir ve sağlaması pahalıdır.
Temizlik
Hiç boşaltılmayan bir outbox tablosu sınırsız büyür ve poller'ı yavaşlatır. Satırları yayınladıktan sonra silin ya da kısa bir denetim süresi boyunca tutup eski bölümleri düşürün. CDC ile satırlar aynı işlemde, eklendikten hemen sonra silinebilir, çünkü connector eklemeyi tablodan değil logdan okur.
Hataları tek tek gözden geçirmek
Tasarımı her çökme noktasına karşı kontrol edin:
- Commit öncesi çökme. Ne sipariş ne olay vardır. İstemci bir hata alır ve yeniden deneyebilir.
- Commit sonrası, aktarıcı çalışmadan önce çökme. İki satır da vardır; aktarıcı başladığında olayı yayınlar.
- Aktarıcının yayından sonra, satırı silmeden önce çökmesi. Olay yeniden başlatmada tekrar yayınlanır ve tüketiciler tekrarı kimliğe göre ayıklar.
- Broker çöktü. Satırlar outbox'ta birikir; servis sipariş almaya devam eder ve broker geri geldiğinde aktarıcı yetişir. Yayınlanmamış en eski satırın yaşına alarm kurun.
Her durumda veritabanı doğruluğun kaynağı olarak kalır; desenin var olma sebebi de bu özelliktir.
Mimari diyagram
Diyagram içeriği
- Order service
- Postgres: one transaction
- orders
- outbox
- Outbox relay
- order-events
- Shipping service
- Email service
- Processed events
- both rows
- new rows
- publish
- OrderPlaced
- seen it?
Nasıl okunur:
- Sol: sipariş servisi tek bir Postgres işleminde orders ve outbox tablolarına iki satır yazar. Çerçeve işlem sınırını gösterir.
- Orta: outbox aktarıcısı, polling ya da CDC ile yeni satırları okur ve order-events konusuna yayınlar.
- Sağ: kargo ve e-posta servisleri olayları tüketir. E-posta servisi harekete geçmeden önce işlenmiş olay kimlikleri tablosuna bakar; tekrarlar bu şekilde emilir.
- SQL bloğu işlemi gösterir; böylece kimse burada "atomik" kelimesinin ne anlama geldiğini tahmin etmek zorunda kalmaz.
Diyagrama ne konmalı
- İşlem sınırı. İki tabloyu da onun içine çizin; desenin tamamı budur.
- Aktarıcı ve türü. Polling ya da CDC, gecikmeyi ve sıralamayı değiştirir.
- Teslimat semantiği. Aktarıcının üzerine "en az bir kez", her tüketicinin üzerine "idempotent" yazın.
- Mesaj anahtarı. Olayların aggregate kimliğiyle anahtarlandığını not edin.
Olay güdümlü mimari yazısı tüketici tarafını daha derinlemesine ele alıyor; saga deseni diyagramı ise çok servisli bir iş akışının her adımında outbox'a nerede ihtiyaç duyulduğunu gösteriyor. Veritabanı replikasyonu yazısı, CDC'nin okuduğu write-ahead log'u açıklıyor.
Archboard'da oluşturun
- Google ya da GitHub ile giriş yapın ve bir diyagram oluşturun.
- C4 Modeli paketinden sipariş servisi için bir Konteyner, outbox aktarıcısı için bir tane daha ekleyin.
- Temel paketinden veritabanı ve işlem adıyla etiketlenmiş bir Çerçeve ekleyin ve içine orders ve outbox tabloları için Varlık-İlişki paketinden iki Varlık düğümü koyun.
- Konu için Veri paketinden Kafka, tüketiciler için Konteyner düğümleri ve işlenen olay kimlikleri için bir Veritabanı ekleyin.
- İşlemin SQL'ini içeren, Temel paketinden bir Markdown bloğu ekleyin.
Örnek 10 diyagram düğümü kullanıyor ve Free planın sınırları içinde kalıyor.
SSS
Transactional outbox hangi sorunu çözer? Bir servis veritabanına yazmayı ve mesaj yayınlamayı iki ayrı adım olarak yaptığında olayların kaybolmasını ya da var olmayan olayların ortaya çıkmasını. Outbox, olayı aynı veritabanı işleminin parçası yapar.
Outbox deseni tam olarak bir kez (exactly once) teslimat sağlar mı? Hayır. En az bir kez teslimatı garanti eder. Tüketiciler tekrarları ele almalıdır; bu genellikle işlenen olay kimliklerini kaydederek yapılır.
Polling mi CDC mi: hangi aktarıcıyı kullanmalıyım? Polling daha basittir ve orta hacimler için yeterlidir. CDC daha düşük gecikme ve commit sırasıyla teslimat sağlar; yüksek hacimde ya da birçok servis outbox kullandığında buna değer.
Event sourcing kullanıyorsam yine de outbox gerekir mi? Tüketicilerin abone olduğu kaynak olay deposunun kendisiyse gerekmez. Bir servis durumu tablolarda tutuyor ve aynı zamanda olay yayınlamak zorundaysa her zaman gerekir.
Outbox tablosu ne kadar büyür? Siz ne kadar izin verirseniz. Yayınlanan satırları hemen silin ya da tabloyu zamana göre bölümleyip eski bölümleri düşürün.