iGamingSep 11, 202610 dk okuma

Maç Zirvelerinde Spor Bahis Postgres'i: Sıcak Satırlar, Sonuçlandırma Gecikmesi ve Beklemeyen Bahis Yerleştirme

Spor Bahis MühendisliğiPostgreSQLBahis Sonuçlandırmaİdempotentlik
Görsel yüklenemedi

Postgres'i büyük maçlarda yavaşlayan ve bahisleri etkinlik bittikten çok sonra sonuçlandıran bir spor bahis platformu, iki iş yükünü tek bir satır kümesinden geçiriyordur: istek başına kısa bir yazma olan bahis yerleştirme ve tek bir sonucun tetiklediği ani bir yük olan sonuçlandırma. İki yolun nasıl ayrıldığı, çekişmenin nereden geldiği ve sonuçlandırma gecikirken bakiyelerin nasıl doğru kaldığı aşağıda.

Büyük bir maç sırasında Postgres'i darboğaza dönüşen bir spor bahis platformunda genellikle tek bir belirti ve iki neden vardır. Bahis yerleştirme ve sonuçlandırma, tam da trafik zirve yaptığında aynı satırlar, kilitler ve bağlantılar için yarışır ve sonuçlandırma, sırasını bekleyebilen bir kuyruk olarak değil, o satırları tutan bir iş olarak çalışır.

Daha fazla donanım, bunun gerçekleştiği trafik seviyesini yükseltir ama nedeni ortadan kaldırmaz. Aşağıdakiler iki yolu birbirinden ayırır, çekişmenin yerini tespit eder ve sonuçlandırma gecikirken bakiyeleri doğru tutar. Aşağıda alıntılanan PostgreSQL davranışları 18 sürümünün dokümantasyonuna aittir.

Kısa yanıt yapısaldır. Yerleştirme ve sonuçlandırma transaction paylaşmayı bırakır: yerleştirme bahsi, bir bakiye rezervasyonunu ve bir outbox satırını bir idempotency anahtarı altında tek bir kısa transaction'da yazar; sonuçlandırma ise sonuç olaylarını, etkileri de anahtarlı olan küçük batch'ler hâlinde tüketir, böylece yeniden teslim edilen bir mesaj para hareket ettirmez. amBrain'in kamuya kanıtlayabildikleri casino platformu mühendisliğidir ve orada ölçüme dayalı olarak yayımladığımız bir rakam canlıda 12 operatördür. Aşağıdaki tasarım bizim bir vakamızdan değil, sorunun mekaniğinden gelir ve içindeki hiçbir sayı bize ait bir sistem üzerinde ölçülmemiştir.

Yerleştirme ve sonuçlandırma, satırları paylaşan iki iş yüküdür

Yerleştirme, başında bekleyen bir insan olan bir istektir: market durumunu oku, bakiyeyi kontrol et, tek bir bahis yaz, yanıt ver. Sonuçlandırma tek bir sonuçtan başlar ve etkilenen marketlerdeki tüm açık bahislere aynı anda yayılır. Büyük bir maç, diğer etkinlikler hâlâ açıkken biter; bu yüzden o ani yük, yeniden bahis yerleştiren hesapların bakiye satırlarına iner.

İki yol da bu satırları kendi transaction'larında yazıyorsa, yerleştirme latency'si aynı hesaptaki en uzun sonuçlandırma transaction'ının bir fonksiyonu hâline gelir. İki yolu ayırmak, kilitler ve zaman üzerine verilmiş bir dizi vaattir:

  • Sonuçlandırma bir bakiye satırını, bir yerleştirme transaction'ının tuttuğundan daha uzun süre tutmaz
  • Sonuçlandırma geride kalabilir ve backlog'u bir yığın açık transaction değil, yaşı olan bir kuyruktur
  • Para üzerindeki her etki, arkasındaki mesaj ne kadar sık teslim edilirse edilsin bir kez gerçekleşir
  • Primary'ye ihtiyaç duymayan okumalar ona dokunmaz

Bakiye satırı, kilit olarak tasarlasanız da tasarlamasanız da bir kilittir

PostgreSQL'in kilitleme bölümü, satır düzeyindeki kilitlerin okuyucuları değil, yalnızca aynı satıra yazanları ve onu kilitleyenleri bloke ettiğini ve kilit isteyen bir transaction'ın bir deadlock tespit edilmedikçe süresiz beklediğini söyler. Dolayısıyla her yerleştirmede güncellenen, hesap başına tek bir satır bir kuyruktur ve olması gereken de budur: kilit, iki yerleştirmenin aynı parayı harcamasını engeller. Önemli olan, kilidi tutan her tarafın onu ne kadar süre tuttuğudur.

