Shard Allocation
Every shard — primary or replica — has to live on some node, and the choice is not arbitrary. Shard allocation is the cluster-manager-side subsystem that decides, on every relevant cluster-state change, where each shard should go: which unassigned shards to assign, which assigned shards to move for balance, and which moves to forbid because they'd violate a constraint (don't put a primary and its replica on the same node; don't fill a disk; respect rack awareness). Get this wrong as a contributor and you produce yellow/red clusters, hot nodes, or data loss across a rack failure.
This chapter dissects AllocationService, the BalancedShardsAllocator, the
AllocationDeciders chain, and the operator-facing _cluster/allocation/explain
and _cluster/reroute APIs. It runs on the cluster manager (formerly master)
and mutates the RoutingTable inside the ClusterState; the
new state is then published. It feeds directly into
Recovery (an assignment triggers a recovery) and
Replication.
After this chapter you can:
- Trace an allocation round from a triggering event to a new
RoutingTable. - Name the major deciders and the real-world failure each one prevents.
- Read
_cluster/allocation/explainand translate aNOdecision into a fix. - Force or cancel an assignment with
_cluster/rerouteand know when it's safe.
The cast
| Concern | Class | grep target |
|---|---|---|
| Orchestrates a round, produces new routing | AllocationService | server/src/main/java/org/opensearch/cluster/routing/allocation/AllocationService.java |
| Decides placement + balancing | BalancedShardsAllocator (impl of ShardsAllocator) | .../allocation/allocator/BalancedShardsAllocator.java |
| Yes/No/Throttle gate per move | AllocationDeciders + concrete *AllocationDecider | .../allocation/decider/ |
| Mutable working state for a round | RoutingAllocation, RoutingNodes | .../routing/allocation/RoutingAllocation.java, .../routing/RoutingNodes.java |
| Where do primaries of existing data come from | GatewayAllocator / ExistingShardsAllocator | server/src/main/java/org/opensearch/gateway/GatewayAllocator.java |
| Manual operator commands | AllocationCommand (AllocateEmptyPrimary, MoveAllocationCommand, …) | .../allocation/command/ |
ls server/src/main/java/org/opensearch/cluster/routing/allocation/decider/
grep -n "class AllocationService\|reroute\|applyStartedShards\|applyFailedShards" \
server/src/main/java/org/opensearch/cluster/routing/allocation/AllocationService.java
What triggers an allocation round
Allocation runs as part of cluster-state updates. The common triggers:
| Trigger | Path |
|---|---|
| Node joins/leaves | Coordinator join/leave → reroute |
Index created / settings changed (e.g., number_of_replicas) | metadata update → reroute |
| Shard started (recovery finished) | AllocationService.applyStartedShards |
| Shard failed | AllocationService.applyFailedShards |
Manual _cluster/reroute | TransportClusterRerouteAction |
| Disk usage crosses a watermark | DiskThresholdMonitor triggers reroute |
grep -rn "reroute\|allocationService.reroute\|applyStartedShards\|applyFailedShards" \
server/src/main/java/org/opensearch/cluster/routing/allocation/ | head
grep -rn "DiskThresholdMonitor" server/src/main/java/org/opensearch/cluster/routing/allocation/
The allocation round
flowchart TD
T[Trigger: node change / shard started/failed / reroute / watermark] --> AS[AllocationService.reroute]
AS --> RN[Build RoutingNodes + RoutingAllocation from current ClusterState]
RN --> EXIST[GatewayAllocator / ExistingShardsAllocator: assign existing primaries from on-disk data]
EXIST --> BSA[BalancedShardsAllocator.allocate]
BSA --> U[allocateUnassigned: place unassigned shards]
U --> M[moveShards: relocate shards that can no longer stay]
M --> B[balance: even out weight across nodes]
U -. each candidate move .-> D[AllocationDeciders.canAllocate / canRemain / canRebalance]
M -. each candidate move .-> D
B -. each candidate move .-> D
D -->|YES / THROTTLE / NO| BSA
B --> NRT[New RoutingTable]
NRT --> CS[New ClusterState -> publish -> recovery starts]
The allocator proposes moves; the deciders veto or throttle them. A move
happens only if every decider returns YES (or, for throttling, the round
schedules it later). One NO blocks it. _cluster/allocation/explain literally
replays this decider chain for a chosen shard and prints each decider's verdict
— that's why it's the single most useful allocation-debugging tool.
grep -n "canAllocate\|canRemain\|canRebalance\|Decision" \
server/src/main/java/org/opensearch/cluster/routing/allocation/decider/AllocationDeciders.java
grep -n "allocateUnassigned\|moveShards\|balance\|weight" \
server/src/main/java/org/opensearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java
The deciders — each one prevents a specific disaster
| Decider | Returns NO when… | The disaster it prevents |
|---|---|---|
SameShardAllocationDecider | a primary and its replica would land on the same node (or host) | losing both copies if that node dies |
DiskThresholdDecider | target node is above low/high/flood_stage disk watermark | filling a disk to 100% (read-only flood) |
AwarenessAllocationDecider | placement would violate cluster.routing.allocation.awareness.attributes (rack/zone balance) | a whole rack/AZ failure taking all copies |
FilterAllocationDecider | shard excluded by index.routing.allocation.{include,exclude,require}.* | placing data on draining/decommissioned nodes |
MaxRetryAllocationDecider | a shard has failed allocation more than index.allocation.max_retries (default 5) | infinite reallocation loops on a poison shard |
ThrottlingAllocationDecider | too many concurrent recoveries already in flight | recovery storm saturating I/O/network |
EnableAllocationDecider | allocation disabled via cluster.routing.allocation.enable (e.g., none/primaries) | accidental rebalancing during a rolling restart |
NodeVersionAllocationDecider | replica would be on an older-version node than primary | BWC: can't recover newer Lucene/segments onto older node |
ShardsLimitAllocationDecider | exceeds index.routing.allocation.total_shards_per_node | piling too many shards on one node |
ls server/src/main/java/org/opensearch/cluster/routing/allocation/decider/ | sed 's/AllocationDecider.java//'
grep -rn "Decision.NO\|Decision.THROTTLE\|Decision.single" \
server/src/main/java/org/opensearch/cluster/routing/allocation/decider/DiskThresholdDecider.java
Warning: Before a rolling restart, operators set
cluster.routing.allocation.enable: primaries(ornone) via theEnableAllocationDeciderso the cluster doesn't try to rebuild replicas of a node that's about to come right back. Forgetting to re-enable it afterward leaves replicas permanently unassigned — a top operational footgun.
RoutingAllocation and RoutingNodes — the scratch pad
AllocationService builds a mutable RoutingNodes (a node→shards view derived
from the immutable RoutingTable) and a RoutingAllocation that carries the
deciders, cluster info (disk usage via ClusterInfoService), and the
unassignedInfo for each shard. The allocator mutates RoutingNodes; at the end,
AllocationService reads the result back into a fresh, immutable RoutingTable
for the new ClusterState. The immutable-snapshot discipline of
ClusterState is preserved: the scratch pad is mutable, the
published artifact is not.
grep -n "class RoutingNodes\|initializeShard\|relocateShard\|startShard\|class RoutingAllocation" \
server/src/main/java/org/opensearch/cluster/routing/RoutingNodes.java \
server/src/main/java/org/opensearch/cluster/routing/allocation/RoutingAllocation.java
UnassignedInfo records why a shard is unassigned (INDEX_CREATED,
NODE_LEFT, ALLOCATION_FAILED, REPLICA_ADDED, …) and the failure count —
that's what MaxRetryAllocationDecider reads and what allocation/explain
reports.
grep -n "enum Reason\|allocationStatus\|failedAllocations" \
server/src/main/java/org/opensearch/cluster/routing/UnassignedInfo.java
Where primaries of existing data come from
For a brand-new index, primaries can be allocated anywhere (empty). For an index
with on-disk data (cluster restart, node rejoin), the primary must go where the
best copy of the data already lives. The GatewayAllocator (an
ExistingShardsAllocator) queries nodes for their on-disk shard copies and
allocation IDs (see in-sync allocation IDs in Replication),
then assigns the primary to the node with the most up-to-date copy. Replicas
prefer nodes that can do a cheap sequence-number-based recovery from the primary
(see Recovery).
grep -rn "ExistingShardsAllocator\|allocateUnassigned\|class GatewayAllocator\|TransportNodesListShardStoreMetadata" \
server/src/main/java/org/opensearch/gateway/ | head
Operator surface: explain and reroute
# Why is THIS shard unassigned / why won't it move? Replays the decider chain.
curl -s 'localhost:9200/_cluster/allocation/explain?pretty' \
-H 'content-type: application/json' \
-d '{"index":"idx","shard":0,"primary":false}'
# Manually move / allocate / cancel (dry_run first!)
curl -s 'localhost:9200/_cluster/reroute?dry_run=true&pretty' \
-H 'content-type: application/json' \
-d '{"commands":[{"move":{"index":"idx","shard":0,"from_node":"nodeA","to_node":"nodeB"}}]}'
allocation/explain output for an unassigned shard lists, per node, every
decider's decision and a human explanation. A red cluster's root cause is
usually one decider saying NO (e.g., DiskThresholdDecider: "the node is above
the high watermark") — read the explanation, fix the underlying condition, and the
next reroute assigns it.
grep -rn "TransportClusterAllocationExplainAction\|ClusterAllocationExplanation" \
server/src/main/java/org/opensearch/action/admin/cluster/allocation/
grep -rn "AllocationCommand\|MoveAllocationCommand\|AllocateEmptyPrimaryAllocationCommand" \
server/src/main/java/org/opensearch/cluster/routing/allocation/command/
Warning:
allocate_empty_primaryandallocate_stale_primarydiscard data — they tell the cluster to accept an empty or stale copy as the new primary. They are last-resort recovery commands for "all good copies are permanently gone," never routine.dry_runeverything first.
Reading exercise
# 1. The reroute entry and how started/failed shards feed back
grep -n "public ClusterState reroute\|applyStartedShards\|applyFailedShards\|deassociateDeadNodes" \
server/src/main/java/org/opensearch/cluster/routing/allocation/AllocationService.java
# 2. The balancer's three jobs
grep -n "allocateUnassigned\|moveShards\|balanceByWeights\|weight(" \
server/src/main/java/org/opensearch/cluster/routing/allocation/allocator/BalancedShardsAllocator.java
# 3. One decider in detail
sed -n '1,120p' server/src/main/java/org/opensearch/cluster/routing/allocation/decider/DiskThresholdDecider.java
# 4. Why-unassigned bookkeeping
grep -n "enum Reason\|AllocationStatus\|failedAllocations" \
server/src/main/java/org/opensearch/cluster/routing/UnassignedInfo.java
Answer:
- List the three phases inside
BalancedShardsAllocator.allocateand what each accomplishes. - For a move to occur, how many deciders must say
YES? What doesTHROTTLEmean vsNO, and which decider issuesTHROTTLEmost often? - Name the decider that prevents a primary and replica from co-locating, and explain why co-location would defeat the purpose of replication.
- Why does
MaxRetryAllocationDeciderexist? What would happen without it for a shard whose underlying disk is corrupt? - After a rolling restart, all replicas are unassigned and the cluster is yellow. Which setting/decider is the likely cause and what's the one-line fix?
- Explain what
allocate_stale_primarydoes and why it is a data-loss operation, citing in-sync allocation IDs from Replication.
Common bugs and symptoms
| Symptom | Likely cause | Where to look |
|---|---|---|
| Replicas stuck unassigned after rolling restart | cluster.routing.allocation.enable left at primaries/none | EnableAllocationDecider; reset to all |
| Cluster yellow, one shard won't allocate | a decider says NO (disk, awareness, filter) | _cluster/allocation/explain |
| Shards refuse to move onto a node | node above disk high watermark | DiskThresholdDecider; free disk or raise watermark |
| Index turns read-only unexpectedly | disk hit flood_stage watermark (95%) | DiskThresholdDecider; clear block after freeing space |
| Shard never reallocates after repeated failures | hit index.allocation.max_retries | MaxRetryAllocationDecider; reroute?retry_failed=true after fixing root cause |
| Both copies of data lost when one rack died | awareness not configured | AwarenessAllocationDecider + awareness.attributes |
| Hot node, uneven shard distribution | balancer weights / total_shards_per_node / filters skewing placement | BalancedShardsAllocator weights, _cat/allocation |
| Replica won't recover onto an upgraded node | version mismatch (older target) | NodeVersionAllocationDecider |
Validation: prove you understand this
- Draw the allocation round (trigger →
RoutingAllocation/RoutingNodes→ existing-shards allocator → balancer's three phases → deciders → newRoutingTable), and state where the result re-enters the immutableClusterState. - For each of
SameShard,DiskThreshold,Awareness,Filter, andMaxRetrydeciders, name the production incident it prevents in one sentence. - Given a red cluster, write the exact
allocation/explainrequest for the primary ofidxshard 0 and describe how you'd read the result to find the blocking decider. - Explain the rolling-restart
enable: primariespattern: what it prevents during the restart and the failure mode if you forget to revert it. - Distinguish
move,allocate_replica,allocate_empty_primary, andallocate_stale_primaryreroutecommands by their data-safety, and say which two can lose data. - Describe how
GatewayAllocatordecides which node should host the primary of an index that already has on-disk data, and how that ties to in-sync allocation IDs.