作者简介:陈翔,福州大学数计学院2015级计算机科学与技术(实验班)本科生 ,对软件定义网络SDN,特别是对P4语言感兴趣。
前言
为深入研究P4语言相关规范及运行操作使用,本系列文章根据P4.org网站给出的《The P4 Language Specification v1.0.2》[1]内容,并通过我们的运行使用的具体实例和分析汇总,希望能为大家研究P4提供一点参考。作为大二和大三的本科生,水平和经验有限,感谢SDNLAB提供平台,希望能和大家相互学习交流。
本系列文章分为三个部分,系列一翻译和阐述 P4.org网站给出的《The P4 Language Specification v1.0.2》的第二部分首部及字段,见链接:http://www.sdnlab.com/17955.html;系列二是翻译和阐述《The P4 Language Specification v1.0.2》的第三部分解析器,见链接:http://www.sdnlab.com/18021.html;系列三是基于Github开源项目中的P4项目源码分析。
本篇文章是系列文章的收尾篇,除了对于Tutorial中的名为Flowlet Switching项目的源码分析之外,也贴出了我们安装P4相关软件时的步骤与顺序,希望能够帮助到大家在安装相关项目的时候少遇到一些问题。
一、: P4-Related Tools Installation
本章节是安装P4相关工具的步骤和说明。
需要注意的是,本说明只适用于 Ubuntu 14.04 系统。
1.1 推荐安装的其他工具
- mininet:SDN网络仿真工具
- vim:编辑器
- scapy:Python的一个功能十分强大的库,可以用于生成数据报
- pip:Python包管理工具
1.2 P4Factory
官方README:P4 Model Repository,如果你对P4已经很熟悉并且能够独立搭建环境,这个repo已经不大适合现在的P4开发了(使用bmv1软件交换机),建议是使用bmv2搭建环境并进行相关开发。
1.安装外部引用的库:
1 | git submodule update --init --recursive |
2.安装Ubuntu14.04系统下所需的所有依赖:
1 | ./install_deps.sh |
3.在启动模拟器之前,需要创建虚拟的端口:
1 | sudo p4factory/tools/veth_setup.sh |
4.使用autoconf工具生成Makefile,并对工作环境进行配置:
1 2 3 | cd p4factory ./autogen.sh ./configure |
5.验证安装是否成功,并测试一个简单的P4程序:
1 2 3 | cd p4factory/targets/basic_routing/ make bm sudo ./behavioral-model |
同时新打开一个终端进行测试:
1 2 | cd p4factory/targets/basic_routing/ sudo python run_tests.py --test-dir tests/ptf-tests/ |
1.3 BMv2
官方README:BEHAVIORAL MODEL REPOSITORY
1.Ubuntu 14.04下要求安装的依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | automake cmake libjudy-dev libgmp-dev libpcap-dev libboost-dev libboost-test-dev libboost-program-options-dev libboost-system-dev libboost-filesystem-dev libboost-thread-dev libevent-dev libtool flex bison pkg-config g++ libssl-dev |
2.使用脚本安装外部依赖库,如thrift。
注意:如果已经安装了P4Factory,请忽略此步,否则会有一系列的版本不匹配问题。
1 | ./install_deps.sh |
3.按照以下步骤安装bmv2:
./autogen.sh
./configure
make
sudo make install # if you need to install bmv2
4.更新Linux库缓存:
1 | sudo ldconfig |
5.检验:
1 | sudo make check |
1.4 P4c-bm
官方README:p4c-bm
1.要求安装好pip;
2.安装步骤:
sudo pip install -r requirements.txt
sudo pip install -r requirements_v1_1.txt
sudo python setup.py install
二、 P4 Tutorial Flowlet Switching 源码分析
Exercise 2: Implementing TCP flowlet switching
最近P4社区更新了Tutorial这个Demo的代码,今天偶然发现了它并且觉得很有学习的价值,于是写了一个对源码的简易解读,希望能够帮助大家学习P4。
2.1 源码
simple_router.p4:Github Source Code
可以从 solution.tar.gz 解压获得。
元数据intrinsic.p4:
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;
2.2 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阶段的时间戳。
2.3 主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中讨论了一般的多路径路由。”
2.4 simple_router.p4
2.4.1 一些声明和宏定义
#include "includes/headers.p4"
#include "includes/parser.p4"
#include "includes/intrinsic.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;
}
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地址。
2.4.2 流控程序
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地址
}
2.4.3 Flowlet这里着重对相对来说比较重要的flowlet流表、new_flow流表进行介绍。
这个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,以表明这个数据报是属于一条新的流量。
2.4.4 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社区的动态,跟进最新的P4讯息。此外,SDNLAB有一个非常棒的SDN技能图谱,链接:https://github.com/PONOUBA/opensdn_engineer_skill_map,我们也在Github整合了一个P4资源的仓库,链接:https://github.com/Wasdns/The-Road-to-P4,希望能够和大家多多交流和沟通,为数据平面可编程和P4的发展做出努力!