☰
Elasticsearch分布式搜索:分片、写入、查询与生产实践
2026/11/5 6:02:52 网站建设 项目流程

上一篇把 ES 的基本盘铺完了:倒排索引怎么建、文档怎么落盘、节点有哪几种角色。但真上手做项目就会发现,那些知识只够让一个单机实例跑起来。数据量一过千 GB、节点数超过三台,问题立刻换了一副面孔:明明返回result: created却查不到,集群莫名其妙变黄,分片卡在UNASSIGNED几个小时不动,翻到第 200 页直接抛异常。这篇“分布式搜索 ES”系列的第二篇,专门拆这些“上生产才会撞上”的部分:分片和副本在物理上怎么摆、一条写入要经过几道关、一次查询为什么要跳两趟、集群在一致性跟可用性之间怎么取值,以及出问题时按什么顺序去看那些命令。

内容偏中高级,适合已经能建索引、会写 DSL,但对着几百个分片和一堆黄色告警无从下手的人。纯新手也能读,只是建议先补一下索引、文档、映射这几个基础概念再回来,不然会有点吃力。下面所有参数和命令都基于 7.x 之后的版本,更早的版本在集群选举和一致性参数上差别不小,别直接套。

1. 分片与副本:分布式的基本单位到底怎么定

1.1 分片不是越多越好,先算清三笔账

很多人第一次建索引,习惯性把number_of_shards设成 10 或者 20,理由是“以后数据会涨”。这个思路在单机上没错,在分布式搜索里是灾难。一个分片在底层就是一个独立的索引库实例,它有自己的段文件、自己的日志文件、自己的文件句柄,还有一份常驻在堆里的段元数据。分片数上去了,每个节点的文件句柄数、堆占用、集群状态体积会同步膨胀,而集群状态是要在主节点之间同步、并且推给每一个节点的。

第一笔账是堆。业内比较稳妥的经验值是每 1GB 堆内存承载 20 个以内的分片,超过这个数,主节点维护集群状态的延迟会明显上升,你会开始看到cluster state update相关的慢日志。一个 8GB 堆的节点,理论上限就是 160 个分片,这个数字包含主分片和副本。

第二笔账是单个分片的大小。业界共识区间是10GB 到 50GB,我个人的偏好是 20~30GB。太小了分片数量爆炸,每次查询要打到几十上百个分片上做归并,协调节点的 CPU 直接被打满;太大了单分片恢复时间变长,一个 100GB 的分片从磁盘重建副本可能要几十分钟,这段时间里集群是降级状态。

第三笔账是磁盘余量。底层合并段的时候,同一份数据在磁盘上会短暂存在两份,加上副本本身占空间,实际占用经常是原始数据的 2.2~2.5 倍。所以规划磁盘时别按 80% 算,留到 60%~70% 比较稳。默认的低水位线是 85%、高水位线 90%、洪泛线 95%,一旦越过洪泛线,索引会被强制设成只读,写入直接失败,那个报错我见过太多次了。

举个实际算过的例子:原始数据 2TB,1 个副本,目标是单分片 25GB 左右。2TB / 25GB ≈ 80 个主分片,取个整齐的数 72 或者 96 都行(能被节点数整除更好)。选 72 主分片 + 1 副本 = 144 个分片,如果 6 个数据节点,每个节点 24 个分片,堆给到 8GB 以上就很舒服。整套算下来,磁盘至少准备 5TB 可用空间。这个推导过程比记住任何结论都重要,因为你的数据量、节点数、读写比都跟别人不一样。

1.2 主分片数创建后就锁死了,但有三条绕路

这是 ES 里最容易被新手忽略的一条硬约束:主分片数量在索引创建时确定,之后不能直接改。原因跟下面的路由公式直接相关,改了分片数,所有文档的路由结果都会变,相当于全量迁移。但这不代表没有补救办法,实际项目里常用三条路。

