TestPMD
常用的网络测试工具--Iperf、Netperf 、MZ
但是,netperf 测试虚拟机的极限性能时, 内核协议栈对网络性能损耗较大,此时 ,可以用 DPDK 的testpmd屏蔽虚拟机内核协议栈的差异,获取实例的真实网络性能
代码语言:txt复制/x86_64-native-linuxapp-gcc/build/app/test-pmd/testpmd -w 0000:04:02.0 -d ./x86_64-native-linuxapp-gcc/lib/librte_pmd_virtio.so.1.1 -- --txd=128 --rxd=128 --txq=32 --rxq=32 --nb-cores=16 --forward-mode=txonly --txpkts=64 --eth-peer=0,fa:16:3e:01:01:40 -i
./x86_64-native-linuxapp-gcc/build/app/test-pmd/testpmd -w 0000:04:02.0 -d ./x86_64-native-linuxapp-gcc/lib/librte_pmd_virtio.so.1.1 -- --txd=128 --rxd=128 --txq=32 --rxq=32 --nb-cores=16 --forward-mode=rxonly -i
查看当前配置参数:
代码语言:txt复制meson configure
主要的配置参数:
示例1:
代码语言:txt复制meson -Denable_kmods=true -Dmax_lcores=128 -Dmachine=sandybridge -Ddisable_drivers=net/af_xdp,net/dpaa,net/dpaa2,net/bnx2x -Dexamples=l3fwd,l2fwd -Dwerror=false snb
ninja -C snb
示例2 reconfigure:
代码语言:txt复制meson --reconfigure -Denable_kmods=true -Dmax_lcores=128 -Dmachine=sandybridge -Ddisable_drivers=net/af_xdp,net/dpaa,net/dpaa2,net/bnx2x -Dexamples=l3fwd,l2fwd -Dwerror=false snb
ninja -C snb
ftrace
用于查看cpu是否有抢占:
(1)ftrace:
代码语言:txt复制 echo 1 > /sys/kernel/debug/tracing/events/sched/enable
代码语言:txt复制 cat /sys/kernel/debug/tracing/per_cpu/cpu1/trace |grep switch
(2)perf sched:
代码语言:txt复制 perf sched record -C 2-3 sleep 5(指定cpu)
代码语言:txt复制 perf sched latency --sort max
代码语言:txt复制 perf sched script
perf
一、perf top 分析CPU占用
1)对整体CPU分析: perf top
2)对指定进程分析cpu占用: perf top -p pid
perf top 可以看到开销高的热点函数, 如果需要更详细的调用分析,可以用perf record
二、perf record 分析函数调用
1,获取数据
代码语言:c复制//对指定进程设置采样时间和采样频率:
perf record -g -F 99 -p "pid" – sleep 60 //持续采样时间60s,采样频率99次/s
perf report //查看生产的数据,分析开销高的热点函数
2、如果觉得可视化效果不好,可以用火焰图进一步展示
代码语言:c复制1) perf script -i perf.data >perf.unfold //将生成数据解析
2)./stackcollapse-perf.pl perf.unfold > perf.folded //利用FlameGraph工具折叠符号
3)./flamegraph.pl perf.folded > perf.svg //生成svg图
或直接用一条命令:
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > perf.svg
工具获取:来自火焰图项目地址:git clone
https://github.com/brendangregg/FlameGraph.git
**PS: perf有时给出的callchain是错误的,这里简单说一下原因及解决方法:**
callchain时指函数的调用路径。通常我们也把它称为call trace。很多同学在使用perf看热点函数的调用路径时,都发现perf给出的callchain是一堆混乱的地址,或者给出的callchain根本不对。
我们先来解释一下perf获得callchain的方法:如果我们需要取callchain,内核就会在采样时保存内核栈以及用户栈中的各个函数的返回地址。对函数返回地址的获取以及对整个栈的遍历,可以通过栈底指针实现。而这个栈底指针,通常会保存在EBP寄存器中。内核也正是通过EBP获得栈底指针的。
但是,当我们利用'-O'以上的优化选项编译程序时,GCC会将栈底指针优化掉,并把EBP作为一个通用寄存器。此时,我们从EBP中读到的值就不再是栈底指针了。perf与内核获得的callchain就是错误的。
**为了解决这个问题,我们建议大家在编译应用程序的调试版本时加上编译参数“-fno-omit-frame-pointer”**。该参数使得GCC在优化程序时保留EBP的栈底指针功能。也只有在这种情况下,我们获得的callchain才是正确的。
对于优化选项“-fomit-frame-pointer”产生的优化加速比,我们后面会给出具体的说明和实验数据。但目前猜测,该选项带来的优化效果不会非常大。它在一定程度上能够减少binary文件的footprint,并带来一定的性能提升。
在最新版本的内核中,已经支持了利用libunwind获得callchain的功能。在libunwind的支持下,可以不通过EBP来获得应用程序的callchain。此时,我们可以通过如下命令执行perf:
#sudo perf top -G dwarf
#sudo perf record -g dwarf
三、perf stat 分析 cache miss
1、什么是 cache miss
缓存的命中率,是CPU性能的一个关键性能指标。我们知道,CPU里面有好几级缓存(Cache),每一级缓存都比后面一级缓存访问速度快。当CPU需要访问一块数据或者指令时,它会首先查看最靠近的一级缓存(L1);如果数据存在,那么就是缓存命中(Cache Hit),否则就是不命中(Cache Miss),需要继续查询下一级缓存。最后一级缓存叫LLC(Last Level Cache);LLC的后面就是内存。
缓存不命中的比例对CPU的性能影响很大,尤其是最后一级缓存的不命中时,对性能的损害尤其严重。这个损害主要有两方面的性能影响:
第一个方面的影响很直白,就是CPU的速度受影响。我们前面讲过,内存的访问延迟,是LLC的延迟的很多倍(比如五倍);所以LLC不命中对计算速度的影响可想而知。
第二个方面的影响就没有那么直白了,这方面是关于内存带宽。我们知道,如果LLC没有命中,那么就只能从内存里面去取了。LLC不命中的计数,其实就是对内存访问的计数,因为CPU对内存的访问总是要经过LLC,不会跳过LLC的。所以每一次LLC不命中,就会导致一次内存访问;反之也是成立的:每一次内存访问都是因为LLC没有命中。
更重要的是,我们知道,一个系统的内存带宽是有限制的,很有可能会成为性能瓶颈。从内存里取数据,就会占用内存带宽。因此,如果LLC不命中很高,那么对内存带宽的使用就会很大。内存带宽使用率很高的情况下,内存的存取延迟会急剧上升。更严重的是,最近几年计算机和互联网发展的趋势是,后台系统需要对越来越多的数据进行处理,因此内存带宽越来越成为性能瓶颈。
针对cache不命中率高的问题,我们需要衡量一下问题的严重程度。在Linux系统里,可以用Perf这个工具来测量。那么Perf工具是怎么工作的呢?
它是在内部使用性能监视单元,也就是PMU(Performance Monitoring Units)硬件,来收集各种相关CPU硬件事件的数据(例如缓存访问和缓存未命中),并且不会给系统带来太大开销。 这里需要你注意的是,PMU硬件是针对每种处理器特别实现的,所以支持的事件集合以及具体事件原理,在处理器之间可能有所不同。。具体用Perf来测量计数的命令格式如:
代码语言:c复制perf stat -e task-clock -e cycles -e context-switches -e migrations -e L1-dcache-loads,L1-dcache-misses,LLC-loads,LLC-load-misses -p pid
▲perf stat 输出解读如下
▪ task-clock
用于执行程序的CPU时间,单位是ms(毫秒)。第二列中的CPU utillized则是指这个进程在运行perf的这段时间内的CPU利用率,该数值是由task-clock除以最后一行的time elapsed再除以1000得出的。
▪ context-switches
进程切换次数,记录了程序运行过程中发生了多少次进程切换,应该避免频繁的进程切换。
▪ cpu-migrations
程序在运行过程中发生的CPU迁移次数,即被调度器从一个CPU转移到另外一个CPU上运行。
▪ page-faults
缺页。指当内存访问时先根据进程虚拟地址空间中的虚拟地址通过MMU查找该内存页在物理内存的映射,没有找到该映射,则发生缺页,然后通过CPU中断调用处理函数,从物理内存中读取。
▪ Cycles
处理器时钟,一条机器指令可能需要多个 cycles。
▪ Cache-references
cache 命中的次数。
▪ Cache-misses
cache 失效的次数。
▪ L1-dcache-load-missed
一级数据缓存读取失败次数。
▪ L1-dcache-loads
一级数据缓存读取次数。
2、如何减小cache miss?
**第一个方案,也是最直白的方案,就是缩小数据结构,让数据变得紧凑。**
这样做的道理很简单,对一个系统而言,所有的缓存大小,包括最后一级缓存LLC,都是固定的。如果每个数据变小,各级缓存自然就可以缓存更多条数据,也就可以提高缓存的命中率。这个方案很容易理解。
**第二个方案,是用软件方式来预取数据。**
这个方案也就是通过合理预测,把以后可能要读取的数据提前取出,放到缓存里面,这样就可以减少缓存不命中率。“用软件方式来预取数据”理论上也算是一种“用空间来换时间”的策略(参见第20讲),因为付出的代价是占用了缓存空间。当然,这个预测的结果可能会不正确。
**第三个方案,是具体为了解决一种特殊问题:就是伪共享缓存**。
这个方案也算是一种“空间换时间”的策略,是通过让每个数据结构变大,牺牲一点存储空间,来解决伪共享缓存的问题。
什么是伪共享缓存呢?
我们都知道,内存缓存系统中,一般是以缓存行(Cache Line)为单位存储的。最常见的缓存行大小是64个字节。现代CPU为了保证缓存相对于内存的一致性,必须实时监测每个核对缓存相对应的内存位置的修改。如果不同核所对应的缓存,其实是对应内存的同一个位置,那么对于这些缓存位置的修改,就必须轮流有序地执行,以保证内存一致性。
比如线程0修改了缓存行的一部分,比如一个字节,那么为了保证缓存一致性,这个核上的整个缓存行的64字节,都必须写回到内存;这就导致其他核的对应缓存行失效。其他核的缓存就必须从内存读取最新的缓存行数据。这就造成了其他线程(比如线程1)相对较大的停顿。
这个问题就是伪共享缓存。之所以称为“伪共享”,是因为,单单从程序代码上看,好像线程间没有冲突,可以完美共享内存,所以看不出什么问题。由于这种冲突性共享导致的问题不是程序本意,而是由于底层缓存按块存取和缓存一致性的机制导致的,所以才称为“伪共享”。
举个具体的多线程cache调优 的例子来理解:
单线程程序:
代码语言:c复制//sig.c
#include<stdio.h>
long long s=0;
void sum(long long num);
int main() {
sum(2000000000);
printf("sum is %lldn", s);
return 0;
}
void sum(long long num){
for(long long i=0; i<num; i )
s =i;
}
未经调优的多线程程序:
代码语言:c复制//mul_raw.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sched.h>
#include <pthread.h>
void* one(void*);
void* two(void*);
long long sum,sum1;
int main(){
pthread_t id1, id2;
pthread_create(&id1, NULL, one, NULL);
pthread_create(&id2, NULL, two, NULL);
pthread_join(id2, NULL);
pthread_join(id1, NULL);
sum =sum1;
printf("sum is %lldn", sum);
return 0;
}
void *one(void *arg){
long long i;
for(i=0; i<1000000000; i )
sum =i;
}
void *two(void *arg){
long long i;
for(i=1000000000; i<2000000000; i )
sum1 =i;
}
编译执行一下:
代码语言:c复制#gcc sig.c -o sig
#gcc mul_raw.c -o mul_raw -lpthread
# time ./sig
sum is 1999999999000000000
real 0m6.993s
user 0m6.988s
sys 0m0.001s
# time ./mul_raw
sum is 1999999999000000000
real 0m10.037s
user 0m18.681s
sys 0m0.000s
这就奇了,明明我们多了一个线程,反而比单线程耗时多了。这是什么缘故呢?
使用perf查看一下:
代码语言:c复制# perf stat -e task-clock -e cycles -e context-switches -e migrations -e L1-dcache-loads,L1-dcache-misses,LLC-loads,LLC-load-misses ./sig
sum is 1999999999000000000
Performance counter stats for './sig':
6791.176387 task-clock (msec) # 1.000 CPUs utilized
15,476,794,037 cycles # 2.279 GHz (80.00%)
8 context-switches # 0.001 K/sec
0 migrations # 0.000 K/sec
10,006,544,037 L1-dcache-loads # 1473.463 M/sec (80.00%)
473,734 L1-dcache-misses # 0.00% of all L1-dcache hits (40.01%)
73,321 LLC-loads # 0.011 M/sec (39.99%)
18,642 LLC-load-misses # 25.43% of all LL-cache hits (60.01%)
6.791355338 seconds time elapsed
# perf stat -e task-clock -e cycles -e context-switches -e migrations -e L1-dcache-loads,L1-dcache-misses,LLC-loads,LLC-load-misses ./mul_raw
sum is 1999999999000000000
Performance counter stats for './mul_raw':
17225.793886 task-clock (msec) # 1.899 CPUs utilized
39,265,466,829 cycles # 2.279 GHz (80.00%)
15 context-switches # 0.001 K/sec
3 migrations # 0.000 K/sec
8,020,648,466 L1-dcache-loads # 465.619 M/sec (80.00%)
98,864,094 L1-dcache-misses # 1.23% of all L1-dcache hits (40.01%)
21,028,582 LLC-loads # 1.221 M/sec (40.00%)
6,941,667 LLC-load-misses # 33.01% of all LL-cache hits (60.00%)
9.069511808 seconds time elapsed
可以明显看出数据都是 L1-dcache-loads ,但是多线程程序的L1 cache miss 比单线程还大, cycles数也明显大了。原因就是“伪共享”:
首先我们通过top -H以及增选Last used cpu发现系统一直将两个线程分别调度到两个core中,也就是保持线程不共享L1cache。而同一个core中的CPU是共享L1cache的,这部分NUMA知识详见:
https://blog.csdn.net/qq_15437629/article/details/77822040
由于sum和sum1在内存中的位置是连续的,可以想象,当线程1更改了sum并放在L1cache中(对于回写策略并不会马上写到内存中)那么这条cache line在其他的cache中都将变成无效的,也就是线程2的L1cache需要去同步线程1的cache,这将浪费大量的cycle,而且几乎每一步都要去同步这个数据,cache miss就大大提高了,耗时也就上去了。
怎么避免这个问题呢?针对产生问题的两个原因有两种解决方案:
方法一:将两个变量隔开,使其不在同一个cache line中,一个很土的办法是:将sum改为sum8,这样他们就不在一个cache line(64B)中了。这一步所做的应该是通常所讲的cache对齐,而且这种方法与硬件和内核调度无关。具有较好的可移植性。
代码语言:c复制//mul.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sched.h>
#include <pthread.h>
void* one(void*);
void* two(void*);
long long sum[8],sum1[8];
int main(){
pthread_t id1, id2;
pthread_create(&id1, NULL, one, NULL);
pthread_create(&id2, NULL, two, NULL);
pthread_join(id2, NULL);
pthread_join(id1, NULL);
sum[0] =sum1[0];
printf("sum is %lldn", sum[0]);
return 0;
}
void *one(void *arg){
for(long long i=0; i<1000000000; i )
sum[0] =i;
}
void *two(void *arg){
for(long long i=1000000000; i<2000000000; i )
sum1[0] =i;
}
编译执行如下:
代码语言:c复制# gcc mul_cacheline.c -o mul -lpthread
linux-zvpurp:/Images/zlk/test # time ./mul
sum is 1999999999000000000
real 0m3.211s
user 0m6.289s
sys 0m0.001s
linux-zvpurp:/Images/zlk/test # perf stat -e task-clock -e cycles -e context-switches -e migrations -e L1-dcache-loads,L1-dcache-misses,LLC-loads,LLC-load-misses ./mul
sum is 1999999999000000000
Performance counter stats for './mul':
6523.654091 task-clock (msec) # 1.934 CPUs utilized
14,866,840,150 cycles # 2.279 GHz (79.35%)
44 context-switches # 0.007 K/sec
4 migrations # 0.001 K/sec
8,038,748,997 L1-dcache-loads # 1232.246 M/sec (78.70%)
512,004 L1-dcache-misses # 0.01% of all L1-dcache hits (40.57%)
81,744 LLC-loads # 0.013 M/sec (40.67%)
13,354 LLC-load-misses # 16.34% of all LL-cache hits (59.56%)
3.373951529 seconds time elapsed
基本达到单线程耗时一半的目标。cache miss和cycles都下去了。
方法二:将线程绑定在同一个core中,这样由于大家共享一个cache line就不会有数据不一致的问题了。我的环境cpu0和cpu36是同一个core,代码优化如下:
代码语言:c复制#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sched.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
void* one(void*);
void* two(void*);
long long sum,sum1;
int main(){
pthread_t id1, id2;
pthread_create(&id1, NULL, one, NULL);
pthread_create(&id2, NULL, two, NULL);
pthread_join(id2, NULL);
pthread_join(id1, NULL);
sum =sum1;
printf("sum is %lldn", sum);
return 0;
}
void *one(void *arg){
long long i;
cpu_set_t mask;
CPU_ZERO(&mask); //置空
CPU_SET(0,&mask);
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
printf("set CPU affinity failue, ERROR:%sn", strerror(errno));
}
for(i=0; i<1000000000; i )
sum =i;
}
void *two(void *arg){
long long i;
cpu_set_t mask;
CPU_ZERO(&mask); //置空
CPU_SET(36,&mask);
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
printf("set CPU affinity failue, ERROR:%sn", strerror(errno));
}
for(i=1000000000; i<2000000000; i )
sum1 =i;
}
编译时要加上-D_GNU_SOURCE。实测效果并没有提升太多(可能是同一个core的开销导致?),而且这种方法需要针对机器优化,可移植性差。
代码语言:c复制# time ./mul
sum is 1999999999000000000
real 0m5.172s
user 0m10.239s
sys 0m0.000s
# perf stat -e task-clock -e cycles -e context-switches -e migrations -e L1-dcache-loads,L1-dcache-misses,LLC-loads,LLC-load-misses ./mul
sum is 1999999999000000000
Performance counter stats for './mul':
10333.513617 task-clock (msec) # 1.982 CPUs utilized
23,481,125,107 cycles # 2.272 GHz (79.95%)
23 context-switches # 0.002 K/sec
4 migrations # 0.000 K/sec
8,016,824,860 L1-dcache-loads # 775.808 M/sec (59.43%)
1,168,405 L1-dcache-misses # 0.01% of all L1-dcache hits (79.05%)
117,485 LLC-loads # 0.011 M/sec (41.07%)
36,319 LLC-load-misses # 30.91% of all LL-cache hits (59.99%)
5.213851777 seconds time elapsed
四、perf sched 分析cpu打断
PMD独占cpu轮询的场景, 如果出现性能抖动类问题,可以用perf sched分析cpu是否有打断,判断是否I层隔离没做好:
代码语言:c复制perf sched record -C 1
perf sched latency --sort max
perf sched script |grep switch
perf sched timehist