MySQL高性能助推:table-cache参数源码详解

曹啸 2018-06-22 10:25:39

 

作者介绍

曹啸,民生银行信息科技部MySQL DBA,目前主要致力于MySQL源码及相关特性研究工作。在银行科技行业工作多年,兼具开发及运维工作经验,同时对银行多个业务领域亦有涉猎。

注:本文系作者原创投稿,首发于民生运维订阅号(ID:CMBCOP)。

 

一、引言

 

MySQL数据库自问世以来,就因它的体积小、速度快、低成本等优势受到众多企业的追捧。同时由于它的完全开源特性,更增进了广大数据库爱好者对其深入研究的兴趣,通过源码的研究与探索,MySQL越来越多的优秀特性被广泛挖掘出来。

 

本文将围绕MySQL table-cache相关参数进行相应的源码解读及性能分析,旨在为使用MySQL的众多数据库工程师提供一些实际开发或运维工作的助益。

 

二、参数源码解读

 

table-cache相关参数具体包括:

 

  • open_files_limit;

  • max_connections;

  • table_open_cache;

  • table_definition_cache。

 

MySQL实例进程在启动时会根据配置文件my.cnf中对这四个参数的设置进行自适应的调整生效,由于MySQL在设置这四个参数时存在严格的顺序和依赖关系,故将它们放在一起分析讨论。

 

首先看一下源码中关于这四个参数的自适应关系函数(源码位于MySQLd.cc),该函数在main函数中被调用,内部分别调用了各自的设置函数。

 

void adjust_related_options(ulong *requested_open_files)

{

 …

adjust_open_files_limit(requested_open_files);

adjust_max_connections(*requested_open_files);

adjust_table_cache_size(*requested_open_files);

adjust_table_def_size();

}  

由于这四个设置函数存在严格的顺序调用关系,下文将依次对各个函数进行拆分说明。

 

1、open_files_limit
 

 

该参数作用为限定了MySQL实例进程能打开的文件描述符最大值。关于该参数设置函数的声明:

 

void adjust_open_files_limit(ulong *requested_open_files)

函数中的参数requested_open_files为指针变量,这是一个贯穿前后的指针,它指向的内存中所存储的内容将会被后续函数多次用到。

 

limit_1= 10 + max_connections + table_cache_size * 2;

limit_2= max_connections * 5;

limit_3= open_files_limit ? open_files_limit : 5000;

request_open_files= max<ulong>(max<ulong>(limit_1, limit_2), limit_3);

由于该参数首先被设置,所以这部分计算逻辑所取用的变量均来自配置文件中的设置值(源码中的table_cache_size对应配置文件中的table_open_cache),根据计算后得出的limit_1,2,3将取最大值存放在变量request_open_files中。

PS:此处需注意的是,request_open_files和requested_open_files是不同的。

 

effective_open_files= my_set_max_open_files(request_open_files);

open_files_limit= effective_open_files;

随后,函数my_set_max_open_files会以request_open_files作为参数,来计算effective_open_files的值。此函数中的计算逻辑会根据不同的系统环境选择不同分支,但因本文的分析和测试均针对服务器,故以Linux分支为主。

 

不同的分支计算逻辑略有不同,在Linux环境下,函数中会调用系统函数getrlimit和setrlimit来获取系统资源的最大使用量,在系统允许范围内,计算所得到的effective_open_files的值与request_open_files的值相等。笔者所用测试物理机测得的系统允许上限值为1024*1024。

 

此外,另一个可能影响effective_open_files数值的是MySQL实例的启动方式:

 

  • 若MySQL实例在非root下启动,则effective_open_files的值会受到系统对于单进程句柄的限制,即命令ulimit –n得到的值;

  • 若在root下启动实例,则不会受到系统对单进程的限制。

 

计算后所得的effective_open_files的值会赋给open_files_limit,作为实例中最终生效的实际参数。

 

