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

Cardinality Patlaması Neden Her Zaman Sorun Olur?

Metrik sistemlerindeki cardinality patlaması sorunlarını, depolama, performans ve maliyet etkileriyle kendi deneyimlerimden örneklerle inceliyorum.

100%

Metrik sistemlerinde sıkça karşıma çıkan, bazen farkına bile varmadan tüm altyapıyı yavaşlatan veya maliyetleri fırlatan sinsi bir sorun var: Cardinality patlaması. Bu durum, özellikle izleme ve gözlemlenebilirlik (observability) platformlarında veri depolama ve sorgulama performansını ciddi şekilde etkiliyor. Yüksek kardinaliteyi doğru yönetemediğimde, bir sistem yöneticisi olarak hem ben hem de çalıştığım ekipler için ciddi baş ağrılarına yol açtığını defalarca gördüm.

Kardinalite patlaması, bir metrik setindeki benzersiz etiket (label) kombinasyonlarının aşırı artması anlamına gelir. Örneğin, HTTP isteklerinin durum kodlarını izlerken status_code: 200 gibi etiketler kullanırız. Ancak bu metrikleri her bir kullanıcının ID’si, her bir isteğin benzersiz oturum ID’si veya her bir URL’nin tam yolu ile etiketlemeye başladığımızda, benzersiz kombinasyonların sayısı hızla katlanır. Bu durum, metrik veritabanlarındaki indeks boyutlarını şişirir, sorguları yavaşlatır ve depolama maliyetlerini öngörülemez seviyelere çıkarır.

Kardinalite Nedir ve Neden Önemlidir?

Kardinalite, bir veri kümesindeki benzersiz değerlerin sayısıdır. İzleme sistemleri bağlamında ise, bir metrik adı için mevcut olan benzersiz etiket (label) kombinasyonlarının sayısını ifade eder. Örneğin, http_requests_total diye bir metriğimiz olsun. Eğer bu metriği sadece method (GET, POST) ve status (200, 404, 500) etiketleriyle izlersek, kardinalitesi nispeten düşük olur (örneğin 2 metot * 3 durum = 6 benzersiz kombinasyon).

Ancak, bu metriğe path (her benzersiz URL yolu) veya user_id (her benzersiz kullanıcı ID’si) gibi etiketler eklediğimde, kardinalite aniden patlama noktasına gelir. Bir e-ticaret sitesi düşünün, binlerce farklı ürün sayfası ve milyonlarca kullanıcı var. Bu durumda her bir isteği path ve user_id ile etiketlemek, milyonlarca hatta milyarlarca benzersiz metrik serisi oluşturur. Her bir işlem için benzersiz bir transaction_id etiketi eklemek de aynı sonucu doğurur: metrik sistemi saniyede çok sayıda yeni seri yaratmaya çalışır ve bu, sistem için kısa sürede yönetilemez bir yük haline gelir.

Bu kadar yüksek kardinalite, metriklerinizi depolayan zaman serisi veritabanları (TSDB’ler) için büyük bir yüktür. Her benzersiz seri için ayrı bir depolama alanı ve indeks girişi gerekir. Bu da doğrudan disk alanı, bellek kullanımı ve işlemci gücü gereksinimlerini artırır. Gözden kaçırılması kolay bir detay gibi görünse de, benim deneyimimde, küçük bir etiket eklemesinin kısa sürede disk kullanımını katlayabildiğini gördüm.

Depolama ve Performans Üzerindeki Etkileri

Cardinality patlamasının en belirgin sonuçlarından biri, depolama maliyetlerinin ve sorgu performansının dramatik bir şekilde kötüleşmesidir. Metrik sistemleri, genellikle zaman serisi veritabanları üzerine kuruludur. Bu veritabanları, her benzersiz metrik serisi için bir başlık (header) ve indeks girişi tutar. Milyonlarca benzersiz seri olduğunda, bu başlıklar ve indeksler diskte çok büyük yer kaplar. worker_id ve task_id gibi yüksek kardinaliteli etiketler yanlışlıkla eklendiğinde, Prometheus sunucusunun disk kullanımı birkaç katına çıkar ve bu, yalnızca birkaç günlük veri için bile geçerli olabilir.

