☰
Elasticsearch集群搭建与高可用实战:从节点角色到故障转移
2026/9/30 7:33:36 网站建设 项目流程

做搜索和日志平台的兄弟应该都经历过那个阶段:单机跑Elasticsearch的时候一切岁月静好,等数据量上来、并发一高,节点CPU飙到90%,磁盘告警,更怕的是半夜一个大查询直接把节点搞挂。这时候才意识到,Elasticsearch集群不是“以后可以再装”的功能,而是从业务第一天就该设计好的底座。这篇文章围绕Elasticsearch集群展开,把概念、机制和实战串起来讲一遍——包括节点分片副本的角色分工、master选举和故障转移到底怎么跑通、三节点集群从零搭起来的完整配置,以及我这两年线上被坑出来的排查和调优经验。适合正在搭集群、研究ES高可用或者做日志平台设计的同学,整篇不说“最佳实践”的黑话,全部按实际能落地的方式讲。

1. 单节点顶不住之后:集群到底解决了什么问题

1.1 单机的天花板:容量、性能与可用性三道坎

很多人第一次接触ES是在单节点上跑个Demo,或者给应用做个站内搜索。数据量在几十GB的时候,单节点完全能扛,甚至给你一种“ES真快”的错觉。但业务量一旦涨起来,单机的问题会集中爆发,绕不开的就三条。

容量是最直观的坎。ES为了保证数据冗余,索引默认带一个副本,这意味着每份数据要在磁盘上存两遍。你买一块2TB的盘,真正能用于主分片存储的也就1TB左右。日志类场景每天增长几十GB,一个月下来磁盘就见底。更头疼的是,ES在做段合并和快照时还需要额外空间,磁盘到90%之后集群会主动拒绝写入,这是保护机制,但业务侧看就是“写入卡死”。

第二个坎是性能和资源争抢。单节点的CPU、内存、IO都是共享的。搜索请求、聚合分析、索引写入、段合并全挤在同一个进程里。数据量一上来,大聚合吃掉大量堆内存,GC暂停可能让查询耗时翻几倍。我曾经在单机环境跑一个分组聚合,数据约2亿文档,只算一个桶的top10,直接OOM。不是ES不行,是单机资源上限摆在那里。

第三个坎是可用性。单节点宕机意味着整个搜索服务不可用,而且ES不会自动帮你把数据恢复到另一个地方。维护窗口、断电、磁盘故障,任何一个意外都能让业务线停摆。这些都是集群要解决的根本问题。

1.2 集群带来的三件套:水平扩展、高可用与故障转移

Elasticsearch集群本质上是把多台机器组织成一个逻辑上的整体,对外统一提供读写能力。它解决的三个核心问题,刚好对应刚才说的三道坎。

水平扩展方面,ES通过分片机制把索引数据拆成多个分片,分散到不同节点上。节点越多,能承载的总数据量和总查询并发就越高。比如一个索引规划10个主分片,3节点集群可以把分片均匀铺开,每个节点只承担一部分数据的存储和检索压力。想扩容?加节点,分片会自动重新平衡过去,不用停机迁移数据。

高可用方面,每个主分片都可以有副本分片,副本分片分布在不同的节点上。只要主分片和副本不落在同一台机器上,任意一个节点挂了,ES就能从副本里提升出新的主分片,服务不中断。默认情况下副本数为1,也就是说至少需要两个可容纳分片的节点,高可用才真正成立。

故障转移则是集群的“自愈”能力。节点掉线后,master节点能感知到变化,将掉线节点上的主分片在其他节点的副本提升为主分片,并自动在存活节点上重新分配副本,让整个系统回到稳定状态。整个过程不需要人工介入。

经常有人拿ES集群和Redis集群、Kafka集群做对比。它们的目标类似,但机制差异很大:Redis集群用槽位分配数据,Kafka按分区副本做高可用,而ES的分片和副本是索引级别的,职责更重,因为ES要同时处理写入、检索和聚合。理解了这个差异,再看ES的很多配置就不会觉得奇怪了。

