网游服务器性能优化工具收集整理

Xsens动作捕捉 2022-11-27 19514

本文内容来源于网上众多文章中的精华,用于汇总,备注等用途

Linux服务器性能分析和工具

网游服务器性能优化工具收集整理  第1张

这里所有的工具,都可以通过man 获得它的帮助文档,这里只介绍一些常规用法。

stat工具

vmstat

我们先看一个vmstat 的例子。用下面的命令让它每5秒中打印一个报告。

网游服务器性能优化工具收集整理  第2张

可以用ctrl+c停止vmstat。vmstat的常规用法是vmstat interval times,即每隔interval秒采 样一次,共采样times次,如果省略times,则一直采集数据到用户手动停止。

第一行的值是显示了自系统启动以来的平均值,第二行开始展示现在正在发生的情况,接下来的行会显示每5秒的间隔内发生了什么。每一列的含义在头部,如下所示:

  • procs

r这一列显示了多少进程正在等待cpu,b列显示多少进程正在不可中断的休眠(通常意味着它们在等待IO ,例如磁盘,网络,用户输入,等等)。

  • memory

swapd列显示了多少块被换出到了磁盘(页面交换)。剩下的三个列显示了多少块是空闲的(未被使用),多少块正在被用作缓冲区,以及多少正在被用作操作系统的缓存。

  • swap

这些列显示页面交换活动:每秒有多少块正在被换入(从磁盘)和换出(到磁盘)。它们比监控swpd列重要多了。大部分时间我们希望看到si 和so 列是0,并且我们很明确不希望看到每秒超过10个块。

  • io

这些列显示了多少块从块设备读取(bi)和写出(bo)。这通常反映了硬盘I/O。

  • system

这些列显示了每秒中断(in)和上下文切换(cs)的数量。除非上下文切换超过100 000次或更多,一般不用担心上下文切换。

  • cpu

这些列显示所有的CPU时间花费在各类操作的百分比,包括执行用户代码(非内核),执行系统代码(内核),空闲以及等待IO。如果正在使用虚拟化,第5列可能是st,显示了从虚拟机中”偷走”的百分比。

内存不足的表现:free memory 急剧减少,回收buffer和cache也无济于事,大量使用交换分区(swpd),页面交换(swap)频繁,读写磁盘数量(io)增多,缺页中断(in)增多,上下文切换(cs)次数增多,等待IO的进程数(b)增多,大量CPU时间用于等待IO(wa)。

iostat

现在让我们转移到iostat 。默认情况下,它显示了与vmstat 相同的CPU 使用信息。我们通常只对I/O统计感兴趣,所以使用下面的命令只展示扩展的设备统计。

网游服务器性能优化工具收集整理  第3张

与vmstat一样,第一行报告显示的是自系统启动以来的平均值,(通常删掉它节省空间),然后接下来的报告显示了增量的平均值,每个设备一行。

有多种选项显示和隐藏列。官方文档有点难以理解,因此我们必须从源码中挖掘真正显示的内容是什么。说明的列信息如下:

为了看懂Linux的磁盘IO指标,先了解一些常见的缩写习惯:rq是request,r是read,w是write,qu是queue,sz是size的,a是average,tm是time,svc是service。

  • rrqm/s 和 wrqm/s
  • 每秒合并的读和写请求。”合并的”意味着操作系统从队列中拿出多个逻辑请求合并为一个请求到实际磁盘。
  • r/s 和 w/s
  • 每秒发送到设备的读和写请求数。
  • rsec/s 和 wsec/s
  • 每秒读和写的扇区数。有些系统也输出为rKB/s和wKB/s ,意味每秒读写的千字节数。(iostat -dkx interval times)
  • avgrq-sz
  • 请求的扇区数。(读扇区数 + 写扇区数) / (读请求次数 + 写请求次数)
  • avgqu-sz
  • 在设备队列中等待的请求数。即队列的平均长度。
  • await
  • 每个IO请求花费的时间,包括在队列中的等待时间和实际请求(服务)时间。
  • svctm
  • 实际请求(服务)时间,以毫秒为单位,不包括排队时间。
  • %util
  • 至少有一个活跃请求所占时间的百分比。更好的说法应该是,服务时间所占的百分比。以上面的输出为例。 一秒中内,读了2.5次,写了1.8次,每次请求的实际请求时间(不包括排队时间)为6.0ms,那么总的时间花费为(2.5+1.8)*6.0ms,即25.8ms,0.0258秒,转换成百分比再四舍五入就得到了util的值2.6%。
  • %util: When this figure is consistently approaching above 80% you will need to take any of the following actions - * increasing RAM so dependence on disk reduces * increasing RAID controller cache so disk dependence decreases * increasing number of disks so disk throughput increases (more spindles working parallely) * horizontal partitioning

下面这个公式可以计算一个请求在整个请求期间,有多少时间用以等待。当这个值大于50%,说明整个请求期间,花费了更多时间在队列中等待;如果这个数很大,则应该采取相应措施。

  • (await-svctim)/await*100: The percentage of time that IO operations spent waiting in queue in comparison to actually being serviced. If this figure goes above 50% then each IO request is spending more time waiting in queue than being processed. If this ratio skews heavily upwards (in the >75% range) you know that your disk subsystem is not being able to keep up with the IO requests and most IO requests are spending a lot of time waiting in queue. In this scenario you will again need to take any of the actions above

IO瓶颈的症状: 1. %util 很高 2. await 远大于svctm 3. avgqu-sz 比较大

下面解释了iowait的作用,需要注意的是,高速CPU也可能导致iowait取值较大。

  • %iowait: This number shows the % of time the CPU is wasting in waiting for IO. A part of this number can result from network IO, which can be avoided by using an Async IO library. The rest of it is simply an indication of how IO-bound your application is. You can reduce this number by ensuring that disk IO operations take less time, more data is available in RAM, increasing disk throughput by increasing number of disks in a RAID array, using SSD (Check my post on Solid State drives vs Hard Drives) for portions of the data or all of the data etc

上面的解释主要参考《高性能mysql》和这篇博客。

例子

cpu 密集型机器

cpu 密集型服务器的vmstat 输出通常在us 列会有一个很高的值,报告了花费在非内核代码上的cpu 时钟;也可能在sy 列有很高的值,表示系统cpu 利用率,超过20% 就足以令人不安了。在大部分情况下,也会有进程队列排队时间(在r列报告的)。下面是一个列子:

网游服务器性能优化工具收集整理  第4张

如果我们在同一台及其上观察iostat 的输出(再次剔除显示启动以来平均值的第一行),可以发现磁盘利用率低于50%:

网游服务器性能优化工具收集整理  第5张

这台机器不是IO密集型的,但是依然有相当数量的IO发生,在数据库服务器中这种情况很少见。另一方面,传统的Web 服务器会消耗掉大量的CPU 资源,但是很少发生IO,所以Web 服务器的输出不会像这个例子。

IO 密集型机器

在IO密集型工作负载下,CPU花费大量时间在等待IO请求完成。这意味着vmstat 会显示很多处理器在非中断休眠(b列)状态,并且在wa 这一列的值很高,下面是个例子:

网游服务器性能优化工具收集整理  第6张

这台机器的iostat 输出显示硬盘一直很忙:

网游服务器性能优化工具收集整理  第7张

%util的值可能因为四舍五入的错误超过100%。什么迹象意味着机器是IO密集的呢?只要有足够的缓冲来服务写请求,即使机器正在做大量的写操作,也可能可以满足,但是却通常意味着硬盘可能会无法满足读请求。这听起来好像违反直觉,但是如果思考读和写的本质,就不会这么认为了:

  • 写请求能够缓冲或同步操作。
  • 读请求就其本质而言都是同步的。当然,程序可以猜测到可能需要某些数据,并异步地提前读取(预读)。无论如何,通常程序在继续工作前必须得到它们需要的数据。这就强制读请求为同步操作:程序必须阻塞直到请求完成。

想想这种方式:你可以发出一个写请求到缓冲区的某个地方,然后过一会完成。甚至可以每秒发出很多这样的请求。如果缓冲区正确工作,并且有足够的空间,每个请求都可以很快完成,并且实际上写到物理硬盘是被重新排序后更有效地批量操作的。然而,没有办法对读操作这么做————不管多小或多少的请求,都不可能让硬盘响应说:”这是你的数据,我等一会读它”。这就是为什么读需要IO等待是可以理解的原因。

发生内存交换的机器

一台正在发生内存交换的机器可能在swpd 列有一个很高的值,也可能不高。但是可以看到si 和 so 列有很高的值,这是我们不希望看到的。下面是一台内存交换严重的机器的vmstat 输出:

网游服务器性能优化工具收集整理  第8张

空闲的机器

下面是一台空闲机器上的vmstat输出。可以看到idle列显示CPU是100%的空闲。

网游服务器性能优化工具收集整理  第9张

dstat

dstat 显示了cpu使用情况,磁盘io 情况,网络发包情况和换页情况。