Varsayılan izolasyon seviyesi olan Read Committed, rezervasyonu basit tutar. Eşzamanlı bir transaction tarafından zaten güncellenmiş bir satır bulan bir UPDATE, o transaction'ın commit ya da rollback edilmesini bekler ve commit edildiyse WHERE koşulunu güncellenmiş sürüm üzerinde yeniden değerlendirir. Tutarı yalnızca kullanılabilir bakiye onu karşıladığında düşen koşullu bir güncelleme karşılıksız harcamaya izin veremez ve SELECT FOR UPDATE gerektirmez.

  • Bakiye kilidini en son alın ve hemen ardından commit edin; kilit gerektirmeyen doğrulama önce çalışır
  • Birden fazla hesabı tutarlı tek bir sırayla kilitleyin; kilitleme bölümü deadlock'lardan kaçınmanın yolu olarak bunu gösterir
  • Market bazındaki riski her yerleştirmenin güncellediği tek bir satırda tutmayın, yoksa popüler tek bir market yerleştirmelerini tek bir kilidin arkasında sıraya sokar; sayacı sabit bir satır kümesine yayın
  • Yerleştirme yolunda lock_timeout ayarlayın; böylece süresiz bir bekleme, aynı idempotency anahtarıyla yeniden denenen, sayılmış bir hataya dönüşür
  • Bakiye sütunlarını indekslerin dışında tutun: depolama bölümü, HOT güncellemeye yalnızca hiçbir indeksli sütun değişmediğinde ve eski satırı tutan sayfada yer olduğunda izin verir; daha düşük bir fillfactor bunu daha olası kılar

Uzun transaction'lar ve autovacuum ani yükü diskte tutar

Bir marketteki tüm açık bahisleri tek bir statement'ta işaretleyen bir sonuçlandırma, bu satır kilitlerini commit'e kadar tutar ve her satırın ölü bir sürümünü geride bırakır. Vacuum bölümü, eski bir sürümün diğer transaction'lar onu hâlâ görebilecekken silinmemesi gerektiğini söyler; bu yüzden uzun bir sonuçlandırma ya da bir transaction içinde boşta bekleyen bir rapor, ani yükün tamamını diskte tutar.

Autovacuum tasarımı gereği geç gelir. PostgreSQL 18, son vacuum'dan bu yana güncellenen ya da silinen satırlar, autovacuum_vacuum_max_threshold değeri ile autovacuum_vacuum_threshold artı autovacuum_vacuum_scale_factor çarpı satır sayısı toplamından küçük olanını aştığında bir tabloyu vacuum eder. Varsayılanlar olan 100.000.000, 50 ve 0.2 ile 50 milyon satırlık bir bahis tablosu, yaklaşık on milyon güncellenmiş ya da silinmiş satırı bekler.

  • Bakiye ve açık bahis tablolarında bu eşikleri tablo bazında geçersiz kılın; vacuum bölümü buna depolama parametreleri üzerinden izin verir
  • Bir idle_in_transaction_session_timeout değeri belirleyin; dokümantasyonu, açık bir transaction'ın yakın zamanda ölmüş tuple'ların vacuum edilmesini engellediği ve tablo şişmesine (bloat) katkıda bulunabileceği konusunda uyarır
  • İndekslenmiş bir durum sütununu çevirmek yerine sonuçlandırma satırları ekleyin; çünkü indekslenmiş bir sütunu değiştiren bir güncelleme HOT olamaz
  • Geçmişi partition'ları ayırarak (detach) ya da silerek (drop) emekliye ayırın; partitioning bölümü bunu toplu bir işlemden çok daha hızlı ve toplu bir DELETE'in VACUUM ek yükünden muaf olarak tanımlar

Kuyruk tabloları bu etkiyi görmeyi kolaylaştırır. 2015'te brandur.org'da yayımlanan Postgres Job Queues & Failure By MVCC yazısında, bir iş kuyruğunun yanında boşta bırakılan tek bir transaction, bir işi kilitleme süresini 0,01 saniyenin altından o seviyenin 15 katına varan zirvelere çıkardı; çünkü ölü iş satırları henüz silinemiyordu.

Bağlantılar ve replikalar aynı zirveye aittir

