背景介绍

在MySQL的开发过程中为了验证某个需要多线程之间配合的功能时,就需要有一种机制使开发人员能够控制每个线程的执行流程,完成多个线程之间的配合,验证特殊并发逻辑下代码处理的正确性。MySQL 提供了DEBUG_SYNC 功能,就是让开发者可以在MySQL服务器代码中通过DEBUG_SYNC宏定义同步点的。你可以在代码中加入你希望定义的同步点。

使用方式

DEBUG_SYNC的功能,默认是关闭的。除非在启动的时候指定了–debug-sync-timeout[=N] 选项,N是可选的,可以指定也可以不指定。不指定的话,默认是300秒。

  1. #define DEBUG_SYNC_DEFAULT_WAIT_TIMEOUT 300

这个选项是启动时变量,为了能在测试中使用DEBUG_SYNC功能,必须在启动的时候指定–debug-sync-timeout[=N] 选项。这个参数有两个作用: 1) 其一是指定wait_for一个同步点的最大等待时间(单位:秒),若超过这个时间就会timeout; 2) 另一个是打开/关闭DEBUG_SYNC功能的选项,当其后的参数N为0时,就关闭了DEBUG_SYNC功能。

代码片段解析

其核心代码主要通过定义的宏DEBUG_SYNC做为入口,其定义如下:

  1. #define DEBUG_SYNC(_thd_, _sync_point_name_) \
  2. do { \
  3. if (unlikely(opt_debug_sync_timeout)) \
  4. debug_sync(_thd_, STRING_WITH_LEN(_sync_point_name_)); \
  5. } while (0)

这个宏的源码实现,主要是通过定义一个同步点(这个同步点是通过这里定义的名字来表示的_sync_point_name_),线程就会在这个同步点执行定义的行为动作,比如是在这个同步点发信号给等在其他同步点的线程、还是等在某个定义的事件上。在DEBUG_SYNC目前同步点的行为只定义了给其它同步点发信号、和等在某个信号上。其实现的主要数据结构如下:

  1. struct st_debug_sync_action {
  2. ulong activation_count = 0; /* max(hit_limit, execute) */
  3. ulong hit_limit = 0; /* hits before kill query */
  4. ulong execute = 0; /* executes before self-clear */
  5. ulong timeout = 0; /* wait_for timeout */
  6. String signal; /* signal to emit */
  7. String wait_for; /* signal to wait for */
  8. String sync_point; /* sync point name */
  9. bool need_sort = false; /* if new action, array needs sort */
  10. bool clear_event = false; /* do not clear signal if false */
  11. };

而其功能的实现主要就是通过debug_sync_execute函数来实现的。以下是在debug_sync中调用debug_sync_find和debug_sync_execute的代码片段。

  1. if (ds_control->ds_active &&
  2. (action = debug_sync_find(ds_control->ds_action, ds_control->ds_active,
  3. sync_point_name, name_len)) &&
  4. action->activation_count) {
  5. /* Sync point is active (action exists). */
  6. debug_sync_execute(thd, action);
  7. /* Statistics. */
  8. ds_control->dsp_executed++;
  9. /* If action became inactive, remove it to shrink the search array. */
  10. if (!action->activation_count) debug_sync_remove_action(ds_control, action);
  11. }