个人觉得,iostat和vmstat 的输出虽然详细,但是不够直观,不如dstat 好用。而且,dstat输出是彩色的,可读性更强。现在dstat作为我首选的,查看系统状态的工具。

dstat使用时,直接输入命令即可,不用任何参数。也可以通过指定参数来显示更加详细的信息。

dstat -cdlmnpsy

网游服务器性能优化工具收集整理  第10张

pidstat

了解系统IO的情况大多数是通过iostat来获取的,这个粒度只能精确到每个设备。通常我们会想了 解每个进程,线程层面发起了多少IO,在Linux 2.6.20之前除了用systemtap这样的工具来实现 是没有其他方法的,因为系统没有暴露这方面的统计。现在可以通过一个名为pidstat的工具, 它的使用方法如下:

pidstat -d interval

此外,pidstat 还可以用以统计CPU使用信息。

pidstat -u interval

统计内存信息:

pidstat -r interval

mpstat

mpstat 用来统计多核处理器中,每一个处理器的使用情况。mpstat使用方法很简单,常见用法如下:

mpstat -P ALL interval times

不出意外,读者执行上面的命令,看不出个所以然来,试试在运行下面这条命令的同时,查看mpstat的输出。

sysbench --test=cpu --cpu-max-prime=20000 run

注意:sysbench是一个基准测试工具,可以用来测试cpu,io,mutex,oltp等。在Debain系统下, 通过下面的命令安装:

sudo apt-get install sysbench

netstat

netstat用于显示与IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况。

就我自己而言,经常使用的用法如下:

netstat -npl

上面这条命令可以查看你要打开的端口是否已经打开,以此来作为程序是否已经启动的依据。

netstat 还有很多其他的用法,在《TCP/IP详解》一书中,作者喜欢用netstat命令来打印路由表,使用方法如下:

netstat -rn

其中,flag域的解释如下:

  • U 该路由可用
  • G 该路由是到一个到网关(路由器)。如果没有设置该标志,说明目的地址是直接相连的
  • H 该路由是到一个主机
  • D 该路由是由重定向报文创建
  • M 该路由已被重定向报文修改

netstat 也可以提供系统上的接口信息:

netstat -in

这个命令打印每个接口的MTU,输入分组数,输入错误,输出分组数,输出错误,冲突以及当前的输出队列的长度。

top工具

top

top 命令的汇总区域显示了五个方面的系统性能信息: 1. 负载:时间,登录用户数,系统平均负载 2. 进程:运行,睡眠,停止,僵尸 3. CPU :用户态,核心态,NICE,空闲,等待IO,中断等 4. 内存:总量,已用,空闲(系统角度),缓冲,缓存 5. 交换分区:总量,已用,空闲

任务区域默认显示:进程ID,有效用户,进程优先级,NICE值,进程使用的虚拟内存,物理内存和共享内存,进程状态,CPU 占用率,内存占用率,累计CPU时间,进程命令行信息。

查看某个进程的所有线程的CPU占用情况:

top -H -p [PID]

htop

另一个更好用的top的替代工具是htop,htop是Linux系统中的一个互动的进程查看器,一个文本模式的应用程序(在控制台或者X终端中),需要ncurses。

网游服务器性能优化工具收集整理  第11张

与Linux传统的top相比,htop更加人性化。它可让用户交互式操作,支持颜色主题,可横向或纵向滚动浏览进程列表,并支持鼠标操作。

与top相比,htop有以下优点:

  • 可以横向或纵向滚动浏览进程列表,以便看到所有的进程和完整的命令行。
  • 在启动上,比top 更快。
  • 杀进程时不需要输入进程号。
  • htop 支持鼠标操作。

参考资料:Linux下取代top的进程管理工具 htop

iotop

通过iostat和dstat我们可以知道系统的当前IO负载,但是IO负载具体是由哪个进程产生的呢?这时候我们需要的是iotop.

iotop是一个用来监视磁盘I/O使用状况的top类工具,具有与top相似的UI,其中包括PID、用户、I/O、进程等相关信息。 iotop使用Python语言编写而成,要求Python 2.5(及以上版本)和Linux kernel 2.6.20(及以上版本)。使用非常简单,在此不做过多介绍,详细信息参见官网:
http://guichaz.free.fr/iotop/

这个命令也可以以非交互的方式使用:

iotop -bod interval

查看每个进程的IO,也可以通过pidstat命令,不像iotop,且pidstat还不需要root权限。

pidstat -d interval

ps

最最常用的用法

ps aux #BSD
ps -ef #linux

ps 参数太多,具体使用方法,请man ps,下面是两种相对常用的使用方式。

  • 杀掉某一程序的方法。
  • ps aux | grep mysqld | grep -v grep | awk { print $2 } | xargs kill -9
  • 杀掉僵尸进程:
  • ps -eal | awk { if ($2 == "Z" ){ print $4}} | xargs kill -9

trace工具

strace

我们写程序,会调用很多库函数,而这些库函数,只是对系统调用的封装,它们最后都会去调用操作系统提供的系统调用,通过系统调用去访问硬件设备。strace的作用就是显示出这些调用关系,让程序员知道调用一个函数,它到底做了些什么事情。当然,strace 远比上面的介绍灵活多变。

下面来看一个例子:

查看mysqld 在linux上加载哪种配置文件,可以通过运行下面的命令行:

strace -e stat64 mysqld --print-defaults > /dev/null

strace太强大,内容也太多了,介绍strace的文章,请点击这里。

time工具

uptime

uptime是最最最最简单的工具了,但是也有值得讨论的地方,那就是它最后输出的三个数字是怎么得来的,有什么含义?

这三个数字的含义分别是1分钟、5分钟、15分钟内系统的平均负荷,关于这些数字的含义和由来,推荐看阮一峰的文章《理解linux系统负荷这里》。

我想说的是,这三个数字我天天看,时时看,但是我从来不相信它。

此话怎讲呢?我之所以天天看,时时看,是因为我将这三个数字显示到了tmux 的状态栏上,所以,我任何时候都可以看到,而不用专门输入uptime这个命令。

为什么我不相信它呢,因为这几个数字只能说明有多少线程在等待cpu,如果我们的一个任务有很多线程,系统负载不是特别高,但是这几个数字会出奇的高,也就是不能完全相信uptime的原因。如果不信,可以执行下面这条语句,然后再看看uptime的输出结果。

sysbench --test=mutex --num-threads=1600 --mutex-num=2048 \
--mutex-locks=1000000 --mutex-loops=5000 run

运行了5分钟以后,我的电脑上输出如下所示。需要强调的是,这个时候电脑一点不卡,看uptime来判断系统负载,跟听cpu风扇声音判断系统负载一样。只能作为线索,不能作为系统负载很高的依据。

20:32:39 up 10:21, 4 users, load average: 386.53, 965.37, 418.57

其他

lsof

lsof(list open files)是一个列出当前系统打开文件的工具。在linux环境下,任何事物都以 文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件。所以如传输 控制协议(TCP)和用户数据报协议(UDP)套接字等,系统在后台都为该应用程序分配了一个文件描 述符,无论这个文件的本质如何,该文件描述符为应用程序与基础操作系统之间的交互提供了 通用接口。因为应用程序打开文件的描述符列表提供了大量关于这个应用程序本身的信息,因此 通过lsof工具能够查看这个列表对系统监测以及排错将是很有帮助的。

lsof 的使用方法可以参考这里。这里仅列出几种常见的用法。

  1. 查看文件系统阻塞 lsof /boot
  2. 查看端口号被哪个进程占用 lsof -i :3306
  3. 查看用户打开哪些文件 lsof -u username
  4. 查看进程打开哪些文件 lsof -p 4838
  5. 查看远程已打开的网络链接 lsof -i @192.168.34.128

分析工具Perf简介

本文为翻译,英语好的同学请阅读原文。

介绍

Perf是一个基于Linux 2.6 +系统的分析工具,它抽象了在Linux中性能度量中CPU的硬件差异 ,提供一个简单的命令行界面。 Perf基于最新版本Linux内核 的perf_events接口。 这篇文章通过示例展示了 Perf工具的使用。 输出结果在Ubuntu 11.04(内核版本2.6.38-8-generic)上获得,硬件是在使用双核英特尔Core2 T7100 CPU的惠普6710 b。 为了可读性,一些输出使用省略号( […])。

命令

Perf工具提供了一组丰富的命令来收集和分析性能和跟踪数据。 命令行的用法与git类似,通过一个通用的命令Perf,实现了一组子命令: stat, record, report,[…]

支持的命令列表:

perf
usage: perf [--version] [--help] COMMAND [ARGS]
The most commonly used perf commands are:
annotate Read perf.data (created by perf record) and display annotated code
archive Create archive with object files with build-ids found in perf.data file
bench General framework for benchmark suites
buildid-cache Manage <tt>build-id</tt> cache.
buildid-list List the buildids in a perf.data file
diff Read two perf.data files and display the differential profile
inject Filter to augment the events stream with additional information
kmem Tool to trace/measure kernel memory(slab) properties
kvm Tool to trace/measure kvm guest os
list List all symbolic event types
lock Analyze lock events
probe Define new dynamic tracepoints
record Run a command and record its profile into perf.data
report Read perf.data (created by perf record) and display the profile
sched Tool to trace/measure scheduler properties (latencies)
script Read perf.data (created by perf record) and display trace output
stat Run a command and gather performance counter statistics
test Runs sanity tests.
timechart Tool to visualize total system behavior during a workload
top System profiling tool.
See perf help COMMAND for more information on a specific command.

