P4 09-Flowlet Switching
P4示例程序-09 Flowlet
功能
当网络中有多条到目的地的等成本路径时,ECMP把流哈希到不同路径。如果几个象流恰好哈希到同一条路径,就会造成该链路拥塞,而其他链路闲置,非常影响性能。Flowlet技术将流拆分为多个体积更小的Flowlet,并将Flowlet哈希到不同的路径,通过细粒度的哈希实现更好的负载均衡。具体而言:如果检测到同一个流的连续两个数据包之间有比较大的空隙,就把接下来的这段重新哈希到另一条路径
拓扑结构
代码
p4app.json1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36{
"p4_src": "p4src/flowlet_switching.p4",
"cli": true,
"pcap_dump": true,
"enable_log": true,
"topology": {
"assignment_strategy": "mixed",
"links": [["h1", "s1"], ["h2", "s6"], ["s1", "s2"], ["s1", "s3"], ["s1", "s4"], ["s1", "s5"], ["s2", "s6"], ["s3", "s6"], ["s4", "s6"], ["s5", "s6"]],
"hosts": {
"h1": {
},
"h2": {
}
},
"switches": {
"s1": {
"cli_input": "sw-commands/s1-commands.txt"
},
"s2": {
"cli_input": "sw-commands/s2-commands.txt"
},
"s3": {
"cli_input": "sw-commands/s3-commands.txt"
},
"s4": {
"cli_input": "sw-commands/s4-commands.txt"
},
"s5": {
"cli_input": "sw-commands/s5-commands.txt"
},
"s6": {
"cli_input": "sw-commands/s6-commands.txt"
}
}
}
}- Headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64const bit<16> TYPE_IPV4 = 0x800; // IPv4 的 EtherType 值
typedef bit<9> egressSpec_t; // 出口端口类型
typedef bit<48> macAddr_t; // MAC 地址类型
typedef bit<32> ip4Addr_t; // IPv4 地址类型
header ethernet_t {
macAddr_t dstAddr; // 目标 MAC 地址
macAddr_t srcAddr; // 源 MAC 地址
bit<16> etherType; // 以太网类型字段
}
header ipv4_t {
bit<4> version; // IP 版本
bit<4> ihl; // 首部长度
bit<6> dscp; // 服务类别
bit<2> ecn; // 显式拥塞通知
bit<16> totalLen; // 总长度
bit<16> identification; // 标识符
bit<3> flags; // 标志位
bit<13> fragOffset; // 分片偏移
bit<8> ttl; // 生存时间
bit<8> protocol; // 上层协议
bit<16> hdrChecksum; // 首部校验和
ip4Addr_t srcAddr; // 源 IP 地址
ip4Addr_t dstAddr; // 目标 IP 地址
}
header tcp_t {
bit<16> srcPort; // 源端口
bit<16> dstPort; // 目标端口
bit<32> seqNo; // 序列号
bit<32> ackNo; // 确认号
bit<4> dataOffset; // 数据偏移
bit<4> res; // 保留字段
bit<1> cwr; // 拥塞窗口减少
bit<1> ece; // ECN 回应
bit<1> urg; // 紧急
bit<1> ack; // 确认
bit<1> psh; // 推送
bit<1> rst; // 重置
bit<1> syn; // 同步
bit<1> fin; // 终止
bit<16> window; // 窗口大小
bit<16> checksum; // 校验和
bit<16> urgentPtr; // 紧急指针
}
struct metadata {
bit<14> ecmp_hash; // ECMP 哈希值
bit<14> ecmp_group_id; // ECMP 组 ID
bit<48> flowlet_last_stamp; // 上一个 flowlet 的时间戳
bit<48> flowlet_time_diff; // 当前与上一次包的时间差
bit<13> flowlet_register_index; // 寄存器索引值
bit<16> flowlet_id; // 当前包所属 flowlet 的 ID
}
struct headers {
ethernet_t ethernet; // 以太网头
ipv4_t ipv4; // IPv4 头
tcp_t tcp; // TCP 头
} - Parser
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30parser MyParser(packet_in packet,
out headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
state start {
transition parse_ethernet; // 初始状态跳转到解析以太网
}
state parse_ethernet {
packet.extract(hdr.ethernet); // 解析以太网头
transition select(hdr.ethernet.etherType){
TYPE_IPV4: parse_ipv4; // 若是 IPv4,跳转到解析 IPv4
default: accept; // 否则直接接受
}
}
state parse_ipv4 {
packet.extract(hdr.ipv4); // 解析 IPv4 头
transition select(hdr.ipv4.protocol){
6 : parse_tcp; // 如果协议号是 6(TCP),解析 TCP
default: accept; // 否则直接接受
}
}
state parse_tcp {
packet.extract(hdr.tcp); // 解析 TCP 头
transition accept; // 解析完成
}
} - Checksum Verification
1
2
3
4control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
apply {
}
} - Ingress Processing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
register<bit<ID_WIDTH>>(REGISTER_SIZE) flowlet_to_id; // 存储每个 flowlet 的 ID
register<bit<TIMESTAMP_WIDTH>>(REGISTER_SIZE) flowlet_time_stamp; // 存储 flowlet 上一次时间戳
action drop() {
mark_to_drop(standard_metadata); // 标记丢包
}
action read_flowlet_registers() {
hash(meta.flowlet_register_index, HashAlgorithm.crc16, // 计算 flowlet 的哈希索引
(bit<16>)0,
{ hdr.ipv4.srcAddr, hdr.ipv4.dstAddr, hdr.tcp.srcPort, hdr.tcp.dstPort, hdr.ipv4.protocol },
(bit<14>)8192);
flowlet_time_stamp.read(meta.flowlet_last_stamp, (bit<32>)meta.flowlet_register_index); // 读取上次时间戳
flowlet_to_id.read(meta.flowlet_id, (bit<32>)meta.flowlet_register_index); // 读取 flowlet ID
flowlet_time_stamp.write((bit<32>)meta.flowlet_register_index, standard_metadata.ingress_global_timestamp); // 写入当前时间戳
}
action update_flowlet_id() {
bit<32> random_t;
random(random_t, (bit<32>)0, (bit<32>)65000); // 生成随机 ID
meta.flowlet_id = (bit<16>)random_t;
flowlet_to_id.write((bit<32>)meta.flowlet_register_index, (bit<16>)meta.flowlet_id); // 写入新的 flowlet ID
}
action ecmp_group(bit<14> ecmp_group_id, bit<16> num_nhops) {
hash(meta.ecmp_hash, HashAlgorithm.crc16, (bit<1>)0, // 计算 ECMP 哈希
{ hdr.ipv4.srcAddr, hdr.ipv4.dstAddr, hdr.tcp.srcPort, hdr.tcp.dstPort, hdr.ipv4.protocol, meta.flowlet_id },
num_nhops);
meta.ecmp_group_id = ecmp_group_id; // 设置 ECMP 组 ID
}
action set_nhop(macAddr_t dstAddr, egressSpec_t port) {
hdr.ethernet.srcAddr = hdr.ethernet.dstAddr; // 将源 MAC 设置为原目标 MAC(占位)
hdr.ethernet.dstAddr = dstAddr; // 设置目标 MAC 地址
standard_metadata.egress_spec = port; // 设置输出端口
hdr.ipv4.ttl = hdr.ipv4.ttl - 1; // TTL 减 1
}
table ecmp_group_to_nhop {
key = {
meta.ecmp_group_id: exact;
meta.ecmp_hash: exact;
}
actions = {
drop;
set_nhop;
}
size = 1024;
}
table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: lpm; // 目的 IP 地址最长前缀匹配
}
actions = {
set_nhop;
ecmp_group;
drop;
}
size = 1024;
default_action = drop;
}
apply {
if (hdr.ipv4.isValid()) {
@atomic {
read_flowlet_registers(); // 读取寄存器
meta.flowlet_time_diff = standard_metadata.ingress_global_timestamp - meta.flowlet_last_stamp; // 计算时间差
if (meta.flowlet_time_diff > FLOWLET_TIMEOUT) { // 若时间差超过超时时间
update_flowlet_id(); // 更新 flowlet ID
}
}
switch (ipv4_lpm.apply().action_run) {
ecmp_group: {
ecmp_group_to_nhop.apply(); // 应用 ECMP 下一跳
}
}
}
}
} - Egress Processing
1
2
3
4
5
6control MyEgress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
apply {
}
} - Checksum Computation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24control MyComputeChecksum(inout headers hdr, inout metadata meta) {
apply {
update_checksum( // 更新 IPv4 首部校验和
hdr.ipv4.isValid(),
{
hdr.ipv4.version,
hdr.ipv4.ihl,
hdr.ipv4.dscp,
hdr.ipv4.ecn,
hdr.ipv4.totalLen,
hdr.ipv4.identification,
hdr.ipv4.flags,
hdr.ipv4.fragOffset,
hdr.ipv4.ttl,
hdr.ipv4.protocol,
hdr.ipv4.srcAddr,
hdr.ipv4.dstAddr
},
hdr.ipv4.hdrChecksum,
HashAlgorithm.csum16
);
}
} - DE parser
1
2
3
4
5
6
7control MyDeparser(packet_out packet, in headers hdr) {
apply {
packet.emit(hdr.ethernet); // 输出以太网头
packet.emit(hdr.ipv4); // 输出 IPv4 头
packet.emit(hdr.tcp); // 若存在,输出 TCP 头
}
} - Switch
1
2
3
4
5
6
7
8V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputeChecksum(),
MyDeparser()
) main; s1-commands.txt1
2
3
4
5
6
7
8
9
10
11
12
13// 交换机S1
// 设置匹配表ipv4_lpm和匹配表ecmp_group_to_nhop的默认动作为丢包
table_set_default ipv4_lpm drop
table_set_default ecmp_group_to_nhop drop
// 设置发完主机Host1的数据包直接通过1端口发出,不使用ECMP
table_add ipv4_lpm set_nhop 10.0.1.1/32 => 00:00:0a:00:01:01 1
// 设置发完Host2的数据包使用ECMP,ECMP的组号为1、共包含4条可选路径
table_add ipv4_lpm ecmp_group 10.0.6.2/32 => 1 4
// 在ECMP组1中,针对哈希结果0、1、2、3分布设置出端口为2、3、4、5
table_add ecmp_group_to_nhop set_nhop 1 0 => 00:00:00:02:01:00 2
table_add ecmp_group_to_nhop set_nhop 1 1 => 00:00:00:03:01:00 3
table_add ecmp_group_to_nhop set_nhop 1 2 => 00:00:00:04:01:00 4
table_add ecmp_group_to_nhop set_nhop 1 3 => 00:00:00:05:01:00 5s2-commands.txt1
2
3
4
5
6
7
8// 交换机S2
// 设置匹配表ipv4_lpm和匹配表ecmp_group_to_nhop的默认动作为丢包
table_set_default ipv4_lpm drop
table_set_default ecmp_group_to_nhop drop
// 设置发完Host2的数据包直接通过1端口发送出去
table_add ipv4_lpm set_nhop 10.0.1.1/32 => 00:00:00:01:02:00 1
// 设置发完Host1的数据包直接通过2端口发送出去
table_add ipv4_lpm set_nhop 10.0.6.2/32 => 00:00:00:06:02:00 2s3-commands.txt1
2
3
4
5
6
7
8// 交换机S3
// 设置匹配表ipv4_lpm和匹配表ecmp_group_to_nhop的默认动作为丢包
table_set_default ipv4_lpm drop
table_set_default ecmp_group_to_nhop drop
// 设置发完Host2的数据包直接通过1端口发送出去
table_add ipv4_lpm set_nhop 10.0.1.1/32 => 00:00:00:01:03:00 1
// 设置发完Host1的数据包直接通过2端口发送出去
table_add ipv4_lpm set_nhop 10.0.6.2/32 => 00:00:00:06:03:00 2s4-commands.txt1
2
3
4
5
6
7
8// 交换机S4
// 设置匹配表ipv4_lpm和匹配表ecmp_group_to_nhop的默认动作为丢包
table_set_default ipv4_lpm drop
table_set_default ecmp_group_to_nhop drop
// 设置发完Host2的数据包直接通过1端口发送出去
table_add ipv4_lpm set_nhop 10.0.1.1/32 => 00:00:00:01:04:00 1
// 设置发完Host1的数据包直接通过2端口发送出去
table_add ipv4_lpm set_nhop 10.0.6.2/32 => 00:00:00:06:04:00 2s5-commands.txt1
2
3
4
5
6
7
8// 交换机S5
// 设置匹配表ipv4_lpm和匹配表ecmp_group_to_nhop的默认动作为丢包
table_set_default ipv4_lpm drop
table_set_default ecmp_group_to_nhop drop
// 设置发完Host2的数据包直接通过1端口发送出去
table_add ipv4_lpm set_nhop 10.0.1.1/32 => 00:00:00:01:05:00 1
// 设置发完Host1的数据包直接通过2端口发送出去
table_add ipv4_lpm set_nhop 10.0.6.2/32 => 00:00:00:06:05:00 2s6-commands.txt1
2
3
4
5
6
7
8
9
10
11
12
13// 交换机S6
// 设置匹配表ipv4_lpm和匹配表ecmp_group_to_nhop的默认动作为丢包
table_set_default ipv4_lpm drop
table_set_default ecmp_group_to_nhop drop
// 设置发完主机Host2的数据包直接通过1端口发出,不使用ECMP
table_add ipv4_lpm set_nhop 10.0.6.2/32 => 00:00:0a:00:06:02 1
// 设置发完Host1的数据包使用ECMP,ECMP的组号为1、共包含4条可选路径
table_add ipv4_lpm ecmp_group 10.0.1.0/24 => 1 4
// 在ECMP组1中,针对哈希结果0、1、2、3分布设置出端口为2、3、4、5
table_add ecmp_group_to_nhop set_nhop 1 0 => 00:00:00:02:06:00 2
table_add ecmp_group_to_nhop set_nhop 1 1 => 00:00:00:03:06:00 3
table_add ecmp_group_to_nhop set_nhop 1 2 => 00:00:00:04:06:00 4
table_add ecmp_group_to_nhop set_nhop 1 3 => 00:00:00:05:06:00 5network.py1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38from p4utils.mininetlib.network_API import NetworkAPI
net = NetworkAPI()
# Network general options
net.setLogLevel('info')
# Network definition
net.addP4Switch('s1', cli_input='sw-commands/s1-commands.txt')
net.addP4Switch('s2', cli_input='sw-commands/s2-commands.txt')
net.addP4Switch('s3', cli_input='sw-commands/s3-commands.txt')
net.addP4Switch('s4', cli_input='sw-commands/s4-commands.txt')
net.addP4Switch('s5', cli_input='sw-commands/s5-commands.txt')
net.addP4Switch('s6', cli_input='sw-commands/s6-commands.txt')
net.setP4SourceAll('p4src/flowlet_switching.p4')
net.addHost('h1')
net.addHost('h2')
net.addLink("h1", "s1")
net.addLink("h2", "s6")
net.addLink("s1", "s2")
net.addLink("s1", "s3")
net.addLink("s1", "s4")
net.addLink("s1", "s5")
net.addLink("s2", "s6")
net.addLink("s3", "s6")
net.addLink("s4", "s6")
net.addLink("s5", "s6")
# Assignment strategy
net.mixed()
# Nodes general options
net.enablePcapDumpAll()
net.enableLogAll()
net.enableCli()
net.startNetwork()
P4仿真
- 启动网络拓扑
- 使用
pingall命令测试h1,h2是否联通
- 监控4条链路 (从
s1-eth2至s1-eth5)sudo tcpdump -enn -i s1-ethX
- 再次
pingall测试h1,h2之间的连接
- 使用
iperf命令测试h1,h2
6.打开h1终端. 使用send.py文件
1
python send.py 10.0.6.2 1000 <sleep_time_between_packets>
P4 09-Flowlet Switching
http://example.com/2025/08/08/P4 09-Flowlet Switching/