← Kursa Dön
📄 Text · 25 min

Scaling Stratejileri — Hot-Warm-Cold Mimari

Giriş — Horizontal/Vertical Scaling ve Hot-Warm-Cold Architecture

Bir restoranın büyüme hikayesi düşün. Başta küçük bir mekan: 10 masa, 1 aşçı, 1 garson. İş büyüyünce iki seçenek var: Ya daha büyük bir mutfak ve daha büyük masa kur (vertical scaling — aynı restoranı büyüt), ya da yan tarafa ikinci bir şube aç (horizontal scaling — yeni restoran ekle). Bir de akıllı bir strateji var: Sabah kahvaltı menüsü ucuz malzemeyle, öğlen iş menüsü orta malzemeyle, akşam fine-dining pahalı malzemeyle çalışsın (hot-warm-cold — kaynak kullanımını zamana göre optimize et).

Elasticsearch'te scaling, cluster'ınız büyüdükçe performansı ve kapasiteyi artırma stratejisidir. Doğru scaling kararı, gereksiz harcamayı önler ve sistemi geleceğe hazırlar.


1. Vertical Scaling — Daha Güçlü Donanım

Ne Zaman?

  • Mevcut node'ların kaynakları (CPU, RAM, disk) yetersiz

  • Cluster küçük (1-3 node) ve node eklemek pratik değil

  • Geçici çözüm gerekiyor

Nasıl?

# ÖNCE: 16GB RAM, 4 core, 500GB disk
# SONRA: 64GB RAM, 16 core, 2TB SSD

# elasticsearch.yml — Heap artışı
# ⚠️ 31GB'ı geçme (compressed oops)
# jvm.options
-Xms30g
-Xmx30g

Vertical Scaling Limitleri

KaynakPratik LimitNeden
RAM (Heap)31GBCompressed oops sınırı
CPU32-64 coreDiminishing returns, Lucene thread model
Disk~10TB/nodeSingle disk failure risk, recovery süresi

💡 İpucu: Vertical scaling'in bir tavanı var. 64 core + 256GB RAM'li tek sunucu, 8x (16 core + 64GB RAM) sunucu kadar verimli değil. Elasticsearch dağıtık bir sistem — horizontal scaling doğal yolu.


2. Horizontal Scaling — Daha Fazla Node

Ne Zaman?

  • Veri hacmi mevcut node kapasitesini aşıyor

  • Arama latency artıyor (shard/node başına iş yükü çok)

  • Yazma throughput'u yetersiz

  • High availability (HA) gereksinimi

Node Ekleme

# elasticsearch.yml — Yeni data node
node.name: data-new-1
node.roles: [data_hot]
cluster.name: production-cluster
discovery.seed_hosts: ["master-1:9300", "master-2:9300", "master-3:9300"]

Yeni node cluster'a katılınca, master otomatik olarak shard'ları rebalance eder.

Rebalance İzleme

# Yeni node ekledikten sonra shard hareketini izle
GET _cat/shards?v&h=index,shard,prirep,state,node&s=node

# Relocating shard'lar
GET _cat/recovery?v&active_only

# Allocation durumu
GET _cat/allocation?v

Scaling Hesaplaması

Kaç node gerektiğini hesaplamak için:

# Veri hesabı
Toplam veri = 500GB (primary)
Replica = 1 → Toplam = 1TB
Hedef shard boyutu = 25GB → 40 shard (20 primary + 20 replica)

# Node hesabı
Disk kapasitesi/node = 2TB
Kullanılabilir disk = %80 = 1.6TB
Node sayısı (disk) = 1TB / 1.6TB = 1 (ama HA için min 3)

# RAM hesabı (shard başına ~50MB heap + data cache)
40 shard × 50MB = 2GB heap overhead
Index boyutu / 50 = ~10GB heap (search/aggregation buffer)
Toplam heap ihtiyacı ≈ 12GB → 30GB heap yeterli per node

# Final: 3 data node (2TB SSD, 64GB RAM, 16 core each)

Shard Rebalancing Hızlandırma

// Yeni node ekledikten sonra rebalance hızını artır
PUT _cluster/settings
{
  "transient": {
    "cluster.routing.allocation.node_concurrent_incoming_recoveries": 4,
    "cluster.routing.allocation.node_concurrent_outgoing_recoveries": 4,
    "indices.recovery.max_bytes_per_sec": "200mb"
  }
}

