Kafayı yemek üzereydim. Geliştirdiğimiz, saniyede binlerce istek alması gereken dağıtık sistemin bir endpoint'i, basit bir veri listeleme işleminde dakikalarca beklemeye başlamıştı. "Bu kadar basit bir sorgu nasıl bu hale gelir?" diye terminale bakakaldım. Meğerse her şey, o meşhur "premature optimization kötüdür, önce çalışsın" mottosuna fazla güvenmemle başlamış.
Performans Krizi ve Panik Modu
Sistem tasarım aşamasındayken, "ileride ölçekleniriz, şimdilik basit tutalım" diyerek veri erişim katmanını oldukça naif bir şekilde yazmıştım. Her istekte, ilişkisel veritabanından JOIN'li devasa sorgular atılıyor, aynı hesaplamalar tekrar tekrar yapılıyordu. Küçük test veri setinde sorun yoktu tabii. Ama canlıya alınıp veri arttıkça, her şey içinden çıkılmaz bir hale geldi. Profiler'ı açtığımda gördüğüm manzara içler acısıydı: Cache miss oranları %90'ları geçiyor, veritabanı CPU'su sürekli %100'e oynuyordu.
Çözüm Arayışı ve "Geç" Optimizasyon
StackOverflow'da bile doğrudan çözüm bulamadım, çünkü sorun spesifik mimarimdeydi. İsyan ettikten sonra kafamı toparlayıp strateji geliştirdim:
1. Agressive Caching: Sık erişilen, az değişen veriler için Redis ile çok katmanlı bir cache stratejisi kurdum.
2. Sorgu Optimizasyonu: O devasa JOIN'leri parçaladım, gereksiz kolonları çekmeyi bıraktım, bazı hesaplamaları materialized view'lara taşıdım.
3. Veri Yapısı Gözden Geçirmesi: En kritik nokta buydu. Sık erişilen verilerin bellek düzenini (data locality) iyileştirdim. Bu sayede cache'lerden getirilen bir veri bloğu içinde, ihtiyaç duyulan diğer ilgili veriler de hazır oluyordu ve cache miss oranı dramatik şekilde düştü.
Sonuç ve Acı Bir Gerçek
Birkaç günlük yoğun optimizasyon çalışmasından sonra, dakikalar süren istekler milisaniyelere düştü. Sistem nefes almaya başladı. O an anladım ki, "premature optimization" ile "tasarım aşamasında performansı düşünmemek" aynı şey değilmiş. İlki, mikro seviyede gereksiz karmaşıklık demek. Ama ikincisi, makro seviyede bir tasarım ihmalidir ve sonradan düzeltmesi çok daha zor ve acı verici oluyor.
Artık şöyle düşünüyorum: "Ölçeklenebilir bir tasarım yap, mikro optimizasyonları sonraya bırak." Siz ne düşünüyorsunuz? Hiç "çalışsın da nasıl çalışırsa çalışsın" deyip, sonra benim gibi cache cehenneminde kaybolduğunuz oldu mu? Daha temel, daha şık bir yaklaşımınız var mı?
Sistem tasarım aşamasındayken, "ileride ölçekleniriz, şimdilik basit tutalım" diyerek veri erişim katmanını oldukça naif bir şekilde yazmıştım. Her istekte, ilişkisel veritabanından JOIN'li devasa sorgular atılıyor, aynı hesaplamalar tekrar tekrar yapılıyordu. Küçük test veri setinde sorun yoktu tabii. Ama canlıya alınıp veri arttıkça, her şey içinden çıkılmaz bir hale geldi. Profiler'ı açtığımda gördüğüm manzara içler acısıydı: Cache miss oranları %90'ları geçiyor, veritabanı CPU'su sürekli %100'e oynuyordu.
SQL:
-- İşte o masum görünen ama katil sorgu:
SELECT FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
WHERE o.status = 'processing'
ORDER BY o.created_at DESC;
StackOverflow'da bile doğrudan çözüm bulamadım, çünkü sorun spesifik mimarimdeydi. İsyan ettikten sonra kafamı toparlayıp strateji geliştirdim:
1. Agressive Caching: Sık erişilen, az değişen veriler için Redis ile çok katmanlı bir cache stratejisi kurdum.
2. Sorgu Optimizasyonu: O devasa JOIN'leri parçaladım, gereksiz kolonları çekmeyi bıraktım, bazı hesaplamaları materialized view'lara taşıdım.
3. Veri Yapısı Gözden Geçirmesi: En kritik nokta buydu. Sık erişilen verilerin bellek düzenini (data locality) iyileştirdim. Bu sayede cache'lerden getirilen bir veri bloğu içinde, ihtiyaç duyulan diğer ilgili veriler de hazır oluyordu ve cache miss oranı dramatik şekilde düştü.
Birkaç günlük yoğun optimizasyon çalışmasından sonra, dakikalar süren istekler milisaniyelere düştü. Sistem nefes almaya başladı. O an anladım ki, "premature optimization" ile "tasarım aşamasında performansı düşünmemek" aynı şey değilmiş. İlki, mikro seviyede gereksiz karmaşıklık demek. Ama ikincisi, makro seviyede bir tasarım ihmalidir ve sonradan düzeltmesi çok daha zor ve acı verici oluyor.
Artık şöyle düşünüyorum: "Ölçeklenebilir bir tasarım yap, mikro optimizasyonları sonraya bırak." Siz ne düşünüyorsunuz? Hiç "çalışsın da nasıl çalışırsa çalışsın" deyip, sonra benim gibi cache cehenneminde kaybolduğunuz oldu mu? Daha temel, daha şık bir yaklaşımınız var mı?