Featured image of post P4示例程序-8 ECMP

P4示例程序-8 ECMP

ECMP即等价多路径路由,网络里如果存在多条到达同一目的地且代价相同的路径,就可以把流量分散到这些路径上,实现负载均衡,以提高带宽利用率。在P4交换机的实现中主要涉及哈希函数。

例程拓扑

拓扑描述文件

 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
from 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()

交换机程序

main函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <core.p4>
#include <v1model.p4>

V1Switch(
    MyParser(),
    MyVerifyChecksum(),
    MyIngress(),
    MyEgress(),
    MyComputeChecksum(),
    MyDeparser()
) main;

定义头部

ECMP的哈希基于源IP地址、目的IP地址、IP协议号、传输层源端口号、传输层目的端口号,因此需要解析以太网帧头部、IPv4头部、TCP头部。

 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
const bit<16> TYPE_IPV4 = 0x800;

typedef bit<9>  egressSpec_t;
typedef bit<48> macAddr_t;
typedef bit<32> ip4Addr_t;

// 定义以太网头部字段
header ethernet_t {
    macAddr_t dstAddr;
    macAddr_t srcAddr;
    bit<16>   etherType;
}

// 定义IPv4头部字段
header ipv4_t {
    bit<4>    version;
    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;
    ip4Addr_t dstAddr;
}

// 定义TCP头部字段
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;
    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;
}

// 自定义元数据存储哈希值ecmp_hash、ECMP组号ecmp_group_id
struct metadata {
    bit<14> ecmp_hash;
    bit<14> c;
}

// 实例化数据包头部
struct headers {
    ethernet_t   ethernet;
    ipv4_t       ipv4;
    tcp_t        tcp;
}

数据包解析

 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
parser 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;
            default: accept;
        }
    }

    state parse_ipv4 {
        packet.extract(hdr.ipv4);
        transition select(hdr.ipv4.protocol){
            6 : parse_tcp;
            default: accept;
        }
    }

    state parse_tcp {
        packet.extract(hdr.tcp);
        transition accept;
    }
}

检查校验和

1
2
3
control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
    apply {  }
}

入队列侧处理

总体逻辑思路为:

  1. 先在匹配表ipv4_lpm里查一遍,决定如何转发该数据包:如果是走单路径,直接执行set_nhop动作进行转发;如果是走ECMP,则执行ecmp_group动作,计算出哈希值、确定ECMP组;
  2. 如果是ECMP,则查第二张匹配表ecmp_group_to_nhop,根据ECMP结果执行转发动作。
 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
control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {
    action drop() {
        mark_to_drop(standard_metadata);
    }
    
    // 执行ECMP、并确定转发路径为哪一个ECMP组里的第几条路径
    action ecmp_group(bit<14> ecmp_group_id, bit<16> num_nhops){
        // 基于数据包的五元组进行哈希
        hash(meta.ecmp_hash,      // 输出的哈希值
             HashAlgorithm.crc16, // 哈希算法
             (bit<1>)0,           // 初始值设置为0
             {                    // 哈希的数据
                 hdr.ipv4.srcAddr,
                 hdr.ipv4.dstAddr,
                 hdr.tcp.srcPort,
                 hdr.tcp.dstPort,
                 hdr.ipv4.protocol
             },
             num_nhops   // 取模范围
        );
        // 填写ECMP组号
	    meta.ecmp_group_id = ecmp_group_id;
    }

    // 执行转发动作
    action set_nhop(macAddr_t dstAddr, egressSpec_t port) {
        // 更新数据包的各字段
        hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
        hdr.ethernet.dstAddr = dstAddr;
        hdr.ipv4.ttl = hdr.ipv4.ttl - 1;

        // 设置出端口
        standard_metadata.egress_spec = port;
    }

    // 根据ECMP组号与哈希值确定具体下一跳的出端口
    table ecmp_group_to_nhop {
        key = {
            meta.ecmp_group_id: exact;
            meta.ecmp_hash: exact;
        }
        actions = {
            drop;
            set_nhop;
        }
        size = 1024;
    }
    
    // 根据目的IPv4地址做最长前缀匹配
    table ipv4_lpm {
        key = {
            hdr.ipv4.dstAddr: lpm;
        }
        actions = {
            set_nhop;
            ecmp_group;
            drop;
        }
        size = 1024;
        default_action = drop;
    }

    apply {
        if (hdr.ipv4.isValid()){
            // 先在匹配表ipv4_lpm里查询,决定该数据包是直接转发还是通过ECMP
            switch (ipv4_lpm.apply().action_run){
                // 若匹配的动作为ecmp_group,则根据ECMP结果执行转发
                ecmp_group: {
                    ecmp_group_to_nhop.apply();
                }
            }
        }
    }
}

出队列侧处理

1
2
3
4
5
control MyEgress(inout headers hdr,
                 inout metadata meta,
                 inout standard_metadata_t standard_metadata) {
    apply { }
}

计算校验值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
control MyComputeChecksum(inout headers  hdr, inout metadata meta) {
    apply {
        // 由于修改了IPv4中的字段(包括TTL),因此需要重新计算校验和
        update_checksum(
                hdr.ipv4.isValid(),
                { hdr.ipv4.version,
                hdr.ipv4.ihl,
                hdr.ipv4.diffserv,
                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);
    }
}

封装数据包

1
2
3
4
5
6
7
control MyDeparser(packet_out packet, in headers hdr) {
    apply {
        packet.emit(hdr.ethernet);
        packet.emit(hdr.ipv4);
        packet.emit(hdr.tcp);
    }
}

运行结果

配置匹配表

 1
 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 5
 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
// 交换机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 2

// 交换机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 2

// 交换机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 2

// 交换机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 2
 1
 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 5

运行测试

1
2
sudo p4run
mininet > pingall

Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计