共计 7952 个字符,预计需要花费 20 分钟才能阅读完成。
导读
x86、arm 指令都很多,无论是利用程序员还是数据库内核研发大多时候都不须要对这些指令深刻了解,然而 Pause 指令和数据库操作太严密了,本文通过一次十分乏味的性能优化来引入对 Pause 指令的了解,冀望能够事倍功半地搞清楚 CPU 指令集是如何影响你的程序的。
文章分成两大部分,第一局部是 MySQL 集群的一次全表扫描性能优化过程;第二局部是问题解决后的原理剖析以及 Pause 指令的前因后果和优缺点以及利用场景剖析。
业务构造
为了解不便做了局部简化:
client -> Tomcat -> LVS -> MySQL(32 个 MySQLD 实例集群,每个实例 8Core)
场景形容
通过 client 压 Tomcat 和 MySQL 集群(对数据做分库分表),MySQL 集群是 32 个实例,每个业务 SQL 都须要通过 Tomcat 拆分成 256 个 SQL 发送给 32 个 MySQL(每个 MySQL 上有 8 个分库),这 256 条下发给 MySQL 的 SQL 不是齐全串行,但也不是齐全并行,有肯定的并行性。
业务 SQL 如下是一个简略的 select sum 求和,这个 SQL 在每个 MySQL 上都很快(有索引)
SELECT SUM(emp_arr_amt) FROM table_c WHERE INSUTYPE='310' AND Revs_Flag='Z' AND accrym='201910' AND emp_no='1050457';
监控指标阐明
- 后述或者截图中的逻辑 RT/QPS 是指 client 上看到的 Tomcat 的 RT 和 QPS;
- RT:response time 申请响应工夫,判断性能瓶颈的惟一指标;
- 物理 RT/QPS 是指 Tomcat 看到的 MySQL RT 和 QPS(这里的 RT 是指达到 Tomcat 节点网卡的 RT,所以还蕴含了网络耗费)
问题形容:
通过 client 压一个 Tomcat 节点 +32 个 MySQL,QPS 大略是 430,Tomcat 节点 CPU 跑满,MySQL RT 是 0.5ms,减少一个 Tomcat 节点,QPS 大略是 700,Tomcat CPU 靠近跑满,MySQL RT 是 0.6ms,到这里性能根本随着扩容线性减少,是合乎预期的。
持续减少 Tomcat 节点来横向扩容性能,通过 client 压三个 Tomcat 节点 +32 个 MySQL,QPS 还是 700,Tomcat 节点 CPU 跑不满,MySQL RT 是 0.8ms,这就重大不合乎预期了。
性能压测准则:
加并发 QPS 不再回升阐明到了某个瓶颈,哪个环节 RT 减少最多瓶颈就在哪里
到这里所有都还是合乎咱们的教训的,看起来就是 MySQL 有瓶颈(RT 减少显著)。
排查 MySQL
现场 DBA 通过监控看到 MySQL CPU 不到 20%,没有慢查问,并且尝试用 client 越过所有中间环节间接压其中一个 MySQL,能够将 MySQL CPU 跑满,这时的 QPS 大略是 38000(对应下面的场景 client QPS 为 700 的时候,单个 MySQL 上的 QPS 才跑到 6000) 所以排除了 MySQL 的嫌疑(这个推理不够谨严为前面排查埋下了大坑)。
那么接下来的嫌疑在网络、LVS 等中间环节上。
LVS 和网络的嫌疑
首先通过大查问排除了带宽的问题,因为这里都是小包,pps 到了 72 万,很天然想到了网关、LVS 的限流之类的 pps 监控,这台物理机有 4 个 MySQL 实例上,pps 9 万左右,9*32/4=72 万
最终所有网络因素都被排除,外围证据是:做压测的时候重复从 Tomcat 上 ping 前面的 MySQL,RT 跟没有压力的时候一样,也阐明了网络没有问题(请思考这个 ping 的作用)。
问题的确认
尝试在 Tomcat 上关上日志,并将慢 SQL 阈值设置为 100ms,这个时候的确能从日志中看到大量 MySQL 上的慢查问,因为这个 SQL 须要在 Tomcat 上做拆分成 256 个 SQL,同时下发,一旦有一个 SQL 返回慢,整个申请就因为这个短板被连累了。均匀 RT 0.8ms,然而常常有超过 100ms 的话对整体影响还是很大的。
将 Tomcat 记录下来的慢查问(Tomcat 减少了一个惟一 id 下发给 MySQL)到 MySQL 日志中查找,果然发现 MySQL 上的确慢了,所以到这里根本确认是 MySQL 的问题,终于不必再纠结是否是网络问题了。
同时在 Tomcat 进行抓包,对网卡上的 RT 进行统计分析:
上是 Tomcat 上抓到的每个 sql 的物理 RT 平均值,下面是 QPS 430 的时候,RT 0.6ms,上面是 3 个 server,QPS 为 700,然而 RT 回升到了 0.9ms,根本跟 Tomcat 监控记录到的物理 RT 统一。如果 MySQL 上也有相似抓包计算 RT 工夫的话能够疾速排除网络问题。
网络抓包失去的 RT 数据更容易被所有人承受。尝试过在 MySQL 上抓包,然而因为 LVS 模块的起因,进出端口、ip 都被批改过,所以没法剖析一个流的响应工夫。
重心再次转向 MySQL
这个时候因为问题点根本确认,再去查看 MySQL 是否有问题的重心都不一样了,不再只是看看 CPU 和慢查问,这个问题显著更简单一些。
教训:CPU 只是影响性能的一个因素,RT 才是后果,要追着 RT 跑,而不是只看 CPU。
通过监控发现 MySQL CPU 尽管始终不高,然而常常看到 running thread 飙到 100 多,很快又降下去了,看起来像是突发性的并发查问申请太多导致了排队期待,每个 MySQL 实例是 8Core 的 CPU,尝试将 MySQL 实例扩容到 16Core(只是为了验证这个问题),QPS 的确能够回升到 1000(没有达到现实的 1400)。
这是 Tomcat 上监控到的 MySQL 状态:
同时在 MySQL 机器上通过 vmstat 也能够看到这种飙升:
以上剖析能够清晰看到尽管 MySQL 整体压力不大,然而仿佛会偶然来一波卡顿、running 工作飙升。
像这种短暂突发性的并发流量仿佛监控都很难看到(根本都被均匀掉了),只有一些实时性监控偶然会采集到这种短暂突发性飙升,这也导致了一开始漠视了 MySQL。
所以接下来的外围问题就是 MySQL 为什么会有这种飙升、这种飙升的影响到底是什么?
perf top
间接用 perf 看下 MySQLD 过程,发现 ut_delay 高得不合乎逻辑:
开展看一下,根本是在优化器中做索引命中行数的抉择:
跟间接在 MySQL 命令行中通过 show processlist 看到的基本一致:
这是 MySQL 的优化器在对索引进行统计,统计的时候要加锁,thread running 抖动的时候通过 show processlist 看到很多 thread 处于 statistics 状态。也就是高并发下加锁影响了 CPU 压不下来同时 RT 激烈减少。
这里 ut_delay 耗费了 28% 的 CPU 必定太不失常了,于是将 innodb_spin_wait_delay 从 30 改成 6 后性能立刻下来了,持续减少 Tomcat 节点,QPS 也能够线性减少。
耗 CPU 最高的调用函数栈是…mutex_spin_wait->ut_delay,属于锁期待的逻辑。InnoDB 在这里用的是自旋锁,锁期待是通过调用 ut_delay 让 CPU 做空循环在等锁的时候不开释 CPU 从而防止上下文切换,会耗费比拟高的 CPU。
最终的性能
调整参数 innodb_spin_wait_delay=6 后在 4 个 Tomcat 节点下,并发 40 时,QPS 跑到了 1700,物理 RT:0.7,逻辑 RT:19.6,cpu:90%,这个时候只须要持续扩容 Tomcat 节点的数量就能够减少 QPS
再跟调整前比拟一下,innodb_spin_wait_delay=30,并发 40 时,QPS 500+,物理 RT:2.6ms 逻辑 RT:72.1ms cpu:37%
再看看调整前压测的时候的 vmstat 和 tsar –cpu,能够看到 process running 抖动显著
比照批改 delay 后的 process running 就很稳固了,即便 QPS 大了 3 倍
预先思考和剖析
到这里问题失去了完满解决,然而不禁要问为什么?ut_delay 是怎么工作的?和 innodb_spin_wait_delay 以及自旋锁的关系?
原理解析既
然调整 innodb_spin_wait_delay 就能解决这个问题,那就要先剖析一下 innodb_spin_wait_delay 的作用
对于 innodb_spin_wait_delay
innodb 通过大量的自旋锁 (比方 InnoDB mutexes and rw-locks) 来用高 CPU 耗费防止上下文切换,这是自旋锁的正确应用形式,在多核场景下,它们一起自旋抢同一个锁,容易造成 cache ping-pong,进而多个 CPU 核之间会相互使对方缓存局部有效。所以这里 innodb 通过减少 innodb_spin_wait_delay 和 Pause 配合来缓解 cache ping-pong,也就是原本通过 CPU 高速自旋抢锁,换成了抢锁失败后 delay 一下(Pause)然而不开释 CPU,delay 工夫到后持续抢锁,也就是把间断的自旋抢锁转换成了更稠密的点状的抢锁(距离的 delay 是个随机数),这样岂但防止了上下文切换也大大减少了 cache ping-pong。
自旋锁如何缩小了 cache ping-pong
多线程竞争锁的时候,加锁失败的线程会“忙期待”,直到它拿到锁。什么叫“忙期待”呢?它并不意味着始终执行 CAS 函数,而是会与 CPU 紧密配合,它通过 CPU 提供的 PAUSE 指令,缩小循环期待时的 cache ping-pong 和耗电量;对于单核 CPU,忙期待并没有意义,此时它会被动把线程休眠。
X86 PAUSE 指令
X86 设计了 Pause 指令,也就是调用 Pause 指令的代码会抢着 CPU 不开释,然而 CPU 会打个盹,比方 10 个时钟周期,绝对一次上下文切换是大几千个时钟周期。
这样利用一旦自旋抢锁失败能够先 Pause 一下,只是这个 Pause 工夫对于 MySQL 来说还不够久,所以须要减少参数 innodb_spin_wait_delay 来将休息时间放大一些。
在咱们的这个场景下对每个 SQL 的 RT 抖动十分敏感(放大 256 倍),所以过高的 delay 会导致局部 SQL RT 变高。函数 ut_delay(ut_rnd_interval(0, srv_spin_wait_delay)) 用来执行这个 delay:
/***************************MySQL 代码 ****************************//**
Runs an idle loop on CPU. The argument gives the desired delay
in microseconds on 100 MHz Pentium + Visual C++.
@return dummy value */
UNIV_INTERN
ulint
ut_delay(ulint delay) //delay 是 [0,innodb_spin_wait_delay) 之间的一个随机数
{ulint i, j;
UT_LOW_PRIORITY_CPU();
j = 0;
for (i = 0; i < delay * 50; i++) { //delay 放大 50 倍
j += i;
UT_RELAX_CPU(); // 调用 CPU Pause}
UT_RESUME_PRIORITY_CPU();
return(j);
}
innodb_spin_wait_delay 的默认值为 6. spin 期待提早是一个动静全局参数,您能够在 MySQL 选项文件(my.cnf 或 my.ini)中指定该参数,或者在运行时应用 SET GLOBAL 来批改。在咱们的 MySQL 配置中默认改成了 30,导致了这个问题。
CPU 为什么要有 Pause
首先能够看到 Pause 指令的作用:
- 防止上下文切换,应用层想要劳动可能会用 yield、sleep,这两操作对于 CPU 来说太重了(随同上下文切换)
- 能给超线程腾出计算能力(HT 共享核,然而有独自的寄存器等存储单元,CPU Pause 的时候,对应的 HT 能够占用计算资源),比方同一个 core 上先跑多个 Pause,同时再跑 nop 指令,这时 nop 指令的 IPC 根本不受 Pause 的影响
- 节能(CPU 能够劳动、然而不让进去),CPU Pause 的时候你从 top 能看到 CPU 100%,然而不耗能。
所以有了 Pause 指令后可能进步超线程的利用率, 节能,缩小上下文切换进步自旋锁的效率。
The PAUSE instruction is first introduced for Intel Pentium 4 processor to improve the performance of“spin-wait loop”. The PAUSE instruction is typically used with software threads executing on two logical processors located in the same processor core, waiting for a lock to be released. Such short wait loops tend to last between tens and a few hundreds of cycles. When the wait loop is expected to last for thousands of cycles or more, it is preferable to yield to the operating system by calling one of the OS synchronization API functions, such as WaitForSingleObject on Windows OS.An Intel® processor suffers a severe performance penalty when exiting the loop because it detects a possible memory order violation. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation in most situations. The PAUSE instruction can improve the performance of the processors supporting Intel Hyper-Threading Technology when executing“spin-wait loops”. With Pause instruction, processors are able to avoid the memory order violation and pipeline flush, and reduce power consumption through pipeline stall.
从 intel sdm 手册以及理论测试验证来看,Pause 指令在执行过程中,根本不占用流水线执行资源。
Skylake 架构的 8163 和 Broadwell 架构 E5-2682 CPU 型号的不同
为什么用得好好的 innodb_spin_wait_delay 参数这次就不行了呢?
这是因为以前业务始终应用的是 E5-2682 CPU,这次用的是新一代架构的 Skylake 8163,那这两款 CPU 在这里的外围差异是?
在 Intel 64-ia-32-architectures-optimization-manual 手册中提到:
The latency of the PAUSE instruction in prior generation microarchitectures is about 10 cycles, whereas in Skylake microarchitecture it has been extended to as many as 140 cycles.The PAUSE instruction can improves the performance of processors supporting Intel Hyper-Threading Technology when executing“spin-wait loops”and other routines where one thread is accessing a shared lock or semaphore in a tight polling loop. When executing a spin-wait loop, the processor can suffer a severe performance penalty when exiting the loop because it detects a possible memory order violation and flushes the core processor’s pipeline. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation and prevent the pipeline flush. In addition, the PAUSE instruction de-pipelines the spin-wait loop to prevent it from consuming execution resources excessively and consume power needlessly. (See Section 8.10.6.1,“Use the PAUSE Instruction in Spin-Wait Loops,”for more information about using the PAUSE instruction with IA-32 processors supporting Intel Hyper-Threading Technology.)
也就是 Skylake 架构的 CPU 的 PAUSE 指令从之前的 10 cycles 改成了 140 cycles。这可是 14 倍的变动呀。
MySQL 应用 innodb_spin_wait_delay 管制 spin lock 等待时间,等待时间工夫从 050 个 Pause 到 innodb_spin_wait_delay50 个 Pause。以前 innodb_spin_wait_delay 默认配置 30,对于 E5-2682 CPU,期待的最长工夫为:30 50 10=15000 cycles,对于 2.5GHz 的 CPU,等待时间为 6us。对应计算 Skylake CPU 的等待时间:30 50 140=210000 cycles,CPU 主频也是 2.5GHz,等待时间 84us。
E5-2682 CPU 型号在不同的 delay 参数和不同并发压力下的写入性能数据:
Skylake 8163 CPU 型号在不同的 delay 参数和不同并发压力下的写入性能数据:
因为 8163 的 cycles 从 10 改到了 140,所以能够看到 delay 参数对性能的影响更加陡峻。
总结剖析
Intel CPU 架构不同使得 Pause 指令的 CPU Cycles 不同导致了 MySQL innodb_spin_wait_delay 在 spin lock 失败的时候(此时须要 Pause innodb_spin_wait_delayN)delay 更久,使得调用方看到了 MySQL 更大的 RT,进而导致 Tomcat Server 上业务并发跑不起来,所以最终压力上不去。
在长链路的排查中,细化定位是哪个节点出了问题是最难的,要盯住 RT 而不是 CPU。
欲速则不达,做压测的时候还是要老老实实地从一个并发开始察看 QPS、RT,而后始终减少压力到压不下来了,再看 QPS、RT 变动,而后确认瓶颈点。
作者:蛰剑
原文链接
本文为阿里云原创内容,未经容许不得转载。