概括来讲,在未达到最大值时,计算所得的effective_open_files 与request_open_files 值相等,并将effective_open_files的最终值赋给open_files_limit;若超过最大值则effective_open_files会以配置文件中的open_files_limit设置值生效,若配置文件中设置也超限,则取系统对单进程文件描述符的限制做生效值。为了更清晰的说明此处逻辑,笔者进行了以下对比测试。

 

为了便于更直观的看清测试结果,我们更改系统单进程文件描述符限制为56789,更改系统对全局文件描述符限制为655350。然后分别使用下列三种配置文件启动实例:

 

配置一:配置文件中设置open_files_limit值为1040000。

 

使用非root用户启动MySQL实例,进入数据库查看变量生效值:

 

 

此时是以系统对单进程文件描述符限制生效的。

 

关闭实例进程,使用root用户启动MySQL实例,再次进入数据库查看变量生效值:

 

 

此时则以配置文件中的设置值生效。

 

配置二:配置文件中设置table_open_cache=520000,open_files_limit=655350。

 

在root用户下启动实例,可以看到此条件下可以按照配置文件生效:

 

 

随后,更改配置文件中设置table_open_cache=530000,open_files_limit不变,在root用户下重启实例,观察变量生效情况:

 

 

产生变化的原因是根据修改的table_open_cache值计算所得的effective_open_files会超出系统允许的上限,故MySQL会使用配置文件中的设置生效,并重新计算table_open_cache的值。

 

配置三:配置文件设置table_open_cache=530000,open_files_limit=1049000。

 

在root用户下重启实例,观察变量生效情况:

 

 

此时由于计算所得的effective_open_files和配置文件中的open_files_limit设置值均超限,故使用系统对单进程文件描述符限制作为生效值,并进一步计算得到实际的table_open_cache。

 

让我们回到源码中继续解读。在设置了open_files_limit生效值后,MySQL源码中通过封装C语言的标准输出函数实现了自己的输出函数,并在一定条件下向error.log中输入相应信息。

 

    if (effective_open_files < request_open_files)

  {

    if (open_files_limit == 0)

    {

      sql_print_warning("Changed limits: max_open_files: %lu (requested %lu)", effective_open_files, request_open_files);

    }

    else

    {

      sql_print_warning("Could not increase number of max_open_files to "

  "more than %lu (request: %lu)", effective_open_files, request_open_files);

    }

  }

根据源码逻辑,当effective_open_files小于request_open_files的值时,error.log中就会记录相应信息。同时又根据配置文件中是否设置了open_files_limit的值来输出不同内容的错误信息。以前文配置一为例,非root用户启动MySQL计算所得的effective_open_files小于request_open_files,查看error.log中的信息有如下内容:

 

 

对于root用户启动,由于比较条件不满足,则无相应信息输出。

 

     if (requested_open_files)

*requested_open_files= min<ulong>(effective_open_files, request_open_files);

函数体的最后在effective_open_files和request_open_files中取小值放在了指针requested_open_files所指的内存中,以便于后续函数对该变量的调用。

 

2、max_connections
 

 

该参数限制了MySQL实例允许的最大连接数,在数据库的维护工作中相较于其他参数也更容易直观的接触到,下面让我们看一下在源码中是如何对这个参数进行设置及生效的。

    

void adjust_max_connections(ulong requested_open_files)

此函数中的参数requested_open_files变量值即为前文函数中requested_open_files指针变量所指内容。关于max_connections的计算逻辑则相对简单。

 

limit= requested_open_files - 10 - TABLE_OPEN_CACHE_MIN * 2;

if (limit < max_connections)

  {

    sql_print_warning("Changed limits: max_connections: %lu (requested %lu)",limit, max_connections);

    max_connections= limit;

  }

首先根据算式计算limit变量的值,此处用到的TABLE_OPEN_CACHE_MIN是在头文件sql_const.h中定义的宏定义变量,值为400。

 

随后将limit的值与配置文件中设定的max_connections进行比较,若limit较小,则向error.log中输出一串提示信息,并以limit作为最终生效值;若limit较大,则直接以配置文件的设置值生效,同时不打印提示信息。以下为操作实例:

 

