一直眼馋三层交换机,而且自己无论使用RouterOS还是OpenWrt配置三层路由进行跨VLAN通信都一直搞不起来,在路由内部每个VLAN都可以访问,然而外部进来的流量就不行。在搜索解决方案的时候无意间发现了OpenvSwitch这个SDN项目,该项目目前主要用在数据中心等地方,完整的方案叫OpenStack,而且囊括进三层之后还有个OpenvNetwork这个项目。经过测试后发现OpenvSwitch完全可以做到我需要的三层功能,而且还便宜不需要其他硬件支持跑在虚拟机上即可。

OpenvSwitch简称OVS,根据数据包不同的转发路径分为普通用户空间转发、内核数据路径转发(kernel datapath)、DPDK驱动转发三种,速度也是由低到高排序的。第一种数据路径的OVS安装较为简单,直接使用系统自带包管理器安装即可,具体参考官方文档。剩下两种里基本都是涉及到内核与驱动的,所以自己编译OVS在所难免。

这里记录下OVS-DPDK的编译流程。我所使用的编译环境是centos-8.5,最小化安装,因为有关内核驱动所以以后的机器也都是这么配置。宿主机是ESXI-6.7。

  • update@2022/4/21:
    • 基于Ubuntu编译ovs-dpdk,ubuntu部分都是基于vmware workstation 16。
    • 基于dpdk-pktgen与dpdk-testpmd发包测试
    • bug fix: ovs bridge添加dpdk网卡参数

编译DPDK

DPDK编译其实还是相对较简单的,官方文档跟着做就好了。看RPM repo里有DPDK和DPDK-devel的包,但是DPDK官方没有这些包的编译说明,所以理想的方式包安装因为个人能力问题只好放弃。

环境配置

Centos 8

升级内核版本到5.4。

1
2
3
dnf -y groupinstall "Development Tools"
dnf -y install python3 wget numactl-devel
pip3 install meson ninja pyelftools

Ubuntu 20.04

vmware虚拟机分配4核4G内存。

1
2
apt-get install build-essential libnuma-dev python3-pyelftools pkg-config libjansson-dev libpcap-dev libelf-dev libssl-dev
pip3 install meson ninja

获取源码

1
2
3
git clone https://dpdk.org/git/dpdk-stable
cd dpdk-stable
git checkout origin/20.11

dpdk与ovs是有版本匹配需求的,详细的在ovs的faq里有说(Q: What DPDK version does each Open vSwitch release work with?),一般两边都用稳定版最好。

编译、安装

1
2
3
4
meson build -Denable_kmods=true
ninja -C build
sudo ninja -C build install
sudo ldconfig

具体说明看官方文档,但是一般情况下默认的就足够了。如果想要一起编译所有example,就在meson build上加上-Dexamples=all选项。

测试

centos默认没有这个环境变量,所以pkg-config找不到配置,这里手动添加下:

1
export PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig

而Ubuntu安装好了pkg-config就行了。执行pkg-config --modversion libdpdk查看是否正常链接到:

image-20220101225515886

ok~一切正常,查看下本机网卡:

image-20220101231852652

驱动类型

dpdk支持的驱动大体上有UIO和VFIO两种,主要区别是UIO不支持DMA与中断等底层特性,所以改进而来VFIO。VFIO完全体需要IOMMU的支持,没有IOMMU的话,要使用VFIO就得进行如下配置:

1
echo 1 > /sys/module/vfio/parameters/enable_unsafe_noiommu_mode

这种VFIO驱动不能将各个用户空间完全隔离,所以存在安全问题,并且如果开启了IOMMU时这种VFIO驱动是不能启用的,一般推荐IOMMU下的VFIO驱动。

具体可以看看别人的说明,我了解的也不多就不逼逼了:

systemD管理DPDK网卡绑定

目标就是开机自动绑定dpdk驱动,绑定脚本dpdk-init:

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
#!/usr/bin/env bash

set -e

NIC_PCI_ADDRS=("0000:0b:00.0" "0000:13:00.0")
DRIVER="vfio-pci"

DPDK_DEVBIND="/usr/local/bin/dpdk-devbind.py"

