Elasticsearch
当商品搜索开始依赖 LIKE '%keyboard%'、日志排障需要跨服务过滤聚合、分页越深数据库越慢时,问题通常已经不只是“再加一个索引”。Elasticsearch 的价值,是把文本相关性、结构化过滤、聚合和近实时查询放进专门的搜索读模型,并让这份读模型可以从权威数据重新构建。
这个能力也划定了它的边界:订单、库存和账务仍由事务系统负责,Elasticsearch 保存的是允许短暂落后、可以校准和重建的搜索投影。后面的实验会沿着同一条链路完成安全启动、显式 mapping、写入查询、最小权限应用接入、分片与容量治理、快照恢复、alias 迁移和故障定位,而不是把若干 API 命令孤立罗列出来。
一、是什么
把 Elasticsearch 放在搜索投影位置
Elasticsearch 是以 Apache Lucene 为核心的分布式搜索与分析引擎。它接收 JSON 文档,把文本建立倒排索引,把 keyword、数值、日期和地理字段建立适合过滤、排序与聚合的索引,再通过 REST API 提供全文检索、结构化查询、聚合、向量检索和近实时分析。
它最适合承担搜索读模型、日志与事件检索、可观测性分析、商品与内容检索。它不是关系数据库:没有外键、多行 ACID 和通用回滚;也不是消息队列:写入索引不等于事件已被所有下游可靠消费;更不能把动态 JSON 当成无需治理的长期事实库。订单、账务、库存等权威事实应留在事务数据库,Elasticsearch 通常由 CDC、消息或可重放批任务构建搜索投影。
一次设计先明确三个边界:源数据在哪里、允许搜索结果落后多久、索引丢失后如何重建。回答不了这三个问题,就不应把 Elasticsearch 放到核心链路。
固定官方入口、版本线与许可
官方资源包括:官网 https://www.elastic.co/elasticsearch;文档 https://www.elastic.co/docs;REST API https://www.elastic.co/docs/api/doc/elasticsearch/;下载 https://www.elastic.co/downloads/elasticsearch;历史版本;源码;发行说明;支持矩阵;维护策略。
下面的可复现实验固定使用 9.5.2。固定实验版本不等于给生产环境自动选版;生产升级前应重新核对官方发行说明、已知问题、维护策略、breaking changes 和支持矩阵,先选择仍受维护且经过应用兼容验证的 major/minor,再固定该线补丁和制品摘要。Elasticsearch、Kibana 和逐节点插件必须使用同一 Stack 版本,客户端及采集组件则按官方兼容说明分阶段升级;不能因为下载页默认值变化就临时跨线。
7.11 起,免费部分源码可按 ELv2 或 SSPL 1.0 使用,后来又增加 AGPLv3 选项;Elastic 默认二进制发行版继续按 ELv2 分发,官方客户端仍采用 Apache 2.0。源码许可、二进制许可和订阅功能不是同一件事,嵌入式分发、修改源码或对外托管前应核对官方许可 FAQ和订阅矩阵。
把协调、分片和 Lucene 画在一条路径上
| 角色 | 主要职责 | 设计重点 |
|---|---|---|
| master-eligible | 选举 elected master、发布 cluster state | 三个投票节点、独立故障域、稳定网络 |
| data | 保存 shard、执行写入与查询 | 磁盘、文件缓存、heap、分片数量与数据层 |
| ingest | 执行 pipeline | CPU 与失败处理,重转换宜独立 |
| coordinating | 分发请求、归并结果 | heap、网络、并发和聚合桶数 |
| ml / transform | 机器学习与持续转换 | 独立容量和订阅边界 |
索引是文档的逻辑集合;primary shard 是写入与数据分布的基本单位;replica shard 是 primary 的可提升副本,并可分担查询。一个 shard 本质上是一个 Lucene index,包含多个不可变 segment。cluster state 保存索引、mapping、模板、路由和分片分配等控制面元数据,由当前 elected master 发布。
从确定性文档 ID 追踪写入路径
文档通过 HTTP 进入任意节点。该节点成为协调节点,完成认证、授权、pipeline 和 routing 计算,把请求发给目标 primary。primary 分配 _seq_no,以 _primary_term 标识任期,更新 Lucene 内存结构并追加 translog,再复制给 in-sync replicas;达到应答条件后返回结果。
client -> coordinating node -> primary shard -> replica shards
|-> translog
|-> refresh -> searchable segmentrefresh 让新 segment 对搜索可见,不等于刷盘;flush 提交 Lucene commit 并开始新的 translog generation,不是日常“清缓存”;index.translog.durability=request 控制 translog 的同步语义,不能替代快照。
客户端超时只代表没有收到响应,不能证明服务端没写。使用业务主键作为确定性 _id;更新时保存 _seq_no 和 _primary_term,用 if_seq_no、if_primary_term 防止旧写覆盖新写。自动生成 _id 后盲目重试可能产生重复文档。
从 Query DSL 追踪查询与 fetch
协调节点把 Query DSL 改写并发送到目标 shards。每个 shard 在自己的 segments 上执行 query phase,返回局部 top-K 和聚合中间结果;协调节点归并后,只向最终命中的 shards 拉取 _source 或 stored fields,形成 fetch phase 响应。
Query DSL -> rewrite -> shard query -> local top-K -> coordinator reduce -> fetch倒排索引负责从词项找到文档;BKD tree 负责数值、日期、IP 和地理范围;doc_values 是 keyword、数值和日期等字段用于排序与聚合的列式结构;_source 保存原始文档,便于返回、重建和 reindex。text 默认不适合排序聚合,给它开启 fielddata 会占用大量 heap,通常应使用 keyword 子字段。
from + size 的成本随页码增加。深分页使用 PIT 加 search_after,并使用稳定且唯一的复合排序;PIT 会保留 segment 引用,用完必须关闭。
理解搜索结果背后的索引状态
mapping 与 analyzer 决定搜索契约
mapping 是搜索契约。字段类型决定可接受的值、索引结构和查询能力;analyzer 决定文本在写入和查询时如何切词。核心索引应显式 mapping,必要时用 dynamic:"strict" 阻止意外字段。动态字段无限增长会造成 mapping explosion 和 cluster state 膨胀。
{
"dynamic": "strict",
"properties": {
"product_id": {"type": "keyword"},
"title": {"type": "text", "fields": {"raw": {"type": "keyword"}}},
"price": {"type": "scaled_float", "scaling_factor": 100},
"created_at": {"type": "date"},
"tags": {"type": "keyword"}
}
}shard、routing 与副本决定分布
primary shard 数决定一个索引的基础并行度和数据分布,创建后不能直接修改;可通过 split、shrink 或 reindex 改变。默认 routing 使用 _id 哈希,同一个 routing 值始终进入同一 shard。自定义 routing 可减少查询扇出,但 routing 倾斜会制造热点。
replica 提供节点故障冗余和查询副本,不是备份。误删索引会同步到副本;快照才用于恢复误操作和集群级故障。
segment、refresh、merge 与 translog 决定可见性
segment 不可变。refresh 产生新 segment 后文档可搜索;后台 merge 合并小 segment、回收删除标记。refresh 太频繁会增加小 segment、merge 和磁盘 IO;大量更新或删除也不会立即归还磁盘。translog 用于恢复尚未进入 Lucene commit 的操作,但不能跨集群保存长期历史。
cluster state 与单文档一致性划定控制面
cluster state 由 elected master 串行发布,字段、shard、alias 和模板过多都会放大状态。Elasticsearch 在单文档层面提供原子更新和乐观并发,不提供跨文档事务。搜索是近实时的;主分片确认、replica 传播、refresh 可见和外部源提交是不同时间点。
与其他系统的本质差异
| 系统 | 核心模型 | 最擅长 | Elasticsearch 不能替代的部分 |
|---|---|---|---|
| PostgreSQL / MySQL | 行、表、事务、约束 | 业务事实、关联、强约束 | 多行 ACID、外键、唯一事实 |
| OpenSearch | Lucene 分布式搜索 | 与 Elasticsearch 相近 | API、插件、许可和版本已分叉 |
| Solr | Lucene、collection | 搜索与可定制检索 | 运维模型、生态和 API 不同 |
| ClickHouse | 列式分析 | 大规模扫描聚合 | 相关性与搜索体验 |
| Kafka | 分区追加日志 | 事件保留、位点、重放 | 可靠事件主线与消费语义 |
| 对象存储 | 对象与生命周期 | 低成本归档 | 低延迟全文检索与聚合 |
二、为什么
从权威数据与搜索目标推导选型
适用与不适用场景
适合使用 Elasticsearch 的条件包括:商品、内容或文档需要全文检索、相关性排序、同义词、自动补全或多字段过滤;日志、指标和事件需要按时间、服务、地区、状态聚合与下钻;地理、向量、复杂文本或高基数筛选让事务库承担搜索成本不经济;搜索可以近实时,且数据有权威来源、可重放事件或已验证快照;团队能持续维护 mapping、分片、容量、权限、升级和恢复。
不适合或不应单独使用的情况包括:只有主键查询、数据量小且数据库索引已满足延迟;资金、订单、库存要求跨行事务、唯一约束和强一致;写后必须立即被所有读请求看到,且不能容忍回源或等待 refresh;需要长期保存、确认、回放消息,却没有消息系统;缺少监控、快照仓库、重建源和 Elasticsearch 值班能力。
常见场景与数据角色
| 场景 | 权威数据 | 索引方式 | 关键风险 |
|---|---|---|---|
| 商品搜索 | 商品数据库 | CDC + 全量校准 | 更新延迟、错删、同义词和价格一致性 |
| 内容检索 | 内容库/对象存储 | 发布事件 + 批量重建 | 权限过滤、富文本解析、重建耗时 |
| 日志分析 | Agent / Kafka / 对象存储 | data stream + ILM | 峰值写入、字段爆炸、保留成本 |
| 应用内搜索 | 业务数据库 | outbox / CDC | 双写不一致、alias 迁移 |
| 向量检索 | 模型管道与原文 | 批量嵌入 + 增量更新 | 模型版本、内存、召回和成本 |
索引只保存搜索所需字段,不复制所有业务表。隐私字段、不可搜索大字段和强事务中间状态应留在源系统;搜索命中后可按 ID 回源读取最新详情。
优势成立的条件与代价
优势来自倒排索引、列式数据、分片并行和近实时 segment,而不是“节点多就一定快”。它要求 mapping 与查询匹配、shard 尺寸受控、heap 与文件缓存充足、bulk 与 refresh 协调、深分页和聚合桶受限。代价包括磁盘放大、replica、快照仓库、重建链路、版本升级和故障处理。复制搜索副本会增加一致性治理和容量成本。
部署形态怎么选
| 形态 | 适合 | 代价与边界 |
|---|---|---|
| 单节点 | 本地学习、CI、小型可重建实验 | 无节点冗余,不是生产高可用 |
| 自建三节点起步 | 需要底层控制且有 SRE 能力 | 主机、存储、证书、升级、快照均自管 |
| ECK | 已有成熟 Kubernetes 平台 | Operator 不替代 K8s、PVC、容量和灾备责任 |
| Elastic Cloud Hosted | 希望减少基础设施维护 | 仍需管理拓扑、数据、权限、费用和恢复 |
| Elastic Cloud Serverless | 工作负载弹性大,不想管理节点 | 部分节点、集群、快照和插件 API 不开放 |
生产一般从三个 master-eligible 节点和至少两个 data 节点的故障域设计起步;小规模可让三个节点同时承担 master 与 data,但要保证任意一台离线仍有投票多数、primary 与 replica 不共处。日志规模大时再按 hot/warm/cold 分层,不要为小规模系统预先堆叠角色。
一致性、可用性、性能与成本取舍
| 目标 | 选择 | 获得 | 付出 |
|---|---|---|---|
| 更低写入成本 | 较长 refresh、批量写 | 吞吐高、小 segment 少 | 搜索可见更晚 |
| 更高可用 | 多副本、跨故障域、快照 | 节点故障仍可服务 | 存储、网络和写放大 |
| 更强读一致 | refresh=wait_for 或回源 | 关键流程看见新状态 | 延迟增加,仍非事务 |
| 更低成本 | 数据分层、ILM、归档 | 热节点容量下降 | 冷查询更慢、复杂度增加 |
| 更快聚合 | 合理 doc_values、预聚合 | 查询延迟下降 | 索引体积和写入成本增加 |
RPO 由快照周期、CDC 位点和源系统保留决定;RTO 由集群切换、恢复速度、重建吞吐和应用切流决定。replica 主要缩短节点故障 RTO,不能替代快照控制 RPO。
竞品选择与结论
竞品选择可归纳为:搜索条件简单时先用关系数据库索引,避免引入第二套系统;需要 Lucene 搜索与 Elastic 生态时评估 Elasticsearch;组织已标准化 OpenSearch 时按它的官方 API、插件和策略设计;主要是海量扫描和 OLAP 时优先 ClickHouse 等列式系统;主要是事件留存和重放时以 Kafka 为主线、Elasticsearch 为查询投影;不想维护 JVM、分片、快照和滚动升级时优先评估托管服务。
满足“需要搜索能力、允许近实时、存在权威源、能够重建、团队能运维”时选择 Elasticsearch。生产方案必须写清数据角色、P95/P99、峰值写入、热窗口、RPO/RTO、分片预算、恢复路径和退出成本。
三、怎么做
固定可复现的安全工具链
准备环境与固定版本
下面的实验环境是 Linux、Docker 24+、curl、jq 和 OpenSSL。宿主至少 4 GiB 可用内存和 10 GiB 磁盘;生产规格必须由真实压测决定。
docker version
curl --version
jq --version
openssl version
free -h
df -hLinux 设置当前官方建议的 vm.max_map_count:
sudo sysctl -w vm.max_map_count=1048576
printf 'vm.max_map_count=1048576\n' | sudo tee /etc/sysctl.d/90-elasticsearch.conf
sudo sysctl --system
sysctl vm.max_map_count期望输出 1048576。若容器日志出现 max virtual memory areas ... too low,说明参数没有应用到宿主。固定实验版本,不使用 latest:
export ES_VERSION=9.5.2
export ES_IMAGE="docker.elastic.co/elasticsearch/elasticsearch:${ES_VERSION}"
export ES_IMAGE_DIGEST='docker.elastic.co/elasticsearch/elasticsearch@sha256:9c1e1afc2bda921b35025e21c72ec6e392266995aa35ad6a47887363592718be'
docker pull "$ES_IMAGE"
docker pull "$ES_IMAGE_DIGEST"
docker image inspect "$ES_IMAGE_DIGEST" --format '{{index .RepoDigests 0}}'生产把审核过的 registry、架构和 digest 写入制品清单;tag 只便于阅读。
用 Docker 启动最小安全实例
首次初始化与已有数据恢复要分开。先检查同名资源:
docker ps -a --filter name='^/es-lab$'
docker volume inspect es-lab-data 2>/dev/null || true如果 volume 已存在,不要注入新 ELASTIC_PASSWORD 期待它重置密码;该变量只在首次初始化时生效。学习环境首次创建:
install -d -m 700 "$PWD/es-lab"
export ELASTIC_PASSWORD="$(openssl rand -base64 32 | tr -d '\n')"
printf '%s' "$ELASTIC_PASSWORD" > "$PWD/es-lab/elastic.password"
chmod 600 "$PWD/es-lab/elastic.password"
docker network create es-lab-net
docker volume create es-lab-data
docker volume create es-lab-backups
docker run -d --name es-lab --restart unless-stopped \
--network es-lab-net -p 127.0.0.1:9200:9200 \
--memory 2g --ulimit nofile=65535:65535 \
-v es-lab-data:/usr/share/elasticsearch/data \
-v es-lab-backups:/mnt/backups \
-e "ELASTIC_PASSWORD=$ELASTIC_PASSWORD" \
-e discovery.type=single-node \
-e ES_JAVA_OPTS='-Xms1g -Xmx1g' \
"$ES_IMAGE_DIGEST" -E path.repo=/mnt/backups资源已存在时先核对是否属于本实验,不直接复用未知数据。等待健康并取得自动生成的 HTTP CA:
ready=false
for attempt in $(seq 1 90); do
running="$(docker inspect -f '{{.State.Running}}' es-lab 2>/dev/null || true)"
if test "$running" != true; then
docker logs es-lab --tail 200
exit 1
fi
if docker cp es-lab:/usr/share/elasticsearch/config/certs/http_ca.crt \
"$PWD/es-lab/http_ca.crt" 2>/dev/null &&
curl --fail --silent --cacert "$PWD/es-lab/http_ca.crt" \
-u "elastic:$ELASTIC_PASSWORD" https://127.0.0.1:9200 >/tmp/es-root.json &&
jq -e --arg version "$ES_VERSION" \
'.version.number==$version and (.cluster_uuid|length>0)' /tmp/es-root.json >/dev/null; then
ready=true
break
fi
sleep 2
done
if test "$ready" != true; then
docker logs es-lab --tail 200
exit 1
fi
jq '{cluster_name,cluster_uuid,version:.version.number}' /tmp/es-root.json最多等待三分钟;容器退出或超时都会打印最近日志并返回非零。日志若是内核参数、内存或数据目录错误,先修对应前提再重新创建;401 则核对这是首次初始化还是旧 volume。已有 volume 恢复时使用原凭据启动,不再传 ELASTIC_PASSWORD,并核对历史 UUID:
export EXPECTED_CLUSTER_UUID='填写原集群 UUID'
export ELASTIC_PASSWORD="$(cat "$PWD/es-lab/elastic.password")"
curl --fail --silent --cacert "$PWD/es-lab/http_ca.crt" \
-u "elastic:$ELASTIC_PASSWORD" https://127.0.0.1:9200/ |
jq -e --arg uuid "$EXPECTED_CLUSTER_UUID" '.cluster_uuid==$uuid'UUID 不符立即停止接流量,检查 endpoint、volume 和恢复记录。超级用户密码应进入密钥管理系统,验证读回后删除本地文件;实验文件保持 0600,到清理章节删除。
用 DEB、RPM 或 tar 包建立 Linux 服务
生产 Linux 优先使用官方 DEB/RPM。安装前从官方下载页固定版本并核对 SHA-512:
sha512sum -c elasticsearch-9.5.2-amd64.deb.sha512
sudo dpkg -i elasticsearch-9.5.2-amd64.deb
sudo systemctl daemon-reload
sudo systemctl enable elasticsearch.servicesha512sum -c elasticsearch-9.5.2-x86_64.rpm.sha512
sudo rpm --checksig elasticsearch-9.5.2-x86_64.rpm
sudo rpm -ivh elasticsearch-9.5.2-x86_64.rpm
sudo systemctl daemon-reload
sudo systemctl enable elasticsearch.service官方 tar.gz 适合不使用系统包管理器的 Linux 主机。以下路径固定为 9.5.2、x86_64,下载和解压由当前运维用户执行,进程使用无登录权限的 elasticsearch 用户,数据不写入 root 家目录:
export ES_VERSION=9.5.2
export ES_TARBALL="elasticsearch-${ES_VERSION}-linux-x86_64.tar.gz"
curl --fail --remote-name "https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-9.5.2-linux-x86_64.tar.gz"
curl --fail --remote-name "https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-9.5.2-linux-x86_64.tar.gz.sha512"
sha512sum -c "$ES_TARBALL.sha512"期望校验输出以 OK 结束;下载失败检查 TLS、代理和固定 URL,校验失败删除两个文件后重新下载,不能继续解压。创建独立目录:
sudo useradd --system --home-dir /opt/elasticsearch --shell /usr/sbin/nologin elasticsearch 2>/dev/null || true
sudo install -d -o elasticsearch -g elasticsearch -m 750 /opt/elasticsearch /var/lib/elasticsearch-tar /var/log/elasticsearch-tar
sudo tar -xzf "$ES_TARBALL" -C /opt/elasticsearch --strip-components=1
sudo chown -R elasticsearch:elasticsearch /opt/elasticsearch
sudo -u elasticsearch /opt/elasticsearch/bin/elasticsearch --version先写最小单节点配置,再启动到后台 PID 文件:
sudo -u elasticsearch tee /opt/elasticsearch/config/elasticsearch.yml >/dev/null <<'YAML'
cluster.name: es-tar-lab
node.name: es-tar-a
path.data: /var/lib/elasticsearch-tar
path.logs: /var/log/elasticsearch-tar
network.host: 127.0.0.1
discovery.type: single-node
YAML
sudo -u elasticsearch /opt/elasticsearch/bin/elasticsearch -d -p /opt/elasticsearch/es.pid自动安全配置完成后,最多等待两分钟,以本地 HTTPS 返回 401 作为“端口与 TLS 已就绪”的信号,同时检查 PID 是否存活:
ready=false
ES_TAR_CLIENT_DIR="$(mktemp -d)"
for attempt in $(seq 1 60); do
if ! sudo -u elasticsearch test -s /opt/elasticsearch/es.pid; then sleep 2; continue; fi
ES_TAR_PID="$(sudo -u elasticsearch cat /opt/elasticsearch/es.pid)"
[[ "$ES_TAR_PID" =~ ^[0-9]+$ ]] && (( ES_TAR_PID > 1 )) || break
sudo -u elasticsearch /bin/kill -0 -- "$ES_TAR_PID" || break
if ! sudo -u elasticsearch test -s /opt/elasticsearch/config/certs/http_ca.crt; then sleep 2; continue; fi
sudo install -o "$(id -u)" -g "$(id -g)" -m 0600 \
/opt/elasticsearch/config/certs/http_ca.crt "$ES_TAR_CLIENT_DIR/http_ca.crt" || break
code="$(curl -q --noproxy '*' --silent --show-error --connect-timeout 2 --max-time 5 \
--output "$ES_TAR_CLIENT_DIR/root.json" --write-out '%{http_code}' \
--cacert "$ES_TAR_CLIENT_DIR/http_ca.crt" https://127.0.0.1:9200/)" || code=000
if test "$code" = 401; then ready=true; break; fi
sleep 2
done
if test "$ready" != true; then
sudo -u elasticsearch tail -n 200 /var/log/elasticsearch-tar/es-tar-lab.log
exit 1
fiPID、日志和原始配置由 elasticsearch 用户读取;这里只把公开 CA 复制到当前运维用户的私有临时目录,不放宽整个配置目录或私钥的权限。生成实验密码、严格验证版本与 UUID,然后用已经核验的 PID 正常停止:
(
set -euo pipefail
ES_TAR_PASSWORD="$(sudo -u elasticsearch /opt/elasticsearch/bin/elasticsearch-reset-password -u elastic -a -b -s)"
curl -q --noproxy '*' --fail --silent --show-error --max-time 10 \
--cacert "$ES_TAR_CLIENT_DIR/http_ca.crt" \
-u "elastic:$ES_TAR_PASSWORD" https://127.0.0.1:9200/ |
jq -e --arg version "$ES_VERSION" '.version.number==$version and (.cluster_uuid|length>0)'
sudo -u elasticsearch bash -s -- "$ES_TAR_PID" <<'BASH'
set -eu
pid="$1"
case "$pid" in ''|*[!0-9]*) exit 1;; esac
test "$pid" -gt 1
kill -0 "$pid"
kill -TERM "$pid"
stopped=false
for attempt in $(seq 1 30); do
if kill -0 "$pid" 2>/dev/null; then
sleep 1
elif test ! -e "/proc/$pid"; then
stopped=true
break
else
printf '进程仍存在,但无法检查其状态;未确认停止。\n' >&2
exit 1
fi
done
test "$stopped" = true
BASH
unset ES_TAR_PASSWORD
)停止脚本非零退出时查看日志并核对进程,不能继续删除 data path。kill -0 失败也可能是权限不足,因此检查和 TERM 使用同一服务身份,并以 /proc 中进程已消失辅助确认;PID 文件在退出时可能被删除,不能每轮重新读取它。tar.gz 没有 systemd 单元和自动升级,生产要自行提供服务管理、日志轮转和滚动升级流程。
安装生成的初始密码立即保存到密钥管理系统。重置前确认 file realm 未禁用、配置路径正确、本地 HTTPS 可达:
export ES_PATH_CONF=/etc/elasticsearch
sudo test -r "$ES_PATH_CONF/elasticsearch.yml"
sudo -E /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -i
ES_PACKAGE_CLIENT_DIR="$(mktemp -d)"
sudo install -o "$(id -u)" -g "$(id -g)" -m 0600 \
/etc/elasticsearch/certs/http_ca.crt "$ES_PACKAGE_CLIENT_DIR/http_ca.crt"
curl -q --noproxy '*' --fail --show-error --max-time 10 \
--cacert "$ES_PACKAGE_CLIENT_DIR/http_ca.crt" -u elastic https://127.0.0.1:9200/若 file realm 被禁用,elasticsearch-reset-password 不可用;使用仍有效的管理身份调用 change password API,或受控恢复 file realm。新凭据认证并从密钥系统读回后,再撤销旧凭据。
源码构建用于贡献和调试,不是常规生产安装:
git clone https://github.com/elastic/elasticsearch.git
cd elasticsearch
git checkout v9.5.2
./gradlew localDistro失败先对照该 tag 的构建说明和支持 JDK;自编译包进入生产前需重新完成兼容、许可和供应链审核。
用健康检查和 CLI 证明端点身份
版本查看只证明二进制版本,不读取 elasticsearch.yml,必须与权限、实际启动和日志验证分开:
sudo -u elasticsearch /usr/share/elasticsearch/bin/elasticsearch --version
sudo test -r /etc/elasticsearch/elasticsearch.yml
sudo -u elasticsearch test -r /etc/elasticsearch/elasticsearch.yml
sudo -u elasticsearch test -w /var/lib/elasticsearch
sudo -u elasticsearch /usr/share/elasticsearch/bin/elasticsearch-keystore list
systemd-analyze verify elasticsearch.service这些检查失败时先修文件属主、目录权限、keystore 或 systemd 单元;Elasticsearch 的 bootstrap checks 和未知配置项仍以实际启动日志为准:
sudo systemctl start elasticsearch
sudo systemctl is-active --quiet elasticsearch
sudo journalctl -u elasticsearch -n 100 --no-pageris-active 非零或 journal 出现 bootstrap/configuration exception 时先停止服务、修首个错误再启动,不能用 --version 的成功替代。
容器使用 docker start|stop es-lab。建立后续公共变量:
export ES_URL=https://127.0.0.1:9200
export ES_CA="$PWD/es-lab/http_ca.crt"
export ES_AUTH="elastic:$(cat "$PWD/es-lab/elastic.password")"根 API、集群健康和节点列表是第一轮检查:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/" |
jq '{name,cluster_name,cluster_uuid,version:.version.number}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cluster/health" |
jq '{status,number_of_nodes,active_primary_shards,unassigned_shards}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/nodes?v"单节点实验可能因 replica 为 1 而 yellow;后面创建的实验索引会设 replica 0。red 代表至少一个 primary 不可用,不能接入业务。curl 返回 401 检查凭据,证书错误检查 CA 和 endpoint,连接拒绝查看进程和监听。
Elasticsearch 没有独立的交互式 SQL shell,日常 CLI 是 curl、Kibana Dev Tools 或语言客户端。需要 SQL 时可调用 _sql API,但 Query DSL 才覆盖完整搜索能力。
建立商品搜索投影与访问边界
用稳定商品 ID 完成 CRUD 与批量写入
先创建一个显式 mapping 的索引:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' -X PUT "$ES_URL/products-v1" -d '{
"settings":{"number_of_shards":1,"number_of_replicas":0},
"mappings":{"dynamic":"strict","properties":{
"product_id":{"type":"keyword"},
"title":{"type":"text","fields":{"raw":{"type":"keyword"}}},
"price":{"type":"scaled_float","scaling_factor":100},
"category":{"type":"keyword"},
"created_at":{"type":"date"}
}}
}' | jq -e '.acknowledged==true'失败若为 resource_already_exists_exception,先 GET mapping 判断是否是预期索引,不要直接覆盖。写入确定性 ID:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' -X PUT \
"$ES_URL/products-v1/_doc/sku-1001?refresh=wait_for" -d '{
"product_id":"sku-1001","title":"Mechanical Keyboard",
"price":599.00,"category":"keyboard","created_at":0
}' | jq '{result,_seq_no,_primary_term,_shards}'期望 result:"created"、_shards.failed:0。读取、更新、删除:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/products-v1/_doc/sku-1001" |
jq '{found,_source,_seq_no,_primary_term}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X POST "$ES_URL/products-v1/_update/sku-1001?refresh=wait_for" -d '{"doc":{"price":579}}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-X DELETE "$ES_URL/products-v1/_doc/sku-1001?refresh=wait_for"删除期望 result:"deleted";重复删除返回 404 是“已不存在”,应用要按幂等语义处理。
Bulk 每个操作由 action 行和可选 source 行组成,文件末尾必须有换行:
cat > /tmp/products.ndjson <<'NDJSON'
{"index":{"_index":"products-v1","_id":"sku-1001"}}
{"product_id":"sku-1001","title":"Mechanical Keyboard","price":599,"category":"keyboard","created_at":0}
{"index":{"_index":"products-v1","_id":"sku-1002"}}
{"product_id":"sku-1002","title":"Wireless Mouse","price":199,"category":"mouse","created_at":0}
NDJSON
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/x-ndjson' -X POST "$ES_URL/_bulk?refresh=wait_for" \
--data-binary @/tmp/products.ndjson > /tmp/products-bulk.json
jq -e '.errors==false and ([.items[][].status]|all(.==200 or .==201))' /tmp/products-bulk.jsonHTTP 200 不代表每个 item 成功,必须检查 errors 和每个 action 状态。对 429 做有上限的指数退避;mapping 400、权限 403 和文档 413 不能盲重试。
用同一商品索引验证查询、聚合与 PIT 分页
全文检索用 match,精确过滤用 term,范围用 range;过滤条件放 bool.filter 可避免无意义评分:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' "$ES_URL/products-v1/_search" -d '{
"track_total_hits":true,
"query":{"bool":{"must":[{"match":{"title":"keyboard"}}],
"filter":[{"term":{"category":"keyboard"}},{"range":{"price":{"lte":1000}}}]}},
"sort":[{"_score":"desc"},{"product_id":"asc"}]
}' | jq '{timed_out,_shards,total:.hits.total,hits:[.hits.hits[]._source]}'期望 timed_out:false、_shards.failed:0。聚合使用 keyword 字段:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' "$ES_URL/products-v1/_search" -d '{
"size":0,"aggs":{"by_category":{"terms":{"field":"category","size":20},
"aggs":{"avg_price":{"avg":{"field":"price"}}}}}
}' | jq '.aggregations.by_category.buckets'分页前两页可用 from/size;深分页建立 PIT:
PIT_ID="$(curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-X POST "$ES_URL/products-v1/_pit?keep_alive=1m" | jq -r .id)"
jq -n --arg pit "$PIT_ID" '{size:100,pit:{id:$pit,keep_alive:"1m"},
sort:[{"product_id":"asc"}]}' > /tmp/pit-query.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' "$ES_URL/_search" -d @/tmp/pit-query.json > /tmp/page.json
PIT_ID="$(jq -er '.pit_id' /tmp/page.json)"
LAST_SORT="$(jq -ce '.hits.hits[-1].sort' /tmp/page.json)"每一页都把上一页最后一个 sort 数组放进 search_after,并立刻用响应中的最新 pit_id 覆盖旧值:
jq -n --arg pit "$PIT_ID" --argjson after "$LAST_SORT" '{
size:100,pit:{id:$pit,keep_alive:"1m"},search_after:$after,
sort:[{"product_id":"asc"}]}' > /tmp/pit-query.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' "$ES_URL/_search" -d @/tmp/pit-query.json > /tmp/page-2.json
PIT_ID="$(jq -er '.pit_id' /tmp/page-2.json)"若 .pit_id 或最后一条 sort 缺失,先检查 timed_out、_shards.failed 和 hits 是否已经为空,不沿用旧 PIT。完成后关闭最后一次响应的 ID,并判断释放成功:
jq -n --arg id "$PIT_ID" '{id:$id}' |
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' -X DELETE "$ES_URL/_pit" --data-binary @- |
jq -e '.succeeded==true and .num_freed>=1'用 alias、API Key 和 TLS 固定访问边界
生产配置至少明确集群名、节点名、角色、数据路径、网络、发现和 TLS。示例不是三节点完整清单:
cluster.name: search-prod
node.name: es-a
node.roles: [ master, data_hot, ingest ]
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 10.0.0.11
http.port: 9200
discovery.seed_hosts: ["10.0.0.11", "10.0.0.12", "10.0.0.13"]
xpack.security.enabled: true
xpack.security.http.ssl.enabled: true
xpack.security.transport.ssl.enabled: true三节点首次安全入群按“建立三个故障域并完成安全入群”的 enrollment 流程完成。cluster.initial_master_nodes 只允许首次引导工具在全新集群写入;形成 cluster UUID 后必须从配置删除,不能在重启、扩容或恢复时重新设置。密码、S3 凭据等安全设置写入 keystore:
sudo -u elasticsearch /usr/share/elasticsearch/bin/elasticsearch-keystore add s3.client.default.access_key
sudo -u elasticsearch /usr/share/elasticsearch/bin/elasticsearch-keystore list节点间 transport TLS 必须互信并校验,HTTP TLS 给客户端使用。查看证书:
openssl s_client -connect es.example.internal:9200 -servername es.example.internal </dev/null 2>/dev/null |
openssl x509 -noout -subject -issuer -dates -ext subjectAltNameNode.js 接入前先建立稳定读写 alias。前提是已经按“用稳定商品 ID 完成 CRUD 与批量写入”创建 products-v1,且当前身份有 alias 管理权限:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' -X POST "$ES_URL/_aliases" -d '{"actions":[
{"add":{"index":"products-v1","alias":"products-read"}},
{"add":{"index":"products-v1","alias":"products-write","is_write_index":true}}
]}' | jq -e '.acknowledged==true'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_alias/products-read,products-write" |
jq -e 'keys==["products-v1"] and .["products-v1"].aliases["products-write"].is_write_index==true'为示例创建一个一小时有效的最小权限 API Key。响应文件和环境变量只存在于当前 0700 实验目录;正式环境把 encoded key 写入密钥管理系统:
umask 077
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' -X POST "$ES_URL/_security/api_key" -d '{
"name":"products-node-demo","expiration":"1h","role_descriptors":{"products_app":{
"cluster":[],"indices":[{"names":["products-read","products-write"],
"privileges":["read","view_index_metadata","create_doc","index","delete"]}]}}}
' > "$PWD/es-lab/products-api-key.json"
export ES_API_KEY="$(jq -er '.encoded' "$PWD/es-lab/products-api-key.json")"
export ES_API_KEY_ID="$(jq -er '.id' "$PWD/es-lab/products-api-key.json")"验证正反权限:
curl --fail --silent --cacert "$ES_CA" -H "Authorization: ApiKey $ES_API_KEY" \
"$ES_URL/products-read/_search?q=*"
code="$(curl --silent --output /tmp/forbidden.json --write-out '%{http_code}' \
--cacert "$ES_CA" -H "Authorization: ApiKey $ES_API_KEY" "$ES_URL/_cluster/settings")"
test "$code" = 403
jq -e '.error.type=="security_exception"' /tmp/forbidden.json三条命令都必须返回 0;负例必须是 403 且 error.type 为 security_exception。后续应用调用统一使用 Authorization: ApiKey,不再混用用户密码。应用凭据禁止写进仓库、镜像和命令历史;轮换顺序是创建候选 key、灰度客户端、新 key 认证、撤销旧 key、确认旧 key 401。
让 mapping、analyzer 和生命周期各守边界
建模从查询反推,不从源表机械复制。精确筛选、聚合和排序使用 keyword、数值、日期或 boolean;全文相关性使用 text,需要精确排序时增加 keyword 子字段;对象数组必须保持对象关系时使用 nested,否则普通 object 会扁平化;任意键标签优先 flattened,防止字段无限增长;大正文可留在对象存储,只把检索片段与 URL 放入索引;金额用整数最小单位或 scaled_float,不要依赖二进制浮点精确账务。
用 _analyze 验证分词:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-H 'Content-Type: application/json' "$ES_URL/products-v1/_analyze" \
-d '{"field":"title","text":"Mechanical Keyboard"}' |
jq '[.tokens[].token]'日志和事件使用 data stream + index template + ILM:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_ilm/policy/logs-30d" -d '{"policy":{"phases":{
"hot":{"actions":{"rollover":{"max_primary_shard_size":"30gb","max_age":"1d"}}},
"delete":{"min_age":"30d","actions":{"delete":{}}}}}}'curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_index_template/logs-app" -d '{
"index_patterns":["logs-app-*"],"data_stream":{},"priority":500,
"template":{"settings":{"index.lifecycle.name":"logs-30d"},"mappings":{"properties":{
"@timestamp":{"type":"date"},"service":{"type":"keyword"},"message":{"type":"text"}}}}
}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -X PUT "$ES_URL/_data_stream/logs-app-prod"写 data stream 必须带 @timestamp,失败 400 时检查模板匹配、时间格式和 mapping。用以下命令观察生命周期:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_data_stream/logs-app-prod?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/logs-app-prod/_ilm/explain?pretty"完成 Node.js 商品搜索闭环
固定客户端、alias 和 API Key
前提是已经按“用 alias、API Key 和 TLS 固定访问边界”创建 alias 和 ES_API_KEY,ES_URL、ES_CA 都是绝对路径。官方客户端 9.5.0 要求 Node.js 22 或更高版本;先验证运行时,再创建独立项目并明确使用 ES module:
mkdir -p "$PWD/es-node-demo"
cd "$PWD/es-node-demo"
node --version
npm init -y
npm pkg set type=module
npm install @elastic/elasticsearch@9.5.0 --save-exact
npm ls @elastic/elasticsearch --depth=0
test -n "${ES_URL:?}" && test -r "${ES_CA:?}" && test -n "${ES_API_KEY:?}"用确定性商品 ID 完成写、搜、批处理和删除
把下面程序保存为 app.mjs。它使用同一种 API Key 认证,完成写入、搜索、Bulk 和删除,并始终关闭客户端:
import fs from 'node:fs'
import { Client } from '@elastic/elasticsearch'
const client = new Client({
node: process.env.ES_URL,
auth: { apiKey: process.env.ES_API_KEY },
tls: { ca: fs.readFileSync(process.env.ES_CA) },
requestTimeout: 3000,
maxRetries: 2,
sniffOnStart: false
})
try {
const indexed = await client.index({
index: 'products-write', id: 'sku-1003', refresh: 'wait_for',
document: { product_id: 'sku-1003', title: 'USB Hub', price: 129, category: 'adapter' }
})
if (!['created', 'updated'].includes(indexed.result)) throw new Error(`index result=${indexed.result}`)
const result = await client.search({
index: 'products-read', track_total_hits: true,
query: { match: { title: 'USB Hub' } }
})
if (result.timed_out || result._shards.failed || !result.hits.hits.some(hit => hit._id === 'sku-1003')) {
throw new Error('search semantic check failed')
}
const bulk = await client.bulk({ refresh: 'wait_for', operations: [
{ index: { _index: 'products-write', _id: 'sku-bulk' } },
{ product_id: 'sku-bulk', title: 'Bulk item', price: 99, category: 'adapter' },
{ delete: { _index: 'products-write', _id: 'sku-bulk' } }
] })
if (bulk.errors) {
const failures = bulk.items.flatMap(item => Object.values(item))
.filter(item => item.status >= 300)
.map(item => ({ status: item.status, type: item.error?.type, reason: item.error?.reason }))
throw new Error(`bulk item failures=${JSON.stringify(failures)}`)
}
const removed = await client.delete({ index: 'products-write', id: 'sku-1003', refresh: 'wait_for' })
if (removed.result !== 'deleted') throw new Error(`delete result=${removed.result}`)
console.log('index/search/bulk/delete ok')
} catch (error) {
const status = error.meta?.statusCode
const type = error.meta?.body?.error?.type
console.error({ status, type, code: error.code, message: error.message })
if (status === 401) console.error('API Key 无效、过期或发给了错误 endpoint')
else if (status === 403) console.error('API Key 缺少目标 alias 权限')
else if (status === 429) console.error('服务端过载;仅对幂等操作做有上限退避')
else if (['UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'CERT_HAS_EXPIRED', 'ERR_TLS_CERT_ALTNAME_INVALID'].includes(error.code)) {
console.error('核对 CA、证书有效期和 endpoint SAN,禁止关闭 TLS 校验')
}
process.exitCode = 1
} finally {
await client.close()
}用退出码和错误分类证明失败没有被吞掉
从项目目录直接运行:
node app.mjs正常输出是 index/search/bulk/delete ok 且退出码为 0。异常输出同时包含 HTTP status、Elasticsearch error type、TLS code 和 message:401 检查 key 与 endpoint;403 检查 alias 权限;429 只对确定性 ID 等幂等操作退避;TLS 错误核对 CA/SAN;Bulk 即使 HTTP 请求成功也会逐 item 输出 status/type/reason。
客户端内部使用持久 HTTP 连接,通常不需要应用再造“每请求一个连接池”。连接超时、请求超时和业务截止时间要分开;只对 GET、确定性 ID 的 index 等幂等操作有限重试。create、update script 和 bulk 中非幂等操作在超时后先读取状态,再决定是否重试。bulk helper 的失败文档必须进入重试或补偿队列,不能只看 Promise 成功。
从单机扩展到可演练的生产集群
建立三个故障域并完成安全入群
单节点只适合可重建实验。下面给出 DEB/RPM 三节点的连续安全入群路径。前提是 es-a/es-b/es-c 已安装相同 Elasticsearch 版本但只有 es-a 启动过,地址分别为 10.0.0.11/12/13,9200 和 9300 双向可达,file realm 未禁用。三个节点使用同一 cluster.name: search-prod,节点名分别固定,角色都设为 [ master, data_hot, ingest ];HTTP 与 transport TLS 保持开启,客户端必须校验 CA 和主机名。
先在 es-a 第一次启动前写入节点身份与可达地址,不手写安全证书或首次引导名单:
sudo tee /etc/elasticsearch/elasticsearch.yml >/dev/null <<'YAML'
cluster.name: search-prod
node.name: es-a
node.roles: [ master, data_hot, ingest ]
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 10.0.0.11
transport.host: 10.0.0.11
YAML首次启动的安全自动配置会生成 HTTP/transport TLS 材料和一次性引导设置。这里有一个可判断前提:自动生成的 cluster.initial_master_nodes 必须只包含 es-a,并与已经写入的 node.name: es-a 完全一致;否则先停止,修正节点身份后重新做全新初始化。DEB/RPM 通过 systemd 启动时不要从 journal 猜密码,服务 active 后检查这两个字段,再用官方工具生成一次 elastic 密码:
sudo systemctl enable --now elasticsearch
sudo journalctl -u elasticsearch -n 120 --no-pager
sudo grep -E '^(node.name|cluster.initial_master_nodes):' /etc/elasticsearch/elasticsearch.yml
sudo grep -Eq '^node.name:[[:space:]]*es-a[[:space:]]*$' /etc/elasticsearch/elasticsearch.yml
sudo grep -Eq '^cluster.initial_master_nodes:[[:space:]]*\[[[:space:]]*"?es-a"?[[:space:]]*\][[:space:]]*$' \
/etc/elasticsearch/elasticsearch.yml
export ES_BOOTSTRAP_PASSWORD="$(sudo /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -a -b -s)"服务必须保持 active,/etc/elasticsearch/certs/http_ca.crt 必须可读;失败先修 bootstrap check、地址、file realm 或证书权限。把 CA 安全复制到管理机的 /run/secrets/search-prod-http-ca.crt,把密码写入密钥系统后记录 cluster UUID:
export ES_URL='https://10.0.0.11:9200'
export ES_CA='/run/secrets/search-prod-http-ca.crt'
export ES_AUTH="elastic:$ES_BOOTSTRAP_PASSWORD"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/" |
jq -er '{name,cluster_name,cluster_uuid,version:.version.number}'UUID 已形成后立即从 es-a 配置删除一次性引导项,但此时保持进程运行,不在缺少 discovery.seed_hosts 时重启。配置会在三节点写入完整 seed hosts 后的逐节点重启中生效;此后所有节点都禁止再设置首次引导项:
sudo sed -i '/^cluster.initial_master_nodes:/d' /etc/elasticsearch/elasticsearch.yml
! sudo grep -q '^cluster.initial_master_nodes:' /etc/elasticsearch/elasticsearch.yml
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/" | jq -er .cluster_uuid在 es-a 分别生成两个 30 分钟有效的一次性 enrollment token,不要写入仓库或日志:
TOKEN_B="$(sudo /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s node --url "$ES_URL")"
TOKEN_C="$(sudo /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s node --url "$ES_URL")"若命令提示 file realm 禁用、无法连接或 TLS 错误,先修复该前提再生成 token。把 TOKEN_B 安全传到 es-b,在它第一次启动前重配置;随后编辑 /etc/elasticsearch/elasticsearch.yml,保留工具写入的安全设置,并把节点参数设为 cluster.name: search-prod、node.name: es-b、node.roles: [ master, data_hot, ingest ]、network.host: 10.0.0.12、transport.host: 10.0.0.12:
sudo /usr/share/elasticsearch/bin/elasticsearch-reconfigure-node --enrollment-token "$TOKEN_B"
sudo sed -i '/^cluster.initial_master_nodes:/d' /etc/elasticsearch/elasticsearch.yml
sudo systemctl enable --now elasticsearch
sudo journalctl -u elasticsearch -n 120 --no-pager在 es-c 用 TOKEN_C 执行相同的 reconfigure 与首次启动步骤,并把节点参数设为 cluster.name: search-prod、node.name: es-c、同一 roles、network.host: 10.0.0.13、transport.host: 10.0.0.13。token 过期或节点已启动过时不要硬改 data path,应停下核对状态并生成新 token。
三个节点都加入后,在 es-a/es-b/es-c 分别执行下面命令,保留 enrollment 写入的全部 TLS/security 配置,只收敛发现终态:
sudo sed -i '/^discovery.seed_hosts:/d;/^cluster.initial_master_nodes:/d' \
/etc/elasticsearch/elasticsearch.yml
printf '%s\n' \
'discovery.seed_hosts: ["10.0.0.11:9300", "10.0.0.12:9300", "10.0.0.13:9300"]' |
sudo tee -a /etc/elasticsearch/elasticsearch.yml
sudo grep -E '^(node.name|discovery.seed_hosts|cluster.initial_master_nodes):' \
/etc/elasticsearch/elasticsearch.yml
! sudo grep -q '^cluster.initial_master_nodes:' /etc/elasticsearch/elasticsearch.yml每台输出都必须有自己的唯一 node.name 和同一份三地址 discovery.seed_hosts,不能再出现 cluster.initial_master_nodes。该项只用于首次引导,已形成集群后永不重设。随后按 es-b → es-c → es-a 一次重启一个节点;每台都先确认 active 和 journal 无 join、UUID 或 TLS 错误,前一台未恢复就不动下一台:
sudo systemctl restart elasticsearch
sudo systemctl is-active --quiet elasticsearch
sudo journalctl -u elasticsearch -n 120 --no-pager从管理机严格校验三个 HTTPS endpoint 的证书,并断言它们属于同一 cluster UUID:
for host in 10.0.0.11 10.0.0.12 10.0.0.13; do
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "https://$host:9200/" |
jq -er '.cluster_uuid + " " + .version.number'
done > /tmp/es-endpoints.txt
test "$(wc -l < /tmp/es-endpoints.txt)" -eq 3
sort -u /tmp/es-endpoints.txt
test "$(sort -u /tmp/es-endpoints.txt | wc -l)" -eq 1输出必须只有一行。多行表示节点加入了不同集群,立即停止写入并检查 cluster.name、发现配置与 data path,不重新设置首次引导名单。再验证节点集合、elected master 与分片分布:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cat/nodes?format=json&h=name,ip,roles,master" > /tmp/es-nodes.json
jq -e 'length==3 and ([.[].name]|sort)==["es-a","es-b","es-c"] and
([.[]|select(.master=="*")]|length)==1' /tmp/es-nodes.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cat/master?format=json&h=node" | jq -e 'length==1 and .[0].node!=""'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cluster/health/products-v1?wait_for_status=green&timeout=2m" |
jq -e '.status=="green" and .timed_out==false and .unassigned_shards==0 and .number_of_nodes==3'建立一个 1 primary、2 replica 的维护检查索引,green 表示三份 shard 已分布到三个节点:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/ha-check" -d '{"settings":{"number_of_shards":1,"number_of_replicas":2}}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cluster/health/ha-check?wait_for_status=green&timeout=2m" |
jq -e '.status=="green" and .timed_out==false and .unassigned_shards==0'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cat/shards/ha-check?format=json&h=state,node" |
jq -e 'length==3 and ([.[].node]|unique|length)==3 and all(.[];.state=="STARTED")'高可用还要求投票多数可达、应用有多个入口或负载均衡、快照仓库独立、客户端可重连且维护演练满足 RTO。滚动维护一次只动一个节点,先限制 replica 分配:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_cluster/settings" -d '{"persistent":{"cluster.routing.allocation.enable":"primaries"}}'
sudo systemctl restart elasticsearch这里的 systemctl 只在当前维护节点执行。节点重新加入且仍是原 cluster UUID 后恢复分配:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_cluster/settings" -d '{"persistent":{"cluster.routing.allocation.enable":null}}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cluster/health?wait_for_status=green&timeout=5m" |
jq -e '.status=="green" and .timed_out==false and .unassigned_shards==0 and .number_of_nodes==3'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cluster/health/products-v1,ha-check?wait_for_status=green&timeout=2m" |
jq -e '.status=="green" and .timed_out==false and .unassigned_shards==0'第一个节点未恢复 green、节点数不是 3、elected master 不稳定或业务 SLO 恶化时停止维护,不处理下一个。进程起不来先查看日志,不删除 data path,也不重设首次引导参数。演练结束删除 ha-check。
用 shard 分配门禁完成扩容和缩容
扩容前确认磁盘水位、分片尺寸、网络、节点版本和角色。新 data 节点加入后先观察分配:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/allocation?v"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cluster/pending_tasks?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/recovery?v&active_only=true"分片恢复会消耗网络与磁盘。延迟或错误率逼近 SLO 时先降低恢复并发或暂停业务扩流,不要同时执行大规模 force merge、快照和迁移。
缩容不能直接关机。先把目标节点排除:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_cluster/settings" -d '{
"persistent":{"cluster.routing.allocation.exclude._name":"es-data-3"}}'等待目标节点无 shard:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cat/shards?format=json&h=index,shard,prirep,state,node" |
jq -e '[.[]|select(.node=="es-data-3")]|length==0'然后停止节点,并清除排除规则:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_cluster/settings" -d '{
"persistent":{"cluster.routing.allocation.exclude._name":null}}'若节点同时是 master-eligible,要先确认剩余投票配置仍满足多数。缩容后重新计算容量和单节点故障余量。
用快照和业务对账证明恢复
快照是增量、按 segment 去重的集群备份。仓库必须位于独立故障域,并且一个仓库只能由一个集群写;其他集群以只读方式注册。容器实验使用共享 volume,仅学习 API,不能计入生产 RPO。
固定仓库、业务位点和快照身份
注册文件系统仓库并验证:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_snapshot/lab-repo" -d '{
"type":"fs","settings":{"location":"/mnt/backups","compress":true}}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-X POST "$ES_URL/_snapshot/lab-repo/_verify" | jq -e '.nodes|length>=1'先冻结权威源的 checkpoint,记录源数据库位点、源 cluster UUID、索引名和应用版本,再创建快照:
export CHECKPOINT='orders-partition-0:18446744073709551620'
export SNAPSHOT="products-$(date -u +%Y%m%dT%H%M%SZ)"
SOURCE_UUID="$(curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/" | jq -er .cluster_uuid)"
SOURCE_VERSION="$(curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/" | jq -er .version.number)"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_snapshot/lab-repo/$SNAPSHOT?wait_for_completion=true" -d "{
\"indices\":\"products-v1\",\"include_global_state\":false,
\"feature_states\":[\"none\"],
\"metadata\":{\"checkpoint\":\"$CHECKPOINT\",\"source_uuid\":\"$SOURCE_UUID\",
\"source_version\":\"$SOURCE_VERSION\",\"purpose\":\"restore-drill\"}}
" > /tmp/snapshot.json
SNAPSHOT_UUID="$(jq -er '.snapshot.uuid' /tmp/snapshot.json)"
jq -e --arg uuid "$SOURCE_UUID" --arg cp "$CHECKPOINT" '.snapshot.state=="SUCCESS" and
.snapshot.shards.failed==0 and .snapshot.metadata.source_uuid==$uuid and
.snapshot.metadata.checkpoint==$cp and (.snapshot.uuid|length>0)' /tmp/snapshot.json保存 snapshot name、UUID、版本、indices、metadata 和 shard failures。PARTIAL 不算成功;先修 primary、仓库权限或网络,再创建新快照。
恢复目标必须是独立空白集群或独立数据卷,不能复用源 data path。源集群在演练期间保持隔离且不切业务流量。先在源端导出控制面参考;这些 GET 响应用于核对,目标重建使用后文就地定义的短 JSON 文件:
install -d -m 700 /tmp/products-control
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_index_template" > /tmp/products-control/index-templates.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_component_template" > /tmp/products-control/component-templates.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_ilm/policy" > /tmp/products-control/ilm.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_ingest/pipeline" > /tmp/products-control/pipelines.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_alias/products-read,products-write" > /tmp/products-control/aliases.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_security/role" > /tmp/products-control/roles.jsonAPI Key 的秘密值不能从快照导出;目标集群要按“用 alias、API Key 和 TLS 固定访问边界”重新创建最小权限 key。产品索引快照显式使用 feature_states:["none"],因此不会恢复安全或 Kibana 系统索引。若需要 Kibana 状态,应单独创建包含 kibana feature state 的快照;若选择 Saved Objects 导出,则先验证 spaces、加密密钥和依赖对象,再通过 Kibana 官方 API 导入。
| 控制面 | 源端保留 | 空白目标的恢复路径 | 通过条件 |
|---|---|---|---|
| index/component templates | GET 导出与版本库 PUT 请求体 | 依赖顺序先 component、后 index template | 名称集合与关键 settings/mappings 一致 |
| ILM | GET policy 与版本库 PUT 请求体 | 先建 policy,再建引用它的 template | policy phase 与引用名称一致 |
| ingest pipeline | GET pipeline 与版本库 PUT 请求体 | 在首笔写入前 PUT pipeline | processors 顺序、版本与模拟结果一致 |
| alias | GET 完整 alias JSON | 数据与控制面验证后调用 _aliases | 唯一 write index、filter/routing 一致 |
| roles 与 API Key | GET role 定义;秘密值不导出 | 重建最小 role,重新签发 API Key | 应用正向操作成功、控制面返回 403 |
| feature states | 快照中显式列出所需 feature state | 只在确认覆盖影响后恢复 | 对应系统功能健康;未选择的 feature 不变 |
在隔离目标恢复数据而不碰源集群
下面用独立 Docker 网络、独立 data volume 和本地 19200 端口建立一次性空白目标。它不加入源网络,只把源快照 volume 以只读方式挂载;每次演练使用唯一容器与数据卷名:
export RESTORE_RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
export TARGET_CONTAINER="es-restore-$RESTORE_RUN_ID"
export TARGET_DATA="es-restore-data-$RESTORE_RUN_ID"
export TARGET_PASSWORD="$(openssl rand -base64 32 | tr -d '\n')"
docker network inspect es-restore-net >/dev/null 2>&1 || docker network create es-restore-net
docker volume create "$TARGET_DATA"
docker run -d --name "$TARGET_CONTAINER" --network es-restore-net \
-p 127.0.0.1:19200:9200 --memory 2g \
-v "$TARGET_DATA:/usr/share/elasticsearch/data" \
-v es-lab-backups:/mnt/backups:ro \
-e "ELASTIC_PASSWORD=$TARGET_PASSWORD" -e discovery.type=single-node \
-e ES_JAVA_OPTS='-Xms1g -Xmx1g' "$ES_IMAGE_DIGEST" -E path.repo=/mnt/backups等待最多两分钟,并同时检查容器退出、TLS、独立 UUID 和同版本兼容:
export TARGET_ES_URL='https://127.0.0.1:19200'
export TARGET_ES_CA="$PWD/es-lab/$TARGET_CONTAINER-http_ca.crt"
export TARGET_ES_AUTH="elastic:$TARGET_PASSWORD"
ready=false
for attempt in $(seq 1 60); do
test "$(docker inspect -f '{{.State.Running}}' "$TARGET_CONTAINER")" = true || break
docker cp "$TARGET_CONTAINER:/usr/share/elasticsearch/config/certs/http_ca.crt" \
"$TARGET_ES_CA" 2>/dev/null || { sleep 2; continue; }
if curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
"$TARGET_ES_URL/" > /tmp/target-root.json; then ready=true; break; fi
sleep 2
done
if test "$ready" != true; then docker logs "$TARGET_CONTAINER" --tail 200; exit 1; fi
jq -e --arg source "$SOURCE_UUID" --arg version "$SOURCE_VERSION" \
'.cluster_uuid!=$source and .version.number==$version' /tmp/target-root.json目标版本必须在官方快照兼容范围内;本例要求与源精确同版本。UUID 相同、容器退出或超时都停止演练,源入口和业务路由保持不变。把同一仓库以只读方式注册到目标,防止两个集群同时写仓库:
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
-H 'Content-Type: application/json' -X PUT "$TARGET_ES_URL/_snapshot/lab-repo" \
-d '{"type":"fs","settings":{"location":"/mnt/backups","readonly":true}}' |
jq -e '.acknowledged==true'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
"$TARGET_ES_URL/_snapshot/lab-repo/$SNAPSHOT?verbose=true" > /tmp/target-snapshot.json
jq -e --arg cp "$CHECKPOINT" --arg snapshot_uuid "$SNAPSHOT_UUID" '.snapshots|length==1 and
.[0].state=="SUCCESS" and .[0].shards.failed==0 and .[0].uuid==$snapshot_uuid and
.[0].metadata.checkpoint==$cp and
.[0].indices==["products-v1"]' /tmp/target-snapshot.jsonUUID、metadata、索引集合或 shard 结果不符就停止,不选择相似名称的快照。空白目标没有同名索引后再恢复:
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
-H 'Content-Type: application/json' \
-X POST "$TARGET_ES_URL/_snapshot/lab-repo/$SNAPSHOT/_restore?wait_for_completion=true" \
-d '{"indices":"products-v1","include_global_state":false,"include_aliases":false}' \
> /tmp/restore-response.json
jq -e '.snapshot.shards.failed==0 and .snapshot.shards.successful>=1 and
.snapshot.shards.total==.snapshot.shards.successful' /tmp/restore-response.json按依赖顺序重建控制面
为演示依赖恢复,在 /tmp/products-control 就地定义五个短 JSON 文件。生产把同样的 PUT 请求体放进配置版本库;API Key 的秘密值仍然重新签发:
jq -n '{template:{mappings:{properties:{product_id:{type:"keyword"}}}},version:1}' \
> /tmp/products-control/component.put.json
jq -n '{policy:{phases:{hot:{actions:{}},delete:{min_age:"365d",actions:{delete:{}}}}}}' \
> /tmp/products-control/ilm.put.json
jq -n '{index_patterns:["products-*"],composed_of:["products-base"],priority:200,
template:{settings:{"index.lifecycle.name":"products-retention"}}}' \
> /tmp/products-control/template.put.json
jq -n '{version:1,processors:[{set:{field:"restore_control",value:"v1"}}]}' \
> /tmp/products-control/pipeline.put.json
jq -n '{cluster:[],indices:[{names:["products-read","products-write"],
privileges:["read","view_index_metadata","create_doc","index","delete"]}]}' \
> /tmp/products-control/role.put.json按依赖顺序执行 component template → ILM → index template → ingest pipeline → role;任一 PUT 未 acknowledged 就停止,不提前创建 alias:
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$TARGET_ES_URL/_component_template/products-base" -d @/tmp/products-control/component.put.json | jq -e '.acknowledged'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$TARGET_ES_URL/_ilm/policy/products-retention" -d @/tmp/products-control/ilm.put.json | jq -e '.acknowledged'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$TARGET_ES_URL/_index_template/products-template" -d @/tmp/products-control/template.put.json | jq -e '.acknowledged'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$TARGET_ES_URL/_ingest/pipeline/products-normalize" -d @/tmp/products-control/pipeline.put.json | jq -e '.acknowledged'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$TARGET_ES_URL/_security/role/products_app_restore" -d @/tmp/products-control/role.put.json |
jq -e 'has("role") and (.role.created|type=="boolean")'逐项 GET 名称和关键内容,防止只创建了同名空对象:
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" "$TARGET_ES_URL/_component_template/products-base" |
jq -e '.component_templates[0].name=="products-base" and .component_templates[0].component_template.version==1'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" "$TARGET_ES_URL/_ilm/policy/products-retention" |
jq -e '.["products-retention"].policy.phases.delete.min_age=="365d"'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" "$TARGET_ES_URL/_index_template/products-template?flat_settings=true" |
jq -e '.index_templates[0].index_template.composed_of==["products-base"] and .index_templates[0].index_template.template.settings["index.lifecycle.name"]=="products-retention"'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" "$TARGET_ES_URL/_ingest/pipeline/products-normalize" |
jq -e '.["products-normalize"].version==1 and .["products-normalize"].processors[0].set.field=="restore_control"'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" "$TARGET_ES_URL/_security/role/products_app_restore" |
jq -e '.["products_app_restore"].indices[0].names==["products-read","products-write"] and (.["products_app_restore"].indices[0].privileges|index("delete"))'全部控制面断言通过后再建立 alias,避免应用提前写入:
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
-H 'Content-Type: application/json' -X POST "$TARGET_ES_URL/_aliases" -d '{"actions":[
{"add":{"index":"products-v1","alias":"products-read"}},
{"add":{"index":"products-v1","alias":"products-write","is_write_index":true}}
]}' | jq -e '.acknowledged==true'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
"$TARGET_ES_URL/_alias/products-read,products-write" |
jq -e 'keys==["products-v1"] and .["products-v1"].aliases["products-write"].is_write_index==true'用摘要、alias 和最小权限 canary 完成验收
比较 mapping、总量和数据摘要。示例索引不足 1000 条;更大索引使用“用同一商品索引验证查询、聚合与 PIT 分页”的 PIT + search_after 遍历全部 _id 与规范化 _source:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/products-v1/_mapping" |
jq -S '.["products-v1"].mappings' > /tmp/source.mapping.json
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" "$TARGET_ES_URL/products-v1/_mapping" |
jq -S '.["products-v1"].mappings' > /tmp/target.mapping.json
cmp /tmp/source.mapping.json /tmp/target.mapping.jsoncurl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/products-v1/_search?sort=product_id&size=1000&track_total_hits=true" > /tmp/source.search.json
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
"$TARGET_ES_URL/products-v1/_search?sort=product_id&size=1000&track_total_hits=true" > /tmp/target.search.json
jq -e '.timed_out==false and ._shards.failed==0' /tmp/source.search.json
jq -e '.timed_out==false and ._shards.failed==0' /tmp/target.search.json
SOURCE_DIGEST="$(jq -cS '{total:.hits.total.value,docs:[.hits.hits[]|{id:._id,source:._source}]}' \
/tmp/source.search.json | sha256sum | awk '{print $1}')"
TARGET_DIGEST="$(jq -cS '{total:.hits.total.value,docs:[.hits.hits[]|{id:._id,source:._source}]}' \
/tmp/target.search.json | sha256sum | awk '{print $1}')"
test -n "$SOURCE_DIGEST" && test "$SOURCE_DIGEST" = "$TARGET_DIGEST"摘要包含总量、全部 ID 与规范化文档;两端请求超时或 shard 失败时 jq 不产出有效摘要,命令必须停止。最后在目标重新创建一小时最小权限 API Key:
umask 077
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
-H 'Content-Type: application/json' -X POST "$TARGET_ES_URL/_security/api_key" -d '{
"name":"products-restore-drill","expiration":"1h","role_descriptors":{"products_app":{
"cluster":[],"indices":[{"names":["products-read","products-write"],
"privileges":["read","view_index_metadata","create_doc","index","delete"]}]}}}
' > /tmp/target-products-api-key.json
export TARGET_API_KEY="$(jq -er .encoded /tmp/target-products-api-key.json)"用它经正式 alias 写入、读取和删除 canary,并确认真实写入落在 products-v1:
curl --fail --silent --cacert "$TARGET_ES_CA" -H "Authorization: ApiKey $TARGET_API_KEY" \
-H 'Content-Type: application/json' -X PUT \
"$TARGET_ES_URL/products-write/_doc/restore-canary?refresh=wait_for" -d '{"product_id":"restore-canary"}' |
jq -e '.result=="created" or .result=="updated"'
curl --fail --silent --cacert "$TARGET_ES_CA" -H "Authorization: ApiKey $TARGET_API_KEY" \
"$TARGET_ES_URL/products-read/_doc/restore-canary" | jq -e '.found==true'
curl --fail --silent --cacert "$TARGET_ES_CA" -u "$TARGET_ES_AUTH" \
"$TARGET_ES_URL/products-v1/_doc/restore-canary" | jq -e '.found==true'
curl --fail --silent --cacert "$TARGET_ES_CA" -H "Authorization: ApiKey $TARGET_API_KEY" \
-X DELETE "$TARGET_ES_URL/products-write/_doc/restore-canary?refresh=wait_for" |
jq -e '.result=="deleted"'最小权限身份访问控制面必须得到 403,而不是 200 或 401:
code="$(curl --silent --output /tmp/target-forbidden.json --write-out '%{http_code}' \
--cacert "$TARGET_ES_CA" -H "Authorization: ApiKey $TARGET_API_KEY" \
"$TARGET_ES_URL/_cluster/settings")"
test "$code" = 403
jq -e '.error.type=="security_exception"' /tmp/target-forbidden.json全部通过后才允许独立切流;失败时删除目标 canary 或整个目标数据卷,源集群保持不变。目标 API Key 在演练后通过 invalidate API 撤销。不再需要目标时删除唯一容器和数据卷;快照仓库 volume 继续保留:
docker rm -f "$TARGET_CONTAINER"
docker volume rm "$TARGET_DATA"
docker network rm es-restore-net
rm -f "$TARGET_ES_CA" /tmp/target-products-api-key.json /tmp/target-forbidden.json
unset TARGET_PASSWORD TARGET_ES_AUTH TARGET_API_KEY TARGET_CONTAINER TARGET_DATA RESTORE_RUN_ID用监控和容量模型约束运行状态
建立监控、告警与值班入口
监控按业务、集群、节点、索引、分片下钻。原生命令:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cluster/health?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cluster/stats?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_nodes/stats/jvm,fs,os,process,indices,thread_pool?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/indices?v&s=store.size:desc"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/shards?v&s=store:desc"至少告警:
| 层级 | 指标 | 告警含义 |
|---|---|---|
| 业务 | 搜索 P95/P99、错误率、零结果率、索引延迟 | 用户体验或数据链路异常 |
| 集群 | status、unassigned shards、pending tasks | primary/replica 或控制面异常 |
| 节点 | heap、GC、CPU、磁盘、文件描述符 | 容量或资源争用 |
| 写入 | bulk item 429/4xx、indexing latency、rejections | 过载或数据错误 |
| 查询 | search latency、timeouts、task backlog | 慢查询或扇出过大 |
| 恢复 | snapshot age/state、recovery time | RPO/RTO 失守 |
| 生命周期 | ILM error、数据层容量 | rollover/迁移/删除停滞 |
短时诊断 hot threads 和 tasks:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_nodes/hot_threads?threads=3"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_tasks?detailed=true&actions=*search*"业务恢复不能只看 green,还要确认真实身份读写、索引延迟、查询结果和错误率回到阈值。
从商品索引和真实查询推导容量
容量以真实索引验证,不用原始 JSON 大小直接推算:
热数据主分片存储 = 每日索引字节 × 热保留天数
集群存储 = 主分片存储 × (1 + replica 数) × 增长与水位余量
节点数 = max(存储约束, CPU 约束, heap/分片约束, 故障余量)先抽取代表数据建立完整 mapping,按真实写查比例压测,再读取:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/indices/products-v1?v&bytes=gb"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/products-v1/_stats/store,docs,segments?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_nodes/stats/jvm,fs,indices?pretty"常见起点是让单 primary shard 落在几十 GiB 范围,但没有适用于所有负载的固定 shard 大小。以恢复时间、查询并行度、merge、heap 和节点故障后的重分配验证。shard 太小会增加 cluster state 与 heap,太大则恢复慢、单 shard 热点明显。
调优顺序:建立 P50/P95/P99、吞吐和资源基线;定位慢查询、热点 shard、mapping 和 bulk;修改一项;用同等数据回归。优先减少无关字段、查询 shards、返回字段、深分页和高基数桶,再考虑加机器。不要把 heap 配满物理内存,文件缓存对 Lucene 同样重要。
离线压测可用 Elastic Rally,目标必须是隔离集群:
esrally race --track=geonames --pipeline=benchmark-only \
--target-hosts=127.0.0.1:9200 --client-options="use_ssl:true,verify_certs:true,ca_certs:'$ES_CA'"官方 track 只用于工具基线,容量结论必须使用真实 mapping、文档、查询比例、bulk、refresh、replica 和硬件。
通过双索引和滚动升级演进
用 Alias、CDC 与终端位点切换 products-v2
用稳定 alias 隔离物理索引。“用 alias、API Key 和 TLS 固定访问边界”已经完成初始化;迁移前先确认当前唯一落点,不能重复添加来掩盖错误状态:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_alias/products-read,products-write" |
jq -e 'keys==["products-v1"] and .["products-v1"].aliases["products-write"].is_write_index==true'迁移先建立使用新 mapping/settings 的 products-v2,记录源数据库权威起始 checkpoint,再从固定边界按稳定 _id 全量导入。随后持续消费 CDC,事件至少包含 operation=index|delete、partition 和十进制字符串 offset;每个 partition 严格按 offset 推进,upsert 覆盖目标,delete 持久化 tombstone 并删除文档,旧事件不得让文档复活。切换窗口停止旧入口写入,记录终止 checkpoint,把窗口内事件回放到 v2;比较同一边界下的全量 ID、规范化文档 digest、删除 tombstone、终端 offset 和代表查询后,原子切换 alias,并以普通应用身份完成写、读、删。
切换前保存完整 alias JSON:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_alias/products-read,products-write" |
jq -S '.' > /tmp/products-alias-before.json停写并完成终端回放后切换,生产 remove 使用 must_exist:true:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X POST "$ES_URL/_aliases" -d '{"actions":[
{"remove":{"index":"products-v1","alias":"products-read","must_exist":true}},
{"remove":{"index":"products-v1","alias":"products-write","must_exist":true}},
{"add":{"index":"products-v2","alias":"products-read"}},
{"add":{"index":"products-v2","alias":"products-write","is_write_index":true}}
]}' | jq -e '.acknowledged==true'请求失败或响应丢失时先 GET alias:完整等于切换前则可重试,完整等于切换后则继续验证,其他状态保持停写并人工恢复。不能把 alias 同时指向两个可写落点。切换后的 canary:
curl --fail --silent --cacert "$ES_CA" -H "Authorization: ApiKey $ES_API_KEY" \
-H 'Content-Type: application/json' -X PUT \
"$ES_URL/products-write/_doc/cutover-canary?refresh=wait_for" \
-d '{"product_id":"cutover-canary","title":"cutover canary","price":1,"category":"test"}'
curl --fail --silent --cacert "$ES_CA" -H "Authorization: ApiKey $ES_API_KEY" \
"$ES_URL/products-read/_doc/cutover-canary"
curl --fail --silent --cacert "$ES_CA" -H "Authorization: ApiKey $ES_API_KEY" \
-X DELETE "$ES_URL/products-write/_doc/cutover-canary?refresh=wait_for"还要确认物理 products-v2 包含 canary、products-v1 不包含。回滚前再次停写,把切换窗口的 index 与 delete 事件回放到 v1,完成同样对账,再按相反 alias actions 切回;不能只切 alias 而丢掉新写。
逐节点升级并保留独立恢复回退
升级输入必须固定批准的 source/target,不从运行时下载页改变目标。示例变量只是说明,执行时替换为本次批准版本,并先核对 breaking changes、deprecation、支持矩阵和插件兼容:
export SOURCE_VERSION='9.5.1'
export TARGET_VERSION='9.5.2'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/" |
jq '{cluster_uuid,version:.version.number}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_migration/deprecations?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/plugins?v"升级顺序固定为 Elasticsearch → Kibana → APM Server → ingest components。每一阶段完成验证后才能进入下一阶段。开始前所有 Elasticsearch 节点必须精确为 source、集群 green、无长期 pending task;创建成功快照并记录 snapshot UUID、源 cluster UUID、索引与 CDC checkpoint;保存 mapping、templates、ILM、SLM、pipelines、roles 和插件清单;用普通应用 API Key 跑写、读、删与代表查询,并保存全量 ID 与文档摘要。
先滚动升级 Elasticsearch。一次只处理一个节点,数据节点按 frozen、cold、warm、hot 顺序,再处理其他 data 节点和非 master-eligible 节点,master-eligible 节点最后。每个节点先限制 replica 分配,停止服务,安装已校验的 target 包与同版本插件,再启动:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_cluster/settings" -d '{"persistent":{"cluster.routing.allocation.enable":"primaries"}}'
sudo systemctl stop elasticsearch
sudo apt-get install "elasticsearch=$TARGET_VERSION"
sudo systemctl start elasticsearchRPM 主机把安装命令替换为固定版本的 sudo dnf upgrade elasticsearch-$TARGET_VERSION。节点未加入、日志出现 join rejection、插件不匹配或接口错误率超过 SLO 时立即停止该阶段,不升级下一节点。节点加入后恢复分配并等到 green:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_cluster/settings" -d '{"persistent":{"cluster.routing.allocation.enable":null}}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cluster/health?wait_for_status=green&timeout=5m" |
jq -e '.status=="green" and .timed_out==false and .unassigned_shards==0'升级窗口只允许 source 与 target 两个版本。全部 Elasticsearch 节点完成后,精确断言目标版本、green、插件版本、代表查询与业务 SLO:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/nodes?format=json&h=name,version,roles" |
jq -e --arg target "$TARGET_VERSION" 'length>0 and all(.[];.version==$target)'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cluster/health" |
jq -e '.status=="green" and .unassigned_shards==0'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/plugins?format=json" |
jq -e --arg target "$TARGET_VERSION" 'all(.[];.version==$target)'Elasticsearch 全部通过后才升级 Kibana。升级前另建一个包含 kibana feature state 的成功快照;也可以用 Kibana Saved Objects API 导出需要的 spaces 对象,但不能把文件系统复制当备份:
export KIBANA_SNAPSHOT="kibana-before-${TARGET_VERSION}"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/_snapshot/lab-repo/$KIBANA_SNAPSHOT?wait_for_completion=true" \
-d '{"indices":"-*","include_global_state":false,"feature_states":["kibana"]}' |
jq -e '.snapshot.state=="SUCCESS" and .snapshot.shards.failed==0 and (.snapshot.uuid|length>0)'Kibana 不支持混合版本滚动升级:先停止所有旧 Kibana 实例,确认旧进程和入口都已停止,再在每台主机安装与 Elasticsearch 相同的 target。管理机先定义严格 TLS 连接变量:
export KIBANA_URL='https://kibana.example.internal:5601'
export KIBANA_CA='/run/secrets/kibana-http-ca.crt'
export KIBANA_AUTH='elastic:替换为管理密码'所有旧实例停止后再安装、启动并验证:
sudo systemctl stop kibana
sudo apt-get install "kibana=$TARGET_VERSION"
sudo systemctl start kibana
sudo systemctl is-active --quiet kibana
curl --fail --silent --cacert "$KIBANA_CA" -u "$KIBANA_AUTH" \
"$KIBANA_URL/api/status" | jq -e '.status.overall.level=="available"'这些命令要在所有 Kibana 主机分阶段执行:所有旧实例都停止后才能安装和启动新版本,不能让旧、新 Kibana 并存。Saved Objects migration 失败、状态不是 available、登录/空间/仪表板异常时停止,不升级 APM。
Kibana 完成后升级 APM Server,并验证 intake、错误率与事件进入目标索引;然后才升级 ingest components,包括 Fleet Server、Elastic Agent、Beats、Logstash 和自建采集客户端,逐类按兼容矩阵验证输入、pipeline 与目标索引。任何阶段的版本、health、SLO、普通身份 CRUD/delete、代表查询或终端 CDC offset 不明确,都停止在该阶段并保持旧入口可用。
全部阶段完成后再次检查 templates、ILM/SLM、pipelines、roles、feature states、插件、应用写读删、代表查询、终端 CDC offset,以及升级前后的全量 ID 与文档摘要。
回退有两条路径:节点尚未升级且未发生新格式写入时,按官方兼容边界停止;已经升级并接收写入后,不能把数据目录交给旧二进制。应恢复升级前快照到独立旧版本兼容集群,再把快照 checkpoint 之后的权威 CDC 事件(含 delete)回放到停写时终端 offset,完成全量 ID、摘要和业务验证后切流。原升级集群保持隔离,直到确认不会再承载写入。
只清理本次实验的索引、凭据与容器
先确认 endpoint 与 cluster UUID 是本实验,再删除实验对象:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/" |
jq '{cluster_name,cluster_uuid,version:.version.number}'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-X DELETE "$ES_URL/products-v1,products-v2,restore-products-v1" || true
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-X DELETE "$ES_URL/_data_stream/logs-app-prod" || true撤销实验 API Key,再删除模板、ILM 和仓库:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X DELETE "$ES_URL/_security/api_key" -d "{\"ids\":[\"$ES_API_KEY_ID\"]}" |
jq -e --arg id "$ES_API_KEY_ID" '.invalidated_api_keys|index($id)'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -X DELETE "$ES_URL/_index_template/logs-app"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -X DELETE "$ES_URL/_ilm/policy/logs-30d"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -X DELETE "$ES_URL/_snapshot/lab-repo"停止容器并保留 volume:
docker rm -f es-lab
docker network rm es-lab-net
unset ES_AUTH ELASTIC_PASSWORD ES_BOOTSTRAP_PASSWORD ES_API_KEY ES_API_KEY_ID TARGET_ES_AUTH TARGET_API_KEY
rm -f "$PWD/es-lab/products-api-key.json" /tmp/products.ndjson /tmp/products-bulk.json
rm -f /tmp/es-root.json /tmp/pit-query.json /tmp/page.json /tmp/page-2.json只有明确不再需要任何实验数据和快照时才删 volume,先二次确认名称:
ES_LAB_DELETE_DIR="$(realpath -e -- "$PWD/es-lab")" &&
test "$ES_LAB_DELETE_DIR" = "$PWD/es-lab" &&
docker volume inspect es-lab-data es-lab-backups &&
if read -r -p '输入 DELETE-ES-LAB-VOLUMES 才删除卷: ' answer &&
test "$answer" = DELETE-ES-LAB-VOLUMES; then
docker volume rm es-lab-data es-lab-backups &&
rm -rf -- "$ES_LAB_DELETE_DIR"
else
printf '未确认删除,保留实验卷和目录。\n'
fi在原实验的父目录执行,先核对上面解析的绝对路径和卷名确属本次实验。路径检查或卷检查失败时不进入确认分支;确认词错误、空输入或 EOF 都保留卷和目录。卷删除失败时也保留本地恢复材料。生产清理必须按备份保留、审计和变更流程执行,不能照搬实验删除命令。
四、问题处理
排障先记录故障开始时间、业务影响、endpoint、cluster UUID 和最近变更,再从只读证据开始。下面每个问题都给出恢复标准;green、HTTP 200 或进程 active 只能证明局部状态。
TLS 握手、401 或 403
现象
客户端出现 certificate_unknown、hostname mismatch、401 security_exception 或 403 forbidden。
影响
TLS 失败时请求进不了认证层;401 表示认证失败;403 表示身份已识别但权限不足。错误地混用三者会导致停服或给应用过大权限。
常见根因
客户端信错 CA、证书 SAN 不含 endpoint、证书过期;密码/API key 失效;角色缺少目标 alias 的权限;客户端连接到了另一个集群。
定位顺序
先证书链和 SAN,再 endpoint 与 cluster UUID,再认证身份,最后检查具体权限。
定位命令
openssl s_client -connect es.example.internal:9200 -servername es.example.internal </dev/null 2>/dev/null |
openssl x509 -noout -issuer -subject -dates -ext subjectAltName
curl --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_security/_authenticate" | jq .
curl --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
"$ES_URL/_security/user/_has_privileges" -d '{"index":[{"names":["products-read"],"privileges":["read"]}]}' | jq .输出判断
证书 issuer、有效期和 SAN 必须匹配;authenticate 应返回预期用户名;has_all_requested:false 说明权限缺失。curl 的 HTTP code 要与 JSON error 一起看。
解决步骤
TLS 故障切回仍可信的旧证书或正确 endpoint,不使用 -k;401 从密钥系统重新发布有效凭据;403 只补目标操作所需 privilege。轮换时先验证新凭据,再撤销旧凭据。
验证
严格 TLS 下应用读写成功;旧凭据返回 401;应用对 _cluster/settings 仍返回 403。
预防
证书到期告警、双 CA 轮换、角色即代码、定期 _has_privileges 正反测试,并在配置中绑定 cluster UUID。
连接拒绝或节点启动失败
现象
curl 报 connection refused,systemd 反复重启,容器退出,应用连接池耗尽。
影响
单节点不可用;多节点可能失去数据副本或投票多数,继续重启会放大恢复流量。
常见根因
配置语法或权限错误、端口占用、磁盘满、vm.max_map_count 过低、heap 超出容器限制、插件版本不匹配、证书或 keystore 无法读取。
定位顺序
先进程和日志,再监听端口,再磁盘/内存/内核参数,最后检查配置、插件和证书。
定位命令
systemctl status elasticsearch --no-pager
journalctl -u elasticsearch -n 200 --no-pager
ss -lntp | grep -E ':9200|:9300'
df -h; free -h; sysctl vm.max_map_count
/usr/share/elasticsearch/bin/elasticsearch-plugin list输出判断
日志首个异常通常比 systemd 的最终退出码更有价值。address already in use、水位/只读、bootstrap check、插件版本和证书权限分别指向不同修复。
解决步骤
保留 data path 与日志;修正首个明确错误;确认属主与权限;插件必须与节点版本精确一致;一次只恢复一个节点。禁止删除数据目录或重新设置 cluster.initial_master_nodes 来“修启动”。
验证
进程持续运行,9200/9300 监听,节点加入原 cluster UUID,分片恢复且真实应用请求成功。
预防
升级前做配置和插件检查,监控磁盘/heap,保留启动日志,滚动重启并设置单节点失败停止条件。
选不出 elected master 或频繁切换
现象
出现 master_not_discovered_exception,cluster state 更新停滞,master 标记频繁变化。
影响
索引创建、mapping、分片分配和多数写操作受阻;局部查询偶尔成功不代表集群安全。
常见根因
投票节点不足或网络分区、9300 不通、master 节点长 GC/磁盘卡顿、节点名或发现配置错误、错误地重新引导集群。
定位顺序
先确认三台投票节点进程与 9300 互通,再看 election 日志、GC 和磁盘,最后核对 discovery 与历史 cluster UUID。
定位命令
curl --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/nodes?v&h=name,ip,node.role,master,heap.percent,cpu"
curl --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cluster/pending_tasks?pretty"
nc -vz 10.0.0.12 9300
journalctl -u elasticsearch --since '-20 min' --no-pager | tail -n 300输出判断
必须有投票多数;日志中的 election、join rejection、cluster UUID mismatch、GC 和 fsync 超时决定下一步。UUID mismatch 不是普通网络故障。
解决步骤
恢复原投票节点与 transport 网络;隔离错误 UUID 节点;缓解 elected master 候选节点的资源争用;不要删除 voting configuration,不要在已有集群重设首次引导名单。
验证
elected master 在观察窗口内稳定,pending tasks 清空,cluster state 连续发布,业务写读和索引管理恢复。
预防
三个投票节点跨故障域、独立资源、稳定时钟与网络;限制 shard/field 膨胀;保留每次部署的 cluster UUID。
集群 red 或 primary 未分配
现象
_cluster/health 为 red,部分索引读写返回 shard unavailable。
影响
至少一个 primary 不可用,对应数据可能不可读写,存在超过 RPO 的风险。
常见根因
数据节点离线、磁盘水位、allocation filter/tier 冲突、损坏、没有有效 shard copy、恢复仓库不可用。
定位顺序
先找 red 索引和 primary,再对一个具体 shard 执行 allocation explain,最后核对节点、磁盘和有效副本。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cat/indices?format=json&health=red&h=health,index,pri,rep,docs.count,store.size" | jq .
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
"$ES_URL/_cluster/allocation/explain" -d '{"index":"affected-index","shard":0,"primary":true}' | jq .输出判断
重点看 current_state、can_allocate、allocate_explanation 和每节点 decider。no_valid_shard_copy 表示不能靠调整规则找回数据。
解决步骤
优先恢复原节点、磁盘或正确的 allocation 约束;没有有效 copy 时从最后成功快照恢复。allocate_stale_primary 与 allocate_empty_primary 会接受数据丢失,只能在明确 RPO 决策后使用。
验证
primary active,索引读写恢复;总量、全量 ID 与文档摘要或权威源对账满足 RPO;应用错误率回落。
预防
副本跨故障域、独立快照、定期恢复演练、allocation 变更审计和磁盘水位提前告警。
集群 yellow 或 replica 未分配
现象
集群为 yellow,primary 可用但存在 unassigned replica。
影响
当前仍可服务,但再失去一个包含 primary 的节点可能转 red;维护与升级风险上升。
常见根因
单节点却配置 replica 1、节点数不足、same-shard 约束、tier/filter 冲突、磁盘水位或 delayed allocation。
定位顺序
先判断是否是明确单节点实验,再看未分配 shard 和 allocation explain,最后检查容量与故障域。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
"$ES_URL/_cat/shards?format=json&h=index,shard,prirep,state,unassigned.reason,node" |
jq '[.[]|select(.state=="UNASSIGNED")]'
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
"$ES_URL/_cluster/allocation/explain" -d '{}' | jq .输出判断
单节点的 same_shard 是预期限制;生产的磁盘、tier 和 filter 决策必须修复,不能把 yellow 当长期正常。
解决步骤
单节点实验将 replica 改为 0;生产增加合格节点、释放磁盘或修正约束,暂停滚动维护直到冗余恢复。
验证
预期 replicas 全部 active、跨故障域分布;模拟失去一台节点仍可读写。
预防
容量保留 N+1 余量,索引模板设置正确副本数,升级门槛要求 green,并监控 unassigned 时间。
磁盘水位与 flood-stage 只读
现象
写入返回 403/429,错误含 cluster_block_exception;索引出现 read_only_allow_delete,磁盘接近满。
影响
新文档与更新停止,日志链路积压;继续写可能损坏节点稳定性。
常见根因
保留策略失效、shard 倾斜、快照或临时文件占用、merge 放大、节点容量不足。
定位顺序
先文件系统与 allocation,再找最大索引/shard,确认 ILM 和近期写入增长,最后处理只读块。
定位命令
df -h
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/allocation?v"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/indices?v&s=store.size:desc"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cluster/settings?include_defaults=true&pretty"输出判断
确认节点是否超过 low/high/flood-stage;找出增长来源。只清除索引 block 而磁盘仍高,会很快再次只读。
解决步骤
先停止非必要写入,扩容、删除已确认过期且有备份的数据或修 ILM;磁盘降到安全线并恢复分配后再清 block:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
-X PUT "$ES_URL/affected-index/_settings" -d '{"index.blocks.read_only_allow_delete":null}'验证
磁盘低于恢复阈值,分片正常分配,普通应用身份 CRUD 成功,积压按受控速率消化。
预防
按增长预测扩容、ILM 失败告警、最大 shard 告警、快照与本地数据分盘,并保留水位余量。
Heap pressure、长 GC 或 circuit breaker
现象
查询 429/500、circuit_breaking_exception、节点暂停、GC 时间升高,所有请求 P99 同时恶化。
影响
协调、搜索和写入线程积压,节点可能离开集群并触发分片恢复风暴。
常见根因
高基数聚合、过多 buckets、text fielddata、深分页、巨大 bulk、过多 shard/field、heap 设置不合理或应用重试风暴。
定位顺序
先 heap/GC 与 breakers,再 hot threads 和 tasks,再定位具体查询、字段和 shard 扇出。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_nodes/stats/jvm,breaker,thread_pool?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_nodes/hot_threads?threads=5"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_tasks?detailed=true&actions=*search*"输出判断
看 old GC、heap percent、breaker tripped、queue/rejected 和具体 task。breaker 是保护动作,不应先盲目调高。
解决步骤
限流高成本查询,取消已确认可取消的异常 task;减少 buckets、分页深度、返回字段和 shard 扇出;移除 fielddata;修 mapping 或预聚合。资源长期不足再扩容。
验证
heap 与 GC 回落,breaker 不再增长,线程队列清空,代表查询结果不变且 P99 恢复。
预防
查询预算、search.max_buckets、慢查询样本、shard/field 上限、负载测试和应用退避熔断。
Bulk 429、部分失败或索引延迟
现象
bulk HTTP 200 但 errors:true,item 出现 429/400/403;CDC lag 持续扩大。
影响
文档缺失或延迟,盲重试可能重复写、乱序覆盖或形成重试风暴。
常见根因
write thread pool 饱和、bulk 批次过大、refresh 太频繁、mapping 冲突、权限错误、ingest pipeline 失败、热点 routing。
定位顺序
先按 item 状态分类,再看 write/indexing pressure、节点资源和热点 shard,最后检查 mapping/pipeline/权限。
定位命令
jq '[.items[]|to_entries[0].value|select(.status>=300)|{status,error}]' /tmp/products-bulk.json
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_nodes/stats/thread_pool,indexing_pressure?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/thread_pool/write?v&h=node_name,active,queue,rejected,completed"输出判断
429 是过载可退避;400 mapping/解析错误需修数据;403 需修权限;同一批内成功 item 不能重发为非幂等新 ID。
解决步骤
只重试失败且可重试的 item,使用确定性 _id,指数退避并设上限;缩小批次,降低并发,适当延长 refresh;修 mapping、pipeline 或热点 routing。不可修数据进入死信与补偿。
验证
bulk errors:false,rejected 不再增长,终端 CDC offset 追平,按权威源抽样与全量 ID、文档摘要对账通过。
预防
item 级错误指标、批次字节和文档数上限、幂等 ID、分区有序 CDC、死信队列与 lag 告警。
查询超时、深分页或聚合过慢
现象
搜索超时、应用 P99 升高,客户端已超时但 _tasks 仍有搜索,协调节点 CPU/heap 高。
影响
查询积压影响其他租户;客户端重试会把一次慢查询放大成多次执行。
常见根因
from 深分页、通配过宽、脚本查询、高基数聚合、过多 shards、排序字段无 doc_values、返回 _source 太大。
定位顺序
先拿真实慢请求和目标索引,再 profile/explain,随后看 tasks、hot threads、shard 数和返回体。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_tasks?detailed=true&actions=*search*"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_nodes/hot_threads?threads=3"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
"$ES_URL/products-read/_search" -d '{"profile":true,"size":10,"query":{"match":{"title":"keyboard"}}}' > /tmp/profile.json输出判断
profile 看各 shard query/collector 时间;任务持续超过客户端 deadline 说明取消或超时链路不完整;_shards.total 过大说明扇出。
解决步骤
先限流/熔断问题查询;改 PIT + search_after;把精确条件放 filter;限制 buckets 与返回字段;给排序聚合字段正确 mapping;缩小索引范围或预聚合。确认 task 无需继续后再按 task ID 取消。
验证
同等数据下结果一致,timed_out:false、_shards.failed:0,P95/P99 和资源回到目标,客户端不再重试风暴。
预防
查询模板、最大页深/桶数、业务 deadline、慢查询回归集和按租户的资源预算。
Mapping 冲突、字段爆炸或搜不到数据
现象
写入 400 mapper_parsing_exception,字段数量快速增长,或文档存在但 match/term 查询无结果。
影响
数据进入失败、cluster state 变大、heap 上升,搜索结果不完整。
常见根因
同字段混用数字/字符串、动态对象键变字段、text 与 keyword 查询混淆、索引/搜索 analyzer 不一致、时区或日期格式错误。
定位顺序
先保存失败文档和错误 JSON,再查 mapping、field caps 和 analyzer,最后检查模板优先级与首批数据。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/products-v1/_mapping?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/products-v1/_field_caps?fields=title,category,price&pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -H 'Content-Type: application/json' \
"$ES_URL/products-v1/_analyze" -d '{"field":"title","text":"Mechanical Keyboard"}' | jq '.tokens'输出判断
mapping 类型一旦创建通常不能原地改变;term 对 text 查的是切词后的精确 token,不等于全文匹配。字段数持续增加说明模型无界。
解决步骤
暂停错误生产者;修数据类型和模板;新建 versioned 索引,用 reindex/源重放迁移;任意键改 flattened;完成全量与查询对账后切 alias。不能用 ignore_malformed 隐藏关键字段错误。
验证
合法文档成功、非法文档明确 400;analyze token 符合预期;代表 match/term/range 查询和总量正确。
预防
显式 mapping、dynamic:"strict"、模板 simulate、契约测试、字段数量与 mapping 大小告警。
Alias 切换歧义或迁移后数据不一致
现象
切换请求超时,alias 指向不明;新索引少文档、旧删除复活,或应用写入落到错误物理索引。
影响
读写分裂、数据丢失或重复;继续切流会扩大无法确定的窗口。
常见根因
切换前未停写或未回放终端事件;CDC 删除未传播;offset 乱序;没有保存 alias before;多个 write index;响应丢失后盲重试。
定位顺序
先暂停写流量,读取完整 alias 和 write landing,再核对起止 checkpoint、CDC lag、tombstone、全量 ID/digest 和物理 canary。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_alias/products-read,products-write" | jq -S .
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/products-v1/_count"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/products-v2/_count"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/products-v1,products-v2/_doc/cutover-canary" | jq .输出判断
状态必须完整等于切换前或切换后;部分 remove/add、两个 write index、未知目标都属于不安全状态。count 相等不证明内容和删除相等。
解决步骤
保持停写。若等于 before,补齐 CDC 到终端 offset 后重试原子 actions;若等于 after,执行应用 canary 和全量对账;其他状态按保存的完整 alias JSON 恢复 before。回滚前把切换窗口 index/delete 事件回放至 v1,再切回。
验证
alias filter/routing/write-index 与预期一致;普通身份写、读、删成功;物理索引落点正确;全量 ID、规范化 digest、tombstone 和终端 offset 一致。
预防
稳定 _id、分区有序 CDC、持久 checkpoint/tombstone、切换前后 alias 快照、must_exist:true 和响应丢失演练。
快照失败或恢复结果不完整
现象
repository verify 失败,snapshot 为 PARTIAL/FAILED,restore 冲突,恢复后计数或查询不一致。
影响
RPO 没有得到保护;误判成功会在真实灾难时无法恢复或恢复错误数据。
常见根因
仓库权限/网络、primary 未分配、多个集群同时写仓库、版本不兼容、目标索引同名、遗漏 global/feature state、快照边界与 CDC checkpoint 未绑定。
定位顺序
先 verify 仓库,再查 snapshot state/UUID/shards/metadata,然后检查目标集群 UUID、版本和冲突,最后做数据与控制面对账。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -X POST "$ES_URL/_snapshot/lab-repo/_verify" | jq .
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_snapshot/lab-repo/$SNAPSHOT?verbose=true" | jq .
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/recovery?v"输出判断
只有 SUCCESS、failed shards 为 0、name/UUID/indices/version/metadata 与记录一致才可恢复。PARTIAL 必须重做,不能只恢复成功 shards。
解决步骤
修仓库权限、网络或 primary;确保单写者;创建新快照;恢复到隔离名称或空白集群;按兼容矩阵选择版本;显式决定 global state 和 feature states;从 source/snapshot/target 三向比较数据、mapping、templates、ILM、pipelines、aliases 和身份。
验证
恢复 shard 全成功,cluster UUID 与源不同;全量 ID、文档摘要、总量、代表查询和应用身份正反权限通过;RPO checkpoint 可解释。
预防
自动快照状态与年龄告警、独立仓库与保留策略、定期空白集群恢复演练、快照和 CDC checkpoint 同步记录。
ILM 卡住、rollover 不发生
现象
data stream 不 rollover,旧 backing index 不迁层或不删除,_ilm/explain 显示 ERROR。
影响
热节点磁盘持续增长,最终触发水位、只读和写入中断。
常见根因
模板未匹配、policy 名错误、rollover 条件未满足、alias/data stream 配置错误、目标 tier 不存在、索引被手工修改或权限不足。
定位顺序
先看 data stream 与 backing indices,再查 _ilm/explain 的 failed_step/reason,之后检查模板 simulate、policy 和目标 tier。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_data_stream/logs-app-prod?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/logs-app-prod/_ilm/explain?pretty"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" -X POST "$ES_URL/_index_template/_simulate_index/logs-app-test" | jq .输出判断
failed_step 与 step_info.reason 是直接原因;simulate 必须命中预期 policy/mapping;等待条件未满足不是错误。
解决步骤
修模板、policy、tier 或权限;确认错误已消除后重试失败步骤:
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" \
-X POST "$ES_URL/logs-app-prod/_ilm/retry"不要手工删除当前 write index。磁盘紧急时先扩容或削减非必要写入。
验证
explain 不再 ERROR,rollover 产生新 write index,旧索引按阶段迁移,磁盘增长回到预算。
预防
模板 simulate、ILM error/age 告警、数据层容量校验和定期 rollover 演练。
升级后节点、插件或客户端异常
现象
节点无法加入、插件报版本不匹配、Kibana 不可用、客户端出现未知字段/弃用 API、业务查询或写入回归。
影响
集群长期混合版本、冗余下降或业务中断;错误原地降级可能让数据目录不可读。
常见根因
未固定 from/to、忽略 breaking changes、插件/Kibana 版本不一致、客户端过旧、升级前有弃用项、节点顺序错误或升级后未做业务验证。
定位顺序
先暂停下一节点和业务扩流,再查所有节点版本/角色、日志、插件、集群健康,随后跑普通身份 CRUD/delete 和代表查询。
定位命令
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/nodes?v&h=name,version,node.role,master"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_cat/plugins?v"
curl --fail --silent --cacert "$ES_CA" -u "$ES_AUTH" "$ES_URL/_migration/deprecations?pretty"
journalctl -u elasticsearch -n 200 --no-pager输出判断
升级窗口只允许 source 与 target 两个批准版本。节点 join rejection、插件版本、unknown setting/API 和 deprecation 必须分别修复;green 不能替代业务门。
解决步骤
未升级节点保持 source,修插件/配置/客户端后再继续;已升级并写入的节点不原地降级。需要整体回退时恢复升级前快照到兼容的独立旧集群,回放 checkpoint 后全部 index/delete 事件,对账后切流,并隔离原升级集群。
验证
所有节点精确为 target,Kibana/插件/客户端兼容;普通身份 CRUD/delete、代表查询、模板/ILM/安全、快照恢复和 CDC 终端 offset 均通过,SLO 稳定。
预防
固定变更版本与制品、预生产回放、升级前快照、deprecation 清零、一次一节点、每步停止条件和独立集群回退演练。