Sorgu performansı da bu durumdan doğrudan etkilenir. Bir metrik sorgusu çalıştırdığınızda, veritabanı ilgili tüm zaman serilerini bulmak için indeksleri taramak zorundadır. Eğer milyonlarca seri varsa, bu tarama işlemi çok uzun sürer. Yaşadığım bir olayda, Grafana dashboard’larımdaki bazı panellerin yüklenmesi belirgin şekilde yavaşlamıştı. Root cause analizi yaptığımda, geliştirme ekibinden bir arkadaşın bir servisin endpoint’lerine request_uuid etiketi eklediğini gördüm. Bu request_uuid her istekte değiştiği için anında milyonlarca yeni seriye yol açmıştı.

# Prometheus relabel_configs örneği: request_uuid etiketini düşürme
- source_labels: [__name__]
  regex: 'http_requests_total'
  action: keep
- source_labels: [request_uuid]
  regex: '.*'
  action: drop

Yukarıdaki gibi relabel_configs kullanarak request_uuid etiketini düşürdüğümde, hem disk kullanımı normale döndü hem de dashboard’lar belirgin biçimde hızlı yüklenmeye başladı. Bu, her bir etiketin ne kadar kritik olduğunu gösteren somut bir örnekti. PostgreSQL gibi ilişkisel veritabanlarını metrik depolama için kullanan sistemlerde yüksek kardinalite, B-tree indekslerinin aşırı şişmesine ve WAL bloat sorunlarına neden olabilir. Bu da hem disk I/O’sunu artırır hem de VACUUM işlemlerini yavaşlatarak genel veritabanı performansını düşürür.

İzleme ve Alarm Sistemlerinde Kardinalite Patlaması

Cardinality patlaması sadece depolama ve performans sorunlarıyla kalmaz, aynı zamanda izleme ve alarm sistemlerimizin etkinliğini de baltalar. Aşırı sayıda benzersiz metrik serisi olduğunda, anlamlı alarmlar tanımlamak neredeyse imkansız hale gelir. Hangi seriye alarm yazacağınızı bilemezsiniz. Örneğin, http_requests_total{status="500", path="/api/v1/users/.*", user_id="12345"} gibi aşırı spesifik bir seriye alarm yazmak yerine, daha genel bir http_requests_total{status="500"} serisine alarm yazmak istersiniz. Ancak yüksek kardinalite durumunda, genel metrikler bile o kadar çok alt seriye bölünür ki, bir sorunun gerçek nedenini bulmak için tüm bu serileri tek tek incelemeniz gerekir.

Sık karşılaşılan bir örnek, mikroservislerin her biri için ayrı instance_id ve deployment_id etiketlerinin kullanılmasıdır. Yeni bir dağıtım yapıldığında deployment_id değişir ve eski seriler artık izlenmez. Bu da geçici olarak “sağlıklı” görünen ama aslında sorunlu olan yeni serilerin alarmları tetiklemesini engelleyebilir. deployment_id etiketi kaldırılmadığı sürece her dağıtımda yeni metrik serileri oluşur, eski seriler ise “ölü” hale gelir. Bu durum, alarmların sürekli olarak ayarlanmasını gerektirir ve operasyonel yükü artırır.

Bir diğer problem ise fail2ban gibi sistemlerde karşımıza çıkan log kardinalitesi. journald’nin RateLimitInterval ve RateLimitBurst ayarları, çok fazla log üreten servislerin sistemi boğmasını engellemek içindir. Eğer bir servis, her bir kullanıcının IP adresini içeren benzersiz bir hata mesajı üretmeye başlarsa, journald bile bu logları limitler ve gerçek sorunu görmek zorlaşır. Bu da bir nevi kardinalite patlamasıdır, sadece metrikler yerine loglar üzerinde gerçekleşir. Bir servisin çok sayıda farklı IP’den aynı kimlik doğrulama hatasını üretmesi, journald’nin rate limitine takılarak kritik bir anda sistemin neden aksadığını anlamayı geciktirebilir.

Maliyet ve Kaynak Tüketimi

Cardinality patlamasının en somut ve acı veren sonuçlarından biri, doğrudan maliyetler üzerindeki etkisidir. Özellikle bulut tabanlı metrik hizmetleri kullanıyorsanız, yüksek kardinalite hızla kontrol edilemez hale gelen faturalara yol açabilir. Çoğu bulut sağlayıcı, veri alımı (data ingestion) ve depolama miktarına göre ücretlendirir. Milyonlarca benzersiz metrik serisi, petabaytlarca veri anlamına gelebilir ve bu da astronomik faturalar demek.