2. 吃透几个概念才算真入门:节点角色、分片与副本

2.1 节点角色怎么分、怎么配

ES节点默认是全能型,既能当master候选,又能存数据,还能做协调。小规模集群这么干没问题,但节点数量一多,角色混在一起经常出问题。比如一个节点既要承担大量写入,又要参与集群状态维护,一旦写入压力飙升,响应变慢,master心跳都受影响。

生产环境里建议按角色拆分。常见角色有这几类:

  • master节点:负责集群状态管理、索引创建删除、分片分配等。master候选节点需要开启node.roles中的master选项。为保证选举可靠性,集群中需要奇数个候选节点。
  • data节点:负责存储分片和副本,执行数据相关的读写操作。这是最繁忙的角色,根据数据冷热可以拆成data_hot、data_warm、data_cold等层级。
  • ingest节点:负责数据预处理管道,比如日志清洗、字段解析。如果写入量很大,最好单独部署ingest节点,避免抢占data节点的资源。
  • coordinating节点:只承担请求路由和结果汇总,不出数据。可以把高并发的查询入口固定在独立协调节点上,不过一般场景不需要单独拆。

角色分配的建议是:超过5个节点的集群,master候选节点和数据节点至少分离。3节点小集群可以每个节点同时做master候选和data,但主节点不要承担过多的data压力,否则集群稳定性会打折扣。

在elasticsearch.yml里这样配置:

node.roles: [ master, data_hot, data_content ]

这是7.x及以后版本支持的方式。如果角色配置为空,默认就是全能节点,这在生产环境容易踩坑,我后面会细说。

2.2 分片与副本:数据在集群里的流动逻辑

分片(shard)是ES数据存储的最小逻辑单元。一个索引在创建时可以指定主分片数量,每个主分片实际存储索引数据的一个子集。分片内部的数据再按段(segment)组织,段是倒排索引落盘后的不可变文件。写入的数据先进入内存Buffer,refresh后变成可检索的段,定期合并成大段。

副本分片(replica shard)是主分片的完整拷贝。副本存在的意义有两个:一是高可用,主分片所在节点挂了,副本可以顶上来;二是提升查询吞吐,读请求可以同时打到主分片和副本上。代价是写入时要同步到副本,副本越多写入放大越明显。默认副本数为1,大多数场景够用。

写入一条文档时,流程是这样的:客户端把请求发给任意节点,该节点充当协调节点。协调节点根据文档ID计算路由,默认将文档分配到某个主分片,然后转发到该主分片的所在节点。主分片写完后,同步给对应的副本分片,所有副本成功后,协调节点才向客户端返回成功。

读取一条文档的路径刚好相反。协调节点收到请求后,根据路由定位到具体分片,然后可以随机选择主分片或副本分片去查询,这种方式天然分摊了读压力。搜索请求(比如match、聚合)则更复杂,协调节点要把请求分发到索引的全部分片,收集各分片返回的局部结果,最后统一合并排序再返回。

这里必须提一句分片规划的分寸。分片太少,单分片数据量过大,影响查询速度和恢复速度;分片太多,每个分片都要消耗资源,集群状态变得庞大,管理开销增加。业界有个常用经验:单个分片的数据量控制在30GB到50GB之间,节点上每GB堆内存最多承担20到25个分片。我的实践是日志类索引按天建,每天一个索引,主分片数由写入吞吐估算——实测20GB/天的日志量,3个主分片就够。

3. 集群的“大脑”:选主、心跳与故障转移机制

3.1 集群发现与master选举

ES集群启动后,节点之间首先要互相发现。6.8之前的常驻方式是Zen Discovery,7.0之后推荐使用基于Seed Hosts的发现机制。生产环境配置discovery.seed_hosts来指定集群内一批候选节点的地址,新节点启动后会向这些地址发送请求,拿到集群中其他节点的列表,然后加入集群。

