理解InnoDB rw-lock的统计数据

译者:bzhaoopenstack

原文链接:https://mysqlonarm.github.io/Understanding-InnoDB-rwlock-stats/

作者: Krunal Bauskar

InnoDB使用互斥锁进行独占访问,使用rw-locks进行共享访问。rw-locks用于控制对缓冲池页、表空间、自适应搜索系统、数据字典、informaton_schema等公共共享资源的访问。总之,rw-locks在InnoDB系统中扮演着非常重要的角色,因此跟踪和监视它们也很重要。

InnoDB 提供了一种简单的方式来跟踪它们, “SHOW ENGINE INNODB STATUS”.

1
2
3
4
RW-shared spins 38667, rounds 54868, OS waits 16539
RW-excl spins 6353, rounds 126218, OS waits 3936
RW-sx spins 1896, rounds 43888, OS waits 966
Spin rounds per wait: 1.42 RW-shared, 19.87 RW-excl, 23.15 RW-sx

在本文中,我们将了解这些统计数据是如何计算的,以及每个数据的意义。我们还将尝试使用不同的用例来描述一些推论,并且接触一下基础又重要的统计数据, 这个bug使当前状态的统计几乎无法进行调优。

rw-lock 自旋算法

rw-locks有三种类型:

  • Shared: 提供资源的共享访问。允许多个共享锁。
  • Exclusive: 提供对资源的独占访问。共享锁等待排他锁。
  • Shared-Exclusive (SX): 对读不一致的资源提供写访问(relaxed exclusive)。

首先我们先尝试理解流程,然后讨论一些调优步骤。

(为了便于讨论,假设 spins=0, rounds=0, os-waits=0).

Locking 步骤

  • Step-1:

尝试获取所需的锁

  • If SUCCESS then return immediately. (spins=0, rounds=0, os-waits=0)
  • If FAILURE then enter spin-loop.
  • Step-2: 开始自旋回环(Spin-loop). 增加自旋计数 (spin-count). (为什么需要自旋循环?如果我们的线程进入等待状态,那么操作系统将把CPU从给定的线程中带走,暂时不让它使用CPU,然后线程将不得不按照操作系统调度的次序等待CPU资源,从而进行下面的任务。更好的方法是在繁忙等待中(busy-wait)使用自旋循环(带有条件的检查确认锁是否被释放)以便保留CPU。由于这些锁大多数都是短时间使用,因此重新获得的机会可能性非常高。).
  • Step-3: 开始N轮自旋。Start spinning for N rounds. (这里N的定义由innodb_sync_spin_loops来控制)。默认为30轮。
    • Step-3a: 每一轮将调用一个PAUSE逻辑(见下面关于PAUSE逻辑的单独一节),这将导致CPU进入PAUSE的X个周期。
    • Step-3b: 每轮检查(软实现)是否对应的锁已经可用(busy-wait)。
      • 如果它可用,那么自旋循环退出。(可能还有一些其他同样正在自旋的检查。我们将使用下面的信息)。
    • Step-3c: 再次尝试获取所需的锁。
      • If SUCCESS then return. (spins=1, rounds=M (M <= N), os-waits=0)
      • If FAILURE,并且此时仍有其他正在自旋,且悬而未决任务 (max=innodb_sync_spin_loops)还是继续自旋。(为什么循环被中断,锁失败。注:该锁被多个线程并行查看。而当多个线程试图获取锁时,它们收到了锁可用的信号。被其他线程取走,所以该线程现在仍在重新尝试)。
    • Step-3d: 当这个线程现在完成了它设置的spin-wait轮循次数,到现在它还没有获得锁。那么它会被认为浪费CPU周期,没有必要继续自旋。最好的选择是放弃挂起的CPU周期并交还给操作系统,让操作系统调度做其他有用的事情。此外,由于所述线程现在将要进入睡眠,它应该向一些公共基础设施注册自身,这些基础设施将帮助它在所述锁可用时发出恢复活动的信号。
    • Step-3e: 这个将线程从唤醒的基础设施正式InnoDB中的同步阵列(sync-array)基础设施。
      • 所述线程通过在同步阵列中预留插槽来注册自身。
      • 在开始等待之前,再试一次看锁是否可用。(因为预留可能很费时间,同时锁这个时候是可用的状态)。
      • 如果仍然没有获得锁,则将等待同步阵列向该线程送回信号。
      • 这种等待称为OS-wait,进入这个循环现在会导致OS-waits计数增加。
    • Step-3f: 如果该线程收到由同步阵列发送回的wait-event信号。它会重新尝试获取锁。
      • If SUCCESS then return. (spins=1, rounds=N, os-waits=1)
      • If FAILURE ,则整个循环从旋转逻辑重新启动(返回Step-3,rounds-count重新初始化为0)。注意:自旋计数(spins count)不会重新递增。

所以现在我们来给这些计数赋予意义

  • spins: 代表在第一次尝试中多少轮数而未能得到一个锁,并不得不进入自旋循环。
  • rounds: 表示执行多少轮PAUSE逻辑。
  • os-waits:自旋循环在多少轮自旋时仍未得到锁而导致os-waits。

在获取所述锁流程中的自旋循环期间中,可能需要超过30轮(innodb_sync_spin_loops)PAUSE逻辑,并且还可能多次进入os-waits。这可能会导致os-waits > spins-count。

PAUSE 逻辑

K = {取 (0 - innodb_spin_wait_delay)之间的随机数 * innodb_spin_wait_pause_multiplier}

调用底层 PAUSE 指令 K 次.