Kendi yan ürünümün finansal hesaplayıcılarının backend’inde, Redis’i cache olarak kullanıyorum. Başlangıçta, her bir kullanıcının benzersiz sorgu parametrelerini Redis anahtarlarına dahil etmiştim. Kısa sürede Redis sunucumun belleği doldu ve OOM eviction policy devreye girerek önemli verileri silmeye başladı. Bu, aslında Redis anahtarlarının kardinalitesinin patlamasıydı. user_id ve query_hash gibi etiketleri doğrudan anahtarlara gömmek yerine, daha genelleştirilmiş anahtarlar kullanarak veya belirli bir TTL (Time-To-Live) belirleyerek bu sorunu çözdüm. Redis OOM eviction policy ayarlarını allkeys-lru olarak değiştirmem de, en az kullanılan anahtarların otomatik olarak silinmesini sağlayarak sistemi bir nebze olsun rahatlattı.

# Redis CLI üzerinden OOM eviction policy kontrolü ve ayarı
redis-cli config get maxmemory-policy
redis-cli config set maxmemory-policy allkeys-lru

Bu durum, bare-metal sunucularda da farklı bir şekilde kendini gösterir. Yüksek kardinalite, Prometheus gibi metrik toplama servislerinin daha fazla CPU ve RAM tüketmesine neden olur. Verilerin diskten okunması, işlenmesi ve indekslenmesi için daha fazla işlem gücü gerekir. Yüksek belleğe sahip bir sunucunun kaynaklarının büyük bölümünün yalnızca Prometheus’un metriklerini indekslemek ve sorgulamak için tükenmesi, o sunucunun diğer kritik iş yükleri için kullanılabilecek kaynaklarının israf edilmesi demektir. Konteyner ortamlarında ise cgroup memory.high yumuşak limitleri sürekli tetiklenebilir, çünkü kardinalite patlaması konteynerin beklenen bellek kullanımını aşar. Bu, sistemde sürekli bir bellek baskısına işaret eder ve çoğu zaman daha fazla kaynak ayırmayı zorunlu kılar.

Çözüm Yaklaşımları ve Benim Deneyimlerim

