📚 第八章 应用协议
📄 一句话概括

三个真实用法:画流量图、端口告警、批量改配置,每个都踩一个坑。 最大的坑是 32 位计数器回绕:千兆口上 34 秒就绕一整圈。

📝 本节和前面几节的关系

前三节讲机制,这一节讲「真用起来会出什么事」。协议本身很简单,坑全在用法上。 Demo 把一个 Trap 拆成字节,看它和「问」在结构上差了什么。

一、画流量图:差分、回绕、重启

最经典的用法是画流量图,而它的原理只有一句:取两次累计计数,相减,再除以时间。 公式是「本次减上次,乘 8,除以间隔秒数」,结果就是 bps。 这里埋着第一个坑:计数器只有 32 位。 2 的 32 次方字节约等于 4 GiB,所以在千兆口上,125000000 字节每秒只要 34.4 秒就能填满一圈。 如果你 60 秒才轮询一次,中间已经绕过一圈多, 相减得到的是一个毫无意义的数——流量图会突然掉到谷底,然后恢复正常, 运维里管这个叫「计数器回绕造成的尖刺」。

链路速率 2 的 32 次方字节能撑多久
1000 Mbps 34.4 秒
100 Mbps 343.6 秒,约 5.7 分钟
10 Mbps 3436 秒,约 57 分钟

解决办法是换 64 位计数器:ifHCInOctets 里的 HC 就是 high capacity, 64 位在千兆口上能撑 1165 年,等于永不回绕。 所以现在的采集器一律优先取 HC 版本,取不到才退回 32 位。 第二个坑是设备重启。 计数器从零开始,本次减上次就是负数,代码里必须判负并丢掉这一个样本。 不判的话,图上会出现一个巨大的负值尖刺,很多监控系统的「流量图出现怪峰」都是这两条之一。

二、端口告警:Trap 是单向的

第二个用法是等设备主动报,也就是 Trap。 一个链路 Down 的 v2c Trap 只有 54 字节, 里面有团体名、有 sysUpTime、有 trapOID、有一个 varbind。 它比其他 PDU 少了一样东西:没有 request-id。 因为不需要配对——GetRequest 要 request-id 是为了把响应认回来, 而 Trap 发出去就完事,没人等它。 少了配对,就等于少了可靠性。 Trap 跑在 UDP 上,丢了就丢了,设备不会重发。 想可靠只能用 InformRequest(A6),那个会让设备等一个确认,没等到就重发, 代价是设备要为每条通知占一份资源。 有人会想「那我让设备连发三次不就行了」,账不是这么算的:

单包丢失率 三次里至少一次成功 三次全都成功
1% 99.9999% 97.0%
5% 99.9875% 85.7%
20% 99.2% 51.2%

第二列确实变好了,但第三列暴露了代价:丢 5% 时,大约每七个事件就有一个被重复上报, 而且是三倍流量。 所以工程上的标准做法是「两条腿走路」:Trap 用来争取秒级发现,关键指标仍然靠轮询兜底。 只用 Trap 会漏,只用轮询会慢。

三、批量改配置:为什么别用 Set

第三个用法最容易被滥用:既然有 SetRequest,那能不能写个脚本把几百台设备的配置全改了? 技术上能,工程上不该。 原因有三条:一是 SNMP 没有事务,改到第七台失败,前六台已经变了, 而协议本身不给你回滚的手段;二是没有语法检查,写错一个 OID 只是返回一个错误码; 三是 OID 和厂商绑定,同一件事在华为和思科下面是两套编号,脚本没法通用。 所以业界的默契是「SNMP 用来看,不用来改」。 真要批量改,今天的答案是 NETCONF 或者 RESTCONF,它们带事务、带候选配置、带回滚, 改错了可以整体丢掉。 这条分工不是技术限制,是设计取舍:SNMP 诞生在「设备很笨、网络很慢」的年代, 它把报文做到了几十字节,代价就是放弃了一切「重」的语义。 理解了这一点,就不会怪它「功能少」。

四、动手 Demo:Trap 报文和「问」有什么不一样

手搓一个 v2c 的 linkDown Trap,把它逐层拆开,看它到底比 GetRequest 少了什么。

def blen(n):
    return bytes([n]) if n < 128 else bytes([0x81, n])

def tlv(tag, body):
    return bytes([tag]) + blen(len(body)) + body

def oid(dotted):
    a = [int(x) for x in dotted.split(".")]
    return tlv(0x06, bytes([a[0] * 40 + a[1]] + a[2:]))