集群中所有主候选节点共同决定谁是master。选举规则不复杂:参与选举的候选节点之间互相投票,得票超过法定票数(quorum)的节点成为master。quorum的计算是master候选节点数除以2再加1。比如配置了3个master候选节点,quorum就是2;如果只有2个候选节点,quorum也是2,所以奇数个候选节点能避免很多尴尬情况。

选举完成后,master节点负责维护集群状态(cluster state),包括索引定义、分片分配路由等。这个状态会同步到所有节点。实际生产中,我见过有人把5个节点全部配置成master候选,这没什么必要,反而增加集群状态同步的复杂度。

3.2 故障转移现场:一个节点挂了会发生什么

这里以最典型的场景举例:3节点集群,其中一个data节点突然宕机,上面有3个主分片和几个副本分片。

首先master节点通过探测机制发现节点失联。ES默认每1秒发送一次探测请求,超过一定次数后判定节点故障。紧接着master进入故障处理流程:将失联节点上的主分片,在存活节点中找到对应的副本分片,把它提升为新的主分片;同时,对缺失副本的分片,在存活节点上创建新的副本,确保副本数恢复到设定值。

这个过程不是瞬间完成的。数据量大的分片提升为主分片后,还要做translog重放、刷新等操作,期间该分片可能短暂处于只读状态。副本重建则涉及全量数据拷贝,几十GB的分片重建,视网络和磁盘情况可能要好几分钟。

整个故障转移过程中,服务不会中断,只是部分查询和写入请求的延迟会上升。这也是为什么集群必须保留至少一台拥有全量副本数据的节点——如果主分片和副本同时都没了,那部分数据就永久丢失了,集群健康状态也会变成红色。

3.3 脑裂问题:根源与规避手段

脑裂(split-brain)是分布式系统里的经典问题。ES集群网络发生分区时,一部分节点可能联系不上master,但它们之间还能互相通信,于是这些节点会自己发起新的master选举,形成一个包含少数节点的“小集群”。这时系统里出现了两个master,两边同时认为自己有权修改集群状态,可能导致分片被重复分配、数据冲突甚至数据损坏。

Zen Discovery时代,规避脑裂的核心参数是discovery.zen.minimum_master_nodes,它的值应该设置为master候选节点数的一半加一。这样,少于法定票数的分区无法选出master,也就不会产生脑裂。

7.x之后,这个参数的设置方式调整了。集群初始化时使用cluster.initial_master_nodes来指定首批参与选举的节点,运行过程中的脑裂防护则由系统根据节点配置自动计算。到了8.x,角色和投票配置更加收敛,但核心思想没变:必须有超过半数的候选节点参与,才能形成合法的master选举。

我曾经碰到过一次脑裂现场,起因就是有人把三个节点的minimum_master_nodes设成了1,导致网络抖动时两个分区各自选出master,集群状态一片混乱。当时查了大半天,最后看日志里发现多个master才反应过来。在配置里务必确保这个值是对的,且不能靠运气,任何网络基础设施都不可能100%稳定。

4. 从零搭一个三节点集群:部署与配置实战

4.1 环境准备:系统参数和磁盘规划

三节点集群是中小规模最推荐的最小高可用单元。我的建议是3台机器,每台配置32GB内存、8核CPU、两块1TB SSD。SSD对ES性能影响极大,尤其是写入和段合并,机械盘在数据量上来以后完全扛不住。

安装ES之前,操作系统层面有几项必须调好,否则启动过程中各种报错。

vm.max_map_count是内存映射区域上限,ES默认需要至少262144。执行:

sysctl -w vm.max_map_count=262144 echo "vm.max_map_count=262144" >> /etc/sysctl.conf

文件描述符限制建议调到65535以上。ES对并发连接和文件句柄要求很高,查一下当前用户限制,直接在limits.conf设置:

