关于运维:系统性能分析从入门到进阶

3次阅读

共计 22067 个字符,预计需要花费 56 分钟才能阅读完成。

作者 | 勿非

本文以零碎为核心, 联合日常工作和用例, 由浅入深地介绍了性能剖析的一些办法和领会, 心愿对想理解零碎性能剖析的同学有所帮忙。

入门篇

资源角度

USE

产品跑在零碎的各种资源下面, 从系统资源的角度入门性能剖析是个不错的抉择, 咱们以业界出名大牛 Brendan Gregg 的 USE 办法开始, USE 特点就是简略无效适宜入门, 用 Brendan 的话形容 USE 的成果:

I find it solves about 80% of server issues with 5% of the effort.

USE 从系统资源的角度, 包含但不限于 CPU, 内存, 磁盘, 网络等, 关注以下 3 个方面:

• Utilization (U): as a percent over a time interval. eg, “one disk is running at 90% utilization”. 大多数状况能够正当揣测利用率高可能会影响性能

• Saturation (S): as a queue length. eg, “the CPUs have an average run queue length of four”. 资源竞争的强烈水平

• Errors (E). scalar counts. eg, “this network interface has had fifty late collisions”. Errors 绝对直观

CPU

对于 CPU, 次要关注以下指标:
• Utilization. CPU 的利用率
• Saturation. 能够是 load average, runqueue length, sched latency 等

CPU 利用率用 top 看下:

top - 17:13:49 up 83 days, 23:10,  1 user,  load average: 433.52, 422.54, 438.70
Tasks: 2765 total,  23 running, 1621 sleeping,   0 stopped,  34 zombie
%Cpu(s): 23.4 us,  9.5 sy,  0.0 ni, 65.5 id,  0.7 wa,  0.0 hi,  1.0 si,  0.0 st

CPU 利用率拆分成了更细粒度的几局部:
• us, sys, ni – 对应 un-niced user, kernel, niced user 的 CPU 利用率
• id, wa – 对应到 idle, io wait 的比例, io wait 实质上也是一种 idle, 区别在于对应 cpu 上有期待 io 的工作
• hi, si – 对应 hardirq, softirq 的比例
• st – 因为超卖等起因, hypervisor 从该 vm 偷走的工夫 (todo: docker)

持续看 load average, 3 个数值别离对应到零碎 1/5/15 分钟内的零碎均匀 load, load 是个比拟含糊的概念, 能够简略认为是对资源有需要的工作数, 包含 on cpu, runnable 的工作, 也包含期待 IO 及任意 D 状态的工作. load 应用采样的形式, 每隔 5 秒采样一样, 越近的采样权重越大, 这样从 1/5/15 的趋势能够看出零碎压力的变动。

load average: 433.52, 422.54, 438.70

在这台 128 个 CPU 的机器上, loadavg 看起来有些偏高, 然而具体影响目前不得而知, 性能低是绝对具体指标而言的, load 高只是景象, 它可能相干也可能无关, 但至多是值得注意的。

再看下 dstat 对于工作状态的统计:
• run – 对应到 /proc/stat 外面的 procs_running, 也就是 runnable 工作数
• blk – 对应到 /proc/stat 外面的 procs_blocked, 阻塞在 I/O 的工作数

实际上和 loadavg 没有本质区别, 只是 load 含糊了 runnable 和 D 状态, 同时 load 应用 1/5/15 分钟的力度, 而 dstat 能够应用更细粒度, 如果只看某一时间点用 load, 如果要察看长时间的变动应用 dstat (/proc/stat)。

#dstat -tp
----system---- ---procs---
     time     |run blk new
07-03 17:56:50|204 1.0 202
07-03 17:56:51|212   0 238
07-03 17:56:52|346 1.0 266
07-03 17:56:53|279 5.0 262
07-03 17:56:54|435 7.0 177
07-03 17:56:55|442 3.0 251
07-03 17:56:56|792 8.0 419
07-03 17:56:57|504  16 152
07-03 17:56:58|547 3.0 156
07-03 17:56:59|606 2.0 212
07-03 17:57:00|770   0 186

内存

这里次要关注内存容量方面, 不关注访存的性能。
• Utilization. 内存利用率
• Saturation. 这里次要考查内存回收算法的效率

简略的内存利用率用 free 命令:
• total – MemTotal + SwapTotal, 一般来说 MemTotal 会略小于实在的物理内存
• free – 未应用的内存. Linux 偏向于缓存更多页面以进步性能, 所以不能简略通过 free 来判断内存是否有余
• buff/cache – 零碎缓存, 个别不须要严格辨别 buffer 和 cache
• available – 预计的可用物理内存大小
• used – 等于 total – free – buffers – cache
• Swap – 该机器上未配置

#free -g
              total        used        free      shared  buff/cache   available
Mem:            503         193           7           2         301         301
Swap:             0           0           0

更具体的信息能够间接去读 /proc/meminfo:

#cat /proc/meminfo
MemTotal:       527624224 kB
MemFree:         8177852 kB
MemAvailable:   316023388 kB
Buffers:        23920716 kB
Cached:         275403332 kB
SwapCached:            0 kB
Active:         59079772 kB
Inactive:       431064908 kB
Active(anon):    1593580 kB
Inactive(anon): 191649352 kB
Active(file):   57486192 kB
Inactive(file): 239415556 kB
Unevictable:      249700 kB
Mlocked:          249700 kB
SwapTotal:             0 kB
SwapFree:              0 kB
[...]

再来看下内存回收相干的信息, sar 的数据次要从 /proc/vmstat 采集, 次要关注:
• pgscank/pgscand – 别离对应 kswapd/direct 内存回收时扫描的 page 数
• pgsteal – 回收的 page 数
• %vmeff – pgsteal/(pgscank+pgscand)

