Featured image of post P4示例程序-9 Flowlet

P4示例程序-9 Flowlet

P4示例程序-8中的ECMP根据哈希结果将流进行负载均衡,然而会导致象流哈希到相同的路径,本示例程序在ECMP的P4交换机程序之上进行改进、实现Flowlet。

例程拓扑

当网络中有多条到目的地的等成本路径时,ECMP把流哈希到不同路径。如果几个象流恰好哈希到同一条路径,就会造成该链路拥塞,而其他链路闲置,非常影响性能。Flowlet技术将流拆分为多个体积更小的Flowlet,并将Flowlet哈希到不同的路径,通过细粒度的哈希实现更好的负载均衡。具体而言:如果检测到同一个流的连续两个数据包之间有比较大的空隙,就把接下来的这段重新哈希到另一条路径。

拓扑与“P4示例程序-8 ECMP”中的拓扑一致。

拓扑描述文件

与“P4示例程序-8 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
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;

定义头部

 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
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;
}

// 自定义元数据
struct metadata {
    // 存储哈希值ecmp_hash、ECMP组号ecmp_group_id
    bit<14> ecmp_hash;
    bit<14> c;
    
    // 存储上一个数据包到达的时间戳,以及距离上一个数据包的时间间隔
    bit<48> flowlet_last_stamp;
    bit<48> flowlet_time_diff;

    // flowlet_last_stamp存储在register数组里
    // 需要索引来确定在数组的哪个位置进行读写
    bit<13> flowlet_register_index;
    // 当前数据包所属的Flowlet编号
    bit<16> flowlet_id;
}

// 实例化数据包头部
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 { }
}

入队列侧处理

在“P4示例程序-8 ECMP”中的代码上进行修改:

  1. 仍然对每个流进行哈希,但增加了Flowlet编号参与哈希;
  2. 每当检测到包和包之间有比较大的空隙,就会更新Flowlet编号,因此会导致哈希结果发生变化。
  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
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#define REGISTER_SIZE 8192
#define TIMESTAMP_WIDTH 48
#define ID_WIDTH 16
// 以下定义等效于bit<48> FLOWLET_TIMEOUT = 200000;
// 48w表示这是一个宽度为48bit的数值,200000是其数值
#define FLOWLET_TIMEOUT 48w200000

control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {
    // 使用寄存器记录每个流的当前Flowlet编号
    register<bit<ID_WIDTH>>(REGISTER_SIZE) flowlet_to_id;
    // 使用寄存器记录每个流的上一次接收到数据包的时间
    register<bit<TIMESTAMP_WIDTH>>(REGISTER_SIZE) flowlet_time_stamp;

    action drop() {
        mark_to_drop(standard_metadata);
    }

    // 读取并更新寄存器中存储的流信息
    action read_flowlet_registers(){
        // 通过对流的五元组进行哈希获得索引,该索引即为该流在寄存器中的索引位置
        hash(meta.flowlet_register_index, HashAlgorithm.crc16,
            (bit<16>)0,
            { hdr.ipv4.srcAddr, hdr.ipv4.dstAddr, hdr.tcp.srcPort, hdr.tcp.dstPort,hdr.ipv4.protocol},
            (bit<14>)REGISTER_SIZE);

        // 根据索引值在寄存器flowlet_last_stamp中读取上一次接收到数据包的时间
        // 并存入元数据的flowlet_last_stamp中
        flowlet_time_stamp.read(meta.flowlet_last_stamp, (bit<32>)meta.flowlet_register_index);

        // 根据索引值在寄存器flowlet_to_id中读取流的当前Flowlet编号
        // 并存入元数据的flowlet_id中
        flowlet_to_id.read(meta.flowlet_id, (bit<32>)meta.flowlet_register_index);

        // 更新当前时间戳为当前数据包的到达时间
        // 到达时间可在标准元数据中获取,即ingress_global_timestamp
        // 根据索引值在寄存器flowlet_time_stamp中写入时间戳
        flowlet_time_stamp.write((bit<32>)meta.flowlet_register_index, standard_metadata.ingress_global_timestamp);
    }

    // 采用随机数更新Flowlet编号
    action update_flowlet_id(){
        // 定义一个32位的临时变量,用来存储接下来生成的随机数
        bit<32> random_t;
        // 调用随机数函数生成一个范围在0~65000之间的随机数
        random(random_t, (bit<32>)0, (bit<32>)65000);
        // 更新Flowlet编号
        meta.flowlet_id = (bit<16>)random_t;
        // 根据索引值在寄存器flowlet_to_id中写入新的Flowlet编号
        flowlet_to_id.write((bit<32>)meta.flowlet_register_index, (bit<16>)meta.flowlet_id);
    }

    action ecmp_group(bit<14> ecmp_group_id, bit<16> num_nhops){
        hash(meta.ecmp_hash,
             HashAlgorithm.crc16,
             (bit<1>)0,
             {
                 hdr.ipv4.srcAddr,
                 hdr.ipv4.dstAddr,
                 hdr.tcp.srcPort,
                 hdr.tcp.dstPort,
                 hdr.ipv4.protocol,
                 meta.flowlet_id    // 在哈希选路时增加Flowlet编号
             },
             num_nhops
        );
	    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;
    }

    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;
        }
        actions = {
            set_nhop;
            ecmp_group;
            drop;
        }
        size = 1024;
        default_action = drop;
    }

    apply {
        if (hdr.ipv4.isValid()){
            // 每个数据包在多流水线上经过时,实际上是并行在不同阶段执行的
            // 如果两个包几乎同时到达,可能同时触发读写寄存器,造成逻辑错误
            // @atomic表示其中的逻辑是原子操作,不能被其它操作打断
            @atomic {
                // 从寄存器中获取流的相关信息
                read_flowlet_registers();
                // 计算当前数据包距离上一个数据包到达的时间间隔
                meta.flowlet_time_diff = standard_metadata.ingress_global_timestamp - meta.flowlet_last_stamp;
                // 如果当前时间间隔超过了阈值,则可以认为是一个新的Flowlet
                if (meta.flowlet_time_diff > FLOWLET_TIMEOUT){
                    update_flowlet_id();
                }
            }

            switch (ipv4_lpm.apply().action_run){
                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);
    }
}

运行结果

配置匹配表

与“P4示例程序-8 ECMP”中的匹配表配置一致。

 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

补充:Thrift读写寄存器

使用Thrift客户端CLI连接到P4交换机:

1
simple_switch_CLI --thrift-port 9090

寄存器的读取使用register_read命令:

1
2
3
4
5
6
# 使用help命令查看register_read的用法
help register_read
# register_read <寄存器的名称> <下标>:输出索引寄存器数组下标位置的值
register_read flowlet_to_id 0
# register_read <寄存器的名称>:输出完整的寄存器数组
register_read flowlet_to_id

寄存器的写入使用register_read命令:

1
2
3
4
# 使用help命令查看register_write的用法
help register_write
# register_read <寄存器的名称> <下标> <写入新值>:在寄存器数组下标位置写入新值
register_write flowlet_to_id 0 100

寄存器的重置使用register_reset命令:

1
2
3
4
# 使用help命令查看register_rest的用法
help register_rest
# register_rest <寄存器的名称>:将寄存器数组重置
register_rest flowlet_to_id

在Python脚本中同样可以通过Thrift对寄存器进行操作:

1
2
3
4
# 寄存器数组重置
controller.register_reset("flowlet_to_id")
# 寄存器数组读取
controller.register_read("flowlet_to_id")
Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计