<

Kardinalite Patlaması: IoT Metrikleri İzleme Sistemini Çökertince

Nisan sonunda bir salı sabahı, İzmir'deki ofiste daha kahvemi koymadan NOC ekibinden mesaj geldi: "Grafana açılmıyor, paneller timeout veriyor." Panele değil, panelin arkasındaki Prometheus'a baktım: bellek kullanımı 6 GB'tan 38 GB'a tırmanmış, pod OOMKilled döngüsüne girmişti. Her yeniden başlatmada WAL replay 14 dakika sürüyor, o 14 dakika boyunca 10 binin üzerinde sahadaki cihaza dair hiçbir şey göremiyorduk. İzleme sistemi çökmüştü ve işin acı ironisi şuydu: onu çökerten şey, izlediği sistem değil, izleme biçimimizin ta kendisiydi.

Aktif zaman serisi sayımıza baktığımda rakamı iki kez okudum: 11,4 milyon. Üç hafta önce bu sayı 480 bindi. Sahaya yeni cihaz eklememiştik. Peki bu 11 milyon seri nereden gelmişti?

Her Etiket Bir Çarpandır

Prometheus'ta bir metrik adı tek başına ucuzdur; pahalı olan etiket kombinasyonlarıdır. Her benzersiz etiket kümesi ayrı bir zaman serisidir ve her seri bellekte kendi index girdisiyle, kendi chunk'larıyla yaşar. "device_id etiketi ekleyelim, cihaz bazında görürüz" dediğiniz anda 10 bin cihaz size 10 bin seri üretir. Buna firmware_version eklerseniz (sahada 14 farklı sürüm dönüyordu) teorik uzay 140 bine çıkar. Bir de region, model, sim_operator derken çarpanlar birbirine binmeye başlar. Biz bu çarpımı yıllardır kontrollü götürüyorduk; 480 bin seri, 6 GB bellekle gayet yaşanabilir bir dengeydi.

Dengeyi bozan şey, iki sprint önce gateway yazılımına giren masum görünüşlü bir satırdı. Bağlantı sorunlarını daha iyi teşhis edebilmek için bir geliştirici arkadaş MQTT oturum metriklerine connection_id etiketi eklemişti — her TCP oturumunda yeniden üretilen, UUID formatında bir kimlik. Kararlı bağlantılarda bu etiket az sayıda değer üretiyordu, testte de öyle görünmüştü. Ama sahada 2G'ye düşen, beş dakikada bir kopup yeniden bağlanan yüzlerce cihaz vardı. Her reconnect yeni bir UUID, her UUID yeni bir seri demekti. Cihaz başına günde 200'den fazla reconnect yapan huysuz bölgeler, tek başlarına milyonlarca ölü seri doğurmuştu.

Suçlu Sandığımız Masumlar

Tabii biz önce yanlış yerlere baktık. İlk hipotez Grafana'ydı: "Birisi ağır bir dashboard yapmıştır, her paneli 10 saniyede bir sorguluyordur." Query loglarını inceledik, ağır sorgular vardı ama bellek büyümesini açıklamıyorlardı; sorgu yükü CPU yakar, kalıcı bellek şişmesi yapmaz. İkinci hipotez retention'dı: "Disk dolmuştur, compaction takılmıştır." TSDB compaction loglarında gecikme vardı ama bu sebep değil sonuçtu. Neredeyse yarım gün, semptomları sebep sanarak dolandık.

Bizi doğru yere götüren komut, Prometheus'un kendi TSDB istatistikleriydi. Status sayfasındaki "highest cardinality labels" tablosuna bakınca her şey ekranda duruyordu: connection_id, 9,7 milyon benzersiz değer. İkinci sıradaki etiketin 14 bin değeri vardı. Ölçek farkı o kadar gülünçtü ki ekipte kısa bir sessizlik oldu. Kardinalite patlaması dedektiflik gerektirmez; nereye bakacağınızı biliyorsanız tablo size suçluyu ismiyle söyler. Biz nereye bakacağımızı altı saat geç hatırladık.

Yangını Söndürmekle Sistemi Değiştirmek Ayrı İşler

Acil müdahale basitti: gateway'lerin scrape config'ine metric_relabel_configs ile connection_id etiketini düşüren bir kural koyduk ve sorunlu metriği kaynağında kapattırdık. Eski seriler retention süresince (bizde 15 gün) indexte yaşamaya devam edecekti; bunu beklemek yerine ilgili metriklere admin API üzerinden tombstone bastık ve compaction sonrası bellek 9 GB'a indi. Ertesi sabah paneller açılıyordu. Ama ben o hafta asıl işin bu olmadığını, aynı filmin başka bir etiketle yeniden çekileceğini biliyordum.