要了解这些数据的具体含意, 须要对内存治理算法有肯定理解, 比方这里的 pgscan/pgsteal 只是针对 inactive list 而言的, 在内存回收的时候可能还须要先把页面从 active list 搬到 inactive list 等. 如果这里有异样, 咱们能够先把这当成入口, 再缓缓深刻, 具体到这里的 %vmeff, 最好状况就是每个扫描的 page 都能回收, 也就是 vmeff 越高越好。

#sar -B 1
    11:00:16 AM     pgscank/s pgscand/s pgsteal/s    %vmeff
    11:00:17 AM          0.00      0.00   3591.00      0.00
    11:00:18 AM          0.00      0.00  10313.00      0.00
    11:00:19 AM          0.00      0.00   8452.00      0.00

I/O

存储 I/O 的 USE 模型:
• Utilization. 存储设备的利用率, 单位工夫内设施在解决 I/O 申请的工夫
• Saturation. 队列长度

咱们个别关注这些局部:
• %util – 利用率. 留神即便达到 100% 的 util, 也不代表设施没有性能余量了, 特地地当初的 SSD 盘外部都反对并发. 打个比方, 一家旅馆有 10 间房, 每天只有有 1 个房间入住, util 就是 100%。
• svctm – 新版 iostat 曾经删掉
• await/r_await/w_await – I/O 提早, 包含排队工夫
• avgrq-sz – 均匀 request size, 申请解决工夫和大小有肯定关系, 不肯定线性
• argqu-sz – 评估 queue size, 能够用来判断是否有积压
• rMB/s, wMB/s, r/s, w/s – 根本语义

资源粒度

当咱们判断资源是否是瓶颈的时候, 只看零碎级别的资源是不够的, 比方能够用 htop 看下每个 CPU 的利用率, 指标工作运行在不同 CPU 上的性能可能相差很大。

内存也有相似状况, 运行 numastat -m

                          Node 0          Node 1          Node 2          Node 3
                 --------------- --------------- --------------- ---------------
MemTotal                31511.92        32255.18        32255.18        32255.18
MemFree                  2738.79          131.89          806.50        10352.02
MemUsed                 28773.12        32123.29        31448.69        21903.16
Active                   7580.58          419.80         9597.45         5780.64
Inactive                17081.27        26844.28        19806.99        13504.79
Active(anon)                6.63            0.93            2.08            5.64
Inactive(anon)          12635.75        25560.53        12754.29         9053.80
Active(file)             7573.95          418.87         9595.37         5775.00
Inactive(file)           4445.52         1283.75         7052.70         4450.98

零碎不肯定就是物理机, 如果产品跑在 cgroup, 那么这个 cgroup 是更须要关注的零碎, 比方在闲暇零碎上执行如下命令:

#mkdir /sys/fs/cgroup/cpuset/overloaded
#echo 0-1 > /sys/fs/cgroup/cpuset/cpuset.cpus
#echo 0 > /sys/fs/cgroup/cpuset/cpuset.mems
#echo $$
#for i in {0..1023}; do /tmp/busy & done

此时从物理机级别看, 零碎的 load 很高, 然而因为 cpuset 的限度, 竞争束缚在 cpu 0 和 1 上, 对运行在其余 cpu 上的产品影响并不大。

#uptime
 14:10:54 up 6 days, 18:52, 10 users,  load average: 920.92, 411.61, 166.95

利用角度

系统资源和利用的性能可能会有某种关联, 然而也能够更间接地从利用的角度登程定位问题:

• 利用能应用多少资源, 而不是零碎提供了多少资源, 这外面可能会有 gap, 零碎是个含糊的概念, 而利用自身却绝对具体. 以下面 cpuset 为例, 物理机是个零碎, cpuset 治理的资源也能够成为零碎, 然而利用在 cpuset 外面还是里面是确定的。

• 利用对资源的需要, 即便系统资源再多, 利用用不上性能也上不去, 也就是零碎可能没问题, 而是利用自身的起因。

以上面的 myserv 为例, 它的 4 个线程 %cpu 都达到了 100, 这个时候再去剖析整个零碎的 load 什么用途不大, 零碎有再多的闲暇 cpu 对 myserv 来说曾经没有意义。

#pidstat -p `pgrep myserv` -t 1
15:47:05      UID      TGID       TID    %usr %system  %guest    %CPU   CPU  Command
15:47:06        0     71942         -  415.00    0.00    0.00  415.00    22  myserv
15:47:06        0         -     71942    0.00    0.00    0.00    0.00    22  |__myserv
...
15:47:06        0         -     72079    7.00   94.00    0.00  101.00    21  |__myserv
15:47:06        0         -     72080   10.00   90.00    0.00  100.00    19  |__myserv
15:47:06        0         -     72081    9.00   91.00    0.00  100.00    35  |__myserv
15:47:06        0         -     72082    5.00   95.00    0.00  100.00    29  |__myserv

常用命令

根本命令

根本命令个别用来读取内核中记录的各种统计信息, 特地是 /proc 上面的各种文件, 这里简略列举局部:
• top – 提供了交互模式和 batch 模式, 不带参数进入交互模式, 按下 h 键能够看到各种性能
• ps – 提供了各种参数查看零碎中工作的状态, 比方 ps aux 或者 ps -eLf, 很多参数能够在须要的时候查看手册
• free – 内存信息
• iostat – I/ O 性能
• pidstat – 查看过程相干的信息, 下面曾经介绍过
• mpstat – 能够查看独自 cpu 的利用率, softirq, hardirq 个数等
• vmstat – 能够查看虚拟内存及各种零碎信息
• netstat – 网络相干
• dstat – 能够查看 cpu/disk/mem/net 等各种信息, 这些 stat 命令哪个不便用哪个
• htop – 下面介绍过
• irqstat – 不便察看中断信息
• sar/tsar/ssar – 收集和查看零碎运行的各种历史信息, 也提供实时模式