Her bağlantı bir backend sürecidir ve dokümantasyona göre varsayılan olarak genellikle 100 olan max_connections'ı artırmak, paylaşılan bellek dahil ona göre boyutlandırılan kaynakları da artırır. Bunun yerine yerleştirmeye ve sonuçlandırmaya ayrı havuzlar verin; böylece bir sonuçlandırma backlog'u kendi bağlantıları için kuyruğa girer.

  • PgBouncer'ın transaction pooling modu bir sunucu bağlantısını yalnızca bir transaction süresince atar; böylece çok sayıda istemci daha az backend'i paylaşır
  • Oturum özellikleri bu modda bozulur: PgBouncer SET ve RESET'i, LISTEN'ı, WITH HOLD cursor'larını ve oturum düzeyindeki advisory lock'ları desteklenmeyenler arasında sayar
  • Protokol düzeyindeki adlandırılmış prepared statement'lar, Ekim 2023'te yayımlanan PgBouncer 1.21.0'dan beri, max_prepared_statements sıfırdan farklı olduğunda bu modda çalışır

Replikalar okumaların yükünü iki bedelle hafifletir. Streaming replication varsayılan olarak asenkrondur; bu yüzden bir commit standby'da küçük bir gecikmeyle görünür hâle gelir. Ayrıca hot standby bölümü, primary'den gelen vacuum temizliğiyle çakışan standby sorgularının yapılandırılmış bir gecikmeden sonra iptal edildiğini söyler; hot_standby_feedback ise temizliği primary'de geciktirerek bunu önler, bu da orada tablo şişmesine (bloat) yol açabilir.

Yerleştirme, ilk yeniden denemeden önce anahtarlanmış tek bir kısa transaction'dır

Yerleştirmeyi, arızasından geriye doğru tasarlayın: bir istemci zaman aşımına uğrar ve yeniden dener; yeniden deneme ikinci bir bahis oluşturmamalı, ilk sonucu almalıdır.

  • İstemci ya da isteği ilk karşılayan edge, her gönderim için bir idempotency anahtarı oluşturur ve her yeniden deneme onu değiştirmeden taşır
  • Tek bir transaction bahis kaydını, koşullu bir bakiye güncellemesi olarak rezervasyonu ve kabul edilen bahis için bir outbox satırını yazar
  • Bahis kayıtlarına yalnızca ekleme yapılır: sonuçlandırma, iptaller ve düzeltmeler bahse referans veren yeni satırlardır; asla düzenleme değil
  • Bir benzersizlik kısıtı, yeniden denemeyi bir çakışmaya çevirir: ON CONFLICT DO NOTHING içeren bir INSERT hiçbir şey eklemez, RETURNING yalnızca eklenen satırları döndürür ve yol, saklanmış sonucu geri okur
  • Stripe, API'si için aynı sözleşmeyi belgeler: bir anahtar için ilk sonuç, başarılı da olsa başarısız da olsa saklanır ve sonraki isteklere döndürülür; farklı parametrelerle yeniden kullanılan bir anahtar ise reddedilir

Depolamayı zamana göre partition'layın, işi markete göre bölün. Partitioning bölümü, partition'lanmış bir tablodaki benzersizlik kısıtının tüm partition anahtarı sütunlarını içermesini şart koşar; bu yüzden idempotency anahtarı ya partition sütununu taşır ya da kendi tablosunda durur. Aynı bölüm, sorgular birkaçı dışındaki tüm partition'ları budadığında planlayıcının birkaç bin partition'a kadar oldukça iyi başa çıktığını da söyler; marketlerin ise ucu açıktır, bu yüzden market başına partition'lar planlama süresini yerleştirme yoluna bindirir.

Outbox satırı olayı güvenilir kılar. Chris Richardson'ın tarif ettiği transactional outbox deseninde mesaj, iş varlıklarını güncelleyen transaction içinde veritabanına kaydedilir ve ayrı bir süreç onu iletir. Aynı tarif bedeli de söyler: relay bir mesajı birden fazla kez yayımlayabilir, bu yüzden consumer'lar idempotent olmalıdır.

Sonuçlandırma, gecikmesine izin verilen bir kuyruktur

