【MySQL 】InnoDB 并发控制

一 前言
   本文先从一条报警说起 Warning: xxxx_thread_running db.status.3306_Threads_running:21 template:yz_db_template
相信很多DBA在运维MySQL的过程中都收到过类似的报警,数据库活跃会话数超过某个阈值,平时执行时间毫秒级别的sql 却执行了秒级别。这样的问题带来的极端情况是 sql执行慢(锁竞争、sql本身慢)--->数据库会话堆积,thread running 飙高--->应用请求处理慢,连接池满--->应用访问请求积压,拖累本身和调用方 ---> 系统雪崩。问题来了为什么sql慢会导致thread running飙高?进而导致其他会话受到影响?且看下文分解。

二 知其然,知其所以然
MySQL并发控制实现逻辑 
1 介绍线程读写数据 进入innodb的工作原理 ,代码。
2 thread_running 的意义。
3 解决方法

server层

InnoDB层
图片介绍介绍  innodb_thread_concurrency 

innodb_thread_concurrency  :并发线程数量
innodb_thread_sleep_delay
innodb_concurrency_tickets
innodb用自己的线程调度机制来控制SQL请求线程如何进入innodb内核工作,并执行相关的操作。innodb_thread_concurrency变量就控制了进入内核的线程数量。0表示不限制进入内核的数量。如果innodb内核中已经有很多线程进入,达到一定数量后就不能再进入了。innodb使用了一种两阶段的方式控制线程进入内核,这终机制减少了操作系统因为线程之间的上下文切换带来的开销。

2阶段如下
线程首先睡眠innodb _thread_sleep_delay所规定的时间(微妙),然后再次尝试进入,如果还是不能进入,就会就进入一个等待队列并且把控制权限交给操作系统。

一旦线程进入了内核,它就会得到一个确定的数据作为凭证,它再次进入内核时因为凭证就会减少一些操作,innodb_concurrency_tickets变量就是控制了凭据的数量,凭据是对线程授权。
同时 innodb_commit_concurrency也控制了多线程并发提交的数量。如果 innodb_thread_concurrency  设置的有点大innodb_commit_concurrency应该做出相应的调整,否则会造成大量线程阻塞。

======
1. innodb_thread_concurrency
innodb有一系列的计数器来统计和控制内部的工作线程。其中最重要的一个是innodb_thread_concurrency,和它相关的innodb_thread_sleep_delay 和innodb_concurrency_tickets。
由于MySQL是插件式db,读取行的时候可以有很多方式,比如说顺序读or随机读,而DML(insert,delete,update)语句是要判断是否已经进入到了innodb线程里,如果超过了 innodb_thread_concurrency的值,首先要等innodb_thread_sleep_delay ms后尝试再次进入工作线程,如果失败,则会进入到FIFO队列等待唤醒。这里要提下为什么需要两次尝试?因为需要减少等待线程的个数和上下文切换的次数。


如果一个线程能够进到innodb层,则会发放一个innodb_concurrency_tickets 票,下次的时候如果在有效期内,则不会检查tickets,代码很简单,在 srv_conc_enter_innodb function in innobase/srv/srv0srv.c里:

  1. if (thread->n_tickets_to_enter_innodb > 0)
  2.   {
  3.     thread->n_tickets_to_enter_innodb--;
  4.     ENTER;
  5.   }
  6.  
  7. retry:
  8.   if (entered_thread < innodb_thread_concurrency)
  9.   {
  10.     entered_threads++;
  11.     thread->n_tickets_to_enter_innodb = innodb_concurrency_tickets;
  12.     ENTER;
  13.   }
  14.  
  15.   if (innodb_thread_sleep_delay > 0)
  16.   {
  17.     thread_sleep(innodb_thread_sleep_delay);
  18.   }
  19.  
  20.   goto retry; // (only once)
  21.  
  22.   WAIT_IN_FIFO_QUEUE;
  23.   thread->n_tickets_to_enter_innodb = innodb_concurrency_tickets;
  24.   ENTER
=====
对于INNODB并发线程数的见解。。
三个参数
innodb_thread_concurrency INNODB存储引擎中允许的最大的线程并发数。
innodb_thread_sleep_delay  单位为毫秒;thread未能进入INNODB存储引擎后,需要等待innodb_thread_sleep_delay毫秒再次尝试进入。
innodb_concurrency_tickets  thread进入INNODB中,会获得innodb_concurrency_tickets次数通行证,该线程在接下来的innodb_concurrency_tickets次进入到INNODB中不需要再进行检查,可直接进入。
三个参数关系
当一个thread需要进入到INNODB存储引擎层(以下简称INNODB),INNODB会检查已经进入到INNODB存储引擎层的thread总数是否超过 innodb_thread_concurrency ,如果超过了,那么该thread需要等待 innodb_thread_sleep_delay (单位:毫秒)毫秒再次进行尝试,如果这次尝试失败后,该thread将会进入到FIFO的队列中进行等待唤醒(此时状态为sleeping)。一旦该thread进入到INNODB中,该thread将会获得 innodb_concurrency_tickets 的通行证,即该thread将会在接下来的innodb_concurrency_tickets次进入到INNODB中都不需要再进行检查,可直接进入。
注意
thread尝试两次进入INNODB存储引擎层的目的是,减少等待线程的数量以及减少上下文切换
=====
innodb_thread_concurrency算法介绍:
1、server层到innodb层读写数据是一条一条记录进行的,每次读写都会进/出一次InnoDB层(row_search_for_mysql),进入InnoDB层的时候会检查当前并发线程数目,当超过innodb_thread_concurrency时,线程将尝试spin和sleep并再次检查,如果并发数还是超过innodb_thread_concurrency,线程将进入到一个FIFO中等待被唤醒,读写记录结束后退出InnoDB层时会将当前并发线程数减1,并检查其是否低于innodb_thread_concurrency,如果是的话,从FIFO中唤醒一个等待的线程,保证并发线程不会超过innodb_thread_concurrency参数。
2、当线程进入InnoDB层后,但在获取数据时由于锁请求无法得到满足而需要挂起时,线程将强制退出InnoDB层,当锁请求满足后,线程继续运行并强制进入到InnoDB层,这会导致实际并发线程数不是严格控制在innodb_thread_concurrency之内
 
