维护日志链路的朋友大概率都遇到过这种场景:应用明明打的是标准JSON,到了Kibana里一看,有的字段变成了一长串嵌套路径,有的关键字段被Filebeat自带字段覆盖了,还有一堆调试用的冗余字段把索引空间撑得很大。标题里拆出来的本质上是三个需求:去掉JSON日志里的部分属性、把嵌套JSON拍平、按业务需求拆分JSON里的内容。这篇就围绕这三个问题,用Filebeat完整走一遍配置、排错和落地思路。
先说清楚这篇内容解决什么:不依赖Logstash、不写ES端复杂管道,尽可能用Filebeat自带的解析能力和处理器(processor)完成JSON日志的采集、字段裁剪、扁平化与拆分。适合正在用Elastic Stack做日志平台、被同事吐槽“Kibana里字段没法看”的开发者,也适合刚接手Filebeat、想知道json.keys_under_root和decode_json_fields到底怎么选的新手。
1. 项目概述与需求拆解
1.1 标题背后的三个核心需求
表面看这只是“Filebeat采集JSON日志”,但结合实际运维场景,诉求其实非常具体。
第一是“去掉部分属性”。业务日志里经常混入大量不必要的内容:调试用的堆栈、内部上下文字段、请求链路里的大对象。如果全部写入Elasticsearch,存储成本会涨,索引mapping也会被这些脏字段撑出一堆动态映射。所以要做字段裁剪。
第二是“JSON日志扁平化”。默认情况下,Filebeat把JSON解析后,如果内容里还有嵌套对象,进入ES后会形成嵌套结构,比如context.orderId、context.userId。这带来的问题是:查询时要写很长路径,Kibana可视化里字段展示很别扭,Painless脚本里取字段也要写完整路径。把嵌套路径拍平成orderId、userId这类顶层字段,就是所谓的扁平化。
第三是“拆分JSON日志”。这个词最容易产生歧义。一种理解是把嵌套对象里的字段拆出来变成独立顶层字段,这其实和扁平化是一回事;另一种理解是日志内容里有一个数组,数组里每个元素代表一条独立业务事件,要把一条日志拆成多条记录。这两种“拆分”的实现方式完全不同,后面会分别说清楚。
1.2 为什么这件事要用Filebeat做
日志从产生到进入Elasticsearch,一共有四个可以干预处理的位置:应用自身、Filebeat、Logstash、ES的Ingest Pipeline。
应用自身处理是最理想的,但现实很骨感:日志格式由业务代码决定,采集链路的维护者往往改不动别人的代码,或者改一发版要排很久。Logstash虽然处理能力强,但中间多一跳组件,部署、监控、维护都多一摊事。ES的Ingest Pipeline要占ES节点CPU,日志量一大容易拖慢写入。
Filebeat的优势在于:资源占用极小,配置灵活,自带的processor已经能覆盖绝大多数JSON处理需求。对中小规模业务来说,直接在Filebeat里把解码、裁剪、扁平化都做完,是性价比最高的方案,数据在传输前就瘦身了,带宽和存储都省。
1.3 明确处理链路与输出目标
为了后面演示不跑偏,先把目标链路定下来:Filebeat读取特定目录下的日志文件;按行解析JSON;删除明确不需要的冗余字段;对嵌套对象做扁平化;将业务时间戳转换为@timestamp;最后输出到Elasticsearch。整条链路只依赖Filebeat本身,不引入Logstash。
这个链路里最关键的是搞清楚每个处理动作的顺序,因为processor是按顺序执行的,前一个的输出就是后一个的输入。顺序错了,结果会千奇百怪。这个点我会在后面的实战配置里反复强调。
2. Filebeat环境与配置基础
2.1 filebeat.yml的配置层级
展开配置之前,先把Filebeat的配置模型讲清楚。filebeat.yml里四个核心层级:filebeat.inputs定义从哪读、parsers或input配置项定义怎么解析、processors定义事件进入输出之前怎么加工、output定义最终发到哪。
一个不错的类比是工厂流水线:input是原料入口,parser是拆箱机,processor是流水线上的一组工位(去杂质、贴标签、改包装),output是出货口。每个工位按顺序工作,前面工位的产物就是后面工位的输入。
2.2 两种JSON解码方式对比
Filebeat处理JSON日志,常见的解码方式有三种,很多人一上来就混用,导致结果莫名其妙。
第一种是log input的json配置项,经典但偏老。配置写法是:
filebeat.inputs: - type: log paths: - /var/log/app/*.log json.keys_under_root: true json.overwrite_keys: true json.add_error_key: true json.message_key: "message"json.keys_under_root: true表示把JSON最外层字段直接放到事件的根级别;json.overwrite_keys: true表示如果业务JSON里有字段和Filebeat自带字段同名,允许覆盖;json.add_error_key: true表示解析失败时加一个error字段,便于排查。
第二种是filestream input的ndjson parser,这是新版本推荐的方式:
filebeat.inputs: - type: filestream id: app-json-logs paths: - /var/log/app/*.log parsers: - ndjson: target: "" overwrite_keys: true add_error_key: truefilestream input是Filebeat 7.x开始重点发展的事件流式采集方式,但它不支持log input的json.*配置,改用parsers里的ndjson。target设为空字符串,效果等同于json.keys_under_root: true。
第三种是用decode_json_fields processor,这是最灵活的方式,也是我比较推荐的方式:
processors: - decode_json_fields: fields: ["message"] target: "" overwrite_keys: true它做的事情是把事件里message字段的内容当作JSON解析,然后按target参数决定放哪里。与input级json配置最大的区别是:decode_json_fields是processor,可以和其他processor任意组合,也可以用条件判断控制它只对某些事件生效。
三种方式的取舍,用表格看更直观:
| 对比项 | log input的json配置 | filestream的ndjson parser | decode_json_fields processor |
|---|---|---|---|
| 适用input | log | filestream | 两者都可用 |
| 作用时机 | input读取阶段 | input读取阶段 | 事件处理阶段 |
| 能否与其他processor自由组合 | 受限 | 受限 | 可以 |
| 能否对部分事件生效 | 否 | 否 | 可以 |
| 版本趋势 | 老项目常见 | 新项目推荐 | 通用 |
实际项目中我的习惯是:老版本存量配置用json.keys_under_root不动它,新写的配置统一用filestream input加decode_json_fields processor,这样处理逻辑都集中在一个地方,排查问题不用翻好几个配置段。
2.3 processor执行顺序的关键性
processors数组里的顺序就是执行顺序,这一点极其重要。举个例子,如果你先做drop_fields删掉message字段,再做decode_json_fields去解message,这时message已经没了,decode会跳过或报错。反过来,先decode把message里的内容解析成字段,再drop_fields删掉冗余字段,逻辑才是通的。
我在真实项目里见过最典型的错误是:在decode_json_fields之前加了一个rename,把message改名了,结果decode_json_fields找不到字段,整个JSON解析静默失败,原始字符串原封不动进了ES。排查流程走了好久,最后发现就是一行配置顺序的问题。
2.4 字段保留策略的隐藏风险
当JSON字段被放到事件根级别后,事件里会同时存在三类字段:Filebeat内部字段(@timestamp、@metadata、host、log、agent等)、业务字段、可能存在的ECS字段。做字段裁剪前,必须清楚哪些字段是ES和Kibana运行依赖的。
比如include_fields白名单,如果fields列表里忘了@timestamp,那么@timestamp会在处理中被过滤掉,日志写入ES后时间轴直接乱掉。这个坑很多人踩过,而且排查难度高,因为数据能进ES,看起来一切正常,就是Kibana时间不对。做白名单保留时,默认把@timestamp、log、host这类系统字段带上再删业务字段,会稳妥很多。
3. JSON日志扁平化:核心实操
3.1 为什么日志要扁平化
拿一条实际日志举例,原始JSON长这样:
{ "timestamp": "2024-01-15T10:30:00Z", "level": "INFO", "logger": "OrderService", "message": "订单创建成功", "context": { "orderId": "ORD-12345", "userId": 10086 } }如果不做扁平化,context整体会作为一个嵌套对象进入ES,Kibana字段列表里看到的就是context.orderId、context.userId,查询要写完整点号路径,建看板时要一层层展开。如果业务里嵌套三层四层,mapping会越来越乱,动态映射产生的字段也会暴涨。
扁平化的目标是让context.orderId变成orderId这样的顶层字段,或者至少变成带前缀的扁平字段,让查询和展示都直观。
3.2 target: ""实现的是“浅扁平化”
很多人误以为decode_json_fields的target设为空字符串,就能把所有嵌套层级都拍平。实际上target: ""只是把JSON最外层的字段提升到事件根级别。打个比方,它拆掉了外包装箱,但里面的小盒子还在。
对于只有一层嵌套的场景,例如JSON里所有字段本来就在顶层,那target: ""确实够用。但如果JSON里本身有嵌套对象,比如context字段下还有orderId,target: ""之后context还是嵌套对象,并没有真正展开。这是“浅扁平化”和“深度扁平化”的区别。
3.3 深度扁平化:script processor递归展平
要实现真正的深度扁平化,Filebeat的script processor可以派上用场。这个processor支持JavaScript,可以对事件做任意操作。下面是我在项目里验证过的一段递归展平脚本:
processors: - script: lang: javascript source: > function flattenNode(node, prefix, output) { for (var key in node) { if (!node.hasOwnProperty(key)) continue; var fullKey = prefix ? prefix + '_' + key : key; var value = node[key]; if (value !== null && typeof value === 'object' && !Array.isArray(value)) { flattenNode(value, fullKey, output); } else { output[fullKey] = value; } } } function process(event) { var doc = event.Get(); if (doc['context'] !== undefined) { var flat = {}; flattenNode(doc['context'], 'ctx', flat); for (var k in flat) { event.Put(k, flat[k]); } event.Delete('context'); } }这段脚本的意图是:把context对象里的所有嵌套字段递归展开,新字段名用下划线拼接,比如context.orderId变成ctx_orderId,context.userId变成ctx_userId,然后把原始的context字段删除。
注意:Filebeat script processor的JavaScript API和Elasticsearch的script processor不一样,这里用的event.Get、event.Put、event.Delete是Filebeat事件对象的原生方法。写脚本时先看对应版本的文档,不同大版本之间API可能有调整。
3.4 关于扁平化字段名的关键认知
这里有个特别容易踩的知识点:如果你想拍平字段,新字段名里千万别带点号。比如把context.orderId改名为context.orderId(还是原来那个名字),这在ES里依然会被当作嵌套路径处理,等于白干。真正要做到扁平化,字段名里不能有点号分隔符,要么用下划线,要么用自定义前缀。
这个道理类似文件路径:a/b/c是路径分隔,a_b_c就是一个普通文件名。ES的字段名机制也一样,点号就是路径分隔符。理解了这一点,你就知道为什么很多人“扁平化”之后Kibana里还是嵌套结构。
3.5 扁平化后的字段名冲突处理
扁平化时最常见的冲突是:两条嵌套路径展平后使用了同一个字段名。例如context.orderId和data.orderId,如果都直接展平成orderId,后写的事件会覆盖先写的字段。
稳妥的做法是带上前缀。我上面的脚本生成ctx_orderId,就是把所有字段都加了ctx前缀,这样从context里出来的字段天然不会和日志顶层的orderId冲突。如果业务上有多个嵌套对象需要展开,建议分别为它们设置不同的前缀,干脆避免冲突,别指望ES能帮你智能合并。
4. 去掉部分属性:三种方案的对比与选型
4.1 黑名单方案:drop_fields直接删
最简单的裁剪手段是drop_fields,写法很直接:
processors: - drop_fields: fields: ["extra_verbose_data", "context.items"] ignore_missing: trueignore_missing: true表示字段不存在时不会报错,这在处理多格式日志时很重要,不然某个字段在这个事件里没有,整个processor会报异常。
这个方案的适用场景是:字段总量可控,你明确知道哪些字段是多余的。比如日志里有一个巨大的extra_verbose调试字段,直接删掉最省事。
需要注意,drop_fields删嵌套字段时,要写的是事件里的实际路径。如果前面已经做了扁平化,原来的context.orderId已经变成ctx_orderId了,这时候再写context.orderId肯定删不到东西。
4.2 白名单方案:include_fields只留需要的
字段特别多、真正用的没几个时,相反的做法更高效:只保留关心的字段。
processors: - include_fields: fields: ["@timestamp", "log", "host", "level", "logger", "message", "ctx_orderId", "ctx_userId"]白名单的好处是:业务日志里以后新增了字段,也不会自动进入ES,索引mapping非常干净,不会出现动态字段泛滥。代价是每次想看一个新字段,都要改配置,灵活性差一些。
这里我必须再次强调:include_fields的列表一定要带上@timestamp、log、host这些系统字段。我自己就吃过亏,第一次配白名单时只想着留业务字段,结果时间字段被过滤掉了,Kibana里所有日志的时间全乱了,排查了很久才定位到是这个原因。
4.3 服务端方案:ES Ingest Pipeline
如果处理逻辑希望集中在ES端,可以在Elasticsearch里建一个ingest pipeline:
PUT _ingest/pipeline/app-log-json { "processors": [ { "remove": { "field": "extra_verbose_data" } }, { "rename": { "field": "context.orderId", "target_field": "ctx_orderId" } } ] }然后在Filebeat的output里引用:
output.elasticsearch: hosts: ["http://localhost:9200"] index: "order-app-%{+yyyy.MM.dd}" pipeline: "app-log-json"这个方案的优势在于:逻辑集中在ES端,多个采集器过来都能复用同一套处理规则,改一次全链路生效。劣势也明显:冗余字段已经通过网络传到了ES,白白占用带宽,而且ingest节点在高峰期的CPU开销不可忽视。
4.4 方案对比与选型建议
| 方案 | 执行位置 | 节省传输带宽 | 新增字段自动保留 | 适合场景 |
|---|---|---|---|---|
| drop_fields | Filebeat | 是 | 是 | 明确知道要删哪些字段 |
| include_fields | Filebeat | 是 | 否,需改配置 | 字段极多且只需要少数几个 |
| ES Ingest Pipeline | ES | 否 | 是 | 多采集器复用同一套规则 |
我的经验是:中小规模日志链路优先全用Filebeat处理。先decode,再drop_fields删明确冗余的字段;如果字段确实太杂,改用include_fields白名单,但一定把系统字段带上。单日志量非常大的场景,建议把复杂的字段重命名和类型转换放到ES pipeline里,Filebeat只做最基础的解码和裁剪,降低Beat端的CPU压力。
5. JSON日志拆分:字段拆分与事件拆分
5.1 先搞清楚“拆分”指什么
“拆分JSON日志”这个词确实容易让人犯迷糊。我基于实际需求把它拆成两类。
第一类是字段拆分:把嵌套对象里的字段拆出来变成顶层字段。比如context里的orderId拆成ctx_orderId,这个本质上就是前文讲的扁平化,只是换了种说法。
第二类是事件拆分:日志里有一个JSON数组,数组中每个元素代表一条独立业务事件,希望一条日志变成多条记录进入ES。比如订单日志里orders数组包含多笔订单,你想让每笔订单成为一条独立日志。
这两种需求的技术路径完全不同,下面分开讲。
5.2 字段拆分的实现思路
字段拆分完全可以在Filebeat内用script processor完成。比如下面这段脚本把orders数组里每个元素展开成order_0_id、order_0_amount这样的字段:
processors: - script: lang: javascript source: > function process(event) { var orders = event.Get('orders'); if (orders && Array.isArray(orders)) { for (var i = 0; i < orders.length; i++) { event.Put('order_' + i + '_id', orders[i].orderId); event.Put('order_' + i + '_amount', orders[i].amount); } event.Delete('orders'); } }这样做的前提是数组长度相对固定。因为数组一旦变成order_0_id、order_1_id这种固定字段,ES的mapping会随数组长度动态增加字段,如果长度不可控,mapping会很难看,字段会无限膨胀。所以字段拆分只适合数组长度有限且稳定的场景。
5.3 事件拆分的正确姿势:Filebeat加Logstash
把一条日志拆成多条记录,Filebeat本身没有这个能力,这是它的边界。要完成事件拆分,标准方案是Filebeat把原日志原样发给Logstash,由Logstash的split filter处理。Logstash配置示例:
input { beats { port => 5044 } } filter { json { source => "message" target => "body" } split { field => "[body][orders]" target => "order" } mutate { remove_field => ["body", "message"] } } output { elasticsearch { hosts => ["http://localhost:9200"] index => "order-detail-%{+yyyy.MM.dd}" } }split filter的工作原理可以这样理解:父事件进来了,orders数组有几个元素,它就复制出几个事件副本,每个副本里把orders数组的某个元素赋值到order字段上,同时保留父事件的其他字段。最后用mutate把body、message这些中间字段删掉,输出的就是干净的独立订单事件。
这里有一个业务上的取舍:事件拆分后,每条日志携带的上下文(比如请求ID、用户ID)会被复制到每条记录中,这会让存储膨胀,但查询体验好很多。如果业务上查询单笔订单时不想关联回父日志,这个代价是值得的。
5.4 拆分后的索引设计建议
事件拆分后,数据量通常比原来多好几倍。写入ES时要注意两点:一是给拆分后的事件设置合理的索引分片数,不要沿用父日志的索引配置;二是在输出端尽量让ES做幂等写入,用业务订单号做文档ID,避免重复采集时产生重复文档。
如果不想为拆分额外维护一套Logstash,也可以在应用端直接修改日志结构,把数组在日志产生时就打成多条独立JSON行。这个方案最干净,但前提是你能改业务代码。
6. 完整实战配置:一个可复现的Demo
6.1 演示日志样例
为了把配置讲具体,模拟一份订单应用日志,存到/var/log/app/order-app.log:
{"timestamp": "2024-01-15T10:30:00Z", "level": "INFO", "logger": "OrderService", "message": "订单创建成功", "context": {"orderId": "ORD-12345", "userId": 10086}, "items": ["SKU001", "SKU002"], "extra_verbose": {"traceId": "trace-01", "stack": "..."}}这份日志包含了我们要演示的所有处理点:timestamp需要转成@timestamp,context需要扁平化,extra_verbose需要删除,items数组要保持原样。
6.2 filebeat.yml完整配置
下面是一份可直接复用的完整配置:
filebeat.inputs: - type: filestream id: order-app-log paths: - /var/log/app/order-app.log parsers: - ndjson: target: "" overwrite_keys: true add_error_key: true processors: - timestamp: field: timestamp layouts: - '2006-01-02T15:04:05Z' - '2006-01-02T15:04:05.999Z' timezone: Asia/Shanghai ignore_failure: true - drop_fields: fields: ["extra_verbose"] ignore_missing: true - script: lang: javascript source: > function flattenNode(node, prefix, output) { for (var key in node) { if (!node.hasOwnProperty(key)) continue; var fullKey = prefix ? prefix + '_' + key : key; var value = node[key]; if (value !== null && typeof value === 'object' && !Array.isArray(value)) { flattenNode(value, fullKey, output); } else { output[fullKey] = value; } } } function process(event) { var doc = event.Get(); if (doc['context'] !== undefined) { var flat = {}; flattenNode(doc['context'], 'ctx', flat); for (var k in flat) { event.Put(k, flat[k]); } event.Delete('context'); } } output.elasticsearch: hosts: ["http://localhost:9200"] index: "order-app-%{+yyyy.MM.dd}"配置里要解释几个关键点。
timestamp processor的layouts用的是Go时间布局格式,不是Java的yyyy-MM-dd格式。Go格式里,2006代表年、01代表月、02代表日、15代表小时、04代表分钟、05代表秒。这是Filebeat和Logstash时间格式最大的不同,很多人第一次用都会在这里翻车。
ignore_failure: true的含义是:时间解析失败时,processor不会让整个事件处理中断,而是继续往下走。这在实际生产环境里很有用,因为偶尔会出现一条格式异常的日志,你不能因为一条脏数据把整批事件都堵住。
drop_fields放在timestamp之后,没有依赖关系,但放在decode之后是关键,因为extra_verbose是解析后的字段,不是原始日志行里的路径。
script processor的flattenNode递归函数会生成ctx_orderId、ctx_userId这些带前缀的扁平字段,然后删除原始的context对象。如果context里还有更深层的嵌套,这个递归函数会继续往下展开,直到所有非对象字段都变成顶层字段。
6.3 启动与验证步骤
写完配置先别急着上生产,本地跑通再说。三个命令很实用:
filebeat test config -c filebeat.yml filebeat test output -c filebeat.yml filebeat -e -c filebeat.yml第一条检查配置语法,第二条检查output连接是否可用,第三条前台运行并输出日志。如果处理逻辑复杂,我习惯先把output改成console做本地调试:
output.console: pretty: true这样可以直观看到每个事件经过processors之后长什么样,确认字段都对了,再把output切回elasticsearch。这个调试手法帮我省了大量去Kibana刷index的时间。
6.4 验证结果与字段对比
经过上面配置处理后,原始日志字段会变成这样:
| 原始字段路径 | 处理后字段 | 处理方式 |
|---|---|---|
| timestamp | @timestamp、timestamp | timestamp processor转换 |
| context.orderId | ctx_orderId | script processor扁平化 |
| context.userId | ctx_userId | script processor扁平化 |
| extra_verbose | 已删除 | drop_fields |
| items | items | 原样保留 |
| level、logger、message | 原样保留 | 根级字段 |
在Kibana的Discover页面里,如果索引模板已经生成,打开字段列表就能看到ctx_orderId、ctx_userId这些顶层字段,不再有context这个嵌套对象。items作为数组字段保留,方便后续做聚合分析。
7. 常见问题与排查技巧实录
7.1 日志解析失败却没有任何报错
最常见的现象是:原始日志明明是JSON,写入ES后message字段还是完整字符串,或者干脆多了一个error字段。原因通常是add_error_key没开启,解析失败的信息被Filebeat吞掉了。
排查时先做的不是改配置,而是用console output直接看Filebeat内部事件长什么样:
output.console: pretty: true如果事件里的message还是原始字符串,说明decode_json_fields根本没匹配到那个字段,检查fields写的对不对。如果事件里有很多error字段,说明JSON解析失败,把日志原文复制出来用格式化工具检查一下,多半是日志里混了多行JSON或者非JSON文本。
7.2 时间字段被覆盖或丢失
Kibana日志时间全是Filebeat采集时刻的实时时间,而不是业务日志本身的时间。这个问题的根源是:没有配置timestamp processor,或者配置了但field写错了,又或者字段在include_fields时被误删。
把业务时间字段转成@timestamp是JSON日志处理的标准动作。同时记得,如果业务字段里也有一个@timestamp或类似字段,decode时overwrite_keys开启后,业务字段也可能反过来覆盖系统时间,导致时间错乱。这种情况建议把业务时间字段rename成一个业务专属名,比如event_time,再用timestamp processor转系统时间。
7.3 嵌套字段依然存在,扁平化没生效
我在实际项目中经历过这种场景:按照网上教程配置了decode_json_fields的target: "",以为能完全展平,结果context还是嵌套在ES里。原因前文已经说过,target: ""只是拆外包装,要完全展开必须用递归脚本。
还有一个隐蔽问题:如果脚本生成的字段名里带了点号,比如直接写成ctx.orderId,那么ES仍然会把它当作嵌套路径。字段名里只要有点号,在ES里就是层级结构。检查脚本生成的字段名,确认没有点号。
7.4 processor顺序导致的诡异结果
processor顺序的问题是排查成本最高的,因为很多错误很隐蔽。比如先做了include_fields,把message以外的业务字段全删了,然后decode_json_fields再去解析message,解析出来的字段当然也没了,因为事件只剩下message一个字段。
经验法则是:先做decode,再做rename,再做字段裁剪,再做时间转换,最后做输出相关处理。按这个顺序排,能避开80%的processor顺序问题。
7.5 script processor的性能隐患
最后讲一个很多人忽略的点:script processor用的是JavaScript引擎,解释执行开销远高于原生processor。日志量小无所谓,但一天几亿条日志时,如果每条都要跑一段递归展平脚本,Filebeat的CPU会明显吃紧。
我踩过这个坑之后的实践原则是:能用原生processor(decode_json_fields、drop_fields、rename、timestamp)解决的,绝不写脚本;脚本只处理真正无法用原生能力完成的逻辑。如果日志量真的很大,把复杂处理尽量往ES Ingest Pipeline或Logstash移,让Filebeat只做最轻量的采集和裁剪。
还有一种更彻底的思路:找业务方沟通,让他们在输出日志前就把JSON结构拍平。比如日志里直接输出orderId,而不是输出context对象让采集端再去拆。很多团队绕了一大圈做扁平化,最后发现让业务改一下日志结构,比任何采集端处理都干净高效。
我这里多分享一句个人体会:Filebeat的processor能力很强,但不代表所有事情都适合在Beat端做。学会判断什么该在Filebeat做、什么该给Logstash、什么该留给ES pipeline,比背一堆配置参数重要得多。日志链路每多一个处理环节,排查链路就多一分复杂度,能用最简单方案解决的,别为了炫技堆复杂处理。