Sorunun Tarihi
İşlemci dış kutusu kalıbı, dağıtık sistem mimarisinde var olan "çift yazma" sorununa kritik bir çözüm olarak ortaya çıktı. Bir hizmet bir veritabanını güncellerken aynı anda bir arabirime mesaj yayınlamak bu iki işlemi atomik hale getirmek, 2PC gibi maliyetli dağıtılmış işlemlere bağımlıdır ki modern mikro hizmetler ölçeklenebilirlik ve erişilebilirlik kısıtlamaları nedeniyle bunlardan kaçınmaktadır. Kalıp, olayları iş verileri güncellemeleri ile aynı yerel veritabanı işlemi içinde bir dış kutu tablosuna yazar, ardından bunları mesaj otobüsüne yayınlamak için ayrı bir iletişim sürecine güvener.
Problem
Temel doğrulama zorluğu, temel altyapı arızaları sırasında (örneğin, PostgreSQL arızaları veya Kafka aracısının yeniden dengelemesi gibi) tam bir kere (veya en az bir kere ile garanti altına alınmış idempotentlik) anlamını sağlamaktır. Sıkı otomatik testler olmadan, yarış koşulları olayların birden fazla kez yayınlanmasına veya tamamen kaybolmasına neden olabilir, bu da veri tutarsızlığına ve mali farklılıklara yol açar. Ayrıca, aşağı akış tüketicilerinin yinelemeli mesajları doğru bir şekilde ele almasını doğrulamak, manuel testlerle sürekli yeniden üretilemeyen karmaşık ağ bölünmeleri ve çökme kurtarma senaryolarını simüle etmeyi gerektirir.
Çözüm
Bir TestContainers tabanlı çerçeve uygulayın ve birincil-replika PostgreSQL kümesi, bir Kafka aracı ve test edilen uygulama hizmetini düzenleyin. Kritik anlarda veritabanı ve iletişim hizmeti arasında kesin ağ bölünmeleri eklemek için Toxiproxy'yi entegre edin. Doğrulama paketi, olayların dış kutu tablosuna benzersiz idempotentlik anahtarları ile yazıldığını, iletişim sürecinin (ister anket yapma ister Debezium CDC tabanlı) bu olayları anahtarları bozulmadan yayınladığını ve tüketicilerin bu anahtarlar temelinde yinelemeleri reddetmek için bir yineleme deposu koruduğunu doğrulamalıdır. Tüm test işçileri, çapraz test enfeksiyonunu önlemek için geçici Zookeeper toplulukları ile izole Docker ad alanlarında çalışmalıdır.
-- İdempotentlik kısıtlaması ile dış kutu tablosu şeması CREATE TABLE outbox ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), aggregate_id UUID NOT NULL, event_type VARCHAR(255) NOT NULL, payload JSONB NOT NULL, idempotency_key VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, processed BOOLEAN DEFAULT FALSE ); -- Tüketici yineleme tablosu CREATE TABLE processed_messages ( idempotency_key VARCHAR(255) PRIMARY KEY, processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
// Tüketici idempotentlik mantığı public void handleEvent(Message event) { try { deduplicationRepository.insert(event.getIdempotencyKey()); businessService.processOrder(event.getPayload()); } catch (DuplicateKeyException e) { log.info("İdempotent yineleme göz ardı edildi: {}", event.getIdempotencyKey()); } }
Problem Tanımı
E-ticaret platformumuz, bir PostgreSQL veritabanından Apache Kafka'ya sipariş olaylarını yayınlamak için dış kutu kalıbını kullandı ve envanter ile ödeme hizmetlerinin senkronize kalmasını sağladı. Kritik bir Kara Cuma etkinliği sırasında, birincil veritabanından bir okuma replikasına ani bir arıza, anket yayınlayıcı hizmetinin beklenmedik bir şekilde yeniden başlamasına neden oldu ve bu da önceden işlenmiş 15,000 "Sipariş Oluşturuldu" olayının yeniden yayınlanmasına yol açtı. Bu zincirleme, aşağı akış tüketicilerinin uygun idempotentlik kontrollerine sahip olmaması nedeniyle, müşterilerin tekrarından ve envanterin aşırı satılmasından kaynaklanan önemli mali kayıplara ve müşteri güveninin sarsılmasına neden oldu.
Çözüm A: Staging'de manuel arıza testi
Artılar: Üretim benzeri altyapıları kullanarak ek otomasyon araçlarına veya karmaşık betik yazımına ihtiyaç duymadan; deneyimli QA mühendislerinin arıza senaryolarında sistem davranışını sezgisel bir şekilde gözlemlemesine izin verir. Eksiler: Veritabanı arızaları doğası gereği öngörülemez ve test yürütmesi ile tam zamanlama yapmak zor; sürekli regresyon testi için CI/CD boru hatlarına entegre edilemez; yeniden üretilebilirlik eksikliği ve insan koordinasyon çatışmaları olmadan paralel yürütme yapılamaz.
Çözüm B: Taklit edilen depolarla birim testi
Artılar: Harici altyapı bağımlılıkları olmadan 100 ms altında son derece hızlı yürütme süreleri sağlar; testler tamamen belirleyici ve IDE ortamlarında hata ayıklaması kolaydır; gerçek dağıtımlı sistemlerde tetiklemesi zor teorik kenar durumlarını simüle etmeye olanak tanır. Eksiler: Taklitler gerçek PostgreSQL işlem yalıtım seviyelerini, Kafka tüketici grubu yeniden dengeleme davranışlarını veya TCP ağ yığını ayrıntılarını simüle edemez; gerçek JDBC sürücüleri veya çekirdek düzey uygulamalardaki yarış koşullarını tespit edemez.
Çözüm C: TestContainers ile konteynerleştirilmiş kaos mühendisliği
Artılar: Gerçek PostgreSQL akış çoğaltması ve Kafka arabirimleri kullanarak gerçekçi bir ortam oluşturur; Toxiproxy veya Pumba kullanarak kesin ağ bölünmeleri ve gecikmeler ekler; tamamen yeniden üretilebilir ve paralel yürütme desteği ile CI/CD boru hatlarına entegre edilebilir. Eksiler: Her test paketi için 5-10 dakika kadar önemli bir başlangıç kurulumu süresi gerektirir; daha yüksek hesaplama kaynakları ve bellek tahsisi talep eder; bağlantı aşınmasını ve boşta kalan konteynerleri önlemek için dikkatli bir temizleme mantığı gerektirir.
Seçilen Çözüm
Gerçek altyapı etkileşimleri yalnızca PostgreSQL'ün birincil düğümde işlemi başarıyla taahhüt etmesine rağmen, tanımanın kaybolduğu özel yarış koşulunu ortaya çıkarabileceği için Çözüm C'yi benimsedik. Özel bir JUnit 5 uzantısı geliştirdik ve kritik işlem aşamaları sırasında ağ kaosunu simüle etmek için Docker Compose ile Pumba'yı yönettik.
Sonuç
Otomatik test paketi hemen dış kutu tablomuzun idempotency_key sütununda benzersiz bir kısıtlamanın eksik olduğunu tespit etti ve bu, yayıcıların yeniden deneme sırasında yinelenen satırlarını oluşturmasına izin verdi. Kısıtlamayı ekledikten ve tüketicilerde tekrar gündeme getirme katmanını uyguladıktan sonra, test artık her CI derlemesinde çalışmakta ve 8 dakika içinde geri bildirim sağlayarak mesaj yinelemeleriyle ilgili üretim olaylarını %95 oranında azaltmaktadır. Bu, bir sonraki çeyrekte potansiyel tekrar ücretlerinden 50.000 $'lık bir kaybı önledi.
Dış kutu kalıbı, saga kalıbından temelde nasıl farklıdır ve iki aşamalı taahhüt (2PC) neden mikro hizmetler için uygun değildir?
Dış kutu kalıbı, yerel veritabanı durum değişiklikleri ile olay yayınlama arasında atomikliği sağlarken, saga kalıbı birden fazla hizmet genelinde uzun ömürlü dağıtılmış işlemleri telafi edici eylemlerle koordine eder. 2PC, kaynakları hizmet sınırları arasında kilitlemek için merkezi bir koordinatör gerektirdiğinden mikro hizmetler için uygun değildir ve bu, aşırı zaman bağımlılığı ve erişim riski yaratır; bir katılımcı hizmeti yanıt vermediğinde, koordinatör zaman aşımına kadar diğer tüm katılımcıları engeller ve bu da mikro hizmetlerin özerklik ilkesini ihlal eder.
Dış kutu iletişimi için anket eden yayıcı ile log tabanlı Değişim Veri Yakalama (CDC) gibi Debezium kullanımı arasındaki kritik ayrım nedir?
Anket eden yayıcılar, dış kutu tablosunu aralıklarla sorgular; bu, uygulaması daha basit olan ve ek altyapı gerektirmeyen bir yöntemdir, ancak 1-5 saniye gecikme getirir ve anket sıklığıyla artan bir sorgu yükü ekler. Debezium ve benzeri CDC çözümleri, WAL (Write-Ahead Log) okuyarak en az veritabanı etkisi ile gerçek zamanlı olay akışı sağlar, ancak onlar, belirli veritabanı yapılandırmaları gibi mantıksal çoğaltma yuvaları gerektiren önemli bir operasyonel karmaşıklığı arttırır ve WAL parçaları tüketimden önce kesildiğinde veri kaybı riski taşır.
"Zombi örneklerini"—ağ bölünme iyileşmesi nedeniyle geçici olarak yeniden ortaya çıkan eski uygulama örnekleri—yaklaşık dış kutu olaylarını yayınlamaktan nasıl korursunuz?
Zombi örnekler, ağ bölünmesi iyileştiğinde eski bir birincil örneğin seçilmesine izin vererek, eski örneğin eski iş listesini işlemeye devam etmesine neden olur. Bunu önlemek için, ZooKeeper veya etcd'e kaydedilen kafesleme jetonları veya dönem numaraları uygulayın; iletişim süreci, yayınlamadan önce döneminin güncel olduğunu doğrulamalıdır. alternatif olarak, yeni bir örnek başlatıldığında otomatik olarak eski üreticileri kafesleyen bir transactional.id ile Kafka'nın işlemciyle kullanın ve yalnızca geçerli aktif örneğin konuya olay yayınlamasını sağlayın.