打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
20个基于DPDL开源项目,带你冲破内核瓶颈
userphoto

2023.07.26 湖南

关注

前言:开源(Open Source,开放源码)被非盈利软件组织(美国的Open Source Initiative协会)注册为认证标记,并对其进行了正式的定义,用于描述那些源码可以被公众使用的软件,并且此软件的使用、修改和发行也不受许可证的限制。

一、OVS-DPDK 虚拟交换机

OVS-DPDK(Open vSwitch with DPDK)是一种高性能的虚拟交换机,它将DPDK(Data Plane Development Kit)和Open vSwitch技术相结合,用于在虚拟化环境中加速数据平面处理。OVS-DPDK采用用户空间数据包处理模式,利用DPDK提供高效、低延迟的数据包处理功能,并且可以与传统的内核态网络协议栈进行无缝衔接。

通过使用OVS-DPDK,可以实现更高的吞吐量和更低的延迟,从而提升网络性能。同时,OVS-DPDK还支持灵活配置和管理网络服务,并且可以与其他开源软件如OpenStack等集成使用。因此,在云计算、SDN、NFV等领域中得到了广泛应用。

要使用 ovs-dpdk,需要在node上构建 DPDK 并使用相应的 DPDK flag重新构建 ovs。OVS-DPDK需要从源码编译,因为高度依赖内核等所在机器的环境,并需要配置很多参数以达到高性能。这意味着很难提供一个ovs-dpdk docker镜像来满足所有情况。

OVS-DPDK需要大页内存作为资源。由于 DPDK 会剥夺系统对 nic 的控制权,我们需要一个 ovs-dpdk 专用的 nic 来传输容器网络,另一个 nic 用于普通主机网络。

普通Pod的ovs网络是在 pod 和 ovs 端口之间放置了一个 veth 对。veth 的一端移动到容器网络命名空间。不能用 OVS-DPDK 做到这一点。当我们请求一个新的 DPDK 端口时,我们最终会在 /var/run/openvswitch/ 这样的目录中得到一个类似 vhost-user 套接字文件的东西。它不能在命名空间之间移动,它必须被挂载到 pod 中就像一个普通的文件(由 Userspace-CNI 提供的功能)。所以不能使用 OVS-DPDK 作为默认网络。Pod对K8S API不可达,K8S不能对pod进行健康检查。因此,需要依赖 Multus 来连接多个网络。Kernel-OVS 仍然是默认网络。此外,Multus 允许为Pod提供 OVS-DPDK 网络。这是同一个 OVS 实例,但是 DPDK port位于另一个支持 DPDK 的bridge上。

ovs-dpdk创建br和port, ovs 集成网桥类型更改为 netdev ,端口类型更改为 dpdkvhostuser 并设置其他 ovs-dpdk 参数。

1.1准备工作

[root@backendcloud-fedora27 ~]# dnf groupinstall "Development Tools"
[root@backendcloud-fedora27 ~]# dnf groupinstall "Virtualization"
[root@backendcloud-fedora27 ~]# dnf install qemu
[root@backendcloud-fedora27 ~]# dnf install automake tunctl kernel-tools pciutils hwloc numactl
[root@backendcloud-fedora27 ~]# dnf install libpcap-devel
[root@backendcloud-fedora27 ~]# dnf install numactl-devel
[root@backendcloud-fedora27 ~]# dnf install libtool

1.2编译DPDK

[root@backendcloud-fedora27 ~]# tar xf dpdk-17.08.1.tar.xz 
[root@backendcloud-fedora27 ~]# ls
anaconda-ks.cfg dpdk-17.08.1.tar.xz dpdk-stable-17.08.1
[root@backendcloud-fedora27 ~]# cd dpdk-
-bash: cd: dpdk-: No such file or directory
[root@backendcloud-fedora27 ~]# cd dpdk-stable-17.08.1/
[root@backendcloud-fedora27 dpdk-stable-17.08.1]# export DPDK_DIR=`pwd`/build
[root@backendcloud-fedora27 dpdk-stable-17.08.1]# make config T=x86_64-native-linuxapp-gcc
Configuration done using x86_64-native-linuxapp-gcc
[root@backendcloud-fedora27 dpdk-stable-17.08.1]# sed -ri 's,(PMD_PCAP=).*,\1y,' build/.config
[root@backendcloud-fedora27 dpdk-stable-17.08.1]# make

make 报错:

/usr/src/kernels/4.18.19-100.fc27.x86_64/Makefile:945: *** "Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel".  Stop.

安装 elfutils-libelf-devel 解决:

[root@backendcloud-fedora27 dpdk-stable-17.08.1]# yum install -y elfutils-libelf-devel

编译OvS-DPDK

[root@backendcloud-fedora27 ~]# wget http://openvswitch.org/releases/openvswitch-2.8.1.tar.gz
[root@backendcloud-fedora27 ~]# tar -xzvf openvswitch-2.8.1.tar.gz
[root@backendcloud-fedora27 ~]# cd openvswitch-2.8.1/
[root@backendcloud-fedora27 openvswitch-2.8.1]# export OVS_DIR=`pwd`
[root@backendcloud-fedora27 openvswitch-2.8.1]# sudo ./boot.sh
[root@backendcloud-fedora27 openvswitch-2.8.1]# sudo ./configure --with-dpdk="$DPDK_DIR/" CFLAGS="-g -Ofast"
[root@backendcloud-fedora27 openvswitch-2.8.1]# sudo make 'CFLAGS=-g -Ofast -march=native' -j10

Create OvS DB and Start OvS DB-Server

[root@backendcloud-fedora27 openvswitch-2.8.1]# pkill -9 ovs
[root@backendcloud-fedora27 openvswitch-2.8.1]# rm -rf /usr/local/var/run/openvswitch
[root@backendcloud-fedora27 openvswitch-2.8.1]# rm -rf /usr/local/etc/openvswitch/
[root@backendcloud-fedora27 openvswitch-2.8.1]# rm -f /usr/local/etc/openvswitch/conf.db
[root@backendcloud-fedora27 openvswitch-2.8.1]# mkdir -p /usr/local/etc/openvswitch
[root@backendcloud-fedora27 openvswitch-2.8.1]# mkdir -p /usr/local/var/run/openvswitch
[root@backendcloud-fedora27 openvswitch-2.8.1]# cd $OVS_DIR
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./ovsdb/ovsdb-tool create /usr/local/etc/openvswitch/conf.db ./vswitchd/vswitch.ovsschema
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./ovsdb/ovsdb-server --remote=punix:/usr/local/var/run/openvswitch/db.sock --remote=db:Open_vSwitch,Open_vSwitch,manager_options --pidfile --detach
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl --no-wait init

Configure Fedora27 for OvS-DPDK

[root@backendcloud-fedora27 openvswitch-2.8.1]# vim /etc/default/grub
...
GRUB_CMDLINE_LINUX_DEFAULT="default_hugepagesz=1G hugepagesz=1G hugepages=4 hugepagesz=2M hugepages=512 iommu=pt intel_iommu=on"
[root@backendcloud-fedora27 openvswitch-2.8.1]# grub2-mkconfig -o /boot/grub2/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-4.18.19-100.fc27.x86_64
Found initrd image: /boot/initramfs-4.18.19-100.fc27.x86_64.img
Found linux image: /boot/vmlinuz-0-rescue-98bddbf29a4f40009b8390e2c27a80ac
Found initrd image: /boot/initramfs-0-rescue-98bddbf29a4f40009b8390e2c27a80ac.img
done
[root@backendcloud-fedora27 openvswitch-2.8.1]# reboot

可以设置cpu隔离:GRUB_CMDLINE_LINUX_DEFAULT=”default_hugepagesz=1G hugepagesz=1G hugepages=16 hugepagesz=2M hugepages=2048 iommu=pt intel_iommu=on isolcpus=1-27,29-55”

[root@backendcloud-fedora27 ~]# mkdir -p /mnt/huge
[root@backendcloud-fedora27 ~]# mkdir -p /mnt/huge_2mb
[root@backendcloud-fedora27 ~]# mount -t hugetlbfs hugetlbfs /mnt/huge
[root@backendcloud-fedora27 ~]# mount -t hugetlbfs none /mnt/huge_2mb -o pagesize=2MB
[root@backendcloud-fedora27 ~]# cat /proc/meminfo
MemTotal: 17650724 kB
MemFree: 11744728 kB
MemAvailable: 11867184 kB
...
HugePages_Total: 4
HugePages_Free: 4
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 1048576 kB
Hugetlb: 5242880 kB
DirectMap4k: 124736 kB
DirectMap2M: 5273600 kB
DirectMap1G: 13631488 kB
[root@backendcloud-fedora27 ~]# cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-4.18.19-100.fc27.x86_64 root=/dev/mapper/fedora-root ro rd.lvm.lv=fedora/root rhgb quiet default_hugepagesz=1G hugepagesz=1G hugepages=4 hugepagesz=2M hugepages=512 iommu=pt intel_iommu=on

配置OVS-DPDK

[root@backendcloud-fedora27 ~]# modprobe vfio-pci
[root@backendcloud-fedora27 ~]# modprobe openvswitch
[root@backendcloud-fedora27 ~]# cd openvswitch-2.8.1
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./ovsdb/ovsdb-server --remote=punix:/usr/local/var/run/openvswitch/db.sock --remote=db:Open_vSwitch,Open_vSwitch,manager_options --pidfile --detach
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./vswitchd/ovs-vswitchd unix:/usr/local/var/run/openvswitch/db.sock --pidfile --detach
2022-09-22T06:09:08Z|00001|ovs_numa|INFO|Discovered 4 CPU cores on NUMA node 0
2022-09-22T06:09:08Z|00002|ovs_numa|INFO|Discovered 1 NUMA nodes and 4 CPU cores
2022-09-22T06:09:08Z|00003|reconnect|INFO|unix:/usr/local/var/run/openvswitch/db.sock: connecting...
2022-09-22T06:09:08Z|00004|reconnect|INFO|unix:/usr/local/var/run/openvswitch/db.sock: connected
2022-09-22T06:09:08Z|00005|dpdk|INFO|DPDK Disabled - Use other_config:dpdk-init to enable
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-init=true

配置隔离cpu,memory提高DPDK性能,非必须。