ACTION=0
DRY_RUN=${DRY_RUN:-}
while [ $# -gt 0 ]; do
case "$1" in
bind)
ACTION=0
;;
unbind)
ACTION=1
;;
--*)
echo "Illegal option $1"
;;
esac
shift $(( $# > 0 ? 1 : 0 ))
done

dpdk_bind() {
$DPDK_DEVBIND -b "$1" "$2"
}

dpdk_unbind() {
$DPDK_DEVBIND -u "$1"
}

dpdk_bind_all() {
for pci_addr in "${NIC_PCI_ADDRS[@]}"
do
dpdk_bind "$DRIVER" "$pci_addr"
done
}

dpdk_unbind_all() {
for pci_addr in "${NIC_PCI_ADDRS[@]}"
do
dpdk_unbind "$pci_addr"
done
}

case "$ACTION" in
0)
dpdk_bind_all
;;
1)
dpdk_unbind_all
;;
*)
echo "Illegal option $ACTION"
;;
esac

exit 0

把绑定脚本放在某个目录即可,我这里就和OVS放在一起了,即/usr/local/share/openvswitch/scripts路径。

  • dpdk.service:
    参考了apt方式安装的dpdk,用我自己的dpdk-init替换了官方的(太复杂。。。)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [Unit]
    Description=DPDK runtime environment
    Documentation=https://dpdk.org/doc/guides/index.html
    DefaultDependencies=false
    After=network-pre.target local-fs.target

    [Service]
    Type=oneshot
    RemainAfterExit=yes
    ExecStart=/usr/local/share/openvswitch/scripts/dpdk-init bind

    [Install]
    WantedBy=multi-user.target

编译dpdk-pktgen

pktgen是一个流量生成器,有基于linux内核协议栈的pktgen和基于dpdk的pktgen,基于协议栈的可以直接用不再赘述,基于dpdk的需要确保dpdk安装正确。

1
2
3
git clone http://dpdk.org/git/apps/pktgen-dpdk
cd pktgen-dpdk
meson build -Dwerror=false

如果想将pktgen安装到bin里面去,就到build目录下执行sudo meson install,否则编译好的二进制文件就在build/app下。-Dwerror=false得加,不然有几个warning会过不去。。。

还发现一个百度出的测试工具,没用过就贴一下dperf

编译OVS

基本跟着官方文档走就可以了。

环境配置

Centos 8

1
dnf -y install libcap-ng-devel openssl-devel checkpolicy desktop-file-utils python3-devel selinux-policy-devel unbound unbound-devel

下面两个是文档生成工具,可以不安装

1
dnf -y python3-sphinx groff

Ubuntu 20.04

1
apt-get install libcap-ng-dev openssl unbound libunbound-dev selinux-policy-dev python3-dev libtool checkpolicy

获取源码

1
2
3
git clone https://github.com/openvswitch/ovs.git
cd ovs
git checkout origin/branch-2.17

编译、安装

1
2
3
4
5
./boot.sh
./configure --with-dpdk=yes
make
sudo make install
sudo make modules_install

这个文档插个眼,说可以编译OVS的RPM包,测试不支持DPDK的OVS包可以成功,支持DPDK的因为少了dpdk-devel这个包会失败。

打包(optional)

debian系参考Debian Packaging for Open vSwitch

  1. 安装依赖

    1
    sudo apt-get install fakeroot graphviz debhelper dh-python dh-autoreconf python3-all python3-sphinx libunwind-dev
  2. 检查依赖是否安装完成,在ovs源码根目录运行,无输出即没问题

    1
    dpkg-checkbuilddeps
  3. 没有找到直接支持DPDK的参数,但是可以在debian/rules文件里添加

    1
    EXTRA_CONFIGURE_OPTS += --with-dpdk=yes

    即给configure传一个参数过去

  4. 打包

    1
    DEB_BUILD_OPTIONS='parallel=4' fakeroot debian/rules binary
  5. 清理

    1
    fakeroot debian/rules clean

生成的deb包与源码目录在同一层级。

测试

测试系统里就有ovs相关命令了,需要对编译结果测试的看文档

image-20220101230115217

但是此时由于hugepages等设置,DPDK是没有开启的。

systemD管理OVS

默认编译安装的OVS脚本都在/usr/local/share/openvswitch/scripts目录下,其他目录的话统一替换即可。

对比用apt方式安装的OVS,编译安装的还差ovs-systemd-reload脚本,把这个放在/usr/local/share/openvswitch/scripts里。

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
#! /bin/sh

# Copyright (c) 2017 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