uptime = tlv(0x43, bytes([0, 0, 0x30, 0x39]))
trap_oid = tlv(0x06, bytes([0x2B, 6, 1, 6, 3, 1, 1, 4, 1, 0]))
vb = tlv(0x30, oid("1.3.6.1.4.1.9.9.1.1") + tlv(0x41, bytes([0, 0, 0, 5])))
pdu = tlv(0xA7, uptime + trap_oid + tlv(0x30, vb))
msg = tlv(0x30, tlv(0x02, b"\x01") + tlv(0x04, b"public") + pdu)
print(len(msg))

h = msg.hex()
for i in range(0, len(h), 32):
    print("    " + h[i:i + 32])

实跑输出:

--------------------------------------------------------------------
Trap 报文和「问」有什么不一样
--------------------------------------------------------------------
**Trap 是设备主动发的**,**方向和管理器问的正好相反。**
**所以它的报文结构也少了一样东西**——**没有 request-id,因为不需要配对。**

**一个链路 Down 的 v2c Trap,54 字节**:

    303402010104067075626c6963a72743
    0400003039060a2b0601060301010401
    003013301106092b0601040109090101
    410400000005

  PDU 类型                谁发             有 request-id 吗            特点
  ------------------------------------------------------------------------------------
  `A0` GetRequest         **管理器发**     **有 request-id**           **等一个响应**
  `A1` GetNextRequest     **管理器发**     **有 request-id**           **walk 用**
  `A2` Response           **设备发**       **带原来的 request-id**     **配对上一条**
  `A3` SetRequest         **管理器发**     **有 request-id**           **改配置**
  `A7` Trap               **设备发**       **没有**                    **没人等它**

**最后一行是 Trap 的本质**:**它是一次「单向广播式」的通知,发出去就不管了。**
**所以它不可靠**——**UDP 丢了就丢了,设备不会重发。**
**要可靠就得用 InformRequest(`A6`)**,**那个会让设备等一个确认。**

**这就带来一个真实的工程问题**:

  单包丢失率     一次到达率     三次里至少一次     三次全都到
  ------------------------------------------------------------
  0%             100%           100.0000%          100.0%
  1%             99%            99.9999%           97.0%
  5%             95%            99.9875%           85.7%
  20%            80%            99.2000%           51.2%

**第三列是「靠重发三次来提可靠性」的效果**:**单包丢 5% 时,发三次能到 99.9875%。**
**但第四列告诉你代价:三次全都到的概率只有 85.7%**——
**也就是说,大约每七个事件就有一个会被重复上报。**
**而且是三倍流量。**
**所以工程上的做法是「Trap 用 UDP 尽力发,关键指标用轮询兜底」。**
**两条腿走路,比把一条腿做粗更划算。**

开头 3034 是「SEQUENCE,长度 52」,加这两个字节正好 54。 接着 020101 是版本 1,0406 7075626c6963 是团体名 public 的 ASCII。 再往后 a727 就是 PDU:A7 是 Trap 的标签,27 是它的长度。 注意它后面直接跟着 43(时间戳), 没有 02 01 xx 那样的 request-id——编号在报文里根本不存在,不是省略了值。 最后那个 varbind 是「哪个口 Down 了」:OID 指着 ifIndex,值是 5。 所以整个报文就是一句话:「第 5 号口在开机 12345 毫秒时 Down 了」。 54 字节,装在 UDP 里发出去,谁收到谁处理,收不到就算了。

30 秒实验

  1. 算一下:千兆口、60 秒轮询一次,32 位计数器绕了几圈?(60 除 34.4,约 1.74 圈,差值完全失效)
  2. 想一下:为什么 Trap 里没有 request-id?
  3. 想一下:为什么「重发三次」不能把 Trap 变成可靠传输?(丢失率是乘性的,只能逼近不能到达)

常见误区

  1. 以为「取回计数器就是流量」。 要减上一次、乘 8、除以时间,而且要先判负。
  2. 以为「Trap 发了就一定到」。 UDP 丢 5% 时,连发三次也只有 85.7% 能全到。
  3. 以为「SNMP 能 Set 所以能用它管配置」。 无事务、无回滚,批量改必然半途而废。
自测三问(点开对答案) 1. 32 位计数器在哪一档速率下会在一次轮询内回绕? —— **千兆口**,34.4 秒一圈,60 秒轮询必然跨圈。 2. Counter32 和 Counter64 的 OID 怎么区分? —— **64 位版本带 HC 前缀**,比如 `ifHCInOctets`。 3. Trap 和 InformRequest 的关键差别? —— **后者要确认、能重发**,代价是设备要为它占资源。

网络管理这一块到此结束。下一节换个话题:多媒体为什么必须跑在 UDP 上。

相关笔记