Github链接:
Exercise 2: Implementing TCP flowlet switching
P4 Tutorials Flowlet Switching
最近P4社区更新了Tutorial这个Demo的代码,今天偶然发现了它并且觉得很有学习的价值,于是简单的写一个对源码的解读。
源码
simple_router.p4:Github Source Code
元数据intrinsic.p4:
/* 这里省略了Barefoot的License声明 */
header_type intrinsic_metadata_t {
fields {
ingress_global_timestamp : 48;
lf_field_list : 32;
mcast_grp : 16;
egress_rid : 16;
}
}
metadata intrinsic_metadata_t intrinsic_metadata;
intrinsic.p4
这个文件是对固有元数据intrinsic_metadata的声明,在P414标准中有对该元数据进行相关解释:
Metadata is state associated with each packet...some metadata has special significance to the operation of the switch. This is called Intrinsic Metadata as it has semantics intrinsic to the operation of the machine.
也就是说,Intrinsic Metadata是用来存数据报中某些特殊状态的元数据,本实例使用的是该元数据中的第一个字段ingress_global_timestamp来表示和记录ingress阶段的时间戳。
主P4程序做了什么?
在对P4程序进行解读之前,先对整个P4干嘛的做个介绍。
这些P4程序是用来做Flowlet Switching的,什么是Flowlet Switching呢?
Flowlet switching leverages the burstiness of TCP flows to achieve better load balancing of TCP traffic. In this exercise, you will start from a program that load-balances based on layer 4 flows: this is generally considered "classic" ECMP. To do this, we compute a hash over the 5-tuple and use this value to choose from a set of possible next hops. This means that all packets belonging to the same flow (i.e. with the same 5-tuple) will be routed to the same nexthop.
简单的来说,Flowlet Switching是用来对TCP突发流量做一个负载均衡的工具,通过ECMP做一个四层的LB。做法是通过算法计算数据报五元组的hash值,然后用这个hash值选择下一跳。意味着同一个流的所有数据报都会被转发到同样的下一跳路由。
那么这个Demo是怎么实现的呢?
1.用modify_field_with_hash_based_offset()
元动作和crc16算法计算五元组的hash值。这个hash值用来区别不同的流量。注意这里不考虑hash碰撞。
2.对于每条流,需要存两个状态:a)记录这条流上一个数据报的时间戳 b)flowlet_id,来区别不同的TCP流量;对于数据流中的每一个数据报都更新它的时间戳,如果上一个数据报的时间戳与当前数据报的时间戳超过了一定阈值(50ms,但是对于数据中心中的高突发性的快速流量而言,这个阈值需要设置的非常小),那么就给flowlet_id+1,以区别不同的TCP流量;同样的一条流中的数据报拥有相同的id,不同的流量之间的id是不一样的。不过这样就意味着我们需要在数据报的处理过程中维持必要的状态信息,可以通过P4程序中的寄存器来实现。时间戳则由P4软件交换机进行计算,并存储在字段intrinsic_metadata.ingress_global_timestamp
中,该字段宽度为32bit,以亚微秒为单位,且只读。
3.有这个flowlet_id之后,即可进行ecmp的hash计算了,用来选择下一跳,在P4的Tutorial中有关于ECMP的实现,参考repo:Action Profile
对于ECMP的介绍:ECMP Wikipedia
"等价多路径路由(英文:Equal-cost multi-path routing,缩写 ECMP),是一个在next-hop封包传送到一个单一目的所产生在多个最佳路径并列时的首要路由权重计算路由策略。多路径路由能被应用于首要路由协定同时发生的状况,因为它是一个受限于单一路由的per-hop决策,它有可能借由在多路径负载平衡流量下提供大幅增加的带宽,然而,它可能在实际部署时发生重大问题。在RFC2991中讨论了一般的多路径路由。"
simple_router.p4
一些声明和宏定义
#define FLOWLET_MAP_BITS 13
#define FLOWLET_MAP_SIZE 8192 // 2^13
#define FLOWLET_INACTIVE_TOUT 50000 // usec -> 50ms
header_type ingress_metadata_t {
fields {
flow_ipg : 48; // inter-packet gap
flowlet_map_index : FLOWLET_MAP_BITS; // flowlet map index
flowlet_id : 16; // flowlet id
flowlet_lasttime : 48; // flowlet's last reference time
ecmp_offset : 14; // offset into the ecmp table
nhop_ipv4 : 32;
}
}
metadata ingress_metadata_t ingress_metadata;
action _drop() {
drop();
}
field_list l3_hash_fields {
ipv4.srcAddr;
ipv4.dstAddr;
ipv4.protocol;
tcp.srcPort;
tcp.dstPort;
}
field_list_calculation flowlet_map_hash {
input {
l3_hash_fields;
}
algorithm : crc16;
output_width : FLOWLET_MAP_BITS;
}
/* registers */
register flowlet_lasttime {
width : 48;
instance_count : 8192;
}
register flowlet_id {
width : 16;
instance_count : 8192;
}
声明的内容包括:
-
一个名为ingress_metadata的元数据,存储状态信息。
-
一个hash字段列表和hash计算器flowlet_map_hash,用来计算flowlet map index,这个偏移位用于确定寄存器实例,查找相应的数据报状态。
-
两个寄存器,flowlet_lasttime用来记录timestamp,flowlet_id记录id。
名为ingress_metadata的元数据保存有以下信息:
-
flow_ipg: In computer networking, a minimal pause may be required between network packets or network frames. Wikipedia IPG
-
flowlet_map_index: flowlet map index, 映射至寄存器的偏移量。
-
flowlet_id: 区别不同流量的flowlet id。
-
flowlet_lasttime: 上一次使用flowlet的时间,用于记录时间戳。
-
ecmp_offset: ecmp的偏移量。
-
nhop_ipv4: 下一跳的ipv4地址。
流控程序
control ingress {
apply(flowlet); // flowlet
if (ingress_metadata.flow_ipg > FLOWLET_INACTIVE_TOUT) { // 时间戳超过阈值
apply(new_flowlet); // flowlet id+1
}
apply(ecmp_group); // ECMP等价路由
apply(ecmp_nhop);
apply(forward); // 三层路由
}
control egress {
apply(send_frame); //用于改写帧头的MAC地址
}
这里着重对相对来说比较重要的flowlet流表、new_flow流表进行介绍。
Flowlet
这个P4程序中最主要的部分是Flowlet流表:
table flowlet {
actions { lookup_flowlet_map; }
}
只有一个动作,跑demo的时候需要通过运行时命令将其设置为默认动作。lookup_flowlet_map动作是该流表的主体:
action lookup_flowlet_map() {
// l3 hash => flowlet map index
modify_field_with_hash_based_offset(ingress_metadata.flowlet_map_index, 0,
flowlet_map_hash, FLOWLET_MAP_SIZE);
// flowlet map index + flowlet_id register => flowlet_id
register_read(ingress_metadata.flowlet_id,
flowlet_id, ingress_metadata.flowlet_map_index);
// ingress_global_timestamp => flow_ipg
modify_field(ingress_metadata.flow_ipg,
intrinsic_metadata.ingress_global_timestamp);
// flowlet map index + flowlet_lasttime register => flowlet lasttime
register_read(ingress_metadata.flowlet_lasttime,
flowlet_lasttime, ingress_metadata.flowlet_map_index);
// update IPG: flow ipg = flow ipg - flowlet lasttime
subtract_from_field(ingress_metadata.flow_ipg,
ingress_metadata.flowlet_lasttime);
// write flowlet lasttime into register cell flowlet_lasttime[flowlet_map_index]
register_write(flowlet_lasttime, ingress_metadata.flowlet_map_index,
intrinsic_metadata.ingress_global_timestamp);
}
步骤一:首先通过l3 hash(flowlet_map_hash)计算出当前数据报flowlet的偏移量flowlet_map_index并记录至元数据中,以确定两个寄存器实例flowlet_lasttime[flowlet_map_index]
(存放当前数据报的时间戳)和flowlet_id[flowlet_map_index]
(存放当前数据报的flowlet id)以存储数据报状态信息:
// l3 hash => flowlet map index
modify_field_with_hash_based_offset(ingress_metadata.flowlet_map_index, 0,
flowlet_map_hash, FLOWLET_MAP_SIZE);
步骤二:通过步骤一所述偏移量访问寄存器flowlet_id实例,获取当前数据报的flowlet id,并存放至元数据中:
// flowlet map index + flowlet_id register => flowlet_id
register_read(ingress_metadata.flowlet_id,
flowlet_id, ingress_metadata.flowlet_map_index);
步骤三:将bmv2交换机计算得到的ingress处理时间记录至数据报对应的寄存器flowlet_lasttime实例中:
// ingress_global_timestamp => flow_ipg
modify_field(ingress_metadata.flow_ipg,
intrinsic_metadata.ingress_global_timestamp);
步骤四:通过步骤一所述偏移量访问寄存器flowlet_lasttime实例,获取当前数据报的时间戳(通过步骤三得到),并记录至元数据中:
// flowlet map index + flowlet_lasttime register => flowlet lasttime
register_read(ingress_metadata.flowlet_lasttime,
flowlet_lasttime, ingress_metadata.flowlet_map_index);
步骤五:更新 flowlet IPG,用于后续流程判断是否超过阈值:
// update IPG: flow ipg = flow ipg - flowlet lasttime
subtract_from_field(ingress_metadata.flow_ipg,
ingress_metadata.flowlet_lasttime);
在Flowlet流表处理完数据报之后,主控程序对数据报进行一个逻辑判断:
if (ingress_metadata.flow_ipg > FLOWLET_INACTIVE_TOUT) { // 时间戳超过阈值
apply(new_flowlet); // flowlet id+1
}
对数据报的时间戳进行判断,如果元数据(存放数据报状态信息,包括在执行存放的lookup_flowlet_map动作时获取的状态信息)中的记录的flowlet IPG值(存放在ingress_metadata.flow_ipg)大于我们设定的阈值(本实验是50ms),就通过new_flowlet流表更新该数据报的flowlet id:
action update_flowlet_id() {
add_to_field(ingress_metadata.flowlet_id, 1);
register_write(flowlet_id, ingress_metadata.flowlet_map_index,
ingress_metadata.flowlet_id);
}
table new_flowlet {
actions { update_flowlet_id; }
}
可以看到,new_flowlet流表只有一个动作update_flowlet_id,该动作更新了这个数据报的flowlet_id,以表明这个数据报是属于一条新的流量。
ECMP
在接下来的处理流程中,我们就可以通过flowlet id来区别不同的流量,做ECMP等价路由。P4程序片段如下:
action set_nhop(nhop_ipv4, port) {
modify_field(ingress_metadata.nhop_ipv4, nhop_ipv4);
modify_field(standard_metadata.egress_spec, port);
add_to_field(ipv4.ttl, -1);
}
···
field_list flowlet_l3_hash_fields { // 用于ECMP计算的字段列表
ipv4.srcAddr;
ipv4.dstAddr;
ipv4.protocol;
tcp.srcPort;
tcp.dstPort;
ingress_metadata.flowlet_id;
}
#define ECMP_BIT_WIDTH 10 // ECMP Hash Value 的位宽
#define ECMP_GROUP_TABLE_SIZE 1024 // ECMP ecmp_group流表的大小
#define ECMP_NHOP_TABLE_SIZE 16384 // ECMP ecmp_nhop流表的大小
field_list_calculation flowlet_ecmp_hash { // ECMP hash
input {
flowlet_l3_hash_fields;
}
algorithm : crc16;
output_width : ECMP_BIT_WIDTH;
}
action set_ecmp_select(ecmp_base, ecmp_count) { // 将 计算得到的 ECMP hash 值记录到元数据中
modify_field_with_hash_based_offset(ingress_metadata.ecmp_offset, ecmp_base,
flowlet_ecmp_hash, ecmp_count);
}
table ecmp_group {
reads {
ipv4.dstAddr : lpm;
}
actions {
_drop;
set_ecmp_select;
}
size : ECMP_GROUP_TABLE_SIZE;
}
table ecmp_nhop {
reads {
ingress_metadata.ecmp_offset : exact;
}
actions {
_drop;
set_nhop;
}
size : ECMP_NHOP_TABLE_SIZE;
}
步骤和思路很清晰:
步骤1.对数据报的ipv4目的地址进行lpm匹配(最长前缀匹配),若匹配,则进行步骤2;
步骤2.根据匹配的表项执行丢包动作,或执行set_ecmp_select动作(转至步骤3);
步骤3.执行set_ecmp_select动作:根据flowlet id和相关数据报信息(源目IP等)计算得到ECMP hash值,存至元数据中;
步骤4.流表ecmp_nhop根据元数据中存储的ecmp hash值(ingress_metadata.ecmp_offset)进行下一跳路由的选择。
结语
本文对P4 Tutorials的Flowlet Demo中的P4源码进行了相关分析,希望能够为P4开发者们提供些许的帮助:)。
2017.4.11