Command Palette

Search for a command to run...

Unit 4.2 · Search and Log Analytics: Elasticsearch, OpenSearch, ELK

Operating a Cluster: Health, Sizing, Lifecycle, Snapshots

Green/yellow/red and how to fix each, node roles, heap and shard sizing, disk watermarks, rollover with ILM/ISM (hot → warm → cold → delete), and snapshots to S3.

Advanced 55 min 4 lab steps 2 failure drills

Start here

The mental model

Cluster health is a traffic light for shard copies. GREEN: every primary and replica shard is assigned. YELLOW: all primaries are fine but some replicas aren't assigned: data is searchable, but one more failure could lose it. RED: at least one primary is missing: some data is unavailable, and searches return partial results.

Most search-cluster problems are capacity problems in disguise: too many shards for the heap, too much data for the disks, or old data kept on expensive nodes. Index LIFECYCLE management automates moving and deleting data as it ages, the way retention works in Kafka (Unit 2.3).

Go deeper

How it works inside

01Node roles

CLUSTER-MANAGER (master) nodes maintain cluster state: indexes, mappings, and which shard lives where. Run 3 dedicated, small ones in production for a stable quorum. DATA nodes hold shards and do the indexing and searching (optionally tiered as hot, warm, and cold). INGEST nodes run ingest pipelines. COORDINATING-only nodes route requests and merge results. Small clusters combine roles; large ones separate them so a heavy search can't starve the managers.

02Heap, memory, and shard sizing

Give the JVM heap about 50% of the node's RAM, at most ~31 GB (above that the JVM loses compressed pointers). The other half is for the OS page cache, which Lucene relies on heavily. Every shard costs heap and file handles regardless of its size, so the classic mistake is thousands of tiny daily indexes × 5 shards each. Guidelines: shards of roughly 10–50 GB, and no more than about 20 shards per GB of heap (fewer is better).

03Disk watermarks

At the LOW watermark (85% disk used by default) the cluster stops allocating new shards to that node. At HIGH (90%) it tries to move shards away. At FLOOD STAGE (95%) every index with a shard on that node becomes READ-ONLY (index.blocks.read_only_allow_delete), and indexing fails cluster-wide for those indexes. Recent versions release the block automatically once disk drops below the high watermark; older ones needed a manual reset.

04Rollover, data streams, and ILM/ISM

For time-series data (logs, metrics, events), don't write to one ever-growing index or rely on date-named indexes. Write to an ALIAS or DATA STREAM that ROLLS OVER to a new backing index when the current one reaches a size or age (e.g. 30 GB or 1 day). Then a lifecycle policy (ILM in Elasticsearch, ISM in OpenSearch) manages each backing index: HOT (fast disks, being written) → WARM (read-only, force-merged, cheaper nodes) → COLD/frozen (object-storage-backed searchable snapshots, or UltraWarm on AWS) → DELETE after the retention period.

Rollover, data streams, and ILM/ISMdiagram
Rendering diagram…

05Snapshots

SNAPSHOTS copy indexes incrementally to a repository (S3, GCS, Azure, shared FS). They're the only real backup: replicas don't protect against a bad delete or mapping mistake (Unit 0.3). Schedule them with SLM (Elasticsearch) or ISM snapshot management (OpenSearch); Amazon OpenSearch Service takes automated hourly snapshots too. Restore into the same or a new cluster, which is also how you migrate between clusters.

Do it