设置配置文件中max_connections值为5000,其他配置沿用前文。以非root用户启动MySQL实例,在数据库中查看max_connections的值:

 

 

此时max_connections按照配置文件中的设置进行生效,原因在于非root用户启动实例,requested_open_files取系统限定的effective_open_files值为56789,计算limit=56789-10-400*2=55979,该值远大于5000,故取较小值即配置文件中的设置值生效。

 

保持同样的条件不变,仅更改配置文件中的max_connections设置值为60000大于55979,使用非root用户重启MySQL实例,再次查看max_connections生效值,已经更新为预期的55979了。

 

 

3、table_open_cache
 

 

此参数规定了内存中允许打开表的数量,从它的作用可以看出,当MySQL在处理查询请求时,table_open_cache将会起到较大作用,有效设置并使用此参数可以降低热点表的频繁开关动作,从而改善性能。关于该参数设置的源码如下:

 

 void adjust_table_cache_size(ulong requested_open_files)

     limit= max<ulong>((requested_open_files - 10 - max_connections) / 2,

                    TABLE_OPEN_CACHE_MIN);

     if (limit < table_cache_size)

    {

    sql_print_warning("Changed limits: table_open_cache: %lu (requested %lu)",limit, table_cache_size);

    table_cache_size= limit;

    }

对比max_connections的实现函数可以看出,两个函数的内部结构相似,都是以requested_open_files为参数,根据各自的逻辑计算出limit变量的值,并将其与配置文件中的设置值比较,取更小的值作为最终生效值。需要注意的是,table_open_cache在源码文件sys_vars.cc中被限定了取值范围(0,512*1024),这个范围会对table_open_cache的实际生效值产生影响。下面看一些配置实例:

 

设置配置文件中table_open_cache=30000,max_connections=5000,在非root用户下启动实例。

 

此模式下,requested_open_files=56789,max_connections生效值5000,算式(requested_open_files - 10 - max_connections) / 2值为25889>400,故limit=25889,同时小于配置文件中的设置值,故最终生效值应为25889。在数据库中查看变量的实际生效值:

 

 

结果符合预期。

 

保持其他条件不变,更改配置文件中table_open_cache值为20000,再用非root用户重启实例,此时limit值仍为25889,但已大于配置文件中的设置值,故生效值应为20000。到数据库中查看实际生效值验证。

 

 

4、table_definition_cache
 

 

此参数乍一看很容易与table_open_cache相混淆,毕竟这对“孪生兄弟”从生效到功能都有相近之处。table_definition_cache定义了内存中可打开的表结构数量。此参数的设置函数与前列很大的一个不同点是没有使用requested_open_files作为参数,仅仅声明了一个无参函数:

 

void adjust_table_def_size()

此参数的设置算式仅使用了已生效的table_open_cache作为计算变量。

 

default_value= min<ulong> (400 + table_cache_size / 2, 2000);

table_open_cache函数中有一个很值得关注的点,在函数体内涉及了MySQL动态哈希链表的访问。这个链表是MySQL在启动主函数中定义并且用来存放部分系统变量的动态链表,下面我们来详细了解一下函数内部的访问流程。

 

首先函数体内定义了一个系统变量类型的指针var,随后调用函数访问哈希表,并将定位到的内存块赋值给类对象var。

 

var= intern_find_sys_var(STRING_WITH_LEN("table_definition_cache"));

此处STRING_WITH_LEN()是一个宏定义,返回内容为对应字符串本身及长度。我们到intern函数内部看一下:

 

var=(sys_var*)my_hash_search(&system_variable_hash,(uchar*) str, length ? length : strlen(str));

return var;

intern函数内部调用my_hash_search函数查找table_definition_cache字符串在哈希链表中对应的结点,并将找到的位置返回类对象指针var。在找到var指针的目标结点后,table_definition_cache设置主函数就把前面计算所得到的值写入var所指向的内存。

 

var->update_default(default_value);

