深入理解MySQL中的CPU自旋锁及其调优实践

科技   2024-09-12 10:30   上海  

一 背景

前一段时间针对 MySQL 使用 TPC-C 导入10000仓的数据,查看数据库性能指标发现 TPS  3-4w/s (不符合预期),伴随 CPU  idle 特别比较高, sys CPU 比较低,CPU 在空跑。于是乎做了基本的诊断 :os系统调用栈 , MySQL 系统参数 。使用 perf top 工具观察 系统函数调用情况,  ut_delay比较突出。

接着调整 MySQL 的 spin_lock 相关的参数,效果如下,insert 性能提升2倍

二 CPU自旋锁的工作机制

什么是 自旋锁

自旋锁(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中使用 Spin Lock 的场景

在 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")

MySQL 自旋锁的参数

优化自旋锁的目的是降低自旋等待对CPU资源的消耗,同时确保系统能够及时响应。MySQL提供了一些系统变量来帮助调整自旋等待的行为。

innodb_spin_wait_delay:  该参数决定线程在每次自旋迭代后等待的时间。增加这个值能增加获取锁的平均时间,同时能会降低CPU的使用率,减少线程上下文切换。在CPU高度争用的环境下,比如高并发写入时,适当增大这个参数可能有助于性能提升。

innodb_sync_spin_loops: 该参数控制自旋等待循环的迭代次数。在高并发的系统中,减少此参数的值有助于线程更快地放弃自旋,从而减少 CPU 的使用。但是,这也意味着线程可能会更频繁地进入休眠状态。

注意:

优化没有银弹。。

如果spinlock相关参数设置的不合理,那么就会出现 ut_delay 休眠的时间过长引起性能问题的情况出现,因为不同的CPU执行pause的时候,暂停的CPU周期数量并不相同,有的是3、40个周期,有的是100个周期。如果生产库上需要调整 这两个参数, 请务必在测试的时候 调大或者调小结合 perf top 命令观察相关函数的调用情况。

参考阅读

  1. https://www.cnblogs.com/chenpingzhao/p/5043746.html
  2. https://www.modb.pro/db/1680772051538878464
  3. 高并发数据库中的spinlock优化 
  4. Intel PAUSE指令变化影响到MySQL的性能,该如何解决?
  5. InnoDB mutex 实现变化

                                           图片来自 通义万相

MySQL数据库联盟
关注后,回复“高可用”,可获取8篇MySQL高可用文章
 最新文章