【玩转Lighthouse】网络性能调优 -- 工具篇

2022-04-15 21:36:12 浏览数 (1)

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 

0 人点赞