İçeriğe Atla
Mustafa Erbay
Kariyer · 10 dk okuma · görüntülenme Read in English

Observability'de Cardinality Patlamasını 3 Adımda Yönetme

Metrik altyapılarında maliyetleri ve diskleri patlatan yüksek cardinality sorununu tespit etme, filtreleme ve yönetme stratejileri.

100%

Sistem odalarında kablo çakarak başladığım bu meslekte, her yeni teknolojinin beraberinde getirdiği gizli maliyetleri ve operasyonel yükleri bizzat yaşayarak öğrendim. Eskiden disk dolunca sunucuya ssh atıp log silerdik; şimdi ise merkezi loglama ve metrik sistemlerimizin (observability) kendi disklerini ve bütçelerini kurtarmak için mesai harcıyoruz.

Özellikle dağıtık mimarilerde ve konteynerize ortamlarda karşımıza çıkan en büyük sinsi canavarlardan biri “cardinality patlaması” (cardinality explosion). Yazılımcıların debug kolaylığı olsun diye metriklere eklediği ufacık bir etiket (label), ay sonunda finans ekibinden gelen binlerce dolarlık faturanın ya da gece yarısı diskin %100 dolmasıyla çalan alarmların baş sorumlusu olabiliyor. Bu yazıda, kariyerim boyunca hem kendi projelerimde hem de danışmanlık verdiğim yapılarda bu sorunu nasıl çözdüğümü somut adımlarla anlatacağım.

Cardinality Nedir ve Neden Bir Platform Mühendisinin Kariyerini Tehdit Eder?

Matematiksel tanımıyla cardinality, bir kümenin benzersiz eleman sayısıdır. Gözlemlenebilirlik (observability) dünyasında ise cardinality, bir metriğin sahip olduğu etiketlerin (labels) alabileceği tüm benzersiz değerlerin kombinasyonudur. Yani bir kartezyen çarpım problemidir. Prometheus, VictoriaMetrics veya Grafana Mimir gibi zaman serisi veritabanlarında (TSDB) her bir benzersiz etiket kombinasyonu, diskte ve bellekte yeni bir “time series” (zaman serisi) oluşturur.

Bir örnekle açıklayayım. Elimizde HTTP isteklerinin süresini ölçen basit bir metriğimiz olsun: http_request_duration_seconds_bucket. Bu metriğe şu etiketleri eklediğimizi varsayalım:

  • method: GET, POST, PUT, DELETE (4 benzersiz değer)
  • status: 200, 201, 400, 401, 500 (5 benzersiz değer)
  • handler: /api/v1/login, /api/v1/checkout, /api/v1/users (3 benzersiz değer)

Bu durumda toplam zaman serisi sayısı: 4 * 5 * 3 = 60’tır. Bu son derece makul ve yönetilebilir bir sayıdır. Ancak işgüzar bir yazılımcı arkadaşımız, debug yaparken kolaylık olsun diye metriğe user_id etiketini eklemeye karar verirse ne olur? Sisteme kayıtlı 50.000 aktif kullanıcı olduğunu düşünelim:

4 (method) * 5 (status) * 3 (handler) * 50.000 (user_id) = 3.000.000 Zaman Serisi!

Tek bir metrik için 3 milyon aktif zaman serisi ürettiniz. TSDB bu serilerin her biri için bellekte (RAM) indeks tutmak zorundadır. Bellek tüketimi bir anda 4 GB’tan 64 GB seviyesine fırlar, disk I/O tavan yapar ve nihayetinde OOM (Out Of Memory) killer devreye girip veritabanınızı kapatır. Eğer Datadog gibi SaaS bir çözüm kullanıyorsanız, ay sonu gelen beklenmedik derecede yüksek fatura sonrasında kendinizi genel müdüre açıklama yaparken bulursunuz. İşte bu yüzden cardinality yönetimi, sadece teknik bir detay değil, bir platform mühendisinin kariyer ömrünü belirleyen kritik bir FinOps yetkinliğidir.

Adım 1: Dinamik Metrik Etiketlerini (Dynamic Labeling) Tespit Etme ve Engelleme

