MySQL得益于其开源属性、成熟的商业运做、良好的社区运营以及功能的不断迭代与完善,已经成为互联网关系型数据库的标配。能够说,X86服务器、Linux做为基础设施,跟MySQL一块儿构建了互联网数据存储服务的基石,三者相辅相成。本文将分享一个工做中的实践案例:因Intel PAUSE指令周期的迭代,引起了MySQL的性能瓶颈,美团MySQL DBA团队如何基于这三者来一步步进行分析、定位和优化。但愿这些思路能对你们有所启发。mysql
在2017年,Intel发布了新一代的服务器平台Purley,并将Intel Xeon Scalable Processor(至强可扩展处理器)从新划分为:Platinum(铂金)、Gold(金)、Silver(银)、Broze(铜)等四个等级。产品定位和框架也变得更加清晰。sql
因美团线上海量数据交易和存储等后端服务依赖大量高性能服务器的支撑。随着线上部分Grantly平台E系列服务器生命周期的临近,以及产品自己的发展和迭代。从2019年开始,RDS(关系型数据库服务)后端存储(MySQL)开始大量上线Purley平台的Skylake CPU服务器,其中包含Silver 4110等。数据库
Silver 4110相比上一代E5-2620 V4,支持更高的内存频率、更多的内存通道、更大的L2 Cache、更快的总线传输速率等。Intel官方数据显示Silver 4110的性能比上一代E5-2620 V4提高了10%。后端
然而,随着线上Skylake服务器数量的增长,以及愈来愈多的业务接入。美团MySQL DBA团队发现部分MySQL实例性能与预期并不相符,有时甚至出现较大程度的降低。通过持续的性能问题分析,咱们定位到Skylake服务器存在性能瓶颈:centos
接下来,咱们将从Intel CPU、ut_delay函数、PAUSE指令三方面入手,进行剖析定位,并探索相关优化方案。缓存
首先,基于上述两代平台的CPU(Grantly和Purley),经过基准测试,横向对比在不一样OS下的性能表现。性能优化
经过基准测试数据,总结以下:服务器
1.在oltp_write_only(只写)的场景下Purley 4110的性能降低较为明显。
2.同为Purley 4110,CentOS 7比CentOS 6 oltp_write_only(只写)性能有提高。微信
咱们经过二维折线图,来展现性能之间的差别:架构
在上图中,同为Purley 4110,CentOS 7比CentOS 6性能有提高。具体提高缘由,因不涉及本文重点内容,因此不在这里详细展开了。
New MCS-based Locking MechanismRed Hat Enterprise Linux 7.1 introduces a new locking mechanism, MCS locks. This new locking mechanism significantly reduces spinlock overhead in large systems, which makes spinlocks generally more efficient in Red Hat Enterprise Linux 7.1.
红帽官网Release Notes显示,从内核3.10.0-229开始,引入了新的加锁机制,MCS锁。能够下降spinlock的开销,从而更高效地运行。普通spinlock在多CPU Core下,同时只能有一个CPU获取变量,并自旋,而缓存一致性协议为了保证数据的正确,会对全部CPU Cache Line状态、数据,同步、失效等操做,致使性能降低。而MSC锁实现每一个CPU都有本身的“spinlock”本地变量,只在本地自旋。避免Cache Line同步等,从而提高了相关性能。不过,社区对于spinlock的优化争议仍是比较大的,后续又有大牛基于MSC实现了qspinlock,并在4.x的版本上patch了。具体实现能够参看:MCS locks and qspinlocks。
在大体了解CentOS 7性能的迭代后,接下来咱们深刻分析一下Skylake CPU 4110致使性能降低的原因。
具体定位4110性能瓶颈,分以下几步:
能够看到,其中占CPU消耗占比较大为:ut_delay函数。
咱们继续深挖一下函数链调用关系:
# Children Self Command Shared Object Symbol # ........ ........ ....... ................... .................................................................................................................................................................................. # 93.54% 0.00% mysqld libpthread-2.17.so [.] start_thread | ---start_thread | |--77.07%--pfs_spawn_thread | | | --77.05%--handle_connection | | | --76.97%--do_command | | | |--74.30%--dispatch_command | | | | | |--71.16%--mysqld_stmt_execute | | | | | | | --70.74%--Prepared_statement::execute_loop | | | | | | | |--69.53%--Prepared_statement::execute | | | | | | | | | |--67.90%--mysql_execute_command | | | | | | | | | | | |--23.43%--trans_commit_stmt | | | | | | | | | | | | | --23.30%--ha_commit_trans | | | | | | | | | | | | | |--18.86%--MYSQL_BIN_LOG::commit | | | | | | | | | | | | | | | --18.18%--MYSQL_BIN_LOG::ordered_commit | | | | | | | | | | | | | | | |--8.02%--MYSQL_BIN_LOG::change_stage | | | | | | | | | | | | | | | | | |--2.35%--__lll_unlock_wake | | | | | | | | | | | | | | | | | | | --2.24%--system_call_fastpath | | | | | | | | | | | | | | | | | | | --2.24%--sys_futex | | | | | | | | | | | | | | | | | | | --2.23%--do_futex | | | | | | | | | | | | | | | | | | | --2.14%--futex_wake | | | | | | | | | | | | | | | | | | | --1.38%--wake_up_q | | | | | | | | | | | | | | | | | | | --1.33%--try_to_wake_up ...
将上述调用经过火焰图进行直观展现:
如今基本能够肯定,全部的函数调用,最后大部分的消耗都在ut_delay上。
3.2.1 MySQL ut_delay实现
接下来,咱们继续看一下MySQL源码中ut_delay函数的功能:
/*************************************************************//** Runs an idle loop on CPU. The argument gives the desired delay in microseconds on 100 MHz Pentium + Visual C++. @return dummy value */ ulint ut_delay( /*=====*/ ulint delay) /*!< in: delay in microseconds on 100 MHz Pentium */ { ulint i, j; UT_LOW_PRIORITY_CPU(); j = 0; for (i = 0; i < delay * 50; i++) { j += i; UT_RELAX_CPU(); } UT_RESUME_PRIORITY_CPU(); return(j); } ... # define UT_RELAX_CPU() asm ("pause" ) # define UT_RELAX_CPU() __asm__ __volatile__ ("pause")
能够了解到,MySQL自旋会调用PAUSE指令,从而提高spin-wait loop的性能。
3.2.2 PAUSE指令周期的演变
咱们能够看下Intel官网,也描述了在新平台架构PAUSE的改动:
Pause Latency in Skylake MicroarchitectureThe 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, so performance-wise it is better to wait while occupying the CPU than yielding to the OS. 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 an OS synchronization API function, such as WaitForSingleObject on Windows* OS or futex on Linux.
...
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 increased latency (allowing more effective utilization of competitively-shared microarchitectural resources to the logical processor ready to make forward progress) has a small positive performance impact of 1-2% on highly threaded applications. It is expected to have negligible impact on less threaded applications if forward progress is not blocked executing a fixed number of looped PAUSE instructions. There's also a small power benefit in 2-core and 4-core systems.
As the PAUSE latency has been increased significantly, workloads that are sensitive to PAUSE latency will suffer some performance loss.
...
衡量程序执行性能的简化公式:
ExecutionTime(T)=InstructionCount∗TimePerCycle∗CPI
即:程序执行时间 = 程序总指令数 x 每CPU时钟周期时间 x 每指令执行所需平均时钟周期数。
MySQL内部自旋,就是经过固定次数的PAUSE循环实现。可知,PAUSE指令周期的增长,那么执行自旋的时间也会增长,即程序执行的时间也会相对增长,对系统总体的吞吐量就会有影响。
显然,Intel文档已说明不一样平台、不一样架构CPU PAUSE定义的周期是不同的。
下面,咱们经过一个测试用例来大体验证、对比一下新老架构CPU执行PAUSE的cycles:
#include <stdio.h> #define TIMES 5 static inline unsigned long long rdtsc(void) { unsigned long low, high; asm volatile("rdtsc" : "=a" (low), "=d" (high) ); return ((low) | (high) << 32); } void pause_test() { int i = 0; for (i = 0; i < TIMES; i++) { asm( "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n"\ "pause\n" :: :); } } unsigned long pause_cycle() { unsigned long start, finish, elapsed; start = rdtsc(); pause_test(); finish = rdtsc(); elapsed = finish - start; printf("Pause的cycles约为:%ld\n", elapsed / 100); return 0; } int main() { pause_cycle(); return 0; }
其运行结果统计以下:
3.2.3 Intel 提高PAUSE猜测
Intel提升PAUSE指令周期的缘由,推测多是减小自旋锁冲突的几率,以及下降功耗;但反而致使PAUSE执行时间变长,下降了总体的吞吐量。
The increased latency (allowing more effective utilization of competitively-shared microarchitectural resources to the logical processor read to make forward progress) has a small positive performance impact of 1-2% on highly threaded applications. It is expected to have negligible impact on less threaded applications if forward progress is not blocked executing a fixed number of looped PAUSE instructions.
接下来,咱们深刻分析一下PAUSE指令致使MySQL写瓶颈的缘由。
首先,经过MySQL 内部统计信息,查看一下InnoDB信号量监控数据:
SEMAPHORES ---------- OS WAIT ARRAY INFO: reservation count 153720 --Thread 139868617205504 has waited at row0row.cc line 1075 for 0.00 seconds the semaphore: X-lock on RW-latch at 0x7f4298084250 created in file buf0buf.cc line 1425 a writer (thread id 139869284108032) has reserved it in mode SX number of readers 0, waiters flag 1, lock_word: 10000000 Last time read locked in file not yet reserved line 0 Last time write locked in file /mnt/workspace/percona-server-5.7-redhat-binary-rocks-new/label_exp/min-centos-7-x64/test/rpmbuild/BUILD/percona-server-5.7.26-29/percona-server-5.7.26-29/storage/innobase/buf/buf0flu.cc line 1216 OS WAIT ARRAY INFO: signal count 441329 RW-shared spins 0, rounds 1498677, OS waits 111991 RW-excl spins 0, rounds 717200, OS waits 9012 RW-sx spins 47596, rounds 366136, OS waits 4100 Spin rounds per wait: 1498677.00 RW-shared, 717200.00 RW-excl, 7.69 RW-sx
可见写操做并阻塞在:storage/innobase/buf/buf0flu.cc第1216行调用上。
跟踪一下发生等待的源码:buf0flu.cc line 1216:
if (flush_type == BUF_FLUSH_LIST && is_uncompressed && !rw_lock_sx_lock_nowait(rw_lock, BUF_IO_WRITE)) { // 加锁前,判断锁冲突 if (!fsp_is_system_temporary(bpage->id.space())) { /* avoiding deadlock possibility involves doublewrite buffer, should flush it, because it might hold the another block->lock. */ buf_dblwr_flush_buffered_writes( buf_parallel_dblwr_partition(bpage, flush_type)); } else { buf_dblwr_sync_datafiles(); } rw_lock_sx_lock_gen(rw_lock, BUF_IO_WRITE); // 加sx锁 } ... #define rw_lock_sx_lock_nowait(M, P) \ rw_lock_sx_lock_low((M), (P), __FILE__, __LINE__) ... rw_lock_sx_lock_func( // 加sx锁函数 /*=================*/ rw_lock_t* lock, /*!< in: pointer to rw-lock */ ulint pass, /*!< in: pass value; != 0, if the lock will be passed to another thread to unlock */ const char* file_name,/*!< in: file name where lock requested */ ulint line) /*!< in: line where requested */ { ulint i = 0; sync_array_t* sync_arr; ulint spin_count = 0; uint64_t count_os_wait = 0; ulint spin_wait_count = 0; ut_ad(rw_lock_validate(lock)); ut_ad(!rw_lock_own(lock, RW_LOCK_S)); lock_loop: if (rw_lock_sx_lock_low(lock, pass, file_name, line)) { if (count_os_wait > 0) { lock->count_os_wait += static_cast<uint32_t>(count_os_wait); rw_lock_stats.rw_sx_os_wait_count.add(count_os_wait); } rw_lock_stats.rw_sx_spin_round_count.add(spin_count); rw_lock_stats.rw_sx_spin_wait_count.add(spin_wait_count); /* Locking succeeded */ return; } else { ++spin_wait_count; /* Spin waiting for the lock_word to become free */ os_rmb; while (i < srv_n_spin_wait_rounds && lock->lock_word <= X_LOCK_HALF_DECR) { if (srv_spin_wait_delay) { ut_delay(ut_rnd_interval( 0, srv_spin_wait_delay)); // 加锁失败,调用ut_delay } i++; } spin_count += i; if (i >= srv_n_spin_wait_rounds) { os_thread_yield(); } else { goto lock_loop; } ... ulong srv_n_spin_wait_rounds = 30; ulong srv_spin_wait_delay = 6;
上述源码可知,MySQL锁等待是经过调用ut_delay作空循环实现的。
InnoDB层有三种锁:S(共享锁)、X(排他锁)和SX(共享排他锁)。 SX与SX、X是互斥锁。加SX不会影响读,只会阻塞写。因此在大量写入操做时,会形成大量的锁等待,即大量的PAUSE指令。
分析到这里,咱们总结一下影响吞吐量的两个因素:
接下来,咱们就从这两方面入手,评估优化空间以及效果。
4.1.1 MySQL 5.7 spin参数优化
咱们能够基于现有MySQL版本、硬件等方面,来寻找优化点。
MySQL针对spin控制这块有个参数能够调整,根据参数特色进行相关优化:
innodb_spin_wait_delay
innodb_spin_wait_delay的单位,是100MHZ的奔腾处理器处理1毫秒的时间,默认innodb_spin_wait_delay配置成6,表示最多在100MHZ的奔腾处理器上自旋6毫秒。
innodb_sync_spin_loops
当 innodb 线程获取 mutex 资源而得不到知足时,会最多进行 innodb_sync_spin_loops次尝试获取mutex资源。
其中innodb_spin_wait_delay参数对PAUSE运行时长是有影响的。针对此参数,咱们进行调优测试。
一样,针对上述参数优化,咱们经过基准测试来对比性能和效果:
能够总结为:
4.2.1 spin_wait_pause_multiplier移植
针对Skylake CPU,PAUSE形成的吞吐量降低,咱们对MySQL 5.7 spin控制参数innodb_spin_wait_delay的调优并未取得明显效果。
因而,咱们将目光投向了MySQL 8.0的新特性:MySQL 8.0 针对PAUSE,源码中新增了spin_wait_pause_multiplier参数,来替换以前写死的循环次数。
4.2.2 spin_wait_pause_multiplier实现
MySQL 8.0源码中,以前循环50次的逻辑修改为了能够调整循环次数的参数:spin_wait_pause_multiplier。
ulint ut_delay(ulint delay) { ulint i, j; /* We don't expect overflow here, as ut::spin_wait_pause_multiplier is limited to 100, and values of delay are not larger than @@innodb_spin_wait_delay which is limited by 1 000. Anyway, in case an overflow happened, the program would still work (as iterations is unsigned). */ const ulint iterations = delay * ut::spin_wait_pause_multiplier; UT_LOW_PRIORITY_CPU(); j = 0; for (i = 0; i < iterations; i++) { j += i; UT_RELAX_CPU(); } UT_RESUME_PRIORITY_CPU(); return (j); } ... namespace ut { ulong spin_wait_pause_multiplier = 50; }
4.2.3 移植spin_wait_pause_multiplier patch优化
既然MySQL 8.0参数spin_wait_pause_multiplier能够控制PAUSE执行的时长,那么就能够减小该值,从而下降总体PAUSE影响。
了解MySQL 8.0相关代码后,咱们将该patch移植到线上的稳定版本:
MySQ >select version(); +------------------+ | version() | +------------------+ | 5.7.26-29-mt-log | +------------------+ 1 row in set (0.00 sec) MySQL>show global variables like '%spin%'; +-----------------------------------+-------+ | Variable_name | Value | +-----------------------------------+-------+ | innodb_spin_wait_delay | 6 | | innodb_spin_wait_pause_multiplier | 5 | | innodb_sync_spin_loops | 30 | +-----------------------------------+-------+ 3 rows in set (0.00 sec)
由上述可知,Silver 4110的PAUSE cycles是E5-2620 v4的14倍左右。基于此,将innodb_spin_wait_pause_multiplier值调整为默认值的1/14,取稍大值:5。即将该参数由原默认的50调整为5。
最后,仍是经过二维折线图来对比该patch调优后的基准测试数据:
上述章节中,咱们测出Cascadelake CPU PAUSE周期降低了。在跟Intel技术专家确认后得知:从Purley的第二代产品Cascadelake开始,Intel将PAUSE的指令周期下降到了44。(估计Intel也发现了第一代增长PAUSE周期后的性能瓶颈问题。)
咱们针对第二代CPU产品继续作基准测试,来看一下性能表现:
接着用perf diff来对比一下4110和4210在ut_delay上的开销:
最后针对本篇内容,咱们能够作个简单的总结:
Intel在新平台CPU产品调大了PAUSE指令周期,在高并发spinlock竞争激烈场景下,可能会形成程序性能较大损耗(特别是执行固定PAUSE次数的程序)。
针对Skylake架构CPU(好比:4110等)PAUSE指令周期较长引发性能问题的优化方法以下:
将MySQL 8.0 innodb_spin_wait_pause_multiplier patch移植到线上稳定版本(或升级到MySQL 8.0),经过下降PAUSE执行时长,来提高吞吐量。
若是是OS为CentOS 6,能够升级到CentOS 7,CentOS 7自己spinlock优化,对MySQL性能也有必定提高。
最简单、直接的方法能够替换为Cascadelake架构CPU。
针对Cascadelake架构CPU,因为Intel自己在PAUSE周期已经优化,性能上已经作了修复。固然也能够采用上述优化方案,让性能提高一个台阶。
春林,2017年加入美团,主要负责MySQL运维开发和优化工做。
美团DBA团队招聘各种人才,Base北京、上海都可。咱们致力于为公司提供稳定、可靠、高效的在线存储服务,打造业界领先的数据库团队。这里有数万各种架构的MySQL实例,天天提供万亿级的OLTP访问请求。真正的海量、分布式、高并发环境。欢迎感兴趣的同窗发送简历至:tech@meituan.com(邮件标题注明:美团DBA团队)
阅读更多技术文章,请扫码关注微信公众号-美团技术团队!