第一条是拆分索引(split)。它把一个索引的每个主分片一分为二或一分为多,前提是索引必须设成只读、所有分片必须在同一个节点上(底层用的是硬链接),而且新的分片数必须是原分片数的整数倍。这条路的限制很硬,实际用得不多,通常只在小索引上做。

第二条是收缩索引(shrink)。把分片数缩下来,同样是只读 + 同节点。适合那种一开始分多了、数据其实没涨起来的场景。我见过一个项目把 20 个分片缩到 5 个,查询延迟降了将近一半,纯粹是因为归并的活儿少了。

第三条是重建加迁移(reindex),也是最常用、最没副作用的。新建一个分片数正确的索引,把映射和设置拷过去,用_reindex把数据搬过去,然后切别名。这个方案的好处是顺便能改映射、能重排字段、能调整刷新间隔,等于给索引做一次全面体检。代价是要占用一份额外的磁盘和一段时间的集群算力,所以务必在低峰期跑,并且把_reindex的requests_per_second限速打开。

三条路的对比:

方案能否改分片数前提条件磁盘开销适合场景
_split只能增加,且为整数倍只读、同节点、硬链接支持约 1 份新数据小索引快速扩容
_shrink只能减少,且为整数因子只读、同节点约 1 份新数据分片设多了想回收
_reindex任意无硬性限制约 1 份新数据 + 迁移算力附带改映射、初始化新索引

提示:无论走哪条路,最后一定要用别名(alias)切换,让业务代码永远读别名而不是物理索引名。这一步做在前面,后面所有重建、迁移、滚动升级都是零停机。

1.3 副本不只是备份,它还扛读

很多人把副本单纯理解成高可用,其实它的作用有三个。第一是故障转移,主分片所在节点挂了,副本能被提升为新的主分片。第二是读吞吐,查询默认会在主分片和副本之间轮询,1 个副本等于查询并发翻倍。第三是隔离,你可以把副本强制调度到另一批机器上,让主副本不落在同一个机架。

但副本不是白给的。每个副本占一份完整磁盘,写入时主分片处理完还要转发给所有副本、等它们确认,副本越多写延迟越高。经验值是线上稳定服务 1 个副本就够,对读并发特别高的索引可以给到 2 个。

有一个非常实用的技巧:大批量灌数据的时候,先把副本数临时设成 0,灌完再调回来。因为初始化过程中每个副本都要做一次完整的段复制,这段时间的写入基本没法用。具体操作就是先PUT /index/_settings把number_of_replicas设为 0,灌完数据再设回 1,让副本在系统空闲时慢慢同步。这个操作我几乎每个项目都会用,能省掉大量“批量导入时集群卡住”的抱怨。

2. 一条文档的写入旅程:从客户端到磁盘

2.1 路由计算决定了文档落在哪一片

写入的第一步不是写,是算。协调节点拿到一条文档,先用公式算出它该去哪个分片:

shard_num = hash(_routing) % number_of_primary_shards

_routing默认就是文档的_id。这个公式解释了两件事:为什么主分片数不能改(改了分母,所有文档位置全变),以及为什么自定义路由要慎重(要是按某个业务字段路由,这个字段分布不均就会造成热点分片)。

自定义路由的价值在于“把需要一起查的数据放在同一个分片上”。比如订单和订单明细,用订单号做路由,两者会落到同一个分片,查询时只打一个分片,不用跨片归并,性能提升非常直观。反过来,如果你的路由字段取值高度集中——比如默认值、空串、某个大客户 ID——那所有数据会挤在一个分片上,其他分片闲着,这就是典型的“热门分片”问题。要做自定义路由,先跑一遍字段的基数统计。

注意:搜索请求可以通过preference参数指定优先使用哪些分片或节点。常用的_local表示优先用本节点上的分片,能省一次网络往返,适合批处理任务;_primary强行走主分片,一般用于调试;还可以传自定义字符串把同一用户的查询固定到同一组分片,提高缓存命中率。