Yüksek cardinality’nin bir numaralı sebebi, etiket değerleri olarak dinamik verilerin kullanılmasıdır. UUID’ler, kullanıcı e-posta adresleri, IP adresleri, query parametreleri veya sipariş numaraları asla ve asla metrik etiketi (label) olmamalıdır. Bu tür detaylı ve dinamik verilerin yeri metrikler değil, loglar veya distributed tracing (trace ID’ler) sistemleridir.

Eğer sisteminizde halihazırda bu hata yapılmışsa, ilk yapmanız gereken hangi etiketlerin bu patlamaya sebep olduğunu bulmaktır. Prometheus kullanıyorsanız, TSDB durumunu incelemek için şu API sorgusunu atarak en yüksek cardinality’ye sahip etiket isimlerini görebilirsiniz:

curl -g 'http://localhost:9090/api/v1/status/tsdb' | jq '.data.labelNamesWithHighestNumOfLabelValues'

Çıktı muhtemelen size hangi etiketin suçlu olduğunu doğrudan söyleyecektir; tipik olarak user_id, transaction_id gibi dinamik etiketler binlerce hatta on binlerce benzersiz değerle listenin başında çıkar.

Bu etiketleri kod seviyesinde temizlemek zaman alabilir. Acil müdahale olarak, Prometheus’un relabel_configs mekanizmasını kullanarak bu etiketleri scrape (toplama) aşamasında drop edebilir veya normalize edebilirsiniz. Örneğin, /api/v1/users/12345 gibi dinamik URL’leri tek bir /api/v1/users/:id kalıbına dönüştürmek hayat kurtarır.

relabel_configs ile Dinamik Path Normalizasyonu

Aşağıdaki Prometheus konfigürasyonu, path etiketindeki UUID veya sayısal ID içeren değerleri tespit edip onları normalize eder. Böylece binlerce farklı zaman serisi oluşması engellenir:

scrape_configs:
  - job_name: 'api-service'
    static_configs:
      - targets: ['localhost:8080']
    relabel_configs:
      # UUID içeren path'leri normalize et
      - source_labels: [__name__, path]
        regex: 'http_request_duration_seconds_bucket;([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})'
        target_label: path
        replacement: '/api/v1/resource/:uuid'
      # Sayısal ID içeren path'leri normalize et
      - source_labels: [path]
        regex: '.*/[0-9]+.*'
        target_label: path
        replacement: '/api/v1/resource/:id'

Bu kural sayesinde, scrape aşamasında veritabanına yazılmadan önce tüm dinamik path’ler birleştirilir. Disk üzerinde oluşacak milyonlarca zaman serisinin önüne daha veri TSDB’ye girmeden geçmiş olursunuz.

Adım 2: Aggregation Rules ve Recording Rules ile Veriyi Önceden Küçültme

Her zaman serisinin saniyeler bazında saklanmasına gerek yoktur. Örneğin, son 3 saatlik CPU kullanımını saniyelik detayda görmek isteyebiliriz ancak 6 ay önceki CPU kullanımını saatlik ortalamalar şeklinde görmek bizim için fazlasıyla yeterlidir. Ham veriyi uzun süre saklamak yerine önceden aggregate (özetlemek) etmek hem disk alanından tasarruf sağlar hem de dashboard’larınızın yüklenme hızını artırır.

Prometheus dünyasında buna Recording Rules diyoruz. Bu kurallar, arka planda belirlediğiniz sorguları periyodik olarak çalıştırır ve sonucunu yeni, düşük cardinality’ye sahip bir metrik olarak kaydeder.

Örneğin, aşağıdaki kural her 1 dakikada bir çalışarak tüm pod’lar bazındaki HTTP istek oranlarını toplar ve servis bazında tek bir metrik haline getirir:

groups:
  - name: api_rules
    rules:
      - record: service:http_requests:rate5m
        expr: sum by (service, status) (rate(http_requests_total[5m]))

