前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【玩转Lighthouse】网络性能调优 -- 工具篇

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

原创
作者头像
用户9670168
修改2022-04-15 21:36:12
8470
修改2022-04-15 21:36:12
举报
文章被收录于专栏:云计算虚拟化云计算虚拟化

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 %lld\n", 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 %lld\n", 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 %lld\n", 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 %lld\n", 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:%s\n", 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:%s\n", 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 

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • TestPMD
  • ftrace
  • perf
    • 一、perf top 分析CPU占用
      • 二、perf record 分析函数调用
        • 三、perf stat 分析 cache miss
          • 四、perf sched 分析cpu打断
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com