Kalıcı çözüm üç katmandan oluştu. Birincisi, etiket bütçesi: her metrik için maksimum seri sayısını tahmin eden ve sınır koyan bir iç kural. Yeni bir etiket eklemek isteyen geliştirici, o etiketin alabileceği değer sayısını PR açıklamasına yazmak zorunda. UUID, session id, kullanıcı id, tam URL, hata mesajı metni — bunların hiçbiri etiket olamaz; bu artık yazılı bir kural ve CI'da metrik lint kontrolü var.

İkincisi, cihaz bazlı gözlem ihtiyacını dürüstçe kabul etmek. "Cihaz başına metrik tutmayın" demek kolay ama saha ekibi tek bir cihazın sinyal geçmişini görmek istediğinde bir cevabınız olmalı. Biz cihaz bazlı ham telemetriyi Prometheus'tan tamamen çıkarıp zaten akmakta olduğu TimescaleDB'ye bıraktık; Prometheus'ta yalnızca filo seviyesinde agregatlar kaldı — bölge, model ve firmware sürümü kırılımında ortalamalar, yüzdelikler, sayaçlar. Tek cihaz sorgusu artık Grafana'da ayrı bir datasource'tan geliyor. Seri sayımız 480 binden 92 bine, Prometheus belleği 6 GB'tan 2,1 GB'a indi; sorguların p99 süresi 4,8 saniyeden 700 milisaniyeye düştü.

Üçüncüsü, erken uyarı. İzleme sistemini izlemiyorduk; artık izliyoruz. Aktif seri sayısı, seri yaratma hızı (churn) ve en yüksek kardinaliteli 10 etiket için ayrı bir panel ve iki alarm kuralımız var: toplam seri 200 bini aşarsa uyarı, bir saatte yüzde 20'den fazla seri artışı olursa kritik alarm. O masum connection_id satırı bugün yazılsaydı, üretime çıktıktan 40 dakika sonra alarm çalar ve olay bir OOM krizi değil, bir Slack mesajı olarak kapanırdı.

Bu arada histogramlar konusunda da bir uyanış yaşadık. Prometheus histogramı masum bir metrik gibi görünür ama her biri, bucket sayısı kadar seriye açılır: 12 bucket'lı bir gecikme histogramı, etiket kombinasyonu başına 14 seri demek (bucket'lar artı sum ve count). Gateway'lerdeki bir HTTP histogramı, endpoint ve status etiketleriyle çarpılınca tek başına 38 bin seri tutuyordu ve kimse bu metriğe altı aydır bakmamıştı. Denetim sırasında "son 90 günde hiç sorgulanmamış metrikler" listesi çıkardık; toplam serilerin yüzde 22'si, hiçbir panelde ve hiçbir alarmda kullanılmayan metriklerden geliyordu. Bunları kaynağında kapattık. Ölçmediğiniz şeyi yönetemezsiniz sözü doğru; ama baktığınız her şeyi ölçmek de ayrı bir hastalık.

Kardinalite Bir Teknik Detay Değil, Maliyet Kalemidir

Bu olaydan aklımda kalan asıl ders teknik değil, kültürel. Metrik eklemek bedava hissettirir: bir satır kod, anında grafik. Oysa her etiket, sistemin ömrü boyunca ödenen bileşik faizli bir borçtur — bellek, disk, sorgu süresi ve en kötü anda gelen operasyonel kırılganlık olarak geri döner. Loglar ve trace'ler yüksek kardinaliteyi kaldıracak şekilde tasarlanmıştır; metrikler ise düşük kardinalite varsayımı üzerine kuruludur. Hangi verinin hangi eve ait olduğunu karıştırdığınızda, ev yıkılır.

Bir de şunu öğrendim: kardinalite patlamaları neredeyse hiçbir zaman kötü mühendislikten çıkmıyor; iyi niyetli teşhis çabasından çıkıyor. Bizim vakadaki connection_id etiketi gerçekten işe yarayacak bir fikirdi — yanlış olan fikir değil, onu koyduğumuz katmandı. IoT filonuzda Prometheus şişiyor, paneller yavaşlıyor ve kimse sebebini bulamıyorsa, TSDB status sayfasındaki kardinalite tablosuna bakın; cevap büyük ihtimalle orada, tek satırda yazıyor. Tablonun neresine bakacağınızdan emin değilseniz iletişim sayfasından ulaşın, yarım saatte beraber bakarız — bu yarım saat bize altı saate ve bir gecelik körlüğe mal olmuştu.

O salı gününden beri ekipte bir deyiş dolaşır: "Etiket eklemek özgürlüktür, etiketin değer kümesi sorumluluktur." Panolarımız artık daha sade, ama hiç bu kadar hızlı ve bu kadar ayakta olmamıştı.

📅 Yayınlanma:  ·  Yakup Zengin