做车联网、储能或者非标设备远程监控的朋友,应该都有过这种痛苦:现场设备跑得好好的,数据都在 CAN 总线上,可总线上那堆十六进制报文既不能直接给业务用,也没法穿到云端。另一头云平台同事天天催“你把结构化数据推上来,车速、温度、SOC 一样不能少”。这个项目最直接的目标,就是把 CAN 总线上那些原始报文,通过 EC312 边缘计算网关解析成业务字段,再发布到 AWS IoT Core,让云端能实时拿到可用的设备状态。整个链路听起来就是“采集—解析—上云”六个字,但真正落地时,牵涉到的物理层配置、DBC 解析、云上凭证和 Topic 设计,处处有坑。这篇文章我把从 CAN 报文到 AWS IoT 的完整路径、每一步的选型理由和踩坑记录都写出来,适合正在做边缘网关、车载数据采集或者工业设备联网的工程师参考。
1. 先把链路全貌看清楚:EC312 在这个架构里扮演什么角色
1.1 为什么选 EC312,而不是单片机直连、工控机加采集卡
我第一次接到这个需求时,脑子里冒出来过几个方案:用 STM32 直接读 CAN、转发 4G 模块;或者用一台迷你工控机插 PCIe CAN 卡;再或者用带 CAN 口的工业路由器。最终定下来用 EC312 这种边缘计算网关,不是因为它在性能和价格上碾压谁,而是它从一开始就是按照“工业接口 + 可编程边缘节点”的方向设计的,少走了很多弯路。
用单片机直连的问题在于:单片机擅长采集,但不擅长处理复杂的应用层逻辑。你要在上面跑 MQTT 客户端、TLS 证书校验、JSON 业务打包、本地缓存,再叠加远程固件升级,开发和维护成本都不低。工控机加采集卡方案则反过来,处理能力强,但设备体积大,现场安装不方便,功耗也偏高。EC312 这类的边缘计算网关,价格和工业路由器差不多,但多一路 CAN 控制器和可用的 Linux 环境,既能用 SocketCAN 标准接口读写总线,又能直接跑 Python 或容器里的业务进程,扩展起来灵活很多。
另一个现实因素是:整个项目后期要接的设备不止一种,有的走 CAN,有的走 Modbus,有的走数字量输入。EC312 作为“边缘计算网关”,最大的价值就是把现场多种接口统一收进来,经过边缘处理后以标准 MQTT 消息出去。我不用在机柜里塞一堆转换器,一台网关就能覆盖大部分现场。
1.2 从总线到云端的完整数据通路
我在纸上画过一条链路,画完才发现,真正要维护的不只是“读报文、发消息”两个动作,而是好几个环节的串联:
- CAN 物理层:总线上的收发器、终端电阻、线缆屏蔽,这些直接决定了报文能不能被稳定采集。很多问题不是程序 bug,而是物理层没做好。
- EC312 内部的 CAN 控制器配置:波特率、采样点、工作模式,这是 SocketCAN 层的东西,对应 Linux 下的
can0网络接口。 - 采集进程:用
candump或者程序读取 CAN 帧,拿到带 ID、DLC、8 字节数据和时间戳的原始报文。 - 解析引擎:将收到的帧 ID 映射到 DBC 定义,按信号布局解码出物理量,比如把十六进制 0x1A2B 换算成 42.5 km/h。
- 边缘规则与本地缓存:数据在网关上先做合法判断、单位转换、缓存。网络抖动时,先存本地,恢复后补传。
- MQTT 链路:用 AWS IoT Device SDK 建立 mTLS 连接,把结构化 JSON 发布到指定 Topic。
- AWS 侧规则与存储:IoT Core 收到后,通过规则引擎转到 Kinesis、S3 或时序数据库,业务侧再消费。
这个链表上每个环节都有可能丢数据或者出脏数据。我当时犯过的最大错误,是上来就冲进 DBC 解析和 MQTT 发布,花了很多时间写业务代码,结果一接实地 CAN 总线,发现采集到的帧在物理层就有大量 CRC 错误。后来学了乖:先验证物理层和 SocketCAN 通不通,再去做上层封装。
2. 最容易翻车的电气和接口配置:终端电阻、收发器与波特率
2.1 终端电阻:不是“要不要接”,而是“接在哪个位置”
CAN 总线在物理层用的是差分信号,要求总线两端各接一个 120Ω 终端电阻,用来匹配传输线阻抗,避免信号反射。EC312 这类网关内部一般不默认带 120Ω 终端电阻,或只提供一个跳线帽开关,具体要看说明书。很多人在实验室用一根短网线把 EC312 和另一个设备对接,两根线一插就能通,于是忽视了终端电阻。等到了现场,总线上接了十几个节点、线缆拉长到几十米,就会出现偶发通信错误,排查起来特别痛苦。
记住一个基本判断原则:终端电阻必须接在总线物理拓扑的两个最远端。如果一个 CAN 网络里只有一个 ECU 和一个 EC312,那就在 ECU 侧和 EC312 侧各接一个;如果总线上还有别的控制单元,且它们内部已经带 120Ω,就不要在 EC312 上重复接。多个终端电阻并联会让总线等效电阻变小,差分信号幅值下降,反而导致通信不稳定。
我在现场用万用表量过 CANH 和 CANL 之间的直流电阻。正常情况下,在总线上任意位置测量,阻值应该在 60Ω 左右(两个 120Ω 并联)。如果量出来接近 120Ω,说明有一端没接终端电阻;如果接近 40Ω 甚至更低,说明有三个以上终端电阻挂上去了。这是一项非常快的现场排查手段,建议所有人先把这个动作练熟。
2.2 CAN 收发器、保护器件和 RS485/CAN 共存的干扰问题
另一个很容易被忽略的细节是收发器与防护电路。CAN 收发器把 CAN 控制器的逻辑电平转换成 CANH/CANL 差分电平,常见型号有 TJA1050、TJA1042、SN65HVD230 等。EC312 内部已经集成收发器,外部只需要做好接口防护。长期运行的工业现场,雷击浪涌、电机启停、变频器干扰都可能打坏收发器,所以很多项目会在 CAN 接口外加保护器件。
这里有一个硬件选型上的经典争论:CAN 接口的防护该用 TVS、气体放电管还是两者组合。实际设计里,CAN 和 RS485 这类差分总线接口的防护思路很接近。我的做法是 TVS 管加 PTC 自恢复保险丝的组合,TVS 管钳位差模和共模过压,PTC 限制过流,成本低、反应快,适合大多数工业场景。气体放电管虽然通流能力强,但响应速度比 TVS 慢,而且存在一个“续流”问题,用在 CAN 接口上要注意后级配合。如果项目对浪涌要求极高,可以采用 TVS + 气体放电管两级防护,中间用阻抗元件退耦,但成本会明显上升。
线上还经常出现 CAN 和 RS485 走同一根多芯电缆的情况,这不绝对禁止,但要注意两种总线的工作频率和电平不同。CAN 的隐性电平靠电阻偏置,共模范围只有 -2V 到 +7V,RS485 的共模范围更宽,一旦相互串扰,CAN 误码率会显著上升。所以现场布线时,CAN 线要尽量单独走屏蔽双绞线,屏蔽层单点接地,别图省事和电源线、动力线绑在一起。
2.3 波特率不一致和采样点带来的“看得到发不出”
CAN 总线不像以太网那样有复杂的协商机制,总线上所有节点必须使用相同的波特率。这个“相同”并不是标称值相同就行,实际晶振总会有误差,要求每个节点的实际位时间误差落在协议允许的范围内,CAN 控制器还会通过同步跳转来修正边沿误差。
把 EC312 接到一个已有的 CAN 网络时,第一步要做的是确认网络的真实波特率。不要只信设备铭牌,最好用 CAN 分析仪或者示波器实测一帧报文的位宽。我遇到过一个很有意思的现象:现场说总线上跑的是 250 kbps,但用示波器一看,真实位宽是 4μs,也就是 250k 没错,可再往下看,发现有些帧用 500k 采样也能解析出数据。原因是一些老设备在报文间隙填充了大量显性位,协议分析工具靠猜测也能蒙对帧头。
用 EC312 配置 SocketCAN 时,命令很直观:
sudo ip link set can0 type can bitrate 250000 sudo ip link set can0 up但这里有个容易被忽略的问题:默认的采样点位置可能和现场设备不匹配。采样点就是控制器在每个位时间内的什么位置去采样电平,一般建议配置在 75% 到 80% 附近。如果总线上有较长线缆,信号边沿变缓,采样点太靠后,就容易采到不确定电平。SocketCAN 支持用sample-point参数调整:
sudo ip link set can0 type can bitrate 250000 sample-point 0.75 sudo ip link set can0 up有位工程师朋友跟我讲过他们当年的疑难杂症:EC312 发送报文偶尔失败,但抓总线波形又看不出明显问题,后来把采样点从默认的 87.5% 调整到 75%,问题消失了。现场总线不是实验室那根 20cm 线,信号质量受分布电容和线缆长度影响,采样点确实需要按实际网络情况微调。
3. 把报文“翻译”成业务字段:DBC、字节序位序和信号换算
3.1 没有 DBC 文件怎么办:用报文逆向做出可用矩阵
CAN 报文本身只是一串带 ID 的数据,业务层要把帧 ID 和数据字节组合映射成具体的物理信号,最常见的方式就是用 DBC 文件描述。DBC 是 Vector 定义的一种文本格式,描述了一条 CAN 报文里包含哪些信号、每个信号在哪几个 bit 上、怎么缩放偏移。
如果主机厂或设备商能提供 DBC,解析工作会轻松很多。但很多老设备、改造项目根本拿不到原始设计文档,只能靠采集到的报文反推。这种时候需要用 CAN 分析仪长时间录制报文,观察哪些帧 ID 的周期比较固定,然后改变设备状态,比如让电机转速升高、让电池温度变化,再回到录制的报文里对比数据位的变化。帧 ID 固定且周期性发送的报文通常是周期性状态帧,比如车速、转速;事件类的报文则由触发条件决定,比如故障码、挡位切换。
几年前我在一个电池包项目上没有拿到 BMS 的 DBC,只拿到一份残缺的通信矩阵 PDF,上面很多信号的名字都对不上。当时白天跑现场录报文,晚上把 8 字节数据逐位拆开,用脚本算每个 bit 在不同工况下的变化范围,硬是拼出了一份可用的精简矩阵。这种逆向做法的准确度需要靠多工况验证,比如 SOC 从 80% 放到 10%,看哪一个数据区间在单调变化,再用已知实际值去拟合偏移和缩放系数。
3.2 用 cantools 把 DBC 快速变成可用的 Python 字典
拿到 DBC 之后,不建议自己写一套解析引擎,除非你想深入协议底层。Python 生态里cantools这个库非常成熟,解析 DBC、编码解码报文都很方便,EC312 上只要 Python 环境没问题,直接安装就能用:
pip install cantools使用流程我拆成三步。第一步加载 DBC 文件:
import cantools db = cantools.database.load_file('vehicle.dbc')第二步,根据收到的 CAN 帧 ID 找到对应的报文定义,然后解码。比如收到 ID 为 0x123 的 8 字节数据:
frame_id = 0x123 data = bytes([0x01, 0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) try: decoded = db.decode_message(frame_id, data) print(decoded) except KeyError: # 未定义的帧 ID,做记录 pass解码返回的是一个字典,key 是 DBC 里定义的信号名,value 是经过缩放偏移处理后的物理值。比如 DBC 里定义了一个VehicleSpeed信号,比例是 0.01,偏移是 0,那原始 bit 拼出来的整数是 4250,cantools会直接返回 42.5。第三步就是把这些值打包成后续要上云的结构化消息。
我在实际工程里会在解码层做一次白名单过滤,只把业务关心的信号挑出来,DBC 里可能定义了上百个信号,全量上云会让消息体巨大,增加流量成本。在边缘节点上做一个“目标信号名数组”的配置文件,只提取需要的字段,这个思路在后续扩展时非常有用。
3.3 Intel 还是 Motorola:字节序位序的经典大坑
讲到 CAN 数据解析,字节序和位序是绕不开的坑。同一个信号,A 工程师按小端解析出 100,B 工程师按大端解析出 25600,这种事故在行业里屡见不鲜。DBC 描述信号时,Intel 格式通常指小端字节序,Motorola 格式通常指大端字节序。
用一句话解释两者区别:Intel 字节序,先取低字节再取高字节,多字节信号把第一个字节放在起始位,数据排列是低地址在前。Motorola 字节序相反,对跨字节信号,高字节放在前面的起始位,而且跨字节时“位序”的编号方式还分为 Motorola Forward MSB 和 Motorola Backward MSB 几种,如果不了解报文设计者的原始约定,很容易搞错。
cantools能处理 DBC 里已经声明好的字节序,所以解析端用库就好。真正的坑在于生成 DBC 时,或者收到一个没有 DBC 而需要手动抠 bit 的报文时。我强烈建议在写任何手动解码逻辑前,先拿一个已知的信号值做验证。比如报文里某信号应当是车速 42.5 km/h,你就手动把这个值转换成预期的 bit 分布,再对照candump抓到的数据,确认自己的手动解析公式没问题。任何“看着像能对上”的猜测都要用真实数据说话,这个习惯能避免很多后续返工。
4. 把数据真正送进 AWS IoT Core:凭证、Topic 和断线策略
4.1 网关在 AWS IoT 里的“户口”和证书烧录
AWS IoT Core 对设备的接入采用的是 X.509 证书双向 TLS 认证。简单说,EC312 上要放三样东西:设备证书、私钥、根 CA 证书。云端创建 Thing 时,会为设备生成唯一的证书,然后通过 Policy 控制这个证书能对哪些 Topic 做 publish/subscribe。设备身份识别的维度很多,最常用的方式是让设备在 MQTT Connect 包里的 Client ID 与 Thing 名称一致,再用证书做传输层认证。
在 AWS 上创建一条新的设备对象,用 CLI 操作非常方便:
aws iot create-thing --thing-name ec312-gateway-01 aws iot create-keys-and-certificate --set-as-active --certificate-pem-outfile gateway01-cert.pem --public-key-outfile gateway01-public.pem --private-key-outfile gateway01-private.pem生成证书后,还要把证书关联到 Thing,并附加 Policy。很多同学第一次调试时明明证书已经生成,却连不上 AWS,大概率是 Policy 没写好,或者证书没 attach 到 Thing。Policy 至少要有这样一段:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["iot:Connect"], "Resource": "arn:aws:iot:us-east-1:123456789012:client/ec312-gateway-01" }, { "Effect": "Allow", "Action": ["iot:Publish"], "Resource": "arn:aws:iot:us-east-1:123456789012:topic/vehicles/ec312-gateway-01/data" }, { "Effect": "Allow", "Action": ["iot:Subscribe", "iot:Receive"], "Resource": "arn:aws:iot:us-east-1:123456789012:topicfilter/vehicles/ec312-gateway-01/cmd" } ] }证书文件放到 EC312 上后,还要注意文件权限。私钥文件如果被其他用户可读,SDK 连接的时候不一定报错,但这在工业项目里是安全隐患。我习惯把证书放在/etc/aws-certs/目录下,并把私钥权限设为 600。
4.2 用 AWS IoT Device SDK 实现发布,Topic 设计要带设备维度
AWS 官方提供的 Python SDK 是awsiot,用起来比较底层,但可控性强。需要提前安装:
pip install awsiotsdkEC312 上跑发布逻辑时,可以用mqtt_connection_builder.mtls_from_path直接加载证书和私钥:
from awscrt import mqtt from awsiot import mqtt_connection_builder endpoint = "your-iot-endpoint.iot.us-east-1.amazonaws.com" client_id = "ec312-gateway-01" connection = mqtt_connection_builder.mtls_from_path( endpoint=endpoint, cert_filepath="/etc/aws-certs/gateway01-cert.pem", pri_key_filepath="/etc/aws-certs/gateway01-private.pem", ca_filepath="/etc/aws-certs/AmazonRootCA1.pem", client_id=client_id, clean_session=False, keep_alive_secs=30 ) connect_future = connection.connect() connect_future.result()Topic 命名上我的经验是:第一层放业务域,第二层放设备唯一标识,第三层放消息类型。比如:
vehicles/ec312-gateway-01/data:设备上报的状态数据vehicles/ec312-gateway-01/health:网关自身的 CPU、内存、CAN 口状态vehicles/ec312-gateway-01/cmd:云端下发到网关的命令
避免把设备唯一标识只放到 Payload 里而不放到 Topic 路径上。两个原因:一是在 AWS IoT 规则引擎里,从 Topic 提取设备名再路由到不同通道非常方便;二是云端做设备级权限控制时,Policy 可以直接按 Topic 资源限制某台设备只能操作自己的路径,安全边界清晰。
发布消息时,Payload 建议统一用 JSON,并且保证字段名稳定。我见过很多项目前期随手定义字段,等数据量大了,云端做分析时才发现同一个含义的字段在不同设备上名字不一致,清洗工作非常痛苦。一个解决办法是在边缘层做一次 JSON Schema 校验,数据结构不对的报文直接丢弃并记录日志。
4.3 断线重连和本地缓冲:网络不可能永远可靠
从现场网关到 AWS IoT Core 的网络要经过基站或者宽带,总会出现瞬时断网。如果网关每次重连都要从有数据的时刻重新开始,必然丢失一部分窗口数据。要缓解这个问题,需要两端配合:MQTT 会话保持加上本地消息缓存。
AWS IoT Core 支持持久会话,客户端连接时设置clean_session=False,这样离线期间云端会帮设备保留订阅关系,但消息队列的保留策略取决于 QoS 和队列长度,不能完全依赖云端缓存。更稳妥的做法是在 EC312 本地落盘缓存。我的实现思路是:解析后的结构化消息先写入一个本地 SQLite 表或者环形文件缓冲,每次发布成功后标记 offset,只有收到 IoT Core 的 PUBACK 才把这条消息标记为已发送。如果网络断了,发布循环会阻塞或报错,消息继续留在本地,重连后再按时间戳顺序补发。
但有两点需要注意:一是必须在消息里带有设备本地采集时间戳和发送时间戳,这样云端后续做数据回放或统计时,能区分真正的数据时间和到达时间;二是本地缓存目录要考虑 flash 寿命,EC312 如果用 eMMC,频繁写入会有寿命问题,缓存文件可以挂到内存盘或者开启定时批量写入,避免高频小文件写放大。
5. 实测中会遇到的三类问题:丢帧、乱序和总线关闭
5.1 收到了部分报文,但客户坚持说“丢了数据”
项目上线后,客户反馈云平台少了一些数据,我们第一反应是查 MQTT 链路,抓了半天的包,最后发现瓶颈根本不在云端,而在 CAN 总线的接收侧。EC312 的 SocketCAN 接收队列如果被瞬时大流量打满,新到的帧会被内核直接丢弃。这个问题尤其在总线报文量大的时候特别明显。
一个快速检查方法是看ip -s -d link show can0输出的统计信息,里面包含了接收丢弃、CRC 错误、总线错误等计数器。如果发现 RX drop 非零,就说明用户态程序读取速度跟不上内核接收速度。解决办法有几个方向:把读 CAN 的进程优先级提高,使用setsockopt调大 SocketCAN 接收缓冲区,或者在采集进程里用单独线程及时把帧搬进内存队列,避免业务处理阻塞读接口。
另一个常见误判是:总线上周期报文是 10ms 一帧,云端看到的时间跨度却存在间隙,但 CAN 分析仪抓到的包是连续的。这种情况往往是边缘处理时,对某些未纳入白名单的帧 ID 做了静默丢弃。比如只上云了车速和转速信号,而客户拿原始 CAN 报文对比,自然觉得“丢了数据”。事实不是丢了,而是解析链路里把不关心的帧过滤掉了。边缘网关这类数据裁剪功能要在项目文档里写清楚,否则验收阶段容易扯皮。
5.2 云端看到的数据时间顺序,和现场实际发生顺序对不上
CAN 采集进程拿到的时间戳和 MQTT 消息里的业务时间戳,经常被混为一谈。EC312 接收 CAN 帧时,SocketCAN 会给帧打上内核时间戳tstamp,这个时间来自系统时钟。如果 EC312 没有配置 NTP 同步,系统时间漂移了,最后上云的数据时间就会不准。
解决方法是两个时间戳分开处理:CAN 帧的内核时间戳用来做现场诊断和报文重放,而给云端业务数据打时间戳时,建议在采集进程里用统一时钟源生成。如果总线上的数据采集对时间精度要求很高,例如碰撞事件还原、多个 ECU 事件联动分析,EC312 需要支持外部时间同步源。普通的 NTP 精度只能到毫秒量级,如果要微秒级同步,通常得靠 PTP 或者接入 GPS 授时模块。
我遇到过一种更隐蔽的乱序:采集线程从 SocketCAN 读到报文后放入队列,解析线程再取出处理,由于多线程调度,进队列顺序和出队列顺序可能不完全一致,导致个别消息发布到云端后乱序。解决办法是在消息体里加一个单调递增的序列号,云端消费时如果发现序列号跳变,就说明边缘侧有乱序或丢失。实际上大多数业务不要求 100% 严格有序,但如果要做数据回放,序列号是必需的。
5.3 总线上出现 BUS-OFF,EC312 是怎么恢复的
CAN 控制器检测到自身发送错误过多时,会进入 BUS-OFF 状态,主动断开与总线的连接。这个机制是为了避免一个故障节点拖垮整条总线。EC312 上的 CAN 控制器也可能因为外部干扰、波特率不匹配或者总线短路进入 BUS-OFF。
有一次现场间歇性通信失败,我们登录 EC312 一看,can0的状态变成了BUS-OFF。SocketCAN 默认会自动恢复,但恢复时间取决于控制器的恢复序列,而且如果干扰源一直存在,节点会在恢复后再次进入 BUS-OFF,形成循环。这时候要做的不是反复手工重启网卡,而是找到干扰源。多发生在电机启停瞬间、变频器运行时。另一个方向是加强外部硬件防护,比如改善屏蔽层接地、增加共模电感。
在软件层面,EC312 上可以写一个监控脚本定时读取 CAN 口状态,如果发现 BUS-OFF 次数异常增长,就通过ip link set can0 down和up重新初始化控制器,同时向云端发一条网关健康告警。这些状态信息单独走一个healthTopic,不要混在业务数据里,方便云端做告警规则。
6. 已交付网关的远程维护:配置热更新与可观测性
6.1 不用每次改业务都跑现场
EC312 这种边缘网关一旦部署到现场,再指望工程师拿着笔记本跑现场升级程序是不现实的。所以项目开始时就要考虑远程维护通道。AWS IoT Core 支持通过预留 Topic 下发命令,我通常会把一些常用配置做成“参数热更新”而不是整个程序升级。比如修改需要采集的 CAN 帧 ID 白名单、调整 MQTT 上报频率、启用或停用某个信号,这些都可以通过下发一条 JSON 配置消息来动态修改,不用重启进程。
在订阅命令 Topic 时,建议整个链路要对消息做版本管理和校验。云端下发配置时,网关收到后先做合法性检查,再写入本地配置文件,并回发一条确认消息。如果检查失败,要在本地保留上一份可用配置,避免进程起来后因为读到半截 JSON 直接崩溃。
远程更新本身也有风险。我在测试环境验证过一个流程:修改上报周期配置,结果数值设成了 1 毫秒一上报,如果就这么全网下发,网关流量和云端费用都会立刻失控。好在那条消息是在测试环境先跑了一遍,触发了上限保护。从那以后,我在边缘程序里对所有可配置参数都加了最大值和最小值约束,防呆设计在远程配置场景里非常重要。
6.2 给云端同事省事的日志设计
业务上云之后,网关上的日志就不只是给现场工程师看了,更要给云端和应用侧同事看。但不要把调试日志直接发到业务 Topic 里,否则后面分析数据时会混入大量噪声。我一般分三条线:业务数据走 data Topic,网关健康指标走 health Topic,调试和错误日志走 log Topic。业务数据只需要最干净的结构化字段,健康指标可以包含 CPU 占用、内存余量、CAN 口丢包计数、MQTT 重连次数,调试日志则只对关键事件上送。
日志格式统一用 JSON 按行输出,也要带上本地时间。EC312 上跑着多个进程时,日志要集中到系统日志里统一管理。否则,很难在问题发生后快速定位到底是采集进程异常、解析进程内存泄漏还是 MQTT 连接被云端断开。我在交付时会让每台网关把进程启动时间、版本号、配置文件的哈希值一起上报,这样云端看到某台设备上报的数据不规律时,能直接判断是不是运行了一个旧的版本。
最后再聊一个落地习惯:设备在客户现场跑起来后,我做的第一件事不是看云平台上的数据图表,而是先翻一遍healthTopic 里的 CAN 口错误计数和网关重启次数。这两个指标能在用户还没发现问题前暴露很多隐患,做好这层可观测性,比盲目加更多业务字段上云更有价值。