case $0 in
*/*) dir0=`echo "$0" | sed 's,/[^/]*$,,'` ;;
*) dir0=./ ;;
esac
. "$dir0/ovs-lib" || exit 1

stop_ovsdb() {
systemctl --job-mode=ignore-dependencies stop ovsdb-server
}

start_ovsdb() {
systemctl --job-mode=ignore-dependencies start ovsdb-server
}

stop_forwarding() {
systemctl --job-mode=ignore-dependencies stop ovs-vswitchd
}

start_forwarding() {
systemctl --job-mode=ignore-dependencies start ovs-vswitchd
}

add_managers() {
:
}

if [ "$1" = "force-reload-kmod" ]; then
force_reload_kmod
else
restart
fi

exit 0

剩下的4个service直接给出,基本不用修改。

  • openvswitch-switch.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    [Unit]
    Description=Open vSwitch
    Before=network.target
    After=network-pre.target ovsdb-server.service ovs-vswitchd.service
    PartOf=network.target
    Requires=ovsdb-server.service
    Requires=ovs-vswitchd.service

    [Service]
    Type=oneshot
    ExecStart=/bin/true
    ExecReload=/usr/local/share/openvswitch/scripts/openvswitch/ovs-systemd-reload
    ExecStop=/bin/true
    RemainAfterExit=yes

    [Install]
    WantedBy=multi-user.target
    Also=ovs-record-hostname.service
  • ovs-vswitchd.service

    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
    [Unit]
    Description=Open vSwitch Forwarding Unit
    After=ovsdb-server.service network-pre.target systemd-udev-settle.service
    Before=network.target networking.service
    Requires=ovsdb-server.service
    ReloadPropagatedFrom=ovsdb-server.service
    AssertPathIsReadWrite=/var/run/openvswitch/db.sock
    PartOf=openvswitch-switch.service
    DefaultDependencies=no

    [Service]
    LimitNOFILE=1048576
    Type=forking
    Restart=on-failure
    Environment=HOME=/var/run/openvswitch
    EnvironmentFile=-/etc/default/openvswitch-switch
    ExecStart=/usr/local/share/openvswitch/scripts/ovs-ctl \
    --no-ovsdb-server --no-monitor --system-id=random \
    --no-record-hostname \
    start $OVS_CTL_OPTS
    ExecStop=/usr/local/share/openvswitch/scripts/ovs-ctl --no-ovsdb-server stop
    ExecReload=/usr/local/share/openvswitch/scripts/ovs-ctl --no-ovsdb-server \
    --no-monitor --system-id=random \
    --no-record-hostname \
    restart $OVS_CTL_OPTS
    TimeoutSec=300
  • ovsdb-server.service
    这里如果没有dpdk的话,删除dpdk.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    [Unit]
    Description=Open vSwitch Database Unit
    After=syslog.target network-pre.target dpdk.service local-fs.target
    Before=network.target networking.service
    PartOf=openvswitch-switch.service
    DefaultDependencies=no

    [Service]
    LimitNOFILE=1048576
    Type=forking
    Restart=on-failure
    EnvironmentFile=-/etc/default/openvswitch-switch
    ExecStart=/usr/local/share/openvswitch/scripts/ovs-ctl \
    --no-ovs-vswitchd --no-monitor --system-id=random \
    --no-record-hostname \
    start $OVS_CTL_OPTS
    ExecStop=/usr/local/share/openvswitch/scripts/ovs-ctl --no-ovs-vswitchd stop
    ExecReload=/usr/local/share/openvswitch/scripts/ovs-ctl --no-ovs-vswitchd \
    --no-record-hostname \
    --no-monitor restart $OVS_CTL_OPTS
    RuntimeDirectory=openvswitch
    RuntimeDirectoryMode=0755
  • ovs-record-hostname.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    [Unit]
    Description=Open vSwitch Record Hostname
    After=ovsdb-server.service ovs-vswitchd.service network-online.target
    Requires=ovsdb-server.service
    Requires=ovs-vswitchd.service
    Requires=network-online.target
    AssertPathIsReadWrite=/var/run/openvswitch/db.sock

    [Service]
    Type=oneshot
    ExecStart=/usr/local/share/openvswitch/scripts/ovs-ctl record-hostname-if-not-set
    ExecStop=/bin/true
    ExecReload=/usr/local/share/openvswitch/scripts/ovs-ctl record-hostname-if-not-set
    TimeoutSec=300
    RemainAfterExit=yes

    [Install]
    RequiredBy=openvswitch-switch.service

把上头四个文件放在/lib/systemd/system/目录下即可,使能服务

1
2
sudo systemctl enable openvswitch-switch.service
sudo systemctl enable ovs-record-hostname.service

启动

1
2
sudo systemctl start openvswitch-switch.service
sudo systemctl start ovs-record-hostname.service

平台配置

DPDK需要硬件虚拟化的支持和hugepages内存的支持,且至少需要双核的cpu。

虚拟机虚拟化及IOMMU配置

ESXi

cpu虚拟化种开启硬件辅助,并分配两个及以上核心,内存直接全部预留给虚拟机。网卡选择为vmxnet3。

image-20220101230518887

VMware Workstation

类似ESXi,如图打开。

Snipaste_2022-04-21_11-30-15

Workstation不支持直接选择网卡型号,所以修改虚拟机目录下的*.vmx文件,修改对应网卡类型如下图所示。

Snipaste_2022-04-21_11-49-57

linux内核开启虚拟化支持

开启虚拟化支持需要添加两个内核启动参数,即iommu=ptintel_iommu=on,不同系统添加方式不同

修改内核参数后需要重启才能生效。

Centos 8

1
2
3
sudo grubby --update-kernel=ALL --args="iommu=pt"
sudo grubby --update-kernel=ALL --args="intel_iommu=on"
sudo reboot

Ubuntu 20.04

修改/etc/default/grub文件,在GRUB_CMDLINE_LINUX参数里添加intel_iommu=on iommu=pt这两个,每个之间用空格分隔,最后更新参数并重启。

1
sudo update-grub && sudo reboot

开启hugepages

默认2M的hugepage,若有自定义设置自行调整,经过测试无需在/etc/fstab里手动挂载,只要添加内核启动参数即可:

  • hugepagesz: 每个大页的大小,默认2M
  • hugepages: 大页的数量

所以总的大小为hugepagesz * hugepages,我这里就是2G的大页。

Centos 8

1
2
3
4
5
sudo grubby --update-kernel=ALL --args="transparent_hugepage=never"
sudo grubby --update-kernel=ALL --args="default_hugepages=2G"
sudo grubby --update-kernel=ALL --args="hugepagesz=2M"
sudo grubby --update-kernel=ALL --args="hugepages=1024"
sudo reboot

Ubuntu 20.04

类似linux内核开启虚拟化支持->Ubuntu 20.04做法,在GRUB_CMDLINE_LINUX参数里添加transparent_hugepage=never default_hugepages=2G hugepagesz=2M hugepages=1024。更新后重启系统。

验证大页内存与IOMMU

Hugepages

1
grep Huge /proc/meminfo
image-20220421113606555

IOMMU

1
dmesg | grep -i dmar
image-20220421113918025

可以看到有“IOMMU enabled”的信息,这说明此运行时系统打开了IOMMU。

网卡绑定DPDK驱动

1
2
modprobe vfio-pci
dpdk-devbind.py -b vfio-pci 0000:0b:00.0
image-20220102230935479

测试

单机双网卡回环-pktgen收发

结构如图所示,dpdk-port0与dpdk-port1分别是两个网口,绑定到dpdk驱动。外部使用网线连接到一起,而对于虚拟机,直接连接到同一个交换机/网络。

pktgen_test_arch

由于虚拟机只有四个核心,所以选择核心1-3给pktgen使用,将2与3号核心分别绑定到两个dpdk网卡上,-P参数允许混杂模式,启动pktgen

1
pktgen -l 1-3 -n 4 -- -P -m "2.0, 3.1"
image-20220421151042602

测试: 网卡0发出1000个包

1
2
set 0 count 1000
start 0

观察到网卡1收到了1000个包

image-20220421151318064

同样地,网卡1也可以发送数据包让网卡0接收。数据包的mac信息、ip信息也都可以修改

1
2
3
4
5
6
7
8
set 0 src mac 00:0c:29:77:d3:89
set 0 dst mac 00:0c:29:77:d3:22

set 0 src ip 10.0.0.2/24
set 0 dst ip 10.0.0.3/24

set 0 proto udp
set 0 size 64

双机双网卡-testpmd转发

使用DPDK自带的testpmd来进行二层转发,测试系统结构如图所示

img20220421uh128js12fd31fde

testpmd依旧使用1-3号核心,直接进入交互模式

1
dpdk-testpmd -l 1-3 -n 4 -- -i

show config fwd用来查看testpmd的转发模式,默认模式为io转发模式,类似于一个二层交换机。可以通过set fwd rxonly设置为仅接受,或者set fwd io设置为io转发模式。set promisc all on/off用于开启/关闭混杂模式。在运行混杂的io转发模式下启动testpmd:

1
start

查看下统计信息

1
show port stats all
image-20220421152925284

此时因为pktgen还没有发送数据包所以都是0。让pktgen从0号网口发送1000个64k数据包,地址都是默认的。

image-20220421153409807

pktgen的1号网口应该收到1000个数据包,但是我这里只收到994个,丢包估计是因为我的笔记本CPU干到100%的原因。再去testpmd看下信息:

image-20220421153624431

testpmd的0号网口收到了1000个包,1号网口转发出去994个包,数量都对得上~也确实丢了6个包。。更进一步的性能测试就不做了,笔记本不太行而且虚拟机之间走的还是VMware的虚拟网络。

双机双网卡-OVS转发

测试系统结构配置与双机双网卡-testpmd转发基本类似,只是将testpmd换成OVS,流表直接缺省的NORMAL即可。OVS提供了一键启动的脚本,位于/usr/local/share/openvswitch/scripts目录下,设置初始化dpdk驱动:

1
2
3
4
export PATH=$PATH:/usr/local/share/openvswitch/scripts
ovs-ctl --system-id=random start
ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-init=true
ovs-ctl --system-id=random restart

如果这里没报错就基本稳了,查看下OVS情况:

1
2
ovs-vsctl get Open_vSwitch . dpdk_initialized
ovs-vswitchd --version
image-20220101231219701

完美~

添加网桥:

1
ovs-vsctl add-br br0

需要设置网桥的参数以支持DPDK驱动:

1
ovs-vsctl set bridge br0 datapath_type=netdev
  • 如果没有这个datapath_type的设置,在添加DPDK网卡的时候就会出错,查看/usr/local/var/log/openvswitch/ovs-vswitchd.log报错日志为:
image-20220102231251064

​ 具体参考文档DPDK Bridges

添加两个DPDK网卡:

1
2
ovs-vsctl add-port br0 dpdk-p0 -- set Interface dpdk-p0 type=dpdk options:dpdk-devargs=0000:0b:00.0
ovs-vsctl add-port br0 dpdk-p1 -- set Interface dpdk-p1 type=dpdk options:dpdk-devargs=0000:13:00.0
image-20220421154240653

查看此时的默认流表ovs-ofctl dump-flows br0,数据包等都是0个。使用pktgrn发送1000个数据包:

image-20220421154702286

基本功能测试完成✅

脚本boy上线

这一顿操作下来还是有点麻烦的,直接上脚本:

(脚本未更新)

PS:写的比较随意

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
#!/usr/bin/env bash

set -e

if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi

DPDK_VER="21.11"
SRC_DIR="/usr/src"

function prepare() {
echo "export LC_ALL=en_US.UTF-8" >> /etc/profile
echo "export PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig" >> /etc/profile
. /etc/profile
dnf -y update
}

function prepare_env_dpdk() {
dnf -y groupinstall "Development Tools"
dnf -y install python3 wget numactl-devel
pip3 install meson ninja pyelftools
}

function prepare_src_dpdk() {
wget https://fast.dpdk.org/rel/dpdk-$DPDK_VER.tar.xz -O $SRC_DIR/dpdk-$DPDK_VER.tar.xz
mkdir -p $SRC_DIR/dpdk-$DPDK_VER
tar xf $SRC_DIR/dpdk-$DPDK_VER.tar.xz --strip-components 1 -C $SRC_DIR/dpdk-$DPDK_VER
# export DPDK_DIR=$SRC_DIR/dpdk-21.11
}

function build_dpdk() {
cd $SRC_DIR/dpdk-$DPDK_VER
meson build
ninja -C build
ninja -C build install
ldconfig
}

function check_dpdk() {
pkg-config --modversion libdpdk
}


function install_dpdk() {
prepare
prepare_env_dpdk
prepare_src_dpdk
build_dpdk
check_dpdk
}

function prepare_env_ovs() {
dnf -y install libcap-ng-devel openssl-devel python3-sphinx checkpolicy desktop-file-utils groff python3-devel selinux-policy-devel unbound unbound-devel
}

function prepare_src_ovs() {
cd $SRC_DIR/
git clone https://github.com/openvswitch/ovs.git
}

function build_ovs() {
cd $SRC_DIR/ovs
./boot.sh
./configure --with-dpdk=static
make
make install
}

function install_ovs() {
prepare_env_ovs
prepare_src_ovs
build_ovs
}

function set_iommu() {
grubby --update-kernel=ALL --args="iommu=pt"
grubby --update-kernel=ALL --args="intel_iommu=on"
}

function enable_hugepages() {
echo "vm.nr_hugepages=2048" > /etc/sysctl.d/hugepages.conf
echo "nodev /dev/hugepages hugetlbfs defaults,nofail 0 0" >> /etc/fstab
echo "you should reboot now"
}

function run_ovs() {
export PATH=$PATH:/usr/local/share/openvswitch/scripts
ovs-ctl --system-id=random start
ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-init=true
ovs-ctl --system-id=random restart
}

function check_ovs_dpdk() {
ovs-vsctl get Open_vSwitch . dpdk_initialized
}

function clean() {
dnf clean all
}

"$@"