Bir sonuç geldiği andan itibaren sonuçlandırma yaşı olan bir backlog'dur ve içindeki hiçbir şey, yerleştirmenin beklediği bir satırı bir batch'ten uzun süre tutmaz:

  • Sıralamayı global olarak değil, market başına sağlayın: Kafka aynı anahtara sahip olayları aynı partition'a yazar ve consumer'ların bir partition'ı yazma sırasıyla okuduğunu belgeler; böylece markete göre anahtarlanmış sonuç olayları sıralı kalır
  • Kuyruk tablosu belli sınırlar içinde işe yarar: dokümantasyon SKIP LOCKED'ı genel amaçlı iş için uygun değil, ancak kuyruk benzeri bir tablonun consumer'ları arasında kilit çekişmesini önlemek için kullanılabilir olarak tanımlar
  • Her batch sınırlı sayıda bahsi sonuçlandırır, bunların defter kayıtlarını yazar, bakiye satırlarını hesap sırasıyla günceller ve commit eder
  • İlerleme etkilerle birlikte commit edilir; böylece batch ortasında ölen bir worker, son commit edilen batch'inden devam eder
  • Düzeltilmiş bir sonuç yeni bir olaydır: ters kayıtlar, ardından yeni sonuçlandırma kayıtları; eski kayıtlar asla düzenlenmez

Sonuçlandırmanın gecikmesine izin vardır. İki kez gerçekleşmesine izin yoktur. Yerleştirmeye ikisine de izin yoktur ve ikisinin bir transaction'ı paylaşamamasının nedeni budur.

Bakiyeler iki sayıya ve bir kez işlenen kayıtlara ihtiyaç duyar

Tek bir bakiye sütunu, kabul edilmiş ama henüz sonuçlandırılmamış bir bahsi tarif edemez. Hesap başına iki sayı tutun - kullanılabilir ve rezerve - ve parayı bunlar arasında yalnızca her biri bir anahtar taşıyan defter kayıtlarıyla taşıyın:

  • Yerleştirme, koşullu güncellemesinde tutarı kullanılabilirden rezerveye taşır
  • Sonuçlandırma rezervasyonu serbest bırakır ve nihai borç kaydını ve varsa alacak kaydını tek bir transaction'da işler; bu kayıtlar bahis, kayıt türü ve sonuçlandırma sürümüyle anahtarlanır
  • Bir iptal rezervasyonu serbest bırakır; sonuçlandırması hiç gelmeyen bir rezervasyonun ise adı konmuş bir sahibi ve bir süre sınırı vardır
  • Bakiye satırı defterin bir projeksiyonudur ve kayıtları hesap başına toplayan planlı bir mutabakat, sapmayı sessizce düzeltmek yerine bir incident olarak raporlar

Teslim tekrarlanabilir: outbox relay'i yeniden yayımlayabilir ve outbox logical decoding ile okunduğunda, dokümantasyona göre bir slot bir çökmeden sonra son değişiklikleri yeniden gönderebilir. Dolayısıyla gereken şey, bir kez gerçekleşen bir etkidir. Her defter kaydının benzersiz bir anahtarı vardır, bakiye güncellemesi insert ile birlikte commit edilir ve yeniden teslim edilen bir mesaj kısıta takılır, para hareket ettirmez.

Bahis geçmişi yazma yoluna değil, bir okuma modeline aittir

Zirvedeki birçok okuma yerleştirmenin üzerinde değil, yanında durur: açık bahisler, geçmiş, her olaydan sonra yenilenen bakiye ekranları. Chris Richardson'ın CQRS tarifi, bu tür sorguları verinin sahibi olan servisin olaylarına abone olarak güncel tutulan bir görünüm veritabanından sunar ve bedel olarak replikasyon gecikmesini ve nihai tutarlı görünümleri sayar. Yerleştirmenin outbox'ı bu olayları zaten yayımlıyor.

  • Yerleştirme yanıtı kabul edilen bahsi döndürür; böylece istemci onu, geride kalabilecek bir görünümden geri okumadan gösterir
  • En güncel duruma ihtiyaç duyan ekranlar açıkça primary'den okur ve bu liste kısa kalır
  • remote_apply olarak ayarlanmış synchronous_commit, her commit'in senkron standby'lar onu replay edene kadar beklemesini sağlar: replikada read-your-writes, bedeli yerleştirme latency'siyle ödenir

Maç henüz sürerken ne ölçülmeli