这里举个 ps 的例子, 咱们监控 mysqld 服务, 当该过程应用的内存超过零碎内存 70% 的时候, 通过 gdb 调用 jemalloc 的 malloc_stats_print 函数来剖析可能的内存透露。

largest=70
while :; do
    mem=$(ps -p `pidof mysqld` -o %mem | tail -1)
    imem=$(printf %.0f $mem)
    if [$imem -gt $largest]; then
        echo 'p malloc_stats_print(0,0,0)' | gdb --quiet -nx -p `pidof mysqld`
    fi
    sleep 10
done

perf

perf 是性能剖析的必备工具, 它最外围的能力是能拜访硬件上的 Performance Monitor Unit (PMU), 对剖析 CPU bound 的问题很有帮忙, 当然 perf 也反对各种软件 event. perf 的次要能力包含:
• 通过采样发现程序热点
• 通过硬件 PMU 深入分析问题的本源, 特地是配合硬件上的优化
perf list 能够列出反对的 event, 咱们能够通过 perf 来获取 cache misses, cycles 等等。

#perf list | grep Hardware
  branch-misses                                      [Hardware event]
  bus-cycles                                         [Hardware event]
  cache-misses                                       [Hardware event]
  cache-references                                   [Hardware event]
  cpu-cycles OR cycles                               [Hardware event]
  instructions                                       [Hardware event]
  L1-dcache-load-misses                              [Hardware cache event]
  L1-dcache-loads                                    [Hardware cache event]
  L1-dcache-store-misses                             [Hardware cache event]
  L1-dcache-stores                                   [Hardware cache event]
  L1-icache-load-misses                              [Hardware cache event]
  L1-icache-loads                                    [Hardware cache event]
  branch-load-misses                                 [Hardware cache event]
  branch-loads                                       [Hardware cache event]
  dTLB-load-misses                                   [Hardware cache event]
  iTLB-load-misses                                   [Hardware cache event]
  mem:<addr>[/len][:access]                          [Hardware breakpoint]

perf 应用的时候个别会传入以下参数:
• 通过 - e 指定感兴趣的一个或多个 event
• 指定采样的范畴, 比方过程级别 (-p), 线程级别 (-t), cpu 级别 (-C), 零碎级别 (-a)

这里应用默认的 event 看下过程 31925 的执行状况. 一个比拟重要的信息是 insns per cycle (IPC), 也就是每个 cycle 能执行多少指令, 其余 pmu event 像 cache misses, branch misses 如果有问题最终都会反映到 IPC 上. 尽管没有一个明确的规范, 然而上面 0.09 的 IPC 是比拟低的, 有必要持续深刻。

#perf stat -p 31925  sleep 1
 Performance counter stats for process id '31925':
       2184.986720      task-clock (msec)         #    2.180 CPUs utilized
             3,210      context-switches          #    0.001 M/sec
               345      cpu-migrations            #    0.158 K/sec
                 0      page-faults               #    0.000 K/sec
     4,311,798,055      cycles                    #    1.973 GHz
   <not supported>      stalled-cycles-frontend
   <not supported>      stalled-cycles-backend
       409,465,681      instructions              #    0.09  insns per cycle
   <not supported>      branches
         8,680,257      branch-misses             #    0.00% of all branches
       1.002506001 seconds time elapsed

除了 stat 外, perf 另一个可能更罕用的形式是采样来确定程序的热点, 当初有如下程序:

void busy(long us) {
    struct timeval tv1, tv2;
    long delta = 0;
    gettimeofday(&tv1, NULL);
    do {gettimeofday(&tv2, NULL);
        delta = (tv2.tv_sec - tv1.tv_sec) * 1000000 + tv2.tv_usec - tv1.tv_usec;
    } while (delta < us);
}
void A() { busy(2000); }
void B() { busy(8000); }
int main() {while (1) {A(); B();}
    return 0;
}

函数 A 和 B 执行工夫的比例, perf 的采样后果和咱们冀望的 2:8 基本一致。

#perf record -g -e cycles ./a.out
#perf report
Samples: 27K of event 'cycles', Event count (approx.): 14381317911
  Children      Self  Command  Shared Object     Symbol
+   99.99%     0.00%  a.out    [unknown]         [.] 0x0000fffffb925137
+   99.99%     0.00%  a.out    a.out             [.] _start
+   99.99%     0.00%  a.out    libc-2.17.so      [.] __libc_start_main
+   99.99%     0.00%  a.out    a.out             [.] main
+   99.06%    25.95%  a.out    a.out             [.] busy
+   79.98%     0.00%  a.out    a.out             [.] B
-   71.31%    71.31%  a.out    [vdso]            [.] __kernel_gettimeofday
     __kernel_gettimeofday
   - busy
      + 79.84% B
      + 20.16% A
+   20.01%     0.00%  a.out    a.out             [.] A

strace

trace 绝对于采样最大的劣势在于精度, trace 能抓住每次操作, 这给调试和了解带来很大不便. strace 专门用来 trace 零碎调用。

strace 通过捕捉所有的零碎调用能疾速帮忙了解利用的某些行为, 这里应用 strace 来看下下面提到的 perf-record 的实现, 很容易发现零碎调用 perf_event_open 以及它的参数, 因为有 128 个 cpu, 针对每个 cpu 都会调用一次该零碎调用。