echo "* - nofile 65535" >> /etc/security/limits.conf echo "* - nproc 4096" >> /etc/security/limits.conf

还要关闭swap。堆内存在物理内存中才能保证性能,一旦swap用了,延迟立马恶化。ES提供了bootstrap.memory_lock配置,配合systemd设置可以锁住内存。最简单的方式是在elasticsearch.yml里加一行:

bootstrap.memory_lock: true

如果报错说无法锁定内存,检查一下LimitMEMLOCK设置,systemd环境默认可能需要调整。

磁盘方面,不要把ES的数据目录和日志目录放在同一个磁盘分区里,否则日志写满会把整个data盘带崩。data目录可以用多个路径,ES会自动把分片分布到多块磁盘上,这样既能扩展容量也能平摊IO。

4.2 elasticsearch.yml核心配置逐项拆解

三个节点的安装包解压后,各节点配置文件的差异很小,重点是节点名、IP地址和角色。下面以node1为例,其余节点对应修改。

cluster.name: es-prod node.name: node-1 node.roles: [ master, data_hot, data_content ] path.data: [ /data1/es, /data2/es ] path.logs: /var/log/elasticsearch network.host: 192.168.10.11 http.port: 9200 transport.port: 9300 discovery.seed_hosts: - 192.168.10.11 - 192.168.10.12 - 192.168.10.13 cluster.initial_master_nodes: - node-1 - node-2 - node-3

逐个解释关键项:

cluster.name必须一致,集群是靠这个名称识别彼此的。node.name在每个节点上必须唯一。node.roles我推荐明确指定,你只有显式告诉节点它要干什么,集群的行为才是可控的。path.data指定数据存储路径,多个路径用逗号分隔,ES会按磁盘容量做均衡分配。network.host不能配成127.0.0.1,否则其他节点访问不到你。discovery.seed_hosts是节点发现使用的种子地址列表,写三台节点的transport端口地址。cluster.initial_master_nodes只在集群首次初始化时生效,用于选出一台初始master,之后这个配置可以删掉,但保留也无碍。

注意transport.port是节点间内部通信端口,http.port是REST端口,这俩要区分清楚。许多人配置防火墙时只放行了9200,忽略9300,导致集群永远无法组起来。

启动前先用bin/elasticsearch前台跑一下,确认日志无报错,再使用-d参数后台启动。全部节点启动后,通过任意节点检查集群状态:

curl http://192.168.10.11:9200/_cluster/health?pretty

正常情况下返回status为green,节点数显示3。

4.3 JVM堆内存和启动参数的那些坑

ES的堆内存设置直接决定它的性能上限,很多人在这上面栽跟头。默认的jvm.options里堆是1GB,这肯定不够用,必须改。

我的经验是:机器内存32GB,给ES堆内存分配16GB,剩下16GB留给操作系统Page Cache。记住一个铁律:ES的堆内存最大值不要超过32GB。因为超过32GB后JVM的压缩指针(Compressed Ordinary Object Pointers)失效,对象头占用增大,相同的数据量需要更多内存,性能反而下降。

在jvm.options里修改:

-Xms16g -Xmx16g

必须将初始堆和最大堆设成一样,避免运行期间动态扩堆带来的停顿。

另一个高频问题是指派给ES的堆内存占机器总内存比例过高。假设机器64GB,你分配了50GB给堆,留给Page Cache的只有14GB,ES进行段文件读取和磁盘写缓存时会很吃力,反而拖累查询性能。所以堆大小和Page Cache之间要留足余地。

启动参数里还常见一个坑:Java版本不匹配。ES 7.17自带OpenJDK,但如果你在系统里装了其他Java,而JAVA_HOME指向了旧版本,启动会直接报错。解决方式就是让ES用自己的内置JDK,不要额外设置JAVA_HOME,或者确保其版本满足要求。