Ölçümleri zirve sırasında, yerleştirme latency'siyle aynı zaman ekseninde alın:

  • Kilit beklemeleri: Lock bekleme olayı türü için pg_stat_activity'yi örnekleyin ve bloklayanları pg_blocking_pids ile bulun; dokümantasyon bunun sık çağrılırsa performansı etkileyebileceği konusunda uyarır
  • log_lock_waits varsayılan olarak kapalıdır ve yalnızca varsayılanı bir saniye olan deadlock_timeout'tan uzun beklemeleri raporlar; bu yüzden log daha kısa beklemelerin hiçbirini göstermez
  • En eski transaction - pg_stat_activity'deki xact_start'tan - ve idle in transaction durumundaki her oturum
  • Sıcak tablolarda temizlik: n_dead_tup, last_autovacuum ve n_tup_upd'ye karşı n_tup_hot_upd
  • Havuz baskısı: SHOW POOLS çıktısındaki cl_waiting ve maxwait; PgBouncer yükselen bir maxwait'i yetişemeyen bir havuz olarak yorumlar
  • Sonuçlandırma backlog'u bir yaş olarak; çünkü bir sayım, büyük bir kuyruğu takılmış bir kuyruktan ayırt edemez
  • Standby başına replay_lag ve logical replication slot'ları için wal_status ile safe_wal_size

Birlikte okunduklarında arızanın yerini gösterirler. Kilit beklemeleri sabitken büyüyen bir havuz kuyruğu bağlantıları işaret eder; sonuçlandırma batch'leriyle birlikte yükselen kilit beklemeleri paylaşılan satırları işaret eder; ölü satırlar tırmanırken ikisinin de kıpırdamaması en eski transaction'ı işaret eder.

Bu işi gerçekten hangi mühendislik firmalarının yaptığını ayırt etmek

Sorunun ikinci yarısının - bu konuda hangi şirketlerin uzmanlaştığı - bir satıcı listesine ihtiyaç duymayan bir testi var. Bu yolları daha önce ayırmış bir firma, ilk görüşmede şunları yapar:

  • Şemayı istemeden önce gerçek bir zirveden yerleştirme latency'sini ve sonuçlandırma backlog'unu tek bir zaman ekseninde ister
  • Latency'nin sahibi olmasını beklediği mekanizmayı ve bu beklentiyi yanlışlayacak ölçümü adlandırır
  • Parayı bir test paketi gibi ele alır: mükerrer teslim, batch ortasında öldürülen bir worker, düzeltilmiş bir sonuç
  • Yollardan birini tek başına değil, yerleştirme trafiği sürerken bir sonucun yarattığı ani yükü yük testinden geçirir
  • Çıkış ölçütlerini önceden söyler: ani yük sırasında bir yerleştirme yüzdeliği ve sonrasında kabul edilebilir bir backlog yaşı
  • Final gecesi sonuçlandırma worker'ları, replikasyon slot'ları ve bağlantı havuzları için birini nöbete koyabilir

Bunlardan herhangi birinde genel kalan bir yanıt, işin teşhis olmadan başlayacağı anlamına gelir.

Yani ilk karar daha büyük bir veritabanı değildir. Karar, yerleştirmenin yavaşladığı gece latency'nin sahibinin hangi mekanizma olduğu ve yerleştirme ile sonuçlandırmanın yol üzerinde herhangi bir yerde hâlâ bir transaction paylaşıp paylaşmadığıdır.

amBrain'in kamuya kanıtlayabildikleri: amBrain; trading platformları, matching engine'ler, real-time bidding sistemleri ve casino platformu mühendisliği alanlarında uzmanlaşmış bir yazılım geliştirme şirketidir. amBrain 2019'dan beri yazılım geliştiriyor. iGaming'de ölçüme dayalı olarak yayımladığımız bir rakam: canlıda 12 operatör. Üç formatta çalışıyoruz: tam teslim, özel ekip ya da sizin ekibinize yerleşen mühendisler.

Masada buna benzer bir tasarım mı var?

Mevcut mimarinizi ve sizi endişelendiren hata senaryosunu getirin; yarım saatte birlikte üzerinden geçelim.

İlgili Yazılar

Görsel yüklenemedi
iGaming
Feb 28, 20266 dk okuma

iGaming Platformlarını Ölçeklemek: 10M Eşzamanlı Kullanıcıyı Yönetmekten Dersler

Yazıyı oku
Görsel yüklenemedi
iGaming
Feb 7, 20265 dk okuma

Sorumlu Oyun Özellikleri Geliştirmek: Teknik Bir İnceleme

Yazıyı oku
Görsel yüklenemedi
iGaming
Jan 15, 20267 dk okuma

Canlı Bahis Mimarisi: Oran Güncellemelerini 50ms'nin Altında İşlemek

Yazıyı oku