某些需要特定内核支持的命令可能无法使用。如果想获得每个子命令的具体选项列表,只需输入命令名紧随其后 - h:

perf stat -h
usage: perf stat [<options>] [<command>]
-e, --event <event> event selector. use perf list to list available events
-i, --no-inherit child tasks do not inherit counters
-p, --pid <n> stat events on existing process id
-t, --tid <n> stat events on existing thread id
-a, --all-cpus system-wide collection from all CPUs
-c, --scale scale/normalize counters
-v, --verbose be more verbose (show counter open errors, etc)
-r, --repeat <n> repeat command and print average + stddev (max: 100)
-n, --null null run - dont start any counters
-B, --big-num print large numbers with thousands separators

事件

Perf工具支持一系列的可测量事件。这个工具和底层内核接口可以测量来自不同来源的事件。 例如,一些事件是纯粹的内核计数,在这种情况下的事件被称为软事件 ,例如:context-switches、minor-faults。

另一个事件来源是处理器本身和它的性能监视单元(PMU)。它提供了一个事件列表来测量微体系结构的事件,如周期数、失效的指令、一级缓存未命中等等。 这些事件被称为PMU硬件事件或简称为硬件事件。 它们因处理器类型和型号而异。

perf_events接口还提供了一组通用的的硬件事件名称。在每个处理器,这些事件被映射到一个CPU的真实事件,如果真实事件不存在则事件不能使用。可能会让人混淆,这些事件也被称为硬件事件硬件缓存事件

最后,还有由内核ftrace基础实现的tracepoint事件。但只有2.6.3x和更新版本的内核才提供这些功能。

可以通过命令获得可支持的事件列表:

perf list
List of pre-defined events (to be used in -e):
cpu-cycles OR cycles [Hardware event]
instructions [Hardware event]
cache-references [Hardware event]
cache-misses [Hardware event]
branch-instructions OR branches [Hardware event]
branch-misses [Hardware event]
bus-cycles [Hardware event]
cpu-clock [Software event]
task-clock [Software event]
page-faults OR faults [Software event]
minor-faults [Software event]
major-faults [Software event]
context-switches OR cs [Software event]
cpu-migrations OR migrations [Software event]
alignment-faults [Software event]
emulation-faults [Software event]
L1-dcache-loads [Hardware cache event]
L1-dcache-load-misses [Hardware cache event]
L1-dcache-stores [Hardware cache event]
L1-dcache-store-misses [Hardware cache event]
L1-dcache-prefetches [Hardware cache event]
L1-dcache-prefetch-misses [Hardware cache event]
L1-icache-loads [Hardware cache event]
L1-icache-load-misses [Hardware cache event]
L1-icache-prefetches [Hardware cache event]
L1-icache-prefetch-misses [Hardware cache event]
LLC-loads [Hardware cache event]
LLC-load-misses [Hardware cache event]
LLC-stores [Hardware cache event]
LLC-store-misses [Hardware cache event]
LLC-prefetch-misses [Hardware cache event]
dTLB-loads [Hardware cache event]
dTLB-load-misses [Hardware cache event]
dTLB-stores [Hardware cache event]
dTLB-store-misses [Hardware cache event]
dTLB-prefetches [Hardware cache event]
dTLB-prefetch-misses [Hardware cache event]
iTLB-loads [Hardware cache event]
iTLB-load-misses [Hardware cache event]
branch-loads [Hardware cache event]
branch-load-misses [Hardware cache event]
rNNN (see perf list --help on how to encode it) [Raw hardware event descriptor]
mem:<addr>[:access] [Hardware breakpoint]
kvmmmu:kvm_mmu_pagetable_walk [Tracepoint event]
[...]
sched:sched_stat_runtime [Tracepoint event]
sched:sched_pi_setprio [Tracepoint event]
syscalls:sys_enter_socket [Tracepoint event]
syscalls:sys_exit_socket [Tracepoint event]
[...]

一个事件可以有子事件(或掩码)。 在某些处理器上的某些事件,可以组合掩码,并在其中一个子事件发生时进行测量。最后,一个事件还可以有修饰符,也就是说,通过过滤器可以改变事件被计数的时间或方式。

硬件事件

PMU硬件事件取决与特定的CPU,由CPU供应商提供文档。如果将Perf工具与libpfm4库链接,则可以提供事件的一些简短描述。有关Intel和AMD处理器的PMU硬件事件的列表,请参阅

  • 英特尔PMU事件表:手册的附录A 在这里
  • AMD PMU事件表:手册的3.14节 在这里

使用perf stat进行统计

对于任何支持的事件,Perf可以在进程运行期间持续计数。 在统计模式下,在应用程序运行结束时事件的发生会被简单地汇总并显示在标准输出上。去产生这些统计数据,使用 stat命令的Perf。 例如:

perf stat -B dd if=/dev/zero of=/dev/null count=1000000
1000000+0 records in
1000000+0 records out
512000000 bytes (512 MB) copied, 0.956217 s, 535 MB/s
Performance counter stats for dd if=/dev/zero of=/dev/null count=1000000:
5,099 cache-misses # 0.005 M/sec (scaled from 66.58%)
235,384 cache-references # 0.246 M/sec (scaled from 66.56%)
9,281,660 branch-misses # 3.858 % (scaled from 33.50%)
240,609,766 branches # 251.559 M/sec (scaled from 33.66%)
1,403,561,257 instructions # 0.679 IPC (scaled from 50.23%)
2,066,201,729 cycles # 2160.227 M/sec (scaled from 66.67%)
217 page-faults # 0.000 M/sec
3 CPU-migrations # 0.000 M/sec
83 context-switches # 0.000 M/sec
956.474238 task-clock-msecs # 0.999 CPUs
0.957617512 seconds time elapsed

如果没有指定事件,perf stat会收集上面列出的常见事件。一些是软事件例如context-switches,另一些是通用硬件事件例如cycles。在哈希符号之后,可以显示衍生指标,例如“ IPC”(每个周期的指令)。。

事件控制选项

Perf工具可以测量的一个或多个事件。事件使用其符号名称指定,后可选跟随掩码和修饰符。事件名称、掩码和修饰符不区分大小写。

默认情况下,事件同时测量用户和内核级别:

perf stat -e cycles dd if=/dev/zero of=/dev/null count=100000

如果测量仅在用户级别,有增加一个修饰词:

perf stat -e cycles:u dd if=/dev/zero of=/dev/null count=100000

同时测量用户和内核(显式):

perf stat -e cycles:uk dd if=/dev/zero of=/dev/null count=100000

修饰符

事件可以通过冒号添加一个或多个修饰符。 修饰符允许用户对事件计数进行限制。

测量PMU事件,通过下示修饰符:

perf stat -e instructions:u dd if=/dev/zero of=/dev/null count=100000

在这个例子中,我们测量用户级别的指令数量。 注意,对于真实事件,修饰符取决于底层的PMU模型。 修饰符可以随意组合。 这张简单的表格,总结了用于Intel和AMD x86处理器的最常见的修饰符。

修饰符

描述

例子

u

priv 3,2,1级别监控(用户)

event:u

k

priv 0级别监控(内核)

event:k

h

在虚拟化环境中监视监控程序事件

event:h

H

在虚拟化环境中监视主机

event:H

G

在虚拟化环境中监视访客机

event:G

以上所有修饰符均视为布尔值(标志)。

硬件事件

要测量硬件供应商文档提供的实际PMU,请传递十六进制参数代码:

perf stat -e r1a8 -a sleep 1
Performance counter stats for sleep 1:
210,140 raw 0x1a8
1.001213705 seconds time elapsed

多个事件

要测量多个事件,只需提供一个用逗号分隔的列表,其中没有空格:

perf stat -e cycles,instructions,cache-misses [...]

理论上,对事件的数量是没有限制的。如果事件多余实际硬件计数器时,内核会自动多路复用。软事件的数量没有限制。你可以同时测量来自不同来源的事件。

然而,如果每个事件使用一个文件描述符,在per-thread(per-thread模式)或per-cpu(系统范围)模式下,则可能达到内核限制的每个进程的最大文件描述符数。在这种情况下,perf将报告一个错误。有关此问题的帮助,请参阅故障排除部分。

事件的多路复用和缩放

如果事件多于计数器,则内核会使用时间多路复用(开关频率= HZ,通常为100或1000)为每个事件提供访问监视硬件的机会。复用仅适用于PMU事件。使用多路复用时,不会一直测量事件。运行结束时,该工具会根据启用的总时间与运行时间来缩放计数。实际公式为:

final_count = raw_count * time_enabled / time_running