补充一个自己踩过的坑:三个节点都配了相同的node.name。集群初始化时后启动的节点一直报“node name already exists”,我当时一脸懵,查了半天才发现三份配置文件复制后没改节点名。节点名是集群内节点的唯一标识,务必不可重复。

5. SpringBoot应用接入集群:客户端选型与读写路由

5.1 客户端选型与连接配置

后端应用接入ES集群,目前最主流的方式是Spring Boot集成。技术选型上有两条路线:Spring Data Elasticsearch,以及官方提供的Elasticsearch Java Client。

我的建议是:如果只是做简单的增删改查和基础搜索,用Spring Data Elasticsearch最省事,它能帮你自动管理文档实体和索引映射,类似JPA的使用方式;但如果要做复杂的查询、聚合、批量写入,直接用官方Java Client,灵活性和性能都好很多。Spring Data底层最终也是调用 RestClient 的,功能封装后的代价是很多高级参数穿透得不够直白。

Spring Boot 3.x配ES 8.x的话,application.yml里这样写:

spring: elasticsearch: uris: - http://192.168.10.11:9200 - http://192.168.10.12:9200 - http://192.168.10.13:9200 username: elastic password: yourpassword connection-timeout: 3s read-timeout: 10s

使用官方Java Client时,创建一个ElasticsearchClient实例:

RestClient restClient = RestClient.builder( new HttpHost("192.168.10.11", 9200, "http"), new HttpHost("192.168.10.12", 9200, "http"), new HttpHost("192.168.10.13", 9200, "http") ).setRequestConfigCallback(builder -> builder.setConnectTimeout(3000).setSocketTimeout(10000) ).build(); ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); ElasticsearchClient client = new ElasticsearchClient(transport);

多节点地址可以全部配置进去,客户端会轮询访问,单个节点故障时自动切换到其他节点。

5.2 路由策略、一致性参数和重试机制

客户端在写入文档时,如果在请求中不指定routing,ES默认拿文档ID做哈希,把文档均匀分布到主分片上,这适合大多数场景。但业务上如果有“同一个用户的数据尽量落在同一个分片”的需求,就需要指定自定义路由,比如routing值为userId。这样同一用户的文档都会落到同一分片,对该用户的数据做聚合查询时,协调节点只需查询单个分片,而不是全分片广播,性能提升非常明显。

写入时有一个参数非常值得关注:wait_for_active_shards。默认情况下只要主分片写入成功就返回成功,这在主分片同步到副本之前出现了节点故障,可能会有数据丢失。为了保证写入可靠性,可以把该参数设为all,要求所有副本分片都写入成功才返回。代价是写入延迟变高,适合金融、订单类高可靠场景,日志类就没必要了。

还有refresh策略。ES写入后默认不是立即可见的,要等refresh间隔(默认1秒)才能查到。Spring Data中可以设置refresh为IMMEDIATE或WAIT_UNTIL,但有性能开销。日志和搜索场景一般用默认,如果业务对实时性要求非常高,再考虑调整。

重试机制上,客户端本身会自动重试连接失败的节点。但要注意,当集群在故障转移期间部分副本不可用,写入容易返回503。服务端和客户端双层的重试逻辑可能导致重复写入,所以写入幂等性设计在业务侧还是要做的,比如带上文档ID,重复写入同一ID会覆盖为相同内容,不会产生脏数据。

6. 集群健康与线上故障排查经验

6.1 三色健康状态不是摆设

ES集群健康状态分三种颜色:green表示所有主分片和副本分片都已正常分配;yellow表示主分片正常分配,但存在未分配的副本分片;red表示至少有一个主分片未分配。看到状态变化,要立刻当回事。

生产环境最常见的是yellow状态。比如某个节点磁盘满了掉线,上面的副本分片在其他节点上可能找不到地方重新分配,于是集群整体变yellow。业务暂时不受影响,因为主分片还能服务,但数据冗余能力已经受损,如果再有节点故障,丢失风险成倍增加。

