引言:性能瓶颈调优
在实际的性能测试中,会遇到各种各样的问题,比如 TPS 压不上去等,导致这种现象的原因有很多,测试人员应配合开发人员进行分析,尽快找出瓶颈所在。
理想的性能测试指标结果可能不是很高,但一定是平缓的。
性能调优步骤
-
确定问题:根据性能监控的数据和性能分析的结果,确定性能存在的问题。
-
确定原因:确定问题之后,对问题进行分析,找出问题的原因。
-
确定解决方案(改服务器参数配置/增加硬件资源配置/修改代码)。
-
验证解决方案,分析调优结果。
注意:性能测试调优并不是一次完成的过程,针对同一个性能问题,上述步骤可能要经过多次循环才能最终完成性能调优的目标,即:测试发现问题 -> 找原因 -> 调整 -> 验证 -> 分析 -> 再测试 ...
性能瓶颈概率分布
60%:数据库瓶颈
- 数据库服务器 CPU 使用率高(慢查询、SQL 过多、连接数过多)
- 抛出连接数过多(连接池设置太小,导致连接排队)
- 数据库出现死锁
25%:应用瓶颈
- 应用出现内存泄露
- 应用出现线程竞争/死锁
- 程序代码的算法复杂度
- 中间件、第三方应用出现异常
- 计算密集型任务引起 CPU 负载高
- I/O 密集型任务引起 I/O 负载高
10%:压测工具瓶颈
- JMeter 单机负载能力有限,如果需要模拟的用户请求数超过其负载极限,也会导致 TPS 压不上去
5%:Linux 机器出现异常
- Linux 可用内存无法回收(开销速率大于回收速率)
系统资源
-
CPU
- 监控内容:CPU 使用率、CPU 使用类型(用户进程、内核进程)
- 瓶颈分析:CPU已压满(接近 100%),需要再看其他指标的拐点所出现的时刻是否与 CPU 压满的时刻基本一致。
-
内存
- 监控内容:实际内存、虚拟内存
- 瓶颈分析:内存不足时,操作系统会使用虚拟内存,从虚拟内存读取数据,影响处理速度。
-
磁盘 I/O
- 监控内容:I/O 速度、磁盘等待队列
- 瓶颈分析:磁盘 I/O 成为瓶颈时,会出现磁盘I/O繁忙,导致交易执行时在 I/O 处等待。
-
网络
- 监控内容:网络流量(带宽使用率)、网络连接状态
- 瓶颈分析:如果接口传递的数据包过大,超过了带宽的传输能力,就会造成网络资源竞争, 导致 TPS 上不去。
发现了瓶颈后,只要对症下药就可以了。简单来说无论哪个地方出现瓶颈,只需要降低压力或者增加这部分瓶颈资源(应用软件没有瓶颈或优化空间之后),即可缓解症状。
- CPU 瓶颈:增加 CPU 资源。
- 内存瓶颈:增加内存、释放缓存。
- 磁盘 I/O 瓶颈:更换性能更高的磁盘(如固态 SSD)。
- 网络带宽瓶颈;增加网络带宽。
CPU
后台服务的所有指令和数据处理都是由 CPU 负责,服务对 CPU 的利用率对服务的性能起着决定性的作用。
top 参数详解
下面以 top 命令的输出例,对 CPU 各项主要指标进行说明:
-
us(user):运行(未调整优先级的)用户进程所消耗的 CPU 时间的百分比。
-
像 shell 程序、各种语言的编译器、数据库应用、web 服务器和各种桌面应用都算是运行在用户地址空间的进程。
-
这些程序如果不是处于 idle 状态,那么绝大多数的 CPU 时间都是运行在用户态。
-
-
sy(system):运行内核进程所消耗的 CPU 时间的百分比。
-
所有进程要使用的系统资源都是由 Linux 内核处理的。当处于用户态(用户地址空间)的进程需要使用系统的资源时,比如需要分配一些内存、或是执行 I/O 操作、再或者是去创建一个子进程,此时就会进入内核态(内核地址空间)运行。事实上,决定进程在下一时刻是否会被运行的进程调度程序就运行在内核态。
-
对于操作系统的设计来说,消耗在内核态的时间应该是越少越好。通常 sy 比例过高意味着被测服务在用户态和系统态之间切换比较频繁,此时系统整体性能会有一定下降。
-
在实践中有一类典型的情况会使 sy 变大,那就是大量的 I/O 操作,因此在调查 I/O 相关的问题时需要着重关注它。
-
大部分后台服务使用的 CPU 时间片中 us 和 sy 的占用比例是最高的。同时这两个指标又是互相影响的,us 的比例高了,sy 的比例就低,反之亦然。
-
另外,在使用多核 CPU 的服务器上,CPU 0 负责 CPU 各核间的调度,CPU 0 上的使用率过高会导致其他 CPU 核心之间的调度效率变低。因此测试过程中需要重点关注 CPU 0。
-
-
ni(niced):用做 nice 加权的进程分配的用户态 CPU 时间百分比。
- 每个 Linux 进程都有个优先级,优先级高的进程有优先执行的权利,这个叫做 pri。进程除了优先级外,还有个优先级的修正值。这个修正值就叫做进程的 nice 值。
- 这里显示的 ni 表示调整过 nice 值的进程消耗掉的 CPU 时间。如果系统中没有进程被调整过 nice 值,那么 ni 就显示为 0。
- 一般来说,被测服务和服务器整体的 ni 值不会很高。如果测试过程中 ni 的值比较高,需要从服务器 Linux 系统配置、被测服务运行参数查找原因。
-
id(idle):空闲的 CPU 时间百分比。
-
一般情况下, us + ni + id 应该接近 100%。
-
线上服务运行过程中,需要保留一定的 id 冗余来应对突发的流量激增。
-
在性能测试过程中,如果 id 一直很低,吞吐量上不去,需要检查被测服务线程/进程配置、服务器系统配置等。
-
-
wa(I/O wait):CPU 等待 I/O 完成时间百分比。
-
和 CPU 的处理速度相比,磁盘 I/O 操作是非常慢的。有很多这样的操作,比如:CPU 在启动一个磁盘读写操作后,需要等待磁盘读写操作的结果。在磁盘读写操作完成前,CPU 只能处于空闲状态。
-
Linux 系统在计算系统平均负载时会把 CPU 等待 I/O 操作的时间也计算进去,所以在我们看到系统平均负载过高时,可以通过 wa 来判断系统的性能瓶颈是不是过多的 I/O 操作造成的。
-
磁盘、网络等 I/O 操作会导致 CPU 的 wa 指标提高。通常情况下,网络 I/O 占用的 wa 资源不会很高,而频繁的磁盘读写会导致 wa 激增。
-
如果被测服务不是 I/O 密集型的服务,那需要检查被测服务的日志量、数据载入频率等。
-
如果 wa 高于 10% 则系统开始出现卡顿;若高于 20% 则系统几乎动不了;若高于 50% 则很可能磁盘出现故障。
-
-
hi:硬中断消耗时间百分比。
-
si:软中断消耗时间百分比。
- 硬中断是外设对 CPU 的中断,即外围硬件发给 CPU 或者内存的异步信号就是硬中断信号;软中断由软件本身发给操作系统内核的中断信号。
- 通常是由硬中断处理程序或进程调度程序对操作系统内核的中断,也就是我们常说的系统调用(System Call)。
- 在性能测试过程中,hi 会有一定的 CPU 占用率,但不会太高。对于 I/O 密集型的服务,si 的 CPU 占用率会高一些。
-
st:虚拟机等待 CPU 资源的时间。
- 只有 Linux 在作为虚拟机运行时 st 才是有意义的。它表示虚机等待 CPU 资源的时间(虚机分到的是虚拟 CPU,当需要真实的 CPU 时,可能真实的 CPU 正在运行其它虚机的任务,所以需要等待)。
案例分析
现象:wa 与 id
-
wa(IO wait)的值过高,表示硬盘存在 I/O 瓶颈。
-
id(idle)值高,表示 CPU 较空闲。
-
如果 id 值高但系统响应慢时,有可能是 CPU 等待分配内存,此时应加大内存容量。
-
如果 id 值持续低于 10,那么系统的 CPU 处理能力相对较低,表明系统中最需要解决的资源是 CPU。
-
现象:CPU 的 us 和 sy 不高,但 wa 很高。
如果被测服务是磁盘 I/O 密集型服务,wa 高属于正常现象。但如果不是此类服务,最可能导致 wa 高的原因有两个:
-
服务对磁盘读写的业务逻辑有问题,读写频率过高,写入数据量过大,如不合理的数据载入策略、log 过多等,都有可能导致这种问题。
-
服务器内存不足,服务在 swap 分区不停的换入换出。
现象:CPU 与吞吐量
-
CPU 占用不高,吞吐量较低,可能是服务端线程池启动太少。
-
CPU 占用很高,吞吐量较低,服务端处理慢,可能操作数据库慢。
-
CPU 占用很高,吞吐量很高:
- 服务端处理能力强,需要调整线程数降低 CPU 使用率。
- 数据库连接数、慢 SQL、文件句柄优化。
- 提升物理设备。
LOAD
Linux 的系统负载指在特定时间间隔内(一个 CPU 周期)运行队列中的平均进程数。
(注意:Linux 中的 Load 体现的是整体系统负载,即 CPU 负载 + 磁盘负载 + 网络负载 + 其余外设负载,并不能完全等同于 CPU 使用率。而在其余系统如 Unix,Load 还是只代表 CPU 负载。)
从服务器负载的定义可以看出,服务器运行最理想的状态是所有 CPU 核心的运行队列都为 1,即所有活动进程都在运行,没有等待。
这种状态下服务器运行在负载阈值下。
通常情况下,按照经验值,服务器的负载应位于阈值的 70%~80%,这样既能利用服务器大部分性能,又留有一定的性能冗余应对流量增长。
查看系统负载阈值的命令如下:
Linux 提供了很多查看系统负载的命令,最常用的是 top 和 uptime。
top 和 uptime 针对负载的输出内容相同,都是系统最近 1 分钟、5 分钟、15 分钟的负载均值
:
这三个数值的使用方法和 CPU 核数相关,首先确认 CPU 物理总核数:
-
/proc/cpuinfo 中的 processors 的最大值不一定是 CPU 的核数,有可能该 CPU 支持超线程技术,从而 processors 是物理核数的 2 倍。
-
这里我们需要准确的核数,具体方法为:找到 /proc/cpuinfo 文件中所有的 physical id 后的数值,取得最大的数值,加一后就是实际的 CPU 个数。然后查找任意一个 processors 下的 cpu cores,即是该颗 CPU 的核数,实际 CPU 个数乘以核数即为 CPU 的物理总核数。
示例:
[root@localhost home]# cat /proc/cpuinfo |grep "physical id"
physical id : 0
physical id : 0
[root@localhost home]# cat /proc/cpuinfo |grep "cpu cores"
cpu cores : 2
cpu cores : 2
物理 CPU 个数为 0+1=1 个,每个 CPU 的核数为 2 个,所以总的物理核数为 2x1=2。
计算结果说明该机器的在单位时间内可以处理的进程数是 2 个,如果单位时间内进程数超过 2 个,就会出现拥堵的情况,load 就会持续增高,增高到一定程度,就会出现系统崩溃等异常情况。
在性能测试过程中,系统负载是评价整个系统运行状况最重要的指标之一。通常情况下:
-
负载测试时:系统负载应接近但不能超过阈值。
-
并发测试时:系统负载最高不能超过阈值的 80%。
-
稳定性测试时:系统负载应在阈值的 50% 左右。
机器针对突发情况的处理
-
如果 1 分钟 load 很高,5 分钟 load 较高,15 分钟 load 起伏不大的情况下,说明该次高 load 为突发情况,可以容忍。
-
如果高 load 持续,导致 5 分钟和 15 分钟 load 都已经超过报警值,这时候需要考虑进行处理。
-
如果 15 分钟 load 高于 1 分钟 load,说明高 load 情况已经得到缓解。
内存
性能测试过程中对内存监控的主要目的是检查被测服务所占用内存的波动情况。
top 参数详解
在 Linux 系统中有多个命令可以获取指定进程的内存使用情况,最常用的是 top 命令,如下图所示:
-
VIRT:进程所使用的虚拟内存的总数。它包括所有的代码,数据和共享库,加上已换出的页面,所有已申请的总内存空间。
-
RES:进程正在使用的没有交换的物理内存(栈、堆)。申请内存后该内存段已被重新赋值。
-
SHR:进程使用共享内存的总数。该数值只是反映可能与其它进程共享的内存,不代表这段内存当前正被其他进程使用。
-
SWAP:进程使用的虚拟内存中被换出的大小。交换的是已经申请但没有使用的空间(包括栈、堆、共享内存)。
-
DATA:进程除可执行代码以外的物理内存总量,即进程栈、堆申请的总空间。
从上面的解释可以看出,测试过程中主要监控 RES 和 VIRT。对于使用了共享内存的多进程架构服务,还需要监控 SHR。
free 参数详解
free 命令显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。如果加上 -h 选项(控制显示单位),输出的结果会友好很多:
有时我们需要持续的观察内存的状况,此时可以使用 -s 选项并指定间隔的秒数:如 free -h -s 3 表示每隔 3 秒输出一次内存的使用情况,直到按下 ctrl + c。
-
Mem 行:物理内存的使用情况。
-
Swap 行:交换空间的使用情况。
-
swap space 是磁盘上的一块区域,可以是一个分区,也可以是一个文件,所以具体的实现可以是 swap 分区也可以是 swap 文件。当系统物理内存吃紧时,Linux 会将内存中不常访问的数据保存到 swap 上,这样系统就有更多的物理内存为各个进程服务,而当系统需要访问 swap 上存储的内容时,再将 swap 上的数据加载到内存中,这就是常说的换出和换入。
-
交换空间可以在一定程度上缓解内存不足的情况,但是它需要读写磁盘数据,所以性能不是很高。因此
当交换空间内存开始使用,则表明内存严重不足
。 -
如果系统内存充足或是做性能压测的机器,可以使用 swapoff -a 关闭交换空间,或在 /etc/sysctl.conf 文件中设置 swappiness 值。
如果系统内存不富余,则需要根据物理内存的大小来设置交换空间的大小,具体的策略网上有很丰富的资料。
-
-
total 列:系统总的可用物理内存和交换空间大小。
-
used 列:已经被使用的物理内存和交换空间大小。
-
free 列:还有多少物理内存和交换空间可用使用(真正尚未被使用的物理内存数量)。
在吞吐量固定的前提下,如果内存持续上涨,那么很有可能是被测服务存在明显的内存泄漏,需要使用 valgrind 等内存检查工具进行定位。
-
shared 列:被共享使用的物理内存大小。
-
buffer/cache 列:被 buffer 和 cache 使用了的物理内存大小。
-
Linux 内核为了提升磁盘操作的性能,会消耗一部分空闲内存去缓存磁盘数据,就是 buffer 和 cache。
-
如果给所有应用分配足够内存后,物理内存还有剩余,linux 会尽量再利用这些空闲内存,以提高整体 I/O 效率,其方法是把这部分剩余内存再划分为 cache 及 buffer 两部分加以利用。
-
所以,
空闲物理内存不多,不一定表示系统运行状态很差,因为内存的 cache 及 buffer 部分可以随时被重用,在某种意义上,这两部分内存也可以看作是额外的空闲内存。
-
-
available 列:还可以被应用程序使用的物理内存大小。
- 从应用程序的角度来说,
available = free + buffer + cache
。请注意,这只是一个很理想的计算方式,实际中的数据往往有较大的误差。
- 从应用程序的角度来说,
释放缓存内存
方式一:手动释放缓存内存
snyc
echo 3 > /proc/sys/vm/drop_caches
free -m
方式二:修改 linux 配置自动释放
/proc/sys/vm/drop_caches 这个值的 0 改为 1
磁盘 I/O
性能测试过程中,如果被测服务对磁盘读写过于频繁,会导致大量请求处于 I/O 等待的状态,系统负载升高,响应时间变长,吞吐量下降。
性能监控时的关注点
-
I/O 使用率:磁盘实际 I/O 是否已接近最大值,接近则有问题。
-
I/O 队列:如果当前 I/O 队列长度一直不为 0,则有问题。
固态硬盘:500M/s
机械硬盘:不超过 200M/s
iostat 参数详解
Linux 下可以用 iostat 命令来监控磁盘状态。
iostat -d 2 10 表示每 2 秒统计一次基础数据,统计 10 次:
-
tps:该设备每秒的传输次数。“一次传输”意思是“一次 I/O 请求”。多个逻辑请求可能会被合并为“一次 I/O 请求”。“一次传输”请求的大小是未知的。
-
kB_read/s:每秒从设备(driveexpressed)读取的数据量,单位为 Kilobytes。
-
kB_wrtn/s:每秒向设备(driveexpressed)写入的数据量,单位为 Kilobytes。
-
kB_read:读取的总数据量,单位为 Kilobytes。
-
kB_wrtn:写入的总数量数据量,单位为 Kilobytes。
从 iostat -d 的输出中,能够获得系统运行最基本的统计数据。但对于性能测试来说,这些数据不能提供更多的信息。需要加上 -x 参数。
iostat -x 参数详解
如 iostat -x 2 10 表示每 2 秒统计一次更详细数据,统计 10 次:
-
rrqm/s:每秒这个设备相关的读取请求有多少被 Merge 了。
- 当系统调用需要读取数据的时候,VFS 将请求发到各个 FS,如果 FS 发现不同的读取请求读取的是相同 Block 的数据,FS 会将这个请求合并 Merge。
-
wrqm/s:每秒这个设备相关的写入请求有多少被 Merge 了。
-
await:每一个 I/O 请求的处理的平均时间(单位:毫秒)。
-
await 的大小一般取决于服务时间(svtcm)以及 I/O 队列的长度和 I/O 请求的发出模式。假设 svtcm 比较接近 await,说明 I/O 差点没有等待时间。
-
假设 await 远大于 svctm(如大于 5),就要考虑 I/O 有压力瓶颈,说明 I/O 队列太长,应用得到的响应时间变慢。
假设响应时间超过了用户能够容许的范围,这时可以考虑更换更快的磁盘。
-
-
svctm:I/O 平均服务时间。
-
%util:在统计时间内有百分之多少用于 I/O 操作。
-
例如,如果统计间隔 1 秒,该设备有 0.8 秒在处理 I/O,而 0.2 秒闲置,那么该设备的 %util = 0.8/1 = 80%,该参数暗示了设备的繁忙程度。
-
%util 接近100% 表明 I/O 请求太多,I/O 系统繁忙,磁盘可能存在瓶颈。
-
iostat -x 完整参数如下:
- rrqm/s: 每秒进行 merge 的读操作数目。即 delta(rerge)/s
- wrqm/s: 每秒进行 merge 的写操作数目。即 delta(wmerge)/s
- t/s: 每秒完成的读 I/O 设备次数。即 delta(rioVs
- w/s: 每秒完成的写 1/O 设备次数。即 delta(wio)/s
- rsec/s: 每秒读扇区数。即 delta(rsect)/s
- ws0c/s: 每秒写扇区数。即 deita(wsect)/s
- rkB/s: 每秒读 K 字节数。是 rsect/s 的一半,因为每扇区大小为 512 字节。(需要计算)
- wkB/s: 每秒写 K 字节数。是 wsect/s 的一半。(需要计算)
- avgrq+sz: 平均每次设备 I/O 操作的数据大小(扇区)。delta(rsect+wsect)/delta(rio+wio)
- avgqu-sz: 平均I/O队列长度,即delta(avea)/s/1000(因为 aveq 的单位为毫秒)。
- await: 平均每次设备 I/O 操作的等待时间(毫秒)。即 delta(ruse+wuse)/delta(rio+wio)
- svctm: 平均每次设备 I/O 操作的服务时间(毫秒)。即 delta(use)/delta(rio+wio)
- %util:一秒中有百分之多少的时间用于 I/O 操作,或者说一秒中有多少时间 I/O 队列是非空的。即 delta(use)/s/1000(因为 use 的单位为毫秒)
网络
性能测试中网络监控主要包括网络流量、网络连接状态的监控。
网络流量监控
方法很多,网上有很多 shell 脚本。也可以使用 nethogs 命令。该命令与 top 类似,是一个实时交互的命令,运行界面如下:
在后台服务性能测试中,对于返回文本结果的服务,并不需要太多关注在流量方面。
理解带宽
针对一些特定的应用,比如直播或网盘(文件上传下载),带宽瓶颈也是一个出现频率较高的场景。
服务端的带宽分为上行(out)和下行(in)带宽(分别对应客户端的下载和上传)。
-
看视频看新闻使用带宽:客户端的下载、服务端的上行带宽。
-
服务端接收客户端的数据使用带宽:客户端的上传、服务端的下行带宽。
一个 Web 服务器如各类新闻网站通常需要更多的服务端上行(out)带宽;而邮件服务器、网盘服务器等则通常需要更多的服务端下行带宽(in)。
理解带宽速率公式
-
1 Mb/s 带宽速度为 128 KB/s(1024Kb / 8KB)
-
100 Mb/s 带宽速度为 12.5 Mb/s(考虑网络损耗通常按 10M/s 或 1280KB/s 算)
示例:5000 万像素手机拍一张照片,照片大小约 20MB,在下述带宽下需要耗时:
-
10M 带宽约 20 秒:耗时 = 流量 / 速率 = 20MB / (10Mb/8) = 20 / 1.25 = 16 秒(按 1MB/s=128KB/s 速度算即 20 秒)
-
100M 带宽约 2 秒:耗时 = 流量 / 速率 = 20MB / (100Mb/8) = 20 / 12.5 = 1.6 秒(按 10MB/s=128KB/s 速度算即 2 秒)
-
1000M 带宽约 0.2 秒:耗时 = 流量 / 速率 = 20MB / (1000Mb/8) = 20 / 125 = 0.16 秒(按 100MB/s=128KB/s 速度算即 0.2 秒)
案例分析
现象:从监控图表可以看出,当前的网络流量已经基本将网络带宽占满,因此网络存在瓶颈。
解决方案:
- 硬件解决:增加带宽(带宽便宜)。
- 软件解决:分析对应业务操作的数据传送内容是否可精简;是否可以异步传送。
网络连接状态监控
性能测试中对网络的监控主要是监控网络连接状态的变化和异常
。
-
对于使用 TCP 协议的服务,需要监控服务已建立连接的变化情况(即 ESTABLISHED 状态的 TCP 连接)。
-
对于 HTTP 协议的服务,需要监控被测服务对应进程的
网络缓冲区的状态、TIME_WAIT 状态的连接数等
。
Linux 自带的很多命令如 netstat、ss 都支持如上功能。
下图是 netstat 对指定 pid 进程的监控结果:
完整命令输出:
数据库
慢查询
更具体的慢 SQL 分析优化,可参见《MySQL 慢 SQL & 优化方案》。
如 MySQL 资源出现瓶颈,首先找慢查询(超过自定义的执行时间阈值的 SQL)。
1)通过 SQL 语句定位到慢查询日志的所在目录,然后查看日志。
show variables like "slow%";
2)慢查询日志在查询结束以后才纪录,所以在应用反映执行效率出现问题时,查询慢查询日志并不能定位问题。这时可以使用show processlist
命令查看当前 MySQL 正在进行的线程状态,可以实时地查看 SQL 的执行情况。
示例:
mysql -uroot -p123456 -h127.0.0.1 -p3307 -e "show full processlist" |grep dbname |grep -v NULL
3)找到慢查询 SQL 后可以用执行计划(explain)进行分析(或反馈给 DBA 和开发处理)。推荐最简单的排查方式,步骤如下:
- 分析 SQL 是否加载了不必要的字段/数据。
- 分析 SQL 是否命中索引。
- 如果 SQL 很复杂,优化 SQL 结构。
- 如果表数据量太大,考虑分表。
- ……
连接数
数据库连接池的使用率
-
当数据库连接池被占满时,如果有新的 SQL 语句要执行,只能排队等待,等待连接池中的连接被释放(等待之前的 SQL 语句执行完成)。
-
如果监控发现数据库连接池的使用率过高,甚至是经常出现排队的情况,则需要进行调优。
查看/设置最大连接数
-- 查看最大连接数
mysql> show variables like '%max_connection%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| extra_max_connections | |
| max_connections | 2512 |
+-----------------------+-------+
2 rows in set (0.00 sec)
-- 重新设置最大连接数
set global max_connections=1000;
在/etc/my.cnf 里面设置数据库的最大连接数
[mysqld]
max_connections = 1000
查看当前连接数
mysql> show status like 'Threads%';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 32 |
| Threads_connected | 10 |
| Threads_created | 50 |
| Threads_rejected | 0 |
| Threads_running | 1 |
+-------------------+-------+
5 rows in set (0.00 sec)
-
Threads_connected:表示当前连接数。跟 show processlist 结果相同。准确的来说,Threads_running 代表的是当前并发数。
-
Threads_running:表示激活的连接数。一般远低于 connected 数值。
-
Threads_created:表示创建过的线程数。
-
如果我们在 MySQL 服务器配置文件中设置了 thread_cache_size,那么当客户端断开之后,服务器处理此客户的线程将会缓存起来以响应下一个客户而不是销毁(前提是缓存数未达上限)。
-
如果发现 Threads_created 值过大的话,表明 MySQL 服务器一直在创建线程,这也是比较耗资源,因此可以适当增加配置文件中 thread_cache_size 值。
-
查询服务器 thread_cache_size 的值
mysql> show variables like 'thread_cache_size';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| thread_cache_size | 100 |
+-------------------+-------+
1 row in set (0.00 sec)
## 锁
详见《MySQL 事务和锁》。
缓存命中率
-
通常,SQL 查询是从磁盘中的数据库文件中读取数据。
-
若当某一个 SQL 查询语句之前执行过,则该 SQL 语句及查询结果都会被缓存下来,下次再查询相同的 SQL 语句时,就会直接从数据库缓存中读取。(注意,MySQL 8 开始已废弃查询缓存功能。)
监控点
-
业务执行过程中 SQL 查询时的缓存命中率(查询语句读取缓存的次数占总查询次数的比例)。
-
如果缓存命中率过低,需要优化对应的代码和 SQL 查询语句,以提高缓存命中率。
案例分析
测试结果分析
结论:从目前的测试结果来看(如下图所示),性能存在问题。
现象:并发数达到 50 时的 TPS 为 52,此时虽然响应时间为 4.4s(小于需求的 5s),但是数据库服务器的 CPU 使用率非常高(接近 100%),因此需要重点关注数据库的调优分析。
排查过程
-
使用 top 命令观察,确定是 mysqld 导致还是其他原因。
- CPU 分为用户 CPU 和内核 CPU。综合其他的各项资源指标来分析,发现内存、磁盘IO、网络等指标无任何异常,因此判断此处不是内核 CPU 占用高,主要原因是用户进程占用的 CPU 高。
- 确认目前 CPU 占用高的为 mysqld 进程。
-
分析数据库服务器 CPU 高的可能原因:慢 SQL、SQL 语句过多、连接数过多等。
-
确认是否存在慢 SQL:
- 查看慢查询日志,看看是否有超过预期指标的 SQL 语句,并分析排查:看看执行计划是否准确、索引是否缺失、数据量是否太大等。
- 目前案例经过慢查询日志的分析,未存在慢查询。
-
确认是否 SQL 语句过多或连接数过多:
- 使用
show full processlist
查看当前数据库中正在执行的 SQL 语句及连接池的状态,发现大量 SQL 在等待执行。 - 再结合操作过程中的系统日志进行分析,发现每进入一次商城首页,就需要在数据库中执行 19 条查询 SQL。
- 使用
-
解决方案
- 硬件解决:增加 CPU。
- 软件解决:为减少一次性加载过多 SQL,可考虑使用分批次、异步加载的方式(展示到什么位置,就查询什么位置的数据)。
JAVA 应用
JVM
JVM(JAVA Virtual Machine):虚拟出来的空间,专门供 JAVA 程序运行。
JAVA 应用运行机制
JVM 体系结构介绍
-
JVM 内存分为三个大区,young 区(年轻代),old 区(年老代)和 perm 区(持久代),其中 young 区又包含三个区:Edgn 区、S0 区(From 区)、S1 区(To 区)。
-
young 区和 old 区属于 heap(堆)区,占据堆内存;perm 区称为持久代,不占据堆内存。
-
PermSpace 主要是存放静态的类信息和方法信息、静态的方法和变量、final 标注的常量信息等。
JAVA 运行时内存划分
重点关注:堆区(动态变化)
。我们常说的性能调优,指的就是堆中的性能调优。
监控点:因此在测试时,需要关注堆区的空间是否持续上升而没有下降。
垃圾回收机制
什么是垃圾回收机制
-
垃圾回收指将内存中已申请并使用完成的那部分内存空间回收,供新申请使用。
-
垃圾回收机制都是针对堆区的内存进行的。
监控点
-
内存泄露:一个对象持有一个引用永远不释放,导致声明周期过长,这样持有的对象对了,内存就不够用了,这样就会频繁 GC。
-
系统在做垃圾回收时,不能够处理任何用户业务的。如果垃圾回收过于频繁,导致系统业务处理能力下降。
-
由于 Full GC 内存比较大,垃圾回收一次时间比较长,那么这段时间内都不能处理业务,对系统影响比较大,因此我们需要关注
Full GC 频率
。
垃圾回收机制的运行步骤如下:
-
新程序执行时需要先申请内存空间,会先从年轻代中申请。
-
在年轻代满了以后,就会进行垃圾回收
Young GC(Minor GC)
(所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程,但这段时间可以忽略不计)。 -
回收时检查年轻代中的内存,是否还在使用。还在使用的部分会移存到生存区 2 中;不使用的部分则释放,此时年轻代内存空间被清空。
-
新程序执行申请内存空间,再从年轻代申请。
-
年轻代又满了,就会进行垃圾回收
Young GC
。还在使用的内存移存到生存区 1 中,并把生存区 2 中的内存也都存到生存区 1 中。此时就会清空年轻代和生存区 2。 -
循环上述 1-5 步。
-
如果部分内存在生存区中存活很久(内存在生存区中移动了 10 次左右),则将这部分内存放入到老年代中。
-
循环上述 1-7 步,直到老年代内存空间全部占满,此时就要进行垃圾回收
Full GC(Major GC)
(Full Gc 会暂停所有正在执行的线程(Stop The World),来回收内存空间,这个时间需要重点考虑)。
JVM dump
什么是 JVM dump
在故障定位(尤其是 out of memory)和性能分析的时候,经常会用到一些文件来帮助我们排除代码问题。这些文件记录了 JVM 运行期间的内存占用、线程执行等情况,这就是我们常说的 dump 文件。
常用的有 heap dump 和 thread dump(也叫 javacore,或 java dump)。我们可以这么理解:heap dump 记录内存信息的,thread dump 是记录 CPU 信息的
。
-
当发现应用内存溢出或长时间使用内存很高的情况下,通过内存 dump 进行分析可找到原因。
-
当发现 cpu 使用率很高时,通过线程 dump 定位具体哪个线程在做哪个工作占用了过多的资源。
-
heap dump
-
heap dump 文件是一个二进制文件,指定时刻的 Java 堆栈的快照,是一种镜像文件,它保存了某一时刻 JVM 堆中对象使用情况。
-
可以通过 Heap Analyzer工具分析 heap dump 文件,哪些对象占用了太多的堆栈空间,来发现导致内存泄露或者可能引起内存泄露的对象。
-
-
thread dump
-
thread dump 文件主要保存的是 java 应用中各线程在某一时刻的运行的位置,即执行到哪一个类的哪一个方法哪一个行上。
-
thread dump 是一个文本文件,打开后可以看到每一个线程的执行栈,以 stack trace 的方式显示。
-
通过对 thread dump 的分析可以得到应用是否“卡”在某一点上,即在某一点运行的时间太长,如数据库查询时长期得不到响应,最终导致系统崩溃。
-
单个的 thread dump 文件一般来说是没有什么用处的,因为它只是记录了某一个绝对时间点的情况。比较有用的是,线程在一个时间段内的执行情况。
-
thread dump 文件在分析时特别有效,困为它可以看出在先后两个时间点上,线程执行的位置。如果发现先后两组数据中同一线程都执行在同一位置,则说明此处可能有问题,因为程序运行是极快的,如果两次均在某一点上,说明这一点的耗时是很大的。通过对这两个文件进行分析,查出原因,进而解决问题。
-
获取 dump 文件
可以利用 JDK 自带的工具获取 thread dump 文件和 heap dump 文件,即 JDK_HOME/bin/ 目录下的 jmap 和 jstack 这两个命令。
1)获取 heap dump 文件
./jmap -dump:format=b,file=heap.hprof 2576
这样就会在当前目录下生成 java 应用进程 pid 为 2576 的 heap.hprof 文件,这就是 heap dump 文件。
如果我们只需要将 dump 中存活的对象导出,那么可以使用 :live 参数:
jmap -dump:live,format=b,file=heapLive.hprof 2576
2)获取 thread dump 文件
./jstack 2576 > thread.txt
这样会将命令执行结果转储到 thread.txt,这就是 thread dump 文件。有了 dump 文件后,我们就能借助性能分析工具获取 dump 文件中的信息(使用 top -H -p <pid> 找出某进程中要分析的线程 ID,然后将线程 ID 转换为 16 进制后,在线程 dump 文件中搜索相关信息)。
打开 dump 文件
1)使用 JDK 自带的 jhat 命令
jhat 是用来分析 java 堆的命令,可以将堆中的对象以 html 的形式显示出来,包括对象的数量、大小等等,并支持对象查询语言。
jhat -port 5000 heap.hrof
当服务启动完成后,我们就可以在浏览器中,通过 http://localhost:5000/ 进行访问,如下所示:
2)使用 eclipse MAT 工具
一般来说,应用程序的 dump 文件都是很大的,jdk 自带命令难以分析这些大文件。在实际的生产环境下,我们必须要借助第三方工具,才能快速打开这些大文件,进行分析定位。
安装好 eclipse mat 分析工具后,将 dump 文件导入 eclipse,点击[Leak Suspects],找到跟公司有关的代码进行分析。
分析 thread dump 文件
线程 dump 详解
线程的状态:
- NEW:未启动,不会出现在 Dump 中。
- RUNNABLE:在虚拟机中执行的。
- BLOCKED:受阻塞并等待在监视器锁。
- WAITTING:无限期等待另一个线程执行特定的操作。
- TIMED_WAITTING:有时限的等候另一个线程执行特定的操作。
- TERMINATED:已退出。
监视器:
调用修饰:
- locked <地址> 目标:注意临界区对象锁可重入,线程状态为 RUNNABLE。
- waitting to lock <地址> 目标:还没有获得锁,进入区等待,线程状态为 BLOCKED
- waitting on <地址> 目标:获得锁了,等待区等待,线程状态为 WAITTING,TIMED_WAITTING。
- parking to wait for <地址> 目标:线程原语,随 current 包出现,与 synchronized 体系不同。
线程动作:
- runnable:线程状态为 RUNNABLE。
- in Object.wait():等待区等待,线程状态为 WAITTING 或 TIMED_WAITTING。
- waitting for monitor entry:进入区等待,线程状态为 BLOCKED。
- waitting on condition:等待区等待,被 park。
- sleeping:休眠的线程,调用了 Thread.sleep()。
分析线程 dump 的入手点
-
进入区等待:BLOCKED、waitting to lock、waitting for monitor entry,这些词表名代码层面已经存在冲突。
-
持续进行的 IO:一般来说被捕捉到的 runnable 的 IO 调用都是有问题的,如 runnable 中有 JDBC 链接的代码。
-
非线程调度的等待区等待:in Object.wait()(情况 1 可能会导致这个情况,造成大量线程堆积)。
-
“死锁”问题的解决办法
-
在最可能死锁的时间点制作 dump。
-
找出引起大量线程阻塞的线程。
-
找出该线程阻塞的原因。
-
阅读代码,遍历其他阻塞或等待的线程,以及它之前的调用是否会造成这个线程的等待。
-
-
注意:排除 GC 干扰,Full GC 时所有线程都会被阻塞住。
- 查看线程 dump 时,首先查看内存使用情况。
- 使用命令“-verbose:gc”,观察是否有 Full GC 字样。
分析 heap dump 文件
什么情况下需要分析堆 Dump
内存不足、GC 异常、怀疑代码内存泄漏,这时需要制作堆 Dump,找出生命周期的错误关联对象以及相关代码。
JVM 内存模型
- 年轻代(Young Generation,包括Eden space、From space、To space)
- 年老代(Old Generation)
- 永久代(PermGen space)
两种 GC
- YoungGen GC:Minor GC
- Full GC:Major GC
常见错误
- out of MemoryError:GC overhead limit exceed:回收时间占系统运行时间的 98% 以上,极有可能是内存泄漏导致的。
案例分析:JVM 堆内存溢出
JVM 堆内存回收详细过程图解:从下图可以很清晰的看到,old 区空间占满后会进行一次 FGC(称为全量 GC),FGC 回收后如果 old 区空间还是不能容纳新生成对象,那么便会产生 java 堆内存溢出[JAVA HEAP OOM]。
性能问题发现过程:
查看服务器上报错日志,发现有如下报错信息[java.lang.OutOfMemoryError: Java heap space];根据报错信息确定是 jvm 堆内存空间不够导致,于是使用 jvm 命令查看(下图所示),发现此时 old 区内存空间已经被占满了。
同时使用 jvisualvm 监控工具也发现 old 区空间被占满(如下图所示,单位为百分比),整个 heap 区空间已经无法再容纳新对象进入。
建议:考虑大量数据一次性写入内存场景。
案例分析:持久代内存溢出
PermSpace 主要是存放静态的类信息和方法信息、静态的方法和变量、final 标注的常量信息等。
现象:
压测某系统接口,压测前1分钟左右 TPS 400 多,之后 TPS 直降为零,后台报错日志:java.lang.OutOfMemoryError:PermGenspace,通过 jvm 监控工具查看持久代(perm区)空间被占满,而 Old 区空闲。
问题定位:
通过注释代码块定位问题,考虑到 perm 区溢出大部分跟类对象大量创建有关,故锁定问题在序列化框架使用可能有问题。
-
获取 JVM dump 文件。
-
安装 eclipse mat 分析工具。
-
将 dump 文件导入 eclipse,点击[Leak Suspects],找到跟公司有关的代码进行分析。
解决方案:
跟开发沟通后选择去掉 msgpack0.6 版本框架,采用 java 原生序列化框架。修改后系统 tps 稳定在 400 多,gc 情况正常。
-
修复前:
-
修复后:
类似问题如何避免:
- 去掉项目无用 jar 包。
- 避免大量使用类对象、大量使用反射。
案例分析:频繁 FGC
现象:系统某接口频繁 FGC。
问题排查及解决方案:
先查 JVM 内存信息找可疑对象,命令为:jmap -histo
从内存对象实例信息中发现跟 mysql 连接有关,然后检测 mysql 配置信息:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
发现系统采用的是 spring 框架的数据源,没有用连接池。
使用连接池的好处:连接复用,减少连接重复建立和销毁造成的大量资源消耗。
然后换做 hikaricp 连接池做对比测试:
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
压测半小时未出现 fgc,问题得到解决。
类似问题如何避免:
- 研发规范统一 DB 连接池,避免研发误用。
- 减少大对象、临时对象使用。
案例分析:减少 mirror GC
现象:
假设,现在有亿级流量电商的抢购活动,活跃用户为 500 万,付费转化率为 10%。活跃时间在抢购的前几分钟,假设每秒产生 1000 单,而每台 Tomcat 的最高并发支持数为 500。现有三台服务器,均为 4 核 8g,每台服务器均部署 Tomcat,使用 nginx 做负载均衡。
有 300 单落在服务器 1 上,每单所在堆空间大小为 1Kb,每秒大约产生 300Kb 的堆对象。可以使用 lucene 来动态计算 javabean 所在堆空间的大小。下单还会产生其他对象,比如优惠券、库存、积分等,此时放大 20 倍,也就是每秒产生 6000Kb 的对象。假设还会有订单查询的操作,此时再放大 10 倍,也就是每秒产生约 58MB 的对象。
此时,堆初始值大小和最大值大小均为 3072MB,老年代大小为 2048MB,新生代大小为 1024MB,Eden 区大小为 819MB,s0 和 s1 区大小均为 102MB。819Mb / 58Mb = 14 秒,即大约 14 秒 Eden 区爆满,触发 mirror Gc,此时停止应用程序的线程。
优化:
因而,此时需要调整 JVM 的配置参数:老年代大小为 1024MB,新生代大小为 2048MB,Eden 区大小为 1638MB,s0 和 s1 区大小均为 204MB。1638Mb/ 58Mb = 28秒,这样能减少 mirror Gc,从而达到优化的效果。但更多的优化可根据实际线上 jvm 运行情况来看。
框架使用不当
案例分析:错误使用框架提供的 API
某系统本身业务逻辑处理能力很快(研发本机自测 tps 可以到达 2w 多),但是接入到 framework 框架后,TPS 最高只能到达 300 左右,而且系统负载很低。
问题排查:
这种现象说明系统可能是堵在了某块方法上,根据这种情况一般采用线程 dump 的方式来查看系统具体哪些线程出现异常情况。
通过线程 dump 发现 [TIMED_WAITING]状态的业务线程占比很高。
# 线程的状态
* NEW:未启动,不会出现在 Dump 中。
* RUNNABLE:在虚拟机中执行的。
* BLOCKED:受阻塞并等待在监视器锁。
* WAITTING:无限期等待另一个线程执行特定的操作。
* TIMED_WAITTING:有时限的等候另一个线程执行特定的操作。
* TERMINATED:已退出。
根据线程 dump 信息,找到公司包名开头的信息,然后从下往上查看线程 dump 信息,从信息中我们可以看到:
- framework.servlet.fServlet.doPost:框架 api 封装了 servletdopost 方法做了某些操作。
- framework.servlet.fServlet.execute:框架 api 执行 servelt。
- framework.process.fProcessor.process:框架 api 进行自身逻辑处理。
- framework.filter.impl.AuthFilter.before:框架使用过滤器进行用户权限过滤
- 。。。然后就是进行 http 请求操作。
由此判断,就是在框架进行权限校验这块堵住了。之后跟开发沟通这块的问题即可。
问题原因:
性能测试是验证 A 系统的处理能力,但是在压测程序里,A 系统却调用了权限校验系统,由于权限校验系统处理能力只有 300 左右,从而拖慢了整个系统处理能力。
因此,需要在压测过程中关闭对权限校验系统调用,只压 A 系统,这样才能压测出 A 真实的处理能力。
解决方案
去掉对 B 系统调用,即去掉权限校验。
@Api(auth=true) 改为 @Api(auth=false)
案例分析:日志框架使用不当
某系统添加 LOGBACK 日志框架输出日志(日志级别为 INFO)后,TPS 从 1000 降到 200 多:
从 JVISUALVM 工具看到有大量业务线程处于 BLOCKED 状态:
优化方案:
日志降级、将日志级别改为 warn,减少日志输出量。
后续建议:
-
合理设置日志级别、精简日志输出。
-
合理设置日志刷盘方式,同步 or 异步。
-
对于 DEBUG、INFO 日志打印、需要先判断日志级别:
if(LOGGER.isDebugEnabled()){do log}
。
OS 内存溢出
问题现象:
某系统线上故障,系统假死,无法提供服务,服务器 ssh 无法登录。
问题根因:
系统使用堆外内存,操作系统内核占用 cache 内存,当 cache 内存占满后,无法释放,导致物理内存 OOM(Out Of Memory)。
为什么会 OOM?
为什么会没有内存了呢?原因不外乎有两点:
-
分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的 JVM 参数指定)太少。
-
应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
优化方案:
通过优化 linux 操作系统内核参数:min_free_kbytes
自动化测试相关教程推荐:
2023最新自动化测试自学教程新手小白26天入门最详细教程,目前已有300多人通过学习这套教程入职大厂!!_哔哩哔哩_bilibili
2023最新合集Python自动化测试开发框架【全栈/实战/教程】合集精华,学完年薪40W+_哔哩哔哩_bilibili
测试开发相关教程推荐
2023全网最牛,字节测试开发大佬现场教学,从零开始教你成为年薪百万的测试开发工程师_哔哩哔哩_bilibili
postman/jmeter/fiddler测试工具类教程推荐
讲的最详细JMeter接口测试/接口自动化测试项目实战合集教程,学jmeter接口测试一套教程就够了!!_哔哩哔哩_bilibili
2023自学fiddler抓包,请一定要看完【如何1天学会fiddler抓包】的全网最详细视频教程!!_哔哩哔哩_bilibili
2023全网封神,B站讲的最详细的Postman接口测试实战教学,小白都能学会_哔哩哔哩_bilibili
总结:
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
如果对你有帮助的话,点个赞收个藏,给作者一个鼓励。也方便你下次能够快速查找。
如有不懂还要咨询下方小卡片,博主也希望和志同道合的测试人员一起学习进步
在适当的年龄,选择适当的岗位,尽量去发挥好自己的优势。
我的自动化测试开发之路,一路走来都离不每个阶段的计划,因为自己喜欢规划和总结,
测试开发视频教程、学习笔记领取传送门!!