3. Hot-Warm-Cold Architecture

Motivasyon

Tüm veriniz eşit değil:

  • Bugünün logları: Sürekli yazılıyor, sürekli okunuyor → hızlı donanım gerekli

  • Geçen haftanın logları: Bazen okunuyor, yazılmıyor → orta donanım yeter

  • Geçen ayın logları: Nadiren okunuyor → ucuz donanım yeter

  • Geçen yılın logları: Neredeyse hiç okunmuyor → en ucuz depolama

Hepsini NVMe SSD üzerinde tutmak para israfı. Hepsini HDD'de tutmak performans felaketi. Çözüm: katmanlı mimari.

Tier Tanımları

TierKullanımDonanımMaliyet
HotAktif okuma/yazmaNVMe SSD, yüksek CPU$$$
WarmOkuma, yazma yokSATA SSD, orta CPU$$
ColdNadiren okumaHDD, düşük CPU$
FrozenÇok nadirenS3/shared storage¢

Node Konfigürasyonu

# Hot node — elasticsearch.yml
node.name: hot-1
node.roles: [data_hot, data_content, ingest]
path.data: /mnt/nvme/elasticsearch

# Warm node — elasticsearch.yml
node.name: warm-1
node.roles: [data_warm]
path.data: /mnt/ssd/elasticsearch

# Cold node — elasticsearch.yml
node.name: cold-1
node.roles: [data_cold]
path.data: /mnt/hdd/elasticsearch

# Frozen node — elasticsearch.yml
node.name: frozen-1
node.roles: [data_frozen]
# Frozen tier genelde searchable snapshots kullanır

ILM ile Tier Geçişi