排查未分配分片的根因,用这个命令:

curl -s "http://127.0.0.1:9200/_cluster/allocation/explain?pretty"

它会很明确地告诉你某个分片为什么无法分配,常见原因是节点磁盘空间不足(watermark)、分配重试次数达到上限、节点不再满足分片过滤规则等。

6.2 生产环境几个高频故障根因

故障一:磁盘水位线导致写入只读。ES默认当磁盘使用率达到85%时,分配器会停止向该节点分配新分片;到90%以上,系统会把相关索引置为只读。很多人遇到业务侧报错“index [xxx] is read-only / allow delete (api)”,第一反应是权限问题,其实是磁盘水位线触发了保护。解决办法不是用API强制改回可写,而是去清理磁盘或扩容,等水位低于阈值后,集群会自动允许恢复写入。

故障二:写入被拒,报ESRejectedExecutionException。这个高频错误表示coordinating节点或data节点上的线程池队列满了。原因往往是瞬时写入峰值过高或有大查询占满了线程。可以临时调大线程池队列,但治本方案是控制批量写入的速率和批量大小。我在日志采集场景里调过bulk size,原来一次500条,改成一次1000条,配合2到3个并发线程,吞吐反而更稳定。

故障三:内存熔断,报CircuitBreakingException。大聚合查询是罪魁祸首,一个带有大量分桶的terms聚合可能把堆内存耗到熔断阈值。排查时先找到吃内存的查询,把它加进慢查询日志里看响应时间,再做分页、限制桶数量、改为近似聚合等优化。必要时增加协调节点前置做请求限流。

故障四:分片恢复速度极慢。节点重启后,数据分片要恢复到集群,如果网络带宽不够,几GB的分片可能恢复几个小时。ES为恢复过程设置了带宽份额和并发数参数,如cluster.routing.allocation.node_concurrent_recoveries。我用默认2,实际恢复时按带宽调大到4或6,但注意不要和正常业务读写抢资源。

6.3 监控指标与日常巡检怎么落地

服务稳定的前提是监控到位。ES自身提供了大量API,不需要额外装组件就能看到核心指标。我习惯每天巡检时跑一遍这几个命令:

curl -s "http://127.0.0.1:9200/_cluster/health" curl -s "http://127.0.0.1:9200/_cat/nodes?v" curl -s "http://127.0.0.1:9200/_cat/indices?v"

_cat/nodes可以看每个节点的cpu、load、堆使用率。最需要警惕的是堆使用率长期超过85%,这很容易引发Full GC。ES 7.x开始更建议观察GC次数和停顿时间,频繁的Old GC就是需要扩容或优化查询的信号。

_cat/indices可以看每个索引的文档数和主分片大小。发现某个索引增长异常快,要及时评估是否需要调整分片数量或生命周期策略。我的习惯是给日志索引装ILM(Index Lifecycle Management)策略,滚动生成新索引,定期删除旧索引,否则集群会被过期数据拖死。

还有一个容易被忽视的指标是段数量。索引的segment数量过多,查询就要遍历大量段文件,性能下降。正常情况下段合并是后台自动做的,但如果写入一直不停,合并且跟不上,段数量会持续上涨。可以查看_cat/segments,如果某个索引段数上千,考虑主动触发_forcemerge,但要在业务低峰期做,这个操作非常消耗IO。

写到这,想起当初第一次搭三节点集群时的狼狈,光是脑裂参数和JVM堆就折腾了两个通宵。现在回头看,这些都是ES集群里最基础却最要命的部分。最后分享一个我自己的运维习惯:每次改集群配置前,先备份elasticsearch.yml和jvm.options,用ansible统一管理三台节点的配置文件,不再手工复制粘贴,权限和内容都固化在代码里。集群这东西,前期多花一小时做规范,后面能少熬好几个晚上的夜。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询