凌晨一点半,产线停线,售后的一台车刷写失败。诊断仪抓回来的日志 28MB,用 Notepad++ 打开,满屏十六进制,找了一个小时,只在一堆0x7F里看到一个 (0x22)。这种场景做汽车电子的人应该都不陌生:UDS 刷写日志不是没人看,是真的没法看。我后来做了一款开源的 UDS/ISO-TP 刷写日志离线分析工具,专门处理这种让人头皮发麻的排查现场。它做的事很简单:把枯燥的十六进制诊断报文还原成可读的 UDS 服务序列、NRC 错误码、刷写阶段耗时和失败上下文,全程离线运行,不依赖任何商业分析仪硬件。这篇文章会把工具的由来、核心解析逻辑、阶段识别原理、架构设计、实操步骤和开发过程中踩过的坑完整讲一遍,适合汽车电子开发、产测工具链、售后诊断和协议栈相关的工程师参考。
1. 每个刷写失败现场都需要一台“日志翻译机”——项目的由来
1.1 先说说刷写日志为什么那么难看
UDS 刷写的过程,本质上是诊断仪和 ECU 之间一问一答的机械对话:进入编程会话、安全解锁、写入身份信息、擦除 Flash、按块下载数据、校验、复位重启。整个过程看起来很有条理,但落到总线日志里就完全不是那么回事了。
一个典型的刷写日志,动辄几十 MB。CAN FD 报文一帧一帧往上堆,一帧最多 64 字节,全是十六进制;普通 CAN 一帧 8 字节,那就更夸张。想确认一条 (0x34) 请求对应的是哪个 ECU、后续有没有正常收到 (0x36) 数据块,靠人眼从几万行报文里逐个比对,基本等于大海捞针。
更要命的是,ISO-TP 把所有 UDS 消息都做了分段和重组。一帧应用层数据超过单帧容量,就要拆成首帧、连续帧,中间还穿插流控帧。一条 (0x34) 请求可能在日志里对应七八帧甚至几十帧。刷写过程中最大的数据下载阶段,更是有几千个连续帧。工程师想从文本编辑器里手动还原这些关系,既痛苦又容易出错。
我在实际项目里见过太多排查姿势,比如用文本编辑器搜7F 22,搜到了就下结论说“(0x22) 服务不支持”,但完全不知道这个错误发生在刷写的哪个阶段、是谁发起的请求、是在哪个 ECU 上收到的。这种结论基本没有指导意义,因为很多 NRC 是上下文相关的,同一个0x24出现在0x27密钥阶段和出现在0x36传输阶段,含义完全不一样。
1.2 在线工具和离线工具的取舍
市面上不缺诊断分析工具,商业的 CANoe、PCAN-Explorer 都很强大,但它们的定位是“在线监测”——你得在现场把硬件接上总线,实时看报文曲线。问题在于:
- 商业工具价格高,授权复杂,部署到产线和售后网点成本极高。
- 在线分析会占用总线资源,本身可能干扰刷写过程。
- 现场抓完日志,问题没解决,日志带回来了,但对应的分析软件可能不在手边。
离线分析工具的定位完全不同。日志从车上取下来,回到办公室慢慢拆解,不占总线资源,不依赖现场设备,可以反复回放、批量统计、做回归对比。对于售后问题回溯和产线批量刷写质量分析来说,离线分析反而更实用。
我当时的目标很简单:做一个能“批量导入、批量分析、一键出报告”的工具,让现场工程师把日志发回来,我这边跑一下就能定位问题。这也是我把核心功能定义为“离线分析”而不是“在线抓包”的原因。
1.3 为什么选择开源
诊断协议有 ISO 14229 和 ISO 15765-2 的标准文本撑着,但各家 ECU 的执行细节千奇百怪。有的支持 (0x31) 例程里的不同子功能,有的对 (0x27) 安全访问的 seed/key 算法完全不同,有的在 (0x34) 请求里加了额外的 DataIdentifier,有的刷写流程里根本不走 (0x27),而是先写指纹再例程擦除。
闭源工具遇到非标需求就只能干瞪眼,因为解析规则是写死的。开源的形式,让社区的开发者可以互相补充不同整车厂、不同 ECU 的解析规则,也能让整车厂和供应商直接 Fork 一份,嵌入到自己的产线工具链或者 CI 流水线里做自动化判断。这个工具要是闭源,价值就少了一大半。
2. 解析层重构:ISO-TP 分段重组与 UDS 服务还原的关键逻辑
2.1 ISO-TP 的四种帧类型和重组流程
ISO-TP(ISO 15765-2)是跑在 CAN 总线上的传输层协议,作用是把超过单帧容量的 UDS 消息切碎再拼起来。解析的第一步,就是先把这些碎片还原成完整的 UDS 消息。
ISO-TP 帧按 N_PCI 类型分为四种:
| 帧类型 | N_PCI | 特征 | 说明 |
|---|---|---|---|
| 单帧 SF | 0x0X | 第一个字节高四位为 0 | 数据长度 <= 7(普通 CAN)或 <= 63(CAN FD) |
| 首帧 FF | 0x1X | 第一个字节高四位为 1 | 后跟 12 位或更长总长度,之后接第一段数据 |
| 连续帧 CF | 0x2X | 第一个字节高四位为 2 | 低四位是 4 位序列号 Sn,从 0 递增到 0xF 循环 |
| 流控帧 FC | 0x3X | 第一个字节高四位为 3 | 包含状态 STS、块大小 BS、最小间隔 STmin |
重组逻辑的关键点,是按“发送方 CAN ID + 接收方 CAN ID + 传输方向 + 逻辑地址”作为复合键来分配缓冲区。收到首帧后,从 FF 的长度字段解析出总字节数,分配缓冲;之后收到的连续帧,要校验 Sn 序号是否连续;遇到流控帧,则记录对方的接收窗口参数。
实际实现时,真正要处理好的不是“完整报文的重组”,而是“错误场景的重组”。刷写失败的日志里,大量存在首帧之后连续帧缺号、流控帧窗口对不上、发送方重传等情况。工具必须把这种不完整帧也记录下来,并标注“重组错误”,而不是静默丢弃。否则分析报告会非常干净,但真正的问题也被丢掉了。
有一个细节很容易被忽略:CAN FD 下 ISO-TP 的 SF 长度上限变大,普通 CAN 一个 SF 最多 7 字节数据,CAN FD 里最多 63 字节;首帧的长度字段也从 12 位扩展到了更多位。如果你的解析器把长度字段的偏移写死,放在 CAN FD 日志上一定出错。
2.2 UDS 服务解析:SID、子功能与 NRC 的正确判定
ISO-TP 还原出来的是完整的 UDS 消息,下一步是把消息拆成“请求”和“响应”,识别服务 ID,解析参数,判定正响应还是负响应。
UDS(ISO 14229-1)的基本规则很简单:
- 请求:SID + Sub-function(如果服务有子功能)或参数。
- 正响应:SID + 0x40。
- 负响应:0x7F + 请求的 SID + NRC。
刷写流程里最常出现的服务,我整理了一份清单:
| 服务 | 名称 | 刷写中的用途 |
|---|---|---|
| 0x10 | DiagnosticSessionControl | 切换到编程会话(0x02)或扩展会话(0x03) |
| 0x11 | ECUReset | 刷写完成后的复位操作 |
| 0x22 | ReadDataByIdentifier | 刷写后读软件版本号验证 |
| 0x27 | SecurityAccess | 安全解锁,请求种子和发送密钥 |
| 0x28 | CommunicationControl | 禁用应用报文,避免刷写期间干扰 |
| 0x2E | WriteDataByIdentifier | 写 VIN、写指纹、写版本信息 |
| 0x31 | RoutineControl | 启动擦除例程、执行校验例程 |
| 0x34 | RequestDownload | 请求下载,声明地址和长度 |
| 0x36 | TransferData | 分块传输数据 |
| 0x37 | RequestTransferExit | 请求退出传输,结束下载 |
| 0x19 | ReadDTCInformation | 读取 DTC 状态 |
| 0x14 | ClearDiagnosticInformation | 清故障码 |
| 0x3E | TesterPresent | 保持会话激活 |
解析 (0x34) 的时候有一个很容易踩的坑:addressAndLengthFormatIdentifier这个字节,高四位表示地址字段的长度,低四位表示长度字段的长度,单位是字节。比如0x14表示地址占 1 字节、长度占 4 字节。很多新手解析工具直接按固定偏移取地址和长度,遇到长度格式不是0x14的 ECU 就解析错乱。必须动态计算字段长度,这是一个高频出错点。
NRC 错误码的映射也不能只做“查表”。同样的0x24,在0x27发送密钥时出现表示“请求序列错误”,在0x34请求下载时出现也可能意味着“没有先进入编程会话”或者“没有先擦除 Flash”。所以工具在标注 NRC 含义的同时,必须结合当前的会话状态、安全状态和已经执行过的步骤,给出更具体的退避线索。
2.3 物理寻址与功能寻址的匹配逻辑
UDS 请求可以发往物理地址(定点寻址,只发给一个 ECU),也可以发往功能地址(广播给一组 ECU)。在总线日志里,同样一条0x10 02,功能寻址时可能引来多个 ECU 的正响应。
离线分析工具不能把“一个请求对应一个响应”当成铁律。要按请求 CAN ID、响应 CAN ID、寻址模式综合判断。这个点早期版本做得不好,我就是简单按 CAN ID 配对,结果功能寻址的日志分析结果一团糟:明明一个刷写请求只成功刷了一个节点,工具却报“多个 ECU 响应”,后来加了寻址模式判定才修好。
物理寻址和功能寻址的处理,还关系到刷写阶段的判定。有些 ECU 在功能寻址下不响应0x3E,有些在物理寻址下才能进入编程会话。如果把所有0x3E的响应都算进阶段时间,统计结果会偏大。这一层逻辑做细致之后,工具的实用价值会明显提升。
3. 刷写阶段的自动识别:从一堆十六进制里找出问题在哪一步
3.1 典型刷写流程与特征服务
不同整车厂的刷写流程有差异,但大体骨架是一致的。典型的刷写流程是:
- 默认会话下先建立通信,通常是
0x10 02进入编程会话。 - 有些实现会先
0x28 03关闭应用报文,或者0x85 02关闭 DTC 记录,避免刷写过程被打扰。 - 安全访问:
0x27 01请求种子,拿到 seed,计算 key,0x27 02发送密钥解锁。 - 写入身份信息:
0x2E写 VIN、软件版本、刷写时间戳等。 - 擦除:有的走
0x31 01启动例程擦除,有的直接通过0x34下载时触发擦除。 - 数据传输:
0x34请求下载,0x36分块传输,0x37退出传输。 - 校验:
0x31 01启动校验例程,或者0x22读回版本和数据校验。 - 复位:
0x11 01或0x11 03复位 ECU,退出编程会话。 - 回到默认会话,
0x22读取软件版本确认刷写成功。
这九个阶段不是每次刷写都会全走,但阶段识别的逻辑就是基于这些特征服务建立状态机。
3.2 状态机设计:从会话状态到阶段状态
阶段识别本质上是一个状态机。输入是逐条解析出的 UDS 服务原语,输出是“当前处于刷写的哪个阶段”。我设计了这样几组状态:
- 会话状态:默认会话、编程会话、扩展会话。
- 安全状态:未解锁、已请求种子、已发送密钥、已解锁。
- 传输状态:空闲、等待下载、传输中、传输退出。
状态转换的规则大致是:
- 收到
0x10 02正响应,会话状态切到编程会话,开始计时“会话切换耗时”。 - 收到
0x27 01正响应,安全状态切到“已请求种子”;收到0x27 02正响应,安全状态切到“已解锁”。 - 收到
0x34正响应,进入“等待下载”状态,记录内存地址和长度。 - 收到
0x36正响应,校验 blockSequenceCounter 是否按序递增,进入“传输中”。 - 收到
0x37正响应,退出下载,开始计时“校验阶段”。
阶段耗时以阶段内第一条请求的时间戳为起点,最后一条响应的时间戳为终点。输出的是一张阶段甘特图或者表格:会话切换耗时、安全解锁耗时、擦除耗时、数据传输耗时、校验耗时、复位耗时,每一段都清清楚楚。
时间戳的精度对阶段耗时计算影响很大。CANalyzer 的 ASC 日志默认自带微秒级时间戳,有些自研 logger 导出的日志时间戳只有相对 Tick,需要配置时基才能换算成秒。归一化层必须处理这个差异,否则算出来的耗时完全不可信。
3.3 失败定位:跨多帧找因果
阶段识别最大的价值,是给失败定位提供上下文。分析输出里最重要的不是图表,而是“失败问题清单”。每条失败结论需要包含:发生时刻、总线通道、CAN ID、完整请求响应帧序列、相关 NRC、当前会话状态、安全状态。
举个例子:一条0x36触发0x7F 36 73(blockSequenceCounter 错误)。光看这一条,现场工程师不知道为啥。结合状态机回放发现,(0x34) 请求下载时声明的块长度是 4 字节,但实际传输数据长度只有 2 字节,而且上一条0x36还没发送完就发了下一条,导致序列号跳变。这种跨多帧的因果链靠人眼很难发现,工具几秒钟就能定位到根因。
另一个常见场景是刷写后校验失败。(0x37) 传输退出后,刷写器通过0x22读取软件版本号验证,如果读回的版本和期望不一致,失败其实发生在“后校验”阶段,而不是传输阶段。没有阶段识别的情况下,这个问题很容易被误归到数据擦写环节。
4. 数据流架构与多格式日志接入:我为什么这样设计
4.1 分层数据流:解析和分析彻底解耦
工具整体架构从一开始就按分层设计,这是为了避免“加一个日志格式就要改 UDS 解析代码”的窘境。整个数据流分四层:
- 输入层:读取原始日志文件(ASC、BLF、pcap、自定义 TXT/CSV),按格式解析出原始报文。
- 归一化层:把不同格式的原始报文转换成统一的“总线报文”中间结构,包含时间、通道、方向、CAN ID、DLC、数据字节。
- 协议层:对这个统一结构执行 ISO-TP 重组,再解析 UDS 服务,送入状态机做阶段识别。
- 分析层与展示层:做统计、失败归因,输出 CLI 汇总、HTML 报告、JSON 结果。
每一层之间通过标准数据结构通信,可以单独写单元测试。新增一个日志格式时,只需要实现一个输入层解析器,生成归一化结构即可,后面的协议解析完全不用动。新增一个诊断规则时,也只需要改协议层和分析层。
有人可能会问,为什么不直接基于 Wireshark 的插件机制做?Wireshark 对单条报文的协议解析很强,但它不太适合做“刷写阶段统计”和“批量失败归因”这类批量分析任务。做一个独立的命令行工具,可以方便地嵌入到产线的自动化框架里,也可以在 CI 流水线里跑批量回归。
4.2 技术栈选型的考量
技术栈选择上,我用了 Python 做核心解析,加上模板渲染生成 HTML 报告。核心原因有三个:
- Python 生态里已经有 python-can、canmatrix 这些库,处理 ASC、BLF 不用自己从头写底层解析。
- 诊断领域的数据量对离线工具来说不算大。一个 32MB 的日志,完整解析加状态机分析,大概几秒到十几秒,性能完全够用。
- 命令行模式可以输出 JSON,方便集成到自动化测试平台,也能被其他语言调用。
当然,如果想追求极致的单文件分发体验,或者想做成一个免安装的桌面软件,Rust 或 Go 也是不错的选择。我没有一上来用 Rust,是因为这个工具的核心价值在于协议覆盖和诊断逻辑的迭代速度,而不是解析速度。开发过程中,算法逻辑的调整频率远超性能优化频率,Python 的开发效率优势更明显。
4.3 多格式接入的几个硬骨头
日志格式的碎片化程度,比我想象的严重得多。常见的格式有:
- ASC 格式:CANalyzer 的 ASCII 日志,字段布局在不同版本之间有差异,header 信息、时间戳列数、IDE 位的表示方式都可能变。
- BLF 格式:二进制格式,需要按 ObjectHeader 逐块解析。python-can 的 BLF 读取器能处理大部分情况,但有些厂商 logger 导出的 BLF 对 ObjectHeader 做了自定义扩展,需要额外的容错逻辑。
- pcap/pcapng:如果场景是车载以太网或 DoIP,需要处理 DoIP 头、TCP 负载,再进到 UDS 解析链。
- 自定义 TXT:产线工具导出的日志最要命,时间戳可能是十六进制 Tick,CAN ID 大小端混乱,数据列分隔符用空格、逗号、TAB 的都有。
针对自定义格式,归一化层必须提供“列映射配置”,让用户指定哪一列是时间、哪一列是 CAN ID、哪一列是数据。这个配置做成 YAML 文件,放到仓库的 sample 目录里,现场工程师按模板改一下就能适配新工具。
这里有一个经验:归一化层绝对不能把时间戳和通道信息“悄悄丢掉”。有的格式没有提供通道,有的没有方向,处理时要填充默认值,并在报告里明确标注“方向未知”“时间戳未校准”,避免后续分析被误导。
5. 十分钟上手:从一条日志到一份可汇报的分析报告
5.1 安装与基本命令
工具发布在 GitHub 上,安装方式走的是标准的 Python 包管理。克隆仓库之后,直接安装依赖就可以跑:
git clone https://github.com/example/uds-log-analyzer.git cd uds-log-analyzer pip install -r requirements.txt命令行入口非常简单。解析单个日志文件:
uds-log-analyzer analyze canalyzer_log.asc解析整个目录下的所有日志文件:
uds-log-analyzer analyze logs/ --format asc --output report.html筛选指定 CAN ID,比如只关心诊断仪 0x7E0 和 ECU 0x7E8 之间的通信:
uds-log-analyzer analyze canalyzer_log.asc --filter 0x7E0,0x7E8 --output report.html输出 JSON 给 CI 或自动化平台:
uds-log-analyzer analyze canalyzer_log.asc --json result.json跑完之后,命令行会打印一个简单的汇总表格,包含总报文数、ISO-TP 消息数、UDS 请求数、正响应数、负响应数、唯一 NRC 列表。完整的分析报告会生成一个 HTML 文件,可以直接发给同事或贴到问题单里。
5.2 报告里的关键内容
HTML 报告的结构分成五块:
- 总体概览:日志时长、总线通道数、报文总数、ISO-TP 消息数、UDS 请求响应计数。
- NRC Top 榜:哪类 NRC 出现次数最多。这是快速定位的第一步,如果
0x73出现几十次,说明数据传输阶段有问题;如果0x33出现,安全解锁就值得怀疑。 - 阶段耗时表:每个刷写阶段的耗时和占比。
- 失败详情:每条负响应对应的完整请求响应上下文,包括时间、CAN ID、当前会话状态、安全状态、前后关键报文。
- 可疑序列:比如同一个服务连续请求多次但始终没有正响应,或者 (0x34) 之后 (0x36) 的块序号跳跃超过阈值。
举个例子,一个 30 秒的刷写日志,报告显示数据传输阶段耗时占比 85%,估算出来的传输速率只有 175 KB/s。这个结论来自 (0x34) 请求里的块长度、(0x36) 报文数量和时间戳的差值。人工统计这个数据基本不可能,工具几秒钟就算出来,而且能把现场工程师的注意力直接引到瓶颈环节。
5.3 与 Wireshark 的衔接
这个工具不是要替代 Wireshark,而是互补。Wireshark 适合单条报文、单个协议的深度解剖,这个工具适合批量、跨报文的链路分析。
工具可以从 pcap/pcapng 导入数据,也能把解析结果导出成 CSV 或 JSON,方便导入 Wireshark 做进一步验证。实测下来,用 CANalyzer 抓取的已知日志做交叉验证,解析出的 ISO-TP 分段消息和 Wireshark 显示的完全一致。这个验证过程非常重要,是工具可信度的基础。我在开发过程中,特意保留了一批带标注的测试日志,每次解析层改动后跑一遍回归,确保不破坏已有功能。
6. 开发过程中踩过的四个深坑与相应处理方案
6.1 多帧交错:多个传输会话同时进行
这是 ISO-TP 解析里最经典的坑。总线上多个 ECU 并发响应,或者诊断仪向多个 ECU 并发请求时,会出现多个传输会话交错的情况。连续帧 A 的 Sn 是 0,连续帧 B 的 Sn 也是 0,如果只按单一缓冲去重组,会把不同传输的数据块串起来,重组结果完全错乱。
解决方案是把重组的复合键从“单个 CAN ID”扩展为“发送方 CAN ID + 接收方 CAN ID + 传输方向 + 逻辑地址”。这样,即使多个传输会话在同一总线上交错,也能各自维护独立的接收缓冲。
进一步要注意的是,ECU 发送正响应时,有些实现使用的响应 CAN ID 和请求 CAN ID 并不是简单的请求 ID 加 8。比如 UDS 物理寻址的典型配置是请求 0x7E0、响应 0x7E8,但功能寻址的请求 0x7DF 可能对应多个 ECU 的私有响应 ID。工具需要支持用户配置 CAN ID 映射关系,否则无法正确归属响应。
6.2 (0x27) 安全访问的“伪正响应”
有一次分析一个售后日志,(0x27 01) 的响应明明是正响应,但紧接着的 (0x34) 请求被 NRC (0x33) 拒绝。一开始我以为是解析问题,后来仔细看才发现,(0x27 01) 返回的种子全是0x00。ECU 返回全零种子,说明它根本不认为当前诊断仪有权限请求种子,或者会话状态不对。
如果工具只按“(0x27) 正响应 = 安全解锁成功”来判断,就会漏掉这个前置问题。这里的经验是:不能只看单条服务响应的正负,要结合后续步骤判断安全解锁是否真正成功。状态机里,我把安全状态改成“解锁中”,只有在后续出现0x2E、0x34、0x31中任意一个服务,且未收到 NRC (0x33) 或 (0x24) 时,才确认“已解锁”。这个策略虽然保守,但能明显减少误报。
6.3 时间戳时基不一致:合并日志后时间倒流
做批量分析的时候,经常遇到多个日志文件需要合并的情况。最头疼的是不同 logger 导出的时间戳单位不一致:有的用毫秒,有的用微秒,有的用 Tick 计数加一个 base time,合并之后时间轴居然出现倒流。
处理方案是在归一化层统一转换为浮点秒,并提供--time-offset和--time-base参数做手动校正。报告里对时间戳倒流的位置打上警告标记,提示用户注意数据质量问题。
这里有一个很重要的经验:绝对不能假设所有日志都从 0 开始。有的日志从1234567.891这种绝对秒数开始,有的从 0 开始,统计总时长时必须用最后一条减去第一条,不能直接输出最后一条时间戳。早期版本我直接输出end_time字段,差点把客户带偏。
6.4 NRC 与普通数据的歧义:简单字节匹配是万恶之源
还有一个非常隐蔽的坑:负响应的判定不能只看首字节0x7F。正常的0x22正响应数据段里,完全可能包含0x7F 0x22 0x00这样的字节序列。如果你只按首字节0x7F判断负响应,就会把这个正响应误判成“(0x22) 服务不支持”,引起大量误报。
负响应判定的充分条件必须是:首字节为0x7F,第二个字节是请求 SID,第三个字节是已知 NRC 码。正响应判定不能只看前两个字节,还要结合当前状态机中的会话状态。
类似的问题也出在0x36的数据段上。0x36正响应的数据载荷是块序列号,取值范围是0x01到0xFF,如果简单搜索整条报文里的0x73字节,会把正常的数据内容误判成 NRC0x73。这类问题带来的教训就是:协议解析绝对不能做简单字节匹配,必须走完整的协议栈,一步都不能省。
我把这四个坑的处理方案都沉淀到了代码注释和设计文档里,后来社区里也有人反馈过类似问题,照着这个思路改就能解决。
7. 这类工具的开源边界与下一步想法
做这种离线分析工具,最难的其实不是第一次写解析代码,而是维护一个能覆盖越来越多 ECU 变体的规则库。每个 OEM 的刷写流程都有一点差异,每次车厂发布新项目,都可能带出新的服务和例程。
开源是这个问题的天然放大器。社区贡献的日志样本和解析规则,可以快速扩展工具的覆盖范围。我在仓库里专门建了一个 rules 目录,每个 OEM 或项目可以放一份独立的解析配置,用 YAML 描述服务列表、阶段转换规则、NRC 提示语和特殊处理逻辑。这样新增一个项目的支持,不需要改主代码。
接下来打算做的方向有几个:支持 DoIP 和以太网日志的解析、导入 S19/HEX 文件对比实际刷写数据、用 SQLite 做大日志集的存储和查询,以及把报告生成做成可扩展的模板系统,方便不同团队定制自己的报告样式。
最后分享一个实际使用建议:拿到日志后,不要急着跑完整分析。先用“快速浏览”模式看前 100 条报文,确认日志范围、CAN ID 映射、寻址方式是否正确。这一步能帮你发现大量“日志本身有问题”的情况,比如抓漏了某个通道、时间戳乱跳、CAN ID 被网关转发改写等。日志本身有问题的话,任何高级分析都是白费力气。这个习惯帮我省下了大把排查时间,也推荐给准备用这类工具的工程师。