2.2 主副本同步:什么才算写入成功

完整的写入链路是这样的:协调节点路由到主分片所在节点,主分片先做校验——映射字段类型对不对、有没有触发字段数上限、版本号有没有冲突、有没有超长字段。校验通过后,数据同时写进内存缓冲区和事务日志,然后把请求并行转发给所有副本分片。副本各自完成后回 ACK,主分片收到足够数量的确认,才向客户端返回成功。

“足够数量”就是wait_for_active_shards控制的。默认值是1,意思是只要主分片写成功就算成功,不管副本。这个默认值的含义是:如果这时候主分片所在节点立刻宕机,而副本还没来得及同步,这条数据就丢了。想更安全可以设成all或者具体数字,代价是可用性下降——副本不够的时候写入会直接失败。

我个人线上项目的做法是:绝大多数索引保持默认1,对于订单、支付这类绝对不能丢的索引设成2(对应 1 个副本,即等待至少一个副本确认)。这是一个很实际的权衡:设成all意味着任何一个副本掉线整个索引就写不进去,风险和收益不对等。

写入还有两个容易被忽略的校验点。一是字段映射的dynamic策略,如果设成strict,出现未定义字段会直接报错;设成true(默认)则会自动推断类型,长期跑下来很可能出现同一字段半数是long半数是text的惨剧,然后聚合就报错了。二是单条文档大小,默认超过 100MB 会被拒,但这个上限意义不大,真实建议是单文档别超过 10MB,超大的文本块拆成多条存。

2.3 refresh、flush、translog、merge 这四件事别搞混

这是面试高频题,也是实际排障时最容易混淆的一组概念。我用一个类比说清楚:把索引库当成一个笔记本,内存缓冲区是写在草稿纸上的内容,段是正式装订的页码,事务日志是边写边记的流水账,磁盘上的提交点是合订本的目录。

refresh(刷新):把内存缓冲区里的内容写成一个新的段,让搜索能看见。它默认每秒执行一次,这就是 ES 号称“近实时”的来源,不是实时,是 1 秒级别的延迟。refresh 跟持久化没关系,它只解决“可见性”。

translog(事务日志):每次写入都会追加到日志里,默认每次请求都 fsync。它是真正的持久化保障,节点崩溃重启后靠它把内存里的数据补回来。可以改成异步刷盘(index.translog.durability: async),代价是崩溃时最多丢一个间隔(默认 5 秒)的数据。

flush(刷盘):做一次完整的提交,把段 fsync 到磁盘、写提交点、清空事务日志。触发条件是事务日志达到512MB(index.translog.flush_threshold_size)或者每隔 30 分钟。所以并不是每次 refresh 都会 fsync。

merge(合并):后台把很多小段合并成大段,同时清理掉被标记删除的文档。段越少,查询越快,但合并本身非常吃磁盘 IO 和 CPU。刚灌完大批数据的时候查询慢,八成是在合并。

动作触发时机解决什么主要开销可调参数
refresh默认每 1 秒写入可见生成小段,段数膨胀index.refresh_interval
translog 落盘每次请求(默认)崩溃恢复磁盘同步index.translog.durability
flush日志满或定时提交点持久化强制 fsync,短时 IO 抖动index.translog.flush_threshold_size
merge后台自动减少段数、清理删除CPU + 磁盘 IOindex.merge.scheduler.max_thread_count

理解了这四者,很多东西就顺了。比如批量灌数据时把index.refresh_interval设成-1,就是为了不生成几千个小段,等灌完再手动 refresh 一次,段的生成数量能少一两个数量级,后面的合并压力也小得多。又比如查询性能突然下降,先看段数量,如果某分片段数是别人的十倍,那大概率是 refresh 太频繁或者合并被限速太狠。

