MySQL Connection Pooling Caching Hatası: Üretimi Bozan %0,001'lik Bug'ın İçyüzü
Özet olarak
MySQL connection pooling caching hatalarını keşfedin: %0,001'lik boş yanıt bug'ı, protokol uyumsuzlukları ve oyun backend'iniz için çözümler.
Backend'iniz, her zaman veri döndüren sorgular için boş sonuç kümeleri döndürüyor. Hata oranı %0,001 — kabaca her 100.000 istekte bir. Telemetriniz hiçbir şey göstermiyor. İzler hiçbir şey göstermiyor. Ve elinizdeki tek ipucu, önbellekleme devre dışıyken bunun asla yaşanmaması.
Cloudflare'ın Hyperdrive ekibi, connection pooler'larındaki MySQL desteğini genel kullanıma sunarken kendilerini tam olarak bu durumda buldu. Buldukları bug, iki katmanın kablo üzerinden akan baytların formatı konusunda anlaşamaması durumunda MySQL connection pooling caching'in nasıl sessizce bozulabileceğine dair bir ustalık dersi. Oyun backend'iniz için havuzlanmış, önbelleklenmiş bir veritabanı katmanı çalıştırıyorsanız — ve çalıştırmalısınız — bu hata modu sizin için de geçerli.
Bu yazı, bug'ı, onu mümkün kılan protokol ayrıntılarını, nasıl tespit edilip giderileceğini ve kendi oyun backend mimariniz için ne anlama geldiğini adım adım inceliyor.
Connection Pooling Neden Vardır?
Bir veritabanına TCP + TLS + MySQL handshake'i kurmak en az beş ağ turu (round trip) gerektirir. Oyun sunucunuz her istek için yeni bir bağlantı açıyorsa, bu maliyeti her leaderboard okumasında, her kayıtlı oyun getirişinde, her envanter sorgusunda ödüyorsunuz.
Pooling, sıcak bağlantıları açık tutup ödünç vererek bunu çözer. MySQL bu konuda Postgres'ten daha dost canlısıdır — Postgres her bağlantı için ayrı bir işletim sistemi süreci başlatırken, MySQL bağlantıları karşılaştırmalı olarak ucuzdur — ancak binlerce eşzamanlı oyuncusu olan popüler bir oyun yine de bağlantı limitlerini hızla tüketebilir.
Pooling ise kendi sorununu getirir: session state sızıntısı. Oyuncu A'nın sorgusu bir oturum değişkeni ayarlarsa ve Oyuncu B aynı bağlantıyı alırsa, bir doğruluk hatası yaşarsınız. Pooler'lar bunu checkout'lar arasında bağlantı temizliği yaparak çözer. MySQL tam olarak bunun için hafif bir komut sağlar: COM_RESET_CONNECTION.
Ve hikayedeki ilk sinsi bug da tam burada yaşıyor.
Bug #1: Bağlantı Sıfırlamalarından Sonra Kodlama Kayması
Hyperdrive ekibi başlangıç handshake'inde utf8 üzerinde anlaştı. Bu çalıştı — ta ki oyuncular sohbette ve karakter adlarında emoji kullanmaya başlayıp veriler bozuk gelene kadar.
Sorun: MySQL'deki utf8, Unicode kod noktası başına yalnızca 1-3 bayt destekleyen utf8mb3 için kullanımdan kaldırılmış bir takma addır. Tam UTF-8 (emoji, CJK uzantıları ve küresel oyuncu tabanınızın kullanacağı Latin alfabesi dışındaki çoğu betik dahil) kod noktası başına 4 bayta kadar gerektirir. Çözüm ise bunun yerine utf8mb4 üzerinde anlaşmak.
Ama işte neredeyse herkesi yakalayan tuzak:
-- Startup handshake: negotiate utf8mb4. All good.
-- Then, between checkouts, the pooler runs:
COM_RESET_CONNECTION;
-- Surprise: the server does NOT reset to the negotiated charset.
-- It resets to the SERVER DEFAULT, e.g. latin1.
COM_RESET_CONNECTION, bağlantıyı başlangıçta anlaştığınız karakter setine geri döndürmez. Sunucunun varsayılan karakter setine sıfırlar — birçok MySQL örneğinde bu hâlâ latin1'dir. İlk sıfırlamadan sonra, havuzdaki her bağlantı sessizce 1990'ların kodlamasına geriler.
Çözüm: oturum yapılandırmanızı yalnızca başlangıçta değil, her sıfırlamadan sonra yeniden uygulayın:
COM_RESET_CONNECTION;
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
SET SESSION time_zone = '+00:00';
Pratik kural: her sıfırlamadan sonra bağlantıyı tamamen yapılandırılmamış kabul edin ve önemsediğiniz her şeyi yeniden tanımlayın. Bağlantı sırasında bir kez ayarladığınız her şey eninde sonunda kaybolur.
Bug #2: %0,001'lik Boş Yanıt Bug'ı
Bulunması günler süren bug buydu. Yüksek hacimli bir müşteri, boş olmayan yanıtların garanti edildiği sorgularda küçük ama tutarlı bir boş yanıt oranı bildirdi. Hata yaklaşık %0,001 oranında gerçekleşiyordu — uçtan uca bir izi güvenilir şekilde yakalamak için çok nadir, görmezden gelmek için çok sık.
Ekip telemetriyi didik didik etti, iz oranlarını artırdı, ağ anormalliklerini aradı. Hiçbir şey yok. İşi çözen tek ipucu: bug yalnızca önbellekleme etkin olan örneklerde ortaya çıkıyordu.
Bunu Mümkün Kılan Mimari
Hyperdrive bölünmüş bir topoloji kullanır:
- Client node — Worker'ın yakınında çalışır, yerel önbellekleme yapar, sorguları iletir.
- Endpoint node — kaynak veritabanına en yakın veri merkezinde çalışır, gerçek bağlantı havuzunu tutar.
Her iki düğüm de kendi veri merkezindeki CDN önbelleğini kullanır. Aynı veri merkezinde çalıştıklarında tek bir önbelleği paylaşırlar. Bu paylaşım bir performans kazancıdır — ancak gizli bir ön koşulu da beraberinde getirir: her iki düğüm de aynı sorgu yanıtı için birebir aynı baytları önbelleğe almalıdır.
Protokol Uyumsuzluğu
MySQL'in istemci/sunucu protokolü, istemci ve sunucunun capabilities (yetenekler) üzerinde anlaştığı bir Bağlantı Aşamasına sahiptir — CLIENT_DEPRECATE_EOF gibi, sonuç kümelerinin nasıl sonlandırıldığını değiştiren isteğe bağlı özellikler:
CLIENT_DEPRECATE_EOFolmadan: sütun tanımlarından sonra EOF paketi, sonuç kümesinin sonunda başka bir EOF.- İle birlikte: metadata EOF atlanır ve son EOF bir OK paketine dönüşür.
Hyperdrive bu yeteneği destekler ve kaynak MySQL sunucusuyla anlaşır. Ancak diğer taraftaki istemci kütüphanesi — örneğin Node.js için mysql2, şu anda CLIENT_DEPRECATE_EOF desteklemiyor — desteklemeyebilir. Bu yüzden Hyperdrive çeviri yapmak zorundadır: sonuç kümelerini EOF paketleri eklemek için yeniden yazar, OK paketlerini EOF'lara geri dönüştürür, böylece baytlar Worker'ın sürücüsünün beklediğiyle eşleşir.
Sonuç: Hyperdrive'ın istemciye geri yazdığı baytlar, kaynak sunucudan aldığı baytlardan farklı bir formattadır.
Hatalı Varsayım
Postgres için Client önbellekleme uygulanırken ekip şu varsayımı yerleştirdi: istemciye geri yazdığınız baytları birebir önbelleğe alabilirsiniz. Postgres'in wire protokolü için bu doğruydu. MySQL için doğru değil.
Şimdi hatayı birleştirelim:
- Endpoint, Q sorgusunu hizmete alır, yanıt baytlarını Hyperdrive capability formatında paylaşılan önbelleğe yazar.
- Client (aynı veri merkezinde) Q için bir istek alır, önbellek kaydını bulur ve capability çevirisi yapmadan Worker'a sunar — çünkü baytlar zaten Worker için yazılmış gibi görünür.
CLIENT_DEPRECATE_EOFüzerinde hiç anlaşmamış olan Worker'ın sürücüsü, OK ile sonlandırılmış sonuç kümesini yanlış ayrıştırır — boş sonuçlar, takılmalar veya protokol desenkronizasyonu üretir.
Bug yalnızca Client ve Endpoint aynı yerde olduğunda ve biri diğerinin önbellek kaydını okuduğunda tetiklenir. Bu yüzden %0,001'di ve sıradan izlemeyle görünmezdi. Bu, bir desync bug'ının wire-protokol karşılığıdır — iki bileşenin paylaşılan durum hakkında farklı varsayımlara sahip olması. Daha önce istemci tahmini ile sunucu otoritesi arasındaki durum sapmasını hatalarından arındırdıysanız, bu hatanın şekli size tanıdık gelecektir; Unreal Engine RPC replikasyon sorununu çözerek state'lerinizi bozan hatayı düzeltme yazısında anlatılan sorunla aynı sınıftadır, yalnızca veritabanı wire katmanında.
Kavramsal olarak çözüm: kanonik formatta önbelleğe alın (kaynağın gönderdiği gibi) ve okuma sırasında isteyen istemcinin yeteneklerine çevirin. Çeviri istemci başına yapılan anlaşmaya bağlıysa, çeviri sonrası baytları asla önbelleğe almayın.
Runbook: Kendi Stack'inizde Önbellek Bozulmasını Tespit Etme
MySQL'in (veya durum bilgisi olan bir wire protokolüne sahip herhangi bir şeyin) önünde bir pooler veya önbellek katmanı çalıştırıyorsanız, bu postmortem'i şöyle uygulayabilirsiniz.
Tespit
Hataları topolojiyle ilişkilendirin. Bug yalnızca Client ve Endpoint'in bir veri merkezini ve önbelleği paylaştığı örneklerde ortaya çıktı. Her yanıtı hangi düğümün sunduğunu ve önbellekten gelip gelmediğini günlüğe kaydedin. Hata oranı önbellek isabetleriyle ilişkiliyse, yazan ve okuyan arasında format uyumsuzluğundan şüphelenin.
Ani artışları değil, nadir ama tutarlı hataları avlayın. %0,001'lik bir hata oranı asla uyarı tetiklemez. Hata takip sisteminizde, hacimden bağımsız olarak aynı imzanın tekrarlanan oluşumları için bir sorgu kurun. Düşük orandaki tutarlılık, trafikle ilişkili bir ani artıştan daha güçlü bir mantık hatası sinyalidir.
Capability asimetrisiyle bilinçli olarak test edin. Biri
CLIENT_DEPRECATE_EOFile, diğeri onsuz iki test istemcisi bağlayın, önbellekleme etkinleştirin ve aynı sorguları yoğun şekilde gönderin. Çoğu protokol çevirisi hatası bu asimetri altında dakikalar içinde ortaya çıkar.
Giderme
Önbellek kayıtlarını kanonikleştirin. Kaynağın ham yanıt formatını saklayın; okuma sırasında çevirin. Bu, istemcileri heterojen yeteneklere sahip her pooler için doğru çözümdür.
Sıfırlamalardan sonra oturum durumunu yeniden tanımlayın. Yukarıda gösterildiği gibi,
COM_RESET_CONNECTIONanlaştığınız ayarları değil, sunucu varsayılanlarını geri yükler.Önbellek anahtarlarınızı sürümleyin. Herhangi bir protokol davranışını (capabilities, kodlama, yanıt çerçeveleme) değiştirirseniz, önbellek anahtarına bir format sürümü ekleyin; böylece dağıtım sırasında eski kayıtlar yeni kod tarafından sunulamaz.
# Cache key hygiene, in pseudocode
def cache_key(query_normalized: str, schema_version: int, format_version: int) -> str:
# format_version bumps whenever wire-format handling changes,
# preventing stale-format entries from poisoning new deploys
return sha256(f"{schema_version}:{format_version}:{query_normalized}")
Önleme
Wire-protokol uygunluk testlerini iki istemci kütüphanesine karşı yazın, bire değil.
CLIENT_DEPRECATE_EOFfarkı yalnızca popüler bir Node.js sürücüsü onu desteklemediği için görünürdü.Katmanlar arasında "aynı baytlar" hakkında hiçbir varsayımda bulunmayın. Mesajları yeniden yazan herhangi bir bileşen, çıktısının girdisiyle bayt birebir aynı olduğunu varsaymamalıdır.
İlk günden itibaren önbellek etkinleştirilmiş şekilde yük testi yapın. Hyperdrive ekibi, önbelleğe özgü bug ortaya çıkmadan önce aylarca güvenilirliği doğruladı — çünkü nadir bug'lar özelliklerin kesişiminde (pooling × caching × capability çevirisi) yaşar.
Oyun Backend'lerinde Pooling ve Caching için Beş En İyi Uygulama
Her zaman
utf8mb4üzerinde anlaşın, aslautf8değil. Oyuncu adları, sohbet ve kayıt verileri 4 baytlık karakterler içerecektir. Ardından herCOM_RESET_CONNECTIONsonrasındaSET NAMES utf8mb4komutunu yeniden çalıştırın, çünkü sıfırlama sizin handshake'inizi değil, sunucu varsayılanlarını geri yükler.İstemci formatındaki verileri değil, kanonik verileri önbelleğe alın. Herhangi bir katman bir yanıtı göndermeden önce dönüştürüyorsa, dönüştürülmüş baytlar asla paylaşılan bir önbelleğe girmemelidir. Kaynak formatını saklayın; okuyucu başına çevirin.
Önbellek anahtarlarına format sürümleri ekleyin. Tek satırlık bir değişiklik, rolling deploy sırasında eski protokol kayıtlarından sizi kurtarır.
Yalnızca hata oranına değil, hata tutarlılığına da uyarı verin. Trafikle ilişkisi olmayan %0,001'lik tekrarlayan bir imza bir mantık hatasıdır. Trafikle ilişkili bir ani artış bir kapasite sorunudur. Bunları farklı şekilde yönlendirin.
Yetenekleri uyumsuz istemcilerle test edin. Pooler'ınızın doğruluğu, ekibinizin (veya SDK kullanıcılarınızın) bağlanabileceği her istemci kütüphanesini işleyebilmesine bağlıdır.
Backend Mimari Dersi
Buradaki en derin ders MySQL ile ilgili değil. Her eş ile ayrı ayrı anlaşma yapan katmanlı sistemlerle ilgili. Her iki tarafta da protokol konuşan bir ara katman — bir pooler, bir proxy, bir önbellek — eklediğiniz anda, sadık bir şekilde çeviri yapma ve yalnızca evrensel olarak geçerli formatları önbelleğe alma yükümlülüğünü üstlenirsiniz. Postgres çalıştı; MySQL bozuldu; önbellekleme mantığında yanlış olan hiçbir şey yoktu, yalnızca farklı bir protokolden miras alınan tek bir varsayım vardı.
Bir oyun stüdyosu için bu, kendi pooler-artı-önbellek katmanınızı elle yazmaya karşı güçlü bir argümandır. Mutlu yolun çalışması için gereken mühendislik çabası belki bir haftadır; nadir yolları doğru yapmak için gereken mühendislik çabası — capability anlaşması, kodlama kayması, önbellek birlikte konumlandırma — adanmış bir altyapı ekibinin tek bir %0,001'lik bug'ı bulmak için günlerce araştırma yapmasına neden olan şeydir. Cloudflare'in kendi itirafı tekrarlanmaya değer: beta ile GA arasındaki fark "bir trençkot giymiş 20 bug düzeltmesiydi."
Oyun backend'iniz, bir pooler, bir önbellek geçersizleştirme şeması ve bir protokol çeviri katmanı sürdürmenize gerek kalmadan güvenilir kalıcılık gerektiriyorsa, Backend-as-a-Service tam burada devreye girer. horizOn, revizyon farkında okuma ve yazma işlemleriyle hesaba bağlı Cloud Save yönetir — istemciniz çakışmaları çözer ve oyuncu verileri için tüm havuzlanmış-bağlantı-artı-önbellek doğruluk mayın tarlasını atlarsınız. Leaderboard'lar ve uzak yapılandırma ile birleştirin; bu yazıda ele alınan sorgu gecikmesi sorunları çoğunlukla sizin sorununuz olmaktan çıkar.
Sonraki adım: bugün kendi pooler yapılandırmanızı denetleyin. Üç şeyi kontrol edin — anlaşılan karakter seti, bağlantı sıfırlamalarından sonra oturum durumunun yeniden tanımlanıp tanımlanmadığı ve önbelleğinizin kanonik mi yoksa çevrilmiş baytları mı sakladığı. On dakika içinde üçüne de cevap veremiyorsanız, yapmanız gereken iş var demektir — ve hiç yapmamayı tercih ederseniz, horizOn'a bir göz atın ve Cloud Save ile leaderboard'larının, veritabanı katmanınızın şu anda elle yaptıklarını karşılayıp karşılamadığını görün.
Kaynak: Hyperdrive'ın İçinde: Nadir bir Hyperdrive MySQL önbellekleme bug'ını nasıl çözdük