如果在整个运行过程中都对事件进行了测量,则可以估算该计数是多少。理解这是一个估计值而不是实际计数非常重要。工作负载较重时会有测量丢失,这种情况会在缩放时引入错误。

目前事件以循环方式进行管理,因此每个事件最终都将有机会运行。如果有N个计数器,则循环列表中最多前N个事件被编程到PMU中。在某些情况下它可能小于该值,因为某些事件可能无法一起测量或与它们使用同一计数器。此外,perf_events接口允许多个工具同时测量同一线程或CPU。每个事件都添加到相同的循环队列中。不能保证工具的所有事件都顺序存储在队列中。

为了避免缩放(在只有一个活动perf_event用户),你可以试着减少事件的数量。下表为一些常见的处理器提供计数器的数量:

处理器

通用的计数器

固定的计数器

英特尔酷睿

2

3

英特尔Nehalem

4

3

通用计数器可以测量任何事件,固定计数器只能测量一个事件。一些计数器可能是用于特殊用途,如看门狗定时器。

下面的例子显示缩放的影响:

perf stat -B -e cycles,cycles ./noploop 1
Performance counter stats for ./noploop 1:
2,812,305,464 cycles
2,812,304,340 cycles
1.302481065 seconds time elapsed

在这里,没有多路复用,因此没有缩放。 让我们再添加一个事件:

perf stat -B -e cycles,cycles,cycles ./noploop 1
Performance counter stats for ./noploop 1:
2,809,725,593 cycles (scaled from 74.98%)
2,810,797,044 cycles (scaled from 74.97%)
2,809,315,647 cycles (scaled from 75.09%)
1.295007067 seconds time elapsed

有多路复用,从而进行缩放。尝试保始终将事件A和B一起测量的方式非常有趣。尽管perf_events内核接口提供了对事件分组的支持,但当前的Perf工具没有。

重复测量

可以使用perf stat多次运行相同的测试工作,并针对每个计数获取均值的标准偏差。

perf stat -r 5 sleep 1
Performance counter stats for sleep 1 (5 runs):
<not counted> cache-misses
20,676 cache-references # 13.046 M/sec ( +- 0.658% )
6,229 branch-misses # 0.000 % ( +- 40.825% )
<not counted> branches
<not counted> instructions
<not counted> cycles
144 page-faults # 0.091 M/sec ( +- 0.139% )
0 CPU-migrations # 0.000 M/sec ( +- -nan% )
1 context-switches # 0.001 M/sec ( +- 0.000% )
1.584872 task-clock-msecs # 0.002 CPUs ( +- 12.480% )
1.002251432 seconds time elapsed ( +- 0.025% )

在这里,sleep运行5次,并打印每个事件的平均计数以及std-dev/mean的比率。

环境控制选项

Perf工具可用于统计每个线程、每个进程、每个cpu或整个系统的事件。在per-thread模式下,计数器只监视指定线程的执行。当线程被调度出时,监视将停止。当线程从一个处理器迁移到另一个处理器时,计数器在当前处理器上保存,并在新处理器上还原。

per-process模式是per-thread的一个变体,进程中的所有 线程都被监控。计数和采样在进程级别被合计。 perf_events接口允许自动继承fork ()和pthread_create ()。 默认情况下,Perf工具使能继承。

per-cpu的模式下,指定处理器上所有线程都被监控。计数和采样在每个CPU上合计。一个事件一次只能监视一个CPU。如果元跨多个处理器进行监控,则需要创建多个事件。Perf工具可以统计跨多个处理器计数和采样。它也可以只监视一个部分处理器。

计数和继承

默认情况下, perf stat统计进程的所有线程和后续的子进程和线程。这可以使用 -i选项进行切换。但它无法获得per-thread和per-process的详细计数。

Processor-wide模式

默认情况下, perf stat使用per-thread计数模式。 要按per-cpu计算,请使用-a选项。当选项被设置时,所有在线处理器都会被监视,并且计数会被合计。例如:

perf stat -B -ecycles:u,instructions:u -a dd if=/dev/zero of=/dev/null count=2000000
2000000+0 records in
2000000+0 records out
1024000000 bytes (1.0 GB) copied, 1.91559 s, 535 MB/s
Performance counter stats for dd if=/dev/zero of=/dev/null count=2000000:
1,993,541,603 cycles
764,086,803 instructions # 0.383 IPC
1.916930613 seconds time elapsed

测量收集了所有CPU上的事件周期和指令。测量的持续时间由dd的执行决定。换句话说,此测量捕获dd进程的执行以及所有cpu上在用户级别以外运行的任何内容。

若要计时测量的持续时间而不消耗周期,可以使用/usr/bin/sleep命令:

perf stat -B -ecycles:u,instructions:u -a sleep 5
Performance counter stats for sleep 5:
766,271,289 cycles
596,796,091 instructions # 0.779 IPC
5.001191353 seconds time elapsed

可以使用-C选项限制监视cpu的一个子集。可以传递要监视的CPU列表。例如,要在CPU0、CPU2和CPU3上测量:

perf stat -B -e cycles:u,instructions:u -a -C 0,2-3 sleep 5

演示机只有两个CPU,但我们可以限制为CPU 1。

perf stat -B -e cycles:u,instructions:u -a -C 1 sleep 5
Performance counter stats for sleep 5:
301,141,166 cycles
225,595,284 instructions # 0.749 IPC
5.002125198 seconds time elapsed

计数在所有被监视的CPU上合计。注意,当测量单个CPU时,计数的周期和指令的数量是减半的。

连接到一个正在运行的进程

可以使用Perf连接到已经运行的线程或进程。 这需要具有附加线程或进程ID的权限。若要附加到进程,-p选项必须是进程ID。若要附加到通常在许多Linux计算机上运行的sshd服务,使用:

ps ax | fgrep sshd
2262 ? Ss 0:00 /usr/sbin/sshd -D
2787 pts/0 S+ 0:00 fgrep --color=auto sshd
perf stat -e cycles -p 2262 sleep 2
Performance counter stats for process id 2262:
<not counted> cycles
2.001263149 seconds time elapsed

决定测量持续时间的是要执行的命令。即使我们附加到进程,我们仍然可以传递命令的名称。它用于计算测量时间。没有它,Perf将一直监视,直到它被杀死。还要注意,附加到进程时,将监视该进程的所有线程。此外,假设继承在默认情况下处于打开状态,子进程或线程也将被监视。要关闭此功能,必须使用-i选项。可以在进程中附加特定线程。所谓线程,我们指的是内核可见线程。换句话说,通过ps或top命令可见的线程。要附加到线程,必须使用-t选项。我们看一下rsyslogd,因为它总是在Ubuntu 11.04上运行,有多个线程。

ps -L ax | fgrep rsyslogd | head -5
889 889 ? Sl 0:00 rsyslogd -c4
889 932 ? Sl 0:00 rsyslogd -c4
889 933 ? Sl 0:00 rsyslogd -c4
2796 2796 pts/0 S+ 0:00 fgrep --color=auto rsyslogd
perf stat -e cycles -t 932 sleep 2
Performance counter stats for thread id 932:
<not counted> cycles
2.001037289 seconds time elapsed

在本例中,线程932在测量的2s期间没有运行。否则,我们将看到一个计数值。附加到内核线程是可能的,但实际上并不推荐这样做。考虑到内核线程倾向于固定到特定的CPU,最好使用cpu-wide模式。

控制输出选项

perf stat可以修改输出以满足不同的需求。

大数字输出

对大多数人来说,很难读懂很大的数字。使用perf stat,可以使用逗号分隔符打印数千个大数字(美式)。为此,必须设置-B选项和设置正确的语言环境LC_NUMERIC。如上面的例子所示,Ubuntu已经正确地设置了语言环境信息。显式调用如下所示:

LC_NUMERIC=en_US.UTF8 perf stat -B -e cycles:u,instructions:u dd if=/dev/zero of=/dev/null count=10000000
100000+0 records in
100000+0 records out
51200000 bytes (51 MB) copied, 0.0971547 s, 527 MB/s
Performance counter stats for dd if=/dev/zero of=/dev/null count=100000:
96,551,461 cycles
38,176,009 instructions # 0.395 IPC
0.098556460 seconds time elapsed

机器可读的输出

perf stat还可以打印计数,格式可以很容易地导入到电子表格或由脚本进行解析。-x选项改变输出的格式,并允许用户传递分隔符。这使得很容易生成CSV样式的输出:

perf stat -x, date
Thu May 26 21:11:07 EDT 2011
884,cache-misses
32559,cache-references
<not counted>,branch-misses
<not counted>,branches
<not counted>,instructions
<not counted>,cycles
188,page-faults
2,CPU-migrations
0,context-switches
2.350642,task-clock-msecs

请注意,选项-x与-B不兼容。

使用Perf记录采样

perf工具可用于收集per-thread、per-process和per-cpu的性能数据。

有几个与采样相关的命令:record、report、annotate。必须首先使用perf record收集样本。这将生成一个名为perf.data的输出文件。然后,可以使用perf report和perf annotate命令分析该文件(可能在另一台计算机上)。该方式类似于OProfile。