关于合并限速,机械盘上是必须配的。默认情况下合并线程会用满磁盘带宽,导致正常的写入和查询全部被拖慢。设置index.merge.scheduler.max_thread_count: 1在机械盘上是很常见的做法,SSD 上可以留默认。另外,对于确定不再写入的历史索引(比如按月归档的日志索引),做一次强制合并(_forcemerge?max_num_segments=1)能把段压成一个,查询速度提升非常明显,但这一步非常吃资源,一定要在业务低峰做,并且只对只读索引做。

3. 一次查询的两次跳转:query then fetch

3.1 查询阶段和取回阶段分别干了什么

搜索不是“在集群里找一遍”,而是跳两趟。

第一趟叫查询阶段。协调节点把请求广播到索引的所有分片(主副轮询),每个分片在本地执行查询,算出匹配的文档 ID 和排序值,但只返回前from + size个 ID 和排序值,不返回文档内容。协调节点把所有这些结果汇总、全局排序,挑出真正要返回的那size条。

第二趟叫取回阶段。协调节点根据文档 ID 反推出它所在的分片,向对应分片发起批量取回请求,拿到_source,组装成最终结果返回。

这个设计的好处很明显:网络上传的只是 ID 和排序值,不是完整文档,带宽省了一大截。坏处也藏在细节里:每个分片都要返回from + size条。假设 10 个分片,你要第 100 页(from=9900, size=100),每个分片都要吐 10000 条 ID 给协调节点,加起来就是 10 万条,协调节点还要对 10 万条做一次全排序。这就是深分页昂贵的根本原因,不是数据多,是放大系数。

3.2 为什么相关性打分在分片之间会飘

打分依赖词频和逆文档频率,而逆文档频率需要“全局文档数”和“包含该词的文档数”。问题在于,每个分片只能看到自己那部分数据,算出来的是局部频率。同一个查询打到不同分片,同一个词的 IDF 不一样,最终得分自然有偏差。

如果你的业务对排序稳定性要求很高,有三条路。第一条是加search_type=dfs_query_then_fetch,先做一次预查询把全局词频统计出来再算分,代价是多一轮往返,分片多的时候延迟会明显增加,一般不建议线上常开。第二条是让索引只有一个分片,小数据量(几十 GB 以内)的场景完全可行,一致性最好。第三条——也是我实际项目里用得最多的——别太依赖纯文本相关性。真实业务里排序通常是“相关性 + 业务权重”的组合,用function_score把销量、时间、权重系数显式加进去,比纠结 TF-IDF 的小数点后第三位有意义得多。

提示:用filter上下文包裹的查询不参与打分,还能命中查询缓存。凡是“是/否”的筛选条件(状态、类目、时间区间),一律放进bool.filter,这是性价比最高的一条优化。

3.3 深分页的三种姿势,别用错场景

from + size有硬上限,默认index.max_result_window是 10000。超过就报Result window is too large。这个上限是可以改的,但改大的代价是协调节点的堆内存成倍消耗,超过几万的偏移量基本就是自找麻烦。

正确做法有三种。滚动(scroll)适合一次性导出全量数据,它会在服务端保留一个快照,然后一批批往下拉。缺点是快照占用资源、不反映实时数据,而且要记得手动清理,超时时间设得太长会拖垮集群。search_after适合“下一页”式的连续翻页,它用上一页最后一条的排序值作为游标,不需要维护服务端状态,效率很高。缺点是只能一页页往下走,不能跳到第 50 页。时间点(PIT)+ search_after是目前推荐的组合,解决了search_after在翻页过程中数据变动导致的重复或漏读问题。

方案能跳页服务端状态实时性典型场景
from/size能无实时前几页浏览
scroll不能有,需手动清理快照全量导出、离线任务
search_after不能无实时无限下拉
PIT+search_after不能有,有 TTL一致快照深度翻页 + 一致性

