P4 09-Flowlet Switching

P4示例程序-09 Flowlet

  • 功能

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

  • 拓扑结构

拓扑结构
  • 代码

  1. p4app.json
    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
    {
    "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"
    }
    }
    }
    }
  2. 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
    64
    const 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 头
    }
  3. 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
    30
    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; // 若是 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; // 解析完成
    }
    }
  4. Checksum Verification
    1
    2
    3
    4
    control MyVerifyChecksum(inout headers hdr, inout metadata meta) {
    apply {
    }
    }
  5. 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
    89
    control 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 下一跳
    }
    }
    }
    }
    }
  6. Egress Processing
    1
    2
    3
    4
    5
    6
    control MyEgress(inout headers hdr,
    inout metadata meta,
    inout standard_metadata_t standard_metadata) {
    apply {
    }
    }
  7. 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
    24
    control 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
    );
    }
    }

  8. DE parser
    1
    2
    3
    4
    5
    6
    7
    control MyDeparser(packet_out packet, in headers hdr) {
    apply {
    packet.emit(hdr.ethernet); // 输出以太网头
    packet.emit(hdr.ipv4); // 输出 IPv4 头
    packet.emit(hdr.tcp); // 若存在,输出 TCP 头
    }
    }
  9. Switch
    1
    2
    3
    4
    5
    6
    7
    8
    V1Switch(
    MyParser(),
    MyVerifyChecksum(),
    MyIngress(),
    MyEgress(),
    MyComputeChecksum(),
    MyDeparser()
    ) main;
  10. s1-commands.txt
    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
  11. s2-commands.txt
    1
    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 2
  12. s3-commands.txt
    1
    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 2
  13. s4-commands.txt
    1
    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 2
  14. s5-commands.txt
    1
    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 2
  15. s6-commands.txt
    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
  16. network.py
    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/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仿真

  1. 启动网络拓扑
启动网络拓扑
  1. 使用pingall命令测试h1,h2是否联通
测试联通
  1. 监控4条链路 (从s1-eth2s1-eth5) sudo tcpdump -enn -i s1-ethX
监控四条链路
  1. 再次pingall测试h1,h2之间的连接
再次测试pingall
  1. 使用iperf命令测试h1,h2
iperf命令

6.打开h1终端. 使用send.py文件

1
python send.py 10.0.6.2 1000 <sleep_time_between_packets>

h1终端

P4 09-Flowlet Switching
http://example.com/2025/08/08/P4 09-Flowlet Switching/
作者
Wsdbybyd
发布于
2025年8月8日
许可协议