基于事件的采样

Perf_events基于基于事件的采样。周期表示为事件发生的次数,而不是计时器计时的次数。当采样计数器溢出时,即从2^64换回0时,记录采样。PMU没有实现64位硬件计数器,但perf_events在软件中模拟该计数器。

perf_events模拟64位计数器的方式仅限于使用实际硬件计数器中的位数来表示采样周期。在小于64位的情况下,内核会自动截断周期。因此,如果在32位系统上运行,最好周期始终小于2^31。

在计数器溢出时,内核记录有关程序执行的信息,也就是采样。记录的内容取决于测量的类型。这都是由使用者和工具指定的。但一般来说,样本中的关键信息是指令指针,即时程序中断在哪里。

基于中断的采样在现代处理器上引入了skid。这意味着每个采样的指令指针指向程序处理PMU中断的位置,而不是计数器实际溢出的位置,即它在采样周期结束时的位置。在某些情况下,如果有分支,这两个点之间的距离可能是几十条或更多的指令。当程序不再向前运行时,这两个位置确实是相同的。因此,在解释分析数据时必须小心。

默认事件:时钟周期计数

默认情况下, perf record使用时钟周期事件做为抽样事件。这是由内核映射到特定PMU事件的一个通用的硬件事件。对于英特尔来说,映射到 UNHALTED_CORE_CYCLES。在CPU频率扩展的情况下,此事件在时间上不能保持恒定不变。英特尔提供了另一个名为UNHALTED_REFERENCE_CYCLES的事件,但此事件当前不适用于perf_events。

在AMD系统中,事件映射到 CPU_CLK_UNHALTED事件,这个事件也受到频率扩展的影响。 在任何英特尔或AMD处理器,周期事件在处理器空闲时不计数,例如当它调用 mwait ()。

时间和速度

perf_events接口允许两种模式表达采样周期:

  • 事件发生的次数(时间)
  • 样本/秒的平均速率(频率)

Perf工具默认使用平均速率。它设置为1000 hz,或1000样本/秒。 这意味着内核会动态调整采样周期以达到目标平均速率。周期内的调整会在原始的分析数据中报告。与此相反,与其他模式相比,采样周期由用户设置,并且在采样之间不发生变化。目前不支持随机采样周期。

收集样本

默认情况下,perf record在运行在per-thread模式下,并且开始继承模式。当执行一个繁忙循环的简单程序时,简单的使用如下:

perf record ./noploop 1
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.002 MB perf.data (~89 samples) ]

上面的示例以1000Hz的平均目标速率收集事件周期的样本。生成的示例将保存到perf.data文件中。如果文件已经存在,可能会提示您通过-F覆盖它。要将结果放入特定文件中,请使用-o选项。

警告:报告的样本数只是估计值。它没有反映实际采集的样本数量。此估计基于写入perf.data文件的字节数和最小样本大小。但每个样本的真正大小取决于测量的类型。一些样本由计数器本身生成,而另一些样本则与后处理期间支持符号相关,例如mmap()信息。

要获取perf.data文件的准确样本数,可以使用perf report命令:

perf record ./noploop 1
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.058 MB perf.data (~2526 samples) ]
perf report -D -i perf.data | fgrep RECORD_SAMPLE | wc -l
1280

指可使用 -F选项自定义采样速度。 例如,仅在用户级别对事件指令进行采样,并且使用250个样本/秒的平均速率:

at an average rate of 250 samples/sec:
perf record -e instructions:u -F 250 ./noploop 4
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.049 MB perf.data (~2160 samples) ]

要指定采样周期,可以使用-c选项。例如,仅在用户级别收集每2000次事件指令的采样,请执行以下操作:

perf record -e retired_instructions:u -c 2000 ./noploop 4
[ perf record: Woken up 55 times to write data ]
[ perf record: Captured and wrote 13.514 MB perf.data (~590431 samples) ]

Processor-wide模式

在per-cpu模式下,收集受监控cpu上执行的所有线程的样本。要在per-cpu模式下切换perf record,需要使用-a选项。默认情况下,在此模式下,所有联机CPU都被监视。正如上面perf stat所解释的,可以使用-C选项限制到CPU的一个子集。

要在所有CPU上以1000个样本/秒的平均目标速率对用户和内核级别的周期采样5秒:

perf record -a -F 1000 sleep 5
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.523 MB perf.data (~22870 samples) ]

使用perf report分析采样

perf record收集的样本会保存到一个二进制文件中,默认情况下,该文件名为perf.data。perf report命令读取此文件并生成简明的执行概要文件。默认情况下,样本按函数排序,样本数最多的优先。可以自定义排序顺序,从而以不同的方式查看数据。

perf report
# Events: 1K cycles
#
# Overhead Command Shared Object Symbol
# ........ ............... .............................. .....................................
#
28.15% firefox-bin libxul.so [.] 0xd10b45
4.45% swapper [kernel.kallsyms] [k] mwait_idle_with_hints
4.26% swapper [kernel.kallsyms] [k] read_hpet
2.13% firefox-bin firefox-bin [.] 0x1e3d
1.40% unity-panel-ser libglib-2.0.so.0.2800.6 [.] 0x886f1
[...]

“Overhead”列指示在相应函数中收集的总样本的百分比。第二列显示被收集样本的进程。在per-thread/per-process模式下,这始终是受监视命令的名称。但在cpu-wide模式下,命令可能会有所不同。第三列显示了样本来源的ELF镜像的名称。如果程序是动态链接的,则这可能会显示共享库的名称。当样本来自内核时,使用伪ELF镜像名[kernel.kallsyms]。第四列指示样本运行的级别,也就是程序被中断时正在运行的级别:

最后一列显示了符号名称。

样本可以使用多种方式进行呈现,即使用排序。例如按共享对象进行排序,使用dso:

perf report --sort=dso
# Events: 1K cycles
#
# Overhead Shared Object
# ........ ..............................
#
38.08% [kernel.kallsyms]
28.23% libxul.so
3.97% libglib-2.0.so.0.2800.6
3.72% libc-2.13.so
3.46% libpthread-2.13.so
2.13% firefox-bin
1.51% libdrm_intel.so.1.0.0
1.38% dbus-daemon
1.36% [drm]
[...]

输出控制选项

为使输出更容易解析,可以修改列分隔符为某一个字符:

perf report -t

内核报告控制选项

Perf工具不知道如何从压缩内核映像(vmlinuz)中提取符号。因此,用户必须将非压缩内核镜像的路径通过 -k传递给Perf:

perf report -k /tmp/vmlinux

当然,内核镜像只有带debug符号编译的才能工作。

Processor-wide模式

在per-cpu的模式中,会从监控CPU上的所有线程上记录的样本。 这样,我们可以收集来自许多不同进程的样本。例如,如果我们监视所有CPU 5s:

perf record -a sleep 5
perf report
# Events: 354 cycles
#
# Overhead Command Shared Object Symbol
# ........ ............... .......................... ......................................
#
13.20% swapper [kernel.kallsyms] [k] read_hpet
7.53% swapper [kernel.kallsyms] [k] mwait_idle_with_hints
4.40% perf_2.6.38-8 [kernel.kallsyms] [k] _raw_spin_unlock_irqrestore
4.07% perf_2.6.38-8 perf_2.6.38-8 [.] 0x34e1b
3.88% perf_2.6.38-8 [kernel.kallsyms] [k] format_decode
[...]

当符号打印为十六进制地址时,这是因为ELF镜像没有符号表。当二进制文件被剥离时就会发生这种情况。我们也可以按cpu排序。这可能有助于确定工作负载是否平衡:

perf report --sort=cpu
# Events: 354 cycles
#
# Overhead CPU
# ........ ...
#
65.85% 1
34.15% 0

计算开销

perf收集调用链时,开销可以在两列中显示为“Children”和“Self”。“self”开销只是通过所有入口(通常是一个函数,也就是符号)的所有周期值相加来计算的。这是perf传统显示方式,所有“self”开销值之和应为100%。

“children”开销是通过将子函数的所有周期值相加来计算的,这样它就可以显示更高级别函数的总开销,即使它们不直接参与更多的执行。这里的“Children”表示从另一个(父)函数调用的函数。

所有“children”开销值之和超过100%可能会令人困惑,因为它们中的每一个已经是其子函数的“self”开销的累积。但是如果启用了这个功能,用户可以找到哪一个函数的开销最大,即使样本分布在子函数上。

考虑下面的例子,有三个函数如下所示。

void foo(void) {
/* do something */
}
void bar(void) {
/* do something */
foo();
}
int main(void) {
bar()
return 0;
}

在本例中,“foo”是“bar”的子级,“bar”是“main”的直接子级,因此“foo”也是“main”的子级。换句话说,“main”是“foo”和“bar”的父级,“bar”是“foo”的父级。

假设所有样本都只记录在“foo”和“bar”中。当使用调用链记录时,输出将在perf report的常规(仅自开销)输出中显示如下内容:

