那套日志检索平台是我两年前给一个做电商的客户搭的。当时就一台 8核16G 的机器,Elasticsearch 单节点,数据量很小,nginx 访问日志、订单日志、应用日志全部一股脑往一个索引里灌,日志保留 7 天,跑得挺顺。后来业务量起来,日志从每天 200G 涨到 2TB,客户第一反应是升配:换 16核32G,不够;加到 32核64G,还是慢;最后干脆上集群,从 1 台变 3 台变 5 台变 8 台。结果呢,节点数翻了 8 倍,查询耗时从 3 秒变成 8 分钟,磁盘还天天报警说水位超了 85%。
这次我把这套系统从头捋了一遍,问题出在一个特别朴素的地方:从来没人在索引设计上动过脑子。120 个分片的主索引挤在集群里,副本数设的 1,等于实际有 240 个分片副本在抢磁盘;节点数从 3 台加到 8 台之后,Master Node 因为分片分布不均,把大量查询的扇出都压在了同一批节点上,横向扩容变成了纵向堆积。这篇文章就把这次翻车的过程和后来的解法完整写出来,供同样在做日志平台、监控后端、埋点分析的朋友参考。
想在本地先摸一遍ES分片查询和集群状态,可以参考es client(elasticsearch管理客户端),它带了一个可视化的集群浏览器,不用每次都往浏览器里粘 _cat 系列的接口,索引、分片、副本的分布状态一眼就能看明白,排查分片不均的时候比命令行顺手很多。
在讲具体怎么改之前,先把结论摆出来,后面所有操作都是围绕这几条展开的:
一、先搞清楚 ES 里到底有几个“片”

ES 里有三种东西最容易搞混,很多人在扩容的时候是按“分片数不变”的思路做的,这是错的。
主分片(shard)是索引数据的真正切分单位,一个索引的主分片数在创建时就定死了,创建之后不能改,只能 reindex 迁移。主分片数量直接决定了数据在集群里的横向切分粒度。
副本分片(replica)是主分片的拷贝,作用是高可用和读扩展。副本数可以随时改,包括动态调整,不需要重建索引。副本不是越多越好,每一个副本都是实打实的磁盘占用和写入放大。
分片副本(shard copy)就是主分片 + 副本分片的统称,ES 官方文档里一般叫 copy。
先把这个概念理清,后面的数字才有意义。我们当时的状态是这样的:
GET _cat/shards/logstash-2024.06.01?v
输出里能看到一个索引下面密密麻麻几十行,index 是按天分的没错,每天一个索引,每个索引 6 个主分片 1 个副本。也就是说集群里同时存在 7 天 × 6 主分片 = 42 个主分片,加上副本一共 84 个分片副本。听起来不算多,但问题在于这些分片在 8 台节点上的分布ES 均衡器只保证“尽可能均匀”,而分片大小差异大的情况下,一个节点上可能同时挂着 1 个 180G 的分片和 6 个 2G 的分片。
我给客户看过一张 _cat/allocation?v&s=disk.percent 的输出,当时那张表里有两行 disk.percent 是 91% 和 88%,其余六台都在 35% 以下。也就是说,扩容出来的 8 台机器,有 6 台基本在空转,查询流量全压在那两台上。这就是“加机器没用”的真相:加的是节点,不是均衡。
二、分片大小该怎么定,别拍脑袋

官方文档给的经验值是单个分片控制在 10GB 到 50GB 之间,低于 10GB 属于浪费,超过 50GB 查询和恢复都会变慢。这个数不是玄学,背后是 ES 的 Lucene segment 合并机制和 segment cache 的工作方式决定的,后面我会再展开讲。这里先说怎么算。
分片数 = 单日数据量 ÷ 目标分片大小。比如单日 2TB,目标分片 30GB,那 2048 ÷ 30 ≈ 68,取整到 64。听起来 64 个分片也不算少,但关键是要保证每个分片大小均匀,所以更稳的做法是按时间切:如果单日数据量在翻倍,直接按天或者按小时建索引,而不是靠主分片数硬撑。
我们最后的调整是:主索引改成 ILM 滚动策略,每天一个索引,主分片 16 个,单个分片落在 25G 上下,副本数从 1 改成 1(成本敏感场景保持 1,关键索引上 2)。改完之后集群里的分片副本总数从 84 降到 32,而且分布均匀了。
三、副本数不是越多越安全,扩容前先想清楚
副本数的选择有个很现实的权衡。副本数 = 1 意味着主分片的副本分布在另一台节点上,单节点故障时数据不丢;副本数 = 2 意味着即使有一台机器整体宕机,数据依然可用,但磁盘占用直接翻三倍。
我们最初在 2TB 的量级上把副本设成了 2,理由是“重要数据多备份一份更保险”。结果就是集群总占用从理论上的 4TB 变成了 6TB,而客户给这台机器的磁盘只有 6TB。除去数据和系统本身,剩下的余量连索引滚动都撑不住,水位线报警一个接一个。
换成副本数 1 之后,同样的磁盘能撑将近一倍的时间窗口。这一点值得强调:很多人在“数据安全”上习惯性地堆副本,但 ES 本身有快照(snapshot)机制,历史数据的保护应该交给快照和异地仓库,而不是靠在线副本。副本的首要职责是高可用和读扩展,不是备份。混淆这两件事,很容易让集群在容量上陷入死循环。
如果确实需要读扩展,要注意副本数不能超过节点数,否则副本就会被分配到同一个节点上,起不到分散压力的作用。这也是“副本数改成 3 但集群还是慢”这类问题的常见原因。
四、查询慢,先分清是扇出多还是数据多

