一 背景
前一段时间针对 MySQL 使用 TPC-C 导入10000仓的数据,查看数据库性能指标发现 TPS 3-4w/s (不符合预期),伴随 CPU idle 特别比较高, sys CPU 比较低,CPU 在空跑。于是乎做了基本的诊断 :os系统调用栈 , MySQL 系统参数 。使用 perf top 工具观察 系统函数调用情况, ut_delay比较突出。
接着调整 MySQL 的 spin_lock
相关的参数,效果如下,insert 性能提升2倍
自旋锁(spin lock)是一种在多线程环境中用于同步的机制,它允许线程在尝试获取一个资源时,如果资源暂时不可用,线程不会进入睡眠状态,而是在一个循环中不断尝试获取资源,直到成功为止。这种方式被称为“自旋”,
线程A 自旋锁 线程B
| |----尝试获取---->|
| |<---已经锁定-----|
|------自旋等待---->|
|<----保持自旋------| |
| |----释放锁------>|
|----获取锁---------| |
在上图中,线程B首先获取了自旋锁。当线程A也尝试获取这个锁时,由于锁已经被占用,线程A不会进入休眠,而是在当前位置不断检查锁是否可用,即“自旋”。一旦线程B释放了锁,线程B便能够立即获取到锁并继续执行。
其实对比自旋锁机制,还有另外一种控制 资源抢占的方法--- 互斥锁(mutex)。比如进程A 抢不到锁,不能访问内存资源,就需要在 用户态,内核态 ,CPU上下文切换调度,增加 CPU 消耗。
自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。
需要注意的是因为线程在等待获取锁的过程中会占用CPU资源进行无效的工作。如果锁被持有的时间较长,则自旋锁可能会浪费大量CPU资源,导致系统性能下降。因此自旋锁适用于 锁的持有时间非常短的情况
下面引用的文章(https://zhuanlan.zhihu.com/p/88427657)说明好处如下:
spinlock优点:没有昂贵的系统调用,一直处于用户态,执行速度快
spinlock缺点:一直占用cpu,而且在执行过程中还会锁bus总线,锁总线时其他处理器不能使用总线
mutex优点:不会忙等,得不到锁会sleep
mutex缺点:sleep时会陷入到内核态,需要昂贵的系统调用
而我们Innodb中大量使用的是先进行spin,如果spin一定次数不能获得,则转入mutex等待(或者sleep),放弃CPU,本处也是如此。
在 MySQL 系统设计中,特别是 InnoDB 存储引擎使用自旋锁来控制对其内部数据结构的访问,以实现高性能和并发。InnoDB存储引擎具有复杂的并发控制机制,自旋锁在其中扮演了重要角色。
MySQL关于spin lock的部分代码。如下代码可以看到MySQL默认作了30次(innodb_sync_spin_loops=30)mutex检查后,才放弃占用CPU资源。
{
/* 在尝试获得锁的过程中旋转,直到`lock_word`变成空闲状态 */
os_rmb;
while (i < srv_n_spin_wait_rounds && lock->lock_word <= X_LOCK_HALF_DECR) {
if (srv_spin_wait_delay) {
// 如果获取锁失败,则调用`ut_delay`来引入随机延迟
ut_delay(ut_rnd_interval(0, srv_spin_wait_delay));
}
i++;
}
spin_count += i;
if (i >= srv_n_spin_wait_rounds) {
// 如果达到旋转等待次数的上限后,主动让出当前占用的CPU时间片
os_thread_yield();
} else {
// 否则,返回到锁等待循环,再次尝试获取锁
goto lock_loop;
// 注意这里的`goto lock_loop;`实际上是永远不会执行的,因为它后面紧跟着的是`os_thread_yield();`
os_thread_yield();
// 主动让出当前占用的CPU时间片
}
...
ulong srv_n_spin_wait_rounds = 30; // 自旋等待循环的次数,即尝试获取锁的最大自旋次数
ulong srv_spin_wait_delay = 6; // 自旋等待之间的延迟时间
其中 ulong srv_n_spin_wait_rounds
的值由 参数innodb_sync_spin_loops
决定,ulong srv_spin_wait_delay
的值由 innodb_spin_wait_delay
ut_delay是Mysql中轻量级锁、读写锁做自旋时,用于产生一个pause暂时让出CPU,避免SPINLOCK引擎严重的CPU性能问题的一个公共函数。每次ut_delay
默认执行pause指令300次( innodb_spin_wait_delay=6*50)
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")
优化自旋锁的目的是降低自旋等待对CPU资源的消耗,同时确保系统能够及时响应。MySQL提供了一些系统变量来帮助调整自旋等待的行为。
innodb_spin_wait_delay
: 该参数决定线程在每次自旋迭代后等待的时间。增加这个值能增加获取锁的平均时间,同时能会降低CPU的使用率,减少线程上下文切换。在CPU高度争用的环境下,比如高并发写入时,适当增大这个参数可能有助于性能提升。
innodb_sync_spin_loops
: 该参数控制自旋等待循环的迭代次数。在高并发的系统中,减少此参数的值有助于线程更快地放弃自旋,从而减少 CPU 的使用。但是,这也意味着线程可能会更频繁地进入休眠状态。
注意:
优化没有银弹。。
如果spinlock相关参数设置的不合理,那么就会出现 ut_delay
休眠的时间过长引起性能问题的情况出现,因为不同的CPU执行pause的时候,暂停的CPU周期数量并不相同,有的是3、40个周期,有的是100个周期。如果生产库上需要调整 这两个参数, 请务必在测试的时候 调大或者调小结合 perf top 命令观察相关函数的调用情况。