Bu kuralı yazdıktan sonra yapmanız gereken iki şey vardır:

  1. Grafana dashboard’larınızdaki karmaşık ve yavaş sorguları service:http_requests:rate5m yeni metriği ile değiştirmek.
  2. Orijinal yüksek cardinality’li http_requests_total metriğinin ham verilerini saklama süresini (retention) kısa tutmak (örneğin 3 gün), aggregate edilmiş yeni metriği ise 1 yıl boyunca saklamak.

Adım 3: Drop ve Keep Filtreleri ile OpenTelemetry Collector Seviyesinde Eleme

Eğer modern bir mimari kuruyorsanız, metrikleri doğrudan Prometheus’a göndermek yerine araya bir OpenTelemetry (OTel) Collector koymak en mantıklı mimari kararlardan biridir. OTel Collector, veriyi kaynağından alıp işledikten sonra istediğiniz hedefe (Prometheus, Datadog, Mimir vb.) yönlendiren bir proxy görevi görür.

Bu mimari bize, veri daha merkezi depolama alanına gitmeden önce üzerinde tam kontrol sahibi olma avantajı sağlar. Collector üzerinde tanımlayacağımız filter ve transform işlemcileri (processors) ile gereksiz metrikleri veya etiketleri daha ağdan geçerken eleyebiliriz.

Büyük ölçekli altyapılarda bu tür ara proxy katmanları, merkezi depolamaya yük binmeden önce gürültüyü kesmek için kritik bir kontrol noktası sağlar. OTel Collector üzerinde uygulayabileceğiniz örnek bir config.yaml şu şekildedir:

receivers:
  otlp:
    protocols:
      grpc:
      http:

processors:
  # Belirli metrikleri tamamen drop et
  filter/drop_noisy_metrics:
    metrics:
      exclude:
        match_type: regexp
        metric_names:
          - ".*\\.internal\\..*"
          - "process\\.cpu\\.time"
          - "jvm\\.gc\\.memory\\.allocated"

  # Metriklerin içindeki yüksek cardinality'li etiketleri sil
  transform/clean_labels:
    error_mode: ignore
    metric:
      - keep_keys(attributes, ["service.name", "http.status_code", "http.method"])

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [filter/drop_noisy_metrics, transform/clean_labels]
      exporters: [prometheus]

Bu konfigürasyon sayesinde:

  1. JVM’in veya iç servislerin ürettiği ve operasyonel olarak kullanmadığımız gürültülü (noisy) metrikleri tamamen eliyoruz.
  2. Kalan metriklerin içindeki user_id, session_id gibi tüm ekstra etiketleri transform işlemcisi ile silip sadece analiz için kritik olan service.name, status_code ve method etiketlerini tutuyoruz.

Gerçek Hayattan Bir Senaryo: Üretim ERP’sindeki Milyonlarca Aktif Time Series’i Nasıl İndirdim?

Birkaç yıl önce, üzerinde çalıştığım bir üretim ERP’sinin izleme altyapısında ciddi bir tıkanma yaşadık. Operatör ekranlarından, el terminallerinden ve üretim bandındaki IoT cihazlarından gelen metrik akışı o kadar yoğundu ki, Prometheus sunucumuz günde birkaç kez disk I/O limitlerine takılıp kilitleniyordu.

Sorunu incelediğimizde, IoT cihazlarından gelen sıcaklık ve basınç metriklerine her bir iş emrinin (work order ID) ve operatör ID’sinin etiket olarak basıldığını gördük. İş emirleri sürekli değiştiği için aktif zaman serisi (active time series) sayısı milyonlar seviyesine ulaşmıştı. TSDB indeksleri RAM’e sığmıyor ve disk sürekli swap yapıyordu.

Optimizasyon sonrası tablo dramatik biçimde değişti: aktif zaman serisi sayısı büyük çoğunluğuyla eridi, sunucu RAM kullanımı kabaca beşte birine indi, disk yazma operasyonları çok belirgin biçimde düştü ve ham veriden özetlenmiş metriklere geçtiğimiz için dashboard sorguları neredeyse anında açılır hale geldi.

