在单节点 Elasticsearch 中制造副本与分配过滤导致的分片未分配,练习 allocation explain

演练目标

环境准备

隔离边界:只使用本机 Docker Compose 的 lab-es 项目,HTTP 只绑定 127.0.0.1:9200;不要连接或修改任何生产 ES 集群。需要 Docker Compose v2,预留约 1.5 GB 内存。

docker version --format '{{.ServerVersion}}' && docker compose version
mkdir -p /tmp/lab-es
cat > /tmp/lab-es/docker-compose.yml <<'EOF'
name: lab-es
services:
  es:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3
    container_name: lab-es
    environment:
      discovery.type: single-node
      xpack.security.enabled: "false"
      ES_JAVA_OPTS: "-Xms512m -Xmx512m"
    ports: ["127.0.0.1:9200:9200"]
    volumes: ["esdata:/usr/share/elasticsearch/data"]
volumes:
  esdata:
EOF
docker compose -f /tmp/lab-es/docker-compose.yml up -d
for i in $(seq 1 60); do
  curl -fsS 'http://127.0.0.1:9200/_cluster/health' >/dev/null 2>&1 && break
  sleep 2
done
curl -s 'http://127.0.0.1:9200/_cluster/health?filter_path=status,number_of_nodes,unassigned_shards&pretty'

首次启动要拉取镜像并初始化数据目录,等待循环最多约 2 分钟。

注入故障

先创建 1 主 1 副的索引。单节点无法放置副本,副本会保持未分配:

curl -fsS -XPUT 'http://127.0.0.1:9200/lab-unassigned?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"settings":{"number_of_shards":1,"number_of_replicas":1}}'
sleep 2
curl -s 'http://127.0.0.1:9200/_cluster/health/lab-unassigned?filter_path=status,number_of_nodes,unassigned_shards&pretty'

再用分配过滤排除所有节点,制造主分片也无法分配:

curl -fsS -XPUT 'http://127.0.0.1:9200/lab-filter?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"settings":{"number_of_shards":1,"number_of_replicas":0,"index.routing.allocation.exclude._name":"*"}}'
sleep 2
curl -s 'http://127.0.0.1:9200/_cluster/health/lab-filter?filter_path=status,number_of_nodes,unassigned_shards&pretty'

观察现象

对两个索引分别执行 allocation explain,并查看磁盘水位默认值:

curl -s -XGET 'http://127.0.0.1:9200/_cluster/allocation/explain?include_disk_info=true&pretty' \
  -H 'Content-Type: application/json' \
  -d '{"index":"lab-unassigned","shard":0,"primary":false}'
curl -s -XGET 'http://127.0.0.1:9200/_cluster/allocation/explain?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"index":"lab-filter","shard":0,"primary":true}'
curl -s 'http://127.0.0.1:9200/_cluster/settings?include_defaults=true&filter_path=defaults.cluster.routing.allocation.disk&pretty'

预期 lab-unassigned 为 yellow、unassigned_shards=1,explain 的 allocation_status 为 no,decider 为 same_shard,说明副本不能和主分片放在同一节点。lab-filter 为 red、unassigned_shards=1,decider 为 filter,说明节点不匹配 index.routing.allocation.exclude._name。水位默认值通常是 low 85%、high 90%、flood_stage 95%,include_disk_info=true 会附带节点磁盘的可用空间和判断信息。

判断与定位

  1. 先写假设:副本数超过可用节点、分配过滤/节点属性不匹配、磁盘超过水位、节点离线,还是索引设置刚变更尚未完成分配。不同原因的 explain decider 不同。
  2. 用 _cluster/health?level=indices 看是哪个索引、多少分片未分配;用 _cat/shards?v 看主分片还是副本未分配。yellow 通常只缺副本,red 表示至少一个主分片未分配。
  3. 用 _cluster/allocation/explain 并指定 index、shard、primary,读取 decider 和 explanation。same_shard 是副本与主分片同节点冲突,filter 是分配过滤,磁盘问题看 disk decider 和 include_disk_info。
  4. 容易误判:看到未分配就加副本或重启节点。单节点上副本永远无法分配,加副本只会继续 yellow;另一个误判是把所有未分配都归因于磁盘水位。本实验把 low/high/flood_stage 都调成极小值后,新建索引的主分片仍可能分配成功,因此必须看 explain 的 decider,而不是只看磁盘百分比。
  5. cluster.routing.allocation.disk.watermark 的三个值必须使用同一种单位(全百分比或全字节)。混用会导致设置解析失败;修改前先读取 include_defaults=true 的当前值,并在变更后确认生效。

修复

先处理副本:把副本数降为 0,索引应变为 green:

curl -fsS -XPUT 'http://127.0.0.1:9200/lab-unassigned/_settings?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"index":{"number_of_replicas":0}}'
curl -s 'http://127.0.0.1:9200/_cluster/health/lab-unassigned?wait_for_status=green&timeout=30s&filter_path=status,unassigned_shards&pretty'

再清除分配过滤,让主分片回到节点:

curl -fsS -XPUT 'http://127.0.0.1:9200/lab-filter/_settings?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"index.routing.allocation.exclude._name":null}'
curl -s 'http://127.0.0.1:9200/_cluster/health/lab-filter?wait_for_status=green&timeout=30s&filter_path=status,unassigned_shards&pretty'

生产环境更安全的顺序是:先确认未分配的是主分片还是副本,再检查磁盘和分配过滤;副本问题可以降副本或加节点,主分片问题必须先恢复数据或节点,不能直接删除索引。磁盘超过 high watermark 时先清理或扩容,超过 flood_stage 时写入会被阻断。

清理与回滚

curl -fsS -XDELETE 'http://127.0.0.1:9200/lab-unassigned' >/dev/null || true
curl -fsS -XDELETE 'http://127.0.0.1:9200/lab-filter' >/dev/null || true
docker compose -f /tmp/lab-es/docker-compose.yml down -v --remove-orphans
rm -rf /tmp/lab-es

只删除本实验的 Compose 项目和 esdata 卷;不要对生产集群执行删除索引或 down -v。

复盘

延伸阅读

读完自测

  1. 单节点索引设置 1 个副本后,为什么集群是 yellow?
  2. _cluster/allocation/explain 中 same_shard 与 filter 分别说明什么?
  3. 磁盘水位 low、high、flood_stage 分别有什么作用?
答案
  1. 主分片已分配,但副本不能放在同一节点,副本未分配,所以集群为 yellow。
  2. 前者表示副本与已有分片在同一节点,后者表示节点不满足分配过滤条件。
  3. low 触发迁出/停止分配新分片的预警,high 阻止向该节点分配分片,flood_stage 还会阻断写入。