← назад к разделу

Эксплуатация ES в production — отдельная дисциплина. Эта статья — то, что senior backend в UCP-стеке должен знать про operations, чтобы не поднимать на оперативных рисках инцидент в первый же месяц жизни кластера.

Index Lifecycle Management (ILM)

Большая часть боли в ES — про рост индекса со временем. Если льёте логи, события, метрики, заказы — индекс растёт без границ. ILM позволяет описать политику «как этот индекс должен жить».

Четыре фазы ILM:

[HOT]     ─── активная запись + чтение, на быстрых нодах (NVMe + много RAM)
   │
   │  rollover: размер 50 GB или возраст 7 дней
   ▼
[WARM]    ─── только чтение, на средних нодах (SSD)
   │
   │  через 30 дней
   ▼
[COLD]    ─── редкое чтение, на дешёвых нодах (HDD, меньше реплик)
   │
   │  через 90 дней
   ▼
[FROZEN]  ─── searchable snapshot на S3, читается дольше (10-100× медленнее)
   │
   │  через 365 дней
   ▼
[DELETE]  ─── удаление

Пример политики для логов:

PUT /_ilm/policy/logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": { "max_size": "50gb", "max_age": "7d" },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "forcemerge": { "max_num_segments": 1 },
          "shrink": { "number_of_shards": 1 },
          "allocate": { "include": { "data_tier": "data_warm" } }
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "allocate": { "number_of_replicas": 0, "include": { "data_tier": "data_cold" } }
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": { "delete": {} }
      }
    }
  }
}

ILM применяется к rollover-индексам через alias + template:

PUT /_index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "logs-policy",
      "index.lifecycle.rollover_alias": "logs"
    }
  }
}

PUT /logs-000001
{
  "aliases": {
    "logs": { "is_write_index": true }
  }
}

Приложение пишет в alias logs, ES сам создаёт logs-000002, logs-000003 при достижении 50 GB или 7 дней. Старые индексы автоматически переходят в warm/cold/delete по политике.

Для time-series (логи, метрики, события) ILM обязателен — иначе индекс утопит кластер.

Snapshots — резервное копирование

ES не имеет встроенного online-бэкапа в стиле pg_dump. Вместо этого — snapshot repository (S3, GCS, Azure Blob, NFS) и _snapshot API.

Регистрация репозитория

PUT /_snapshot/s3-backup
{
  "type": "s3",
  "settings": {
    "bucket": "marketplace-es-backups",
    "region": "eu-west-1",
    "compress": true,
    "base_path": "es-cluster-1"
  }
}

S3-плагин включён в дистрибутив ES с 7.12+. Нужно настроить IAM-роль на узлах ES для доступа к bucket.

Создание snapshot

PUT /_snapshot/s3-backup/snapshot-2026-05-18?wait_for_completion=false
{
  "indices": "products,orders,logs-*",
  "include_global_state": false
}

Snapshot инкрементальный: ES сохраняет только новые сегменты (immutable Lucene-файлы), которых ещё нет в репозитории. Первый snapshot полный, следующие быстрые.

Restore

POST /_snapshot/s3-backup/snapshot-2026-05-18/_restore
{
  "indices": "products",
  "rename_pattern": "products",
  "rename_replacement": "products-restored",
  "include_global_state": false
}

rename_pattern нужен, потому что нельзя восстановить snapshot поверх существующего индекса. Восстанавливаем в новое имя, потом alias-переключение.

Snapshot Lifecycle Management (SLM)

Автоматизация:

PUT /_slm/policy/daily-snapshots
{
  "schedule": "0 30 1 * * ?",
  "name": "<daily-snap-{now/d}>",
  "repository": "s3-backup",
  "config": {
    "indices": ["products", "orders"],
    "include_global_state": false
  },
  "retention": {
    "expire_after": "30d",
    "min_count": 5,
    "max_count": 50
  }
}

Snapshot каждый день в 01:30, хранятся 30 дней / минимум 5 / максимум 50.

Hot / Warm / Cold / Frozen

Для эффективности на больших кластерах — многоуровневое хранилище:

TierHardwareЧто хранить
HotNVMe, много RAM, high CPUActive writes, последние 1-7 дней, частые reads
WarmSSD, средний RAMRead-only, 7-30 дней
ColdHDD, мало RAM, 0 replicasРедкие reads, 30-90 дней
FrozenSearchable snapshots на S3, кэш на diskАудит, compliance, очень редкие reads (10-100× медленнее)

Назначение узла:

# elasticsearch.yml
node.roles: [data_hot, data_content]
# или
node.roles: [data_warm]
# или
node.roles: [data_cold]
# или
node.roles: [data_frozen]

ILM-политика автоматически перемещает индексы между tiers.