Overhead Symbol
........ .....................
60.00% foo
|
--- foo
bar
main
__libc_start_main
40.00% bar
|
--- bar
main
__libc_start_main

启用--children选项时,子函数(即foo和bar)的self开销值将添加到父函数中,以计算children开销。在这种情况下,报告可以显示为:

Children Self Symbol
........ ........ ....................
100.00% 0.00% __libc_start_main
|
--- __libc_start_main
100.00% 0.00% main
|
--- main
__libc_start_main
100.00% 40.00% bar
|
--- bar
main
__libc_start_main
60.00% 60.00% foo
|
--- foo
bar
main
__libc_start_main

在上述输出中,“foo”的“self”开销(60%)被添加到“bar”、“main”和“__libc_start_main”的“children”开销中。同样,“bar”的“self”开销(40%)添加到“main”和“libc”的“children”开销中。

因此,首先显示__libc_start_main和main,因为它们有相同(100%)的“子”开销(即使它们没有“自”开销),并且它们是foo和bar的父级。

从v3.16开始,默认情况下会显示“children”开销,并按其值对输出进行排序。通过在命令行上指定--no-children选项或在perf配置文件中添加“report.children=false”或“top.children=false”,禁用“children”开销。

使用perf annotate分析源码

可以使用perf annotate深入到指令级分析。为此,需要使用要解析的命令的名称调用perf annotate。所有带样本的函数都将被反汇编,每条指令都将报告其样本的相对百分比:

perf record ./noploop 5
perf annotate -d ./noploop
------------------------------------------------
Percent | Source code & Disassembly of noploop.noggdb
------------------------------------------------
:
:
:
: Disassembly of section .text:
:
: 08048484 <main>:
0.00 : 8048484: 55 push %ebp
0.00 : 8048485: 89 e5 mov %esp,%ebp
[...]
0.00 : 8048530: eb 0b jmp 804853d <main+0xb9>
15.08 : 8048532: 8b 44 24 2c mov 0x2c(%esp),%eax
0.00 : 8048536: 83 c0 01 add $0x1,%eax
14.52 : 8048539: 89 44 24 2c mov %eax,0x2c(%esp)
14.27 : 804853d: 8b 44 24 2c mov 0x2c(%esp),%eax
56.13 : 8048541: 3d ff e0 f5 05 cmp $0x5f5e0ff,%eax
0.00 : 8048546: 76 ea jbe 8048532 <main+0xae>
[...]

第一列报告在该指令在捕获函数==noploop()==的样本百分比。如前所述,您应该仔细解读这些信息。

如果使用-ggdb编译应用程序,perf annotate可以生成源代码级信息。下面的代码片段显示了在使用此调试信息编译noploop时,同一次执行noploop时的更多信息输出。

