P4 08-ECMP
P4示例程序-08 ECMP
功能
ECMP即等价多路径路由,网络里如果存在多条到达同一目的地且代价相同的路径,就可以把流量分散到这些路径上,实现负载均衡,以提高带宽利用率。在P4交换机的实现中主要涉及哈希函数
拓扑结构
代码
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/ecmp.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
58const bit<16> TYPE_IPV4 = 0x800; // IPv4以太网类型
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; // 源IPv4地址
ip4Addr_t dstAddr; // 目的IPv4地址
}
header tcp_t{
bit<16> srcPort; // 源端口号
bit<16> dstPort; // 目的端口号
bit<32> seqNo; // 序列号
bit<32> ackNo; // 确认号
bit<4> dataOffset; // 数据偏移
bit<4> res; // 保留字段
bit<1> cwr; // CWR位
bit<1> ece; // ECE位
bit<1> urg; // 紧急指针有效
bit<1> ack; // 确认位
bit<1> psh; // 推送标志
bit<1> rst; // 重置连接
bit<1> syn; // 同步序列编号
bit<1> fin; // 终止连接
bit<16> window; // 滑动窗口大小
bit<16> checksum; // TCP校验和
bit<16> urgentPtr; // 紧急指针
}
struct metadata {
bit<14> ecmp_hash; // ECMP哈希值
bit<14> ecmp_group_id; // ECMP组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; // 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
63control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
action drop() {
mark_to_drop(standard_metadata); // 丢弃数据包
}
action ecmp_group(bit<14> ecmp_group_id, bit<16> num_nhops){
hash(meta.ecmp_hash, // 对以下字段进行哈希计算
HashAlgorithm.crc16, // 哈希算法为crc16
(bit<1>)0, // 随机种子
{ hdr.ipv4.srcAddr,
hdr.ipv4.dstAddr,
hdr.tcp.srcPort,
hdr.tcp.dstPort,
hdr.ipv4.protocol},
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地址设为原先的目的地址
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; // ECMP组ID精确匹配
meta.ecmp_hash: exact; // 哈希值精确匹配
}
actions = {
drop; // 丢弃
set_nhop; // 设置下一跳
}
size = 1024; // 表大小为1024
}
table ipv4_lpm {
key = {
hdr.ipv4.dstAddr: lpm; // 目的IP地址最长前缀匹配
}
actions = {
set_nhop; // 设置下一跳
ecmp_group; // 使用ECMP
drop; // 丢弃
}
size = 1024; // 表大小为1024
default_action = drop; // 默认动作为丢弃
}
apply {
if (hdr.ipv4.isValid()){ // 如果IPv4头部有效
switch (ipv4_lpm.apply().action_run){ // 应用ipv4_lpm表并获取运行的action
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
20control MyComputeChecksum(inout headers hdr, inout metadata meta) {
apply {
update_checksum(
hdr.ipv4.isValid(), // 仅在IPv4有效时计算校验和
{ 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); // 使用16位加法算法
}
} - 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 5send.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
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#!/usr/bin/env python3
# 指定解释器为 Python 3
import argparse # 可选模块,用于命令行参数解析(此脚本未使用)
import sys # 用于访问命令行参数
import socket # 用于主机名到 IP 的转换
import random # 用于生成随机端口号
import struct # 用于处理二进制数据(此脚本未使用)
# 从 Scapy 导入必要的函数和协议层
from scapy.all import sendp, get_if_list, get_if_hwaddr
from scapy.all import Ether, IP, UDP, TCP
def get_if():
"""
自动获取名为 eth0 的网络接口名称。
返回值:接口名称字符串,如 'h1-eth0'
"""
ifs = get_if_list() # 获取所有网络接口列表
iface = None # 默认未找到接口
for i in ifs:
if "eth0" in i: # 查找包含 'eth0' 的接口(适用于 Mininet 环境)
iface = i
break
if not iface:
print("Cannot find eth0 interface")
exit(1)
return iface
def main():
"""
主函数:向指定目标地址发送指定数量的随机 TCP 数据包。
"""
if len(sys.argv) < 3:
# 如果命令行参数数量不足,则提示用法并退出
print('pass 2 arguments: <destination> <number_of_random_packets>')
exit(1)
# 获取目标地址(第一个参数)并解析为 IP 地址
addr = socket.gethostbyname(sys.argv[1])
# 获取网卡名称
iface = get_if()
print("sending on interface %s to %s" % (iface, str(addr)))
# 发送指定数量的随机 TCP 包
for _ in range(int(sys.argv[2])):
# 构造以太网帧,目的地址为广播地址 ff:ff:ff:ff:ff:ff
pkt = Ether(src=get_if_hwaddr(iface), dst='ff:ff:ff:ff:ff:ff')
# 添加 IP 层,目标 IP 为用户输入的地址
# 添加 TCP 层,目标端口和源端口随机生成
pkt = pkt / IP(dst=addr) / TCP(
dport=random.randint(5000, 60000), # 随机目的端口
sport=random.randint(49152, 65535) # 随机源端口
)
# 发送数据包(通过指定接口),不显示详细信息
sendp(pkt, iface=iface, verbose=False)
if __name__ == '__main__':
main()network.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/ecmp.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仿真
- 启动网络拓扑
sudo p4run
- 使用
pingall命令测试h1,h2是否联通
- 监控4条链路 (从
s1-eth2至s1-eth5)sudo tcpdump -enn -i s1-ethX
- 再次
pingall测试h1,h2之间的连接
- 在两个主机之间做
iperf由于属于同一流的所有数据包都有相同的5元元组,因此哈希总是返回相同的索引
P4 08-ECMP
http://example.com/2025/08/06/P4 08-ECMP/