值得注意的是,即便函数已将计算结果赋给目标结点,但函数依然会首先判断配置文件中是否设置了table_definition_cache的值。若配置文件中未设置,则以计算所得的结果作为生效值;若配置文件中有对应设置,则优先以配置文件中的设置生效。

 

     if (! table_definition_cache_specified)

table_def_size= default_value;

另一个需要注意的是虽然MySQL默认配置文件中设置的table_definition_cache优先生效,但在头文件sql_const.h中宏定义了table_definition_cache的下限值为400,故即便在配置文件中设置了一个很小的值,MySQL也会自动将生效值上调为下限值。下面看一些对应的配置实例:

 

我们选择前例中requested_open_files=56789,max_connections=5000,table_open_cache=25889的条件进行测试。

 

首先,我们在配置文件中设置table_definition_cache=35486并将其注释,保存文件。

 

重启MySQL实例,令修改的配置文件生效。根据前面对源码的分析,计算公式default_value= min<ulong> (400 + table_cache_size / 2, 2000)=2000,此时配置文件中的设置无效,应以计算结果生效。进入数据库中查看生效值:

 

 

然后修改配置文件,取消table_definition_cache的相关注释,保存配置文件并重启实例,到数据库中查看生效值:

 

 

此时已按照配置文件设置生效。

 

最后为验证MySQL对table_definition_cache下限的自适应调整,我们修改配置文件中的对应值为table_definition_cache=15并保存,重启,再次进入数据库查看生效值:

 

 

可以看到,生效值已被MySQL自适应调整为源码中宏定义的下限值。

 

至此,上文已完成对table_cache相关的源码分析内容,当我们在实际工作中需要调整相关参数,可以参考前文并配置。现在让我们进一步思考,在知晓了这些参数间的关联和配置方法后,如何设置相应的值才算合理?这些参数对MySQL的实际使用性能又会有多大的影响?下文将会对这部分内容进行测试分析。

 

三、 参数性能影响及测试分析

 

上文介绍了table-cache相关参数在MySQL数据库正常运转过程中存在至关重要的作用,但并不是每个参数的微调都会对性能产生显著的影响,以下将对它们逐一进行说明。

 

严格来讲,open_files_limit和max_connections对MySQL的重要性并非体现在性能方面。

 

对于open_files_limit来说,不合理的设置将会直接导致MySQL实例down掉或在启动时根本无法正常拉起进程。对于这些场景,MySQL会有简单直接的错误信息来提示DBA需要进行相应的调整。

 

对于max_connections来说,设置过大可能会对其它参数的生效产生影响,设置过小又无法满足业务高峰时的连接需求,从而造成大量的连接等待和超时。通常根据实际情况设置在能够满足业务峰值的大小即可。

 

基于上述原因,这两个参数的性能影响在此不做深入探讨,而把重点放在对另外两个参数的测试及分析。

 

1、table_definition_cache(TDC)
 

 

本节对TDC可能产生的性能影响进行测试分析。使用的MySQL测试版本为5.7.18,测试服务器为单实例单库,库中共建立40000张表,每张表内5000行数据。测试条件为保证其他参数一致的前提下分别设置TDC=10000和TDC=50000进行对比。

 

首先比较两种场景下全局变量的区别,分别修改配置文件中TDC设置值为10000和50000,再分别重启实例,查看相关全局变量如下:

 

  • TDC=10000

 

 

  • TDC=50000

 

 

通过对比可以看出,重启后两种场景的TDC生效值分别已达到预期的10000和50000。同时,我们在此处对比另外一个变量open_table_definitions,这个变量表示在当前状态下打开的表结构数量,两种场景下在MySQL实例刚刚重启后由于TDC的不同,打开的表结构数量是不同的:

 

  • TDC=10000的场景下打开的表结构数量被限制在10000;

  • TDC=50000的场景下实例中的表结构全部被打开(其中有40000张测试表+219张系统表)。

 

对比结果说明当TDC生效值大于库中全表数量时,实例启动时会将所有表结构加载到内存;当TDC生效值小于库中全表数量时,MySQL会按照TDC的生效值加载相应数量的表结构到内存。

 