Çözüm için şu adımları uyguladım:

  1. Etiket Taşıma: İş emri ve operatör bilgilerini metriklerden tamamen çıkardım. Bu bilgileri PostgreSQL veritabanımızdaki log tablolarına ve OpenTelemetry trace attribute’larına yönlendirdim.
  2. Prometheus Relabeling: Kod güncellemesi yayına alınana kadar geçen sürede, Prometheus üzerinde labeldrop kuralı tanımlayarak gelen metriklerdeki dinamik etiketleri anında sildim.
  3. Mimir Entegrasyonu: Uzun vadeli analizler için Grafana Mimir kurarak verileri s3 uyumlu bir nesne depolama (object storage) alanına gönderdim ve yerel disk bağımlılığını ortadan kaldırdım.

Bu basit ama kararlı adımlar sayesinde, ek donanım maliyetine girmeden mevcut altyapının ömrünü uzattık ve sistemin kararlılığını sağladık.

Kariyer Perspektifi: Finansal Bilinçli Mühendislik (FinOps) ve Bütçe Yönetimi

Teknoloji dünyasında kıdemlendikçe (seniority arttıkça) fark ettiğim en önemli şey, teknik başarıların sadece “en yeni kütüphaneyi kullanmak” veya “en karmaşık mimariyi kurmak” ile ölçülmediğidir. Gerçek mühendislik, kısıtlı kaynaklarla (bütçe, donanım, zaman) en optimum çözümü üretebilmektir.

Bir platform mühendisi veya takım lideri olarak, şirketinizin bulut (cloud) faturasını kontrol altında tutmak doğrudan sizin sorumluluğunuzdadır. CFO’nun karşısına çıkıp “Efendim, Kubernetes cluster’ımız çok stabil” demek yerine, “Yaptığımız cardinality optimizasyonu sayesinde observability bütçemizi belirgin biçimde düşürdük ve sorgu hızlarımızı kat kat artırdık” demek, kariyerinizde sizi bir üst lige taşıyacak olan asıl çıktıdır.

Mühendislik kararları her zaman trade-off (ödünleşim) içerir. Cardinality’yi düşürürken detay seviyesinden feragat edersiniz; ancak doğru segmentasyon ve doğru araç seçimiyle (metrik vs. log vs. trace) hem bütçeyi korumak hem de sistemin sağlığını izlemek mümkündür.

Sonraki adım olarak, kendi sisteminizdeki Prometheus veya kullandığınız SaaS aracının arayüzüne girip en yüksek cardinality’ye sahip ilk 3 metriği tespit edin. Göreceksiniz ki, sadece o 3 metriği normalize etmek bile altyapınızda çok ciddi bir rahatlama sağlayacaktır.

Paylaş:

Bu yazı faydalı oldu mu?

Yükleniyor...

Bu yazı nasıldı?

Sıkça Sorulanlar

Bu makale ile ilgili okurların sorduğu yaygın sorular.

