系统与软件性能瓶颈与性能调优模型详解
一、性能测试瓶颈定位与调优
1、性能瓶颈与调优
在实际运行中的性能测试中会遇到的问题类型多样例如TPS压力无法突破等这些情况背后原因繁多测试团队需与开发团队紧密协作以识别问题根源
理想的性能测试指标结果可能不是很高,但一定是平缓的。
1. 性能瓶颈预兆

2. 性能调优步骤
确定问题:根据性能监控的数据和性能分析的结果,确定性能存在的问题。
确定原因:确定问题之后,对问题进行分析,找出问题的原因。
确定解决方案(改服务器参数配置/增加硬件资源配置/修改代码)。
验证解决方案,分析调优结果。
请特别注意:在优化系统性能时,并非一次性就能完成的任务。对于同一类性能问题而言,这些步骤可能会经历多个循环以确保达到最佳的性能优化效果。也就是说:发现问题后->找出原因->进行调整->验证改进->分析效果->再次测试 ...
3. 性能瓶颈概率分布
60%:数据库瓶颈
- 数据库服务器负载过重(执行缓慢的SQL语句、过量的SQL指令以及超出连接池容量导致)
- 数据库抛出"超时"异常(由于网络延迟或其他原因未能及时响应)
- 数据库发生死锁现象
25%:应用瓶颈
- 系统内存溢出
- 多线程操作可能导致资源竞争或死锁问题
- 软件架构的计算复杂性分析需重点关注
- 靠赖外部服务可能导致功能异常或服务中断
- 处理高负荷计算任务会导致处理器资源饱和
- 数据读写频繁导致存储设备压力增大
10%:压测工具瓶颈
JMeter的处理能力有限,在模拟用户请求数达到或超越其处理能力时,会导致TPS难以进一步提升
5%:Linux 机器出现异常
- Linux 可用内存无法回收(开销速率大于回收速率)
2、系统资源

CPU
- 关注的重点:处理器负载情况及其运行状态
- 性能瓶颈判断:当处理器饱和运行(超过95%)时,请留意其他关键性能指标的变化趋势是否呈现出相似的关键转型时刻
内存
- 监控重点:物理内存量与交换区容量
- 内存资源管理机制:当系统出现内存量不足时, 操作系统的处理机制是调用交换区以满足需求, 并通过交换区获取所需资源以提高运行效率。
磁盘 I/O
- 监控重点:关注I/O速率以及磁盘等待队列的长度
- 性能瓶颈分析:当发生瓶颈时(即系统出现性能问题),会发生以下情况:磁盘I/O变得繁忙(即系统进入高负载状态),进而使得交易执行过程中会因 I/O 这一部分而延迟
网络
- 监控重点:包括网络流量的带宽占用率以及网络连接状态
- 瓶颈分析:当接口发送的数据包规模超过规定阈值时(即数据包大小超过指定上限),会导致带宽资源争夺,并进而使TPS难以达到预期值。
一旦发现系统有性能瓶颈后,则需要找到问题根源并采取相应的解决措施。简单来说,在任何地方发现性能瓶颈时,则可以通过减少压力或者增加该区域的资源(如应用软件中没有进一步优化的空间)来缓解问题。
- 解决CPU瓶颈的方式是提升处理能力
- 针对内存问题,在扩大内存容量的同时优化缓存使用
- 为了克服磁盘I/O限制,在硬件上升级至更高性能设备
- 当面临网络带宽限制时,则需采取措施扩大传输速度
1. CPU
所有指令与数据处理均由 CPU 承担,在提升系统响应速度方面发挥着关键作用。与此同时,在提升系统响应速度方面发挥着关键作用的服务对 CPU 的利用率同样至关重要。
下面以 top 命令的输出例,对 CPU 各项主要指标进行说明:

us(user):运行(未调整优先级的)用户进程所消耗的 CPU 时间的百分比。
这些包括 shell 程序、多种编程语言的编译器、数据库相关应用、Web 服务器以及多种桌面应用程序都是运行在用户地址空间中的进程。
当这些程序不处于 idle 状态时,则它们 majority 的 CPU 时间主要处于 user态。
sy(system):运行内核进程所消耗的 CPU 时间的百分比。
任何进程所需的系统资源都会由Linux核心进行专门管理与分配。一旦处于用户地址空间中的一个进程如需获取所需资源(如内存分配、I/O操作或子进程创建等),就必须切换至核心地址空间以执行相关操作。实际上,在决定下一个时间段将执行哪个进程中扮演重要角色的进程调度机制就会运行于核心地址空间中。
在操作系统设计中,尽量减少内核态的运行时间是一个重要原则。当 sy 比例过高时,在用户态与内核态之间的频繁切换会严重影响系统的性能表现。这会导致系统的整体性能出现一定的下降趋势
在实践过程中存在某些典型的案例会导致sy数值上升的情形出现;这些情况通常是耗时较长的IO操作所引发的;因此,在进行I/O相关问题排查时应特别注意此类情况
通常,在大多数后台服务的CPU时间片中使用最多的参数是us和sy指标。它们之间存在相互影响的关系:当us参数增加时,sy参数就会相应下降。
另外,在多核处理器服务器上运行时,在线调度任务时序性能依赖于各核心间的协调效率。当主线程占用率过高时,在线调度性能会显著下降。因此,在测试过程中应特别关注主线程的工作状态并采取相应的优化措施以确保系统的整体性能得到保障
ni(niced):用做 nice 加权的进程分配的用户态 CPU 时间百分比。
- 每个Linux进程都具有一定的优先权,在操作系统调度中这些高优先权的进程能够获得执行优势,并被称为pri(优先)。除了基本的优先权之外,在某些情况下还会有额外的修正因素存在。
- 在这里所展示的ni参数代表了经过nice值调整后的CPU使用情况。当系统中的所有进程中都没有应用过nice值时,则ni参数将显示为零。
- 通常情况下,在正常负载下各服务系统的ni参数数值不会过高。但如果在实际测试过程中发现ni数值显著偏高,则可能需要从服务器Linux系统的配置设置以及被测服务的相关运行参数中寻找潜在问题。
id(idle):空闲的 CPU 时间百分比。
一般情况下, us + ni + id 应该接近 100%。
线上服务运行过程中,需要保留一定的 id 冗余来应对突发的流量激增。
在进行性能测试时,若观察到id始终保持较低水平而吞吐量无法提升,则需排查被测服务的线程或进程配置情况以及服务器系统的配置参数设置等细节信息。
wa(I/O wait):CPU 等待 I/O 完成时间百分比。
相比之下,在处理速度上来说,
磁盘 I/O 操作表现得非常低效。
存在许多类似的场景,
当 CPU 启动进行一次磁盘读写操作时,
在等待该操作的结果之前,
系统必须保持空闲状态。
Linux 系统在计算系统平均负载时会将 CPU 处于等待 I/O 操作状态的时间纳入考量,在观察到系统的平均负载过高时,则可以通过 wa 命令来判断系统的性能瓶颈是否是由过量的 I/O 操作引起的。
I/O操作会引发CPU的wa指标上升;一般而言,网络I/O所消耗的wa资源不高,然而大量的磁盘读写操作会显著增加wa指标.
当待测服务并非 I/O 强调型的应用时,此时应评估被测服务的日志量及数据加载频率等。
当 wa 超过 10%时,则会出现轻微卡顿;当 wa 超过 20%时,则系统基本无法响应;当 wa 超过 50%,很可能会导致磁盘损坏。
hi:硬中断消耗时间百分比。
si:软中断消耗时间百分比。
- 外设向CPU或内存发送异步信号属于硬中断;软件通过发送中断指令到操作系统内核来实现软中断。
- 通常是由处理程序或进程调度程序向内核发送中断请求(System Call)。
- 在性能测试过程中,在hi下CPU占用率为一定值;而针对I/O密集型服务,在si下的CPU占用率更高。
st:虚拟机等待 CPU 资源的时间。
仅限于 Linux 在其作为虚拟化环境中的虚拟机运行时才具有特殊意义。
这一术语代表的是一个虚机等待实际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高属于常见情况;但如果被测服务是非磁盘I/O密集型的,则wa高的主要原因有两个。
服务中磁盘读写业务流程存在缺陷,在处理速度方面表现不佳,并且在数据吞吐量上显著增加。这种状况通常与不当的数据加载方式以及过于冗杂的日志信息有关。
服务器内存不足,服务在 swap 分区不停的换入换出。
现象:CPU 与吞吐量。
CPU 占用不高,吞吐量较低,可能是服务端线程池启动太少。
CPU 占用很高,吞吐量较低,服务端处理慢,可能操作数据库慢。
CPU 占用很高,吞吐量很高:
- 服务端性能优越;通过调整线程数量来降低 CPU 负载。
- 数据库连接数量过多;存在慢 SQL 执行问题;文件句柄管理不善。
- 通过优化物理设备提升系统整体性能。
2. LOAD
Linux系统的进程密度是指在固定时间段内每个CPU周期内的平均进程数量。
Linux 系统中 Load 表示整体系统负担,并非单纯反映 CPU 运行程度。它包括 CPU 加载、磁盘加载、网络加载以及其他设备负载,并非单纯反映 CPU 运行程度。而在其他操作系统如 Unix 系统中,则 Load 只表示 CPU 加载情况。
从服务器负载的定义可知,在最佳状态下所有 CPU 核心均处于运行状态且数量适中,并无进程等待资源分配的情况下服务器能够实现最大化的资源利用率。这种情况下服务器负载达到了预设阈值水平并维持稳定运作状态
通常情况下,服务器的负载应遵循经验值设置在7至8成左右的水平。这样既能充分利用服务器的整体性能,在一定程度上还能适应性地处理增长中的流量需求
查看系统负载阈值的命令如下:

Linux 提供了很多查看系统负载的命令,最常用的是 top 和 uptime。
top 和 uptime 在处理负载时输出的内容一致,在这些指标下系统都会显示最近 1 分钟内、5 分钟内以及 15 分钟内的负载平均值

这三个数值的使用方法和 CPU 核数相关,首先确认 CPU 物理总核数:
在 /proc/cpuinfo 文件中记录的 processors 数值未必等于 CPU 核的数量。具体而言,在支持超线程技术的情况下(如果该 CPU 支持超线程技术),其 processors 数量将是物理核数的两倍。
为了准确获得CPU的数量, 我们需要遵循以下步骤: 首先, 通过查看 /proc/cpuinfo 文件中的 physical id字段值, 获取所有物理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
AI助手
物理 CPU 个数为 0+1=1 个,每个 CPU 的核数为 2 个,所以总的物理核数为 2x1=2。
计算结果显示该设备在单位时间内的处理能力为2个进程。当单位时间内处理的进程数量超过规定值时, load值会持续上升。当达到一定水平时会出现异常情况
当执行性能测试时,在评估整个系统的运行状况中占据重要地位的是系统负载水平这一重要指标。一般而言:
负载测试时:系统负载应接近但不能超过阈值。
并发测试时:系统负载最高不能超过阈值的 80%。
稳定性测试时:系统负载应在阈值的 50% 左右。
机器针对突发情况的处理:
当 1 分钟窗口内负载达到很高水平,在 5 分钟窗口内的负载水平相对较高,在较长的时间段(如 15 分钟)内负载波动不大的情况下,则表明此次高负载事件属于突发性质,并且是可以容忍的。
当 high load 继续存在时,在持续的 high load 的情况下,在 5 分钟和 15 分钟的时间间隔内都超过了警报阈值水平的情形出现时,则需要立即采取相应的措施。
如果 15 分钟 load 高于 1 分钟 load,说明高 load 情况已经得到缓解。
3. 内存
在性能测试过程中进行内存使用情况进行监控的主要目标是评估其内存使用量的变化趋势。
在Linux系统中存在若干命令可用于查询指定进程的内存使用情况。主要采用top命令。如下图所示:

VIRT:进程占用的虚拟内存总量。它包含所有代码块、数据以及共享库,并且包含已经置换出的页面块以及全部申请的内存空间
Resource status: The current state of the physical memory (stack and heap) that is not swapped. After allocating memory, this memory segment is now being reinitialized.
SHR:表示系统中所有程序共用的内存总量。这个数值仅仅显示了这些内存可能与其他程序有共享关系,并不表示这些内存此时正在被其他程序占用。
SWAP值代表进程在虚拟内存中被替换出去的部分有多大。这通常发生在进程试图访问超过可用物理内存空间时,系统会将超出部分的数据迁移到磁盘上以避免数据丢失或系统崩溃。交换操作涉及将那些已申请但尚未得到充分利用的空间区域(如栈区、堆区以及共享内存部分)重新分配给其他正在运行的任务以提升整体资源利用率。
DATA:进程除可执行代码以外的物理内存总量,即进程栈、堆申请的总空间。
通过上述分析可以看出,在测试过程中主要关注这两个指标:RES 和 VIRT。此外,在采用共享内存机制的多线程服务中还需关注 SHR。
免费命令用于揭示系统内存资源的使用状况,包括物理内存、交换内存(swap)以及内核缓冲区内的内存占用情况。若要获得更友好的输出结果,则应考虑附加-h选项(控制显示单位大小)。