[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl set Open_vSwitch . other_config:pmd-cpu-mask=0x10000001
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-lcore-mask=0xffffffeffffffe
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-socket-mem="1024,1024"

Creating an OvS-DPDK Bridge and Ports

[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl show
52de1671-20cc-438c-be6a-d41e7923100b
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl add-br br0 -- set bridge br0 datapath_type=netdev
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl add-port br0 vhost-user1 -- set Interface vhost-user1 type=dpdkvhostuser
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl add-port br0 vhost-user2 -- set Interface vhost-user2 type=dpdkvhostuser
[root@backendcloud-fedora27 openvswitch-2.8.1]# ./utilities/ovs-vsctl show
52de1671-20cc-438c-be6a-d41e7923100b
Bridge "br0"
Port "vhost-user1"
Interface "vhost-user1"
type: dpdkvhostuser
Port "br0"
Interface "br0"
type: internal
Port "vhost-user2"
Interface "vhost-user2"
type: dpdkvhostuser

Binding Nic Device to DPDK

[root@backendcloud-fedora27 openvswitch-2.8.1]# modprobe vfio-pci
[root@backendcloud-fedora27 openvswitch-2.8.1]# cp ~/dpdk-stable-17.08.1/usertools/dpdk-devbind.py /usr/bin/
[root@backendcloud-fedora27 openvswitch-2.8.1]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:7a:c2:a0 brd ff:ff:ff:ff:ff:ff
inet 192.168.126.143/24 brd 192.168.126.255 scope global dynamic ens33
valid_lft 1442sec preferred_lft 1442sec
inet6 fe80::7e7a:3d91:e5b0:4a63/64 scope link
valid_lft forever preferred_lft forever
3: ens34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:7a:c2:aa brd ff:ff:ff:ff:ff:ff
inet 192.168.126.144/24 brd 192.168.126.255 scope global dynamic ens34
valid_lft 1398sec preferred_lft 1398sec
inet6 fe80::49fe:5d8c:8b86:95d8/64 scope link
valid_lft forever preferred_lft forever
4: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
link/ether 52:54:00:30:d1:c3 brd ff:ff:ff:ff:ff:ff
inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0
valid_lft forever preferred_lft forever
5: virbr0-nic: <BROADCAST,MULTICAST> mtu 1500 qdisc fq_codel master virbr0 state DOWN group default qlen 1000
link/ether 52:54:00:30:d1:c3 brd ff:ff:ff:ff:ff:ff
6: ovs-netdev: <BROADCAST,PROMISC> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 22:0e:6b:c1:4c:91 brd ff:ff:ff:ff:ff:ff
7: br0: <BROADCAST,PROMISC> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 0e:3b:21:02:03:47 brd ff:ff:ff:ff:ff:ff
[root@backendcloud-fedora27 openvswitch-2.8.1]# dpdk-devbind.py --status

Network devices using DPDK-compatible driver
============================================
<none>

Network devices using kernel driver
===================================
0000:02:01.0 '82545EM Gigabit Ethernet Controller (Copper) 100f' if=ens33 drv=e1000 unused=vfio-pci *Active*
0000:02:02.0 '82545EM Gigabit Ethernet Controller (Copper) 100f' if=ens34 drv=e1000 unused=vfio-pci *Active*
...
[root@backendcloud-fedora27 openvswitch-2.8.1]# dpdk-devbind.py --bind=vfio-pci ens34
Routing table indicates that interface 0000:02:02.0 is active. Not modifying
[root@backendcloud-fedora27 openvswitch-2.8.1]# yum install -y net-tools
[root@backendcloud-fedora27 openvswitch-2.8.1]# ifconfig ens34 down
[root@backendcloud-fedora27 openvswitch-2.8.1]# dpdk-devbind.py --bind=vfio-pci ens34
[root@backendcloud-fedora27 openvswitch-2.8.1]# dpdk-devbind.py --status

Network devices using DPDK-compatible driver
============================================
0000:02:02.0 '82545EM Gigabit Ethernet Controller (Copper) 100f' drv=vfio-pci unused=

Network devices using kernel driver
===================================
0000:02:01.0 '82545EM Gigabit Ethernet Controller (Copper) 100f' if=ens33 drv=e1000 unused=vfio-pci *Active*
...

Using DPDK vhost-user Ports with VMs

去官网下载镜像并修改root密码:

[root@backendcloud-fedora27 ~]# virt-customize -a centos7vm1.qcow2 --root-password password:666666
-bash: virt-customize: command not found
[root@backendcloud-fedora27 ~]# dnf install -y libguestfs-tools
[root@backendcloud-centos9 ~]# virt-customize -a centos7vm1.qcow2 --root-password password:666666
[ 0.0] Examining the guest ...
[ 6.8] Setting a random seed
[ 6.8] Setting passwords
[ 8.3] SELinux relabelling
[ 12.9] Finishing off
[root@backendcloud-centos9 ~]# virt-customize -a centos7vm2.qcow2 --root-password password:666666

启动两个dpdk vm:

qemu-system-x86_64 -m 1024 -smp 4 -cpu host,pmu=off -hda /root/centos7vm1.qcow2 -boot c -enable-kvm -no-reboot -net none -nographic \
-chardev socket,id=char1,path=/usr/local/var/run/openvswitch/vhost-user1 \
-netdev type=vhost-user,id=mynet1,chardev=char1,vhostforce \
-device virtio-net-pci,mac=00:00:00:00:00:01,netdev=mynet1 \
-object memory-backend-file,id=mem,size=1G,mem-path=/dev/hugepages,share=on \
-numa node,memdev=mem -mem-prealloc

qemu-system-x86_64 -m 1024 -smp 4 -cpu host,pmu=off -hda /root/centos7vm2.qcow2 -boot c -enable-kvm -no-reboot -net none -nographic \
-chardev socket,id=char2,path=/usr/local/var/run/openvswitch/vhost-user2 \
-netdev type=vhost-user,id=mynet2,chardev=char2,vhostforce \
-device virtio-net-pci,mac=00:00:00:00:00:02,netdev=mynet2 \
-object memory-backend-file,id=mem,size=1G,mem-path=/dev/hugepages,share=on \
-numa node,memdev=mem -mem-prealloc

若上面的操作是在vmware上操作,需要加上上面额外的参数pmu=off,为了规避vmware的bug。若不加会报下面的错误:

[root@backendcloud-fedora27 ~]# qemu-system-x86_64 -m 1024 -smp 4 -cpu host -hda /root/centos7vm1.qcow2 -boot c -enable-kvm -no-reboot -net none -nographic \
> -chardev socket,id=char1,path=/usr/local/var/run/openvswitch/vhost-user1 \
> -netdev type=vhost-user,id=mynet1,chardev=char1,vhostforce \
> -device virtio-net-pci,mac=00:00:00:00:00:01,netdev=mynet1 \
> -object memory-backend-file,id=mem,size=1G,mem-path=/dev/hugepages,share=on \
> -numa node,memdev=mem -mem-prealloc
qemu-system-x86_64: error: failed to set MSR 0x38d to 0x0
qemu-system-x86_64: /builddir/build/BUILD/qemu-2.10.2/target/i386/kvm.c:1806: kvm_put_msrs: Assertion `ret == cpu->kvm_msr_buf->nmsrs' failed.
Aborted (core dumped)

1.3安装iperf3,并测试

[root@localhost ~]# iperf3 -s
-----------------------------------------------------------
Server listening on 5201
-----------------------------------------------------------
Accepted connection from 192.168.0.2, port 48462
[ 5] local 192.168.0.1 port 5201 connected to 192.168.0.2 port 48464
[ ID] Interval Transfer Bandwidth
[ 5] 0.00-1.00 sec 828 MBytes 6.95 Gbits/sec
[ 5] 1.00-2.00 sec 775 MBytes 6.51 Gbits/sec
[ 5] 2.00-3.00 sec 852 MBytes 7.15 Gbits/sec
[ 5] 3.00-4.00 sec 1.03 GBytes 8.85 Gbits/sec
[ 5] 4.00-5.00 sec 928 MBytes 7.79 Gbits/sec
[ 5] 5.00-6.00 sec 905 MBytes 7.59 Gbits/sec
[ 5] 6.00-7.00 sec 824 MBytes 6.91 Gbits/sec
[ 5] 7.00-8.00 sec 962 MBytes 8.07 Gbits/sec
[ 5] 8.00-9.00 sec 987 MBytes 8.28 Gbits/sec
[ 5] 9.00-10.00 sec 856 MBytes 7.19 Gbits/sec
[ 5] 10.00-10.04 sec 35.2 MBytes 7.97 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bandwidth
[ 5] 0.00-10.04 sec 0.00 Bytes 0.00 bits/sec sender
[ 5] 0.00-10.04 sec 8.80 GBytes 7.53 Gbits/sec receiver
-----------------------------------------------------------
Server listening on 5201
-----------------------------------------------------------
[root@localhost ~]# iperf3 -c 192.168.0.1
Connecting to host 192.168.0.1, port 5201
[ 4] local 192.168.0.2 port 48464 connected to 192.168.0.1 port 5201
[ ID] Interval Transfer Bandwidth Retr Cwnd
[ 4] 0.00-1.00 sec 861 MBytes 7.22 Gbits/sec 1087 177 KBytes
[ 4] 1.00-2.00 sec 745 MBytes 6.26 Gbits/sec 566 182 KBytes
[ 4] 2.00-3.00 sec 908 MBytes 7.61 Gbits/sec 755 184 KBytes
[ 4] 3.00-4.00 sec 1.01 GBytes 8.66 Gbits/sec 824 212 KBytes
[ 4] 4.00-5.00 sec 935 MBytes 7.85 Gbits/sec 589 165 KBytes
[ 4] 5.00-6.00 sec 875 MBytes 7.34 Gbits/sec 514 194 KBytes
[ 4] 6.00-7.00 sec 850 MBytes 7.13 Gbits/sec 718 188 KBytes
[ 4] 7.00-8.00 sec 983 MBytes 8.25 Gbits/sec 949 158 KBytes
[ 4] 8.00-9.00 sec 930 MBytes 7.80 Gbits/sec 649 180 KBytes
[ 4] 9.00-10.00 sec 892 MBytes 7.48 Gbits/sec 668 147 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-10.00 sec 8.80 GBytes 7.56 Gbits/sec 7319 sender
[ 4] 0.00-10.00 sec 8.80 GBytes 7.56 Gbits/sec receiver

iperf Done.

对比非OVS-DPDK虚拟机

[root@backendcloud-fedora27 ~]# yum -y install wget openssl-devel gcc make python-devel openssl-devel kernel-devel graphviz kernel-debug-devel autoconf automake rpm-build redhat-rpm-config libtool python-twisted-core python-zope-interface PyQt4 desktop-file-utils libcap-ng-devel groff checkpolicy selinux-policy-devel
[root@backendcloud-fedora27 ~]# adduser ovs
[root@backendcloud-fedora27 ~]# su - ovs
[ovs@backendcloud-fedora27 ~]$ mkdir -p ~/rpmbuild/SOURCES
[ovs@backendcloud-fedora27 ~]$ cd ~/rpmbuild/SOURCES
[ovs@backendcloud-fedora27 ~]$ wget http://openvswitch.org/releases/openvswitch-2.5.10.tar.gz
[ovs@backendcloud-fedora27 ~]$ tar -zxvf openvswitch-2.5.10.tar.gz
[ovs@backendcloud-fedora27 ~]$ rpmbuild -bb --nocheck openvswitch-2.5.10/rhel/openvswitch-fedora.spec
[ovs@backendcloud-fedora27 ~]$ exit
[root@backendcloud-fedora27 ~]# yum localinstall yum localinstall /home/ovs/rpmbuild/RPMS/x86_64/openvswitch-2.5.10-1.fc27.x86_64.rpm -y
[root@backendcloud-fedora27 ~]# ovs-vsctl --version
ovs-vsctl (Open vSwitch) 2.5.10
Compiled Sep 22 2022 16:43:31
DB Schema 7.12.1
[root@backendcloud-fedora27 ~]# systemctl start openvswitch.service
[root@backendcloud-fedora27 tmp]# ovs-vsctl show
0425b4a1-cfb0-4cbb-94a8-68bd581ce48e
[root@backendcloud-fedora27 ~]# ovs-vsctl add-br br1
[root@backendcloud-fedora27 tmp]# ovs-vsctl show
0425b4a1-cfb0-4cbb-94a8-68bd581ce48e
Bridge "br1"
Port "br1"
Interface "br1"
type: internal
ovs_version: "2.5.10"
[root@backendcloud-fedora27 tmp]# cat test.xml
<network>
<name>test</name>
<forward mode="bridge"/>
<bridge name="br1"/>
<virtualport type="openvswitch"/>
</network>
[root@backendcloud-fedora27 tmp]# virsh net-list --all
Name State Autostart Persistent
----------------------------------------------------------
default active yes yes

[root@backendcloud-fedora27 tmp]# virsh net-define test.xml
Network test defined from test.xml

[root@backendcloud-fedora27 tmp]# virsh net-list --all
Name State Autostart Persistent
----------------------------------------------------------
default active yes yes
test inactive no yes

[root@backendcloud-fedora27 tmp]# virsh net-start test
Network test started

[root@backendcloud-fedora27 tmp]# virsh net-list --all
Name State Autostart Persistent
----------------------------------------------------------
default active yes yes
test active no yes

[root@backendcloud-fedora27 tmp]# ovs-vsctl show
0425b4a1-cfb0-4cbb-94a8-68bd581ce48e
Bridge "br1"
Port "br1"
Interface "br1"
type: internal
ovs_version: "2.5.10"
[root@backendcloud-fedora27 tmp]# virt-install --virt-type kvm --name test-vm1 --ram 1024 --boot hd --disk path=c1.qcow2 --network network=test,mac=52:54:00:aa:69:dd --noautoconsole --keymap=en-us
WARNING No operating system detected, VM performance may suffer. Specify an OS with --os-variant for optimal results.

Starting install...
Domain creation completed.
[root@backendcloud-fedora27 tmp]# virt-install --virt-type kvm --name test-vm2 --ram 1024 --boot hd --disk path=c2.qcow2 --network network=test,mac=52:54:00:aa:69:de --noautoconsole --keymap=en-us
WARNING No operating system detected, VM performance may suffer. Specify an OS with --os-variant for optimal results.

Starting install...
Domain creation completed.
# 不加 "--keymap=en-us" kvm虚拟机会出现键盘乱码
[root@backendcloud-fedora27 tmp]# ovs-vsctl show
0425b4a1-cfb0-4cbb-94a8-68bd581ce48e
Bridge "br1"
Port "vnet0"
Interface "vnet0"
Port "br1"
Interface "br1"
type: internal
Port "vnet1"
Interface "vnet1"
ovs_version: "2.5.10"

二、Lagopus虚拟交换机

Lagopus是一种开源的高性能虚拟交换机软件,由日本国立信息通信技术研究所(NICT)开发。它基于DPDK(Data Plane Development Kit)和OpenFlow协议,并采用了多线程、多核、流水线等技术,以实现高效的数据包处理。

Lagopus支持多种不同类型的网络接口和数据包格式,可以在物理服务器上或虚拟机中运行。它提供灵活的配置选项和API接口,使得用户可以轻松地进行自定义设置和扩展。同时,Lagopus还支持与其他SDN控制器集成使用。

由于其高性能、可定制化以及与其他开源软件兼容等特点,Lagopus在云计算、网络虚拟化、NFV等领域中具有广泛应用价值。

高性能软件 OpenFlow 1.3 交换机和路由器

特征

  • Lagopus 交换机

    • Best OpenFlow 1.3 compliant switch

    • OpenFlow Switch Specification 1.3.4

    • High performance software data plane with DPDK

  • Lagopus 路由器

    • 多个VRF

    • VLAN交换

    • DPDK支持

    • 仍然是测试版

三、MoonGen数据包生成器

MoonGen是建立在一个脚本化的高速数据包生成libmoon。整个负载生成器由 Lua 脚本控制:发送的所有数据包均由用户提供的脚本制作。多亏了令人难以置信的快速 LuaJIT VM 和数据包处理库 DPDK,它可以在仅使用单个 CPU 内核的情况下用 64 字节数据包使 10 Gbit/s 以太网链路饱和。即使每个数据包都被 Lua 脚本修改,MoonGen 也能达到这个速率。它不依赖于重播相同缓冲区之类的技巧。

MoonGen 还可以接收数据包,例如,检查被测系统丢弃了哪些数据包。由于接收也完全由用户的 Lua 脚本控制,因此可用于实现高级测试脚本。例如,可以使用两个相互建立连接的 MoonGen 实例。此设置可用于对防火墙等中间设备进行基准测试。

MoonGen 重点关注四个要点:

  • 高性能和多核扩展:每个 CPU 内核每秒 > 2000 万个数据包

  • 灵活性:每个数据包都是由用户提供的 Lua 脚本实时制作的

  • 精确和准确的时间戳:在商品硬件上以亚微秒精度进行时间戳

  • 精确和准确的速率控制:在商品硬件上可靠地生成任意流量模式

MoonGen 建立在libmoon 之上,它是 DPDK 的 Lua 包装器。

用户可以为他们的实验编写自定义脚本。建议在脚本中使用硬编码的设置特定常量。脚本就是配置,为脚本编写一个复杂的配置界面是无关紧要的。或者,有一个简化(但功能较弱)的命令行界面可用于快速测试。

下图显示了架构以及如何处理多核支持。

执行从必须在用户脚本中定义的主任务开始。此任务在使用的 NIC 上配置队列和过滤器,然后启动一个或多个从属任务。

请注意,Lua 没有任何对多线程的本机支持。因此,MoonGen 会为每个线程启动一个新的且完全独立的 LuaJIT VM。新的 VM 接收序列化参数:要执行的函数和参数,例如要从中发送数据包的队列。线程仅通过底层库共享状态。

示例脚本quality-of-service-test.lua展示了如何使用此线程模型来实现典型的负载生成任务。它通过发送两种不同类型的数据包来实现 QoS 测试并测量它们的吞吐量和延迟。它通过启动两项数据包生成任务来实现:一项用于后台流量,一项用于优先流量。第三个任务用于对传入的数据包进行分类和计数。

四、FastClick处理程序

FastClick是一种高性能的用户空间点击处理程序,用于优化网络应用程序中的数据包接收和处理。它最初由EPFL(瑞士洛桑联邦理工学院)开发,旨在提高网络应用程序的吞吐量和响应时间。

FastClick通过将数据包处理逻辑移动到用户空间,并使用自定义算法来实现快速匹配和过滤,从而实现了比内核中处理更高效的数据包接收和处理。它支持多线程操作,并具有灵活、可配置的流水线模型,可以轻松地进行定制以适应不同类型的网络应用程序。

FastClick已被广泛用于高性能网络设备(例如路由器、交换机等)和分布式系统中,以提供快速、可靠的数据包处理功能。

4.1安装fastclick

安装fastclick可以使用npm,Component和Bower。另外也提供了Ruby版的gem fastclick-rails以及.NET提供了NuGet package。最直接的可以在页面引入fastclick js文件。如:

在页面直接引入fastclick.js

<script type='application/javascript' src='/path/to/fastclick.js'></script>

使用npm安装

npm install fastclick

初始化FastClick实例

初始化FastClick实例建议在页面的DOM文档加载完成后。

纯Javascript版

if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}

jQuery版

$(function() {
FastClick.attach(document.body);
});

类似Common JS的模块系统方式

var attachFastClick = require('fastclick');
attachFastClick(document.body);

调用require('fastclick')会返回FastClick.attach函数。

使用needsclick过滤特定的元素

如果页面上有一些特定的元素不需要使用fastclick来立刻触发点击事件,可以在元素的class上添加needsclick:

<a class="needsclick">Ignored by FastClick</a>

不需要使用fastclick的情况

以下这几种情况是不需要使用fastclick:

1、FastClick是不会对PC浏览器添加监听事件

2、Android版Chrome 32+浏览器,如果设置viewport meta的值为width=device-width,这种情况下浏览器会马上出发点击事件,不会延迟300毫秒。

<meta name="viewport" content="width=device-width, initial-scale=1">

3、所有版本的Android Chrome浏览器,如果设置viewport meta的值有user-scalable=no,浏览器也是会马上出发点击事件。

4、IE11+浏览器设置了css的属性touch-action: manipulation,它会在某些标签(a,button等)禁止双击事件,IE10的为-ms-touch-action: manipulation

五、OpenFastPath数据包处理框架

OpenFastPath(OFP)是一个高性能的数据包处理框架,可以用于构建网络应用程序和设备,例如路由器、交换机等。它提供了一个用户空间的数据包处理引擎,可以通过与多种硬件平台和操作系统集成来实现快速、可扩展的网络应用程序。

OFP支持多个协议栈(例如TCP/IP、UDP/IP等),并提供了一系列基本功能模块,如ARP缓存管理、IP地址管理、ACL过滤等。它还支持虚拟化和容器化部署,并具有灵活的API和可扩展的插件架构,可以轻松地定制以适应不同类型的网络应用程序需求。

OFP最初由Linux基金会主导开发,并已成为Linux基金会项目之一。目前,它已经被广泛应用于各种高性能网络设备和云计算环境中。

fastpath 的介绍:

1. 提供了 Workgroup 协议的实现,Workgroup 的概念就是专门对应在线客服这个典型场景了。这是企业或组织机构的客服需求的核心概念和功能,类似于呼叫中心。

2.Server 端的历史记录存储。默认 Openfire 本身是不记录信息历史记录的,只记录离线留言。注意,离线消息和消息历史记录是两个不同的概念,离线消息是对方不在线的情况,server 端先保存起来,等对方上线后再发给他,发完了消息在 server 端就被删除了;而消息历史记录是只 server 端记录的所有对话消息,当然客户端也可以实现在客户端上自己的历史记录。

fasthpath:主要分为两端:agent - 客服,user - 用户,为人工客服实现了排队路由等基本的呼叫中心功能。

fasthpath:实现的原理就是以技能组为标准对用户加入对应的技能组在路由给对应技能组的客服人员,客服人员收到 offer 之后,可以接受,fastpath 就可以创建 openfire 的聊天室邀请用户和 agent 加入进来进行会话,实现客服功能。其中 fastpath 还提供了一些其他的附加功能:比如转接,路由过程指定客服,设置最大聊天室等等,这些功能可以再以后开发过程中有需要的情况下查看源码即可了解。

下面给出最简单的:user 去排队,客服加入技能组,来显示整个的客服功能。

这是 openfire 提供的插件,在 smack 中也提供了对应的 api,这里提供一个 agent 端的实例

/**
* 加入技能组
* @param workGroupName
* @return
*/
public boolean joinWorkGroup(String workGroupName,int maxChats){
boolean bResult=false;
agentSession=new AgentSession(workGroupName,connection);
agentSession.addInvitationListener(new WorkgroupInvitationListener(){
public void invitationReceived(WorkgroupInvitation workgroupInvitation) {
// System.out.println("workgroupInvitation.getWorkgroupName():"+workgroupInvitation.getWorkgroupName());
// System.out.println("workgroupInvitation.getMessageBody():"+workgroupInvitation.getMessageBody());
// System.out.println("workgroupInvitation.getMetaData():"+workgroupInvitation.getMetaData());
// System.out.println("workgroupInvitation.getInvitationSender():"+workgroupInvitation.getInvitationSender());
// System.out.println("workgroupInvitation.getGroupChatName():"+workgroupInvitation.getGroupChatName());
joinRoom(workgroupInvitation.getGroupChatName());
// System.out.println("name:"+workgroupInvitation.getGroupChatName()+",id:"+workgroupInvitation.getSessionID());
/*try {
agentSession.sendRoomTransfer(RoomTransfer.Type.user, "10110@kfas1", workgroupInvitation.getSessionID(), "转接");
} catch (XMPPException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
}

});
agentSession.addOfferListener(new OfferListener(){

public void offerReceived(Offer offer) {
System.out.println("offer.getUserJID();"+offer.getUserJID());
System.out.println("offer.getContent();"+offer.getContent());
offer.accept();
}

public void offerRevoked(RevokedOffer revokedOffer) {
System.out.println("revokedOffer.getReason():"+revokedOffer.getReason());
System.out.println("revokedOffer.getUserJID():"+revokedOffer.getUserJID());
}

});
agentSession.addQueueUsersListener(new QueueUsersListener(){

public void averageWaitTimeUpdated(WorkgroupQueue workgroupQueue, int averageWaitTime) {
// System.out.println("averageWaitTime:"+averageWaitTime);
// System.out.println("workgroupQueue.getAverageWaitTime():"+workgroupQueue.getAverageWaitTime());
// System.out.println("workgroupQueue.getCurrentChats():"+workgroupQueue.getCurrentChats());
//
}

public void oldestEntryUpdated(WorkgroupQueue workgroupQueue, Date oldestEntry) {
// System.out.println("oldestEntry:"+oldestEntry.toString());

}

public void statusUpdated(WorkgroupQueue workgroupQueue, Status status) {
// System.out.println("status:"+status.toString());

}

public void usersUpdated(WorkgroupQueue workgroupQueue, Set uers) {
for (Iterator iterator = uers.iterator(); iterator
.hasNext();) {
Object user = (Object) iterator.next();
System.out.println("user:"+user.toString());

}

}

});
try {
agentSession.setOnline(true);
/*Presence presence=new Presence(Presence.Type.available);
presence.setTo("demo@workgroup.kftest2");
presence.setPriority(1);
connection.sendPacket(presence);
System.out.println("presence OK");*/
agentSession.setStatus(Presence.Mode.available,maxChats,"OK");
System.out.println(agentSession.getMaxChats());
} catch (XMPPException e) {
e.printStackTrace();
}

bResult=true;
return bResult;
}

用户 user 端的加入排队实例,也是基于 smack 包编写,可以发现 smack 都提供了对应的对象进行编程:

public void joinQueue(String workgroupName, Map metaData) {
workgroup = new Workgroup(workgroupName, connection);
//监听技能组中队列的事件
//这里监听,由于workGroup监听connection的包,如果第二次初始化,第一次的队列的监听还会触发
this.listenForQueue();

workgroupInvitationListener = new WorkgroupInvitationListener(){
public void invitationReceived(WorkgroupInvitation workgroupInvitation) {
String room = workgroupInvitation.getGroupChatName();
joinRoom(room);
}
};
workgroup.addInvitationListener(workgroupInvitationListener);

if (workgroup != null) {
try {
workgroup.joinQueue(metaData, userid);
} catch (XMPPException e) {
//异常的情况也继续排队,由于技能没有agent会service-unavailable(503)
log.error("[>"+callInfo.getJid()+"<]: Unable to join chat queue.",e);
}
}
}


/**
* 监听队列情况
*
*/
public void listenForQueue(){
queueListener =new QueueListener(){
//加入队列成功返回基础消息
public void joinedQueue() {

}

//离开队列事件
public void departedQueue() {
}

//队列位置变化
public void queuePositionUpdated(int currentPosition) {
}

//队列时间变化
public void queueWaitTimeUpdated(int secondsRemaining) {
}
};
workgroup.addQueueListener(queueListener);
}

六、VPP数据包处理框架

VPP(Vector Packet Processing)是一个高性能的数据包处理框架,它是Cisco开源的一个项目。VPP提供了一种可扩展的、用户空间的数据包处理引擎,可以通过与多种硬件平台和操作系统集成来实现快速、可扩展的网络应用程序。

VPP支持多个协议栈(例如TCP/IP、UDP/IP等),并提供了一系列基本功能模块,如ARP缓存管理、IP地址管理、ACL过滤等。它还支持虚拟化和容器化部署,并具有灵活的API和可扩展的插件架构,可以轻松地定制以适应不同类型的网络应用程序需求。

VPP是一个高度可编程化和可定制化的数据包处理框架,在运营商级网络设备、云计算环境中得到广泛应用。除了Cisco之外,许多公司和组织也在使用VPP构建自己的网络产品或服务。

以下安装方式在centos7上安装测试(可用)

有三种安装方式:源码安装、yum安装、vpp-config安装

源码安装:

1.使用git将VPP源码克隆下来(没有git可使用 yum install git -y 安装)

[root@localhost ~]# mkdir source
[root@localhost ~]# cd source
[root@localhost source]# git clone https://gerrit.fd.io/r/vpp

2. 安装依赖环境,进入VPP目录下执行:

[root@localhost source]# cd vpp
[root@localhost vpp]# yum install -y epel-release python-pip net-tools
[root@localhost vpp]# make install-dep

3. 安装dpdk,执行第4步代码编译时,会自动下载dpdk并一起编译(可忽略)

[root@localhost vpp]# make dpdk-install-dev

4. 进行代码编译(make distclean可以清除编译生成文件 )

[root@localhost vpp]# make build

5. 制作rpm包

[root@localhost vpp]# make pkg-rpm

6. 安装VPP

[root@localhost vpp]# cd build-root/
[root@localhost build-root]# rpm -i vpp*.rpm

7. 启动VPP(并设置开机启动)

[root@localhost ~]# systemctl enable vpp
[root@localhost ~]# systemctl start vpp
[root@localhost ~]# systemctl status vpp.service

8.测试安装是否成功

root@localhost ~]# vppctl
显示如下代表安装成功:

七、ClickOS高性能虚拟机

ClickOS是一种专门为云计算设计的高性能虚拟机,它基于Xen虚拟化技术,并集成了高效的网络协议栈和数据平面加速技术。ClickOS主要用于构建可定制、低延迟、高吞吐量的网络功能虚拟化(NFV)系统,如防火墙、负载均衡器、路由器等。ClickOS的特点在于其精简的操作系统内核,以及对热插拔设备和硬件辅助虚拟化技术的支持,使得它可以实现比传统虚拟机更高的性能和更低的资源开销。

八、http://FD.io数据平面开发工具包

http://FD.io是一个开放源代码的数据平面开发工具包,它提供了一组数据平面服务和库,以便于构建高性能、可扩展、灵活的网络功能虚拟化(NFV)系统。http://FD.io旨在为各种硬件和软件环境下的网络应用程序提供通用的、高效的数据平面支持,并且可以快速地配置和部署。http://FD.io项目由Linux基金会主导,采用Apache 2.0许可证发布,目前已成为全球最大的数据平面开源社区之一。

http://FD.io的一个关键项目是VPP(Vector Packet Processing:矢量报文处理)。VPP是高度模块化的项目,新开发的功能模块很容易被集成进VPP,而不影响VPP底层的代码框架。这就给了开发者很大的灵活性,可以创新不计其数的报文处理解决方案。

除了VPP,http://FD.io充分利用DPDK特性以支持额外的项目,包括NSH_SFC, Honeycomb 和ONE来加速网络功能虚拟化的数据面。此外,http://FD.io还与其他关键的开源项目进行集成,以支持网络功能虚拟化和软件定义网络。目前已经集成的开源项目包括:K8s、OpenStack、ONAP和OpenDaylight。

下图是http://FD.io的网络生态系统概览:

VPP简介

VPP到底是什么?一个软件路由器?一个虚拟交换机?一个虚拟网络功能?事实上,它包含所有这些,并且包含更多。VPP是一个模块化和可扩展的软件框架,用于创建网络数据面应用程序。更重要的是,VPP代码为现代通用处理器平台(x86、ARM、PowerPC等)而生,并把重点放在优化软件和硬件接口上,以便用于实时的网络输入输出操作和报文处理。

VPP充分利用通用处理器优化技术,包括矢量指令(例如Intel SSE, AVX)以及I/O和CPU缓存间的直接交互(例如 Intel DDIO),以达到最好的报文处理性能。利用这些优化技术的好处是:使用最少的CPU核心指令和时钟周期来处理每个报文。在最新的Intel Xeon-SP处理器上,可以达到Tbps的处理性能。

VPP架构

VPP是一个有效且灵活的数据面,它包括一系列按有向图组织的转发图形节点(graph node)和一个软件框架。该软件框架包含基本的数据结构、定时器、驱动程序、在图形节点间分配CPU时间片的调度器、性能调优工具,比如计数器和内建的报文跟踪功能。

VPP采用插件(plugin)架构,插件与直接内嵌于VPP框架中的模块一样被同等对待。原则上,插件是实现某一特定功能的转发图形节点,但也可以是一个驱动程序,或者另外的CLI。插件能被插入到VPP有向图的任意位置,从而有利于快速灵活地开发新功能。因此,插件架构使开发者能够充分利用现有模块快速开发出新功能。

VPP架构:报文处理有向图

输入节点轮询(或中断驱动)接口的接收队列,获取批量报文。接着把这些报文按照下个节点功能组成一个矢量(vector)或者一帧(frame)。比如:输入节点收集所有IPv4的报文并把它们传递给ip4-input节点;输入节点收集所有IPv6的报文并把它们传递给ip6-input节点。当ip6-input节点被调度时,它取出这一帧报文,利用双循环(dual-loop) 或四循环(quad-loop)以及预取报文到CPU缓存技术处理报文,以达到最优性能。这能够通过减少缓存未命中数来有效利用CPU缓存。当ip6-input节点处理完当前帧的所有报文后,把报文传递到后续不同的节点。比如:如果某报文校验失败,就被传送到error-drop节点;正常报文被传送到ip6-lookup节点。一帧报文依次通过不同的图形节点,直到它们被interface-output节点发送出去。

VPP图形节点的处理逻辑

按照网络功能一次处理一帧报文,有几个好处:

  • 从软件工程的角度看,每一个图形节点是独立和自治的。

  • 从性能的角度看,主要的好处是可以优化CPU指令缓存(i-cache)的使用。当前帧的第一个报文加载当前节点的指令到指令缓存,当前帧的后续报文就可以“免费”使用指令缓存。这里,VPP充分利用了CPU的超标量结构,使报文内存加载和报文处理交织进行,达到更有效地利用CPU处理流水线。

  • VPP也充分利用了CPU的预测执行功能来达到更好的性能。从预测重用报文间的转发对象(比如邻接表和路由查找表),以及预先加载报文内容到CPU的本地数据缓存(d-cache)供下一次循环使用,这些有效使用计算硬件的技术,使得VPP可以利用更细粒度的并行性。

VPP有向图处理的特性,使它成为一个松耦合、高度一致的软件架构。每一个图形节点利用一帧报文作为输入和输出的最小处理单位,这就提供了松耦合的特性。通用功能被组合到每个图形节点中,这就提供了高度一致的架构。

在有向图中的节点是可替代的。当这个特性和VPP支持动态加载插件节点相结合时,新功能能被快速开发,而不需要新建和编译一个定制的代码版本。

九、Pktgen-DPDK数据包生成工具

Pktgen-DPDK是一种基于DPDK(Data Plane Development Kit)的数据包生成工具,可以用于测试和评估网络设备、协议栈等各种网络应用程序。它提供了丰富的功能,可以进行不同类型和大小的数据包生成,以及流量控制、速率控制等测试。此外,Pktgen-DPDK还支持多核处理、超线程和NUMA架构,并提供了Web界面来方便用户使用。由于采用了DPDK技术,Pktgen-DPDK具有非常高的性能和低延迟,并广泛应用于云计算、虚拟化、SDN/NFV等领域。

pktgen-dpdk使用dpdk加速包的发送接收,也可以发送接收pcap包,命令行如下:

./app/app/x86_64-native-linuxapp-gcc/pktgen -l 0-4 -n 3 -- -P -m "[11:3].0,[2:4].1" -s 0:[.pcap_filepath] (pktgen-dpdk.3.4.8)

pktgen-dpdk逻辑设计

在pktgen-main.c文件中包含了main主入口函数main()以及参数配置函数pktgen_parse_args(),其中pktgen结构体是所有的参数都是用的,参数配置函数主要是对pktgen结构中个成员赋值。

main函数中pktgen初始化如下:

memset(&pktgen, 0, sizeof(pktgen));

pktgen.flags = PRINT_LABELS_FLAG;
pktgen.ident = 0x1234;
pktgen.nb_rxd = DEFAULT_RX_DESC;
pktgen.nb_txd = DEFAULT_TX_DESC;
pktgen.nb_ports_per_page = DEFAULT_PORTS_PER_PAGE;

if ( (pktgen.l2p = l2p_create()) == NULL)
pktgen_log_panic("Unable to create l2p");

pktgen.portdesc_cnt = get_portdesc(pktgen.portlist,
pktgen.portdesc,
RTE_MAX_ETHPORTS,
0);

然后初始化log,cpu

	pktgen_init_log();
pktgen_cpu_init();

配置初始化port信息

void
pktgen_config_ports(void)
{
uint32_t lid, pid, i, s, q, sid;
rxtx_t rt;
pkt_seq_t *pkt;
port_info_t *info;
char buff[RTE_MEMZONE_NAMESIZE];
int32_t ret, cache_size;
char output_buff[256] = { 0 };
uint64_t ticks;

/* Find out the total number of ports in the system. */
/* We have already blacklisted the ones we needed to in main routine. */
pktgen.nb_ports = rte_eth_dev_count();
if (pktgen.nb_ports > RTE_MAX_ETHPORTS)
pktgen.nb_ports = RTE_MAX_ETHPORTS;

if (pktgen.nb_ports == 0)
pktgen_log_panic("*** Did not find any ports to use ***");

pktgen.starting_port = 0;

/* Setup the number of ports to display at a time */
if (pktgen.nb_ports > pktgen.nb_ports_per_page)
pktgen.ending_port = pktgen.starting_port +
pktgen.nb_ports_per_page;
else
pktgen.ending_port = pktgen.starting_port + pktgen.nb_ports;

pg_port_matrix_dump(pktgen.l2p);
....
....
for (s = 0; s < NUM_TOTAL_PKTS; s++)
pktgen_port_defaults(pid, s);

其中主要获取cpu端口数,以及填充结构体info,其中函数pktgen_port)defaults()主要设置一些默认显示的信息,其中里面的函数pktgen_packet_rate()计算传输率,此函数作用与动态的展示发送接收的数据参数。

main()往下调用的函数是pktgen_clear_display():

void
pktgen_clear_display(void)
{
if (!scrn_is_paused()) {
scrn_pause();

scrn_cls();
scrn_pos(100, 1);

pktgen_update_display();

scrn_resume();

pktgen_page_display(NULL, NULL);
}
}

其中pktgen_update_dsiplay()修改pktgen.flag标志:

void
pktgen_page_display(struct rte_timer *tim __rte_unused, void *arg __rte_unused)
{
static unsigned int update_display = 1;

/* Leave if the screen is paused */
if (scrn_is_paused())
return;

scrn_save();

if (pktgen.flags & UPDATE_DISPLAY_FLAG) {
pktgen.flags &= ~UPDATE_DISPLAY_FLAG;
update_display = 1;
}

update_display--;
if (update_display == 0) {
update_display = UPDATE_DISPLAY_TICK_INTERVAL;
_page_display();

if (pktgen.flags & PRINT_LABELS_FLAG)
pktgen.flags &= ~PRINT_LABELS_FLAG;
}

scrn_restore();

pktgen_print_packet_dump();
}

函数中主要执行两个函数_page_display(),pktgen_printf_paket_dump(),前者调用pktgen_page_stats(),显示端口上的统计信息:

void
pktgen_page_stats(void)
{
port_info_t *info;
unsigned int pid, col, row;
struct rte_eth_stats *rate, *cumm, *prev;
unsigned sp;
char buff[32];
int display_cnt;

if (pktgen.flags & PRINT_LABELS_FLAG)
pktgen_print_static_data();
...
...

其中函数pktgen_print_static_data()显示一些静态数据,动态数据主要通过info结构体传输。

然后main函数调用rte_timer_setup()

void
rte_timer_setup(void)
{
int lcore_id = rte_get_master_lcore();

/* init RTE timer library */
rte_timer_subsystem_init();

/* init timer structures */
rte_timer_init(&timer0);
rte_timer_init(&timer1);

/* load timer0, every 1/2 seconds, on Display lcore, reloaded automatically */
rte_timer_reset(&timer0,
UPDATE_DISPLAY_TICK_RATE,
PERIODICAL,
lcore_id,
pktgen_page_display,
NULL);

/* load timer1, every second, on timer lcore, reloaded automatically */
rte_timer_reset(&timer1,
pktgen.hz,
PERIODICAL,
lcore_id,
pktgen_process_stats,
NULL);
}

其中timer0,用于更新显示,timer1用于统计数据。

main函数接着调用pktgen_cli_start(),最终调用cli_start(),用于执行运行时参数。

pktgen 流量生成器

下面说一下发送接收包,在main函数的前部分还有一个重要的函数:

ret = rte_eal_remote_launch(pktgen_launch_one_lcore, NULL, i);

	/* Configure and initialize the ports */
pktgen_config_ports();

pktgen_log_info("");
pktgen_log_info("=== Display processing on lcore %d", rte_lcore_id());

/* launch per-lcore init on every lcore except master and master + 1 lcores */
for (i = 0; i < RTE_MAX_LCORE; i++) {
if ( (i == rte_get_master_lcore()) || !rte_lcore_is_enabled(i) )
continue;
ret = rte_eal_remote_launch(pktgen_launch_one_lcore, NULL, i);
if (ret != 0)
pktgen_log_error("Failed to start lcore %d, return %d", i, ret);
}
rte_delay_ms(1000); /* Wait for the lcores to start up. */

其中pktgen_launch-one_lcore()调用pktgen_main_tx_loop(),pktgen_mian_rx_loop(),pktgen_main_rxtx_loop()。

这些函数相应的调用函数,同时更新pktgen结构或是其中的成员,以pktgen_main_rxtx_loop()为例。

pktgen_main_rxtx_loop() -> pktgen_main_transmit() -> pktgen_send_pkts() ->pktgen_send_burst() -> _send_burst_..()-> pkt_do_tx_tap() -> write()

下面说一下pktgen作为流量生成器的一些个人理解。

pktgen作为linux内核模块的一部分,因此可以直接加载内核模块generate流量(lsmod pktgen),网上参考资料说是产生udp类型的packet,在此详细探讨一下。

在pktgen_main_transmit()中:

static __inline__ void
pktgen_main_transmit(port_info_t *info, uint16_t qid)
{
struct rte_mempool *mp = NULL;
uint32_t flags;

flags = rte_atomic32_read(&info->port_flags);

/*
* Transmit ARP/Ping packets if needed
*/
if ((flags & SEND_ARP_PING_REQUESTS))
pktgen_send_special(info, flags);

/* When not transmitting on this port then continue. */
if (flags & SENDING_PACKETS) {
mp = info->q[qid].tx_mp;

if (flags & (SEND_RANGE_PKTS | SEND_PCAP_PKTS | SEND_SEQ_PKTS)) {
if (flags & SEND_RANGE_PKTS)
mp = info->q[qid].range_mp;
else if (flags & SEND_SEQ_PKTS)
mp = info->q[qid].seq_mp;
else if (flags & SEND_PCAP_PKTS)
mp = info->q[qid].pcap_mp;
}

if (rte_atomic32_read(&info->q[qid].flags) & CLEAR_FAST_ALLOC_FLAG)
pktgen_setup_packets(info, mp, qid);

pktgen_send_pkts(info, qid, mp);
}

flags = rte_atomic32_read(&info->q[qid].flags);
if (flags & DO_TX_FLUSH)
pktgen_tx_flush(info, qid);
}

最终调用函数pktgen_setup_cb(),调用了pktgen_range_ctor()和pktgen_packet_ctor()

其中结构体:

typedef struct pkt_seq_s {
/* Packet type and information */
struct ether_addr eth_dst_addr; /**< Destination Ethernet address */
struct ether_addr eth_src_addr; /**< Source Ethernet address */

struct cmdline_ipaddr ip_src_addr; /**< Source IPv4 address also used for IPv6 */
struct cmdline_ipaddr ip_dst_addr; /**< Destination IPv4 address */
uint32_t ip_mask; /**< IPv4 Netmask value */

uint16_t sport; /**< Source port value */
uint16_t dport; /**< Destination port value */
uint16_t ethType; /**< IPv4 or IPv6 */
uint16_t ipProto; /**< TCP or UDP or ICMP */
uint16_t vlanid; /**< VLAN ID value if used */
uint8_t cos; /**< 802.1p cos value if used */
uint8_t tos; /**< tos value if used */
uint16_t ether_hdr_size;/**< Size of Ethernet header in packet for VLAN ID */

uint32_t mpls_entry; /**< MPLS entry if used */
uint16_t qinq_outerid; /**< Outer VLAN ID if Q-in-Q */
uint16_t qinq_innerid; /**< Inner VLAN ID if Q-in-Q */
uint32_t gre_key; /**< GRE key if used */

uint16_t pktSize; /**< Size of packet in bytes not counting FCS */
uint16_t pad0;
uint32_t gtpu_teid; /**< GTP-U TEID, if UDP dport=2152 */
uint8_t seq_enabled; /**< Enable or disable this sequence through GUI */

pkt_hdr_t hdr __rte_cache_aligned; /**< Packet header data */
uint8_t pad[MBUF_SIZE - sizeof(pkt_hdr_t)];
} pkt_seq_t __rte_cache_aligned;
void  pktgen_packet_ctor(port_info_t *info, int32_t seq_idx, int32_t type){
.....
if (likely(pkt->ethType == ETHER_TYPE_IPv4)) {
if (likely(pkt->ipProto == PG_IPPROTO_TCP)) { //构造TCP
if (pkt->dport != PG_IPPROTO_L4_GTPU_PORT) {
/* Construct the TCP header */
pktgen_tcp_hdr_ctor(pkt, l3_hdr, ETHER_TYPE_IPv4);


/* IPv4 Header constructor */
pktgen_ipv4_ctor(pkt, l3_hdr);
} else {
/* Construct the GTP-U header */
pktgen_gtpu_hdr_ctor(pkt, l3_hdr, pkt->ipProto,
GTPu_VERSION | GTPu_PT_FLAG, 0, 0, 0);


/* Construct the TCP header */
pktgen_tcp_hdr_ctor(pkt, l3_hdr, ETHER_TYPE_IPv4);


/* IPv4 Header constructor */
pktgen_ipv4_ctor(pkt, l3_hdr);
}
} else if (pkt->ipProto == PG_IPPROTO_UDP) {
if (pkt->dport != PG_IPPROTO_L4_GTPU_PORT) {
/* Construct the UDP header */
pktgen_udp_hdr_ctor(pkt, l3_hdr, ETHER_TYPE_IPv4);


/* IPv4 Header constructor */
pktgen_ipv4_ctor(pkt, l3_hdr);
} else {
/* Construct the GTP-U header */
pktgen_gtpu_hdr_ctor(pkt, l3_hdr, pkt->ipProto,
GTPu_VERSION | GTPu_PT_FLAG, 0, 0, 0);


/* Construct the UDP header */
pktgen_udp_hdr_ctor(pkt, l3_hdr, ETHER_TYPE_IPv4);


/* IPv4 Header constructor */
pktgen_ipv4_ctor(pkt, l3_hdr);
}
} else if (pkt->ipProto == PG_IPPROTO_ICMP) {
udpip_t *uip;
icmpv4Hdr_t *icmp;


/* Start from Ethernet header */
uip = (udpip_t *)l3_hdr;


/* Create the ICMP header */
uip->ip.src = htonl(pkt->ip_src_addr.addr.ipv4.s_addr);
uip->ip.dst = htonl(pkt->ip_dst_addr.addr.ipv4.s_addr);
tlen = pkt->pktSize - (pkt->ether_hdr_size + sizeof(ipHdr_t));
uip->ip.len = htons(tlen);
uip->ip.proto = pkt->ipProto;


icmp = (icmpv4Hdr_t *)&uip->udp;
icmp->code = 0;
if ( (type == -1) || (type == ICMP4_TIMESTAMP)) {
icmp->type =
ICMP4_TIMESTAMP;
icmp->data.timestamp.ident = 0x1234;
icmp->data.timestamp.seq = 0x5678;
icmp->data.timestamp.originate = 0x80004321;
icmp->data.timestamp.receive = 0;
icmp->data.timestamp.transmit = 0;
} else if (type == ICMP4_ECHO) {
icmp->type = ICMP4_ECHO;
icmp->data.echo.ident = 0x1234;
icmp->data.echo.seq = 0x5678;
icmp->data.echo.data = 0;
}
icmp->cksum = 0;
/* ICMP4_TIMESTAMP_SIZE */
tlen = pkt->pktSize - (pkt->ether_hdr_size + sizeof(ipHdr_t));
icmp->cksum = cksum(icmp, tlen, 0);
if (icmp->cksum == 0)
icmp->cksum = 0xFFFF;


/* IPv4 Header constructor */
pktgen_ipv4_ctor(pkt, l3_hdr);
}
} else if (pkt->ethType == ETHER_TYPE_IPv6) {
if (pkt->ipProto == PG_IPPROTO_TCP) {
/* Construct the TCP header */
pktgen_tcp_hdr_ctor(pkt, l3_hdr, ETHER_TYPE_IPv6);


/* IPv6 Header constructor */
pktgen_ipv6_ctor(pkt, l3_hdr);
} else if (pkt->ipProto == PG_IPPROTO_UDP) {
/* Construct the UDP header */
pktgen_udp_hdr_ctor(pkt, l3_hdr, ETHER_TYPE_IPv6);


/* IPv6 Header constructor */
pktgen_ipv6_ctor(pkt, l3_hdr);
}
} else if (pkt->ethType == ETHER_TYPE_ARP) {
/* Start from Ethernet header */
arpPkt_t *arp = (arpPkt_t *)l3_hdr;


arp->hrd = htons(1);
arp->pro = htons(ETHER_TYPE_IPv4);
arp->hln = ETHER_ADDR_LEN;
arp->pln = 4;


/* FIXME make request/reply operation selectable by user */
arp->op = htons(2);


ether_addr_copy(&pkt->eth_src_addr,
(struct ether_addr *)&arp->sha);
arp->spa._32 = htonl(pkt->ip_src_addr.addr.ipv4.s_addr);


ether_addr_copy(&pkt->eth_dst_addr,
(struct ether_addr *)&arp->tha);
arp->tpa._32 = htonl(pkt->ip_dst_addr.addr.ipv4.s_addr);
}

这里主要是根据类型填充首部结构,其中pktgen_tcp_hdr_ctor()填充ip首部和tcp首部,return l3_hdr,然后在调用pktgen_ipv4_ctor()填充ip header对于pktgen_range_ctor(), 主要是根据range_info_t结构体重的最大最下值以及步长递增地修改首部结构。

从上面可以看书pktgen不只是产生udp类型的数据包,也可以产生其他类型的数据包,但是只是产生了流量,而没有payload。

pktgen-dpdk发送pcap包

发送pcap数据包需要使用 -s:[pcap.filepcath]参数。

static int pktgen_parse_args(int argc, char **argv)
{
....
case 's': /* Read a PCAP packet capture file (stream) */
port = strtol(optarg, NULL, 10); //将字符串port转换为长整型port
p = strchr(optarg, ':'); // ++p指向待发送的pcap文件
if ( (p == NULL) ||
(pktgen.info[port].pcap =
_pcap_open(++p, port)) == NULL) {
pktgen_log_error(
"Invalid PCAP filename (%s) must include port number as P:filename",
optarg);
pktgen_usage(prgname);
return -1;
.......

}

此处p指向pcap文件(此处为绝对路劲)

pcap_info_t *
_pcap_open(char *filename, uint16_t port)
{
pcap_info_t *pcap = NULL;

if (filename == NULL) {
printf("%s: filename is NULL\n", __FUNCTION__);
goto leave;
}

pcap = (pcap_info_t *)rte_malloc("PCAP info",
sizeof(pcap_info_t),
RTE_CACHE_LINE_SIZE);
if (pcap == NULL) {
printf("%s: malloc failed for pcap_info_t structure\n",
__FUNCTION__);
goto leave;
}
memset((char *)pcap, 0, sizeof(pcap_info_t));

pcap->fd = fopen((const char *)filename, "r");
if (pcap->fd == NULL) {
printf("%s: failed for (%s)\n", __FUNCTION__, filename);
goto leave;
}

if (fread(&pcap->info, 1, sizeof(pcap_hdr_t),
pcap->fd) != sizeof(pcap_hdr_t) ) {
printf("%s: failed to read the file header\n", __FUNCTION__);
goto leave;
}

/* Default to little endian format. */
pcap->endian = LITTLE_ENDIAN;
pcap->filename = strdup(filename);

/* Make sure we have a valid PCAP file for Big or Little Endian formats. */
if ( (pcap->info.magic_number != PCAP_MAGIC_NUMBER) &&
(pcap->info.magic_number != ntohl(PCAP_MAGIC_NUMBER)) ) {
printf("%s: Magic Number does not match!\n", __FUNCTION__);
fflush(stdout);
goto leave;
}

/* Convert from big-endian to little-endian. */
if (pcap->info.magic_number == ntohl(PCAP_MAGIC_NUMBER) ) {
printf(
"PCAP: Big Endian file format found, converting to little endian\n");
pcap->endian = BIG_ENDIAN;
pcap->info.magic_number = ntohl(pcap->info.magic_number);
pcap->info.network = ntohl(pcap->info.network);
pcap->info.sigfigs = ntohl(pcap->info.sigfigs);
pcap->info.snaplen = ntohl(pcap->info.snaplen);
pcap->info.thiszone = ntohl(pcap->info.thiszone);
pcap->info.version_major = ntohs(pcap->info.version_major);
pcap->info.version_minor = ntohs(pcap->info.version_minor);
}
_pcap_info(pcap, port, 0);

return pcap;

leave:
_pcap_close(pcap);
fflush(stdout);

return NULL;
}

在 pkgen_config_ports()中

			/* Setup the PCAP file for each port */
if (pktgen.info[pid].pcap != NULL)
if (pktgen_pcap_parse(pktgen.info[pid].pcap, info, q) == -1)
pktgen_log_panic("Cannot load PCAP file for port %d", pid);
/* Find out the link speed to program the WTHRESH value correctly. */
pktgen_get_link_status(info, pid, 0);

其中调用了pktgen_pcap_parse(),在里面读取pcap文件数据的函数是_pcap_read()

size_t
_pcap_read(pcap_info_t *pcap,
pcaprec_hdr_t *pHdr,
char *pktBuff,
uint32_t bufLen)
{
do {
if (fread(pHdr, 1, sizeof(pcaprec_hdr_t),
pcap->fd) != sizeof(pcaprec_hdr_t) )
return 0;

/* Convert the packet header to the correct format. */
_pcap_convert(pcap, pHdr);

/* Skip packets larger then the buffer size. */
if (pHdr->incl_len > bufLen) {
(void)fseek(pcap->fd, pHdr->incl_len, SEEK_CUR);
return pHdr->incl_len;
}

return fread(pktBuff, 1, pHdr->incl_len, pcap->fd);
} while (1);
}

然后调用dpdk的rte_mempool_create(),可以看到每次只能发送一个pcap文件,如果需要发送多个pcap文件,则需要解析多个pcap文件路劲,赋值给pcap.fd(可以使用缓存预先存储)。

十、MTCP

MTCP(Many-to-Many TCP)是一种基于用户态的高性能TCP/IP协议栈,主要用于数据中心、云计算等高吞吐量和低延迟的网络应用。相比于传统内核态协议栈,MTCP具有更低的上下文切换开销、更少的锁竞争以及更好的可扩展性和灵活性。MTCP支持多线程套接字API,并提供了类似POSIX socket API的接口,可以很方便地替换标准socket API使用。此外,MTCP还提供了丰富的工具来帮助用户进行网络应用程序开发和调试,如Packet Generator、Flow Monitor等等。

所以说在mTCP的源码文件中是直接附带了一个DPDK的。总的说来,官方的安装步骤大致分成三部分:

  1. 检查安装该有的库

  2. 安装dpdk环境

  3. 安装mTCP环境

接下来详细叙述安装步骤

相关库的安装以及检查,linux头文件的检查:

- libdpdk (Intel's DPDK package*)
- libnuma
- libpthread
- librt
- libgmp (for DPDK/ONVM driver)
- For Debian/Ubuntu, try apt-get install linux-headers-$(uname -r)$

注意,这些库的名字可能在不同的系统上略有不同,可能是"-dev","-devel"形式的。另外千万注意检查是否存在运行内核版本一致的linux头文件。

DPDK安装与设置,这儿有个坑。mtcp自带的用于安装dpdk的脚本,相关命令都是sudo开头的,你的系统上可能没有安装sudo这个命令的,另外如果你是在root权限下的话,可能由于sudoers文件的配置原因,也是无法使用这个命令的。在执行脚本之前,确保能够顺利执行sudo命令。另外需要注意的是,在这儿直接使用mtcp自带的dpdk,可以进入mtcp/dpdk的文件夹,在里面直接使用dpdk的脚本来安装环境,具体过程可以参考dpdk官方文档。

Download dpdk submodule
git submodule init
git submodule update

进入dpdk文件夹,安装dpdk,推荐参考dpdk官方文档。主要参考其中的第3部分与第5部分。

创建一个单独的build文件夹,在dpdk目录下执行 meson 。

cd build
ninja
ninja install
ldconfig

注意最后两条命令需要root身份执行。至此dpdk安装完毕,可以去检查一下是否存在一个 kmod目录。

接下来安装相关的驱动,因为dpdk会接管网卡,使用自己的驱动的。此处可以有两个选择uio_pci_generic以及igb_uio.ko。我们选择后者,因为是mTCP中要求的。

sudo modprobe uio
sudo insmod kmod/igb_uio.ko

至此dpdk部分已经安装完毕。可以使用ifconfig命令观察一下网卡情况,记一下那张以太网的网卡名称,可能是eth0,也可能是eno1类似的名字。

最后设置DPDK的环境,可以使用dpdk里面附带的脚本来操作,mtcp/dpdk/usertools/dpdk-devbind.sh用于网卡的绑定,先使用命令/dpdk-devbind.sh --status 查看当前的网卡状态,应该会得到类似如下的信息:

其中需要注意的就是Network devices using kernel driver一栏,我们需要将相关的网卡驱动卸载掉,换成dpdk的驱动,让dpdk来接管网卡。可以看到下面有一些重要信息,其中00:1f.6是网卡的PCI总线地址,eno1是其名,e1000e是其正在使用的驱动,igb_uio是dpdk的驱动,我们等下就是要将这个驱动装上去的。特别注意的是后面还有一个Active,这说明当前机器正在使用这个网卡,如果想要换驱动的话,需要先将这个网卡卸下来。如果你是使用的ssh远程登陆的话,而且你的机器只有这一张网卡的话,那么卸载网卡后你就没有办法登陆机器了,所以卸载之前慎重。

  • 1. ifconfig eno1 down 网卡卸载了

  • 2. ./dpdk-devbind.sh --bind=igb_uio 00:1f.6 绑了新驱动了

1. 最后我们使用mtcp自带的工具来注册以太网端口

./setup_mtcp_dpdk_env.sh

注意选择register the Ethernet ports项,我的版本上是24,你的版本可能不同的。然后选择选项退出这个脚本,我的版本是35选项,你的可能不同。

mTCP环境安装,主要参考官方文档:

sudo ifconfig dpdk0 x.x.x.x netmask 255.255.255.0 up
export RTE_SDK=
export RTE_TARGET=x86_64-native-linuxapp-gcc
./configure --with-dpdk-lib= R T E S D K / RTE_SDK/RTE
S

DK/RTE_TARGET
make
  • 至此安装完毕,可以最后做一些检查工作。

  • autoreconf -ivf ,观察输出是否有error

  • ifconfig 检查是否有dpdk0这张网卡

  • 检查mtcp/lib目录下的libmtcp.a文件

  • 检查mtcp/include下是否有头文件

  • 检查apps/example下是否有二进制文件

十一、Butterfly – 连接虚拟机

Butterfly 连接虚拟机 (VM) 并控制其流量。

每个 VM 流量都包含在特定的VXLAN 网络中,并且流量由(EC2/Openstack-like)安全组过滤。

安全组可以应用于任何 VM 接口,并包含一个简单的网络规则列表(默认丢弃流量)。

虚拟网卡

在 Butterfly 中,虚拟 NIC(或 vnic)使您能够通过 vhost-user 向 Qemu VM 添加虚拟网络接口。每个 vnic 都有一个 24 位的网络 ID,称为 VNI。如果两个具有相同 VNI 的 vnic 位于不同的物理主机上,Butterfly 会通过 VXLAN 封装 VM 数据包,并将它们发送到相应的物理主机。一旦收到,数据包将被解封装并路由到它们的最终目的地。使用相同 VNI 创建的所有 vnic 都位于同一网络上。如果具有相同 VNI 的两个 vnic 位于同一物理主机上,则数据包不会退出到物理网络。

Butterfly 旨在使用专用DPDK端口连接到物理网络 。它允许 Butterfly 在使用物理 NIC 卸载功能时在 VM 之间具有非常低的延迟。

对于 VM 到 VM 通信,不会发生校验和和分段,因为数据包不会在物理网络上传输。这使 Butterfly 能够在 VM 之间进行高速和低延迟的通信。

示例:在 vni "1337" 上创建新的 vnic "vnic_1":

butterfly nic add --ip 42.0.0.1 --mac 52:54:00:12:34:01 --vni 1337 --id vnic_1

数据筛选

使用 Butterfly 中的集成防火墙(NetBSD 的 NPF)为每个 vnic过滤 VM 流量。过滤规则根据其安全组中包含的规则应用于每个 VM 。一个 vnic 可以使用多个安全组,一个安全组可以由多个 vnic 使用。当一个 vnic 使用多个安全组时,规则会累积。安全组包含要允许的规则列表(默认策略是阻止)和成员列表(IP 地址)。

Butterfly 规则主要由协议/端口和允许的源描述。此源可以是 CIDR 块安全组的成员。

示例:在“mysg”安全组中添加一条规则,允许 22 端口上的 TCP 协议中的 42.0.3.1:

butterfly sg rule add mysg --ip-proto tcp --port 22 --cidr 42.0.3.1/32

示例:在“mysg”安全组中添加一条规则,允许“users”安全组成员在80端口使用TCP协议:

butterfly sg rule add mysg --ip-proto tcp --port 80 --sg-members users

注意:当一个或多个 vnic 使用的安全组被修改时,附加到每个受影响的 VM 的防火墙规则会重新加载。

使用

Butterfly 是一个可以通过网络 API 控制的守护进程。

它与客户端打包在一起,主要允许您添加/删除/列出 vnic 和安全组。

您当然可以直接编写对 Butterfly API 的调用。API 消息传输基于ZeroMQ,消息以Protobuf 格式编码。查看协议 以获取更多详细信息。

下面是一个 Butterfly 示例,其中 6 个 VM 隔离在三个网络(VNI 42、51 和 1337)中。

Butterfly 绑定一个专用网卡来发送/接收 VXLAN 数据包,并绑定一个套接字(默认:tcp)来监听对其 API 的查询。如果您使用 DPDK 兼容卡,您将无法通过它访问 API。

您可以使用几行客户端调用来构建此配置:

butterfly nic add --ip 42.0.0.1 --mac 52:54:00:12:34:01 --vni 42 --id vnic_1
butterfly nic add --ip 42.0.0.1 --mac 52:54:00:12:34:01 --vni 51 --id vnic_2
butterfly nic add --ip 42.0.0.2 --mac 52:54:00:12:34:02 --vni 51 --id vnic_3
butterfly nic add --ip 42.0.0.3 --mac 52:54:00:12:34:03 --vni 51 --id vnic_4
butterfly nic add --ip 42.0.0.1 --mac 52:54:00:12:34:01 --vni 1337 --id vnic_5
butterfly nic add --ip 42.0.0.2 --mac 52:54:00:12:34:02 --vni 1337 --id vnic_6

提示:如果您想查看图形的外观:运行butterfly status并复制webgraphviz.com 中的点图

您可以随时编辑安全组,这会自动更新 vnics 过滤。在下面的示例中,我们创建了一个新规则,以允许 http 协议中的所有人,并要求一些 vnic 使用此安全组。

butterfly sg add sg-web
butterfly sg rule add sg-web --ip-proto tcp --port 80 --cidr 0.0.0.0/0
butterfly nic sg add vnic_1 sg-web
butterfly nic sg add vnic_2 sg-web

注意:Butterfly API 使用幂等性,这意味着两次调用应该产生相同的结果。

十二、OpenNet开源网络库

OpenNet是一个高性能的开源网络库,由中国电信研究院开发。它提供了一组可扩展、可重用和易于使用的网络编程接口,支持TCP、UDP、HTTP等协议。OpenNet采用事件驱动模型,基于epoll或kqueue实现高并发网络通信,在处理海量并发连接时表现出色,被广泛应用于互联网服务端开发领域。

十三、NetBricks

NetBricks是一个高性能的网络函数框架,由斯坦福大学研究团队开发。它提供了一组可扩展、可重用和易于使用的网络编程接口,支持TCP、UDP等协议。NetBricks采用数据包处理管线(packet processing pipeline)模型,基于DPDK实现高速数据包处理,在网络功能虚拟化(Network Function Virtualization, NFV)、边缘计算(Edge Computing)等领域有广泛应用。

十四、F-Stack

F-Stack是一个基于DPDK(Data Plane Development Kit)的高性能网络框架,由中国国内开发团队研发。它提供了一组可扩展、高效和易用的网络编程接口,支持TCP/IP协议栈和UDP等协议,具有零拷贝(zero copy)、多核并行处理、CPU亲和性等特性。F-Stack被广泛应用于虚拟化环境、大规模互联网服务器集群等场景中。

F-Stack的主要特点包括:

  1. 支持DPDK:利用DPDK提供的零拷贝技术来避免数据复制带来的性能损失;

  2. 多核并行处理:通过利用多核心CPU实现同时处理多个连接以及发送/接收数据;

  3. 高效:使用预分配内存池技术来减少动态内存分配操作以及减轻垃圾回收压力;

  4. 简单易用:提供了面向事件驱动编程模型,简化了网络编程流程;

  5. 兼容POSIX标准:可以直接使用POSIX标准函数进行开发。

十五、ThunderX NFV

hunderX NFV是卡普林公司(Cavium)推出的一款面向网络功能虚拟化(NFV)场景的处理器,基于ARMv8架构。它具有高性能、低功耗和可扩展性等优势,适合用于构建大规模的虚拟化网络环境。

ThunderX NFV采用了多核心设计,每个核心都可以支持多线程,最多可以实现96个物理核心和192个虚拟核心。此外,它还集成了丰富的硬件加速引擎,例如加密/解密、压缩/解压、流量管理等,并且支持RDMA和SR-IOV等新型技术。

除了硬件方面的优势外,ThunderX NFV还提供了全套的软件生态系统。它可以兼容Linux操作系统,并且支持OpenStack、DPDK和OPNFV等开源平台。此外,卡普林公司还提供了完整的SDK和工具链,使得用户可以轻松地进行应用开发和调试。

十六、DPVS

DPVS(Direct Project Virtual Server)是一个高性能、轻量级的四层负载均衡软件,由中国电信开发。它基于Linux内核实现,可以在多个服务器之间分配并平衡网络流量,以提高应用的可靠性和可扩展性。

DPVS支持多种负载均衡算法,例如轮询、最小连接数和源地址哈希等。它还支持会话保持和健康检查等功能,可以确保请求被正确地路由到目标服务器,并避免单个服务器出现过载情况。此外,DPVS还可以与其他网络设备集成使用,例如防火墙和VPN等。

DPVS采用了用户态数据包处理技术,在数据包转发时无需进入内核空间进行处理。这种设计使得DPVS具有卓越的性能和吞吐量,并且能够快速响应高峰期的流量负载。同时,DPVS还支持CPU亲和性调度、NUMA感知和动态配置等特性,以优化系统资源利用率。

编译安装

虽然github官方上说明不再支持dpdk-stable-20.11.1之前的版本了,但是目前新分支还出与开发阶段,我没有编译通过。所以我用的版本还是dpdk18.11.2 + dpvs1.8. 首先安装dpvs1.8版本在github上的官方介绍一样下载1.8版本的dpvs源代码。

$ git clone https://github.com/iqiyi/dpvs.git
$ cd dpvs

下不来的话可以直接去码云网站https://gitee.com 下载release包。然后进到dpvs目录,再下载dpdk源码:

$ wget https://fast.dpdk.org/rel/dpdk-20.11.1.tar.xz   # download from dpdk.org if link failed.
$ tar xf dpdk-20.11.1.tar.xz

接下来打补丁,在打补丁之前我们先来安装一些工具包防止在后面的编译过程中报错,有的话可以跳过:

yum install  python3 (Python 3.5 or later.)	
pip3 install meson ninja //Meson (version 0.49.2+) and ninja
pip3 install pyelftools 或者yum install pyelftools
yum install -y popt-devel automake libnl3 libnl3-devel openssl openssl-devel numactl kernel-devel libpcap-devel unzip patch numactl-devel

另外,注意安装gcc前要安装和内核版本匹配的kernel-devel, 查看 ls /usr/src/kernels/, 并且 ldd --version 查看glibc版本要求 glibc >= 2.7(拜托2.17 大于 2.7)

编译dpdk

在编译dpvs之前要先编译DPDK:

cd dpvs-1.8/
cp patch/dpdk-stable-18.11.2/*.patch dpdk-stable-18.11.2/
cd dpdk-stable-18.11.2/
patch -p1 < 000
...
make config T=x86_64-native-linuxapp-gcc
make
export RTE_SDK=$PWD
export RTE_TARGET=build

echo 8192 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 8192 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
// 如果很多个numa节点,可以用如下命令:
for i in {0..7};do echo 8192 > /sys/devices/system/node/node$i/hugepages/hugepages-2048kB/nr_hugepages;done

mkdir /mnt/huge
mount -t hugetlbfs nodev /mnt/huge
grep Huge /proc/meminfo
# 需要开机自动挂载的话可以在
$ echo "nodev /mnt/huge hugetlbfs defaults 0 0" >> /etc/fstab

modprobe uio
cd dpdk-stable-18.11.2
insmod build/kmod/igb_uio.ko
insmod build/kmod/rte_kni.ko carrier=on
./usertools/dpdk-devbind.py --status
// 172.18.8.129: eno4 100.200.0.129 30:fd:65:32:e8:c3 0000:1a:00.3
// 172.18.8.129: eno1 30:fd:65:32:e8:c0 0000:1a:00.0
ifconfig eno4 down
./usertools/dpdk-devbind.py -b igb_uio 0000:1a:00.3
./usertools/dpdk-devbind.py --status

./usertools/dpdk-devbind.py -u 0000:1a:00.0
./usertools/dpdk-devbind.py -b i40e 0000:1a:00.0

编译dpvs

编译dpvs比较简单,只需要配置export PKG_CONFIG_PATH=/opt/sj/dpvs-1.8/dpdk-stable-18.11.2/dpdklib/lib64/pkgconfig/libdpdk.pc 然后到dpvs目录下编译即可

cd ..
make // 报 inline 函数未定义的错误,需要在 src/Makefile CFLAGS 参数最后-mcmodel=medium之前加上 -fgnu89-inline
make install

运行DPVS

拷贝./conf/下合适的配置文件到 /etc/dpvs.conf
总结一下,我遇到过的问题:

  1. 运行./bin/dpvs 如果报段错误退出则需要调整配置文件中的pktpool_size,我猜测是因为这个值太大了导致内存不足。可以把这个值改小一些,并且屏蔽一些cpu,减少消耗。我这样操作之后能跑起来了。

  2. 我遇到另外一个coredump退出的情况,使用1中的方法不生效。于是我打开src/Makefile中的DEBUG开关,用gdb跑dpvs,因为不知道断点在哪里只能run,得到的信息如下,基本没用

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff440d700 (LWP 47658)]
0x00000000006162f9 in conn_term_lcore ()
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7.x86_64 libgcc-4.8.5-16.el7.x86_64 li.0.2k-24.el7_9.x86_64 zlib-1.2.7-19.el7_9.x86_64

于是,我又打开了/etc/dpvs.conf中的log_level为DEBUG,再次启动程序得到有效信息如下所示:

EAL: rte_mem_virt2phy(): cannot open /proc/self/pagemap: Too many open files

应该是系统允许最大打开文件数太小所致,用ulimit -a查看果然只有1024,用ulimit -n 1048576改成较大的数量,再次启动DPVS进程这次可以了。

3. 报错“DPVS_MAX_SOCKET is smaller than system numa nodes!”,查看源码发现是numa节点数小于系统常量DPVS_MAX_SOCKET,再次查找这个DPVS_MAX_SOCKET,发现它是在src/config.mk文件中定义并被编译到源码里,因此需要修改成numa节点数(numactl --hardware查看numa节点数)并再次编译。

十七、Vhost-user-net

Vhost-user-net是一种虚拟化网络设备技术,它通过将网络设备抽象为一个用户态程序来实现高性能和灵活性。具体来说,它使用了vhost-user协议来进行数据交换,从而避免了内核空间与用户空间之间的上下文切换和数据复制等开销。

在Vhost-user-net中,vhost-user程序扮演着两个角色:vhost-user客户端和vhost-user服务器。其中,vhost-user客户端运行在虚拟机中,负责接收和发送网络数据包。而vhost-user服务器则运行在主机上,负责处理网络数据包并将其转发到物理网卡或其他虚拟机中。

Vhost-user-net可以与多种虚拟化管理器和操作系统集成使用,例如QEMU、KVM、Xen等。通过这些软件的支持,Vhost-user-net可以实现多租户隔离、动态配置、快速迁移等特性,并且支持多种协议(如TCP/IP、UDP/IP、VLAN等)和各种网络功能(如防火墙、NAT、负载均衡等)的部署。

十八、OpenNFP

OpenNFP(Open Network Function Pipeline)是一种基于FPGA实现的高性能网络功能虚拟化(NFV)框架。它提供了一个可编程的数据平面,并且允许将各种网络功能模块组合在一起,以构建自定义的网络应用程序。

OpenNFP具有以下几个特点:

  1. 基于硬件:使用FPGA实现高速数据包处理和转发,可以实现比传统软件路由器更高的性能和吞吐量。

  2. 可编程:OpenNFP提供了一个可编程的数据平面,可以通过修改硬件逻辑来支持不同类型的协议、报文格式和网络功能。

  3. 模块化:OpenNFP支持将多个网络功能模块组合在一起,形成一个完整的网络应用程序。这些模块可以独立开发、测试和部署,从而提高了开发效率和灵活性。

  4. 开放源代码:OpenNFP是一个开源项目,任何人都可以获取源代码并进行修改、定制或扩展。

十九、VCL

VCL (Virtual Computing Lab) 是一种基于云计算技术的虚拟化实验平台,用于支持远程教育和在线实验。它允许用户通过网络连接到云端的虚拟机上,进行各种软件开发、测试和实验等操作。

VCL 的主要特点如下:

  1. 高度灵活:VCL 支持多种操作系统和软件环境的虚拟化,并提供了多种配置选项和管理工具,可以满足不同用户需求的定制化要求。

  2. 易于使用:VCL 提供了简单易用的 Web 界面,用户可以通过几个简单步骤就能创建自己的虚拟机环境,并轻松地进行远程访问。

  3. 安全性高:VCL 采用严格的身份认证和授权机制,确保只有合法用户才能访问虚拟机资源;同时还提供了安全审计和监控功能,及时检测并防范任何潜在威胁。

  4. 成本低廉:由于 VCL 是基于云计算技术构建的,在硬件设备、网络带宽等方面都具有较高的资源利用效率,从而降低了成本。

二十、libmoon

ibmoon是一个基于DPDK和LuaJIT的高性能网络应用框架,主要用于开发高速数据包处理、网络测试和流量生成工具。

Libmoon的主要特点如下:

  1. 基于DPDK:Libmoon使用DPDK提供的高性能数据包I/O功能,支持多个物理或虚拟网卡,并可通过CPU亲和性和NUMA优化等手段进一步提升性能。

  2. 基于LuaJIT:Libmoon使用LuaJIT作为脚本引擎,使得用户可以通过简单易用的Lua语言编写复杂的网络应用程序,从而大大降低了开发难度。

  3. 高效灵活:Libmoon采用事件驱动模型,具有高效、灵活、可扩展等特点。它可以实现自定义协议解析、数据包过滤、路由选择、负载均衡等功能,并支持多种流量生成模式。

  4. 易于部署:Libmoon提供了方便易用的部署工具,并与Docker等容器技术兼容。用户只需在任意硬件平台上安装DPDK和LuaJIT即可运行。

目前Libmoon已被广泛应用于各种网络应用场景,如大规模分布式系统测试、DDoS攻击仿真、智能路由器设计等。它为用户提供了高度灵活、易于使用和高性能的网络应用开发框架,是一种非常有价值的技术。

二十一、DPDK项目实战

那什么样的程序员适合学习dpdk技术?

对于dpdk来说,它更看重计算机原理和底层技术,和业务上的关联不大,适合:

  1. 计算机相关专业,对底层技术原理感兴趣的应届生;

  2. 对网络原理、dpdk、高性能网络开发的在职工程师;

  3. 工作中从事dpdk/vpp/ovs等开发的工程师;

  4. 有良好的计算机原理和底层技术基础,想往互联网行业发展的桌面开发,c++开发工程师等等。

这里给大家推荐零声教育全网独家的【dpdk-网络协议栈-vpp-OVS-DDos-虚拟化技术课程体系,通过32个项目案例,2W+行手写代码,全面解析4个dpdk技术方向:

21.1DPDK网络专栏

(1)dpdk基础知识

  • 1.多队列网卡,vmxnet/e1000

  • 2.igb_ uio与vfio模块

  • 3.kni模块

  • 4.hugepage的理解

  • 5.零拷贝

  • 6.dpdk与netmap区别

  • 7.dpdk的工作环境

(2)网络协议栈

  • 1.dpdk-arp

  • 2.netsh静态arp表设置

  • 3.dpdk-icmp

  • 4.udp协议格式分析

  • 5.udp协议sendto, recvfrom实现

  • 6.dpdk-ip

  • 7.dpdk-tcp

  • 8.tcp协议栈bind,listen, accept实现

  • 9.tcp协议栈recv, send, close的实现

  • 10.tcp三次握手实现

  • 11.tcp四次挥手实现

  • 12.tcp acknum与seqnum的确认机制实现

  • 13.tcp的并发连接设计

  • 14.epoll并发的实现

  • 15.tcp协议栈与epoll之间的回调实现

(3)dpdk组件项目

  • 1.dpdk-ac

  • 2.dpdk-kni

  • 3./dev/ kni的原理分析

  • 4.kni_ dev的流程

  • 5.kni的t缓冲区,rx缓冲区

  • 6.kni的用户空间与内核空间映射

  • 7.mbuf如何转化为kernel的sk_ buff

  • 8.dpdk- timer

  • 9.bpftrace的使用

  • 10.dpdk- bpf源码流程

(4)dpdk经典项目

  • 1.dpdk- dns

  • 2.dpdk- gateway

  • 3.dpdk-ddos熵计算源码

  • 4.ddos attach检测精确度调试

  • 5.ddos attach测试T具hping3

  • 6.布谷鸟hash原理与使用

21.2储存技术专栏

(1)高效磁盘io读写spdk(C)

  • 1.存储框架spdk,为技术栈打开扇存储的大门

  • 2.spdk运行环境与vhost

  • 3.NVMe与PCl的关系

  • 4.手把手实现spdk_ server

  • 5.nvme与pcie以及手写nvme读写操作

  • 6.bdev与blob之间的关系

  • 7.实现blob异步读写

  • 8.blobstore的读写操作实现与rpc的关系

  • 9.fio性能测试性能对比libaio,io_ uring,psync

  • 10.fio plugin工作流程

  • 11.fio plugin开发

(2)spdk文件系统的实现

  • 1.文件系统功能拆解

  • 2.spdk_ env_ init与spdk_ app init的差别

  • 3.spdk_ _thread_ poll实现rpc回调

  • 4.fs_ operations结构体定义

  • 5.file_ operations结构体定义

  • 6.dir_ operations结构体定义

  • 7.syscall的hook实现

  • http://8.io内存管理

  • 9.基数树对文件系统内存管理

  • 10.spdk_ blob的open,read,write,close

  • 11.测试用例与调试入口函数

(3)spdk kv存储的实现

  • 1.KV存储拆解Set, Get, Mod, Del

  • 2.app/.a库/so库对于kv存储的选择

  • 3.bdev与blob对于kv存储的选择

  • 4.kv service启动blob资源操作

  • 5.kv service关闭blob资源回收

  • 6.kv service接口set,get,modify,delete

  • 7.kv遍历与查找实现

  • 8.page存储chunk的管理

  • 9.pagechunk的get与put

  • 10.page单查找与多页查找

  • 11.btree, artree, hashmap,radixtree, rbtree之间的选择

  • 12.slab的实现

  • 13.slab分配slot与释放slot

  • 14.为kv加上conf文件

  • 15.测试用例与性能测试

21.3安全与网关开发专栏

(1)可扩展的矢量数据包处理框架vpp(c/c++)

  • 1.vpp命令详解

  • 2.mac/ip转发plugin

  • 3.load_ balance plugin

  • 4.flowtable plugin

  • 5.vpp源码之间的差异

  • 6.多网卡数据接收与转发

  • 7.解决plugin编译加载

  • 8.vpp启动load so的流程

  • 9.vpp的结构体vlib_ _main实现分析

  • 10.vpp的结构体vnet_ main

  • 11.vector的操作实现

  • 12.vpp vcl库与LD_ PRELOAD实现分析

  • 13.vcl原理讲解

  • 14.vcl tcpserver实现原理

  • 15.vcl tcpclient实现原理

  • 16.vcl与iperf3的客户端与服务器

  • 17.vcl与nginx的wrk性能测试

  • 18.vcl与haproxy的性能测试

  • 19.vpp 1801版本与vpp 2206版本

  • 20.vpp httpserver的实现源码

  • 21.vpp plugin quic源码分析

  • 22.vpp plugin hs_ app的源码

  • 23.vpp plugin rdma的实现分析

  • 24.vpp plugin loadbalance

  • 25.vpp plugin nat的源码分析

  • 26.vpp host-stack tcp协议实现

  • 27.vpp plugin的测试用例实现

(2)golang的网络开发框架nff-go(golang)

  • 1.nff-go实现的技术原理

  • 2.nff-go/low.h实现分析

  • 3.nff- go数据接收的实现

  • 4.nff-go数据发送的实现

  • 5.ipsec协议解析与strongswan的ipsec

  • 6.nff go的缺陷与不足

21.4虚拟化与云原生专栏

(1)DPDK的虚拟交换机框架OvS

  • 1.ovs编译安装,ovs核心组件内容

  • 2.ovs-vswitchd的工作原理

  • 3.ovs-vswitchd与dpdk的关系

  • 4.ovs-vsctl的网桥,网口操作

  • 5.qemu-system-x86_ 64构建多子网

  • 6.ovs与qemu数据流分发

  • 7.ovs搭建docker跨主机通信

  • 8.ovsdb server与ovsdb协议

  • 9.json-rpc为控制面提供开发

  • 10.ovs-tcpdump/ovs-l3ping

  • 11.OvS 4种数据路径

  • 12.VXL AN数据协议

  • 13.ovs流量统计

(2)高性能4层负载均衡器DPVS

  • 1.dpvs的技术组件与功能边界

  • 2.lvs+keepalived配置高可用server

  • 3.dpvs与|lvs+ keepalived的关系

  • 4.dpvs.conf的配置文件

  • 5.dpvs的FNat/NAT/SNAT模式

  • 6.dpvs的DR模式

  • 7.dpvs的tun模式

  • 8.通过quagga配置ospf

  • 9.dpvs的tc流控操作与源码实现

  • 10.dpvs代码架构分析

  • 11.dpvs测试用例ipset, tc,mempool

21.5测试工具专栏

(1)perf3

  • 1.vpp vcl的perf3接口hook

  • 2.perf3测网络带宽

  • 3.tcp吞吐量测试

  • 4.udp丢包与延迟测试

  • 5.json测试结果输出

(2)TRex

  • 1.TRex的运行原理

  • 2.TRex与dpdk

  • 3.构建TRex测试系统

  • 4.t-rex -64- debug gdb调试

  • 5.bg-sim- 64模拟单元测试

  • 6.YAML文件编写

  • 7.流编排与自动化框架

  • 8.报文变量设置

(3)dpdk-pktgen

  • 1.pktgen命令讲解

  • 2.default.cfg配置文件分析

  • 3.120M bits/s的转发速率

(4)fio

  • 1.ioengine的实现

  • 2.ioengine_ ops的分析

  • 3.iodepth的分析

  • 4.spdk_ nvme的fio分析

  • 5.spdk_ bdev的fio分析

  • 6.spdk_ blob的ioengine实现

  • 7.psync,io_ uring, libaio性能对比

21.6性能测试专栏

(1)性能指标

  • 1.吞吐量bps

  • 2.拆链/建链pps

  • 3.并发

  • 4.最大时延

  • 5.最小时延

  • 6.平均时延

  • 7.负载

  • 8.包速fps

  • 9.丢包率

(2)测试方法

  • 1.测试用例

  • 2.vpp sandbox

  • 3.perf3灌包

  • 4.rfc2544

还不熟悉的朋友,这里可以先领取一份dpdk新手学习资料包(入坑不亏):

DPDK技术交流群(1106675687):正在跳转

想系统学习,也可以扫码备注咨询,领取粉丝专属福利优惠券。

学习后可从事开发岗位:

  1. 高级网络开发工程师

  2. DPDK开发工程师

  3. 云产品研发工程师

  4. 云基础开发工程师

  5. 高性能优化工程师

  6. SDN开发工程师

  7. NFV开发工程师

写在最后:

Dpdk作为目前在互联网越来越流行的底层技术,受到越来越多的优秀互联网公司的欢迎与使用。但对于不少程序员来说可能只是见过或是听过,并没有深入的学习和使用。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
研发干货 | LS1028A 网络应用测试
fedora23忘记root密码怎么修改?
云数据中心网络分析及安全技术方案实践
美团云的网络架构演进之路
可编程网卡芯片在滴滴云网络的应用实践
当SDN 遇到物联网
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服