代码调用逻辑
主要函数:
innodb_srv_conc_enter_innodb
innodb_srv_conc_exit_innodb
srv_conc_force_enter_innodb
srv_conc_force_exit_innodb
 
调用逻辑
进入/退出InnoDB层
  1. ha_innobase::index_read
  2. ha_innobase::general_fetch
  3. row_check_index_for_mysql
  4.                 ...
  5.                 innodb_srv_conc_enter_innodb(prebuilt->trx);
  6.  
  7.                 ret = row_search_for_mysql((byte*) buf, mode, prebuilt, match_mode, 0);
  8.  
  9.                 innodb_srv_conc_exit_innodb(prebuilt->trx);
  10.                 ...
锁等待逻辑:
  1. srv_suspend_mysql_thread
  2.                 ...
  3.                 if (trx->declared_to_be_inside_innodb) {
  4.  
  5.                                 was_declared_inside_innodb = TRUE;
  6.  
  7.                                 /* We must declare this OS thread to exit InnoDB, since a
  8.                                 possible other thread holding a lock which this thread waits
  9.                                 for must be allowed to enter, sooner or later */
  10.                                 srv_conc_force_exit_innodb(trx);
  11.                 }
  12.  
  13.                 /* Suspend this thread and wait for the event. */
  14.  
  15.                 thd_wait_begin(trx->mysql_thd, THD_WAIT_ROW_LOCK);
  16.                 os_event_wait(event);
  17.                 thd_wait_end(trx->mysql_thd);
  18.  
  19.                 if (was_declared_inside_innodb) {
  20.  
  21.                                 /* Return back inside InnoDB */
  22.                                 srv_conc_force_enter_innodb(trx);
  23.                 }

5.6的改进
从5.6开始,默认编译会使用GCC atomic,可避免热点锁srv_conc_mutex的频繁加锁/释放。
Pre-5.5如果超过并发限制时,就给其线程分配一个slot,让其进入信号量等待,原子操作则无需使用信号量。
function srv_conc_enter_innodb:
ifdef HAVE_ATOMIC_BUILTINS
    srv_conc_enter_innodb_with_atomics(trx);
#else
    srv_conc_enter_innodb_without_atomics(trx);
#endif /* HAVE_ATOMIC_BUILTINS */
 
5.6引入adaptive sleep,允许innodb根据系统负载进行自适应调整,当innodb_adaptive_max_sleep_delay>0, innodb_thread_sleep_delay则 会动态调整(以前者为上限)。
ada还ptive 的算法是:
  如果sleep了当前值以后还是不能进入,就把sleep时间+1;
  如果还有线程在sleep时,已经有了空闲线程,就把当前值的sleep 时间减半。
innodb_thread_sleep_delay降低比增加的更快,这样在并发线程数很高时,当限制并发数早就达到,其他线程的每次sleep时间会缓慢拉长。而当Innodb层很空闲时,sleep时间又会快速降到非常低
调整sleep到一个优化值的目的是,过小的sleep值可能会产生太多的线程切换,但过长的sleep时间,在并发比较空闲的时候又会影响性能。新的并发控制策略有利于随着负载的变化而做自适应调整。



thread_running的意义
thread_running状态变量记录了当前并发执行stmt/command的数量,执行前加1执行后减1;
代码逻辑
  1. do_command
  2. -->dispatch_command
  3.     ...
  4.     inc_thread_running
  5.     ...
  6.     mysql_execute_command or execute_some_command
  7.     ...
  8.     dec_thread_running
  9.     ...
Thread_running突然飙高的诱因:
1 客户端连接暴增;
2 系统性能瓶颈,如CPU,IO或者mem swap;
3 异常sql;
往往在这种情况下,MySQL server会表现出hang住的假象。



Innodb_thread_concurrency不足
1线程因为锁等待而退出innodb层,当获取锁时可以直接重入innodb(跳过此参数检查),因此系统并发执行的线程数可能大于此参数值。
2代码路径靠后,此时线程已经开始执行命令,进入到ha_innobase,应该在mysql_start_query前限制即thread_running。
为此好多技术高手开发了相应patch,从server层控制并发执行的线程数,下文将做描述。






请使用浏览器的分享功能分享到微信等