集群慢查询排查,我会先跑两个命令把问题拆开。
第一个是慢日志。ES 7 以后慢日志默认关闭,需要动态打开:
PUT /_cluster/settings
{
"transient": {
"index.search.slowlog.threshold.query.warn": "2s",
"index.search.slowlog.threshold.query.info": "1s",
"index.search.slowlog.threshold.fetch.warn": "500ms",
"index.search.slowlog.threshold.fetch.info": "200ms"
}
}
然后去节点日志目录里找 slowlog.json,能看到每个慢查询的耗时拆分:request time、rewrite time、fetch time。这三个时间告诉你瓶颈在哪一段。如果 rewrite time 特别大,说明查询在分片上扇出的数量太多,或者命中了太多小 segment;如果 fetch time 大,说明命中的文档本身太大,需要限制返回字段。
第二个是搜索分析:
GET /logstash-*/_search
{
"size": 0,
"query": { "match_all": {} },
"aggs": {
"per_shard": { "terms": { "field": "_shard", "size": 100 } }
}
}
用 _shard 这个虚拟字段做聚合,能看到查询实际打到了哪些分片上。如果结果里某一个分片的耗时远超其他,那基本可以确定是这个分片所在的节点有问题,要么磁盘 IO 打满,要么那个节点上堆了太多分片。查询扇出到 120 个分片再快也快不到哪去,这是扇出型慢;如果是单分片内部就慢,那是数据型慢,得看磁盘和 segment。
我们后来还加了两个立竿见影的优化。一是 mapping 里把没在查询条件里用到的字段 "index": false,或者直接 "enabled": false,日志场景下这一条能省掉三到四成的索引空间。二是关掉了不需要的动态映射,把 dynamic 设成 strict,防止日志里偶尔蹦出来的新字段触发 mapping 爆炸,把分片拆成一堆碎片。碎片一旦多了,merge 跟不上,查询性能就会一路往下走。
五、磁盘水位:ES 里最容易踩的那三个百分比
ES 7.0 之后把磁盘水位改成了 flood stage 这个概念,三档:
flood_stage.frozen: 95%
flood_stage.read_only: 97%
flood_stage.read_only_allow_delete: 99%
到了 95% 这个档位,受影响索引的索引会被设为只读,分片还会被强制分配到其他节点上,集群进入自我保护状态。这时候你会看到大量 circuit_breaking_exception 和 ClusterBlockException,查不动也写不进去。之前那次「节点加到 8 台还是慢、水位天天报警」的根因就在这:副本占着 6TB,实际水位早就踩线了,集群大部分时间都处于只读边缘状态。
另外 transient 和 persistent 两种设置的区别也经常搞错。transient 的水位配置在节点重启后会失效,persistent 的会写进集群状态里持久保存。生产环境一定要设 persistent,不然某次重启之后保护就没了。
六、如果集群已经烂了,怎么救

分片数不能在已有索引上直接改,这是 ES 的硬限制。所以补救路径只有两条:要么在应用层调整写入策略让新索引用新的分片数(老索引继续滚动淘汰),要么做 reindex 全量迁移。
reindex 的写法大概是这样,注意要带上 conflicts 忽略和 wait_for_completion 参数,大索引直接跑会超时:
POST _reindex?wait_for_completion=false&conflicts=proceed
{
"source": { "index": "logstash-old" },
"dest": { "index": "logstash-new" }
}
跑之前建议先做一次 snapshot,reindex 过程中源索引不要动,不然数据会不一致。我们那次迁移了 3 个索引,前后花了大概 6 小时,期间查询切到新索引,老索引保留观察了一周才删。
还有一个更省事的思路是 ILM(索引生命周期管理)。与其手工建索引,不如让 ES 按 rollover 策略自动切分,配合 delete 阶段自动清理过期索引。我们后来的日志保留策略是:热数据 3 天留在集群,温数据转存到对象存储,温索引保留 30 天。全流程 ILM 托管,不用再人工盯。
最后
回头看这次翻车,最贵的成本不是那台加出来的机器,而是三个月里所有人都以为“机器不够”这件事。
集群性能问题,很大一部分在第一次设计的时候就已经埋下了。分片数定错了,后面加多少节点都是徒劳;副本的意义搞混了,磁盘就会一直吃紧;慢查询不拆开看,盲目扩集群只会让扇出更夸张。写完这篇我又把配置检查了一遍,现在那套系统 5 台机器扛着每天 3TB 的日志,查询 P95 从 8 分钟降到了 1.2 秒,连续跑了半年没再动过架构。
如果你正在做日志平台或者监控后端,建议在最开始就把三件事定下来:单个分片的目标大小、副本数、以及日志的保留周期。这三个数字定了,后面的容量规划和成本核算才有依据。ES 本身能力很强,但它对设计的要求也比一般存储系统高不少毕竟它为了让分布式查询快,把所有事情都摊在了分片这个粒度上。


喜欢
顶
难过
囧
围观
无聊