用search_after有个硬要求:排序字段必须是唯一的,或者在最后补一个_id做兜底,否则排序值相同的文档会被跳过或重复。这个坑我踩过一次,翻页时偶发丢数据,排查了半天才定位到排序键不唯一。

4. 一致性、可见性与并发控制

4.1 写入一致性:在数据安全和可用性之间取值

前面提过wait_for_active_shards,这里展开说一下怎么选。这个参数的本质是“我愿意为了数据安全牺牲多少可用性”。

设成1(默认):只要主分片活着就能写,性能最好,风险是主分片刚写完就挂、副本还没同步,这条数据就没了。这个窗口非常小(毫秒级),对绝大多数业务可以接受。

设成具体数字2:要求至少一个副本确认。适合绝对不能丢的场景,代价是副本全部掉线时写入会失败。这里要提醒一句,副本掉线是很常见的事——节点重启、磁盘抖动、网络闪断都会触发副本重建,那段时间如果卡了2,整个写入链路就堵了。所以设成2的索引,一定要配合足够的副本数和合理的超时。

设成all:所有副本都确认才算成功。我基本不推荐,收益太小,风险太大。

配套的还有index.write.wait_for_active_shards,可以在索引设置层面设默认值,免得每次请求都写一遍。另外,这个参数只控制“等待多少个分片活跃”,不保证数据已经同步到副本,真正的数据安全还得靠副本数 + 事务日志落盘策略一起看。

4.2 近实时可见性:快和准的取舍

写入成功后立刻查,默认是查不到的,因为还没 refresh。这在很多业务流程里会造成困惑,比如用户下单后跳转到订单列表,发现刚下的单不在里面。

ES 给了三个层级的控制。refresh=true表示写入后立即刷新,能马上查到,代价是每次写都生成一个小段,写入吞吐直接腰斩,只适合极低频操作。refresh=wait_for表示请求挂起,等下一次自动刷新完成后返回,既不额外生成段,又能保证返回时数据可查,这是读后写场景的最优解。refresh=false就是默认行为,写完就返回,不保证可见。

我做过一次压测对比,同一个集群,全部用refresh=true和全部用refresh=wait_for,前者写入吞吐只有后者的三分之一左右,而且段数量涨得飞快,后续合并压力巨大。所以除非是极端场景,一律优先wait_for。

另外要注意refresh=wait_for的默认超时是一分钟,而且如果索引的refresh_interval设成了-1(批量导入时的常见设置),这个请求会一直挂到超时。这个组合出过不少线上问题,用之前先确认刷新间隔。

4.3 并发写冲突:两种版本控制怎么选

ES 的并发控制是乐观锁。每个文档有_seq_no和_primary_term两个字段,写入时可以带上if_seq_no和if_primary_term,如果文档在这期间被别人改过,请求会返回 409 冲突,让你重新读取再写。这是最精确的做法,适合“读-改-写”的更新逻辑。

另一种是外部版本号(version_type=external),把上游系统的版本号(比如时间戳)带进来,ES 只接受版本号更大的写入。这个方式特别适合做数据同步:上游数据库有update_time字段,同步程序把时间戳当版本号写进 ES,重复消费、乱序消费都不会造成脏数据。这是我从一个数据同步项目里学到的最实用的一招,比在应用层做去重简单得多。

冲突了怎么办?如果业务能容忍,直接重试几次就够了;如果是批量更新,用_update_by_query的时候带上conflicts=proceed让它跳过冲突文档,比整个任务失败好得多。但要记住,跳过意味着数据可能不一致,得靠后续的全量校对兜底。

5. 容量规划与索引设计:把问题挡在事前

5.1 从数据量反推节点数和堆大小

这一块是很多人拍脑袋的重灾区。我给一个可落地的推导顺序。

先定堆。堆的设置有一条硬规则:不要超过 31GB,因为超过之后指针压缩失效,实际可用内存反而下降。另一条经验规则是堆不超过物理内存的一半,剩下的留给文件系统缓存——搜索性能极度依赖页缓存,把内存全给堆是典型的错误做法。所以一个 64GB 内存的机器,堆给 31GB 是合理的;一个 32GB 的机器,堆给 16GB。