Mevcut metriklerimde cardinality patlamasını nasıl tespit ederim ve hangi araçları kullanmalıyım?
Ben ilk kez bir alarmın disk %100 dolmasını gördüğümde Prometheus'un `promtool tsdb analyze` komutunu çalıştırdım; bu, en çok zaman serisi yaratan label kombinasyonlarını listeler. Ardından Grafana’nın Explore sekmesinde aynı sorguyu çalıştırıp "label values" grafiği çizerdim. Eğer Prometheus remote‑write hedefi varsa, VictoriaMetrics’in `vmselect` API’siyle "label cardinality" endpointini sorgulamak da hızlı bir kontrol sağlar. Bu iki adım, yüksek cardinality’nin nereden geldiğini gösterir; ardından bir relabel config ekleyerek gereksiz label’ları filtreleyebilir ya da metric’i yeniden adlandırarak sorunlu kombinasyonları ortadan kaldırabilirsiniz.
Label sayısını azaltmak için relabeling mi yoksa metrikleri tamamen bırakmak mı daha avantajlı?
Benim deneyimimde, relabeling çoğu zaman ilk tercihim olur çünkü sadece problemli label’ları siler, metrik veri bütünlüğünü korur. Örneğin `instance` label’ını `job` ile birleştirerek aynı veri setini iki kez saklamazsınız. Ancak relabeling karmaşık bir regex içeriyorsa, bakım maliyeti artar ve yeni bir etiket eklenince kaçırma riski doğar. Metrik tamamen bırakmak (drop) ise basit ama riskli; bir hata durumunda önemli bir gözlem eksikliği yaşanabilir. Dolayısıyla, düşük maliyetli bir relabel kuralları yazıp, sadece aşırı yüksek cardinality gösteren kombinasyonları drop etmek, dengeyi en iyi korur.
Beklenmedik bir yüksek cardinality alarmı aldığımda hangi adımları izlemeliyim?
İlk olarak alarmın geldiği metric’i `prometheus_tsdb_series` sorgusuyla inceleyip, en çok yeni seri oluşturan label çifti hangisidir bulurum. Ben genellikle `label_values(metric_name, label_name)` komutuyla o label’ın kaç benzersiz değere sahip olduğunu kontrol ederim. Eğer değerler beklenenden çok yüksekse, ilgili kodda hangi değişkenin dinamik olarak label eklediğini bulmak için tracing veya logging eklerim. Sorunlu etiketi geçici olarak `relabel_configs` içinde drop eder, ardından metric’i yeniden deploy edip, bir iki saat içinde trendi izlerim. Çözüm kalıcı olduğunda, kodda label oluşturma mantığını sabit bir enum’a çekerek tekrar etmez.
Cardinality’i düşük tutmak her zaman en iyi uygulama mıdır, yoksa bazı durumlarda yüksek cardinality tercih edilmelidir?
Benim projelerimde, düşük cardinality genellikle disk ve bellek tasarrufu sağlasa da, bazı senaryolarda detaylı debug bilgisi için yüksek cardinality kaçınılmazdır. Örneğin, bir çok‑tenant ortamında her tenant’ın ayrı bir `tenant_id` label’ı taşıması, sorunlu bir tenant’ı hızlıca izole etmenizi sağlar; bu durumda yüksek cardinality kabul edilebilir bir maliyettir. Ancak bu tür durumlarda, veri saklama süresini kısaltmak (örneğin 7 gün tutmak) ve sadece kritik alarm metriklerini uzun vadeli tutmak gibi dengeleyici stratejiler geliştiririm. Yani, cardinality’i tamamen kısıtlamak yerine, iş ihtiyacına göre “high‑cardinality but short‑term” ve “low‑cardinality long‑term” bir model benimserim.
ME

Mustafa Erbay

Sistem Mimarisi · Network Uzmanı · Altyapı, Güvenlik ve Yazılım

2006'dan bu yana sistem mimarisi, network, sunucu altyapıları, büyük yapıların kurulumu, yazılım ve sistem güvenliği ekseninde çalışıyorum. Bu blogda sahada karşılığı olan teknik deneyimlerimi paylaşıyorum.

Kişisel Notlar

Bu notlar sadece sizde saklanır. Tarayıcınızda yerel olarak tutulur.

Hazır 0 karakter

Yorumlar

Sunucu Taraflı AI Moderasyon

Yorumlar sunucuda yapay zeka ile denetlenir ve kalıcı olarak saklanır.

?
0/2000

Sunucu taraflı AI denetim

✉️ Ücretsiz · Spam yok · İstediğin an çık

Yeni yazılardan haberdar olun

Yeni içerikler ve teknik notlar e-postanıza gelsin.

  • 📌
    Haftanın en iyisi Sadece okumaya değer tek yazı
  • 🔧
    Alet çantası Bu hafta kullandığım araçlar
  • 🧠
    Perde arkası Blog'a girmeyen notlar

Spam yapmıyoruz. İstediğiniz zaman ayrılabilirsiniz. · Sadece Umami (self-hosted, Google yok) ile takip.

Okuma İstatistikleriniz

0

Yazı Okundu

0dk

Okuma Süresi

0

Gün Serisi

-

Favori Kategori

İlgili Yazılar