有时需要持续地监控内存的状态。此时可以使用-s选项并设置每秒间隔的时间:例如,在终端中输入-free -h -s 3 将会每隔3秒输出一次内存使用情况的信息,并在按下Ctrl+C时退出。
Mem 行:物理内存的使用情况。
Swap 行:交换空间的使用情况。
swap space 是一块存储区域,在磁盘上可以作为一个分区也可以作为一个文件存在。因此 swap 空间具体实现的方式既可以是 swap 分区也可以是 swap 文件。当系统面临物理内存不足的情况时,Linux 会将不常用到的内存数据暂存于 swap 区域中以供使用,并在必要时将这些缓存数据加载回内存以便其他进程运行(即常说的换出),而当系统需要访问 swap 区域中的数据时,则会将其加载到内存中完成相关操作(即常说的换入)。
在一定程度上可以缓解内存不足的情况。然而它需要从磁盘进行读写操作因而其性能相对较低。由此可见当交换空间的内存开始被使用时则表明内存已经极度匮乏。
当系统的可用内存较为充足或者用于进行性能测试的机器时,在终端运行swapoff -a命令可以关闭交换空间;同时也可以通过修改配置文件中的swappiness参数来实现同样的效果。
当系统的可用内存不足时,在终端运行swapoff -a命令可能会导致部分数据无法加载(默认情况下swapoff会禁用部分磁盘),建议基于物理内存量进行相应的调整;可供选择的方法在网络上及其丰富。
total 列:系统总的可用物理内存和交换空间大小。
used 列:已经被使用的物理内存和交换空间大小。
free 列:请问目前还有多少物理内存容量和交换空间未被占用(真正尚未被使用的物理内存数量)。
在吞吐量保持恒定的条件下,在观察到内存使用量持续增加的情况下,则很可能表明被测服务存在明显的内存泄漏现象。建议采用valgrind等专业的内存检查工具来进行定位分析。
shared 列:被共享使用的物理内存大小。
buffer/cache 列:被 buffer 和 cache 使用了的物理内存大小。
Linux 内核为了优化磁盘操作性能而会利用一部分空闲内存来存储访问频率较高的文件及目录信息以及临时存储文件内容的空间
如果为所有应用分配了足够的内存后,物理内存仍有剩余空间,则Linux系统会尽可能地利用这些空闲空间来提升整体I/O效率。其具体方法是将这部分剩余的内存划分为缓存区及缓冲区两部分加以使用。
所以,并非如此——即使当前的物理内存资源较为有限,“由于这些区域具有循环利用的特点,并非真正的空闲空间”。
available 列:还可以被应用程序使用的物理内存大小。
在程序设计层面而言,在程序运行过程中变量available的值由三部分组成:自由空间(free)、缓冲区(buffer)以及缓存区(cache)。需要注意的是,并非这种简单的加法运算能够完全准确地反映实际情况——仅仅是一种理想化的计算模型
释放缓存内存:
方式一:手动释放缓存内存。
snyc
echo 3 > /proc/sys/vm/drop_caches
free -m
AI助手
方式二:修改 linux 配置自动释放。
/proc/sys/vm/drop_caches 这个值的 0 改为 1
AI助手
4. 磁盘 I/O
在性能测试过程中, 当被测服务对磁盘进行大量读写操作时, 将会引起大量请求陷入 I/O 阻塞状态, 从而导致系统负载水平上升, 响应时间显著增加, 吞吐量明显降低。
性能监控时的关注点:
I/O 使用率:磁盘实际 I/O 是否已接近最大值,接近则有问题。
I/O 队列:如果当前 I/O 队列长度一直不为 0,则有问题。
固态硬盘:500M/s
机械硬盘:不超过 200M/s
AI助手
iostat 参数详解:
Linux 下可以用 iostat 命令来监控磁盘状态。
iostat -d 2 10 表示每 2 秒统计一次基础数据,统计 10 次:

TPS表示该设备每秒完成的传输次数。所谓的"一次传输"实际上指的是"一个I/O请求数组"。通常情况下,多个逻辑请求会被批量处理为"一个I/O请求数组",而单个请求则可能被视为独立的一次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 操作。
例如,在一个秒的时间段内,“例如,在一个秒的时间段内”,该设备在处理 I/O 的过程中占用了 0.8 秒的时间段(即 80\% 的时间),剩余时间有 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 的单位为毫秒)
AI助手
5. 网络
性能测试中网络监控主要包括网络流量、网络连接状态的监控。
1)网络流量监控
多种方法存在实现系统管理功能的方法,在线学习平台提供了大量教学资源库以及丰富的互动工具包以支持教学过程中的各个环节。另外一种途径是采用nethogs工具进行配置管理操作,在这种情况下需要确保配置数据的安全性和一致性以避免潜在的问题发生

在后台系统性能评估中对处理文本返回的服务无需过多关注其流量表现
理解带宽:
针对特定领域中的某些应用场景而言,在直播平台和网盘服务常见于文件上传与下载操作的情境下
服务端的带宽划分为向上方(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 秒)
案例分析:

观察结果:通过监控图表的数据可以看出,在当前状态下,网络流量已占据了大部分带宽资源,并且导致了带宽被充分使用的情况发生。
解决方案:
- 硬件解决方案需提升系统处理能力(成本较低)。
- 软件解决方案需评估对应业务流程的数据传输细节;并判断其能否进一步优化或采用异步传输方式。
2)网络连接状态监控
性能测试中对网络的监控主要是监控网络连接状态的变化和异常。
在使用 TCP 协议的服务端口上,必须持续关注其连接状态的动态变化(具体而言,涉及 ESTABLISHED 状态的 TCP 连接)。
在HTTP协议下,应关注被测服务对应进程对其网络缓存情况以及等待连接数量的变化趋势进行实时监控。
Linux 自带的很多命令如 netstat、ss 都支持如上功能。
下图是 netstat 对指定 pid 进程的监控结果:

完整命令输出:

3、数据库
1. 慢查询
如 MySQL 资源出现瓶颈,首先找慢查询(超过自定义的执行时间阈值的 SQL)。
1)通过 SQL 语句定位到慢查询日志的所在目录,然后查看日志。
show variables like "slow%";
AI助手
慢Query logs仅会在完成一次完整Query操作后才会记录相关信息。因此,在遇到执行效率异常的问题时使用Slow Query logs无法找到具体原因。这时可以通过运行show processlist命令观察当前MySQL正在处理的各种线程状态,并实时追踪SQL的执行情况。
示例:
mysql -uroot -p123456 -h127.0.0.1 -p3307 -e "show full processlist" |grep dbname |grep -v NULL
AI助手
3)识别出性能瓶颈后可以通过执行计划(EXPLAIN)语句来深入分析问题所在,并且也可以选择直接向DBA和开发者反馈结果以便进一步优化。建议采用最为简便的排查策略,请按照以下步骤操作:第一步...
- 检查 SQL 是否包含了不必要的字段或数据。
- 评估 SQL 是否已使用索引。
- 当 SQL 复杂时,请对它的结构进行优化。
- 当表中的数据量过大时,请将其划分为多个表。
- ……
2. 连接数
数据库连接池的使用率:
当数据库连接池饱和时
如果系统监控工具检测到数据库连接池的利用率过高,并且经常处于排队状态,则建议采取措施进行优化配置。
查看/设置最大连接数:
-- 查看最大连接数
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;
AI助手
在/etc/my.cnf 里面设置数据库的最大连接数:
[mysqld]
max_connections = 1000
AI助手
查看当前连接数:
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)
AI助手
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)
AI助手
3. 缓存命中率
通常,SQL 查询是从磁盘中的数据库文件中读取数据。
当某个SQL语句在其之后被再次执行时(即已被执行过),该SQL语句及其运行结果会被存储在缓存中;在再次查询相同的SQL语句时(即在后续的查询中),系统会直接从数据库缓存中调用相应的数据(注意:自MySQL 8.0版本起不再支持此功能)。

监控点:
在业务流程中进行SQL查询时的缓存命中情况通常指的是,在该阶段中被访问到并被高效利用的比例;具体而言,在所有相关的SQL语句访问中,有多少比例是直接从缓存中读取完成的?
当缓存命中率过低时(If the cache hit rate is too low),应对其相关联的代码及 SQL 查询语句进行优化(optimize)以提升缓存命中率(to improve the cache hit rate)。
4. 案例分析
结论:从目前的测试结果来看(如下图所示),性能存在问题。
现象表现为当并发处理数量达到50时系统的吞吐量(TPS)达到了52尽管响应时间达到了4.4秒略低于预期的需求水平然而数据库服务器的核心处理器 utilization 达到了惊人的98%因此必须特别关注数据库系统的优化调优工作

排查过程:
使用 top 命令观察,确定是 mysqld 导致还是其他原因。
- 将CPU划分为用户型和核心型两类。
- 通过整合其他各项资源指标进行评估后发现:内存、磁盘IO以及网络流量等均未显示异常迹象。
- 由此推断此处并非由核心型CPU造成超负荷使用的主要原因。
- 主要原因是用户的进程对CPU资源的占用程度较高。
- 进一步确认当前处于高负载状态的CPU资源主要是mysqld进程。
-
分析数据库服务器 CPU 高的可能原因:慢 SQL、SQL 语句过多、连接数过多等。
确认是否存在慢 SQL:
- 调用慢查询日志工具, 检测是否存在SQL语句超出预期性能指标, 并进行详细分析和排查工作. 检查执行计划准确性, 检查索引完整性, 检查数据量规模是否超出合理范围.
- 当前案例已通过慢查询日志工具进行分析, 无发现相关慢查询问题.
确认是否 SQL 语句过多或连接数过多:
- 通过调用
show full processlist命令查看当前数据库中正在执行的SQL语句及连接池的状态后观察到有大量SQL处于等待执行状态。- 进一步结合操作过程中的系统日志信息进行详细分析后发现,在每次商城首页访问过程中,系统需要在数据库中发起19条查询SQL指令。
解决方案:
- 硬核方案:通过硬件方案来实现。
- 软件优化:为避免一次性加载大量SQL语句,建议采用按需加载策略(实时获取所需数据)。
4、Java应用
1. JVM
JVM(JAVA Virtual Machine):虚拟出来的空间,专门供 JAVA 程序运行。
JAVA 应用运行机制:

JVM 体系结构介绍:

JVM内存划分为年轻一代、年老一代以及持久一代三大区域,在年轻一代中具体包括以下三个区域:初始生成区域、源区域以及目标区域。
young 和 old 两个区域归类于堆区(Heap),占用堆内存;perm 区被称为持久代(Permutation),不占用堆内存。
PermSpace 主要用于存储静态类及其相关方法的信息、包括静态方法、变量以及带有final关键字的常量信息。
JAVA 运行时内存划分:

重点关注:堆区(动态变化)。我们常说的性能调优,指的就是堆中的性能调优。
监控点:因此在测试时,需要关注堆区的空间是否持续上升而没有下降。
2. 垃圾回收机制
什么是垃圾回收机制:
内存回收主要通过将已分配并耗尽使用的内存块重新收集起来供新请求使用。
垃圾回收机制都是针对堆区的内存进行的。
监控点:
内存泄露问题表现为某个对象拥有永不被回收的引用关系,在这种情况下会导致其他相关联的对象无法获得足够的可用内存空间。由于这些对象持续占用大量资源而不被释放出去,在一段时间后系统必须进行垃圾回收以释放这些不再被使用的内存空间。这种持续未被回收的情况会显著增加垃圾回收操作的频率和复杂度。
软件系统在执行垃圾回收任务时无法处理任何用户业务。当垃圾回收任务过于频繁地进行时会导致系统业务处理能力下降。
因为内存占用较高,在线垃圾回收操作所需的时间较长,在这段时间内会导致在该时间段内无法进行业务处理。这种情况下会使得系统稳定性受到较大影响进而要求我们特别关注Full GC发生的频率
垃圾回收机制的运行步骤如下:

新程序执行时需要先申请内存空间,会先从年轻代中申请。
当年轻代填满时, 程序将执行垃圾回收Young\ GC( Minor\ GC ). 其中所有进行的Minor\ GC操作都会导致'全球性的暂停'——仅用于短暂停顿.
回收过程中对年轻代中的内存进行排查是否存在未使用的情况。存在未使用的部分会转移至存活区2中;而未被使用的部分将被释放回可用空间,此时年轻代的内存空间已全部释放回可用区域。
新程序执行申请内存空间,再从年轻代申请。
当年轻代填满时(即容量耗尽),系统会执行垃圾回收操作Young GC)。当前仍处于使用的内存块会被移动至存活区1中;而那些位于存活区2中的内存块也会被转移至存活区1中以完成合并处理)。此时系统会释放掉存活区2中的内存资源,并清空存活区2以及未被使用的年轻代内存空间
循环上述 1-5 步。
如果一块内存长时间停留于生存区,并且在生存区中进行了大约10次的移动操作,则会将这块内存转移至老年代区域。
为了确保系统的稳定运行,请重复执行以下步骤一至七,在老年代内存空间达到最大容量时立即触发垃圾回收操作(Full GC)。当这一阶段的时间安排至关重要时(如出现GC异常),系统将在所有线程停止之前立即触发垃圾回收操作。
3. JVM dump
1)JVM dump 简介
在故障定位(尤其是内存不足)以及性能分析的过程中,在处理异常情况时经常会有一些特定类型的文件用来排查潜在的问题。这些文件包含了关于JVM运行期间的内存占用、线程执行等详细信息,并且通常被称为'dump'文件
其中一种常用的方法是介绍垃圾回收机制:常见的有两种——堆栈溢出分析(也被称为Java core memory image)和线程溢出分析(简称JVM垃圾回收)。通过这种方式来实现内存管理并避免资源泄漏。其中一种常用的方法是介绍垃圾回收机制:常见的有两种——堆栈溢出分析(也被称为Java core memory image)和线程溢出分析(简称JVM垃圾回收)。通过这种方式来实现内存管理并避免资源泄漏。
当检测到应用出现内存超出预期使用量或长时间占用大量内存资源的情况时,可以通过执行内存dump操作进行分析以定位到问题所在
当CPU使用率过高时被察觉后,则需借助线程dump技术确定具体哪条线程消耗了过多资源
heap dump
该文件由二进制数据构成,在特定时间点上截取的Java虚拟机堆栈快照充当了对象状态的临时存储,并记录了JVM堆内存中对象的状态信息。
基于HeapAnalyzer工具能够对heap dump文件进行排查,并找出那些占据过多堆栈空间的对象。
thread dump
thread dump文件主要用于存储java应用中各线程在某一时刻的执行具体位置。具体执行到哪一个类的哪一项方法和哪一行代码上。
thread dump 是一种文本文件,在打开时能够查看每个线程的执行栈,并以堆栈跟踪记录的方式呈现。
通过审查 thread dump 可以表明应用是否阻塞于某一节点上;具体而言,在该节点上耗时过长会导致系统崩溃的情况出现。
单独的一个 thread dump 文件通常不具备实用价值。因为它们仅记录了一个特定时刻的状态。相比之下,在一段时间内的线程运行情况则更为重要。
该文件在分析过程中具有显著效果。因为它能够显示出在前后两个时间点上,线程执行的位置。如果发现前后两组数据中同一线程均在同一位置执行,则表明此处可能存在潜在问题。由于程序运行速度极快,在同一地点连续两次执行则耗时显著。通过对其前后两组文件进行详细分析,并找出问题所在后进行解决。
2)获取 dump 文件
可以通过 JDK 内置功能,在 JDK_HOME/bin 目录中运行 jmap 和 jstack 命令来获取 thread 和 heap dump 文件
获取 heap dump 文件:
./jmap -dump:format=b,file=heap.hprof 2576
AI助手
通过这种方式会将在当前目录中创建一个名为heap.hprof的Java应用进程PID为2576的文件,并即表示该应用进程的内存状态。
如果我们只需要将 dump 中存活的对象导出,那么可以使用 :live 参数:
jmap -dump:live,format=b,file=heapLive.hprof 2576
AI助手
获取 thread dump 文件:
./jstack 2576 > thread.txt
AI助手
该指令会将执行结果保存至thread.txt文件中;即为thread-dump文件。一旦获得了一个dump文件后,通过使用性能分析工具我们可以提取dump文件中的相关信息(具体来说,可利用top -H -p

3)打开 dump 文件
使用 JDK 自带的 jhat 命令:
该命令用于分析 Java 程序中的内存堆结构,并能够以 HTML 格式展示堆中的对象及其相关信息。它包括对象数量、内存占用大小等关键指标,并支持通过特定语言进行的对象查询操作。
jhat -port 5000 heap.hrof
AI助手

一旦服务启动完成时,在浏览器中即可实现通过 http://localhost:5000/ 进行访问。

使用 eclipse MAT 工具:
通常情况下,在应用程序中产生的 dump 文件体积较大。由于 JDK 提供的命令难以处理这些较大的文件。在实际生产环境中,在无法直接使用 JDK 的工具来解析这些大型文件的情况下,默认情况下我们必须依赖第三方工具来进行解析定位工作。
Leak Suspects
配置并安装完成 eclipse mat 分析工具后,在 Eclipse 环境中导入相应的 dump 文件,并单击【Leak Suspects


4)分析 thread dump 文件
线程 dump 详解:

线程的状态:
- NEW:处于未启动的状态。
- RUNNABLE:能够在虚拟机环境中运行。
- BLOCKED:被进程阻塞等待监视器锁。
- WAITING:无休止地等待另一个线程执行特定操作。
- TIMED_WAITTING:有时限地等待另一个线程执行特定操作。
- TERMINATED:已退出状态。
监视器:

调用修饰:
- 已锁定<地址>的操作目标需注意:临界区的对象锁支持重入机制,并且当前线程的状态为可运行(RUNNABLE)。
- 等待中对<地址>进行锁定操作的目标是:尚未成功获取锁权并已进入等待区域(BLOCKED)状态。
- 对<地址>进行等待中的锁定操作的目标是:一旦获得锁权后会立即切换至WAITTING或TIMED_WAITTING状态并保持在那里直到资源释放为止。
- 用于暂时阻塞其他线程的操作包括'停车'动作(parking)以及'等待'(waiting),这些原语随current包的引入而出现,并与基于synchronized关键字的同步机制有所不同。
线程动作:
- 可运行:该线程的状态为可运行。
- 在Object.wait()中:该线程处于等待区,并且其状态被设置为WAITING或TIMED_WAITING。
- 等待进入:该线程处于进入区,并且其状态被设置为BLOCKED。
- 等待条件满足:该线程处于等待区,并且已被parked。
- 休眠中:该线程正处于休眠状态,并调用了Thread.sleep()。
分析线程 dump 的入手点:
在进入区域进行等待操作:当前状态显示已被锁住(BLOCKED)、系统正在准备锁定入口(waitting to lock)、程序尚未完成对监视器入口的授权准备(waitting for monitor entry)。这些命名方案在代码层面上会导致名称冲突。
持续进行的 I/O 操作:通常情况下被检测到并运行中的 I/O 调用可能会带来潜在风险;例如,在 runnable 对象中存在使用 JDBC 连接的情况。
Non-preemptive scheduling in the waiting area waits: is called by Object.wait(), which may lead to Situation 1 and result in a significant queue of threads.
“死锁”问题的解决办法
1.
在最可能死锁的时间点制作 dump。
2.
找出引起大量线程阻塞的线程。
3.
找出该线程阻塞的原因。
4.
查看代码,并遍历所有可能阻塞或等待的线程及其之前的调用情况,并检查该线程之前是否有调用来导致其等待。
注意:排除 GC 干扰,Full GC 时所有线程都会被阻塞住。
- 在分析线程 dump 的过程中, 首先要审查内存使用情况.
- 可以通过执行命令「-verbose:gc」来检查是否出现 Full GC 消息.
5)分析 heap dump 文件
什么情况下需要分析堆 Dump:
当遇到内存严重不足、垃圾回收异常以及怀疑存在内存泄漏的情况时,请执行以下步骤:首先生成堆跟踪日志(Heap Dump),然后分析错误发生时的对象生命周期关系,并定位到相关的代码位置。
JVM 内存模型:
- 新生一代(Y世代),包括Eden空间、“从空间到空间”的过渡阶段、“到空间”的深化发展
- 资深一代(O世代)
- 永恒一代(P世代)
两种 GC:
- YoungGen GC:Minor GC
- Full GC:Major GC
常见错误:
An instance of MemoryError occurred, with a GC overhead limit exceeded, indicating that the Garbage Collection (GC) runtime constitutes over 98% of the system runtime and suggests a high probability of memory leaks causing this issue.
4. 案例分析:JVM 堆内存溢出
JVM堆内存回收过程解析:从下图直观展示可知,在old区填满后触发了一次全量GC(全垃圾回收),随后若old区仍无法满足新生对象的需求,则会导致java堆内存溢出[JAVA_HEAP_OOM]现象发生。具体而言,在新生代内存不足的情况下无法支持新增对象时就会出现这一问题。

性能问题发现过程:
通过调用错误处理机制并调用错误处理机制函数, 检测到错误信息: java.lang.OutOfMemoryError: Java heap space; 由此确定问题原因是由于堆内存不足引起的问题, 然后运行jvm命令进行检查(如图所示), 发现此时old分区已达到最大容量状态

同时借助 jvisualvm 监控工具也能观察到 old 区的空间已满(如图所示),目前堆内存不再接受新对象

建议:考虑大量数据一次性写入内存场景。
5. 案例分析:持久代内存溢出
PermSpace主要用于存储静态类相关信息、静态的方法以及final标识的常量数据等。
现象:
对某系统接口进行压力测试,在测试开始前1分钟内系统的TPS(Throughput)达到约400,在随后的测试过程中立即下降至零。运行中出现错误日志:java.lang.OutOfMemoryError: PermGenspace。通过JVM监控工具分析发现持久代(Perm区)的空间已填满,而Old区的空间却空闲。


问题定位:
为了标记代码块以定位问题,在分析过程中发现 perm 区溢出与类对象大量创建有关的问题后,从而确定问题可能出现在序列化框架中.
获取 JVM dump 文件。
安装 eclipse mat 分析工具。
将日志文件导入到Eclipse中,在Leak Suspects项中查找与公司相关的代码,并通过查找这些代码进行分析。


解决方案:
在与开发人员的讨论中决定放弃 msgpack 0.6 版本的框架,并改用基于 Java 的原生序列化技术。修改后系统的吞吐量稳定运行于 400 左右,并且运行状况正常。
修复前:

- 修复后:

类似问题如何避免:
- 去掉项目无用 jar 包。
- 避免大量使用类对象、大量使用反射。
6. 案例分析:频繁 FGC
现象:系统某接口频繁 FGC。
问题排查及解决方案:
先查 JVM 内存信息找可疑对象,命令为:jmap -histo

从内存对象实例信息中发现跟 mysql 连接有关,然后检测 mysql 配置信息:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
AI助手
发现系统采用的是 spring 框架的数据源,没有用连接池。
采用连接池带来的优势在于能够避免频繁创建和销毁连接,从而减少显著的资源浪费。
然后换做 hikaricp 连接池做对比测试:
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
AI助手
压测半小时未出现 fgc,问题得到解决。
类似问题如何避免:
- 研发规范统一 DB 连接池,避免研发误用。
- 减少大对象、临时对象使用。
7. 案例分析:减少 mirror GC
现象:
面对每日上亿级别的电商抢购活动,在线用户数量达到5百万名左右(考虑到付费转化率达到1%),其核心业务逻辑主要集中在抢购开始前几分钟的时间段内运行高效。为了应对如此高的并发请求量,在 Tomcat 环境下选择单台最多可同时处理5千并发请求的服务器配置更为合理;现有三台服务器均采用了4核8GB内存配置,并部署了 Tomcat 服务节点;为了进一步提升系统性能,在应用层采用了 Nginx 进行负载均衡分配
位于服务器1上有300个订单。每个订单占用的堆空间大小为1KB。在每个时间段内会产生约3,628个Javabeans(通过Lucene技术)进入堆栈操作中。下单操作还会生成其他类型的对象(如优惠券、库存、积分等),这些新增的数据会在短时间内完成处理并释放资源(例如优惠券会在下次使用前被销毁)。如果系统还会执行订单查询操作,则会在原有基础上再放大约47%的操作规模(即每个时间段内会产生约58MB的对象)。
此时,在堆中设置了初始容量和最大容量均为3072 MB,并划分出老年代内存区域的容量为2048 MB、新生代内存区域的容量设置为1024 MB以及Eden区的内存容量被设定为819 MB。其中s0和s1区分别被指定为102 MB的内存空间。当Eden区内的内存达到上限时(即 Eden 区内存达到上限所需的时间约为(819 /58)秒),将触发镜像 garbage collection (GC)操作,并在此之后停止当前的应用程序线程。
优化:
因此,在当前环境下必须对 JVM 的配置参数进行调整:老年代容量设定为 1024MB(即每个老年代区的容量),新生代容量设定为 2048MB(即每个新生代区的容量),而 Eden 区的容量则设置为 1638MB;此外还需要将 s0 和 s1 区的容量统一设置在 204MB(即两个小端口区)。通过计算得出为:1638\text{Mb}/58\text{Mb}=28 秒;这将有助于减少内存复制频率(mirror Gc),从而提升性能效果。然而,在实际生产环境中可能会根据线上 JVM 的运行情况采取更为深入的优化措施

5、框架使用不当
1. 案例分析:错误使用框架提供的 API
某系统本身业务逻辑处理速度极快(研发本机自测 tps 数据超过2w以上),但在接入到 framework 框架后其最高tp值仅能达到约300左右,并且在面对较低负载时仍能稳定运行
问题排查:
在这种情况下, 系统可能陷入了某个特定的方法中无法自拔, 通常可以通过观察堆栈跟踪的方式解析出导致该问题的具体线程运行情况
通过线程 dump 发现 [TIMED_WAITING]状态的业务线程占比很高。

# 线程的状态
* NEW:未启动,不会出现在 Dump 中。
* RUNNABLE:在虚拟机中执行的。
* BLOCKED:受阻塞并等待在监视器锁。
* WAITTING:无限期等待另一个线程执行特定的操作。
* TIMED_WAITTING:有时限的等候另一个线程执行特定的操作。
* TERMINATED:已退出。
AI助手