首先在debug_sync_find里通过二分查找是否有同步点的要执行的行为动作,若是找到的话,就通过debug_sync_execute函数去执行。在debug_sync_execute根据定义的同步点执行次数,去判断是否达到了执行的次数,若没有达到执行的次数,则会在每次都会等这个event的信号。

  1. if (action->execute) {
  2. action->execute--;
  3. /** 如果本线程也需要等待某个信号,它首先把自己在processlist表里的状态设置成等待状态,为了能让其它线程能及时的看到*/
  4. if (action->wait_for.length()) {
  5. debug_sync_thd_proc_info(thd, ds_control->ds_proc_info);
  6. }
  7. /* 如果定义了需要唤醒的同步点,就需要把这些唤醒的同步点设置成signaled,加入到全局变量中,然后唤醒其它等待线程*/
  8. if (action->signal.length()) {
  9. /**把这些唤醒的同步点设置成signaled,加入到全局变量中*/
  10. if (!s.empty()) add_signal_event(&s);
  11. /* 唤醒等待同步点读线程*/
  12. mysql_cond_broadcast(&debug_sync_global.ds_cond);
  13. }
  14. /* 然后自己再等待在自己定义的事件上,等待被唤醒*/
  15. if (action->wait_for.length()) {
  16. while (!is_signalled(&wait_for) && !thd->killed &&
  17. opt_debug_sync_timeout) {
  18. error = mysql_cond_timedwait(&debug_sync_global.ds_cond,
  19. &debug_sync_global.ds_mutex, &abstime);
  20. }
  21. /* 如果定义了 CLAER行为则清除等待事件,以后再执行到此不必再等待该事件 */
  22. if (action->clear_event) clear_signal_event(&wait_for);
  23. }
  24. /* 如果定义了 HIT_LIMIT行为,则达到了指定的次数,会返回错误消息,并kill这个线程 */
  25. if (action->hit_limit) {
  26. if (!--action->hit_limit) {
  27. thd->killed = THD::KILL_QUERY;
  28. my_error(ER_DEBUG_SYNC_HIT_LIMIT, MYF(0));
  29. }
  30. 。。。
  31. }
  32. }

用法简介

在源代码中定义一个同步点

在源码中使用的例子如下所示,开发者可以在任意的位置加入同步点,并给同步点命名,这样这个同步点就可以在接下来的测试案例中使用了。

  1. open_tables(...)
  2. DEBUG_SYNC(thd, "after_open_tables");
  3. lock_tables(...)

在测试场景中使用同步点

测试场景使用的语法,可以参考 https://dev.mysql.com/doc/internals/en/syntax-debug-sync-values.html。在测试场景中,同步点的使用主要有以下几种情况: 1)SET DEBUG_SYNC=‘sync point name SIGNAL signal name WAIT_FOR signal name 是最常用的方法。 比如: SET DEBUG_SYNC= ‘after_open_tables SIGNAL opened WAIT_FOR flushed’; 大部分情况下同步点都是未激活状态,当对整个同步点请求某个行为时就激活了这个同步点。比如上面这个例子,当执行到同步点after_open_tables后会向等待opened事件发送信号同时等在flushed时间上时,就激活了after_open_tables同步点。

2)SET DEBUG_SYNC= ‘after_open_tables SIGNAL a,b,c WAIT_FOR flushed’; 这中用法和1)的主要区别就是一次唤醒多个事件a、b、c,其它和1)相同

3)SET DEBUG_SYNC= ‘WAIT_FOR flushed NO_CLEAR_EVENT’; 默认情况下, 当等待线程收到唤醒的信号后,就会从全局信号中把这个信号清除。但如果等待这个信号的线程有多个的时候,就不能其中一个线程被唤醒后马上清除它,这样就需要在等待线程,在等待信号上指定NO_CLEAR_EVENT。直到所有的等待线程都唤醒了,然后再通过SET DEBUG_SYNC= ‘RESET’; 去清除这个event的唤醒信号。

4)SET DEBUG_SYNC= ‘name SIGNAL sig EXECUTE 3’; 一般情况下,等待线程被激活执行完后,马上就清除唤醒等待线程的信号。为了不立马清除激活信号,我们可以通过关键字EXECUTE指定执行的次数,执行完指定的次数后,才清除激活信号。比如这个例子指定了执行3次、每执行完一次这个数字就会减1,直到减到0为止。

5) SET DEBUG_SYNC= ‘name WAIT_FOR sig TIMEOUT 10 EXECUTE 2’; 在MySQL启动的时候,可以通过参数debug-sync-timeout指定一个等待事件的超时时间,也可以通过TIMEOUT 关键字为每个等待事件单独指定超时时间。这个例子就是等待线程最长等待10秒,若超过10秒还没收到唤醒等待事件的信号,就会超时不再等待了。

6)SET DEBUG_SYNC= ‘name SIGNAL sig EXECUTE 2 HIT_LIMIT 3’; 如果你想在执行完指定的次数后,返回一个错误消息并且中断这个线程的话,可以通过HIT_LIMIT来指定。这个例子中就是在执行完3次后,会返回一个错误消息并且中断这个查询。

7)SET DEBUG_SYNC= ‘name CLEAR’; 这个是可以在任何时候都清除name指定的同步点,不管它执行了还是没执行。

参考

https://dev.mysql.com/doc/internals/en/debug-sync-facility.html