Hands-on lab

  1. 1

    Read cluster health and explain yellow

    On a single node, any index with replicas is yellow: a replica may never live on the same node as its primary. _cluster/allocation/explain tells you why a shard is unassigned, and is the first command in any red/yellow incident.

    terminal
    $ curl -s -XPUT localhost:9200/logs-test -H 'content-type: application/json' -d '{"settings":{"number_of_replicas":1}}' > /dev/null
    curl -s 'localhost:9200/_cluster/health?pretty' | grep -E 'status|unassigned_shards"'
    curl -s localhost:9200/_cluster/allocation/explain -H 'content-type: application/json' -d '{"index":"logs-test","shard":0,"primary":false}' | jq -r '.node_allocation_decisions[0].deciders[0].explanation'
    ── expected output ──
    "status" : "yellow",
    "unassigned_shards" : 1,
    a copy of this shard is already allocated to this node [[logs-test][0], node[...], [P], s[STARTED]]
  2. 2

    The _cat APIs: your dashboard in a terminal

    Human-readable views of nodes, shards, indexes, and disk. ?v adds headers and s= sorts.

    terminal
    $ curl -s 'localhost:9200/_cat/allocation?v'
    curl -s 'localhost:9200/_cat/indices?v&s=store.size:desc&h=health,index,pri,rep,docs.count,store.size' | head -4
    ── expected output ──
    shards disk.indices disk.used disk.avail disk.total disk.percent host node
    4 12.4mb 41.2gb 58.7gb 99.9gb 41 172.17... 3f1c...
    health index pri rep docs.count store.size
    yellow logs-test 1 1 0 208b
    green products 1 0 3 9.8kb
  3. 3

    An ISM policy: hot → warm → delete

    OpenSearch's ISM syntax (Elasticsearch ILM is similar in spirit, different in JSON). The ism_template attaches the policy to every new index matching logs-shop-*.

    logs-policy.jsonwhole filejson
    {
      "policy": {
        "description": "ShopLite logs: rollover daily, warm after 3d, delete after 30d",
        "default_state": "hot",
        "states": [
          { "name": "hot",
            "actions": [ { "rollover": { "min_size": "30gb", "min_index_age": "1d" } } ],
            "transitions": [ { "state_name": "warm", "conditions": { "min_index_age": "3d" } } ] },
          { "name": "warm",
            "actions": [ { "read_only": {} }, { "force_merge": { "max_num_segments": 1 } }, { "replica_count": { "number_of_replicas": 1 } } ],
            "transitions": [ { "state_name": "delete", "conditions": { "min_index_age": "30d" } } ] },
          { "name": "delete", "actions": [ { "delete": {} } ] }
        ],
        "ism_template": [ { "index_patterns": ["logs-shop-*"], "priority": 100 } ]
      }
    }
    terminal
    $ curl -s -XPUT localhost:9200/_plugins/_ism/policies/logs-shop -H 'content-type: application/json' -d @logs-policy.json | jq -r ._id
    curl -s -XPUT localhost:9200/logs-shop-000001 -H 'content-type: application/json' -d '{"settings":{"plugins.index_state_management.rollover_alias":"logs-shop"},"aliases":{"logs-shop":{"is_write_index":true}}}' | jq .acknowledged
    ── expected output ──
    logs-shop
    true
  4. 4

    Register an S3 snapshot repository and snapshot

    Needs the repository-s3 plugin (built into the managed services) and credentials via IAM role. Then snapshot on a schedule and test restores.

    terminal
    $ curl -s -XPUT localhost:9200/_snapshot/s3-backups -H 'content-type: application/json' -d '{"type":"s3","settings":{"bucket":"shoplite-search-snapshots","base_path":"prod"}}'
    curl -s -XPUT 'localhost:9200/_snapshot/s3-backups/snap-2026-09-27?wait_for_completion=true' | jq -r .snapshot.state
    ── expected output ──
    {"acknowledged":true}
    SUCCESS

Operate it

Knobs that matter

SettingDefaultWhat it doesWhen to change it
Heap (-Xms/-Xmx)1g (in Docker images)JVM heap for the node.~50% of RAM, ≤ 31 GB, Xms = Xmx.
cluster.routing.allocation.disk.watermark.*85% / 90% / 95%Disk thresholds for allocation and read-only blocks.Keep defaults, but alert at 75% and plan capacity well before low.
index.routing.allocation.require.<attr>—Pin indexes to nodes with an attribute (e.g. temp: warm).Used by lifecycle policies for hot/warm/cold tiers.
cluster.max_shards_per_node1000Safety limit on shards per data node.Don't raise it to fix 'too many shards'; reduce shard count instead.
Dedicated manager nodesnoneSeparate small nodes for cluster state.3 in any production cluster with more than a few data nodes.

3am practice

Failure drills

Each drill is a real failure mode. Read the scenario and the output, decide what's wrong, then reveal the diagnosis.

Drill #1

Logs stop arriving, cluster is fine... ish

At 03:00, logs from every service stop appearing in dashboards. Fluent Bit logs are full of errors.

terminal
$ kubectl -n logging logs ds/fluent-bit | tail -1
── what you'll see ──
[error] [output:opensearch:opensearch.0] HTTP status=429 URI=/_bulk, response: {"type":"cluster_block_exception","reason":"index [logs-shop-000213] blocked by: [TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark, index has read-only-allow-delete block];"}

Drill #2

Red after a node dies

One of three data nodes is terminated by a spot interruption. Health goes red and some product searches return partial results.

terminal
$ curl -s 'localhost:9200/_cat/shards?v&h=index,shard,prirep,state,unassigned.reason' | grep UNASSIGNED
── what you'll see ──
products-v3 2 p UNASSIGNED NODE_LEFT
products-v3 2 r UNASSIGNED NODE_LEFT

The bigger picture

Connects to

Prove it

Interview questions

01

What do yellow and red cluster health mean and how do you troubleshoot?

02

How do you size shards and why does it matter?

03

What happens at the flood-stage watermark?

0/3 · 0%