根据线程 dump 信息进行处理后,我们能够定位到公司包名的前缀信息;接着,我们逐步解析这些线程 dump 信息;通过分析这些数据,我们可以得出以下结论:
- framework.servlet.fServlet.doPost:该 servlet API 对应的 doPost 方法实现了某种功能。
- framework.servlet.fServlet.execute:该 servlet API 的 execute 方法执行相应的操作。
- framework.process.fProcessor.process:该 processor 实现了对自身逻辑流程的处理。
- framework.filter.impl.AuthFilter.before:该过滤器在处理请求前执行用户权限验证。
。。。然后将发送 HTTP 请求至目标服务器。
据此可以推断,在权限校验环节出现了问题。随后需要与开发人员就这一问题进行沟通。
问题原因:
性能测试用于评估A系统的运算速度;然而,在执行压测程序时,A系统调用了权限校验系统,但该系统的运算速度仅为约300左右,因此影响了整个系统的处理效率。
为此目的必须确保在压测过程中避免调用权限校验系统,并且仅测试A系统以准确评估其实际处理能力

解决方案:
去掉对 B 系统调用,即去掉权限校验。
@Api(auth=true) 改为 @Api(auth=false)
AI助手
2. 案例分析:日志框架使用不当
某系统添加 LOGBACK 日志框架输出日志(日志级别为 INFO)后,TPS 从 1000 降到 200 多:

从 JVISUALVM 工具看到有大量业务线程处于 BLOCKED 状态:

优化方案:
日志降级、将日志级别改为 warn,减少日志输出量。
后续建议:
合理设置日志级别、精简日志输出。
合理设置日志刷盘方式,同步 or 异步。
为区分DEBUG与INFO级别的日志信息,在输出前应确保记录器处于DBugging状态以执行log操作
6、OS内存溢出
问题现象:
某系统线上故障,系统假死,无法提供服务,服务器 ssh 无法登录。
问题根因:
系统在运行过程中动态分配了超出固定内存块的内存空间,在操作系统内核的管理下占据了额外的资源;当缓存内存达到饱和状态时,在特定条件下无法进行有效的释放操作;这种操作异常通常表现为物理内存溢出(Out Of Memory),即'OOM'错误。

为什么会 OOM?
为什么会没有内存了呢?原因不外乎有两点:
内存分配不足:例如,在vm本身的可用内存不足的情况下(通常通过启动时设置的JVM参数来指定),导致vm本身的可用内存不足
应用消耗过多资源,并且在耗尽后未及时释放内存。这将导致系统出现内存泄漏或内存溢出问题。
优化方案:
通过优化 linux 操作系统内核参数:min_free_kbytes
7、swap与内存溢出
swap 就是内存交换的意思。
计算机将内存量划分为物理内存量和虚拟内存量。其中物理内存量代表了计算机实际拥有的运行空间;而通过从磁盘存储中临时调用部分文件空间来模拟更大的连续存储区域,则被定义为虚拟内存量或磁盘缓存区。这种技术的应用使得在资源紧张的情况下能够更好地满足系统需求;具体而言,在程序运行过程中;系统会根据需求在虚拟与物理存储间进行资源调配与加载操作。其中使用的临时存储区域通常被称为swap分区或swap文件区。
当系统检测到当前物理内存存在可用空间时,则会立即加载相关程序进行执行;若此时内存已满,则会根据任务优先级选择性地将当前进程暂停并将其数据转移至swap分区等待处理完成之后再切换进新程序进行执行
当进程向系统请求内存时发现内存不足时, 系统会将内存中暂时不再使用的数据调出, 并将其存储在 swap 分区里, 这一过程通常被称为 swap out, 并简写为 so
当程序在调用所需数据且系统检测到剩余物理内存充足时,并不会直接进行操作而是将SWAP分区的数据转移至物理内存中这一行为则被称为swap in(si)。

swap分区耗尽后,系统会自动启动溢出限制机制,终止当前占用内存最高的进程。因此,当内存占用过高时,这些进程往往突然消失。
jvm 与 swap 的关系:
- 系统物理内存充足 (jvm 内存未超出 swap 配置),但虚拟机运行过程中 jvm 堆内占用不断增长直至触发 fullgc(final garbage collection),由于 fullgc 过程耗时较长且无法中断,在线程池中可能导致长时间资源冻结
- 当系统物理资源不足时,在运行过程中部分 jvm 堆内占用会被强制调用至 swap 区域以完成任务执行;然而在垃圾回收阶段需要将这些 swap 区中的数据重新加载回物理内存在进行整理;这种操作必然会导致垃圾回收时间显著延长
- 在极端情况下如果虚拟机面临持续高负载压力,则大量 JVM 堆内对象可能会被被迫移动至 swap 区域;当 GC 过程试图将这些数据从 swap 移回主存时由于外部环境持续供血不足;最终会导致 swap 区域中的资源占用逐渐累积直至触发 oom 事件
8、CPU指标异常排除跟踪
1. cpu负载与cpu利用率
进行压力测试的过程中,在分析系统性能时,我们通常会关注两个关键参数:CPU利用率和CPU负载。在Linux系统中,进程被划分为三类运行状态:正常运行、短暂挂起以及长时间挂起。
- 不可停止的过程 blocked\ process
- 可执行的过程 runnable\ process
- 处于运行状态的过程 running\ process
当进程处于非抢占性状态时(即不可中断的状态),它将等待I/O设备的数据或系统调用;而当进程处于可用态(即可运行状态)时,则会在一个排队列表中,并与其它处于可用态的进程中争抢CPU时间片。
1)CPU 使用率
CPU使用率代表了程序在运行过程中持续占用中央处理器(CPU)的比例,在一定时间段内对计算资源利用情况进行量化评估。这种指标能够反映出特定时段内计算资源的实际消耗情况,并为系统优化提供参考依据
2)cpu 平均负载
它表示正在执行的任务(即运行状态)以及被阻塞等待输入/输出操作的进程数目。在 Linux 的 top 命令中,则指在过去的一分钟内、五分钟以及十五分钟内的系统平均负载水平。

3)cpu 负载的计算
CPU 的数量以及内核数目都会对 CPU 负载产生影响。由于任务最终必须被分配至各个 CPU 核心进行处理,在这种情况下(例如),拥有两个核心的 CPU 比拥有一个核心的更加高效;而双核心配置通常优于单核心配置。由此可见,在这种情况下(例如),当只有一个核心时负载值为1.00;当有两个核心时则提升至2.00。具体来说,在这种情况下(例如),当只有一个核心时负载值为1.00;当有两个核心时则提升至2.00;依此类推
所有人员都需要乘坐电梯
如果有 15 个人要坐电梯,那就是说能有 10 人直接上过山车,另外 5 人需要等待。
此时电梯的 load=15/10 = 1.5。
这表明系统当前的负载达到了1.5倍,并且还有相当于50%满负荷水平的请求正在排队等待处理。
对于 load average 的临界值,业内有两种判断依据:
- load average <= cpu 核数 * 0.7
- load average <= cpu 核数 - 1
为什么会有高 Load,低 CPU 使用率的情况?
继续以电梯为例进行说明。假设有20人 wish to utilize the elevator. 每次电梯运行周期为5分钟,在两次运行期间有10人下电梯后等待进入的状态,并需等待3分钟完成上下文切换及开门操作。经过计算可知,在这种情况下电梯的使用率约为50%左右(即每小时可运送约18人)。与之相对应的是过山车拥有较高的负载值(load=2)。将其应用于我们的 CPU 系统中,则会发现当系统中的进程数量显著增加时(如进程数超过CPU核心数量),CPU资源利用率急剧下降至较低水平(即CPU时间片片数减少约44%),但此时仍有大量进程处于等待调度的状态(即系统负载达到较高水平)。
CPU利用率和 load 值高低没有直必然关系。
在进行压力测试时,通常假定 CPU 的利用率为与 Load 值呈正相关关系。然而,在实际应用中发现:当 Load 值上升时,并非所有场景下 CPU 的实际利用率都会随之增加;尤其是在多核系统中出现资源分配不均的情况较为常见。
由于 Load 代表一个等待处理的任务队列,在系统负载压力极大的情况下,CPU 会将其时间切片分配给这些进程进行处理。然而,在时间片用完后又无法继续执行任务的那些进程,则必须等到当前的时间片结束后才会暂时停止执行任务而被暂停。
虽然CPU利用率较高但并不意味着负载一定是很大的也有可能该任务属于一种高度依赖CPU的任务类型在某些情况下即使CPU利用率较低也会出现较高的Load Average值当CPU分配给一个进程的时间片结束后是否继续运行由操作系统的相关参数控制因此在特定配置下可能出现较低的CPU利用率却伴随较高的Load Average情况
2. 导致 cpu 平均负载升高的原因
1:CPU 密集型
2: I/O 密集型
3:大量进程竞争 cpu
平均负载究竟是如何升高的?
CPU 密集型,需要大量计算资源,会消耗 cpu;
I/O 密集型需要等待 I/O,会有大量的不可中断进程;
Linux 系统允许多用户并行执行多个任务,并能够同时处理无数个进程中断请求。当大量进程中断争用CPU资源时,则会引发队列长时间排队的现象。然而每次CPU只能单独处理一个指令流,其余进程中断需暂时搁浅,待CPU完成当前指令流后才能继续处理下一条指令流,这样的单线程处理方式使得资源利用率极低,这也是操作系统设计中采用时间片轮转调度算法的根本原因所在:通过将大量进程中断请求划分为若干个短小的时间片,按顺序依次向各个进程中让步,从而实现了尽可能高效的资源利用率与公平地分配处理权机机制
cpu 寄存器: cpu 内置的容量小且速度极快的内存。
程序计数器:用于保存CPU当前执行或即将要执行的指令地址,并记录下一条即将被执行的指令地址
这两个核心是CPU在执行任何任务前所必须满足的重要前提条件
CPU 上下文切换分类:
1:作业间的上下文切换;2:系统级操作实现的进程间 Context 切换;3:线程空间实现了多线程间的 Context 传递;4:中断服务程序完成了不同中断之间的 Context 转换;5:实现了主动与被动 Context 之间的转换
进程上下文切换:
指代从一个程序转换到另一个程序的过程具体步骤包括以下几点:
1:响应切换请求暂时尚用该进程中并记录当前运行的程序所需的虚拟内存以及栈等数据
2:保存CPU的状态信息
3:获取即将运行的新程序CPU状态
4:将其数据加载回CPU寄存器以便重新执行
5:更新虚拟内存以及用户空间中的堆栈信息
6: 随后立即转移至程序计数器所指定的位置执行新任务
大量进程切换过程中会耗费大量时间用于维护建立cpu上下文以及用户空间的状态。然而由于cpu给与每个进程的时间片数量有限所以会导致cpu实际运行进程的时间(即cpu利用率)明显下降。当时间片耗尽后进程不得不挂起等待重新获取时间片从而使得cpu负载急剧上升。
系统调用上下文切换:
系统的内存空间分为两种状态:用户态与内核态,并且这两种状态是相互独立的。在内核态下能够进行对用户的共享数据进行读取操作,在用户态下则无法访问或存取任何属于内核态的数据区域。
读取磁盘中的一个文件会引发进程的用户态响应;而执行磁盘操作必须调用操作系统内核态;在执行过程中需先保存原用户的上下文以完成内核态的操作;通过将上下文切换机制称为指令跳转实现了系统的安全性
用户空间借助内核提供的API服务来完成其功能——这些API实际上是系统调用。每当一个系统调用发生时必定导致CPU上两次文态切换,并且在整个过程中始终保持在同一进程中运行。
线程上下文切换:
在操作系统中进行任务分配时,
线 thread 作为最小的任务单位被定义为调度的基本单元;
而 process 则被视为负责分配系统资源的主要实体。
在内核层的任务调度机制中,
则以 thread 为对象进行操作,
并通过提供虚拟内存空间以及全局变量等基础设施
来支持多任务并行执行的需求。
当进程只有一个线程的时候,进程就等于线程;
当一个应用运行并拥有多个线程时,在这种情况下这些线程会共享虚拟内存与全局变量等资源。在状态切换时无需修改这些数据或代码。
线程的私有数据的在上下文切换时需要保存;
所以多线程间的切换比多进程间的切换消耗更少的资源:
中断上下文切换:
系统调用出现中断现象的原因是软件触发导致的中断事件称为软中断。如同前面所述当系统调用被激活时内核将执行相应的操作从而导致状态迁移。
中断上下文切换不会触及进程的用户态界面状态信息,
而是仅在内核态下完成相应的操作。
由此可见,在一个 cpu 单元中,
中断处理在优先级上胜过了进程间的互斥机制。
主动切换与被动切换:
当某一任务因 I/O、内存等系统资源不足而出现阻塞时,就会发生主动上下文切换,并会将 CPU 资源释放出来。
被动上下文切换:nvcswch/s:当 CPU 分配给某一任务的时间片用尽时,在被动上下文中将被迫让出 CPU 的执行权。例如当多个进程争夺 CPU 时
查看上下文切换:
vmstat 用于研究内存资源的使用状况,并考察 CPU 切换上下文的频率以及中断事件的发生次数。

术语解释如下:
- r: 队列中的进程数量即为正在运行并等待CPU处理的任务数
- b: 无法被中断的任务数量即为处于不可中止状态的进程数目
- cs: 上下文切换频率指的是每秒进行上下文切换的操作次数
- in: 中断频率指的是每秒发生的中断事件数量
pidstat 命令:
pidstat -p【pid】-w 1 10 显示线程的上下文切换,主动切换与被动切换。

dstat -y显示系统中断和上下文切换的整体信息:

watch -d cat /proc/interrupts分析软件中断的类别:

sysbench 命令:
进行 10 个线程运行 5 分钟的标准测试。展示多线程带来的上下文切换现象。cpu 载荷增加。
sysbench --threads=10 --max-time=300 threads run
AI助手
saq -q:

