Эксплуатация 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
Для эффективности на больших кластерах — многоуровневое хранилище:
| Tier | Hardware | Что хранить |
|---|---|---|
| Hot | NVMe, много RAM, high CPU | Active writes, последние 1-7 дней, частые reads |
| Warm | SSD, средний RAM | Read-only, 7-30 дней |
| Cold | HDD, мало RAM, 0 replicas | Редкие reads, 30-90 дней |
| Frozen | Searchable 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_status | red / yellow / green | red = ALERT, yellow = warn |
elasticsearch_jvm_memory_used_bytes / max_bytes | Heap usage | > 85% sustained |
elasticsearch_jvm_gc_collection_seconds_count | Частота GC | spike в 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 не присоединяется к кластеру.
Что почитать дальше
- Fundamentals — основы, на которых строится этот раздел.
- Query DSL и relevance — оптимизация запросов.
- Spring Data Elasticsearch — клиентский код.
- Observability Style Guide — общие правила observability.
- Elastic: ILM docs.
- Elastic: Snapshot and Restore.