#strace -v perf record -g -e cycles ./a.out
perf_event_open({type=PERF_TYPE_HARDWARE, size=PERF_ATTR_SIZE_VER5, config=PERF_COUNT_HW_CPU_CYCLES, sample_freq=4000, sample_type=PERF_SAMPLE_IP|PERF_SAMPLE_TID|PERF_SAMPLE_TIME|PERF_SAMPLE_CALLCHAIN|PERF_SAMPLE_PERIOD, read_format=0, disabled=1, inherit=1, pinned=0, exclusive=0, exclusive_user=0, exclude_kernel=0, exclude_hv=0, exclude_idle=0, mmap=1, comm=1, freq=1, inherit_stat=0, enable_on_exec=1, task=1, watermark=0, precise_ip=0 /* arbitrary skid */, mmap_data=0, sample_id_all=1, exclude_host=0, exclude_guest=1, exclude_callchain_kernel=0, exclude_callchain_user=0, mmap2=1, comm_exec=1, use_clockid=0, context_switch=0, write_backward=0, namespaces=0, wakeup_events=0, config1=0, config2=0, sample_regs_user=0, sample_regs_intr=0, aux_watermark=0, sample_max_stack=0}, 51876, 25, -1, PERF_FLAG_FD_CLOEXEC) = 30

blktrace

iostat 因为粒度太粗有的时候并不能很好地定位问题, blktrace 通过跟踪每个 I /O, 并在 I / O 的要害门路打桩, 能够取得更准确的信息, 从而帮忙剖析问题. blktrace 封装了几个命令:
• blktrace: 收集
• blkparse: 解决
• btt: 弱小的剖析工具
• btrace: blktrace/blkparse 的一个简略封装, 相当于 blktrace -d /dev/sda -o – | blkparse -i –

简略看下 blktrace 的输入, 外面记录了 I / O 门路上的要害信息, 特地地:
• 工夫戳, 性能剖析的要害信息之一
• event, 第 6 列, 对应到 I / O 门路上的关键点, 具体对应关系能够查找相应手册或源码, 了解这些关键点是调试 I / O 性能的必要技能
• I/O sector. I/ O 申请对应的扇区和大小

$ sudo btrace /dev/sda
8,0    0        1     0.000000000  1024  A  WS 302266328 + 8 <- (8,5) 79435736
8,0    0        2     0.000001654  1024  Q  WS 302266328 + 8 [jbd2/sda5-8]
8,0    0        3     0.000010042  1024  G  WS 302266328 + 8 [jbd2/sda5-8]
8,0    0        4     0.000011605  1024  P   N [jbd2/sda5-8]
8,0    0        5     0.000014993  1024  I  WS 302266328 + 8 [jbd2/sda5-8]
8,0    0        0     0.000018026     0  m   N cfq1024SN / insert_request
8,0    0        0     0.000019598     0  m   N cfq1024SN / add_to_rr
8,0    0        6     0.000022546  1024  U   N [jbd2/sda5-8] 1

这是 btt 的一个输入, 能够看到 S2G 的个数和提早, 失常状况不应该呈现这个问题, 这样就找到了一条能够深刻的线索。

$ sudo blktrace -d /dev/sdb -w 5
$ blkparse sdb -d sdb.bin
$ btt -i sdb.bin
==================== All Devices ====================
            ALL           MIN           AVG           MAX           N
--------------- ------------- ------------- ------------- -----------
Q2Q               0.000000001   0.000014397   0.008275391      347303
Q2G               0.000000499   0.000071615   0.010518692      347298
S2G               0.000128160   0.002107990   0.010517875       11512
G2I               0.000000600   0.000001570   0.000040010      347298
I2D               0.000000395   0.000000929   0.000003743      347298
D2C               0.000116199   0.000144157   0.008443855      347288
Q2C               0.000118211   0.000218273   0.010678657      347288
==================== Device Overhead ====================
       DEV |       Q2G       G2I       Q2M       I2D       D2C
---------- | --------- --------- --------- --------- ---------
 (8, 16) |  32.8106%   0.7191%   0.0000%   0.4256%  66.0447%
---------- | --------- --------- --------- --------- ---------
   Overall |  32.8106%   0.7191%   0.0000%   0.4256%  66.0447%

进阶篇

大学教材

通过教程可能系统地理解一门课的全貌, 网上搜到的大部分性能剖析的教程都是基于 Raj Jain 的 The Art of Computer Systems Performance Analysis, 这本书外面次要包含几个局部:

• Part I: AN OVERVIEW OF PERFORMANCE EVALUATION
• Part II: MEASUREMENT TECHNIQUES AND TOOLS
• Part III: PROBABILITY THEORY AND STATISTICS
• Part IV: EXPERIMENTAL DESIGN AND ANALYSIS
• Part V: SIMULATION
• Part VI: QUEUEING MODELS

书的重心放在 performance analysis 下面, 波及较多概率和统计的计算, 另外 rice 大学的这个教程写得挺不错[1]。

技术博客

• 参考文末 [2] 有工夫能够都过一遍, 总的来说次要包含 3 个局部:
• 性能剖析的办法集. 代表作 USE 办法
• 性能数据的收集. 代表作 “ 工具大图 ”
• 性能数据的可视化. 代表作 火焰图
• 文末链接[3]
• 文末链接[4]
• 文末链接[5]

知识结构

零碎性能剖析在深度和广度上都有要求, 对底层包含 OS 和硬件, 以及一些通用能力要做到足够深, 对下层产品的了解又须要有足够的广度, 近一年在混合云亲手摸过的产品预计不下二十款, 当然重点剖析过的只有几个。

操作系统

操作系统是系统分析的根底, 不论是 I /O, 内存, 网络, 调度, docker 等等都离不开操作系统, 操作系统常识能够从 Understanding the Linux Kernel 开始, 这本书尽管老了但不障碍了解 OS 的基本概念, 缓缓做到能浏览内核文档和源码。

在适配某款 arm 平台的时候发现, 在 numa off 的状况下:
• ecs 绑在 socket 0 上性能好
• mysql 绑在 socket 1 上性能好

能确定的是, 该平台跨 socket 性能拜访不论是 latency 还是 throughput 和本地拜访都有较大差距, 所以一个正当的方向是跨 socket 的内存拜访, 如果有相似 x86 pcm 的话会比拟间接, 然而该平台上短少该类 pmu 来查看跨 socket 的信息, 咱们尝试从 OS 的角度来答复这个问题。