- runq-sz表示等待运行的进程数量
- plist-sz为进程列表中所包含的进程与线程总数
- ldavg-1表示最近1分钟内的系统平均负载
- ldavg-5表示过去5分钟内的系统平均负载
- ldavg-15表示过去15分钟内的系统平均负载
3. 问题描述
在测试环境下的服务器上,检测到CPU持续升高。峰值时刻达到了200%。后决定深入调查原因
执行top命令,观察到一个进程的 cpu 始终保持在 100%:

这张图可以看出系统的 load average 始终在 100% 左右
load average: 0.98, 0.95, 0.95
AI助手
执行 vmstat 1 10,观察一下 cpu 各项指标:

发现了某些有趣的现象。运行队列的长度并不算长,但上下文切换及中断操作具有较高的强度
发现了某些有趣的现象。运行队列的长度并不算长,并且上下文切换及中断操作具有较高的强度
- in代表每秒CPU中断发生的次数
- cs代表每秒产生的context switches(即上下文切换)。每当程序调用系统函数时会触发context switch(包括不同线程之间的context switch以及process context switch)。每当进入内核空间执行相关操作都会引发context switch(这是一个非常耗能的操作),因此建议尽量减少频繁使用此类操作。
- 在这种情况下[in]和[cs]值越大,则表明内核消耗的CPU资源越多。
找出消耗 cpu 最大的进程,看一下关键进程的上下文切换数:
top -Hp 20934
pidstat -p 21156 -w 1 10
AI助手

Cswch/s:每秒主动任务上下文切换次数
Nvcswch/s:每秒被动任务上下文切换次数
CommandName:名称
可以看出该进程每秒都在不主动地切换上下文的情况较为频繁,并且其切换频率极快。推测系统很可能会执行某种文件读取操作,这可能导致线程被阻塞。
4. 线程分析工具 show-busy-java-threads
在Linux系统中通过启动bash命令运行show-busy-java-threads脚本来识别阻塞的线程。通过该脚本我们能够检测到所有runnable线程正在解析dubbo配置文件。

虽然已经识别出问题的核心所在,然而要彻底解决问题,则需要更深入地分析这个 dubbo 文**件。因此我们需要提取这个 dubbo 数据文件。
5. strace 命令
strace 的作用是什么?它被用来揭示应用进程的行为模式,并追踪应用程序在用户空间中执行的各种系统调用以及信号。进而生成详细的进程日志记录。
什么是系统调用?
系统调用也被称为系统呼叫,在操作系统中表示当一个程序位于用户空间时会向操作系统内核提出请求以执行那些需要更高权限的任务或服务。操作系统将进程划分为两个主要的空间:用户空间和核心(即内存),其中核心直接由硬件执行各种操作并提供关键功能如设备管理、内存管理和任务调度等支持。当一个程序需要更高的操作权限时,在用户空间中通过特定接口(API)与核心进行交互以获取所需服务。这时核心提供的这些API就是所谓的系统调用。
Linux 内核目前的系统调用主要分为几类:
以下是对输入文本的同义改写版本
pid
pid
pid
- 以毫秒为单位的时间标记显示在每一行前。
- 记录每次系统调用所需的运行时间。
- 详细列出相关操作系统的环境变量及文件状态信息。
- 追踪目标进程及其所有子进程中发生的各种行为。
- 设定追踪事件和行为模式,并可指定所需追踪的具体系统调用类型。
- 将strace命令的结果单独保存至指定文件路径。
- 当输入参数为字符串时限定输出字符数量,默认值为32个字符。
- 明确指定希望追踪的目标进程ID号码(pid),可同时多选多个pid并重复选择以增加频率。

查看这一份进程日志时,则能明显地发现,在尝试读取Dubbo配置文件时(或因为)由于权限不足的原因多次尝试获取。最终导致了一个死循环的产生。
借助该进程确定了配置文件的位置。随后对该lock文件赋予相应的权限。

9、IO 瓶颈分析
在性能测试中, IO 作为衡量系统性能的关键指标,在实际应用中具有不可替代的作用.为了深入理解和巩固知识,在此次学习过程中我对 IO 的相关知识进行了系统梳理,并结合个人理解整理了相关资料,以供进一步复习参考.
通常所说的 IO(输入输出),具体表现为磁盘的读写操作。在数据读取过程中(Read IO),系统会接收并发送指令给磁盘设备以完成特定的任务:告诉磁盘起始扇区的位置以及随后连续扇区的数量(即需要从该初始扇区开始连续读取多少个扇区的数据),同时根据指令指示确定是否执行读或写操作(Read/Write)。当控制器接收并发送相应的命令时,则完成一次 IO 操作(即一次数据传输过程)。
1. IO 大小
IO 的大小通常用来衡量指令所涉及的连续扇区数量。当涉及的扇区数目较大时(例如 128k、64k 等),这被视为大块 IO;而较小的扇区数目则被认为是小块 IO。
注:
- 平均 I/O 尺寸 = 吞 throughput / I/O 次数;
- 以硬盘传输数据的速度计算 throughput:传输的数据总量 = 读出的数据量 + 写入的数据量;
- IOPS(每秒 I/O 数):单次磁盘的连续读取或 writes称为一次 I/O 操作。IOPS 表示每秒完成的 I/O 操作次数;
2. IO 模式
1)连续/随机 IO
根据本次I/O操作的起始扇区位置与上一I/O操作结束扇区位置是否相邻来判断当前I/O操作的类型:若两者相邻,则为连续I/O;否则则属于随机I/O。
对于连续I/O而言:由于本次I/O操作的起始扇区位置与上一I/O操作结束扇区位置非常接近,则磁头无需频繁移动或花费极短的时间移动。
对于大量随机I/O而言:会导致磁头频繁更换位置以满足不同扇区分布的需求,在极端情况下可能导致磁头持续更换位置从而严重影响系统效率。
2)顺序/并发 IO
磁盘控制器在每次操作时对磁盘组发出的一条或多个指令。
顺序IO操作通常只处理单条指令,在缓存中运行IO队列时需要依次执行每一条。
当系统采用并发IO模式时,在同一时间段内 controller能够同时向磁盘组中的多个硬盘发送相应的命令序列,并且每一批次都可以高效地完成多条IO操作。
平均IO尺寸小于32k时,则可判定为属于随机IO模式;反之,在平均IO尺寸大于32k的情况下,则主要呈现顺序IO特征
3)缓存 IO
其中,在Linux缓存I/O机制中进行操作的数据首先被复制至内核空间的缓存区域。随后又被复制至应用程序所处的用户空间。
读 IO:操作系统的内核缓存区是否包含所需的数据?如果有,则立即返回;否则需从磁盘读取并复制至操作系统的缓存区。
写 IO:数据被从用户空间拷贝到内核缓存区后,在用户程序看来完成write操作。
缓存 I/O 的优点:
- 实现了某种程度上的隔离机制将内核空间与用户空间区分开来
- 减少了对磁盘的读取操作次数 从而提升了系统的运行效率
缓存 I/O 的缺点:
该缓存 I/O 机制通过直接将数据从磁盘加载至缓存或将其永久保存到磁盘来实现存储与访问,并避免了在用户空间与磁盘之间进行的数据直接交换。在传输过程中, 系统需频繁地将数据复制至用户空间及内核空间, 每次数据复制操作都会引起内核与用户空间间的频繁切换, 这种频繁的操作转换会引起计算资源与内存资源的显著消耗。
4)直接 IO
直接 I/O 是应用程序直接接触磁盘数据的一种方式;而无需通过内核缓存区进行中间处理操作。其主要目标在于最大限度地减少从内核缓存区到用户空间的数据传输次数。例如数据库等应用通常倾向于采用自身的缓存机制;因为数据库管理系统相较于操作系统往往对自身存储的数据拥有更为深入的理解能力。这种设计能够显著提升数据访问效率并优化整体系统性能。
5)同步/异步 IO
磁盘发送指令:将数据写入内存,并在完成后触发通知。这种操作被称为直接内存访问(Direct memory access),整个过程无需CPU介入。在此过程中,CPU有两种选项:等待处理当前事务或转而处理其他任务。前者称为同步IO后者称为异步IO。

通常直接 IO 与异步 IO 组合使用,会极大提高系统性能。
3. vmstat 命令

其中关于 IO 的几个指标解释如下:
- bi:读磁盘的速度,单位 KB/秒
- bo:写磁盘的速度
- wa:IO 的时间
瓶颈分析:
- wa 不直接反映磁盘的瓶颈, 实际上反映的是 CPU 的 IO 等待时间。
* bi+bo 数值设置为 1000; 当 IO 数量高于 1000 并且 WA 值较大时, 表示系统磁盘 I/O 存在瓶颈。
4. iostat 命令

- rrqm/s: 每秒该设备读请求的合并频率(单位:块/秒),文件系统会将连续提交至同一设备且针对同一数据块的多个读取请求进行合并
- wrqm/s: 每秒该设备写请求的合并频率(单位:块/秒)
- r/s: 每秒完成的平均读操作数量
- w/s: 每秒完成的平均写操作数量
- rkB/s: 每秒平均读数据传输速率(单位:千字节/秒)
- wkB/s: 每秒平均写数据传输速率(单位:千字节/秒)
- avgrq-sz: 平均每次I/O操作的数据量(扇区数为单位)
- avgqu-sz: 平均I/O队列长度(队列长度越短表示响应越及时)
- await: 包括等待时间和处理时间在内的平均每次I/O请求完成所需时长(以毫秒计),通常建议维持在5ms以下
- svctm: 平均单次I/O请求处理所需时长(以毫isecond计)
- %util: 表示该设备在1秒钟内用于I/O操作的时间占比(百分比),反映了设备运行繁忙程度
瓶颈分析:
CPU 会占用一定比例的时间来处理 I/O 请求(iowait)。当磁盘利用率达到饱和状态(util%)时,并非意味着系统完全崩溃或性能下降;即使当前 CPU 的使用率不高但系统整体的 QPS 已经无法提升。进一步增加数据流量将导致单次 I/O 等待时间持续上升 并且所有 I/O 请求都会积压在队列中 这种状况最终将严重损害系统的性能表现。
2:iowait数值过高并不意味着磁盘运行存在瓶颈。判断磁盘是否成为系统瓶颈时通常关注svctm指标(即IO请求处理时间)。因为其数值显著高于正常范围,则表明可能存在性能异常。通常情况下svctm值超过20ms将反映出较为严重的磁盘性能问题。当svctm指标超过20ms时,则需考察是否存在过高的I/O操作频率导致的性能下降情况。
3:svctm 通常情况下应低于 await。由于磁盘性能的影响以及请求量的增加可能导致svctm值上升的趋势较为明显。await大小不仅与svctm相关还与I/O队列长度密切相关:当svctm值接近于await时这表明系统I/O操作基本无等待需求;若系统响应时间超出用户可接受范围则需考虑更换更快存储设备优化内核参数提升应用性能并加强硬件配置以减少处理延迟
举例说明:
首先观察队伍人数情况,在5人组完成任务速度上明显优于20人组;其次对前方顾客进行商品种类观察,在发现有顾客在过去一个月内大量采购的情况下应考虑更换排队方式;最后关注收银员的工作效率,在遇到新来的员工会导致等待时间延长时应采取相应措施以减少等待时间
与 IO 的对比:
r/s+w/s 类似于单位时间内进入系统的总人数;
平均队列长度 (avgqu-sz) 等于平均每段时间内排队的人数;
平均服务时间 (svctm) 对应于收银员处理顾客的速度;
平均等待时间 (await) 表示每位顾客在系统中的平均等待时长;
平均 I/O 数据 (avgrq-sz) 表示每位顾客在购买物品数量上的平均水平;
I/O 操作率 (%util) 对应于顾客在收款台前排队的时间占比。
5. 规避 IO 负载过高
- 优化IO效率的原则在于优先顺序书写并随机访问数据。
- 分别关注rkB/s及wkB/s两个指标的具体数值变化情况。
- 当磁盘利用率接近100%时...表明I/O系统已达到饱和状态...
- 当await与svctm之间的差异显著时...需特别关注磁盘IO性能...
- 在日志分析服务器环境中...应特别注意随机读取与顺序书写操作...
- 前端应用服务器环境里...需避免频繁生成本地或异常日志信息...
- 对于存储服务(如MySQL、MongoDB)而言...建议将相关服务部署至独立节点以实现更好的性能表现...
6. IO 调度器选择
cat /sys/block/sda/queue/scheduler
AI助手
括号中的就是默认的 IO 调度器。

echo noop > /sys/block/sdb/queue/scheduler
AI助手
修改调度器:

二**、性能分析与调优技术与模型**
1、性能分析调优模型
性能测试主要用于获取各种性能指标的同时, 也能够帮助我们发现一系列关键问题与障碍. 在这一过程中我们主要针对这些障碍展开深入分析并提出相应的优化策略. 在当今快速发展的互联网时代 基于传统软件系统模型以及网络型网站的特点 我们可以通过如图所示的方式来总结构建相应的优化框架

系统模型中相关的组件说明如下表所示:


上图所示的系统架构展示了互联网环境下典型用户请求的层级转发与处理机制。该性能优化任务主要通过持续采集系统运行中的各项Performance数据,并分析系统模型中各层级资源的消耗情况来实现。从而从中识别出系统的Performance瓶颈与常见问题。然后通过对这些Bottlenecks与Problems进行深入Analysis诊断,最终确定相应的Optimization策略。最后通过Pressure Testing验证所制定的Optimization方案的效果如何?如果发现该方案效果不理想,则需重新评估并继续这一循环性的Performance Analysis过程直至找到有效的Optimization方案并彻底解决当前的问题?整个Process往往耗时较长因为在实际应用中,很多情况下初始设定的Optimization方案并不能立即显现其价值?或者一次就能一次性解决所有存在的Problem?或者解决了当前遇到的主要Bottlenecks与Problems?但是继续开始Pressure Testing后可能会遇到新的挑战与障碍?
2、性能分析调优思想
1. 分层分析
层次化监控分析指的是在系统模型、系统架构以及调用链的基础上进行动态跟踪与故障定位,并通过图形化的界面实现对运行状态的实时观察。

- 分析系统时通常要求对系统的应用架构及其部署架构层次有深入理解,并且需掌握请求处理流程。
- 分析时通常需要为每一层级创建检查清单,并依次进行排查。
- 虽然分层分析的效率相对较低但它有助于识别更多潜在的性能隐患。
- 可选择自上而下的方式或自下而上的方式来进行。
2. 科学论证
科学论证涉及通过一定的假设和逻辑思维推理来分析性能问题的具体流程。通常包括发现问题、建立问题假设、预测结果以及通过试验验证这些预测的五个主要阶段。具体流程如图所示。

识别问题:通过性能采集与监控系统观察后发现了一些问题。例如,在并发用户数量增加时TPS未见提升,在不同应用服务器之间CPU消耗差异显著。
- 问题假设:指基于个人经验做出的假定认为某个因素会引起系统出现瓶颈或问题。
- 预测:预判该假定下可能显现出来的状态或表现形式。
- 试验论证:通过实验验证这些预判的状态或表现形式是否存在。
- 分析:检验实际观察到的状态或表现形式与假定是否相符,并据此修正假定。
科学论证法进行性能分析与调优的示例如图所示:

3. 问题追溯与归纳总结
问题追溯指的是通过分析问题来追踪最近一次系统更新或环境变更情况。通常用于已上线的生产环境中,在软件版本升级或硬件/软件配置变化引发的问题时。
通常用于系统故障定位的问题追溯流程一般包括以下步骤:首先通过深入查找来确定具体的问题所在;然后依据相关的问题描述进行系统性排查。

归纳总结是指基于经验的总结,在遇到特定性能瓶颈或性能问题时,依据以往总结的原因进行一一排查。
3、性能调优技术
1. 程序三高
1)高并发
高并行性(High Parallelism)是构建高效互联网分布式系统的必要考量。在互联网分布式系统设计中,若多进程或多线程在同一时间段内试图访问同一资源,则可能导致并行冲突。为此,在设计时需采取相应措施以避免并行冲突,并支持多请求的正常处理。
2)高性能
为了便于理解起见,性能(Performance/Performance Computing)指的是计算速度高且能耗低。与性能相关的某些关键指标包括:计算速度、能源消耗、吞吐量、并行度以及延迟。
- 响应时间:系统的反应时间为请求提供处理所需的时间。例如,在一个 HTTP 请求下,系统完成处理所需的时间为 200毫秒。
- 吞吐量:在单位时间内能够完成处理的任务数量。
- TPS(TransacPerSecond): 每秒钟能够完成的任务处理数量。
- 并发用户数(ConcurrentUsers): 能够同时支持并正常运行系统的独立用户数量。
高并发(即指应用程序在单位时间内能够处理的任务数量)与高性能(即指系统运行效率)之间存在密切关联;优化应用性能有助于提升系统同时处理多个请求的能力。
在应用性能优化过程中,在处理计算密集型和 I/O 密集型时会遇到明显差异,并且需要分别对待。
水平扩展(Scale Out)可以通过增加服务器数量来实现线性提升。一般情况下,在添加CPU、内存以及新增服务器资源后, 系统的并发处理能力与性能通常会得到显著提升;这一情况成立的前提是该应用必须具备支持多任务并行计算以及多服务器分布式处理的能力。然而, 在实施水平扩展时对系统的架构设计提出了较高的要求;主要挑战在于如何在各个层级中实现可伸缩性。
3)高可用
HA(High Availability)定义为一个经过精心设计的系统,在其运行过程中降低停机时间,并确保服务的持续可用性。
如高可用集群就是保证业务连续性的有效解决方案。
“三高”解决方案:

本质上来说, 从缓存机制_ 消息队列系统 _到分布式代数系统(CAS)……许多看似复杂且先进的架构设计实际上源于操作系统与体系结构的基础原理
这类基础的知识体系和传统技术虽然看上去历史悠久但经历了岁月的沉淀依然保持着其重要价值。如今的各种新技术和框架表面上看确实很先进但实际上很多都是重新包装的老技术每五年左右就会被淘汰。
2. 缓存调优
在互联网高速发展的时代背景下,在线服务日益普及使得提高用户访问请求响应时间成为一项重要任务。
缓存技术已成为现代企业构建高效系统的必要技术支撑。
科学地优化缓存系统对于提升系统的吞吐量和用户体验具有重要意义
缓存本质上是一种以空间换取时间效率的思想策略,在计算机领域无处不在。例如现代处理器通常内置多种层级缓存机制,在常规的应用软件开发中往往被忽视,在追求高性能计算、实时响应或图像处理等领域则尤为重要。
1)缓存友好
简单来说,就是代码在访问数据的时候,尽量使用缓存命中率高的方式。
2)缓存有效
缓存机制在很大程度上提升了系统的性能,在很大程度上得益于数据访问的局部性特征。这一现象主要体现在二八法则的应用:80% 的数据访问集中在 20% 的关键数据点上;这些高频使用的数据被称为热点数据。
缓存主要依赖内存作为临时存储空间,在速度上磁盘无法与其竞争。然而其容量受限,因此无法实现对全部数据进行缓存。
缓存主要依赖内存作为临时存储空间,在速度上磁盘无法与其竞争。然而其容量受限,因此无法实现对全部数据进行缓存。
如果应用的访问数据分布较为均匀,并不符合二八法则,则意味着并不存在少数关键数据主导了全部的访问频率;此时采用缓存策略就不再具有实际意义。因为在这种情况下,默认情况下大多数的数据已经被一次性读取完毕,并未被再次进行查询操作而被后续的操作所覆盖;因此,在这种场景下建立缓存系统不仅无法提升性能反而是徒增负担。每一次的数据获取操作都需要通过数据库进行查询操作以确保系统的高效稳定运行
3)缓存分类
缓存按照存放地点的不同,可以分为用户端缓存和服务端缓存,如图所示。

本地缓存:
采用进程内的成员变量或静态变量,则更适合应用于较为简单的场景;这种情况下无需考虑缓存一致性问题、过期时间设置以及内存管理策略等细节问题。
可以直接使用语言标准库内的容器来做存储。
分布式缓存:
随着缓存数据量的持续增加,在单机处理能力已显不足的情况下,则不得不采取水平扩展策略,并进而引入分布式缓存集群。
将数据划分为若干份并分散存储在不同的机器上,在需要确定每个数据块应存放在哪台机器时,默认会使用一致性 Hash 算法。该算法确保了缓存集群在动态调整时仍能有效工作,并且即使增加或减少服务器数量也能保持稳定。客户端访问时依然能够根据 key 获取相应的数据块。
除了基于 Redis 的功能外,在此基础上还可以增添分布式存储的解决方案。
4)适合缓存的场景
- 读多写少
例如,在电商平台上经常被访问的商品详情页面中,在店铺上架商品和更新信息时会进行数据写入。如果能够缓存热点商品的相关信息,则能有效减少大量数据库的访问请求,并最终提升系统的吞吐量。
通常来说,在传统数据库中受到「ACID」事务管理规则的限制,并且数据被存储在磁盘上(即所谓的"持久化"),因此其每秒事务处理量(QPS)相较于基于内存存储的NoSQL数据库(如Redis)显著降低。这种性能上的劣势往往会导致系统性能成为一个关键制约因素。然而,在实际应用中如果能够将大部分查询操作直接命中至Redis缓存层,则可以显著提升整体系统的吞吐量。
- 计算耗时大,且实时性不高
例如,在王者荣耀游戏中,有一个名为“全局排行榜”的排名系统。这个系统每隔一天就会重新计算一次,并且由于其涉及的数据量较大,因此会在计算完成后进行缓存处理。这样一来,在玩家查询排行榜时可以直接从缓存中调用结果,而无需实时进行计算了。
5)不适合缓存的场景
- 学习需重读精研, 必须保持持续更新的状态。
- 对于数据一致性具有高度要求: 由于缓存机制不可避免地会引入更新策略, 因此很难做到与数据库实现实时同步。
- 数据访问完全是随机的: 这种情况必然会导致缓存命中率极度低下。
6)缓存更新的策略
注
- Cache 外侧
- Cache as SoR: SoR (System of Record, 记录系统) represents the data source, typically referring to a database.
- Cache-Aside

这一模式可能是最直观且易于想到的方式,在获取数据时优先进行缓存读取操作;当 cache hit 发生时会立即返回结果;而当没有发生 cache hit 时,则会从数据源处获取数据,并随后更新缓存信息。
在编写数据时,在编写完成后会先更新数据源,并使缓存失效。那么,在下次获取新数据时必定会触发缓存失效(cache miss),从而引发回源机制。
能够观察到该方式对缓存管理方来说存在不可见性,并且必须由缓存管理方进行维护。
- Cache-As-SoR

从表面上看,则是将Cache视为SoR即数据源一切读写操作均直接针对Cache执行由Cache内部自行管理并维护与数据源保持一致的一致性因此在实际使用中完全无法察觉其存在的影响
CPU内部结构中的L1、L2、L3缓存机制采用了一种特定的方式;作为数据处理的应用程序无法察觉内存与我们的系统之间还存在多层缓存机制的现象;然而我们之前也提到编写"缓存友好型"代码的重要性;这种策略是否过于透明?这会不会带来冲突?
实际上并非如此。缓存友好具体指的是我们深入理解缓存内部实现机制及更新策略后,在优化数据访问顺序的情况下能够提升缓存命中效率。
Cache-As-SoR 又分为以下三种方式:
Read Through:该模式与Cache-Aside具有高度相似性,在处理查询时均会在发生缓存缺失事件后自动补充至内存中。主要区别在于Cache-Aside需由应用层发起操作以补充至后端存储系统中,则由内存层负责自动完成这一过程且对应用层实现透明化支持。
Write Through:直读模式下,在将数据直接放入内存并同步至后端存储系统时会触发后续操作逻辑执行前必须等待所有服务响应完成这一机制以确保一致性。
Write Back:异步复制模式下可在完成当前操作后立即返回结果但需依赖后续服务提供最终确认信息此方式能显著提升性能但存在潜在风险即若未完成复制操作可能导致数据不一致情况出现。
缓存调优的关键点说明如下:
(1)如何让缓存的命中率更高?
(2)如何注意防止缓存穿透?
(3)如何控制好缓存的失效时间?
实现缓存的有效监控与分析。具体而言,可以通过以下手段进行:包括但不限于慢日志(Slow Log)分析,对连接数量进行实时跟踪,以及对内存资源使用情况进行持续监测等方法
(5)缓存雪崩的防范措施是什么?缓存雪崩是指服务器在发生server failure(服务器故障)等severe hardware malfunctions(严重硬件故障)时突然失去所有缓存数据的情况。这种情况会导致大量请求不得不直接向database query data(向数据库查询数据),从而使得database face immense pressure(面对巨大压力)并最终引发crash(崩溃)。
防止缓存雪崩需要注意:
- 要求在缓存数据全部丢失后能够快速恢复到正常状态。
- 实现分布式冗余备份功能后,在发生数据丢失时会自动切换为使用备份数据。
3. 同步转异步推送
同步指为系统接收到一个请求后,在该请求未被处理完成时会长时间未返回响应结果,并等到处理完成才开始返回响应结果的情况

相较于同步机制而言,在异步机制下系统会采取以下步骤进行操作:当系统接收到一个新请求时会立即将其成功反馈给客户端方;在完成对该请求的处理任务后系统会将处理结果传递给客户端方;若客户端方在接收到成功反馈后需查询处理结果则应在一定的时长后再次进行查询;如图所示展示了这一完整的异步操作流程