并不是所有的架构都提供底层的PAUSE指令。x86有提供,但ARM没有。即使x86深藏了这个PAUSE指令,它也会随着处理器的不同系列而继续变化。老一代处理器的周期约为10-15次(pre-skylake),Skylake系列的周期约为140次,然后CascadeLake系列的周期数又降下来了 (我看到在属于CascadeLake系列的Intel(R) Xeon(R) Gold 6266C CPU @ 3.00GHz芯片上是13次). (除了Cascadelake系列以外,我个人并没有在其他平台上对它进行基准测试) 但这些信息是可供参考的。这意味着延迟引入PAUSE指令(按照周期计算)会持续不断的变化,所以针对每一代/类型的处理器调整PAUSE逻辑是非常重要的。 这里有两个可配置的变量可以解决这个问题,innodb_spin_wait_delay 和 innodb_spin_wait_pause_multiplier。

统计数据解读

现在我们已经了解了统计数据,让我们看看这个数字,并试着做出一些推断。

不过,在谈及进一步的细节之前,让我先看看这个bug, 它描述了导致这些统计数据不一致和不正确的原因和修复方法。

为了得到一个公正的结论,我们将使用mysql对应版本,并打上补丁。 (正如bug中指出的,不使用修复补丁统计数据不能产生正确的数据,因此各种解释和调优都毫无用处).

Use Case 1

1
2
3
4
RW-shared spins 338969, rounds 20447615, OS waits 592941
RW-excl spins 50582, rounds 1502625, OS waits 56124
RW-sx spins 12583, rounds 360973, OS waits 10484
Spin rounds per wait: 60.32 RW-shared, 29.71 RW-excl, 28.69 RW-sx

让我们分析一下共享自旋的情况:

1
RW-shared spins 338969, rounds 20447615, OS waits 592941
  • 在头一次尝试中,用了338K 次仍未获取到锁,迫使线程进去自旋锁状态(spin-lock)。
  • 在每个自旋周期内,执行了60轮PAUSE周期(因此,所述自旋周期执行了2次)。
  • OS-waits/spins = 592/338 = 1.75表明大部分被分流进入了OS-wait(PAUSE的延迟不够)。
  • 表明对于大多数自旋周期,单一的操OS-wait是不够的,因此这种操作是在重复进行的。

Conclusion: 该Use-case是高竞争情况。而且,诸如PAUSE循环无法产生所需的延迟来获得锁,导致每个自旋周期产生如此之多的PAUSE循环。

256 thread oltp-read-write workload on 24 vCPU ARM machine

Use Case 2

1
2
3
4
RW-shared spins 35943, rounds 777178, OS waits 19051
RW-excl spins 4269, rounds 121796, OS waits 4164
RW-sx spins 13407, rounds 321954, OS waits 7346
Spin rounds per wait: 21.62 RW-shared, 28.53 RW-excl, 24.01 RW-sx

让我们分析一下共享自旋的情况:

1
RW-shared spins 35943, rounds 777178, OS waits 19051
  • 流程中,自旋循环35K次。
  • 只有19K次(大约是自旋循环的一半)引起了OS-waits。
  • 平均每个自旋周期也限制在21.62,这表明,对于每个自旋周期,平均有22轮PAUSE循环。

Conclusion: 该Use-case表示中度竞争情况。

16 thread oltp-read-write workload on 24 vCPU ARM machine

Use Case 3

让我举一个常见的例子,以供参考。这是16个线程的oltp-read-write工作负载在基于x86_64的16CPU虚拟机上。

1
2
3
4
RW-shared spins 39578, rounds 424553, OS waits 7329
RW-excl spins 5828, rounds 78225, OS waits 1341
RW-sx spins 11666, rounds 67297, OS waits 449
Spin rounds per wait: 10.73 RW-shared, 13.42 RW-excl, 5.77 RW-sx
  • 流程中自旋循环39K次。
  • 只有7K(约占自旋循环的20%) 导致OS-waits。
  • 每自旋周期平均数也限制为10。

Conclusion: 该Use-case表示低竞争情况。

16 thread oltp-read-write workload on 24 vCPU x86_64 machine

调优注意事项

记得我们在上面看到的高竞争案例。通过优化一些代码,可以显著减少共享自旋的争用。

1
2
3
4
RW-shared spins 318800, rounds 13856732, OS waits 374634
RW-excl spins 35759, rounds 656955, OS waits 22310
RW-sx spins 10750, rounds 226315, OS waits 5598
Spin rounds per wait: 43.47 RW-shared, 18.37 RW-excl, 21.05 RW-sx

每个自旋周期的轮数: 平均数从 60 降到 43

每个自旋周期的OS-wait次数: 从1.75 降到1.17

这性能好起来了吗?不是太好。有许多因素需要考虑。

  • 你看到TPS有改善吗?
  • 有时,它可能会建议简单地增加PAUSE循环。但是,增加PAUSE循环超过某个点将会导致延长自旋周期,最终浪费宝贵的CPU周期,尤其是这会导致线程返回到OS-wait状态。(这种方式可能对HT案例和多核案例更有效)。
  • 同样,如上所述,不同处理器的系列和类型会影响PAUSE循环延迟。

有许多因素需要考虑。甚至我正在研究这个问题,看看我们如何为所有类型的CPU来优化它。一旦我在这个研究中发掘到一些非常好的通用的算法(或者我们可以开发一些自动的、自调整的或自适应的算法),我会发布更多关于这个问题的博客,用户无需担心。

结论

正如我们在上文看到的,rw-locks统计数据可以帮助我们更好地理解系统中锁的争用。当然,它不是有关InnoDB争用的唯一说明,因为互斥锁没在这些统计数据里面。调优可能具有挑战性,因为以错误的方式过度调优也会影响性能。

如果你还有问题/疑问,请告诉我。会试着去回答他们。

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×