PUT _ilm/policy/tiered-logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_primary_shard_size": "30gb",
            "max_age": "1d"
          },
          "set_priority": {
            "priority": 100
          }
        }
      },
      "warm": {
        "min_age": "3d",
        "actions": {
          "shrink": {
            "number_of_shards": 1
          },
          "forcemerge": {
            "max_num_segments": 1
          },
          "allocate": {
            "number_of_replicas": 1
          },
          "set_priority": {
            "priority": 50
          }
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "set_priority": {
            "priority": 0
          },
          "allocate": {
            "number_of_replicas": 0
          }
        }
      },
      "frozen": {
        "min_age": "90d",
        "actions": {
          "searchable_snapshot": {
            "snapshot_repository": "my-s3-repo",
            "force_merge_index": true
          }
        }
      },
      "delete": {
        "min_age": "365d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

Data Tier Preference

Elasticsearch 7.10+ ile _tier_preference ayarı otomatik çalışır:

// Index'in tier tercihini kontrol et
GET logs-000001/_settings?flat_settings=true&filter_path=*.settings.index.routing.allocation.include._tier_preference

// Manuel tier taşıma
PUT old-logs/_settings
{
  "index.routing.allocation.include._tier_preference": "data_cold,data_warm,data_hot"
}

_tier_preference sırası önemli: İlk tercihi dener, yoksa ikinciye fallback yapar.

Tier Geçişini İzleme

# Hangi index hangi tier'da?
GET _cat/indices?v&h=index,health,pri,rep,store.size,segments.count&s=store.size:desc

# Node'ların tier dağılımı
GET _cat/nodes?v&h=name,role,disk.used,disk.avail

# ILM durumu
GET logs-*/_ilm/explain?filter_path=indices.*.phase,indices.*.action

4. Maliyet Optimizasyonu

Tier Bazlı Maliyet Analizi

Senaryo: Günde 100GB log, 1 yıl tutma.

Seçenek A: Tek Tier (Tümü Hot)

365 gün × 100GB = 36.5TB (primary)
+ Replica = 73TB toplam
NVMe SSD @ $0.20/GB/ay = $14,600/ay

Seçenek B: Hot-Warm-Cold

Hot (7 gün): 700GB × 2 = 1.4TB NVMe   @ $0.20 = $280/ay
Warm (23 gün): 2.3TB × 2 = 4.6TB SSD  @ $0.10 = $460/ay
Cold (335 gün): 33.5TB × 1 = 33.5TB HDD @ $0.03 = $1,005/ay
Toplam: $1,745/ay

Tasarruf: %88 ($14,600 → $1,745)

Replica Stratejisi

TierReplica SayısıNeden
Hot1-2HA + arama performansı
Warm1HA (arama nadir)
Cold0-1Maliyet (snapshot'tan restore edilebilir)
Frozen0Snapshot'ta zaten var
// Cold tier'da replica kaldır (ILM ile otomatik)
"cold": {
  "actions": {
    "allocate": {
      "number_of_replicas": 0
    }
  }
}

5. Searchable Snapshots — Frozen Tier

Elasticsearch 7.12+ ile gelen en güçlü maliyet optimizasyonu: veriyi S3/GCS/Azure Blob üzerinde tutup, gerektiğinde local cache ile arama yapmak.

Repository Oluşturma

PUT _snapshot/my-s3-repo
{
  "type": "s3",
  "settings": {
    "bucket": "my-es-snapshots",
    "region": "eu-central-1",
    "base_path": "es-frozen"
  }
}

Searchable Snapshot Mount

// Frozen mount (minimal local cache)
POST _snapshot/my-s3-repo/snapshot-2024.01/_mount?storage=shared_cache
{
  "index": "logs-2024.01",
  "renamed_index": "frozen-logs-2024.01"
}

// Full mount (tamamen local cache — warm tier gibi)
POST _snapshot/my-s3-repo/snapshot-2024.01/_mount?storage=full_copy
{
  "index": "logs-2024.01"
}

Frozen Tier Node Konfigürasyonu

# elasticsearch.yml — Frozen node
node.roles: [data_frozen]

# Shared cache ayarları
xpack.searchable.snapshot.shared_cache.size: 500gb
xpack.searchable.snapshot.shared_cache.size.max_headroom: 10gb

6. Auto-Scaling (Elastic Cloud / Kubernetes)

Elasticsearch Autoscaling API

// Autoscaling policy oluştur
PUT _autoscaling/policy/hot-tier-policy
{
  "roles": ["data_hot"],
  "deciders": {
    "reactive_storage": {}
  }
}

// Kapasiteyi oku
GET _autoscaling/capacity

// Yanıt:
{
  "policies": {
    "hot-tier-policy": {
      "required_capacity": {
        "total": {
          "storage": 5368709120,
          "memory": 34359738368
        },
        "node": {
          "storage": 1073741824,
          "memory": 8589934592
        }
      },
      "current_capacity": {
        "total": {
          "storage": 3221225472,
          "memory": 25769803776
        },
        "node": {
          "storage": 1073741824,
          "memory": 8589934592
        }
      },
      "current_nodes": [
        { "name": "hot-1" },
        { "name": "hot-2" },
        { "name": "hot-3" }
      ]
    }
  }
}

Kubernetes ile Auto-Scaling

# ECK (Elastic Cloud on Kubernetes) örneği
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: production-cluster
spec:
  version: 8.12.0
  nodeSets:
  - name: hot
    count: 3
    config:
      node.roles: [data_hot, data_content, ingest]
    podTemplate:
      spec:
        containers:
        - name: elasticsearch
          resources:
            requests:
              memory: 64Gi
              cpu: 16
            limits:
              memory: 64Gi
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 2Ti
        storageClassName: fast-ssd
  - name: warm
    count: 2
    config:
      node.roles: [data_warm]
    podTemplate:
      spec:
        containers:
        - name: elasticsearch
          resources:
            requests:
              memory: 32Gi
              cpu: 8
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 4Ti
        storageClassName: standard-ssd
  - name: cold
    count: 2
    config:
      node.roles: [data_cold]
    podTemplate:
      spec:
        containers:
        - name: elasticsearch
          resources:
            requests:
              memory: 16Gi
              cpu: 4
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 8Ti
        storageClassName: standard-hdd
  - name: master
    count: 3
    config:
      node.roles: [master]
    podTemplate:
      spec:
        containers:
        - name: elasticsearch
          resources:
            requests:
              memory: 8Gi
              cpu: 2

7. Rolling Upgrade / Rolling Restart

Node'u Güvenli Şekilde Yeniden Başlatma

// 1. Shard allocation'ı durdur
PUT _cluster/settings
{
  "transient": {
    "cluster.routing.allocation.enable": "primaries"
  }
}

// 2. Flush yap (translog temizle)
POST _flush/synced

// 3. Node'u restart et (OS level)
// systemctl restart elasticsearch

// 4. Node'un cluster'a katılmasını bekle
GET _cat/nodes?v

// 5. Cluster yellow veya green olana kadar bekle
GET _cluster/health?wait_for_status=yellow&timeout=60s

// 6. Allocation'ı geri aç
PUT _cluster/settings
{
  "transient": {
    "cluster.routing.allocation.enable": "all"
  }
}

// 7. Green olana kadar bekle
GET _cluster/health?wait_for_status=green&timeout=5m

Rolling Upgrade Script (Bash)

#!/bin/bash
NODES=("data-1" "data-2" "data-3" "data-4" "data-5")
ES_URL="https://localhost:9200"
CREDS="-u elastic:changeme"

for node in "${NODES[@]}"; do
    echo "=== Upgrading $node ==="

    # 1. Allocation durdur
    curl -s -X PUT "$ES_URL/_cluster/settings" $CREDS \
        -H "Content-Type: application/json" \
        -d '{"transient":{"cluster.routing.allocation.enable":"primaries"}}'

    # 2. Synced flush
    curl -s -X POST "$ES_URL/_flush/synced" $CREDS

    # 3. Node restart (SSH ile)
    ssh "$node" "sudo systemctl stop elasticsearch"
    ssh "$node" "sudo yum update elasticsearch -y"  # veya apt
    ssh "$node" "sudo systemctl start elasticsearch"

    # 4. Node'un katılmasını bekle
    echo "Waiting for $node to join..."
    while true; do
        NODES_COUNT=$(curl -s "$ES_URL/_cat/nodes" $CREDS | wc -l)
        if [ "$NODES_COUNT" -ge "${#NODES[@]}" ]; then break; fi
        sleep 5
    done

    # 5. Allocation aç
    curl -s -X PUT "$ES_URL/_cluster/settings" $CREDS \
        -H "Content-Type: application/json" \
        -d '{"transient":{"cluster.routing.allocation.enable":"all"}}'

    # 6. Green olana kadar bekle
    echo "Waiting for green..."
    while true; do
        STATUS=$(curl -s "$ES_URL/_cluster/health" $CREDS | jq -r '.status')
        if [ "$STATUS" = "green" ]; then break; fi
        sleep 10
    done

    echo "=== $node upgraded successfully ==="
done
echo "All nodes upgraded!"

8. Capacity Planning

Temel Formül

Storage İhtiyacı = Günlük Veri × Tutma Süresi × (1 + Replica) × 1.15 (overhead)

Örnek:
50GB/gün × 90 gün × 2 (replica=1) × 1.15 = 10,350GB ≈ 10.4TB

RAM Hesabı

Heap = min(31GB, Toplam RAM / 2)
OS Page Cache = Toplam RAM - Heap

Kural: Index boyutunun en az %50'si kadar OS page cache olmalı
Örnek: 2TB data/node → min 1TB RAM (OS cache) → Toplam 64GB RAM (31 heap + 33 cache)

Node Sayısı

Min Node = max(Disk İhtiyacı / Disk Per Node, RAM İhtiyacı / RAM Per Node, 3)

Örnek:
Disk: 10.4TB / 2TB per node = 6 node (disk)
RAM: Yeterli (64GB/node)
HA: min 3 data + 3 master = 6
Sonuç: 6 data node + 3 master node

Java ile Capacity Planner

public class CapacityPlanner {

    public record ClusterPlan(
        int hotNodes, int warmNodes, int coldNodes,
        int masterNodes,
        String hotSpec, String warmSpec, String coldSpec
    ) {}

    public ClusterPlan planCluster(
            double dailyIngestGB,
            int retentionDays,
            int hotDays, int warmDays) {

        // Veri hesabı
        double hotDataGB = dailyIngestGB * hotDays * 2; // replica=1
        double warmDataGB = dailyIngestGB * warmDays * 2;
        int coldDays = retentionDays - hotDays - warmDays;
        double coldDataGB = dailyIngestGB * coldDays * 1; // replica=0

        // Node hesabı (overhead %15)
        double hotDiskPerNode = 2000; // 2TB NVMe
        double warmDiskPerNode = 4000; // 4TB SSD
        double coldDiskPerNode = 8000; // 8TB HDD

        int hotNodes = Math.max(3, (int) Math.ceil(
            hotDataGB * 1.15 / hotDiskPerNode));
        int warmNodes = Math.max(2, (int) Math.ceil(
            warmDataGB * 1.15 / warmDiskPerNode));
        int coldNodes = Math.max(2, (int) Math.ceil(
            coldDataGB * 1.15 / coldDiskPerNode));

        return new ClusterPlan(
            hotNodes, warmNodes, coldNodes, 3,
            "64GB RAM, 16 core, 2TB NVMe",
            "64GB RAM, 8 core, 4TB SSD",
            "32GB RAM, 4 core, 8TB HDD"
        );
    }
}

// Kullanım:
// Günde 100GB, 1 yıl tutma, 7 gün hot, 23 gün warm
var planner = new CapacityPlanner();
var plan = planner.planCluster(100, 365, 7, 23);

System.out.println("Hot nodes: " + plan.hotNodes() + " (" + plan.hotSpec() + ")");
System.out.println("Warm nodes: " + plan.warmNodes() + " (" + plan.warmSpec() + ")");
System.out.println("Cold nodes: " + plan.coldNodes() + " (" + plan.coldSpec() + ")");
System.out.println("Master nodes: " + plan.masterNodes());

9. Best Practices

✅ Yap

KonuÖneri
Horizontal > VerticalElasticsearch dağıtık — node ekle
Hot-warm-coldMaliyet optimizasyonu için tier kullan
ILMTier geçişini otomatize et
Searchable snapshotsFrozen tier ile maliyet %90 düşer
Capacity planningBüyümeyi 6 ay önceden planla
Rolling restartAllocation disable → restart → enable
Over-provision %20Spike'lara hazırlıklı ol

❌ Yapma

KonuNeden
Tek dev nodeSPOF, scaling imkansız
Tüm veriyi hot'ta tutPara israfı
Cold tier'da replica=1Gereksiz maliyet (snapshot'tan restore edebilirsin)
Allocation disable unutmaRestart sonrası açmazsan shard atanmaz
Under-provisionDisk dolunca index read-only olur

10. Yaygın Hatalar ve Çözümleri

Hata 1: "Node Eklendi Ama Shard Taşınmıyor"

// Kontrol 1: Allocation aktif mi?
GET _cluster/settings?flat_settings=true&filter_path=*.cluster.routing.allocation.enable

// Kontrol 2: Node doğru role sahip mi?
GET _cat/nodes?v&h=name,role

// Kontrol 3: Index'in allocation filter'ı var mı?
GET products/_settings?flat_settings=true&filter_path=*.settings.index.routing.allocation*

// Çözüm: Rebalance tetikle
POST _cluster/reroute?retry_failed=true

Hata 2: "Warm Tier'a Geçiş Çok Yavaş"

// Recovery hızını artır
PUT _cluster/settings
{
  "transient": {
    "indices.recovery.max_bytes_per_sec": "200mb",
    "cluster.routing.allocation.node_concurrent_incoming_recoveries": 4
  }
}

Hata 3: "Rolling Restart Sırasında Veri Kaybı"

// Sorun: Allocation disable etmeden restart yapıldı
// Shard'lar hemen redistribute edildi, node geri geldiğinde tekrar taşındı

// Doğru sıra:
1. cluster.routing.allocation.enable: "primaries"
2. POST _flush/synced
3. Node restart
4. Node katılsın, health kontrolü
5. cluster.routing.allocation.enable: "all"
6. Green olmasını bekle

Özet

  1. Horizontal scaling Elasticsearch'ün doğal büyüme yolu — node eklemek, tek node'u büyütmekten neredeyse her zaman daha etkili.

  2. Hot-warm-cold architecture maliyet optimizasyonunun anahtarı — aynı veri yıl boyunca NVMe SSD'de durmamalı, tier'lar arası otomatik geçiş yapılmalı.

  3. ILM + Data Tiers birlikte kullanılarak veri yaşam döngüsü tamamen otomatize edilir — hot'ta yazılır, warm'a shrink edilir, cold'a taşınır, frozen'da arşivlenir.

  4. Searchable snapshots ile frozen tier'da veri S3 gibi ucuz storage'da tutulup gerektiğinde aranabilir — maliyet %90'a kadar düşer.

  5. Rolling restart sırasında allocation disable → flush → restart → enable sırası izlenmezse gereksiz veri taşıma saatler sürer.

  6. Capacity planning altın formül: Günlük veri × tutma süresi × replica × overhead. 6 ay ilerisini planlayın, %20 over-provision yapın.