首先通过将内存压测工具跑在不同的 socket/node 上, 发现 numa off 体现出了和 numa on 雷同的性能特色, 和硬件产生确认该平台 numa off 和 on 的实现在硬件上并没有区别, 只是 bios 不传递 numa 信息给操作系统, 这样是能够晓得物理地址在哪个 socket/node 上的。

接下来只有确定 ecs/mysql 的物理内存地位, 就能够用于判断性能和跨 socket 的相关性. Linux 在用户态能够通过 pagemap 将虚拟地址对应到物理地址, 只须要稍加批改 tools/vm/page-types.c 就能拿到过程对应的所有物理地址. 经确认, 的确 ecs/mysql 的性能和它们应用的物理内存的地位强相干。最初要答复的是为什么 ecs 和 mysql 体现恰好相反, 留神到 ecs 应用 hugepage 而 mysql 应用 normal page, 有如下假如, 具体代码这里不再列出。

• 系统启动的时候, 物理内存加到搭档零碎是先 socket 0 后 socket 1
• socket 1 上的内存会被先分进去, 所以 mysql 调配的内存在 socket 1. 特定集群的机器不会随便跑其余过程
• 在 ecs 的 host 上, 因为要调配的 hugepage 曾经超过了 socket 1 上的所有内存, 所以前面调配的 hugepage 曾经落在了 socket 0
• hugepage 的调配是后进先出, 意味着 ecs 一开始调配到的 hugepage 在 socket 0, 而该机器资源并没全副用完, 测试用的几个 ecs 内存全落在了 socket 0 上, 所以将 ecs 过程绑到 socket 0 的性能更好

硬件常识

如果始终是 x86 架构, 事件会简略很多, 一是 x86 的常识大家近朱者赤; 近墨者黑很久了, 多多少少都理解一些, 二是架构变动绝对较小, 各种利用都适配较好, 须要调优的用例较少. 随着各种新平台的崛起, 它们性能各异, 对整个零碎性能带来的冲击是微小的, 这不是影响某个产品, 这影响的简直是所有产品. 最根本地, 咱们要解决以下问题:

• 新的平台上, 利用原有的很多假如被突破, 须要从新适配, 否则性能可能不迭预期. 比方在 Intel 下面, 开关 numa 的性能差距不大, 在其余平台上可能就不一样
• 新的平台要取代老的平台, 就存在性能的比拟. 因为平台性能差别大并且差别点多, 尽管 speccpu 之类的 benchmark 能肯定水平反馈平台整体的计算性能, 但很多时候还须要联合不同场景别离进行性能调优
• 不排除新平台存在某种 bug 或者未知的 feature, 都须要咱们去摸索解决的方法

数据分析

在收集了大量数据后, 通过数据分析能够放大数据的价值
• 数据提取. 利用各种工具比方 awk/sed/perl 等脚本语言提取所需的数据
• 数据抽象. 从不同角度加工数据, 辨认异样, 比方单机 / 集群别离是什么体现, 统计哪些值
• 可视化. 可视化是数据处理十分重要的能力, 一图胜千言, 火焰图就是最好的例子. 罕用画图工具有 gnuplot, excel 等

比方剖析 MapReduce 工作在 10 台机器的集群上的性能, 即便每台机器都体现出肯定的共性, 然而如果从集群角度看的话则更加显著, 也很容易验证这种共性。

换种显示方式则更加显著, 很容易晓得在不同阶段的体现, 比方失常 Map 和 Reduce 阶段 cpu 利用率也只有 80%, 这个是否合乎预期, 另外在 Map 和 Reduce 切换的时候, 零碎 idle 很显著, 会不会是潜在优化点。

如果有对照的话, 能够直观地看不到不同体现, 特地是微小的长尾工夫有进一步优化的空间。

Benchmarking

Benchmarking 是获取性能指标最根本的伎俩, 也是测试罕用的办法, 每个畛域简直都有本人的一套测试用例. 对于 benchmarking, 首先须要晓得它测的是什么. 以 spec cpu2017 为例, 它次要测试的是处理器, 内存子系统以及编译器的性能, 那么在测试的时候咱们除了关注 CPU 型号, 还要思考内存大小插法型号, 以及编译器及其参数等等, 在做性能比对时也能分明它的应用范畴。

Benchmark 的一个特点是可重复性, spec.org 做得很好的一点是下面有大量颁布的测试后果, 能够参考这些测试后果来验证咱们本人的测试方法参数是否正当. 如果想测试 cpu2017, 第一件事就是先重做他人的测试, 直到能复现他人的数据, 这个过程可能会有很多播种, 对 benchmark 也会有更多理解. 以 intel 8160 为例, 在硬件基本一致的状况下, 不经额定的配置本人环境 cpu2017 integer rate 只能跑到 140, 而 spec.org 下面的测试用例能达到 240, 性能逐渐迫近 240 的过程, 也是深刻了解 cpu2017 的过程。

对于性能数据, 首先想要强调的是有数据并不一定比没数据强, 只有解释过的数据才是无效数据, 没解释过的数据反而会引起不必要的误判, 比方下面 cpu2017 的例子, 在做不同平台性能比照的时候, 8160 到底用 140 还是 240 呢, 得出的论断会十万八千里. 再比方应用上面的命令测试某新平台的内存提早:

lat_mem_rd -P 1 -N 1 10240 512

测试出的提早是 7.4ns, 不加分析采纳该后果就可能得出新平台提早太好的谬误论断. 所以看待数据要足够审慎, 个别会有几个阶段:

  1. 在信赖关系建设前, 对他人的数据放弃审慎. 一是有可能本人对这块还没有足够了解, 二是须要测试报告提供足够的信息供别人做判断。
  2. 置信本人的数据. 必须置信本人, 然而抉择置信本人的数据, 是因为有过具体正当的剖析。
  3. 置信他人的数据. 信赖链建设之后, 以及本人有了足够了解后, 抉择置信.

