Lab 3:有状态流量监测¶
在本节实验中,实验者需要使用 Tofino Register 保存跨报文状态,根据一个流已经到达的报文数量执行正常转发、DSCP 标记或丢弃,并通过 Counter 和 Digest 观察数据平面的处理结果。
实验目标¶
- 根据 IPv4 五元组计算 Hash,将不同流映射到 Register entry;
- 使用
RegisterAction原子更新报文计数; - 根据计数结果选择不同的数据平面动作;
- 使用 Counter 统计各类处理结果;
- 使用 Digest 将阈值事件上报控制面。
实验环境¶
实验原理¶
无状态处理与有状态处理¶
在无状态处理中,交换机只根据当前报文的 header、metadata 和控制面下发的表项决定动作。相同报文在相同表项配置下会得到相同结果,先前经过的报文不会影响本次处理。
在有状态处理中,数据面还会读取并修改保存在交换机中的状态。本实验为每个 flow bucket 保存一个计数,因此同一条流的第 1 个、第 6 个和第 9 个报文可以得到不同处理结果。这个计数由报文在数据面中直接更新,不需要控制面逐包参与。
P4 通过 extern 使用目标架构提供的专用能力。Hash、Register、RegisterAction、Counter、Digest 和 Checksum 都是 TNA 提供的 extern;P4 程序负责指定输入字段和处理逻辑,编译器负责将它们映射到 Tofino 的硬件资源。
IPv4 五元组¶
网络中的“流”通常指具有相同通信特征的一组报文。对于 IPv4 上的 TCP 或 UDP 流,通常使用以下五个字段共同标识一条流,这组字段称为 IPv4 五元组:
| 字段 | 含义 |
|---|---|
| IPv4 source address | 发送方 IP 地址 |
| IPv4 destination address | 接收方 IP 地址 |
| IP protocol | 传输层协议,如 TCP(6)或 UDP(17) |
| Transport source port | 发送方的 TCP/UDP 端口 |
| Transport destination port | 接收方的 TCP/UDP 端口 |
本实验只处理 UDP 报文,因此后两个字段分别取自 UDP source port 和 destination port。五个字段全部相同的报文属于同一条流;任意一个字段不同,就被视为另一条流。实验中的 Flow A 和 Flow B 只有 UDP source port 不同,因此是两条不同的 IPv4/UDP 流。
使用 Hash 选择 Register entry¶
P4 Register 可以看作数据平面中的数组,访问时需要提供一个整数 index。本实验声明了 1024 个 Register entry,用于保存不同流的报文计数,因此 index 的范围是 0..1023。
为了给每条流选择一个 entry,数据平面对 IPv4 五元组计算 32-bit CRC32 Hash,再取 Hash 结果的低 10 bit 作为 flow_index。10 bit 可以表示 1024 个不同的 index,因此 flow_index 可以直接访问 flow_packet_count[0..1023]。相同的 IPv4 五元组会得到相同的 Hash,从而在后续报文到达时继续访问同一个 entry。
Host A 向 10.2.3.4:9000 发送 IPv4/UDP 报文时,数据平面按以下过程选择 Register entry:
IPv4 五元组
-> CRC32 Hash(32 bit)
-> 取低 10 bit 得到 flow_index
-> 访问 flow_packet_count[flow_index]
CRC32 Hash 的作用只是快速选择 Register entry,不是加密或报文差错校验。由于最终只有 1024 个可选 entry,不同的 IPv4 五元组仍可能得到相同的 flow_index,这种情况称为 Hash collision。发生碰撞时,两条流会共享同一个 Register entry 和报文计数。因此,Register entry 只能近似对应一条流,不能替代完整的 IPv4 五元组作为流的唯一标识。
Metadata、Register、Counter 与 Digest¶
这四类对象承担不同的职责:
| 对象 | 生命周期 | 本实验中的用途 |
|---|---|---|
| Metadata | 仅随当前报文经过 pipeline,处理结束后消失 | 在各处理阶段之间传递 Register index、当前计数和处理结果 |
| Register | 保存在数据平面中,后续报文可以继续读取和修改 | 保存每个 flow bucket 已到达的报文数量 |
| Counter | 保存在数据平面中,按指定 index 累加报文数和字节数 | 汇总 normal、marked 和 dropped 三类处理结果 |
| Digest | 数据面发给控制面的异步事件 | 在某条流首次达到阈值时,上报 IPv4 五元组、bucket 和计数 |
Register 与 Counter 都能跨报文保存数值,但用法不同。Register 的值会被数据面读出并参与后续决策;Counter 主要用于统计和观测。本实验使用 indirect Counter,由 P4 程序显式提供 index,而不是将 Counter 固定绑定到某一条 match-action 表项。
Digest 不会把原报文复制到 CPU,也不会等待控制面返回结果后再继续转发。它只将 Digest.pack() 中指定的少量字段异步上报,适合报告“计数刚刚达到 5”一类事件。控制面处理完成后需要回复 ACK,使硬件能够释放该批 Digest 使用的资源。
为什么使用 RegisterAction¶
Tofino pipeline 中可能同时存在多个属于同一条流的报文。一次计数更新包含“读取旧值、加一、写回新值”三个逻辑步骤。如果把读取和写入拆成彼此独立的操作,两个报文可能读到相同旧值并写回相同新值,导致少计一个报文。
RegisterAction 将对同一个 Register entry 的读取、计算和写回作为一次原子操作执行。这里的“原子”表示其他报文不会在本次更新中间修改该 entry,从而保证每个到达的报文只增加一次计数。
报文处理流程¶
每个符合实验条件的报文依次完成以下操作:
- 从 IPv4 和 UDP header 中取出 IPv4 五元组并计算 Hash;
- 使用 Hash 的低 10 bit 选择一个 Register entry;
- 使用
RegisterAction将该 entry 的计数原子加一,并把新计数写入当前报文的 metadata; - 根据新计数选择正常转发、DSCP 标记后转发或丢弃;
- 根据处理结果更新对应的 Counter;
- 如果新计数恰好为 5,额外向控制面发送一条 Digest。
状态处理规则¶
Register 完成递增后,当前报文根据新计数执行以下动作:
| 新计数 | 数据平面动作 |
|---|---|
1..4 |
正常转发 |
5 |
正常转发,并发送一次 Digest |
6..8 |
将 DSCP 修改为 46,更新 IPv4 checksum 后转发 |
>=9 |
丢弃 |
非 UDP 报文以及 destination address 或 destination port 不符合测试条件的报文直接丢弃,不更新实验使用的 Register 和 outcome Counter。
实验步骤¶
1. 基于 IPv4 五元组计算 Hash¶
在 P4 程序中使用 TNA Hash extern,对 IPv4 五元组中的以下字段计算 32-bit CRC32:
IPv4 source address
IPv4 destination address
IP protocol
UDP source port
UDP destination port
取 Hash 结果的低 10 bit 作为 Register index。Hash 计算与后续 RegisterAction 应在同一个 table action 中完成。计算得到的 index 写入 ig_md.flow_index;这是当前报文的临时 metadata,而真正跨报文保存的计数位于 flow_packet_count[index]。
2. 实现 Register 原子更新¶
使用 RegisterAction 在一次操作中读取旧值、计算新值并写回:
new_count = flow_packet_count[index] + 1
flow_packet_count[index] = new_count
本实验使用 8-bit Register。计数达到 255 后保持饱和,避免数值回绕为 0。控制面执行 show registers 时会先同步硬件 Register,再显示非零 entry。
8-bit 无符号整数的最大值是 255。如果直接对 255 加一,结果会回绕为 0,使已经进入丢弃区间的流重新被当作新流处理。饱和计数会在到达最大值后保持 255,可避免这一问题。
3. 实现 Counter 与 Digest¶
使用一个 indirect Counter 的三个 index 分别记录:
| Counter index | 统计内容 |
|---|---|
0 |
正常转发的报文 |
1 |
DSCP 标记后转发的报文 |
2 |
因达到阈值而丢弃的报文 |
当一个流的计数第一次达到 5 时,使用 Digest.pack() 将 IPv4 五元组、Register index 和新计数上传控制面。条件使用 == 5 而不是 >= 5,因此每个未发生碰撞的 flow bucket 只会在越过阈值时上报一次。控制面 callback 读取 Digest 后发送 ACK。
4. 更新 DSCP 与 Checksum¶
DSCP 是 IPv4 Differentiated Services 字段的高 6 bit,可用于给报文标记不同的服务类别。本实验使用十进制值 46,使进入 6..8 计数区间的报文能够在 Host B 抓包中被清楚地区分;这个标记本身并不会自动产生队列调度或带宽保证。
DSCP 属于 IPv4 header,因此修改后需要重新计算 IPv4 header checksum。Host B 的验证脚本会同时检查 DSCP 和 checksum。
5. 编译与运行¶
完成前面的 P4 修改后,在 Tofino 交换机上进入 p4-stateful-monitor 目录:
make
./contrl
控制面程序开启 9/0 和 9/1、配置转发规则、初始化 Counter,并注册 Digest callback。出现以下提示符后即可开始测试:
[CONTROL] status=READY
lab3>
控制器支持以下命令:
show counters
show registers
clear counters
clear registers
clear all
help
quit
Register 会在多轮测试之间保留状态。每次重新发送测试流量前,应执行:
lab3> clear all
6. 配置实验主机¶
在 Host A 上运行:
ip link set eth0 down
ip link set eth0 address 02:00:00:00:00:01
ip link set eth0 up
ip addr replace 192.168.1.2/24 dev eth0
在 Host B 上运行:
ip link set eth0 down
ip link set eth0 address 02:00:00:00:00:02
ip link set eth0 up
ip addr replace 192.168.2.2/24 dev eth0
使用 ip -br link show dev eth0 确认两端接口均为 UP 和 LOWER_UP。
实验验证¶
测试脚本发送两个 UDP source port 不同的流:
| Flow | UDP source port | 发送数量 |
|---|---|---|
| A | 40001 |
10 |
| B | 40002 |
3 |
先在控制器中清空状态:
lab3> clear all
在 Host B 启动接收脚本:
python3 scripts/verify_packets.py
随后在 Host A 发送测试报文:
python3 scripts/send_tests.py
两个流未发生 Hash collision 时,Host B 应观察到:
| Flow | 正常转发 | DSCP=46 | 丢弃 | 接收总数 |
|---|---|---|---|---|
| A | 5 | 3 | 2 | 8 |
| B | 3 | 0 | 0 | 3 |
Flow A 的第 5 个报文到达时,控制面应输出一条 Digest:
[DIGEST] src=192.168.1.2:40001 dst=10.2.3.4:9000 bucket=... count=5
测试结束后查询 Register 和 Counter:
lab3> show registers
lab3> show counters
预期 packet counter 为:
[COUNTER] normal packets=8 bytes=...
[COUNTER] marked packets=3 bytes=...
[COUNTER] dropped packets=2 bytes=...
硬件 byte counter 可能包含 Ethernet FCS,因此测试时以 packet counter 和 Host B 的接收数量为主要检查依据。