Kafayı yiyecektim dostlar. Kendi oyun sunucumu yazıyorum, her şey güzel gidiyor derken, aniden oyun dünyası donuyor. Oyuncular "abi hareket etmiyor" diye isyan ediyor. Meğerse sorun, PostgreSQL'in derinliklerinde bir deadlock sarmalındaymış. Debug için harcadığım o saatler, kod yazmaktan çok, sabır testiydi.
Loglarda Gördüğüm Kabus
Sunucu loglarını açtım ve şu cümleyle karşılaştım: `deadlock detected`. İçimden "tamam, buldum" dedim. Aşağıya indim, detaylarına baktım. İki query birbirini bekliyordu. Biri oyuncunun envanterini güncelliyor, diğeri aynı oyuncunun ticaret teklifini işliyordu. İkisi de aynı satırlara kilit vurmaya çalışıyor, biri diğerini bırakmıyordu. StackOverflow'da bile bu kadar net bir örnek bulamazsınız.
Debug Süreci ve İsyanım
İlk başta, "transaction isolation level" ile oynayayım dedim. Belki READ COMMITTED yetmez, REPEATABLE READ denerim diye düşündüm. Şaka gibi ama, durum daha da kötüleşti. Sonra fark ettim ki, asıl mesele transaction'ları sıraya sokmak değil, kilit alma sırasını (lock order) tutarlı hale getirmekmiş.
Koduma döndüm, bütün update ve delete işlemlerini taradım. "Acaba hangi tabloya önce, hangisine sonra lock atıyorum?" diye. Meğerse, EnvanterService sınıfımda tablolara bir sırada, TradeService'te ise tam tersi bir sırada erişiyormuşum. İnanılmaz bir detay ve saatlerimi yedi.
Çözüm ve Alınan Ders
Çözüm aslında basitti: Global bir lock sırası belirlemek. Projede bir doküman açtım, başına "DEADLOCK KORUMA PROTOKOLÜ" yazdım. İçine "Tüm transaction'larda tablolara şu sırada erişilecek: 1. players, 2. inventory, 3. trade_offers..." diye bir kural koydum. Ve tabii ki, tüm transaction'ları mümkün olduğunca kısa tutmak.
Aldığım hayat dersi şu: Kodu yazarken "çalışıyor" olması yetmez. Özellikle multi-thread ve database işlerinde, kaynaklara erişim sırası, çalışmasından daha kritik olabilir. Bir daha asla "lock order"ı hafife almam.
Siz de böyle görünmez bir deadlock tuzağına düşüp, saatlerinizi debug'a harcadınız mı? Bu işin otomatik tespiti için pg_stat_activity dışında kullandığınız pratik bir yöntem var mı?
Sunucu loglarını açtım ve şu cümleyle karşılaştım: `deadlock detected`. İçimden "tamam, buldum" dedim. Aşağıya indim, detaylarına baktım. İki query birbirini bekliyordu. Biri oyuncunun envanterini güncelliyor, diğeri aynı oyuncunun ticaret teklifini işliyordu. İkisi de aynı satırlara kilit vurmaya çalışıyor, biri diğerini bırakmıyordu. StackOverflow'da bile bu kadar net bir örnek bulamazsınız.
SQL:
-- Process 123: UPDATE inventory SET item_id = 5 WHERE player_id = 42;
-- Process 456: UPDATE trade_offers SET status = 'accepted' WHERE player_id = 42;
İlk başta, "transaction isolation level" ile oynayayım dedim. Belki READ COMMITTED yetmez, REPEATABLE READ denerim diye düşündüm. Şaka gibi ama, durum daha da kötüleşti. Sonra fark ettim ki, asıl mesele transaction'ları sıraya sokmak değil, kilit alma sırasını (lock order) tutarlı hale getirmekmiş.
Koduma döndüm, bütün update ve delete işlemlerini taradım. "Acaba hangi tabloya önce, hangisine sonra lock atıyorum?" diye. Meğerse, EnvanterService sınıfımda tablolara bir sırada, TradeService'te ise tam tersi bir sırada erişiyormuşum. İnanılmaz bir detay ve saatlerimi yedi.
Çözüm aslında basitti: Global bir lock sırası belirlemek. Projede bir doküman açtım, başına "DEADLOCK KORUMA PROTOKOLÜ" yazdım. İçine "Tüm transaction'larda tablolara şu sırada erişilecek: 1. players, 2. inventory, 3. trade_offers..." diye bir kural koydum. Ve tabii ki, tüm transaction'ları mümkün olduğunca kısa tutmak.
Aldığım hayat dersi şu: Kodu yazarken "çalışıyor" olması yetmez. Özellikle multi-thread ve database işlerinde, kaynaklara erişim sırası, çalışmasından daha kritik olabilir. Bir daha asla "lock order"ı hafife almam.
Siz de böyle görünmez bir deadlock tuzağına düşüp, saatlerinizi debug'a harcadınız mı? Bu işin otomatik tespiti için pg_stat_activity dışında kullandığınız pratik bir yöntem var mı?