更多工具

ftrace

想要疾速了解代码实现, 没有什么比打印调用门路更间接了. ftrace 能够用来解决 2 个问题:
• 谁调用了我. 这个只有在执行对应函数的时候拿到对应的栈就能够, 多种工具能够实现
• 我调用了谁. 这个是 ftrace 比拟 unique 的性能
为了不便咱们应用 ftrace 的 wrapper trace-cmd, 假如咱们曾经晓得 I / O 门路会通过 generic_make_request, 为了查看残缺的门路咱们能够这样:

#trace-cmd record -p function --func-stack -l generic_make_request dd if=/dev/zero of=file bs=4k count=1 oflag=direct

通过 report 来查看就高深莫测了:

#trace-cmd report
cpus=128
              dd-11344 [104] 4148325.319997: function:             generic_make_request
              dd-11344 [104] 4148325.320002: kernel_stack:         <stack trace>
=> ftrace_graph_call (ffff00000809849c)
=> generic_make_request (ffff000008445b80)
=> submit_bio (ffff000008445f00)
=> __blockdev_direct_IO (ffff00000835a0a8)
=> ext4_direct_IO_write (ffff000001615ff8)
=> ext4_direct_IO (ffff0000016164c4)
=> generic_file_direct_write (ffff00000825c4e0)
=> __generic_file_write_iter (ffff00000825c684)
=> ext4_file_write_iter (ffff0000016013b8)
=> __vfs_write (ffff00000830c308)
=> vfs_write (ffff00000830c564)
=> ksys_write (ffff00000830c884)
=> __arm64_sys_write (ffff00000830c918)
=> el0_svc_common (ffff000008095f38)
=> el0_svc_handler (ffff0000080960b0)
=> el0_svc (ffff000008084088)

当初如果咱们想持续深刻 generic_make_request, 应用 function_graph plugin:

$ sudo trace-cmd record -p function_graph -g generic_make_request dd if=/dev/zero of=file bs=4k count=1 oflag=direct

这样就能够拿到整个调用过程 (report 后果略微整顿过):

$ trace-cmd report
    dd-22961                 |  generic_make_request() {dd-22961                 |    generic_make_request_checks() {dd-22961      0.080 us   |      _cond_resched();
    dd-22961                 |      create_task_io_context() {dd-22961      0.485 us   |        kmem_cache_alloc_node();
    dd-22961      0.042 us   |        _raw_spin_lock();
    dd-22961      0.039 us   |        _raw_spin_unlock();
    dd-22961      1.820 us   |      }
    dd-22961                 |      blk_throtl_bio() {dd-22961      0.302 us   |        throtl_update_dispatch_stats();
    dd-22961      1.748 us   |      }
    dd-22961      6.110 us   |    }
    dd-22961                 |    blk_queue_bio() {dd-22961      0.491 us   |      blk_queue_split();
    dd-22961      0.299 us   |      blk_queue_bounce();
    dd-22961      0.200 us   |      bio_integrity_enabled();
    dd-22961      0.183 us   |      blk_attempt_plug_merge();
    dd-22961      0.042 us   |      _raw_spin_lock_irq();
    dd-22961                 |      elv_merge() {dd-22961      0.176 us   |        elv_rqhash_find.isra.9();
    dd-22961                 |        deadline_merge() {dd-22961      0.108 us   |          elv_rb_find();
    dd-22961      0.852 us   |        }
    dd-22961      2.229 us   |      }
    dd-22961                 |      get_request() {dd-22961      0.130 us   |        elv_may_queue();
    dd-22961                 |        mempool_alloc() {dd-22961      0.040 us   |          _cond_resched();
    dd-22961                 |          mempool_alloc_slab() {dd-22961      0.395 us   |            kmem_cache_alloc();
    dd-22961      0.744 us   |          }
    dd-22961      1.650 us   |        }
    dd-22961      0.334 us   |        blk_rq_init();
    dd-22961      0.055 us   |        elv_set_request();
    dd-22961      4.565 us   |      }
    dd-22961                 |      init_request_from_bio() {dd-22961                 |        blk_rq_bio_prep() {dd-22961                 |          blk_recount_segments() {dd-22961      0.222 us   |            __blk_recalc_rq_segments();
    dd-22961      0.653 us   |          }
    dd-22961      1.141 us   |        }
    dd-22961      1.620 us   |      }
    dd-22961                 |      blk_account_io_start() {dd-22961      0.137 us   |        disk_map_sector_rcu();
    dd-22961                 |        part_round_stats() {dd-22961      0.195 us   |          part_round_stats_single();
    dd-22961      0.054 us   |          part_round_stats_single();
    dd-22961      0.955 us   |        }
    dd-22961      2.148 us   |      }
    dd-22961    + 15.847 us  |    }
    dd-22961    + 23.642 us  |  }

uftrace

uftrace 在用户态实现了一个相似 ftrace 的性能, 对须要疾速了解用户态的逻辑会有帮忙, 然而须要加上 -pg 从新编译源码, 详情见[6]。

#gcc -pg a.c
#uftrace ./a.out
# DURATION     TID     FUNCTION
            [69439] | main() {[ 69439] |   A() {0.160 us [ 69439] |     busy();
   1.080 us [69439] |   } /* A */
            [69439] |   B() {0.050 us [ 69439] |     busy();
   0.240 us [69439] |   } /* B */
   1.720 us [69439] | } /* main */

BPF

BPF (eBPF) 是这几年的热点, 通过 BPF 简直能够看清零碎的各个角落, 给诊断带来了极大的不便. BPF 不是一个工具, BPF 是生产工具的工具, BPF 工具编写是性能剖析必须把握的技能之一。