В небольших кластерах (3-5 узлов) — всё на одном tier, не overengineerit'е. Hot/warm/cold нужен от ~10 TB данных или ~50 узлов.

Force merge

Каждое чтение шарда — это чтение всех segments этого шарда. Если у вас 100 сегментов в шарде, ES читает 100 файлов на запрос. После rollover'а старый индекс не получает писем — можно сжать его в один segment:

POST /products-2025-01/_forcemerge?max_num_segments=1

Эффект: 100 сегментов → 1 сегмент → faster reads (на 10-30%), меньше metadata в RAM.

Не делать на горячем индексе: операция тяжёлая, блокирует индексацию, может занять часы.

ILM делает force merge автоматически в warm-фазе.

Sizing — сколько чего

Грубые ориентиры:

Heap

JVM heap = ~50% от RAM узла, не больше 31 GB (из-за compressed oops). Если данных больше — лучше больше узлов с heap 30 GB, чем один с 64 GB.

RAM узла

Идеал — 64 GB (32 GB heap + 32 GB OS page cache для Lucene). 128 GB допустимо, но heap всё равно 31 GB, остальное — OS cache.

Шарды на узле

≤ 600 шардов на узел при 30 GB heap. При больше — overhead на metadata убивает performance. Слишком много шардов — типичная ошибка новичков.

Если у вас 5000 индексов × 5 shards × 2 replicas = 50 000 шардов на 10 узлов = 5000 шардов на узел — это слишком, кластер задыхается. Уменьшите число шардов или число индексов (через ILM consolidation).

Размер одного шарда

10-50 GB — sweet spot. Меньше — много шардов, overhead. Больше — медленные queries, долгие merges.

Для индекса в 1 TB → ~20-100 primary shards. С 2 replicas → 60-300 шардов всего → 6-30 data-узлов.

TPS на запись

Один узел тянет 5-20K документов/сек на индексацию (зависит от размера документа, mapping, refresh interval). Для 100K docs/sec — нужен кластер 5-20 data-узлов.

Мониторинг

Prometheus exporter

Стандартный путь — elasticsearch_exporter. Контейнер рядом с ES, скрейпит _nodes/stats и экспортирует в Prometheus-формате.

Что мониторить

МетрикаЧто значитАлерт
elasticsearch_cluster_health_statusred / yellow / greenred = ALERT, yellow = warn
elasticsearch_jvm_memory_used_bytes / max_bytesHeap usage> 85% sustained
elasticsearch_jvm_gc_collection_seconds_countЧастота GCspike в young GC ОК, old GC > 1/min — плохо
elasticsearch_indices_indexing_index_time_secondsВремя индексациирастёт → backpressure
elasticsearch_indices_search_query_time_secondsВремя поискарастёт → проблемы со scoring или mapping
elasticsearch_thread_pool_rejected_countОтказы из-за полного thread pool> 0 — кластер не справляется
elasticsearch_filesystem_data_available_bytesСвободное место< 15% — watermark, ES блокирует индексацию

Watermarks

ES блокирует индексацию по проценту использования диска:

  • cluster.routing.allocation.disk.watermark.low (default 85%) — не создавать новые шарды на этом узле.
  • cluster.routing.allocation.disk.watermark.high (default 90%) — перенести шарды на другие узлы.
  • cluster.routing.allocation.disk.watermark.flood_stage (default 95%) — все индексы на этом узле в read-only.

Если флуд-stage сработал — индексация останавливается, нужен ручной вмешательство (расширить диск или удалить данные + снять флаг).

Типичные оперативные ловушки

1. Слишком много шардов

Самая частая ошибка. Каждый шард = накладные расходы. Один индекс на 100 GB лучше 100 индексов по 1 GB.

2. Refresh interval по умолчанию

При high write throughput refresh_interval=1s создаёт много мелких сегментов. Подняв до 30s, можно получить 2-3× больше throughput.

3. _all source storage

Если вы храните JSON в БД и в ES одновременно, в ES можно отключить _source:

PUT /products
{
  "mappings": {
    "_source": { "enabled": false }
  }
}

Экономит место. Но: невозможно сделать update/reindex/script — нужен полный re-index из источника. Использовать только для логов/метрик.

4. Mapping explosion

Если вы пишете JSON с динамическими полями ({ "attr_color": "...", "attr_size": "..." } с тысячами разных attr_*), ES создаст mapping на каждое уникальное поле. На миллионе уникальных полей — out of memory.

Решение: dynamic: false в mapping + flatten в одно поле attributes с типом flattened (8.x+) или nested.

5. Большие aggregations

terms aggregation с size: 10000 на индексе в миллиарды документов — может убить узел. Использовать size: 100 + composite aggregation для пагинации.

6. Cluster join по multicast

В 7.x+ нужен явный список master-узлов (discovery.seed_hosts). Без этого node не присоединяется к кластеру.

Что почитать дальше