去年我接手了一套 Elasticsearch 集群,六个节点全部裸机安装,版本乱到一台 7.5 一台 7.9,JVM 参数各写各的,数据目录直接坐在系统盘上,连个监控都没有。某个周六夜里节点宕掉,因为 discovery 配置太随意,剩下两台机器反复选举,日志刷了好几百兆才有人发现。就是从那次事故开始,我把这套 ES 全部推倒,用 Docker Swarm 重新编排成标准化集群,从节点角色、编排文件到备份监控,一步步收敛成今天要讲的这套方案。
这篇文章就是那次重构的完整记录,核心是讲清楚这几件事:为什么选 Swarm 而不是 K8s、主从数据节点怎么在 Swarm 里划分角色、编排文件怎么写才是生产级配置、上线后备份恢复和滚动更新怎么处理。对正在维护 ES 集群、或者准备把 ES 容器化的团队来说,这篇可以当一份落地手册来用,照着做至少能避开我踩过的那几个大坑。
1. 抛开 K8s,我为什么选 Docker Swarm 扛 ES 生产集群
先说一个容易被带偏的问题。现在一提容器编排,很多人默认"那就上 K8s",但我的观点是:工具选型要结合团队维护能力和应用特性,Elasticsearch 这种有状态中间件,对调度器的需求根本没那么强,Swarm 反而是更省心的选择。
1.1 Swarm 和 K8s 的真实成本差异
K8s 本身不是一个组件,是一套平台。要跑起来,至少需要 etcd、api-server、controller-manager、scheduler、kubelet 五个核心组件,还要考虑 CNI 网络插件、CoreDNS、Ingress Controller、Dashboard,以及配套的 RBAC、Helm、监控体系。这些组件每一样都有版本兼容问题,光升级一次集群版本,就够运维团队折腾几天。
Swarm 呢?它内置在 Docker 引擎里,安装 Docker 之后docker swarm init一下,控制面就起来了。不需要额外的存储组件,不需要额外的 API Server,docker stack deploy直接吃 Compose 文件。对一个三到六人的后端加运维团队来说,Swarm 的学习成本几乎是零——团队里任何一个会写docker-compose.yml的人,到了 Swarm 环境里基本可以无缝上手。
再算资源账。K8s 的 control plane 组件加起来至少要 2GB 内存,etcd 还要占用磁盘 IO。Swarm 的 control plane 非常轻,manager 节点多跑几个容器只占几百 MB。省出来的资源,放在 ES 这种吃内存的数据库上,就是在给核心业务加资源。
1.2 什么样的企业场景适合 Swarm
Swarm 适合的场景,画个像大概是这样的:
- ES 集群规模在 3 到 15 个节点之间,不需要分钟级自动扩缩容,偶尔手动扩容就够了。
- 团队没有专门的 K8s 平台组,运维由后端开发兼职,需要的是"看得懂、能维护"的工具。
- 中间件种类不多,ES、Redis、MySQL,用 Compose 文件都能描述清楚。
- 有滚动重启和故障恢复诉求,但不需要 K8s 那套复杂的 Operator 机制。
反过来,如果公司已经有成熟的 K8s 平台,ES 的 Pod 归平台组统一管理,那直接用 K8s 没毛病,不用为了 ES 单独维护一套 Swarm。如果 ES 集群要支持多租户、动态扩缩容、和 K8s 上的业务服务做细粒度网络互通,那 Swarm 也确实不如 K8s 顺手。
有些朋友跟我聊过 Doris、ClickHouse 这类 OLAP 组件,问是不是部署策略可以照搬 ES 这套。我的回答是:底层思路一致,都是"角色分离 + 主节点仲裁 + 数据节点水平扩展",但具体编排方式必须按各自的节点发现协议去适配,千万别混着来。ETL 链路里 ES 负责检索、Doris 负责聚合分析,数据模型本来就各管一摊,部署上各自保持独立集群反而是最稳的。
2. 集群角色的物理划分:主节点、数据节点、协调节点的 Swarm 落地
ES 集群不像 Redis Cluster 那样所有节点角色一致。它有三种基础角色,每种角色干的活不一样,对硬件的要求也不一样。这一步不设计好,后面再调就是拆东墙补西墙。
2.1 节点角色的边界与误区
- master 节点:负责集群状态管理、索引元数据维护、分片分配决策。它们不存业务数据,但集群元数据变更全走这里。生产环境必须独立部署,数量保持奇数(3 个或 5 个)。
- data 节点:真正存放分片数据,承担索引写入和查询请求的 IO 压力。这是集群的大头,容量规划主要做在这一层。
- coordinating 节点:接收客户端请求,做路由分发和结果聚合,本身不存数据、也不参与主节点选举。规模大了以后可以独立加一层,用来隔离查询压力。
- ingest 节点:执行数据预处理管道,比如字段转换、日志切分。如果写入链路里有 ingest pipeline,可以单独划一批节点出来,避免预处理和搜索抢资源。
很多人图省事,一个节点同时打 master 和 data 标签,小集群这么干没问题,到了 6 节点以上就会出现一个经典症状:data 节点频繁 Full GC,主节点心跳超时,集群反复进入 red 状态。因为 master 职责要求响应快、GC 稳定,而 data 节点的 IO 起伏大,两者放在同一进程里就是互相拖累。这句话我希望每个组集群的人都看进去:生产环境主数据不分家,事故早晚找上门。
2.2 用 Swarm 标签和约束把角色钉死在指定主机上
Swarm 本身没有"角色"概念,它只有节点标签(labels)和调度约束(placement constraints)。我们把这两件事结合起来就行。
假设有三台机器跑 master,三台机器跑 data,物理上分了两组。初始化 Swarm 之后,给每一台机器打上对应标签:
docker node update --label-add es_role=master node-1 docker node update --label-add es_role=master node-2 docker node update --label-add es_role=master node-3 docker node update --label-add es_role=data node-4 docker node update --label-add es_role=data node-5 docker node update --label-add es_role=data node-6然后在 ES 服务的编排文件里,通过deploy.placement.constraints把服务固定到对应主机上:
services: es-master: deploy: placement: constraints: - node.labels.es_role == master es-data: deploy: placement: constraints: - node.labels.es_role == data这样跑起来之后,Swarm 的调度器只会在满足标签的主机上启动副本。千万不要用 Swarm 默认的全局调度去跑 ES,否则一个节点挂了,Swarm 可能把你的主节点服务重新调度到数据节点上,ES 数据目录错乱,这个坑踩下去相当酸爽。
2.3 网络与端口规划
ES 有两个核心端口:9200 给 HTTP 客户端访问,9300 给节点间传输层做集群通信。在 Swarm 里,我们用一个 overlay 网络把集群内部通信隔离起来,外部访问只暴露 9200。
创建网络时建议开启加密和别名选项:
docker network create -d overlay \ --attachable \ --opt encrypted \ es-net需要在外部访问 9200 的节点,比如 Kibana、Logstash、业务服务,再单独接入这个es-net。节点之间用服务名互相解析,这一步很关键,ES 的 discovery 配置直接依赖它。
端口规划上,9300 千万不要暴露到宿主机公网。就算你在云上做了安全组,也建议只暴露 9200,9300 只存在于 overlay 网络内部,避免别人直接探测你的节点传输端口。
3. Stack 编排文件逐行拆解:生产级配置不该用默认值
ES 官方镜像的默认配置能跑起来,但上生产到处都是坑。下面是经过实际验证的一份es-stack.yml,我会拆开讲清楚每一处为什么要这么写。
version: "3.8" services: es-master: image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2 hostname: es-master networks: - es-net environment: - cluster.name=es-prod - node.name=es-master-{{.Node.Hostname}} - node.roles=master - discovery.seed_hosts=es-master,es-data1,es-data2 - cluster.initial_master_nodes=es-master - xpack.security.enabled=false - ES_JAVA_OPTS=-Xms4g -Xmx4g volumes: - /data/es/master:/usr/share/elasticsearch/data deploy: mode: replicated replicas: 3 placement: constraints: - node.labels.es_role == master resources: limits: memory: 8g restart_policy: condition: any delay: 5s max_attempts: 3 es-data: image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2 hostname: es-data networks: - es-net environment: - cluster.name=es-prod - node.name=es-data-{{.Node.Hostname}} - node.roles=data,ingest - discovery.seed_hosts=es-master,es-data1,es-data2 - ES_JAVA_OPTS=-Xms8g -Xmx8g volumes: - /data/es/data1:/usr/share/elasticsearch/data deploy: mode: replicated replicas: 3 placement: constraints: - node.labels.es_role == data resources: limits: memory: 16g restart_policy: condition: any delay: 5s max_attempts: 3 networks: es-net: external: true3.1 镜像版本怎么选
我在这套方案里用的是7.10.2。原因很现实:企业里大量插件、代码、以及团队对配置体系的熟悉程度都停在 7.x;而 7.10 之后,官方把一些集群分片分配的核心参数改成默认值,部分老版本的工具链会失灵。8.x 系列默认强制开启安全认证,节点间需要配置 TLS 证书,对运维体系要求又高了一个台阶,从 7.x 升级到 8.x 不是改个镜像标签就完事的事,建议等团队有充足预案再动。
如果你确实要上 8.x,请重点确认三件事:安全证书如何统一分发、Kibana 版本必须对齐、以及旧的.security索引迁移策略。没有想清楚之前,别上了生产再反悔。
3.2 环境变量与 JVM 内存的黄金规则
cluster.name必须统一,集群内节点才能互相识别。node.name我用了模板变量{{.Node.Hostname}},这样每个节点拿到独立名字,出现问题时一眼能从日志中定位是物理机上的哪个容器。
discovery.seed_hosts填的是 overlay 网络里其他节点的服务名,这比 IP 更可靠,因为 Swarm 重新调度后 IP 会变。cluster.initial_master_nodes只要在集群首次启动时指定 master 节点的名字,引导建立集群后这个配置的使命就结束了。注意,如果三个 master 节点是同一份镜像启动的,它们的node.name必须唯一,否则 ES 会认为自己分裂成了两个节点。
JVM 堆内存,我见过最多的问题是拍脑袋配。官方建议是:
- 堆大小不要超过物理内存的一半,因为 ES 的 Lucene 要用除了堆之外的系统内存做文件缓存;
- 堆最小值
Xms和最大值Xmx必须设成一样,防止运行中堆动态扩容引发 Full GC; - 单节点堆建议控制在 32GB 以内,超过 32GB,JVM 的压缩指针失效,内存利用率反而下降。
数据节点上,我会把总内存 16GB 的机器配 8GB 堆,剩下给文件系统缓存。resources.limits.memory设置为堆的两倍左右,给 Lucene 和系统开销留余量,否则容器可能因为 RSS 超过限制被 OOMKilled。
3.3 系统参数与数据卷权限:两个最容易翻船的点
ES 官方在生产环境要求vm.max_map_count至少是 262144,这个系统参数是 ES 用来做 mmap 内存映射的分区数,默认值 65530 对单机够用,对集群远远不够。如果不开,日志里会出现这样一段:
max virtual memory areas vm.max_map_count [65530] likely too low, increase to at least [262144]在每台宿主机上执行:
sysctl -w vm.max_map_count=262144 echo 'vm.max_map_count=262144' >> /etc/sysctl.conf第二个容易翻船的点是数据卷权限。ES 官方镜像约定以uid:1000(elasticsearch 用户)访问容器内的数据目录,如果你在宿主机直接mkdir /data/es/xxx,默认目录 owner 是 root,容器一启动就报AccessDeniedException,数据节点根本起不来。这是初学 ES 容器化必踩的坑。
mkdir -p /data/es/master /data/es/data1 chown -R 1000:1000 /data/es或者干脆在数据目录挂载后加一个初始化容器去chown,但我更推荐宿主机层面处理好,简单直接,不增加编排复杂度。
3.4 健康检查与优雅停止
生产环境我会在每个 ES 服务里加上健康检查,等集群完全就绪后再让 Swarm 认为服务健康:
healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:9200/_cluster/health"] interval: 30s timeout: 10s retries: 5 start_period: 60sstart_period非常关键。ES 冷启动时要先恢复分片,这段时间可能长达一两分钟,如果不给 grace period,Swarm 会反复重启容器,导致分片恢复和重启互相打架。
同样需要关注的还有停止顺序。ES 在容器收到 SIGTERM 后优雅关闭时,会优先把本节点的分片迁移走,这个过程可能需要几十秒。Swarm 里可以通过stop_grace_period告诉调度器多给点时间:
stop_grace_period: 2m不做这个设置的话,Swarm 默认 10 秒就强制杀进程,分片来不及迁移,重新启动后集群恢复时间会拖长很多。
4. 部署实录:从 swarm init 到集群绿灯全流程,以及踩过的坑
理论说完了,我们完整跑一遍流程。下面这个顺序是我踩过坑之后总结出来的最优路径,不要随意调换。
4.1 六台机器的标准操作路径
第一步,初始化 Swarm 集群。假设六台机器 IP 分别是 172.16.1.11 到 172.16.1.16,第一台做 manager:
docker swarm init --advertise-addr 172.16.1.11这一步会输出一个docker swarm join --token ...的命令,其余五台机器跑这个命令加入。如果不小心把 token 丢了,在 manager 节点执行docker swarm join-token worker重新获取。
第二步,打节点标签:
docker node update --label-add es_role=master 172.16.1.11 docker node update --label-add es_role=master 172.16.1.12 docker node update --label-add es_role=master 172.16.1.13 docker node update --label-add es_role=data 172.16.1.14 docker node update --label-add es_role=data 172.16.1.15 docker node update --label-add es_role=data 172.16.1.16注意 manager 节点默认也可以运行容器,但为了不让 Swarm 自动把 ES 调度到任意 manager 上,靠的就是我们前面约束里的es_role标签。什么样的标签打在你身上,Swarm 就只知道往哪调度。
第三步,宿主机系统参数和磁盘目录处理,照着 3.3 小节执行,确认所有机器都做了。
第四步,部署 stack:
cd /opt/es-stack docker stack deploy -c es-stack.yml es第五步,观察服务状态:
docker service ls docker service ps es_es-master docker service ps es_es-data当每个服务都显示3/3的时候,说明三个副本都起来了。这时候开始在任意一台机器上检查集群健康:
curl http://<任意节点IP>:9200/_cluster/health?pretty期望看到status: green。如果只是yellow,说明有副本分片还没分配,多半是磁盘水位、权限或者节点内存的问题;如果red,那就是有主分片无法分配,这时候就要进入排查链路了。
4.2 集群红色状态的完整排查链路
我先把话放这儿:集群红了第一件事不是重启节点,而是看分片为什么没分配。重启很容易把事情搞大,因为节点加入退出的抖动会让分片分配器反复错乱。
排查路径我固定用三连命令:
curl 'localhost:9200/_cat/health?v' curl 'localhost:9200/_cat/shards?v&h=index,shard,prirep,state,unassigned.reason' curl 'localhost:9200/_cat/allocation?v'第一命令看整体状态。第二命令看哪些分片 unassigned,unassigned.reason会提示是NODE_ADDED、CLUSTER_RECOVERED、还是INDEX_CREATED。第三命令看数据节点磁盘使用率。
真实项目里遇到最多的场景是磁盘水位问题。ES 默认把cluster.routing.allocation.disk.watermark.low设为 85%、high设为 90%、flood_stage设为 95%。一旦磁盘超过 90%,ES 会自动把该节点上的分片迁移走;超过 95%,索引直接进入只读模式,写入全挂。
我之前就处理过一次线上故障:一个数据节点的磁盘到了 96%,集群先是 yellow,然后大量写报错cluster_block_exception。当时我先清理了服务器上的日志和临时文件,让磁盘降到 85% 以下,然后手动解除索引只读:
curl -XPUT 'localhost:9200/_all/_settings' -H 'Content-Type: application/json' \ -d '{"index.blocks.read_only_allow_delete": null}'注意,这个命令要在所有索引上执行,而且执行前确认磁盘是真的降下来了,否则又会马上弹回去。
另一个高频坑是节点之间网络命名空间隔离导致 discovery 失败。ES 节点在 overlay 网络中通过 9300 端口互相连接,排查时先确认容器之间能否 ping 通服务名:
docker exec -it <es-master容器ID> curl -s http://es-data1:9200能通说明 overlay 网络正常,不通就要检查--opt encrypted是否影响了节点间通信,有的老版本 Docker 对加密 overlay 的支持有 bug。遇到这种情况,临时去掉--opt encrypted重建网络,基本都能恢复。
4.3 Windows 开发机上连集群要留意什么
很多同事习惯在 Windows 笔记本上用本地装好的 Elasticsearch 做开发,一兴奋直接从 7.10 官网 zip 包启动单节点,结果本地磁盘、JVM 参数、系统环境和服务器集群完全对不上,联调时各种问题说不清楚。其实 Windows 上直接跑elasticsearch.bat想连 Swarm 集群,跨网络和认证配置会有额外成本,远不如在开发机上装个 Docker Desktop,然后通过--network host或者用同一套 Compose 文件起一个 developer 配置文件,直接连集群的 9200 端口,或者干脆通过 Kibana 做数据验证。
如果必须在 Windows 上单机跑 ES 做功能测试,几个建议:
- 用和线上一致的 7.10.2 版本,避免版本行为差异;
- 在
config/elasticsearch.yml里关掉 xpack 安全特性,否则连本地_cluster/health都要带用户名密码; - 注意 Windows 防火墙是否拦截了 9200/9300;
- 不要在生产集群和本地单节点之间搞什么"组集群",版本、角色、系统参数全不一样,效果参考价值极低。
5. 上线后必做的三件事:备份恢复、滚动更新、监控告警
集群亮了绿灯,很多人就以为运维结束了。真正的事故往往发生在第三个月,比如误删索引、节点硬件故障、或者一次错误的配置更新把整个集群搞挂。所以这三件事,上线当天就必须做好。
5.1 Snapshot 快照备份到共享存储
ES 的备份不是"导数据"这么简单,正确做法是注册一个快照仓库,然后定期执行快照。快照仓库要求有一个所有节点都能访问的共享文件系统,企业场景常用 NFS,或者 S3 对象存储。
在 Swarm 环境里,因为每个副本可能漂移到不同宿主机,所以共享存储一定要走网络文件系统,不要在本地磁盘上建目录。NFS 挂载所有数据节点后,在 Kibana 里注册仓库:
PUT /_snapshot/es_backup { "type": "fs", "settings": { "location": "/data/es_backup", "compress": true } }备份三个核心点:
- 快照仓库要和数据节点放在不同的故障域,别让集群数据盘挂了的同时备份盘也挂在同一台宿主机上;
- 定期执行快照策略,用 Cron 任务,每天凌晨执行一次完整快照,保留最近三十天;
- 恢复前先检查快照状态,不是备份了就万事大吉,要定期做恢复演练。
恢复数据的基本姿势:
# 查看快照列表 GET /_snapshot/es_backup/_all # 恢复指定索引 POST /_snapshot/es_backup/20260112_001/_restore { "indices": "nginx-log-*", "rename_pattern": "nginx-log-(.+)", "rename_replacement": "restored-nginx-log-$1" }这里强调一下,恢复索引时建议重命名,避免和线上现存索引冲突,确认数据正确后再切换别名,别直接把线上的原索引覆盖掉。
5.2 Swarm 滚动更新策略:一次动一个节点
ES 集群升级最忌讳"一把梭"。容器化的优势在于可以精确控制滚动节奏,Swarm 的update_config配合健康检查,可以做一节点一节点的灰度升级。
在 stack 文件里配置:
deploy: update_config: parallelism: 1 delay: 60s order: start-first failure_action: rollback monitor: 120sparallelism: 1:同时只更新一个节点;delay: 60s:更新完一个等一分钟再动下一个,给分片恢复留时间;order: start-first:新容器先起来,再停旧容器,减少可用副本数为零的时间窗口;failure_action: rollback:如果健康检查失败,自动回滚到上一个版本;monitor: 120s:更新完成后观察两分钟再判定是否成功。
更新的时候用命令指定镜像版本:
docker service update --image docker.elastic.co/elasticsearch/elasticsearch:7.10.2 es_es-data升级过程中,密切观察集群健康状态。一个节点更新完毕后,主分片的副本重新分配要时间,如果直接看到yellow,别急着继续推进,等它变绿再让 Swarm 动下一个节点。整个过程就像多人协作分片迁移,你越有耐心,线上越稳定。
5.3 监控告警:cerebro + elasticsearch-exporter + Prometheus
最后也最容易被忽视的是监控。我推荐组合一套轻量的监控方案:
- Cerebro:一个 Web 界面工具,可以直观看到集群节点状态、分片分布、实时迁移信息。排查问题时比命令行效率高一截。启动一个 Cerebro 容器,连接集群 9200 地址就行。
- elasticsearch-exporter:Prometheus 生态的 ES 指标导出器,把集群状态、分片状态、节点 JVM、磁盘空间等指标导给 Prometheus。
- Grafana:配上 ES 监控仪表盘模板,一张大屏全搞定。
Prometheus 采集到的关键指标里,我会重点盯着这几个:
| 指标项 | 推荐阈值 | 说明 |
|---|---|---|
elasticsearch_cluster_health_status | green | 状态不是 green 要立即报警 |
elasticsearch_cluster_shards_percentage_unassigned | 小于 1% | 未分配分片大于 0 就要关注 |
elasticsearch_node_jvm_heap_used_percent | 不超过 75% | JVM 堆使用率长时间超 75%,GC 压力会非常大 |
elasticsearch_node_filesystem_free_bytes | 不小于 20GB | 磁盘低于 20GB 就触发预报警 |
elasticsearch_node_thread_pool_search_rejected | 持续为 0 | 有 rejected 说明查询线程池队列爆了 |
告警别只接一个渠道,钉钉/企业微信/短信至少配两个。同时给 Cerebro 也留个入口,排障时开个页面就能看到分片在哪里、谁在拖后腿,比抓日志舒服太多。
5.4 关于集群容量规划的一点私房心得
最后想分享一个很多人没有的意识:ES 集群的容量规划要按"场景 + 增长曲线"一起算,而不是按磁盘总数除一下每个节点。
我有一个大致公式,每 1GB 原始数据在未压缩的 ES 存储里,经过倒排索引、doc values、_source 原始文档等多重存储,膨胀比一般在 1.2 到 1.5 倍左右,然后每个索引默认要留一个副本,所以实际占用是数据量的 2.4 到 3 倍。加上系统预留 20% 磁盘空间,假设你每个月新增 100GB 原始日志,那一个月真实占用的空间大约是 250GB 到 300GB。按这个估算,再决定数据节点的数量和每块磁盘的大小,会靠谱很多。
至于cluster.routing.allocation.disk.watermark这几个水位参数,不要随便调低去压榨磁盘空间。你把水位调低,ES 就会在磁盘还很宽裕的时候开始往别的节点搬分片,迁移流量反而拖垮集群性能。保留默认值是个稳妥的选择。
最后说两句
这套 Swarm + ES 集群方案上线到现在,数据节点从 3 个扩到 6 个,经历了两轮滚动升级和一次真实的磁盘故障演练,没有出过像样的生产事故。容器编排不是越复杂越好,能把故障恢复、滚动更新、备份恢复这三件事跑顺,再复杂的系统也能在一个看板里管清楚。
如果你正在纠结要不要把裸机的 ES 容器化,我的建议很明确:先从非核心日志集群开始练手,把整套流程跑熟,再逐步把核心业务索引迁过去。容器化不是目的,稳定可复现才是。