------------------------------------------------
Percent | Source code & Disassembly of noploop
------------------------------------------------
:
:
:
: Disassembly of section .text:
:
: 08048484 <main>:
: #include <string.h>
: #include <unistd.h>
: #include <sys/time.h>
:
: int main(int argc, char **argv)
: {
0.00 : 8048484: 55 push %ebp
0.00 : 8048485: 89 e5 mov %esp,%ebp
[...]
0.00 : 8048530: eb 0b jmp 804853d <main+0xb9>
: count++;
14.22 : 8048532: 8b 44 24 2c mov 0x2c(%esp),%eax
0.00 : 8048536: 83 c0 01 add $0x1,%eax
14.78 : 8048539: 89 44 24 2c mov %eax,0x2c(%esp)
: memcpy(&tv_end, &tv_now, sizeof(tv_now));
: tv_end.tv_sec += strtol(argv[1], NULL, 10);
: while (tv_now.tv_sec < tv_end.tv_sec ||
: tv_now.tv_usec < tv_end.tv_usec) {
: count = 0;
: while (count < 100000000UL)
14.78 : 804853d: 8b 44 24 2c mov 0x2c(%esp),%eax
56.23 : 8048541: 3d ff e0 f5 05 cmp $0x5f5e0ff,%eax
0.00 : 8048546: 76 ea jbe 8048532 <main+0xae>
[...]

使用perf annotate分析内核

Perf工具不知道如何从压缩内核镜像(vmlinuz)中提取符号。正如perf report中的示例,用户必须通过-k传递非压缩内核镜像的路径:

perf annotate -k /tmp/vmlinux -d symbol

在一次说明,这只使用带debug符号编译的内核。

使用perf top进行现场分析

perf工具可以以类似于Linux top工具的模式运行,实时打印采样函数。默认的采样事件是cycles,默认的顺序是每个符号的采样数递减,因此perf top显示了花费大部分时间的函数。默认情况下,perf top以processor-wide模式运行,在用户和内核级别监视所有在线的CPU。使用-C选项可以只监视CPU的一个子集。

perf top
-------------------------------------------------------------------------------------------------------------------------------------------------------
PerfTop: 260 irqs/sec kernel:61.5% exact: 0.0% [1000Hz
cycles], (all, 2 CPUs)
-------------------------------------------------------------------------------------------------------------------------------------------------------
samples pcnt function DSO
_______ _____ ______________________________ ___________________________________________________________
80.00 23.7% read_hpet [kernel.kallsyms]
14.00 4.2% system_call [kernel.kallsyms]
14.00 4.2% __ticket_spin_lock [kernel.kallsyms]
14.00 4.2% __ticket_spin_unlock [kernel.kallsyms]
8.00 2.4% hpet_legacy_next_event [kernel.kallsyms]
7.00 2.1% i8042_interrupt [kernel.kallsyms]
7.00 2.1% strcmp [kernel.kallsyms]
6.00 1.8% _raw_spin_unlock_irqrestore [kernel.kallsyms]
6.00 1.8% pthread_mutex_lock /lib/i386-linux-gnu/libpthread-2.13.so
6.00 1.8% fget_light [kernel.kallsyms]
6.00 1.8% __pthread_mutex_unlock_usercnt /lib/i386-linux-gnu/libpthread-2.13.so
5.00 1.5% native_sched_clock [kernel.kallsyms]
5.00 1.5% drm_addbufs_sg /lib/modules/2.6.38-8-generic/kernel/drivers/gpu/drm/drm.ko

默认情况下,第一列显示自运行开始以来的总样本数。通过按“Z”键,可以将其更改为打印自上次刷新以来的样本数。当处理器不处于暂停状态(即不空闲)时,cycle事件也会统计CPU周期。因此,这不等于墙面时间。此外,事件还受频率扩展的影响。

也可以深入到单个函数中,查看哪些指令具有最多的样本。要深入到指定函数,请按“s”键并输入函数名。这里我们选择了顶部函数noploop(上面没有显示):

------------------------------------------------------------------------------------------------------------------------------------------
PerfTop: 2090 irqs/sec kernel:50.4% exact: 0.0% [1000Hz cycles], (all, 16 CPUs)
------------------------------------------------------------------------------------------------------------------------------------------
Showing cycles for noploop
Events Pcnt (>=5%)
0 0.0% 00000000004003a1 <noploop>:
0 0.0% 4003a1: 55 push %rbp
0 0.0% 4003a2: 48 89 e5 mov %rsp,%rbp
3550 100.0% 4003a5: eb fe jmp 4003a5 <noploop+0x4>

使用perf bench进行基准测试

perf bench命令包含多个多线程微内核基准测试,用于在Linux内核和系统调用中执行不同的子系统。这使得黑客可以轻松地测量更改的影响,从而帮助缓解性能衰退。

它还充当一个通用的基准框架,使开发人员能够轻松地创建测试用例、透明进行整合和使用富性能工具子系统。

sched:调度器基准测试

测量多个任务之间的pipe(2)和socketpair(2)操作。允许测量线程与进程上下文切换的性能。

$perf bench sched messaging -g 64
# Running sched/messaging benchmark:
# 20 sender and receiver processes per group
# 64 groups == 2560 processes run
Total time: 1.549 [sec]

mem:内存访问基准测试

numa: numa调度和MM基准测试

futex: futex压力基准测试

处理futex内核实现的细粒度方面。它对于内核黑客非常有用。它目前支持唤醒和重新排队/等待操作,并强调私有和共享futexes的哈希方案。下面时nCPU线程运行的一个示例,每个线程处理1024个futex来测量哈希逻辑:

$ perf bench futex hash
# Running futex/hash benchmark:
Run summary [PID 17428]: 4 threads, each operating on 1024 [private] futexes for 10 secs.
[thread 0] futexes: 0x2775700 ... 0x27766fc [ 3343462 ops/sec ]
[thread 1] futexes: 0x2776920 ... 0x277791c [ 3679539 ops/sec ]
[thread 2] futexes: 0x2777ab0 ... 0x2778aac [ 3589836 ops/sec ]
[thread 3] futexes: 0x2778c40 ... 0x2779c3c [ 3563827 ops/sec ]
Averaged 3544166 operations/sec (+- 2.01%), total secs = 10

故障诊断和建议

本节列出了很多建议来避免使用Perf时常见的陷阱。

打开文件的限制

Perf工具所使用的perf_event内核接口的设计是这样的:它为per-thread或per-cpu的每个事件使用一个文件描述符。

在16-way系统上,当您这样做时:

perf stat -e cycles sleep 1

您实际上创建了16个事件,从而消耗了16个文件描述符。

在per-thread模式下,当您在同一16-way系统上对具有100个线程的进程进行采样时:

perf record -e cycles my_hundred_thread_process

然后,一旦创建了所有的线程,您将得到100*1(event)*16(cpus)=1600个文件描述符。Perf在每个CPU上创建一个事件实例。只有当线程在该CPU上执行时,事件才能有效地度量。这种方法加强了采样缓冲区的局部性,从而减少了采样开销。在运行结束时,该工具将所有样本合计到一个输出文件中。

如果Perf因“打开的文件太多”错误而中止,有以下几种解决方案:

  • 使用ulimit-n增加每个进程打开的文件数。注意:您必须是root
  • 限制一次运行中测量的事件数
  • 限制正在测量的CPU数量

增加打开文件限制

超级用户可以更改进程打开的文件限制,使用 ulimit shell内置命令:

ulimit -a
[...]
open files (-n) 1024
[...]
ulimit -n 2048
ulimit -a
[...]
open files (-n) 2048
[...]

使用build-id表示二进制文件

perf record命令在perf.data中保存者与测量相关的所有ELF镜像的唯一标识符。在per-thread模式下,这包括被监视进程的所有ELF镜像。在cpu-wide模式下,它包括系统上运行的所有进程。如果使用-Wl,--build-id选项,则链接器将生成这些唯一标识符。因此,它们被称为build-id。当将指令地址与ELF映像关联时,build id是一个非常有用的工具。要提取perf.data文件中使用的所有生成id项,请发出:

perf buildid-list -i perf.data
06cb68e95cceef1ff4e80a3663ad339d9d6f0e43 [kernel.kallsyms]
e445a2c74bc98ac0c355180a8d770cd35deb7674 /lib/modules/2.6.38-8-generic/kernel/drivers/gpu/drm/i915/i915.ko
83c362c95642c3013196739902b0360d5cbb13c6 /lib/modules/2.6.38-8-generic/kernel/drivers/net/wireless/iwlwifi/iwlcore.ko
1b71b1dd65a7734e7aa960efbde449c430bc4478 /lib/modules/2.6.38-8-generic/kernel/net/mac80211/mac80211.ko
ae4d6ec2977472f40b6871fb641e45efd408fa85 /lib/modules/2.6.38-8-generic/kernel/drivers/gpu/drm/drm.ko
fafad827c43e34b538aea792cc98ecfd8d387e2f /lib/i386-linux-gnu/ld-2.13.so
0776add23cf3b95b4681e4e875ba17d62d30c7ae /lib/i386-linux-gnu/libdbus-1.so.3.5.4
f22f8e683907b95384c5799b40daa455e44e4076 /lib/i386-linux-gnu/libc-2.13.so
[...]

build-id缓存

每次运行结束时,perf record命令都会更新一个build id缓存,其中包含带有样本的ELF镜像的新条目。缓存包含:

  • 带样本的ELF镜像的build-id
  • 带有样本的ELF镜像的副本

给定的build-id是不可变的,它们唯一地标识二进制文件。如果重新编译二进制文件,将生成新的build-id,并在缓存中保存ELF图像的新副本。缓存保存在磁盘上的默认目录$HOME/.debug中。系统管理员可以使用全局配置文件==/etc/perfconfig==为缓存指定备用全局目录:

$ cat /etc/perfconfig
[buildid]
dir = /var/tmp/.debug

在某些情况下,关掉 build-id缓存更新可能时有益的。为此,你需要使用perf record的 -n选项 性能记录

perf record -N dd if=/dev/zero of=/dev/null count=100000

访问控制

对于某些事件,必须是root才能调用perf工具。本文档假定用户具有root权限。如果您试图在权限不足的情况下运行perf,它将报告

No permission to collect system-wide stats.

其他场景

分析睡眠时间

此功能显示程序在何处睡眠或等待某物的时间和时间。

第一步是收集数据。我们需要收集sched_stat和sched_switch事件。Sched_stat事件是不够的,因为它们是在任务的上下文中生成的,这会唤醒目标任务(例如释放锁)。我们需要相同的事件,但带有目标任务的调用链。此调用链可以从之前的sched_switch事件中提取。

第二步是合并sched_start和sched_switch事件。这可以通过“perf-inject-s”来完成。

$ ./perf record -e sched:sched_stat_sleep -e sched:sched_switch -e sched:sched_process_exit -g -o ~/perf.data.raw ~/foo
$ ./perf inject -v -s -i ~/perf.data.raw -o ~/perf.data
$ ./perf report --stdio --show-total-period -i ~/perf.data
# Overhead Period Command Shared Object Symbol
# ........ ............ ....... ................. ..............
#
100.00% 502408738 foo [kernel.kallsyms] [k] __schedule
|
--- __schedule
schedule
|
|--79.85%-- schedule_hrtimeout_range_clock
| schedule_hrtimeout_range
| poll_schedule_timeout
| do_select
| core_sys_select
| sys_select
| system_call_fastpath
| __select
| __libc_start_main
|
--20.15%-- do_nanosleep
hrtimer_nanosleep
sys_nanosleep
system_call_fastpath
__GI___libc_nanosleep
__libc_start_main
$cat foo.c
...
for (i = 0; i < 10; i++) {
ts1.tv_sec = 0;
ts1.tv_nsec = 10000000;
nanosleep(&ts1, NULL);
tv1.tv_sec = 0;
tv1.tv_usec = 40000;
select(0, NULL, NULL, NULL,&tv1);
}
...

参考文档:

Linux kernel profiling with perf

三款Profiler工具

补充,三款Linux平台下主流的热点分析工具,分别是GNU gprof、Valgrind和Google perftools,三款工具的主要特点如下表:

工具

使用命令

是否需要重新编译

Profiling速度

是否支持多线程热点分析

是否支持链接库热点分析

GNU gprof

./test; gprof ./test ./gmon.out

Valgrind

Valgrind –tool=callgrind ./test

非常慢

Google perftools

LD_PRELOAD=/usr/lib/libprofiler.so CPUPROFILE=./test.prof ./test

Cpuload测试程序

#include <vector>
#include <iostream>

#ifdef WITHGPERFTOOLS
#include <gperftools/profiler.h>
#endif

using namespace std;

int foo(vector<int> v) {
int result = 0;
for(auto x: v) {
result += x;
}
return result % 1000;
}

int main() {
#ifdef WITHGPERFTOOLS
ProfilerStart("profile.log");
#endif
vector<int> v;
v.push_back(1);

int result = 0;
for (int i=0; i<10000; i++) {
result = foo(v);
v.push_back(result);
}
#ifdef WITHGPERFTOOLS
ProfilerStop();
#endif
cout << result << "\n";
return 1;
}

GNU gprof

是GNU G++自带的热点分析工具,使用方法是:1. 使用-pg选项重新编译代码;2. 执行程序./test,生成热点分析结果gmont.out;3.使用gprof查看结果gprof ./test ./gmon.out。因为gprof要求用-pg重新编译代码,需要在Debug模式下进行Profiling,所以速度较慢。另外gprof不支持多线程的热点分析。这个工具另一个大问题是,不支持链接库的热点分析。很多大型项目为了模块化管理会生成很多动态链接库供其他程序调用,如果要分析每个模块的热点,这个工具就不适用了。

测试脚本如下:

#!/bin/bash

# build the program with profiling support (-gp)
g++ -std=c++11 -pg cpuload.cpp -o cpuload

# run the program; generates the profiling data file (gmon.out)
./cpuload

# print the callgraph
gprof cpuload

Valgrind

是一系列工具的套装,包括内存分析、热点分析等。它的使用非常简单,安装好之后,直接调用Vallgrind中的callgrind工具即可,命令为Valgrind –tool=callgrind ./test。使用该工具Profiling无需重新编译代码,也支持多线程和链接库的热点分析,但是由于Profiling原理的特殊性,其Profiling速度非常之慢,比直接运行程序慢了将近50倍,所以并不适合稍大型程序的热点分析。

测试脚本如下:

#!/bin/bash

# build the program (no special flags are needed)
g++ -std=c++11 cpuload.cpp -o cpuload

# run the program with callgrind; generates a file callgrind.out.12345 that can be viewed with kcachegrind
valgrind --tool=callgrind ./cpuload

# open profile.callgrind with kcachegrind
kcachegrind profile.callgrind

与gprof相反,我们不需要使用任何特殊的编译标志来重建应用程序。我们可以像valgrind一样执行任何可执行文件。当然,执行的程序应该包含调试信息,以获得具有人类可读符号名称的表达性调用图。

下面是一个KCachegrind,其中包含cpuload演示的评测数据:

网游服务器性能优化工具收集整理  第12张

Google perftools

原是Google内部的性能分析工具,后来在Github上开源了,地址是
https://github.com/gperftools/gperftools。这套工具提供了CPU探查器、快速线程感知malloc实现、内存泄漏检测器和堆探查器。gperftools 的工作原理为通过定期采样当前正在执行的指令进行性能分析,如果某个函数被采样到的次数越多,则该函数在执行时占用的时间比例越大,很可能就是性能瓶颈。 gperftools 可以在被测软件处于 Release 模式下进行性能分析,所以能最大程度的模拟软件的实际使用情况。这个工具使用起来也非常简单,只需Preload其.so文件并指定生成的Profiling文件路径即可,命令为LD_PRELOAD=/usr/lib/libprofiler.so CPUPROFILE=./test.prof ./test。程序结束之后,使用命令google-pprof –web ./test ./test.prof即可查看热点分析结果。使用该工具Profiling无需重新编译代码,也支持多线程和链接库的热点分析,同时由于其是通过定期采样正在执行的指令进行热点分析,所以Profiling速度非常快,和正常release下执行程序的速度几乎相当。通过试用,发现gperftools的易用性、可视化效果等都是最好的,所以推荐大家使用gperftools。

下面主要介绍一下CPU profiler的使用情况。

使用gperftools创建应用程序选定部分的CPU配置文件需要以下步骤:

编译程序时启用调试符号(以获得有意义的调用图),并链接gperftools profiler.so

#include <gperftools/profiler.h>,并要使用ProfilerStart(“nameOfProfile.log”);和ProfilerStop()包围需要分析的代码块;

执行程序以生成分析数据文件

要分析评测数据,请使用pprof(与gperftools一起分发)或将其转换为与callgrind兼容的格式,然后使用KCachegrind进行分析。

测试脚本如下:

#!/bin/bash

# build the program; For our demo program, we specify -DWITHGPERFTOOLS to enable the gperftools specific #ifdefs
g++ -std=c++11 -DWITHGPERFTOOLS -lprofiler -g ../cpuload.cpp -o cpuload

# run the program; generates the profiling data file (profile.log in our example)
./cpuload

# convert profile.log to callgrind compatible format
pprof --callgrind ./cpuload profile.log > profile.callgrind

# open profile.callgrind with kcachegrind
kcachegrind profile.callgrind

结论

gprof是经过评估的剖析器中的恐龙——它的起源可以追溯到20世纪80年代。在过去的几十年里,它似乎得到了广泛的应用,是一个很好的解决方案。但它对多线程应用程序的支持有限,无法分析共享库,需要使用兼容的编译器和特殊标志重新编译,这会产生相当大的运行时开销,因此不适合在当今的实际项目中使用它。

Valgrind提供最精确的结果,非常适合多线程应用。它非常容易使用,并且有KCachegrind用于可视化/分析分析分析分析数据,但是被测应用程序的缓慢执行使其无法用于更大、运行时间更长的应用程序。

gperftools CPU profiler的运行时开销很小,提供了一些很好的功能,比如有选择地分析某些感兴趣的领域,并且对多线程应用程序没有问题。KCachegrind可用于分析分析分析数据。与所有基于采样的探查器一样,它存在统计不准确性,因此结果不如Valgrind准确,但实际上这通常不是什么大问题(如果需要更准确的结果,可以随时增加采样频率)。推荐使用它。

实际项目服务器性能提升经验

网游服务器常会遇到的问题

DB IO的处理

当我们用机器人做压力测试, 往往会发现DB队列总是一个痛点.

  • 绝大部分操作都是道具上的
  • 道具占最多这个是能想到的. 原因可能:
    • 生成一个道具需要写多次DB, 如,一次记录道具本身, 一次记录用来做道具最大ID(算唯一ID用的)
    • 更新一个道具的时候, 很有可能更新了多次
    • 玩家登陆的时候, 会把刚刚load的每个道具都保存一次
  • 这是道具本身实现不太合理的地方, 还有就是机器人程序, 测试程序本身也要设计的比较合理, 不合理的道具压测实现也会造成DB压力非常大. 例如:
    • 某个功能机器人会把所有的装备都删一遍, 然后再加一遍
    • 某个功能机器人可能会不停的添加道具(或者装备), 最后背包满了, 就要往邮件里面塞
    • 类似的功能有很多等等
  • 测试程序本身, 需要比较合理的设计, 尽可能去贴合玩家的真实操作.
  • 玩家的定时存档
  • 大部分操作都是立即存档的, 但是涉及到Player表, 就会延迟存档(大概1-2分钟), 这是MMOG常用的操作.
  • 可能的解决办法,可以把多数玩家集中保存操作打散(每个玩家有自己的保存时间,为了防止玩家集中时间登入游戏,造成savedb相对集中,可以考虑在一段时间内随机), 帧率才稳定.
  • dbIO造成非常多的垃圾对象。可以考虑使用内存池技术,避免生成大量小对象;还有就是减少dbio的操作,可以尝试合批后,一并提交。
  • DB操作的时间越来越长
  • db操作的表,或对象是否有建立索引;
  • 减少很多不必要的DB操作,合批;
  • 使用连接池+线程池的办法增加db并发能力;
  • 合理设计数据结构,避免巨大的单条记录,考虑不同属性分表;超大数据库分库等等。
  • 采用商业化的云db产品(如mysql,mongodb,redis等)

广播和网络IO处理

整个核心思想就是减少每一个包上的编解码消耗(以及产生的垃圾对象),并且在一定程度上实现合批(比如,每个逻辑帧合并一批的网络消息;或者消息累加到一定数量/大小,再合并发出).

合理规划同步消息, 例如:

  • 玩家移动消息
  • 因为很多客户端为了提升游戏体验,对移动有预判,不会等到服务器返回便自己开始走,服务返回之后会不断矫正的位置,差别不大就不需要矫正;差异比较大时会有矫正过程,一条客户端消息会可能会有多条服务器消息返回。
  • 一个跳跃有4个左右的消息, 一个滑步有3个左右的消息
  • 有的游戏设定,每次跳跃和滑步都需要使用怒气(能量类似的东西),然后这些东西加减, 也需要同步给所有客户端, 实际上这些可以让客户端自己去模拟和维护. 还有跳跃和滑步也是, 最多1~2个输入就可以完成.
  • 战斗部分
  • 由于很多技能是无目标战斗(AOE技能), 砍一刀很有可能砍刀10个怪, 但是伤害如果发10个怪, 那么就需要做10个编解码, 发10次广播消息。
  • 其他部分
  • AOI算法,多人同屏的时候,非常重要,常用算法:九宫格、灯塔、十字链表、三轴十字链表等等。
  • 九宫格AOI算法,核心是把整个地图划分成大小相等的正方形格子,每个格子有容器存储格子里的玩家;
  • 灯塔AOI,是把整个地图划分成比较大的格子,每个格子称为一个灯塔,玩家视野一般涉及上下左右4个灯塔,网易的pomelo有实现这个算法;
  • 十字链表,把场景中的实体按位置从小到大用双向链表保存起来,X轴用一条双向链表,Y轴用一条双向链表,因为在画坐标时X轴和Y轴刚好呈十字,所以称十字链表(链表中保存的是一个点;或者,链表中保存的是一条线段)。

内存分配的优化

内存分配情况,需要借助于Visual Studio之类的工具来分析了, 具体可以看前面的文章。 处理方式也比较简单--使用pool,重写内存分配器;找到一个非法使用情况,就fix一个.

比较容易遗漏的小型对象的申请:比如闭包,如闭包在Player处理某个东西时候需要,那么就把闭包和闭包的状态存在Player对象上(闭包也是一个小型对象);临时的容器,这个可以使用threadlocal来优化,每次用的时候clear一下。

还有一个比较关键的是, Linux下native部分的内存分配. 可能服务器在Windows Server下长时间跑, 都没有内存泄漏, 但是在Linux下跑会有内存泄漏, 最后查找原因是非托管部分泄漏了. 可尝试换成jemalloc之后解决。

性能优化的建议

用profile工具优化性能时,内存申请释放可能会影响部分性能(特别是很多小型对象),比如某些语言的GC(如java,c#),会很耗时;频繁的申请释放内存,即使对c或c++这样的语言来说,也是极力避免的。考虑使用内存池技术,提升申请和释放效率。

当内存/GC问题解决掉了之后,之前提到的很多性能问题都可能会被缓解,包括DB执行更新操作耗时越来越长这样的情况(当然首先得是db本身压力不大)。

物理引擎的耗时,很可能会占用整个逻辑帧30%左右的时间片。比如,用一些高效的碰撞器替代计算复杂的碰撞器,比如球型碰撞器的性能 > Box碰撞器 > Mesh碰撞器。对非必要的高频操作降频处理(如调整物理引擎fixedUpdate)等。另外,物理引擎建议和逻辑功能分开成不同的服务器。

每逻辑帧处理的功能越少,计算量越少,在精度等因素允许的情况下尽可能减少每帧的运算量。可以分帧处理某些复杂逻辑。

多线程/并行化,可以使用线程池或并行库(ppl/tbb)等来提升并发处理能力。

The End