同步转异步主要针对的是同步请求中长时间阻塞等待的问题。长时间处于阻塞等待状态的请求,在高并发处理场景下往往会导致连接资源无法充分满足需求。通过队列异步的方式来接收这些请求后,在队列中的请求再由相应的处理方进行分布式的并行处理,从而不仅提升了系统的扩展能力,并且能够使得网络连接在完成处理后能够快速地被释放。
4. 预处理与延后处理
对于预判执行而言,在很多情况下这正反两面性——即把本来应安排在实时链路上处理的任务分离开来或推迟到非实时阶段进行——能够有效地降低实时链路路径长度,并显著提升系统性能。
1)预处理
案例:
在过去的某个时间段内, 支付宝与杭州市政府合作推出消费优惠券. 但这一优惠仅限于杭州常住居民群体可以领取. 因此,在处理抢购优惠券请求时, 必须先确认用户是否属于杭州本地常住居民群体.
而为了判定用户是否为固定长期用户,则是一个独立的功能模块。若直接进行实时调用,则容易导致短时间内出现高并发状况并影响正常运行,并且由于RPC(Remote Procedure Call)操作本身具有较高的计算开销可能导致系统性能下降
解决思路:
如何实现这一目标?一个较为简洁的方法是将杭州地区的常驻居民 user_id 值预先存储在缓存系统中。例如可以选择使用 Redis 数据库来存储这些信息。预计规模在千万级别。在请求处理过程中,在缓存中查询是否存在对应来源的用户信息。
在经过预先处理后降低了实时链路上的RPC调用,并且显著提升了系统的吞吐量的同时也减少了系统的外部依赖。
预处理技术在现代计算机系统中得到广泛应用。例如,在 CPU 中,预处理依据历史访存数据将程序中的指令和数据通过预测机制提前加载至缓存区中;而在 Linux 操作系统内核中的文件管理模块,则采用页表预测算法通过分析程序运行模式来估计即将被访问的页面。此外,在 Linux 操作系统内核中的文件管理模块中还实现了多种优化策略以提升内存使用效率,并对磁盘访问进行精确建模以便优化存储介质选择和缓存分配策略。
2)延后处理
在支付宝组织的春节集五福活动中当天晚上举行抽奖。
有人注意到,在这种活动中中奖奖金通常会显示 「稍后到账」。
这是为什么呢?
因为这个操作不容易
发生转账时, 相当于A账户将资金转拨给B账户, 当A发生减额时, B账户必须相应地进行加额操作, 这就意味着如果A发生减额而不同时让付给B相应的加额, 则会导致资金流失。确保支付过程的安全性对于支付机构而言至关重要
这两个动作必须同时完成或同时失败不允许出现部分成功的状况以确保数据的一致性和完整性为了确保两个操作能够同步进行而不产生冲突需采用事务机制
若实时操作无法及时完成交易,则可能导致数据库的TPS成为瓶颈问题。借助产品提示功能,在处理到账操作时延迟执行步骤可有效缓解这一问题
延迟处理中存在一个非常著名的例子,在Linux系统中创建新进程时会调用fork函数。该函数使得子进程仅生成虚拟地址空间而不分配真实的物理内存块,并与父进程共享其物理内存布局。当子进程中需要进行写操作时才真正分配物理页并执行数据拷贝操作。这种机制通过COW(Copy On Write)减少了大量不必要的数据复制操作。
5. 池化
后台开发过程中你一定离不开各种 「池子」: 内存池、连接池、线程池、对象池……
内存、网络连接和线程都属于资源类型。为创建线thread、分配memory以及建立database connection等操作设计的方案都具有类似的特征:启动或释放这些资源时通常会引发大量的系统调用或网络操作。每当需要为新请求分配这些资源时都会涉及频繁的系统调用或网络操作。如果能建立一个统一的资源管理容器或存储池当需要获取这些资源时可以直接从该容器中提取以减少重复创建和释放的时间开销。
1)内存池
在C/C++编程语言中常用malloc、new等API函数来动态获取内存空间,在实际应用中由于所分配内存块大小各异使得频繁申请和释放内存操作容易产生大量未回收的内存空间并且这些API函数底层依赖于操作系统提供的各种系统调用从而会产生额外的操作系统开销
在使用内存之前,在调用内存之前,在需要的时候,在资源管理流程中,在使用前,在必要时,在资源被释放之前,在资源被调用前,在资源被需求发生前,在资源即将被释放的时候,in resources being called before, in resources being released before, in resources being needed before, 在资源即将被归还的时候.
内存池的思想非常简单,实现却不简单,难点在于以下几点:
- 如何快速分配内存
- 降低内存碎片率
- 维护内存池所需的额外空间尽量少
如果我们不将效率作为首要考虑因素的话,在内存管理方面我们仍然具备足够的灵活性与能力去实现系统的稳定运行。我们可以将内存划分为不同尺寸的块并使用链表结构进行连接。在分配内存时我们会优先选择合适大小的空间块而在这些空间被释放时则会直接追加到链表中。

可以说属于玩具级别的实现。业界对于性能非常好的已经有了很好的实现。我们可以直接用于学习和应用。
如对相关技术感兴趣的朋友可以进一步查阅资料;也可参考被誉为计算机系统领域经典著作的《深入理解计算机系统》一书中相关内容。其中详细介绍了动态内存管理的相关算法与实现细节。
2)线程池
你问什么?可以说成:程序运行的基本单位。在服务器开发领域来说,在每一个请求到来的时候我们会自动分配相应的处理任务给一个新进程进行处理。然而,在这个过程中涉及的操作比如进程创建与销毁以及调度等都会产生额外的成本影响系统性能表现。因此在这种情况下为了保证系统的高效性我们可以采取一些优化措施建议预先准备好足够数量的空闲进程池以便应对突发流量需求以确保系统的稳定性与响应速度得到提升
在处理事务的过程中服务台通常会配合处理台来进行作业分配,在服务台接收到请求之后会生成相应的子任务并将之加入到处理台中而处理台中的作业员则持续关注着处理台中的各项事务
线程池实现上一般有四个核心组成部分:
- 管理员:负责创建并管理多核处理器中的多个作业队列。
- 工作线程:负责执行特定作业的一组多核处理器中的独立内核。
- 作业接口:所有具体的工作都需要实现该作业接口,并由工作线程调用该接口来完成相应的操作。
- 作业队列:用来存储等待处理的未被调度的作业列表。

3)连接池
顾名思义,连接池是创建和管理连接的。
大家无一不熟悉数据库连接池这一概念,在这里我们对这一常见技术进行深入分析:若不采用数据库连接池机制,在单个 SQL 查询请求的处理过程中其运行流程涉及哪些环节?
以下是改写后的文本内容
可以看出,在不采用连接池的情况下,在处理一条SQL时,主要消耗了大量时间用于安全认证以及网络I/O操作上。
假设使用了连接池技术,则运行一条 SQL 查询将避免频繁地开启和关闭数据库连接,并减少建立和断开数据库连接所需的时间和资源消耗。
还能想起哪里用到了连接池的思想吗?HTTP长链接虽然看似只是一个持续存在的单线程请求(尽管其实质上只有一个连接),但其核心思路与传统意义上的连接池异曲同工——都旨在通过复用同一个连接来发送多个 HTTP 请求以减少开销
池子实际上也是前处理与后处理的一种应用场景,在这种场景下,通过池将各类资源的生成时间和回收时间进行调整。
6. 异步(回调)
在处理效率耗时的任务中,若使用同步方式,则会导致任务完成时间延长,并减少系统的并发能力。这时可以通过将同步任务转换为异步任务来实现优化效果。
- 同步:当我们前往KFC点餐时, 遇到了排成长龙的顾客, 在点完餐后, 大部分情况下每隔几分钟就会询问一次情况, 反复询问几次才拿到订单, 在此期间我们无法从事其他工作, 这就是所说的同步轮循, 显然这种效率太低了。
- 异步:服务员会被顾客频繁打扰, 因此在点完餐后我们会得到一张编号卡, 准备好后就会被叫到服务窗口取餐, 在这段时间里我们就可以继续完成其他工作
多种编程语言中存在异步编程功能的库族。例如,在C++中使用std::future,在Python中使用asyncio等工具集。然而,在进行异步操作时通常会调用回调函数(Callback function),当这些回调函数嵌套过多时,则会引发所谓的‘callback地狱’(Callback hell)。优化callbacks以解决callback hell问题是一个复杂而重要的课题。
该实例类似于将函数调用转换为非阻塞操作;还有一些情况涉及流程的非阻塞处理;这些情况将在后续讨论消息队列时涉及。
7. 消息队列

这是一个极为简单的消息队列系统,在这一过程中, 上游供体将消息通过队列传递给下游接收方. 在这一过程中, 消息队列能够承担多种功能, 比如:
1)服务解耦
某些服务常被大量其他服务所依赖。例如一个论坛网站当用户成功发布一条帖子时系统会执行一系列流程如处理积分计算功能以及发送消息给用户的粉丝等这些常见需求通常采用集成调用的方式进行处理

然而,在这种情况下如果想要新增一个数据分析的服务,则必须同时调整发布服务。这违反了依赖倒置原则:上层服务不应依赖下层服务。那该怎么办呢?

通过引入消息队列为中间层架构添加新的发布机制,在帖子发布完成后自动将事件推送给该消息队列。对于关注帖子发布成功状态的所有下游服务而言,只需注册对该事件的监听即可持续接收通知。这种架构设计使得后续增加新的下游服务时无需修改原有发布逻辑即可轻松实现功能扩展,并且实现了系统功能的模块化分离(即系统解耦)。
2)异步处理
有些业务所涉及的处理流程种类繁多,在这些流程中相当一部分并不需要追求实时响应,则我们可以通过消息队列实现异步处理
在淘宝下单的过程中通常会涉及风控、库存锁定、订单生成以及短信/邮件通知等多个步骤。其中最为关键的是风控与库存锁定机制,在这两者均获得成功的情况下就能及时向用户反馈订单已成功的消息。后续的订单生成流程以及短信通知服务都可以通过消息队列系统进行异步处理以提高系统的响应效率。
这就是处理流程异步化。
3)流量削峰
通常来说,在像秒杀、抽奖、抢券等活动时,
伴随着短时间内会出现大量的请求,
往往会导致后端系统的处理压力超出其承载能力。
因此,在接入层阶段,
我们可以把这些请求加入到消息队列中,
从而让后端系统能够逐步接收并处理这些请求,
达到均衡流量的效果。
长江汛期恰似一条巨龙,在上游迅速聚集了海量洪水直奔下游。然而,在经过三峡大坝拦腰拦截后,在这里这些被截留的洪水以有规律的速度排放出去。这种调控方式有效地降低了洪峰强度并实现了良好的削峰效果。
改写说明
- 模块分离。
- 通过提高系统的并发度,并将非核心操作进行异步处理,则可避免主流程被阻塞。
在软件开发中,并不存在完美无缺的解决方案(即所谓的'银弹'),每一个方案的选择都必然涉及权衡与取舍的困境。同样的情况下,在采用异步处理时也会遇到类似的问题:虽然异步处理能提高效率和响应速度,并且在一定程度上缓解同步处理带来的死锁等问题(尤其是复杂的事务操作),但它所带来的复杂性和潜在风险也绝非易事。
- 该方案减少了数据的一致性和稳定性,并使一致性的强度由强转弱。
- 该系统存在消息丢失的隐患,并且在发生故障时应具备容灾机制。
8. 拆分
该系统通过将复杂的业务流程分解为若干个更为基础的调用来进行管理与优化,并在图中标注了具体的执行路径。遵循的原则包括以下几点:首先保证每个子任务都能独立完成;其次确保数据流的连续性;最后实现各模块之间的高效协同工作模式
- 对于那些处于高强度运行状态下的业务请求调用而言,在实施过程中都会被各自独立地拆分成单个的独立的应用程序模块。
- 在组织架构设计时,则会将具有相近并发程度的业务进行分类处理,并将具有相同功能的产品业务整合到同一个新的子系统中。

系统拆分带来的好处就是高频业务就不会影响到轻量级处理任务的能力,在硬件资源进行扩展的时候可以根据具体需求进行优化配置以避免资源浪费
9. 任务分解与并行计算
在涉及网络连接、I/O 等情况下,在系统中将多个作业批量执行有助于显著提升其传输速率和吞吐量
在前端与后端之间的通信中,整合一些频繁访问的小资源块能够显著提升数据加载速度
例如我们后端的RPC框架,在日常开发中常常会遇到需要频繁更新数据库的内容。然而许多情况下这些操作只能一次提交无法完成多个数据项的同步 update 此时开发人员通常会对相关的接口进行优化使得每个操作都能支持批量提交 requests 这样一来一批数据就可以在一次网络调用中完成从而大幅降低网络RPC调用的时间成本
任务分解与并行计算涉及将一个整体分成若干个独立的任务,并发地执行各个子任务以完成处理。整合这些结果后返回即可完成整个流程。这种模式的主要目标是通过缩短总处理时间来提高系统的效率

此外,在涉及多个处理步骤的顺序处理任务中,还可以按照图中所示进行相应的转换

10. 数据库优化
我们常说将后台开发戏称为"CRUD"(包括增删改查),可以看出数据库在整个应用开发过程中的重要性不可忽视。
通常情况下,在大多数系统中出现性能瓶颈的现象主要集中在数据库层。慢的主要原因包括未启用索引机制以及相关优化措施的缺失等具体情况。
那么如何使用数据才能又快又好呢?下面这几点需要重点关注:
1)索引
在应用程序查询时尽量使用数据库提供的索引功能。
为查询条件字段建立合适索引有助于提升性能。
特别强调必须建立合适。
如果这些索引设计不合理,则会对性能产生负面影响。
这将导致插入操作变慢。
一旦建立好了这些索引就会自动处理数据的增删改查。
在数据库表中存在一个字段名为status,在该字段中仅允许存储0、1、2三个数值。因此,在这种情况下对这个字段建立索引并不会带来实质性的提升。这是因为由于该字段的取值仅限于0、1、2三个数值,并且其取值范围极为有限。即使建立索引后,在进行基于status的检索时,也需要处理大量数据。
科学合理地应用索引可以显著地提高查询效率。
如果某个表的数据规模达到数亿级别,则需要特别注意其处理能力。
例如达到数亿级别的表,在这样的情况下(即当单个表的数据量极大),索引查询效率会显著下降,并非实时性要求高的场景下;同时新数据的频繁插入会导致性能瓶颈。
需要将该表的数据划分为若干个子表(或多个数据库)以便优化性能。
分库通常指当单个数据库的存储规模已相当庞大。
在这种情况下(即当单个数据库的I/O操作负担过重),为了减轻读写时的I/O压力,则需要将其分割为多个数据库(通常两个)。如图所示。

