Ocak ayının son haftasında bir müşteri, filosundaki 120 aracın aylık kilometre raporunu istedi. Rapor ekranı 4 dakika 12 saniye döndü ve sonunda zaman aşımıyla düştü. Aynı rapor bir yıl önce 9 saniyede geliyordu. Arada değişen tek şey veriydi: konum tablomuz 210 milyon satırdan 940 milyon satıra çıkmış, disk üzerindeki boyutu 1.1 terabaytı geçmişti. NetFleet'te telematik verisiyle boğuştuğumuz o dönemde bu tabloya saniyede ortalama 800, pik saatlerde 2.400 satır yazıyorduk.
Asıl korkutucu olan sorgu süresi değildi. Tabloya yeni bir indeks eklemek istediğimizde ALTER TABLE'ın tahmini süresi 14 saatti. Yani şema fiilen donmuştu. Bir hata yapsak geri dönüşü yoktu.
Önce Suçu Yanlış Yere Attık
İlk teşhisimiz klasikti: "İndeksler kötü." İki hafta boyunca composite indeks kombinasyonları denedik, EXPLAIN çıktılarıyla yattık kalktık. Kısmi iyileşme oldu; 4 dakikalık sorgu 100 saniyeye indi. Ama yazma tarafı kötüleşti, çünkü her yeni indeks insert maliyetini büyütüyordu. Pik saatlerde replikasyon gecikmesi 40 saniyeye tırmandı.
Sonra aylık partisyonlamaya geçtik. Bu gerçekten nefes aldırdı: eski aylara dokunmayan raporlar hızlandı, veri silmek DELETE yerine DROP PARTITION ile saniyeler sürer oldu. Altı ay kadar böyle idare ettik. Ama temel problem yerinde duruyordu: MySQL, satır bazlı depolamasıyla, "bir cihazın üç aylık hız ortalaması" gibi analitik sorgular için her satırın tamamını diskten okuyordu. Bizim iş yükümüz yüzde 95 ekleme, yüzde 5 aralık sorgusuydu ve neredeyse hiç UPDATE yoktu. Bu, ders kitabındaki zaman serisi profiliydi ve biz onu genel amaçlı bir OLTP motorunda tutuyorduk.
Partisyonlama döneminde bir şeyi daha denedik ve anlatmaya değer: arşiv tablosu yaklaşımı. Üç aydan eski veriyi gece işleriyle ayrı bir arşiv tablosuna taşıyıp ana tabloyu küçük tutmak istedik. Kâğıt üzerinde mantıklı; pratikte taşıma işinin kendisi geceleri sunucuyu inletiyor, arşive taşan veriyi sorgulamak isteyen raporlar için de UNION'lı, bakımı çirkin bir sorgu katmanı gerekiyordu. İki tablolu hayat dört ay sürdü ve bize net bir şey öğretti: veri yaşam döngüsü uygulama koduyla değil, depolama motorunun kendi mekanizmalarıyla yönetilmeli. Bu içgörü, zaman serisi veritabanlarına bakmamızın asıl tetikleyicisi oldu; retention ve downsampling'i birinci sınıf kavram olarak sunan bir motor arıyorduk artık.
InfluxDB ile Kısa ve Öğretici Bir Flört
İlk adayımız InfluxDB oldu; zaman serisi denince akla ilk o geliyordu. Prototip etkileyiciydi: aynı veri yüzde 90 daha az yer tuttu, aralık sorguları milisaniyelerle ölçülüyordu. Sonra kardinalite duvarına çarptık. Cihaz kimliğini, sürücü kimliğini ve alarm tipini tag olarak modelleyince seri sayısı 4 milyonu geçti ve bellek kullanımı kontrolden çıktı; 16 GB'lık test makinesi günde bir kez OOM ile ölüyordu. Tag'leri azaltınca bellek düzeldi ama bu kez "sürücüye göre grupla" gibi sorgular yavaşladı. Bir de ekipte kimsenin bilmediği ayrı bir sorgu dili öğrenme maliyeti vardı; raporlama katmanımızdaki yüzlerce SQL'in çevirisi başlı başına projeydi.
TimescaleDB Kararı ve Çift Yazım Dönemi
İkinci aday TimescaleDB idi. PostgreSQL uzantısı olması iki şeyi birden çözüyordu: SQL bilgimiz aynen geçerliydi ve konum verisini araç, müşteri, sürücü tablolarıyla JOIN'leyebiliyorduk. Hypertable yapısı, bizim elle yürüttüğümüz partisyonlamayı çok daha ince taneli ve otomatik yapıyordu. Sıkıştırma açıldığında 1.1 TB'lık veri 96 GB'a indi; yaklaşık 12 kat.
Geçişi büyük patlama yöntemiyle yapmadık. Altı hafta boyunca çift yazım uyguladık: her konum paketi hem MySQL'e hem TimescaleDB'ye gitti. Bu dönemde iki sistemin gün sonu sayımlarını karşılaştıran bir kontrol işi koştu ve ilk hafta bize utanç verici bir bug yakalattı: saat dilimi dönüşümünde gece yarısına denk gelen kayıtlar yanlış güne yazılıyordu. Çift yazım olmasa bunu müşteri fark edecekti. Tarihsel verinin geri doldurulmasını 10 milyon satırlık parçalar hâlinde, gece pencerelerinde yaptık; toplam 11 gece sürdü.
Yazma yolunda da bir sürprizle karşılaştık. Cihazlar veriyi her zaman kronolojik göndermiyor; ağ kesintisinden dönen bir cihaz, iki gün öncesine ait paketleri bugünün paketlerinin arasına serpiştirebiliyor. MySQL'de bu kimsenin umurunda değildi; TimescaleDB'de ise sıkıştırılmış eski chunk'lara geç gelen bu kayıtlar, chunk'ın açılıp yeniden sıkıştırılmasına yol açıyor ve bunu yüz binlerce kez yaparsanız fark ediliyor. Çözümümüz sıkıştırma penceresini geciktirmek oldu: bir chunk, üzerinden 7 gün geçmeden sıkıştırılmıyor; geç gelen verinin yüzde 99.9'u bu pencereye sığıyor. Kalan binde birlik kuyruk için de ayrı bir geç-veri tamponu tuttuk. Zaman serisi motorları kronolojik yazım varsayımına yaslanır; sahadan veri topluyorsanız bu varsayımı sorgulamadan geçmeyin.
Downsampling: Asıl Kazanç Buradaydı
Geçişin en büyük getirisi motor değişimi değil, veri yaşam döngüsünü nihayet tasarlamamız oldu. Ham konum verisini 90 gün tuttuk. Continuous aggregate ile her cihaz için 5 dakikalık özetler (ortalama hız, kat edilen mesafe, alarm sayısı) ürettik ve bunları 2 yıl sakladık. Günlük özetler ise süresiz kaldı. Aylık kilometre raporu artık 940 milyon ham satırı değil, önceden hesaplanmış günlük özet tablosunu okuyordu: 4 dakikalık sorgu 280 milisaniyeye indi.
Burada bir hata da yaptık ve pahalıya öğrendik. İlk downsampling şemasında "maksimum hız" alanını özetlere koymayı unutmuştuk. 90 gün dolup ham veri silinmeye başlayınca, bir sigorta şirketinin talep ettiği geçmişe dönük hız ihlali analizi için elimizde veri kalmadığını fark ettik. Özet şemasını tasarlarken kendinize sorulacak soru "bugün hangi raporlar var" değil, "iki yıl sonra hangi soruya hayır demek zorunda kalmak istemem" olmalı. Ham veriyi silmeden önce en az bir çeyrek boyunca özetlerin gerçekten yettiğini kanıtlayın.
Donanım tarafındaki muhasebe de ilginç. MySQL kurulumumuz, o tabloyu taşıyabilmek için 64 GB RAM'li, NVMe diskli bir makineye şişmişti ve yine de nefes nefeseydi. TimescaleDB aynı işi 32 GB'lık bir makinede, diskin yarısını boş bırakarak yapıyor. Sıkıştırmanın bir yan hediyesi de yedekleme oldu: gece yedeği 1.1 TB yerine 96 GB kopyaladığı için 6 saatten 40 dakikaya indi ve yedeğin sisteme etkisi hissedilmez hâle geldi. Ekipteki öğrenme eğrisi ise korktuğumuzdan yumuşaktı; sorgular sonuçta SQL ve PostgreSQL'in EXPLAIN'i, MySQL'den gelen biri için birkaç haftalık alışkanlık meselesi. En çok yadırganan şey chunk kavramı oldu; "tablom kaç parçada" sorusuna alışmak zaman aldı.
Geriye Dönüp Bakınca
Bu arada raporlama katmanında ummadığımız bir kültür değişimi oldu. Sorgular ucuzlayınca ürün ekibi, eskiden "veritabanını yorar" diye açmaya çekindiğimiz ekranları istemeye başladı: saatlik rölanti analizi, sürüş tarzı skorları, filo karşılaştırmaları. Yani performans çalışması yalnızca mevcut ekranları hızlandırmadı; ürünün hayal gücünü genişletti. Yavaş altyapının en sinsi maliyeti, kimsenin istemeye cesaret edemediği özelliklerdir.
Bu geçiş bize dört ay ve epey uykusuz gece maliyetine geldi ama sorgu süreleri ortalama 40 kat kısaldı, depolama maliyeti onda birine indi ve şema yeniden nefes alır oldu. En kalıcı ders şuydu: problem MySQL'in yavaşlığı değildi; iş yükümüzün karakterini geç teşhis etmemizdi. Yazma ağırlıklı, ekleme temelli, zamana göre sorgulanan veri birikiyorsa, tablo 100 milyon satıra gelmeden yaşam döngüsünü tasarlamak gerekiyor. Biz 940 milyonda uyandık; siz o kadar beklemeyin. Benzer bir geçişin eşiğindeyseniz ve nereden başlayacağınızı kestiremiyorsanız buradan yazın, yaşadıklarımızı memnuniyetle anlatırım.