随后我们来比较TDC的差别是否会对性能产生影响。测试使用sysbench工具模拟客户端向服务器发送请求,申请访问的表数量为35000,连接并发数为50,连接发送请求持续时间为5min,使用以上测试模式分别测试TDC=10000和TDC=50000两条件下四种基本SQL语句的qps,对比结果如下:

 

 

从测试结果对比可以看出,不同的TDC对MySQL基准性能的影响并不大。那么导致这样的因素是什么?

 

我们查看两个测试后全局状态变量open_table_definitions的值进行对比:

 

  • TDC=10000

 

  • TDC=50000

 

通过比较可以发现,虽然TDC设置值为10000的模式无法在启动实例时将所有表结构都加载入内存,但在实际请求到达服务器时,MySQL允许在内存中“超载”TDC定义的表结构数量,这就使得实例当前已打开的表结构数量可超过TDC的限定值。同时,对于单一表来说,重复访问并不会增加表结构的打开次数。因此,TDC对于性能的影响便不是很大了。

 

2、table_open_cache(TOC)
 

 

本节测试TOC可能产生的性能影响。使用的MySQL版本及测试库环境与TDC测试相同。首先比较两种不同TOC对性能带来的影响。根据前文,使用root和非root用户启动实例会导致open_files_limit的生效值有区别,进而影响TOC的生效值。本例在配置文件中设置TOC值为350000,再分别使用root及非root用户启动实例,观察实际生效值如下:

 

  • root用户启动:

 

 

  • 非root用户启动:

 

 

由于TOC决定了内存中打开表的数量,功能上对查询SQL的影响更为明显,故使用sysbench仅发送select语句并统计qps,申请访问的表数量是35000,访问持续时间为5min。更改不同的连接并发数分别测试qps并绘制成折线图,结果如下:

 

 

 

从曲线图可以看出,TOC较大(=350000)的条件下,查询语句的qps随并发数增加变化比较明显,整体呈现先迅速上升后平稳回落的趋势,在64-128并发范围内达到高峰。反观TOC较小(=30262)的模式下qps随并发数的增加无明显变化,而且持续处于较低水平。

 

为了更加清晰的分析TOC对性能的影响,我们统计了每个并发下独立测试的缓存命中率并绘制成曲线进行比较:

 

 

 

比较二者命中率曲线可以看出,TOC较大的模式下,缓存命中率随并发数的变化趋势与qps曲线基本一致,虽然存在一定的波动,但整体命中率均在99%以上,这时我们可以判定缓存的效率是较高的;但TOC较小的模式下,缓存命中率在不同并发数下普遍较差(低于50%),同时随并发数的增加还有急速下降的趋势。

    

我们再统计一下各个并发测试下的table_open_cache_overflows的值绘制成曲线并观察:

 

 

从曲线图可以看出,TOC较大的模式下overflow值非常小(基本为0),这与该模式下缓存命中率较高的结果相吻合;但TOC较小的模式下则存在较大的overflow,这说明该模式下较小的缓存无法满足大量的并发访问请求,缓存不得不持续将新表刷入内存。

 

四、总结

 

通过本文对table-cache相关源码的分析及对比测试,我们可以得出一些相关结论:

 

  • open_files_limit,max_connections, table_open_cache, table_definition_cache四个参数在MySQL实例启动时依次生效,且相互存在制约关系,若需要单独更改某一变量,要注意可能产生的影响;

  • open_files_limit参数的生效会受到不同启动方式的影响进而影响其它参数的生效值,设置时要按需选择;

  • open_files_limit和max_connections参数对性能影响较小,设置时可满足业务量需求即可,二者更多影响的是MySQL实例的正常运作;

  • table_definition_cache参数对性能影响较小;

  • table_open_cache参数对实际性能影响较大,但生效值需要在上下限间合理设置,为尽可能发挥cache性能,生效值应设置为大并发下可维持较高命中率的同时不发生缓存overflow。

 

以上是本文全部内容,希望读到此文的广大前辈和同行能够积极提出文中的不足并不吝指正。

 

活动预告