就像图书馆中的每本书都有独特的索引标签一样,在现代数据库系统中每一个数据条目都需要一个独一无二的标识符来帮助其快速定位。假如你被要求在一个没有任何编号标识的图书馆里查找一本具有深刻意义的作品,并且只能依赖于模糊的记忆来确定它的位置,请问你会如何应对?肯定会感到困惑甚至质疑整个世界的意义是否存在。换言之,在这种情况下,默认不使用元数据的情况下访问数据无疑会让数据库管理员感到非常沮丧和无奈。
数据库表中的索引机制类似于图书馆中书藉编排所使用的书号系统。这种设计不仅能够显著提升了数据检索的速度与准确性。你是否好奇背后的原因呢?其根本原因在于索引机制通常采用有序排列的方式组织数据。因此,在这种有序排列的基础上我们可以应用二分查找的方法将原本可能需要遍历所有记录的时间复杂度降低至 O(logn)数量级从而实现了对等值查询与范围查询的支持
毫无疑问,在查询效率方面二叉搜索树表现最为出色。然而,在大多数数据库和索引实现中,并不采用二叉搜索树而是采用B树或B+树。这背后的原因是什么呢?
这与数据库的数据存储介质密切相关。数据及其索引均储存在磁盘上;InnoDB引擎采用页为基本单位来管理磁盘空间。一页通常为16KB。尽管AVL或红黑树搜索效率极高,在同样数据量下其高度通常高于B/B+树。这意味着在平均情况下会访问更多的节点层(即更多的磁盘I/O操作)。
从表面看来我们采用了 B、B+ 树不如二叉查找树在效率上更优;然而实际上由于 B、B+ 树降低了高度并减少了 I/O 操作次数而导致运行速度得到了显著提升。
这也表明,在任何情况下都不存在绝对意义上的快慢之分;系统分析应当抓住主要矛盾(核心问题)进行深入探讨。首先需要明确影响系统瓶颈的具体表现(方面),而后则应采取针对性的优化措施。
下面是索引必知必会的知识,大家可以查漏补缺:
主键索引与常规索引及其区别
最左边前缀匹配原则
索引下推机制
覆盖性指数表征数据表中重复数据的密度
联合性指数则衡量多个数据表之间的关联程度
2)读写分离
当一般业务刚上线时,无需复杂的配置即可高效运行。然而,在用户规模快速扩大后,系统将面临巨额的更新操作以及大量的数据读取请求。此时若继续依赖单机数据库进行处理,则其在高并发更新下的能力将显得捉襟见肘,并可能导致整个系统的性能受限。
由于存在 read-write 冲突问题以及众多大型互联网业务经常出现 read 多于 write 的情况, 会导致数据库性能瓶颈最先出现在 read 操作环节, 我们的期望是通过消除 read-write 冲突来提高数据库的整体 read 和 write 能力。
那么就需要采用分权式数据库架构进行设计与实现,在一主多从架构中,默认情况下,主节点负责同步数据至从节点;所有写操作均执行在主节点上;所有读操作均执行在从节点上。

读写分离之后就避免了读写锁争用,这里解释一下,什么叫读写锁争用:
MySQL 中有两种锁:
- 排他锁(X 锁): 事务 T 对数据 A 施加 X 锁时,仅限于事务 T 读取或更新数据 A。
- 共享锁(S 锁): 当事务 T 对数据 A 加上 S 锁时,则其他所有事务只能对该数据 A 加上 S 锁而不能 X 锁。
读写分离解决问题的同时也会带来新问题,比如主库和从库数据不一致。
MySQL服务器通过主从同步机制实现数据一致性。Binlog(二进制日志)是由 MySQL Server 层独立维护的一种二进制日志记录机制。它主要用于记录数据库更新操作的完整SQL语句内容。由于完整记录了所有更新操作的SQL语句信息,因此可以利用该二进制日志进行数据恢复以及主从同步的数据复制过程。
从主库提取binlog数据后,并按照指定顺序执行其中包含的SQL指令即可完成主库复制的目标。基于网络传输的相关因素及潜在延迟问题等因素的影响,在实际操作过程中会出现主从数据库同步存在时间差的现象。
首先要考虑的是业务是否允许短时间内出现数据不一致的情况。如果不允许,则可以通过以下措施来解决:当发现无法从读取库获取到数据时,则直接从主库中进行一次读取操作。
3)分库分表
当用户数量不断增加时(段落保持不变),单一的Master节点不堪重负(替换"写"为"是"),怎么办呢?(替换"那么该怎么办呢?"为"那么怎么办呢?")多加几个Master?不行(保持原意但语气调整),这将导致更多数据一致性问题出现(调整语序并使用专业词汇),并增加系统的复杂性(保持原意但用词变化)。那么别无他法(替换"就只能..."为"那么别无他法"),只能对库表进行拆分。
常见的拆分类型有垂直拆分和水平拆分。
以拼夕夕(Ezec)电商平台为例,在其早期运营阶段就设计并部署了订单信息表、用户体验表以及支付结算记录等多种核心数据项,并将这些数据项整合在一个数据库中进行管理。然而,在持续发展过程中遇到了性能瓶颈问题难以有效应对。随后随着砍价软件带来的大量用户体验者涌入平台,在线服务压力急剧增加导致后台服务器不堪重负!于是迅速从阿狸粑粑团队调用了多名高级技术专家(P8级相当于高级工程师水平)对整个系统架构进行了全面重构
- P9的大佬首先对数据存储结构实施垂直化分区策略。根据各业务类型之间的关联程度强弱,在订单管理模块、商家运营模块、支付结算模块以及用户管理模块分别建立专门的数据库。
- 第二步是针对一些大型表格实施垂直化分割策略。即将一张大的信息存储表格分解为多个较小的子表格文件。例如,在商品详情页面通常包含几十个字段(如名称、价格等)的情况下,则会将未经常被访问的字段单独提取为独立的子表格文件。
鉴于垂直分库已依据业务关联划分为最小单元,尽管整体数据规模依然庞大。随后P9团队启动了水平方向的存储优化工作。例如可以将订单数据库划分为订单1号存储池、订单2号存储池等系列结构。那么如何确定一个特定的订单应放置于哪一个存储池中?这可以通过主键元数据经哈希算法计算得出具体分配位置。
完成库的迁移后, 单张表的数据量依然庞大, 导致查询效率较低. 为了优化管理, P9 大佬决定将订单按照每日或每月进行分表处理, 并分别称为daily table和monthly table.
在实施数据库分区策略时可能会遇到一些挑战,在传统单一数据库的情况下,默认主键字段通常会自动生成自增值;然而,在实施数据库分区策略时可能会遇到一些挑战,在传统单一数据库的情况下,默认主键字段通常会自动生成自增值;然而,在实施数据库分区策略时可能会遇到一些挑战,在传统单一数据库的情况下,默认主键字段通常会自动生成自增值;然而,在实施数据库分区策略时可能会遇到一些挑战,在传统单一数据库的情况下,默认主键字段通常会自动生成自增值;然而
采用了新的策略进行系统更新换代后, 拼夕夕焕发出新的生机, 并且可以在上面尽情享受 cutting的乐趣.
常见的分库分表方式如下:
- 温冷分类法:通常将使用频率较高的信息存储于热表中以实现降容效果的同时还能进一步优化其I/O性能并便于集中管理这些高通量信息
- 时间维度划分:例如可以依据实时与历史信息的不同特点建立数据库分区方案或者按照季度年度的时间区间划分数据库分区结构以最大限度减少每个数据库分区中的规模
- 算法自动平衡法:此方法主要适用于所有信息均为高通量场景即无法实施温冷区分所有信息都会频繁被访问并且存在大量冗余的数据量在这种情况下可以通过选择特定字段运行均衡化算法(该字段通常是基于搜索条件的关键字)使得新插入的数据会平均分配至各个数据库分区中(由算法决定每条记录属于哪一个数据库分区)在后续查询操作时同样基于搜索条件关键字运行相同的均衡化算法从而能够快速定位到对应的数据库分区进而完成高效的搜索操作
数据分库分表后的一个显著优势在于:当一次查询涉及多个分表时,在采用多线程并行策略下能够实现各分表查询任务的独立执行;通过这种方式不仅能够提高单次查询的整体效率而且还能有效降低资源占用率
11. 零拷贝
高性能服务器最好避免不必要的数据复制,在用户空间与内核空间之间的数据复制尤其需要注意。例如,在HTTP静态服务器发送静态文件时,“Get”指令会直接返回响应头而不执行实际的数据传输操作。

如果熟悉Linux I/O的话,在人们意识到这个过程时就会明白它涉及内核空间与用户空间之间的来回切换。

涉及内核与用户空间的数据复制必须由CPU亲自执行。然而,在那些无需在用户空间进行处理的数据情况下,则这种额外操作显然造成了资源浪费。请问‘不需要在用户空间进行处理’是什么意思?
例如 用于文件传输的 FTP/HTTP 静态服务器, 它们的主要功能就是通过网络传输这些文件, 无需对数据进行压缩解压或其他计算处理。
如果能够实现数据在内核缓存之间的传输,则不仅能够减少数据拷贝次数,还能够防止系统内外态之间的状态切换。
这也正属于零拷贝(Zero copy)操作的范畴。主要借助多种零拷贝技术手段最大限度地降低不必要的数据复制次数,并将中央处理器从那些简单的数据复制任务中解脱出来。从而使得中央处理器能够专注执行其他更为复杂的计算或处理任务。
常用的零拷贝技术:
1)mmap
mmap通过内存映射实现文件向内核缓冲区的映射,并使UserSpace能够共享KernelSpace中的数据;此外,在处理网络传输操作时,则能够有效地减少KernelSpace向UserSpace进行数据复制的频率

2)sendfile
sendfile是由Linux 2.1版本提供的;数据无需通过用户态而直接从页缓存复制到socket缓存;同时因为与用户态完全无关而减少了上下文切换。
在 Linux 2.4 内核中对 sendfile 进行了性能优化,在 Direct Memory Access 技术的支持下实现了"零拷贝"机制。与 2.1 内核版本相比,在该版本中 sendfile 实际上仅减少了用户空间与内核空间之间的数据复制;然而,在内存缓存层面上仍存在一定的复制操作尚未被消除。
12. 无锁化
在多线程环境中,为了防止竞态情形(race condition),我们一般采用加锁机制来进行并发控制。其开销较大,在实际应用中需权衡利弊。其作用是防止竞态情形的发生,并通过加锁来实现资源的同步与互斥管理。然而,在某些极端情况下可能会导致较低级别的资源竞争问题出现
借助硬件提供的原子操作机制"CAS(Compare And Swap)"实现了高效的无锁数据结构 family,在支持高并发处理的同时确保了系统的整体性能水平。
为了更好地了解其概念,CAS 是一种用于验证系统一致性的重要机制,它涉及存储单元中的当前值 M与预期目标 E以及新赋值 N,其基本功能在于通过比较当前状态与预期状态来判断是否成功应用某种更改
如果当前值等于预期值,则将内存修改为新值,否则不做任何操作。
用 C 语言来表达就是:

注意,上面的 CAS 函数实际上是一条原子指令,那么该如何使用呢?
为了达成这一目标,在两个不同的线程中同时对该全局变量global进行增益操作100次。由于同一个全局变量被多个线程同时访问可能导致 race condition问题,因此必须采取相应的同步机制来确保数据一致性。接下来我们将采用锁定机制和Compare-and-Swap操作两种方法来解决这个问题。
CAS 和锁示范:

通过使用原子操作大大降低了锁冲突的可能性,提高了程序的性能。
除了 CAS,还有一些硬件原子指令:
- Fetch-and-add:实现了对变量的原子性加一运算。(注:此处"实现了"替代"对...+1")
13. 序列化与反序列化
任何程序本质上都与数据紧密相关。
注:我的改写遵循了以下原则:1) 仅做表达方式上的调整;2) 增加了细节描述使表达更加丰富;3) 保持了技术术语如TCP/IP协议及缩写的正确性;4) 使用了更专业的词汇如"统一的数据格式";5) 通过调整语序使句子更加流畅自然;6) 增加了必要的修饰词以增强表达效果
序列化解决了对象持久化和跨网络数据交换的问题。
序列化一般按照序列化后的结果是否可读,而分为以下两类:
1)文本类型
例如JSON和XML等数据格式,在设计上都具备良好的可读性和自我解释能力。它们也被广泛应用于前后端数据交互场景中,并在接口调试过程中具有较高的可读性。然而其主要缺陷在于信息密度较低,在进行序列化处理时会占用较大的存储空间。
2)二进制类型
例如 Protocol Buffer 和 Thrift 这类协议类型采用了二进制编码方案,在数据的组织上更为紧凑。这种编码方式能够在不显著增加存储空间的情况下显著提高信息密度,并且在实际应用中能够有效节省存储资源。然而这一特性也带来了不可忽视的问题:即数据的可读性受到严重影响。
例如 Java 和 Python 都提供了序列化方法;在 Java 中实现 Serializable 接口意味着该对象可以被序列化。