再定分片。用总数据量除以目标分片大小(20~30GB),得到主分片数,然后保证主分片数 × (1 + 副本数) ÷ 节点数 ≤ 每 GB 堆 20 个分片。如果算出来超标,要么减少分片数(把单分片调大),要么加节点。

最后定磁盘。总数据量 × (1 + 副本数) × 1.3(合并余量),得到的是可用空间下限,再反推需要多少块盘。别忘了一旦某个节点磁盘越过 85% 低水位,分片就不会再往它上面分配了;越过 95% 洪泛线,它上面的索引会变只读。我见过最惨的一次事故,就是磁盘预留不足,灌数据灌到 95%,几个索引同时变只读,业务全线写入失败,只能临时扩盘解决。

5.2 映射里最烧钱的几个默认值

映射设计的坑,几乎都和“默认”有关。动态映射会因为一条脏数据给某个字段推断出错误的类型,之后所有写入都被拒;文本字段默认会同时建text和keyword两个子字段,存储直接多一份;给不需要检索的字段保留了索引,写放大白白增加。

我通常的调法是这样几条。只用于精确匹配和聚合的字段,直接上keyword,顺手加ignore_above: 256防止超长值撑爆索引。只用于展示、不需要检索的大字段(比如详情 HTML),index: false并且关掉doc_values,能省下可观的空间。需要全文检索的字段用text,但配一个keyword子字段用于排序,别忘了给排序字段设norms: false。数值字段如果只用于筛选、不做范围计算,可以适当选更小的类型,integer能解决的别用long。

还有一个必须关掉的东西:text字段上的fielddata。默认是关的,但总有人为了在text上做聚合把它打开,然后某天一个大聚合把整个节点的堆打满。要做文本聚合,就用keyword子字段,这是唯一正确的做法。

配置项默认值建议影响
dynamictrue生产索引设strict防止脏字段污染映射
indextrue仅展示字段设false省写入放大与磁盘
doc_valuestrue不聚合不排序的字段设false省列式存储空间
ignore_above无keyword设 256防超长值撑爆索引
normstrue仅筛选用途设false省每文档一字节
fielddatafalse永远不在text上开极易 OOM

5.3 冷热分层:让老数据不再抢资源

数据是有温度分布的。最近三天的日志每小时被查几百次,三个月前的可能一年都不看一次,但两者在集群里占着同样的资源。冷热分层的思路是把它们分开。

具体做法是给节点打上属性标记(比如node.attr.tier: hot和node.attr.tier: warm),然后在索引上设index.routing.allocation.require.tier: hot,强制分片只能落在对应节点。再配一套生命周期策略:热阶段保持正常刷新和 1 个副本,超过 7 天进温阶段,合并段、降副本、迁移到温节点,超过 90 天直接删除。

温节点可以用配置低一些的机器,甚至用大容量机械盘,因为这部分数据只读不写,对 CPU 和 IO 的要求低得多。我做过一个日志项目,冷热分层之后热节点数量砍掉一半,查询 P99 反而降了,因为热节点上的分片少了,集群状态和归并压力都小了。

生命周期的策略模板大概长这样,可以直接改改就用:

PUT _ilm/policy/logs_policy { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_primary_shard_size": "30gb", "max_age": "1d" }, "set_priority": { "priority": 100 } } }, "warm": { "min_age": "7d", "actions": { "forcemerge": { "max_num_segments": 1 }, "shrink": { "number_of_shards": 1 }, "allocate": { "require": { "tier": "warm" } }, "set_priority": { "priority": 50 } } }, "delete": { "min_age": "90d", "actions": { "delete": {} } } } } }

要注意forcemerge和shrink都很重,一定要放在温阶段而不是热阶段,并且错开业务高峰。还有rollover的别名必须指向单个索引、并且别名得带is_write_index标记,这个细节漏了会一直抱错。

6. 生产环境常见故障排查实录

6.1 集群变黄变红,按这个顺序看

先说颜色含义,很多人会记反。绿色是所有主分片和副本都分配好了;黄色是所有主分片都在,但有副本没分配;红色是有主分片没分配,意味着有数据查不到。所以黄色通常不致命(数据完整,只是冗余度不够),红色必须立刻处理。

排查顺序我固定是四步。第一步看整体,GET _cluster/health?level=indices,直接定位是哪个索引出问题。第二步看分片清单,GET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason加个过滤只看未分配的。第三步对具体分片做诊断,GET _cluster/allocation/explain,这个接口会把“为什么没分配”讲得非常清楚,包括所有节点的拒绝理由。第四步才是动手,根据原因决定是调水位、调过滤条件、还是手动重路由。

# 1. 集群整体与逐索引健康状况 GET _cluster/health?level=indices # 2. 只看未分配的分片及其原因 GET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason&s=state # 3. 让集群自己解释为什么不分 GET _cluster/allocation/explain # 4. 节点磁盘与分片分布 GET _cat/allocation?v GET _cat/nodes?v&h=name,heap.percent,disk.used_percent,load_1m

注意:不要看到黄色就急着手动重路由。手动_reroute会绕过分配器的判断,可能把分片塞到一个本来就快满的节点上,几天后引发更大的问题。先让allocation/explain把话说完。

6.2 未分配分片的典型原因速查

原因标记实际含义常见触发处理方向
NODE_LEFT节点离线重启、宕机、网络分区等节点回来,或调副本数
DISK_THRESHOLD磁盘超水位数据增长未及时扩容清理或扩盘,临时调水位
ALLOCATION_FAILED分配失败段损坏、映射冲突看 explain 详情,必要时恢复快照
CLUSTER_RECOVERED集群刚恢复全量重启耐心等,别急着干预
REPLICA_ADDED新增副本调高了副本数等同步完成
NO_VALID_SHARD_COPY无可用副本主副本同时丢失从快照恢复
AWAITING_INFO等待信息主节点刚选举完等几分钟

这张表我贴在运维文档里,出问题对着查,八成能直接定位。特别说一下DISK_THRESHOLD,如果只是临时超了、马上要扩盘,可以临时把水位调高应急,处理完记得调回来:

PUT _cluster/settings { "transient": { "cluster.routing.allocation.disk.watermark.low": "90%", "cluster.routing.allocation.disk.watermark.high": "93%", "cluster.routing.allocation.disk.watermark.flood_stage": "97%" } }

调完记得把索引的index.blocks.read_only_allow_delete解除,否则即使水位降下来,索引还是只读的。这一步最容易漏,我见过有人调了水位、等了半天还是写不进去,最后发现只读锁没解。

6.3 写入被拒绝:线程池和批量大小

报错es_rejected_execution_exception说明线程池队列满了。写线程池的队列默认是 200,搜索线程池默认是 1000,队列一满就直接拒绝,不做等待。这其实是很好的保护机制——与其让请求堆积把节点拖死,不如快速失败让客户端重试。

遇到这个错,先分清是写入还是查询。写入被拒通常是三种情况:批量请求太大(一次几万条)、集群在合并段、副本同步跟不上。对应的处理是把批量请求控制在 5~15MB,别按条数算,按体积算;检查段数量和合并状态;适当降低并发。

这里有个反直觉的点:批量导入时并发数不是越高越好。我做过对比,8 个并发灌数据比 32 个并发的总耗时更短,因为高并发下大量请求在队列里排队、触发拒绝和重试,实际有效吞吐反而下降。找到那个拐点比一味加并发有用得多。

6.4 查询变慢:一条从外到内的定位路径

查询慢的原因多得数不清,我习惯按“从外到内”的顺序排查。先看是不是深分页或大批量聚合(from + size太大、聚合桶太多);再看是不是查询本身写得烂(前缀通配符*xxx、正则、脚本查询);然后看段数量(某分片段数远超别人说明合并跟不上);最后看资源和缓存(堆压力、查询缓存命中率、页缓存是否够)。

性能剖析接口(profile)能给出每个子查询的耗时占比,比自己猜高效得多。慢查询日志也一定要开, thresholds 设成 warn 1s、info 500ms 这种量级,事后复盘全靠它。

PUT /my_index/_settings { "index.search.slowlog.threshold.query.warn": "1s", "index.search.slowlog.threshold.query.info": "500ms", "index.search.slowlog.threshold.fetch.warn": "500ms" }

还有一个很容易被忽略的点:分片分布不均。如果某个节点上的分片数明显多于其他节点,它的负载就更高,查询会拖后腿。用_cat/allocation看一眼就知道,必要时用分配过滤或者重平衡来匀一匀。

7. 一份能直接抄的巡检清单与参数模板

巡检这事儿,与其每次出问题现查,不如固定成清单,每天跑一遍。我用的清单大致分四块。

集群层面看健康状态、节点数、主节点是否稳定(有没有频繁选举——那通常意味着网络或 GC 有问题)。资源层面看堆使用率(长期 75% 以上就该扩容了)、磁盘使用率(85% 是红线)、负载。索引层面看分片数、段数量、未分配分片、只读索引。业务层面看写入拒绝次数、慢查询数量、P99 延迟。

对应的几条常用命令:

GET _cluster/health GET _cat/nodes?v&h=name,role,heap.percent,disk.used_percent,load_1m,node.role GET _cat/indices?v&h=index,health,pri,rep,docs.count,store.size&s=store.size:desc GET _cat/shards?v&h=index,shard,prirep,state,node&s=state GET _nodes/stats/thread_pool,indices/fielddata?human

一个我反复用的索引模板,把前面说的那些“该关的关掉、该限的限上”都写进去了:

PUT _index_template/app_template { "index_patterns": ["app-*"], "template": { "settings": { "number_of_shards": 6, "number_of_replicas": 1, "refresh_interval": "5s", "index.mapping.total_fields.limit": 1000, "index.max_result_window": 10000, "index.merge.scheduler.max_thread_count": 1, "index.translog.durability": "request", "index.search.slowlog.threshold.query.warn": "1s" }, "mappings": { "dynamic": "strict", "properties": { "title": { "type": "text", "fields": { "raw": { "type": "keyword", "ignore_above": 256 } } }, "category": { "type": "keyword" }, "price": { "type": "scaled_float", "scaling_factor": 100 }, "created": { "type": "date" }, "detail": { "type": "text", "index": false } } } } }

刷新间隔设成 5 秒而不是默认的 1 秒,是我在多数业务索引上的默认选择——写入吞吐能提升一截,而 5 秒的可见性延迟在业务上基本感知不到。只有那些强读后写场景的索引才需要单独调回 1 秒,并在写入时用wait_for兜底。

批量灌数据的时候,除了前面说的副本设 0、刷新关掉,还有两个配套动作值得一起做:把index.translog.durability临时改成async,减少每次请求的 fsync;灌完之后手动 refresh 一次、把副本调回来、再跑一次强制合并(如果是只读的历史数据)。这套组合拳下来,同样数据量的导入时间通常能压缩到原来的三分之一左右。


最后分享一个小习惯:我每接一个新集群,第一件事不是建索引,而是把上面那几条巡检命令跑一遍,记下当时的堆、磁盘、段数量作为基线。后面出任何异常,跟基线一比,问题范围立刻缩小一大半。这套做法比任何监控大盘都来得直接,因为它用的是你自己的数据,而不是别人的经验值。

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

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

立即咨询