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.
-
Etiketleri Filtreleme ve Düşürme (Relabeling): Prometheus gibi sistemlerde
relabel_configsen 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ğimrequest_uuidörneği bunun somut bir uygulamasıydı. Benzer şekilde, bir mikroservisin her dağıtımında değişengit_commit_hashetiketini düşürmek, metrik serilerinin dağıtımdan bağımsız kalmasını sağlar. Bu,blue-green deploymentstratejileri uygularken kritiktir. -
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
pathetiketini/api/v1/users/123yerine/api/v1/users/*olarak genelleştirebiliriz. Bu genelliklerecording rules(kayıt kuralları) ile yapılır. Örneğin her bir ürünün benzersiz seri numarasını içerenproduct_serial_numberetiketini, sadeceproduct_typeetiketiyle değiştirmek kardinaliteyi büyük ölçüde düşürür. -
Ö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.
- 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_idveyasession_idgibi 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.
-
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,
hostnameyerineservice_nameveenvironmentgibi 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, sadecenetwork_zonevesegment_typegibi daha genel etiketlerle metriklere anlam katmak kardinaliteyi düşük tutar. -
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_idetiketiyle metrik göndermesi beklenmedik bir kardinalite artışına yol açabilir. Bu durumdarelabel_configsilecontainer_idetiketini düşürmek en pratik çözümdür. -
Kardinaliteyi Düzenli Olarak İzleyin: Metrik sisteminizin kendi kardinalitesini izlemesi çok önemlidir. Prometheus’un
tsdb_head_seriesveyaprometheus_tsdb_head_active_seriesgibi 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.
- 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.