Cardinality patlamasıyla başa çıkmak için birkaç farklı yaklaşım denedim ve her birinin kendine göre trade-off’ları olduğunu gördüm. Temel strateji, gereksiz etiketleri ya toplamadan önce atmak ya da topladıktan sonra düşürmek ve verileri daha yüksek seviyelerde aggrege etmektir.

  1. Etiketleri Filtreleme ve Düşürme (Relabeling): Prometheus gibi sistemlerde relabel_configs en güçlü araçlardan biridir. Kaynak sistemden gelen metrikleri hedef sisteme yazmadan önce etiketleri yeniden düzenleyebilir, düşürebilir veya değiştirebilirsiniz. Yukarıda verdiğim request_uuid örneği bunun somut bir uygulamasıydı. Benzer şekilde, bir mikroservisin her dağıtımında değişen git_commit_hash etiketini düşürmek, metrik serilerinin dağıtımdan bağımsız kalmasını sağlar. Bu, blue-green deployment stratejileri uygularken kritiktir.

  2. Veri Agregasyonu: Bazı metrikleri daha düşük bir çözünürlükte depolamak veya daha az detaylı etiketlerle toplamak, kardinaliteyi önemli ölçüde azaltır. Örneğin, her 10 saniyede bir gelen CPU kullanım metriklerini her 1 dakikada bir ortalama alarak depolayabiliriz. Ya da path etiketini /api/v1/users/123 yerine /api/v1/users/* olarak genelleştirebiliriz. Bu genellikle recording rules (kayıt kuralları) ile yapılır. Örneğin her bir ürünün benzersiz seri numarasını içeren product_serial_number etiketini, sadece product_type etiketiyle değiştirmek kardinaliteyi büyük ölçüde düşürür.

  3. Örnekleme (Sampling): Özellikle çok yüksek hacimli olaylarda, tüm verileri toplamak yerine örnekleme yapmak bir seçenektir. Her 100 istekten sadece birinin metriklerini toplamak gibi. Ancak bu, verilerde detay kaybına yol açar ve bazı nadir durumları gözden kaçırma riskini taşır. Bu yüzden kritik sistemlerde dikkatli kullanılmalıdır. Örneğin yüksek hacimli bir olay akışında, her olayı tek tek loglamak yerine yalnızca belirli tiplerdeki ya da belirli bir eşiği aşan olayların metriklerini toplamak, detayı korurken yükü ciddi ölçüde azaltır.

  1. Doğru Metrik Tasarımı: En başından itibaren, hangi etiketlerin gerçekten gerekli olduğunu ve hangilerinin yüksek kardinaliteye yol açacağını düşünmek kritik. Bir metriği tasarlarken kendime hep şu soruyu sorarım: “Bu etiketin her bir benzersiz değeri, bir sorunun nedenini bulmamda bana gerçekten yardımcı olacak mı, yoksa sadece gürültü mü?” Çoğu zaman, request_id veya session_id gibi her zaman benzersiz olan değerler, metrik etiketleri olarak kullanılmamalıdır. Bunlar daha çok trace veya log sistemlerinde anlamlıdır.

Pratik Uygulamalar ve Tavsiyeler

Cardinality patlamasını önlemek ve yönetmek için edindiğim pratik tecrübelerimi ve tavsiyelerimi paylaşmak isterim. Bunlar, hem sıfırdan sistem kurarken hem de mevcut bir sistemdeki sorunları giderirken işime yarayan yaklaşımlar oldu.

  1. Etiketleri Kısıtlayın ve Standardize Edin: Bir metrik için kullanacağınız etiket sayısını ve her etiketin alabileceği benzersiz değer aralığını baştan belirleyin. Örneğin, hostname yerine service_name ve environment gibi daha genel etiketler kullanmak, farklı makinelerde çalışan aynı servisi tek bir mantıksal varlık olarak izlemenizi sağlar. Ağ segmentasyonunda da her bir VLAN için ayrı bir etiket kullanmak yerine, sadece network_zone ve segment_type gibi daha genel etiketlerle metriklere anlam katmak kardinaliteyi düşük tutar.

  2. Otomatik Keşif Sistemlerini Dikkatli Yapılandırın: Prometheus gibi sistemler, servis keşfi (service discovery) sayesinde otomatik olarak hedefleri bulup metrik toplamaya başlar. Ancak bu, yeni eklenen her servisin veya konteynerin yüksek kardinaliteli etiketlerle gelmeyeceği anlamına gelmez. Örneğin konteyner tabanlı bir dağıtımda, her yeni konteynerin otomatik olarak container_id etiketiyle metrik göndermesi beklenmedik bir kardinalite artışına yol açabilir. Bu durumda relabel_configs ile container_id etiketini düşürmek en pratik çözümdür.

  3. Kardinaliteyi Düzenli Olarak İzleyin: Metrik sisteminizin kendi kardinalitesini izlemesi çok önemlidir. Prometheus’un tsdb_head_series veya prometheus_tsdb_head_active_series gibi dahili metrikleri, aktif seri sayısını takip etmenizi sağlar. Bu metriklerde ani artışlar gördüğünüzde, bir kardinalite patlamasının başladığını anlayabilirsiniz. Ben bu metrikler için bir alarm tanımlamayı öneriyorum: aktif seri sayısı belirlediğiniz bir eşiği aştığında otomatik bildirim almak, sorunu erken yakalamanın en pratik yoludur.

# Prometheus'ta aktif metrik serisi sayısını gösteren sorgu
sum(prometheus_tsdb_head_active_series)

Bu sorgu, anlık aktif metrik serisi sayısını verir ve bu sayıyı belirli bir eşiğin üzerinde tutmak genellikle iyi bir uygulamadır. Örneğin, bu değer bir anda belirgin biçimde yükseldiğinde, hemen incelemeye alıyorum.

  1. SLO’ları (Service Level Objectives) ve Hata Bütçelerini Kullanın: SLO’lar, hangi metriklerin gerçekten önemli olduğunu ve ne kadar detayda izlenmesi gerektiğini belirlememde bana yol gösteriyor. Eğer bir metrik, bir SLO’ya doğrudan katkıda bulunmuyorsa veya bir hata bütçesini yönetmek için gerekli değilse, yüksek kardinaliteli etiketlerle izlemenin bir anlamı yoktur. Örneğin bir üretim hattının verimlilik SLO’sunu takip ederken, her bir makinenin benzersiz seri numarası yerine yalnızca makine tipini ve üretim bandını içeren etiketleri kullanmak çoğu zaman yeterlidir.

Sonuç

Cardinality patlaması, göz ardı edildiğinde sistemlerinizi yavaşlatan, maliyetlerinizi artıran ve izleme yeteneğinizi zayıflatan sinsi bir problem. Bu konuya yaklaşımım her zaman proaktif olmuştur; yani sorunu ortaya çıkmadan önce engellemek. Metriklerinizi tasarlarken, hangi etiketlerin gerçekten operasyonel anlam taşıdığını ve hangilerinin sadece gürültü yarattığını iyi düşünmeliyiz.

Deneyimlerim bana gösterdi ki, doğru relabel_configs kuralları, akıllı agregasyon stratejileri ve metrik kardinalitesini sürekli izlemek, bu sorunla başa çıkmanın anahtarıdır. Yüksek kardinaliteye sahip metrikler, çoğu zaman bir sorunu çözmekten çok, yeni sorunlar yaratır. Bu yüzden, request_id gibi benzersiz tanımlayıcıları loglarda ve tracing sistemlerinde tutmaya devam ederken, metrik sistemlerimizi daha genelleştirilmiş ve anlamlı etiketlerle beslemeliyiz. Böylece hem sistemlerimiz daha kararlı çalışır hem de gerçekten önemli olan verilere odaklanabiliriz. Bir sonraki yazıda, bu yaklaşımları bir systemd unit kullanarak nasıl otomatize ettiğimi ve cgroup limitlerini nasıl yönettiğimi anlatacağım.

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.

Cardinality patlamasını önlemek için metrik etiketlerini nasıl tasarlamaya başlıyorum?
Ben önce metriği tanımlamadan, iş ihtiyacını ve sorgu senaryolarını netleştiririm. Ardından, her bir label'ın gerçekten sorgulanması gerektiğini sorgularım; örneğin `user_id` ya da tam `url_path` gibi dinamik değerleri doğrudan etiketlemek yerine, bunları bir bucket ya da hashed değerle temsil ederim. Etiket sayısını ikiye katlamaktan kaçınmak için ortak bir `service` ve `environment` gibi sabit etiketlerle başlar, ardından sadece düşük kardinaliteye sahip `method` ve `status` gibi değerleri eklerim. Bu yaklaşım, indeks büyüklüğünü kontrol altında tutar ve depolama maliyetini belirgin biçimde azaltır.
Prometheus ve OpenTelemetry arasında cardinality yönetiminde hangisini tercih etmeliyim?
Ben her iki aracı da kullandığım projelerde birleştirerek çalışıyorum. Prometheus, sorgu dili ve yerel retention politikalarıyla kardinaliteyi izlemeyi kolaylaştırıyor; `promtool` ve `metrics-watcher` gibi eklentilerle anormallikleri anında görebiliyorum. OpenTelemetry ise toplama katmanında daha fazla kontrol sunar; label sanitization ve attribute limits gibi yapılandırmalar sayesinde veri girişini kaynakta sınırlayabiliyorum. Eğer mevcut altyapınız Prometheus tabanlıysa, öncelikle Prometheus'un limitlerini ayarlayıp ardından OpenTelemetry collector'ı ile etiketleri filtrelemek en pratik çözüm olur.
Kardinalite patlaması tespit ettiğimde sistemde ne tür adımlar atıyorum ve hatayı nasıl geri alıyorum?
İlk olarak, Grafana’da anormal artış gösteren metrikleri `increase` ve `count` fonksiyonlarıyla izlerim, ardından Prometheus UI üzerinden `label_values` sorgusuyla en yüksek kardinaliteye sahip label’ları tespit ederim. Sorunlu label’ı bulduğumda, ilgili uygulama kodunda etiketleme mantığını devre dışı bırakıp, bir `feature flag` ile geçici bir çözüm uygularım. Veri kaybını önlemek için, eski yüksek kardinalite serilerini bir bucket’a taşıyarak uzun vadeli saklamayı durdururum. Son adımda, yeni etiket stratejisiyle yeniden toplama başlatır ve bir hafta boyunca trendleri izleyerek sorunun tamamen çözüldüğünden emin olurum.
Düşük cardinality için önerilen en iyi uygulama 'label cardinality limit' mi, yoksa 'label pruning' mu? Hangisi daha etkili?
Ben iki yöntemi de bir arada kullanmayı tercih ediyorum; sadece birine bağlı kalmak genellikle eksik kalır. `label cardinality limit` ile sistem seviyesinde bir üst sınır koymak, ani patlamaları önler ve alarm mekanizması sağlar. Ancak, bu limit aşılırsa hangi label’ın soruna yol açtığını göstermez. Bu yüzden `label pruning` ile düşük faydalı, yüksek kardinaliteye sahip etiketleri periyodik olarak temizliyorum; örneğin `user_id` gibi tek seferlik değerleri toplama aşamasında atıyorum. Bu kombinasyon, hem önleyici hem de düzeltici bir yaklaşım sunar ve uzun vadeli maliyetleri kayda değer ölçüde düşürür.
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