这里举个应用 BPF 来剖析 QEMU I/ O 提早的例子. 为了简化问题, 先确保 vm 外面的块设施只有 fio 在应用, fio 管制设施只有一个并发 I /O, 这样咱们在 host 上抉择 2 个观察点:
• tracepoint:kvm:kvm_mmio. host 捕捉 guest mmio 操作, guest 外面最终通过写该 mmio 发送申请给 host
• kprobe:kvm_set_msi. 因为 guest 外面 vdb 应用 msi 中断, 中断最终通过该函数注入

因为 host 上有多个 vm 和虚拟盘须要辨别, 应用以下信息捕捉并且只捕捉咱们关注的这个设施:
• 只关注该 qemu-kvm pid
• vbd mmio 对应的 gpa, 这个能够在 guest 外面通过 lspci 取得
对于 kvm_set_msi, 应用如下信息:
• struct kvm 的 userspace_pid, struct kvm 对应的 qemu-kvm 过程
• struct kvm_kernel_irq_routing_entry 的 msi.devid, 对应到 pci 设施 id

#include <linux/kvm_host.h>
BEGIN {
    @qemu_pid = $1;
    @mmio_start = 0xa000a00000;
    @mmio_end = 0xa000a00000 + 16384;
    @devid = 1536;
}
tracepoint:kvm:kvm_mmio /pid == @qemu_pid/ {if (args->gpa >= @mmio_start && args->gpa < @mmio_end) {@start = nsecs;}
}
kprobe:kvm_set_msi {$e = (struct kvm_kernel_irq_routing_entry *)arg0;
    $kvm = (struct kvm *)arg1;
    if (@start > 0 && $kvm->userspace_pid == @qemu_pid && $e->msi.devid == @devid) {@dur = stats(nsecs - @start);
        @start = 0;
    }
}
interval:s:1 {print(@dur); clear(@dur);
}

执行后果如下:

@dur: count 598, average 1606320, total 960579533
@dur: count 543, average 1785906, total 969747196
@dur: count 644, average 1495419, total 963049914
@dur: count 624, average 1546575, total 965062935
@dur: count 645, average 1495250, total 964436299

更深了解

很多技术须要重复去了解验证, 每一次可能都有不同的播种, 这里举个 loadavg 的例子. 援用 kernel/sched/loadavg.c 最开始的一段正文:

  5  * This file contains the magic bits required to compute the global loadavg
  6  * figure. Its a silly number but people think its important. We go through
  7  * great pains to make it work on big machines and tickless kernels.

这里的 silly 我想说的是 loadavg 有肯定的局限性, 总的来说 loadavg 是有肯定语义和价值的, 毕竟它只用了 3 个数字形容了过来一段时间的 ”load”, 反过来说如果 loadavg is silly, 那么有没有更好的抉择?
• 如果是实时察看的话, vmstat/dstat 输入的 runnable 和 I /O blocked 的信息是种更好的抉择, 因为绝对于 loadavg 每 5 秒的采样, vmstat 能够做到粒度更细, 而且 loadavg 的算法某种程度能够了解为有损的。
• 如果是 sar/tsar 的话, 假如收集距离是 10min 的话, loadavg 因为能笼罩更大的范畴, 的确比 10min 一个的数字蕴含更多的信息, 但咱们须要思考它对调试的真正价值.

另外, 5 秒钟的采样距离是比拟大的, 咱们能够结构个测试用例执行了大量工夫但跳过采样
• 获取 load 采样点的工夫
• 测试用例刚好跳过该采样点
查看 calc_load_fold_active 在 cpu 0 上的调用工夫:

kprobe:calc_load_fold_active /cpu == 0/ {printf("%ld\n", nsecs / 1000000000);
}

运行没有输入, 监控上一层函数:

#include "kernel/sched/sched.h"
kprobe:calc_global_load_tick /cpu == 0/ {$rq = (struct rq *)arg0;
    @[$rq->calc_load_update] = count();}
interval:s:5 {print(@); clear(@);
}

执行后果合乎预期:

#./calc_load.bt -I /kernel-source
@[4465886482]: 61
@[4465887733]: 1189
@[4465887733]: 62
@[4465888984]: 1188

查看汇编发现这里代码被优化, 然而刚好 id_nr_invalid 调用没优化, 当然最不便的是可能间接在函数偏移处间接打点:

kprobe:id_nr_invalid /cpu == 0/ {printf("%ld\n", nsecs / 1000000000);
}

依据这个工夫戳, 能够很容易跳过 load 的统计:

while :; do
        sec=$(awk -F. '{print $1}' /proc/uptime)
        rem=$((sec % 5))
        if [$rem -eq 2]; then # 1s after updating load
                break;
        fi
        sleep 0.1
done
for i in {0..63}; do
        ./busy 3 & # run 3s
done

大量 busy 过程胜利跳过 load 的统计, 能够构想像 cron 执行的工作也是有这个可能的. 尽管不能否定 loadavg 的价值, 但总的来说 load 有以下缺点:
• 零碎级别的统计, 和具体利用产生的分割不够间接
• 应用采样的形式并且采样距离 (5s) 较大, 有的场景不能实在反映零碎
• 统计的距离较大(1/5/15 分钟), 不利于及时反映过后的状况
• 语义略微不够清晰, 不只包含 cpu 的 load, 还包含 D 状态的工作, 这个自身不是大问题, 更多能够认为是 feature

Linux 减少了 Pressure Stall Information (PSI), PSI 从工作的角度别离统计了 10/60/300s 内因为 cpu/io/memory 等资源有余而不能运行的时长, 并依照影响范畴分成 2 类:
• some – 因为短少资源导致局部工作不能执行
• full – 因为短少资源导致所有工作不能执行, cpu 不存在这种状况

咱们在一台 96c 的 arm 机器上扫描所有 cgroup 的 cpu.pressure:

这里会引出几个问题, 篇幅起因这里不再开展。
• 父 cgroup 的 avg 为什么比子 cgroup 还小? 是实现问题还是有额定的配置参数?
• avg10 等于 33, 也就是 1 / 3 的工夫有 task 因为没有 cpu 而得不到执行, 思考到零碎 cpu 利用率在 40% 左右并不算高, 咱们怎么正当对待和应用这个值

top - 09:55:41 up 127 days,  1:44,  1 user,  load average: 111.70, 87.08, 79.41
Tasks: 3685 total,  21 running, 2977 sleeping,   1 stopped,   8 zombie
%Cpu(s): 27.3 us,  8.9 sy,  0.0 ni, 59.8 id,  0.1 wa,  0.0 hi,  4.0 si,  0.0 st

RTFSC

有的时候 RTFM 曾经不够了, 手册包含工具自身的更新没对上内核的节奏, 咱们回到下面页面回收的例子, 预计有的同学之前就有疑难, 没有 scan 哪里来的 steal。

#sar -B 1
    11:00:16 AM     pgscank/s pgscand/s pgsteal/s    %vmeff
    11:00:17 AM          0.00      0.00   3591.00      0.00
    11:00:18 AM          0.00      0.00  10313.00      0.00
    11:00:19 AM          0.00      0.00   8452.00      0.00

先看 sysstat (sar) 外面的实现, 次要是读取剖析 /proc/vmstat:
• pgscand: 对应到 pgscan_direct 域
• pgscank: 对应到 pgscan_kswapd 域
• pgsteal: 对应到 pgsteal_结尾的域

#gdb --args ./sar -B 1
(gdb) b read_vmstat_paging
(gdb) set follow-fork-mode child
(gdb) r
Breakpoint 1, read_vmstat_paging (st_paging=0x424f40) at rd_stats.c:751
751             if ((fp = fopen(VMSTAT, "r")) == NULL)
(gdb) n
754             st_paging->pgsteal = 0;
(gdb)
757             while (fgets(line, sizeof(line), fp) != NULL) {(gdb)
759                     if (!strncmp(line, "pgpgin", 7)) {(gdb)
763                     else if (!strncmp(line, "pgpgout", 8)) {(gdb)
767                     else if (!strncmp(line, "pgfault", 8)) {(gdb)
771                     else if (!strncmp(line, "pgmajfault", 11)) {(gdb)
775                     else if (!strncmp(line, "pgfree", 7)) {(gdb)
779                     else if (!strncmp(line, "pgsteal_", 8)) {(gdb)
784                     else if (!strncmp(line, "pgscan_kswapd", 13)) {(gdb)
789                     else if (!strncmp(line, "pgscan_direct", 13)) {(gdb)
757             while (fgets(line, sizeof(line), fp) != NULL) {(gdb)

看下 /proc/vmstat 都有什么:

#grep pgsteal_ /proc/vmstat
pgsteal_kswapd 168563
pgsteal_direct 0
pgsteal_anon 0
pgsteal_file 978205
#grep pgscan_ /proc/vmstat
pgscan_kswapd 204242
pgscan_direct 0
pgscan_direct_throttle 0
pgscan_anon 0
pgscan_file 50583828

最初看看内核的实现, pgsteal 和 pgscan 的逻辑是一样, 除了 nr_scanned 换成了 nr_reclaimed:

当初问题很清晰了:
• 这里 sar 获得是零碎的 /proc/vmstat, 而 cgroup 外面 pgscan_kswapd 和 pgscan_direct 只会加到 cgroup 的统计, 不会加到零碎级的统计
• cgroup 外面 pgsteal_kswapd 和 pgsteal_direct 同样只会加到 cgroup 本人的统计
• 然而次要 pgscan_anon, pgscan_file 和 pgsteal_anon, pgsteal_file 都只加到零碎级的统计
• sar 读取了 pgscan_kswapd, pgscan_direct, 以及 pgsteal_, 这里 还包含了 pgsteal_anon 和 pgsteal_file

这整个逻辑都乱了, 咱们有必要解决这个 bug 让 sar 的输入变得更加有意义. 那么在 cgroup 内是不是没问题?

#df -h .
Filesystem      Size  Used Avail Use% Mounted on
cgroup             0     0     0    - /sys/fs/cgroup/memory
#grep -c 'pgscan\|pgsteal' memory.stat
0

这些统计信息在 cgroup v1 上齐全没有输入, 而只在 v2 版本有输入. 在以前内核没有专门 LRU_UNEVICTABLE 的时候, 如果有很多比方 mlock page 的时候, 碰到过不停扫描却不能回收内存的状况, 这个统计会十分有用, 即便是当初我置信这个统计还是有用的, 只是大部分时候还不必看到这么细。

多上手

纸上得来终觉浅, 本人入手去做带来很多益处:
• 答复预设问题. 调试剖析就是一直提出问题和验证的过程, 没有上手的话就会始终停留在第一个问题上. 比方我想理解某平台上物理内存是怎么编址的, 没有文档的话只能本人去试验
• 提出新的问题. 调试剖析中不怕有问题, 怕的是提不出问题
• 会有意外播种. 很多时候并不是无意为之, 比方筹备的是剖析 cpu 调频是否降功耗, 下来却发现零碎始终运行在最低频率
• 纯熟. 纯熟就是效率
• 改良产品. 能够试想下在整个云环境所有机器上扫描 (相似全面体检) 会发现多少潜在问题

参考资料

[1]https://www.cs.rice.edu/~john…
[2]https://brendangregg.com/
[3]http://dtrace.org/blogs/bmc/
[4]https://blog.stgolabs.net/
[5]https://lwn.net/
[6]https://github.com/namhyung/u…
[7]https://www.brendangregg.com/[8]The Art of Computer Systems Performance Analysis

正文完
 0