CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • MySQL开发与调试指南 说明
  • 第一章 MySQL的历史与架构
  • 第二章 使用MySQL源代码的基础操作
  • 第三章 核心类、结构、变量和API
  • 第四章 客户端/服务器通信
  • 第五章 配置变量
    • 配置变量教程
      • 配置文件和命令行选项
      • 配置选项解析的内部机制
      • 添加新配置选项示例
    • 特定配置变量的有趣之处
      • big-tables
      • concurrent-insert
      • core-file
      • default-storage-engine
      • delay-key-write
      • ftstopwordfile
      • innodbbufferpool_size
      • innodbflushlogattrx_commit
      • innodbfileper_table
      • innodblockwait_timeout
      • innodbforcerecovery
      • init - file
      • keybuffersize
      • language
      • log
      • log-bin
      • log-isam
      • log-slow-queries
      • maxallowedpacket
      • max_connections
      • maxheaptable_size
      • maxjoinsize
      • maxsortlength
      • myisam-recover
      • querycachetype(续)
      • readbuffersize
      • relay-log
      • server-id
      • skip-grant-tables
      • skip-stack-trace
      • slave-skip-errors
      • sortbuffersize
      • sql-mode
      • table_cache
      • temp-pool
      • transaction-isolation
  • 第六章 基于线程的请求处理
  • 第七章 存储引擎接口
  • 第八章 并发访问与锁机制
  • 第九章 解析器与优化器
  • 第十章 存储引擎
  • 第十一章 事务
  • 第十二章 复制
目录

第五章 配置变量

# 第五章 配置变量

研究MySQL服务器的配置变量,能让我们深入了解其内部工作原理。在某些情况下,一个特定名称的变量的存在本身就很有意义。例如,key_buffer_size表明MySQL使用了键缓存;query_cache_size意味着服务器可以缓存查询结果,这样在查询引用的表没有任何修改且重复运行时,就可以避免不必要的工作;innodb_flush_log_at_trx_commit说明InnoDB存储引擎支持事务,并且可以选择在每次提交事务时不将事务日志写入磁盘;slave_compressed_protocol则揭示了MySQL支持与请求压缩数据传输的从库进行复制。

其他一些选项名称可能没有这么直观,但通过思考该选项存在的原因,并研究源代码以了解不同设置对行为的影响,你会学到很多知识。

MySQL有超过200个不同的选项。每个选项都有其意义。有些选项揭示了某项功能的存在,有些展示了MySQL优化算法的丰富性,有些体现了MySQL的自我管理能力,有些是为了解决或规避特定平台的漏洞而设置的,还有一些只是允许用户选择用于某些内部操作的文件或目录,而它们的存在让我们得以窥探MySQL在幕后的工作。由于篇幅限制,我们无法逐一探讨所有选项,所以将重点关注那些最有趣的选项。

# 配置变量教程

为了弥补本章对许多配置变量介绍的不足,本节包含一个简要的“基础操作”教程,介绍选项解析的工作原理。这将帮助你在源代码中查找不熟悉的选项,以及添加自己的选项。

# 配置文件和命令行选项

mysqld可以在命令行中接收配置变量设置,也可以从配置文件中读取。可以存在多个配置文件,其内容会与命令行配置选项合并。

首先检查的文件是/etc/my.cnf。默认情况下,在尝试加载/etc/my.cnf(无论成功与否)之后,还会按以下顺序搜索其他位置:首先是由编译时定义的宏DATADIR指定目录下的my.cnf,然后是启动mysqld的实际用户(而非有效用户)主目录下的.my.cnf(注意文件名开头的点)。需要注意的是,无论之前的加载尝试是否成功,都会按此顺序继续尝试加载后续文件。因此,多个配置文件的设置可能会合并生效。

要查看mysqld在Linux系统上加载了哪些配置文件,可以运行以下Shell命令:

$ strace   -e  stat64  mysqld  --print-defaults > /dev/null
1

该命令的输出类似于:

stat64("/etc/my.cnf",  {st_mode=S_IFREG|0644,  st_size=2025,  ...})  =  0
stat64("/usr/var/my.cnf", 0xbfffdcbc)     =  -1  ENOENT  (No  such  file  or directory)
stat64("/home/sasha/.my.cnf", 0xbfffdcbc)  =  -1  ENOENT  (No  such  file  or directory) upeek:  ptrace(PTRACE_PEEKUSER,  ...  ):  Input/output  error
1
2
3

最后一行中strace关于ptrace()的错误消息可以忽略。输出中重要的部分是对stat64()的跟踪调用。每次调用中的第一个参数就是mysqld尝试加载的配置文件名称。这里依次是/etc/my.cnf、/usr/var/my.cnf和/home/sasha/.my.cnf。可以看到,第一个文件存在,而后两个文件不存在。

获取相同信息的另一种方法是运行:

$ mysqld  --verbose  --help  |   head  -15
1

输出内容大致如下:

/reiser-data/oreilly/mysql-5.1.11/sql/mysqld   Ver 5.1.11-beta-log for pc-linux-gnu on i686  (Source distribution)
Copyright  (C)  2000  MySQL  AB,  by  Monty and others
This  software  comes  with  ABSOLUTELY  NO  WARRANTY .  This  is free  software, and you are welcome to modify and redistribute  it  under  the GPL  license
Starts the MySQL  database  server Usage:  /usr/bin/mysqld  [OPTIONS]
Default options are read from the following files  in the given order: /etc/my.cnf ~/.my.cnf  /usr/var/my.cnf
The following groups are read: mysqld  server mysqld-5 .1
The following options may  be given as the first argument:
--print-defaults             Print  the  program argument  list and exit
--no-defaults                  Don't  read default options from any options file
1
2
3
4
5
6
7
8
9

该消息告诉我们,mysqld将按顺序检查/etc/my.cnf、~/.my.cnf和/usr/etc/my.cnf。

可以使用命令行参数--no-defaults禁用配置文件的加载。如果使用该参数,它必须是第一个参数。也可以使用--defaults-file和--defaults-extra-file指定其他配置文件。当指定--defaults-file时,会跳过编译时指定的配置文件序列,转而加载指定的文件。如果指定了--defaults-extra-file,则在加载完编译时指定的配置文件序列后,再加载指定的文件。需要注意的是,与--no-defaults一样,--defaults-file和--defaults-extra-file必须放在参数列表的开头,否则会出现未知选项的错误。因此,这两个选项不能一起使用。

当多个相互冲突的配置选项来源合并时会发生什么呢?要理解其中的情况和原因,一个好方法是查看mysqld在幕后处理配置选项的过程。sql/mysqld.cc中的main()函数首先会调用同样在sql/mysqld.cc中的init_common_variables()函数,而init_common_variables()函数又会调用mysys/default.c中的load_defaults()函数。load_defaults()函数负责加载所有可用的配置文件。你可能会注意到,load_defaults()函数接收&argv(命令行参数字符串数组)和&argc(命令行参数的数量)作为参数。这样做是为了将配置文件中的选项插入到列表中,让命令行参数处理代码认为这些选项是在命令行中指定的。需要注意的是,配置文件中的参数会按照文件处理顺序,插入到常规命令行参数之前。

命令行参数随后会通过调用sql/mysqld.cc中的get_options()函数进行处理,get_options()函数会将控制权转移到mysys/my_getopt.c中的handle_options()函数。handle_options()函数会从合并后的参数列表的第一个参数开始,依次处理这些参数。如果列表中同一个变量被设置了多次会怎样呢?从参数处理逻辑可以看出,列表中最后出现的设置将实际生效。

但前面的规则有一个例外。出于安全考虑,在处理链中,--user选项不允许被后续的选项值重置。

因此,我们可以发现,除了一些例外情况,在设置冲突时,命令行参数的优先级最高。其次是实际用户主目录下的.my.cnf,然后是编译时DATADIR中的my.cnf,最后是/etc/my.cnf。

尽管mysqld提供了多种加载配置的方式,但推荐的做法是只使用/etc/my.cnf,并确保mysqld不会加载其他配置文件,同时也不在命令行中指定参数 。

配置文件遵循以下非正式定义的格式:

[section_name ]
option_name=option_value#comment  
option_name=option_value
#comment
option_with_no_argument
1
2
3
4
5

配置文件中可以有多个部分,每个部分通常由同名的程序使用。因此,mysqld会查找标记为mysqld的部分。每行只能有一个选项。注释可以单独占一行,也可以放在选项行的末尾,并且以#开头。对于数值型选项值,可以使用后缀K、M或G分别表示千字节、兆字节或吉字节,小写后缀同样也被允许。参数周围可以使用单引号或双引号。以下示例说明了这些规则:

[mysqld]
key-buffer-size=128M
# 确保记录不使用键的查询
log-long-format
long-query-time='3' # 超过这个时间的查询就算太长
max-connections=300
socket="/var/lib/mysql/mysql.sock" 
datadir=/var/lib/mysql
1
2
3
4
5
6
7
8

从历史上看,服务器配置参数被分为两类:选项和变量。在3.23版本中,数值型变量必须使用set-variable选项来设置。例如,在3.23版本中,max-connections=300的等效写法是:

set-variable= max_connections=300 
1

出于对向后兼容性的坚持,MySQL的后续版本仍然支持3.23版本的变量设置语法。然而,在4.0版本中,配置参数处理代码的重写消除了这种区分。

# 配置选项解析的内部机制

配置变量由sql/mysqld.cc中的struct my_option my_long_options[]定义,而struct my_option在include/my_getopt.h中定义。表5-1按结构中的定义顺序列出了其成员。表5-2列出了var_type成员中使用的变量类型代码,表5-3列出了arg_type成员中使用的参数类型代码。

描述 定义
const char *name 配置文件中出现的选项名称。在命令行中,选项名称前需加双连字符:--。
int id 选项的唯一整数值代码。如果该代码在可打印的ASCII字符范围内,它也用于命令行选项的短格式(前缀为单个连字符)。例如,如果值是字符b的ASCII代码,那么该选项可以在命令行中用-b指定。
const char *comment 选项的简要说明,会显示在mysqld --help的输出中。
gptr *value 一个指针,指向用于存储解析后的选项值的内存位置。指针所指向变量的类型应由var_type成员的相应值指定。如果该选项不接受参数,则应设置为0。
gptr *u_max_value 一个指针,指向用于存储选项最大可能值的内存位置。通常指向max_system_variables结构的一个成员,从而实现对其初始化。
const char **str_values 目前在代码中似乎未被使用。显然它旨在指向一个包含选项可能字符串值的数组。如果添加自己的选项,应将其设置为0。
ulong var_type 变量类型代码。有关可能的值及其含义,请见表5-2。包含这些值的预处理宏在include/my_getopt.h中定义。
enum get_opt_arg_type arg_type 参数类型代码。有关可能的值,请见表5-3。enum get_opt_arg_type在include/my_getopt.h中定义。
longlong def_value 默认值。
longlong min_value 最小值。如果指定的值低于此最小值,选项的实际值将被设置为最小值。
longlong max_value 最大值。如果指定的值高于此最大值,选项的实际值将被设置为最大值。
longlong sub_size 在将选项值存储到与该选项相关的变量之前,需要从选项值中减去的值。
long block_size 选项值将被调整为该参数的倍数。
int app_type 显然是为未来使用保留的。可以安全地设置为0。
宏 十进制值
--- ---
GET_NO_ARG 1
GET_BOOL 2
GET_INT 3
GET_UINT 4
GET_LONG 5
GET_ULONG 6
GET_LL 7
GET_ULL 8
GET_STR 9
GET_STR_ALLOC 10
GET_DISABLED 11
GET_ASK_ADDR 128
值 描述
--- ---
NO_ARG 该选项不接受参数。提供参数会报错。这种类型通常用于布尔标志。
OPT_ARG 该选项可以接受参数,但不提供参数也不会报错。在这种情况下,变量的值将设置为默认值。这种类型通常用于那些让MySQL记录某些信息的选项,这些选项可以选择指定日志位置;或者用于启用某项功能的选项,该功能有多种操作模式,其中一种是合理的默认模式,其他模式则不太常用。
REQUIRED_ARG 该选项要求用户提供参数。如果未提供参数,将报告错误。这种类型通常用于数值型变量,或者其他只指定选项名称而期望服务器提供合理默认值没有意义的选项。

my_long_options的每个成员都对应一个配置选项。选项解析在sql/mysqld.cc中的get_option()函数中进行,get_option()函数只是对mysys/my_getopt.c中的handle_options()函数的封装。如果处理某个选项只是简单地初始化一个变量,那么在my_long_options数组相应成员的定义中提供合适的地址和变量类型就足够了。在某些情况下,则需要更复杂的初始化过程。在这些情况下,会使用sql/mysqld.cc中的get_one_option()回调函数。handle_options()函数在初始化选项值后,会为每个选项调用这个函数。

# 添加新配置选项示例

我们来考虑一个添加简单新配置选项的示例。有时,在尝试启动mysqld时可能会出现问题。一个陈旧的mysqld实例可能正在运行,并占用新实例试图获取的资源,这将导致错误。我们将添加一个名为kill-old-mysqld的选项,如果存在旧的mysqld实例,它会将其终止。本节所示的代码可从本书前言中列出的网站获取。

在这个示例中,我假设你已经完成了第2章中的步骤,并且拥有一个之前成功构建过的源代码树。

首先,我们在文本编辑器中打开sql/mysqld.cc文件。然后找到my_long_options的初始化部分,并在数组中某个位置添加以下条目(按选项名称的字母顺序添加是个不错的选择,但添加在其他位置也可行):

{"kill-old-mysqld", OPT_KILL_OLD_MYSQLD,   "Kill old  instance of mysqld on  startup",
(gptr*) &opt_kill_old_mysqld,  (gptr*)  &opt_kill_old_mysqld,  0,GET_BOOL,  NO_ARG, 0, 0, 0, 0, 0, 0},
1
2

注意,我们现在引用了一个不存在的值OPT_KILL_OLD_MYSQLD和一个变量opt_kill_old_mysqld,它们都还不存在。让我们快速解决这些问题。在同一文件中找到enum options_mysqld,并将OPT_KILL_OLD_MYSQLD添加到其中(按照惯例,新选项通常添加在末尾,但添加在其他位置也可行)。然后,我们添加一个全局变量,定义如下:

my_bool opt_kill_old_mysqld  = 0;
1

就代码功能而言,它的位置并不重要,但遵循既定惯例是个好主意。你可以搜索my_bool opt_skip_slave_start,并将其放在大致相同的区域。

现在,解析器可以识别该选项,并且变量也得到了初始化。我们的下一步是在该选项存在时实际执行一些操作。在main()函数中,调用init_common_variables()之后,添加以下代码:

if  (opt_kill_old_mysqld) kill_old_mysqld( );
1

现在我们需要声明并定义kill_old_myslqd()函数。首先,在文件顶部的某个位置(紧跟在static void mysql_init_variables(void)行之后是个不错的位置)添加以下声明:

static void  kill_old_mysqld(void);
1

然后在文件的某个位置(也可以放在mysql_init_variables()定义之后)添加以下定义:

static void  kill_old_mysqld(void) {
    File fd  =  -1;

    /*  pid value  can  have  no  more than  20  digits,
    and we  need  one  extra  byte  for  the  new  line  character */
    char  buf[21]; char*  p;
    long  pid;
    /* return  if we  cannot open the file  */
    if  ((fd=  my_open(pidfile_name,O_RDONLY,MYF(0)))  <  0) return;
    /*  Populate the  buffer.  For the  sake of  simplicity we  do  not deal
    with the  case of a  partial read, and  leave  it as an exercise for
    the meticulous reader. */
    if  (my_read(fd,  buf,  sizeof(buf), MYF(0)) <= 0) goto err;
    /*  boundary for  strchr( )  */ buf[sizeof(buf)  -  1]= 0;
    /* find the end of  line and  put a  \0 terminator  instead  */ if  (!(p=  strchr(buf,'\n')))
        goto err; *p= 0;
    if  (!(pid=  strtol(buf,0,10))) goto err;
    /*  Support for Windows  is  left as an exercise for
    the reader  */ #ifndef     WIN
    /* A  crude  kill method with  no  checks. A more refined method  is  left
    as an exercise for the reader.
    */
    kill(pid,  SIGTERM); sleep(5);
    kill(pid,  SIGKILL); sleep(2);
    #endif
    /* Cleanup.  Should  be executed  in  all  cases, success or error
    */ err:
    if  (fd  >= 0)
        my_close(fd,MYF(0));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

因为我们只修改了sql/mysqld.cc文件,所以只需在sql目录中运行make命令即可。如果你修改时没有输入错误,它将生成一个支持新选项的新mysqld二进制文件。

你可能已经从源代码的注释中注意到,为了使这个示例保持简单,我把很多复杂的工作留作练习让读者完成。希望这能帮助你理解MySQL开发团队所面临的挑战。由于对可移植性和健壮性的要求,即使是对代码库进行简单的添加,也涉及大量的错误检查和处理,以及许多可移植性方面的变通方法。从“在我这里能运行”到“准备好投入生产发布”还有很长的路要走。

# 特定配置变量的有趣之处

既然你已经了解了配置变量的一般处理方式,本节将介绍一些对mysqld有重大影响的特定变量。

# big-tables

MySQL优化器会尽可能避免在解析查询时使用临时表。然而,在某些情况下,这一棘手的任务不得不做。然后,如果可能的话,它会尝试使用内存临时表。不幸的是,表的大小并不总是能够提前预估。有时在填充表的过程中,会达到内存表的最大大小限制(该限制由tmp_table_size设置控制)。发生这种情况时,临时表需要转换为磁盘类型(即MyISAM)。这意味着要重新创建表,并将内存表中到目前为止收集到的行重新填充到新表中。

对于典型的MySQL使用场景,将内存表转换为磁盘表的情况很少发生。然而,有些应用程序经常遇到这种情况。如果你提前知道临时结果集太大,无法存储在内存中,big-tables选项就很有用了。它告诉服务器甚至不用尝试创建内存表,直接从基于磁盘的表开始。

启用big-tables选项后,对于特定的查询,仍可以使用SQL_SMALL_RESULT查询选项来覆盖该设置。或者,当big-tables选项被禁用时,可以使用SQL_BIG_RESULT选项强制优化器从基于磁盘的表开始。

要进一步研究这个选项,可以在sql/sql_select.cc中查找create_tmp_table()函数。和这个文件中的许多其他函数一样,这是一个非常大的函数。找到定义的起始位置后,你可能需要再次使用编辑器的搜索功能,这次查找OPTION_BIG_TABLES。

# concurrent-insert

在早期的3.23版本中,对MyISAM表的一个常见抱怨是,使用表锁(而不是行锁或页锁)会由于不必要的高锁争用而导致严重的性能下降。当流行的开发网站SourceForge(http://sourceforge.net)通过自己的基准测试发现MySQL在其应用中的扩展性不佳,并迁移到PostgreSQL时,这个问题在开源社区引起了广泛关注。其他一些用户也观察到了类似的结果。高负载下的性能下降被认为是由锁争用引起的。

确实,有充分的理由这么认为。虽然读锁是共享的,但写锁是排他的。如果一个线程正在更新一条记录,其他每个想要读取或写入其他记录的线程都必须进入队列等待锁。这意味着你必须暂停该线程,并且会发生上下文切换。如果这种情况频繁发生,很快你的CPU就会只忙于在不同线程之间切换,而无法进行实际工作。

事实证明,这个假设至少在很大程度上是不正确的。问题归因于LinuxThreads无法高效处理频繁获取和释放的互斥锁,而MySQL服务器必须大量进行这类操作。在对LinuxThreads应用补丁后,只要对读写查询进行适当优化,进行大量读写混合操作的基准测试的性能就会表现良好。

然而,在此期间添加了一个部分解决方法。虽然在一般情况下,实现最小冲突类型的锁相当困难,但在一种特殊情况下,只需对代码进行一些更改就可以最小化锁争用。当向表中插入一条记录时,MyISAM存储引擎首先会尝试找到一个之前删除的记录,其空间足够容纳新记录,然后用新记录覆盖该空间。然而,如果没有标记为删除的记录,新记录将被写入数据文件的末尾。在后面这种情况下,事实证明允许INSERT和SELECT操作并发进行并不那么困难。

启用此选项后,MyISAM存储引擎会尽可能尝试使用这种优化方式。

要了解更多关于并发插入的内容,可以研究myisam目录下的这些文件:

mi_open.c
mi_extra.c
mi_write.c
mi_range.c
mi_rkey.c
mi_rnext.c
mi_rnext_same.c 
mi_rprev.c
mi_rsame.c
1
2
3
4
5
6
7
8
9

并在其中搜索concurrent_insert。

# core-file

调试多线程程序可能颇具挑战。当程序崩溃但没有生成核心转储文件(core file)时,调试就更具挑战性了。有时你非常需要这个核心转储文件,因为崩溃问题可能无法在调试器中复现。而且在使用线程的情况下,有些平台并不太愿意生成核心转储文件。

在不幸发生崩溃时,这个选项会充分利用MySQL的神奇功能,促使不太配合的内核写出核心转储文件。这个神奇功能在sql/stacktrace.c中的write_core()函数中实现。

# default-storage-engine

过去,这个选项被称为default-table-type,目前该名称仍受支持。随着MySQL AB从一家努力打造优秀数据库的小公司发展成为一家试图在企业界崭露头角的大型企业,人们发现IT经理对“存储引擎(storage engine)”这个术语的接受度比对“表类型(table type)”要高得多,尽管对于MySQL开发者来说,“表类型”可能是一个更直观的术语。

由于其发展历史和Monty的洞察力,MySQL最终形成了一个非常强大的架构,它将存储引擎与解析器和优化器充分抽象分离,从而支持多种存储引擎。

一种存储引擎类型是MEMORY,它仅将表存储在内存中。MyISAM提供持久化存储以及许多出色的功能,如全文搜索和空间索引,但它不支持事务和行级锁。InnoDB支持事务和行级锁,但在某些操作上比MyISAM慢,并且需要更多的磁盘空间。根据应用程序的需求,你可以在创建表时为每个表选择合适的存储引擎类型。

在创建表时可以指定存储引擎。如果省略该选项,则使用default-storage-engine指定的存储引擎。也可以使用ALTER TABLE命令为现有表更改存储引擎。default-storage-engine的默认值是MyISAM。

要研究存储引擎,可以查看以下文件:

sql/handler.h
sql/handler.cc
sql/ha_myisam.h  
sql/ha_myisam.cc 
sql/ha_innodb.h  
sql/ha_innodb.cc 
sql/ha_heap.h
sql/ha_heap.cc
1
2
3
4
5
6
7
8

以及其他匹配sql/ha_*.cc和sql/ha_*.h模式的文件。

# delay-key-write

这个选项在3.23版本早期添加,用于优化在MyISAM表中更新键(INSERT、UPDATE和DELETE)的查询。通常情况下,服务器会在每个查询结束时将更改的键块从MyISAM键缓存中刷新出去。在某些情况下,这可能会导致严重的性能下降。

解决这个性能问题的一种方法是延迟键块刷新。当键写入被延迟时,更改的块不会在查询结束时被刷新出去。刷新操作会在以下情况之一发生:

  • 使用FLUSH TABLES将所有表从表缓存中移除。
  • 使用FLUSH TABLE将表从表缓存中移除。
  • 服务器关闭时刷新表缓存。
  • 新表将当前表从表缓存中置换出去。
  • 新的键块将更改的键块从键缓存中置换出去。

如果delay-key-write设置为ON,只有设置了DELAY_KEY_WRITE=1的表才会以这种方式处理。当设置为ALL时,所有MyISAM的键写入都会被延迟,而不管表的选项如何。当设置为OFF时,不管表的选项如何,都不会进行延迟键写入。

使用这个选项的优点是性能提升,缺点是如果发生崩溃,表损坏的风险会更高。

要进一步研究这个选项,可以在myisam/mi_locking.c中找到mi_lock_database()定义的起始位置,并查找对flush_key_blocks()的调用。你可能还想研究mysys/mf_keycache.c中的flush_key_blocks()函数本身。

# ft_stopword_file

MyISAM表支持全文键,这使得存储引擎能够通过字符串中间的单词快速查找记录。相比之下,常规的B树索引只能用于基于键的整个值或至少键的前缀来查找记录。

全文搜索功能具有高度的可定制性。这个选项是众多全文搜索配置选项之一。在进行全文索引时,为了提高索引质量,一些常用词会被忽略。例如,如果文本列包含常规的英语句子,对单词“the”进行索引几乎没有价值,因为它会出现在绝大多数记录中。这样的词被称为停用词(stop words)。

默认情况下,MySQL使用myisam/ft_static.c中ft_precompiled_stopwords数组定义的内置停用词列表。如果要索引的文本是标准英语句子的集合,这个列表效果很好,但如果用于其他语言或文本集合不是常规文本,它可能就不适用了。你可以创建自己的停用词列表,并使用这个选项指定。请注意,如果你更改了停用词列表,需要对现有表重新索引,可以使用REPAIR TABLE tbl_name QUICK命令来完成。

要了解更多关于全文索引的工作原理,可以查看以下文件:

myisam/ft_boolean_search.c
myisam/ft_eval.c
myisam/ft_nlq_search.c 
myisam/ft_parser.c
myisam/ft_static.c
myisam/ft_stem.c
myisam/ft_stopwords.c 
myisam/ft_update.c
1
2
3
4
5
6
7
8

# innodb_buffer_pool_size

这个缓冲区设置是InnoDB最重要的变量之一。它控制用于缓存InnoDB表数据和索引的内存大小。请注意,InnoDB与MyISAM在表数据缓存方式上有所不同。MyISAM只缓存键,并且只是寄希望于操作系统能很好地缓存数据。而InnoDB并不依赖操作系统,而是自行处理数据缓存的问题。

要了解更多关于InnoDB缓冲的工作原理,可以从查看innobase/buf/buf0buf.c中的buf_pool_init()、buf_page_create()和buf_page_get_gen()函数开始。还可以查看innobase/include/buf0buf.h中buf_page_get_gen()的宏包装器,特别是代码中最常用的buf_page_get()。同一文件中还对缓冲的内部机制进行了详细解释。

# innodb_flush_log_at_trx_commit

InnoDB在设计上比MyISAM有更严格的数据安全要求。它尽力确保即使在事务进行过程中突然断电,数据也能保持一致,并且损失最小。然而,必须在性能和数据安全之间取得良好的平衡,并且每个应用程序都有自己的标准。

InnoDB维护一个事务日志,用于在服务器启动时进行恢复。无论是否发生崩溃,都会尝试进行恢复。如果发生崩溃,日志中会有待处理的事务需要重做。如果没有崩溃,日志中不会找到待处理的事务,也就无需进行任何操作。

因此,我们可以看到事务日志的完整性在恢复过程中至关重要。确保其完整性的一种方法是在每次事务提交时将其刷新到磁盘。这样在发生崩溃时,我们就可以恢复所有已提交的事务。然而,如果应用程序频繁执行短事务,这将成为性能瓶颈。每次日志刷新至少意味着一次磁盘写入操作,即使使用现代磁盘,每秒也只能进行有限次数的写入,尽管InnoDB可以通过批量提交在一定程度上克服这个限制。

这个问题可以通过稍微降低数据安全要求的严格程度来解决,即每秒只将日志刷新到磁盘一次。在磁盘I/O正常(这在硬件和操作系统正常运行时是可以预期的)的假设下,采用这种方法,我们的数据仍然是一致的,但恢复后的数据可能会有最多一秒的延迟。对于许多应用程序来说,这种风险可以忽略不计,并且由于磁盘写入次数大幅减少,性能可提升约百倍,这是值得的。

当innodb_flush_log_at_trx_commit设置为0时,日志缓冲区每秒写入一次日志文件,并对文件描述符执行刷新到磁盘的操作,但在事务提交时不执行任何操作。当该值为1时,每次事务提交时,日志缓冲区都会写入日志文件,并对文件描述符执行刷新到磁盘的操作。当设置为2时,每次提交时,日志缓冲区会写入文件描述符,但不对其执行刷新到磁盘的操作。不过,文件描述符的刷新操作每秒进行一次。

我必须指出,由于进程调度问题(例如,我们可能无法在希望刷新的时候获得CPU资源),每秒一次的刷新并不能100%保证,但会尽力尝试。除了CPU极度过载的情况,实际的刷新间隔非常接近一秒。当服务器获得CPU资源时,它会检查自上次刷新以来是否已过一秒,如果是,则再次进行刷新。

要进一步研究这个选项,可以查看innobase/srv/srv0srv.c中的srv_master_thread()函数(查找对log_buffer_flush_to_disk()函数的调用) 以及innobase/trx/trx0trx.c中的trx_commit_off_kernel()函数(查找对log_write_up_to()函数的调用)。

# innodb_file_per_table

从一开始,MyISAM表就具有在服务器无需干预的情况下,轻松按表进行备份和复制的优势。这之所以可行,是因为MyISAM表存储在三个文件中:table_name.frm用于存储表定义,table_name.MYD用于存储数据,table_name.MYI用于存储键。

当InnoDB被引入MySQL时,许多用户怀念在文件系统层面操作表的便利性。最初,InnoDB表只能存储在表空间文件或原始设备中。不过,4.1.1版本增加了将每个表存储在其自己文件中的功能。

启用innodb_file_per_table后,新表的索引和数据将存储在一个单独的文件table_name.ibd中。尽管如此,这并没有像MyISAM表那样,给予用户自由操作这些文件的权限。在撰写本文时,InnoDB仍然在其全局表空间中存储大量元信息,这使得此类操作无法进行,不过目前相关工作正在推进,以便支持这些操作,使将.ibd文件从一个服务器实例复制到另一个实例成为可能。

目前,innodb_file_per_table仅有助于在同一服务器上备份和恢复单个表,即便如此也需要一些技巧。备份必须在服务器关闭时,或者在所有事务提交且没有新事务开始之后进行。恢复时,首先运行ALTER TABLE tbl_name DISCARD TABLESPACE,然后将.ibd文件复制到相应的数据库目录中,最后运行ALTER TABLE tbl_name IMPORT TABLESPACE。

请注意,这个选项属于NO_ARG类型。这意味着它不接受常规的非布尔参数。如果该选项存在,则表示开启;如果不存在,则表示关闭。不过,它也可以选择性地接受布尔参数1或0。

要了解这个选项的更多详细工作原理,可以研究innobase/dict/dict0crea.c中的dict_build_table_def_step()函数。查找其中的srv_file_per_table变量。

# innodb_lock_wait_timeout

与仅支持表级锁的MyISAM存储引擎不同,InnoDB可以对单个记录进行锁定,这被称为行级锁。这能为各种应用带来显著的性能提升,但不幸的是,它也带来了一个问题:潜在的死锁。例如,假设线程1获取了记录A的排他锁,与此同时,线程2获取了记录B的排他锁。然后,线程1在仍然持有记录A的锁的情况下,尝试获取记录B的锁,但必须等待线程2释放它。在此期间,线程2在仍然持有记录B的锁的情况下,试图锁定记录A。这样一来,两个线程都无法继续执行,从而形成了死锁状态。

虽然可以使用一种避免潜在死锁的算法,但这种策略很容易导致严重的性能下降。InnoDB从不同的角度来处理这个问题。通常情况下,死锁非常罕见,特别是在编写应用程序时如果对这个问题有所了解的话。因此,InnoDB不是去预防死锁,而是让死锁发生,但它会定期运行一个锁检测监视器,释放处于死锁状态的 “被困” 线程,并允许它们返回,向客户端报告由于等待锁的时间超过了该选项的值,它们已被中止。

需要注意的是,死锁监测线程实际上并不会检查每个线程持有的锁的序列来判断是否存在逻辑数据库死锁。相反,它假设如果某个线程等待请求的锁的时间超过了时间限制,那么它很可能处于逻辑死锁状态。即使实际上并非如此也没关系 —— 用户宁愿收到错误消息,也不愿在毫无进展的情况下无限期等待。

死锁检测线程在innobase/srv/srv0srv.c中的srv_lock_timeout_and_monitor_thread()函数中实现。

# innodb_force_recovery

在理想情况下,事务型数据库永远不需要这个选项。但不幸的是,会发生一些非常意外的情况。即使是一个逻辑完美、实现无误的事务系统,也依赖于一个假设,即它能够准确地从磁盘读取上次写入的数据。任何有经验的计算机专业人员都知道,由于硬件故障、操作系统错误、用户失误等多种原因,这个假设有时并不成立。此外,尽管InnoDB本身非常健壮,但也可能存在漏洞。归根结底,有时表空间会严重损坏,导致标准的恢复算法失效。

从纯粹的理论角度来看,数据损坏意味着磁盘上替代旧表空间的数据只是一堆随机数据。幸运的是,在大多数实际情况下,有更好的解决办法。通常,致命的损坏只会破坏少数几个页面,而其余数据是完好无损的。因此,有时通过一些合理的推测,有可能恢复大部分丢失的数据。

这个选项告诉InnoDB在恢复丢失数据时要尝试的程度。0表示不超出标准恢复算法的范围,而6表示不惜一切代价启动数据库,然后尽力运行查询而不崩溃。如果该选项的值大于0,则不允许执行更新表的查询。用户需要导出表以挽救数据,然后重新创建一个干净的表空间并重新填充数据。

要了解强制恢复时会发生什么,可以在以下文件中搜索变量srv_force_recovery: innobase/buf/buf0buf.c innobase/dict/dict0dict.c innobase/fil/fil0fil.c innobase/ibuf/ibuf0ibuf.c innobase/log/log0recv.c innobase/row/row0mysql.c innobase/row/row0sel.c innobase/srv/srv0srv.c innobase/srv/srv0start.c innobase/trx/trx0sys.c innobase/trx/trx0undo.c

# init - file

这个选项在服务器启动时从指定文件运行一组SQL命令。它的一个可能用途是将基于磁盘的表中的数据加载到内存(即MEMORY)表中,以实现更快的访问。

init - file还可用于验证某些数据的完整性、执行清理操作、确保重要表的存在,或执行类似的其他操作。例如,你可以在其中写入SET GLOBAL var=value或LOAD INDEX INTO CACHE。

要进一步了解这个选项,可以研究sql/mysqld.cc中的read_init_file()和bootstrap()函数,以及sql/sql_parse.cc中的handle_bootstrap()函数。

# key_buffer_size

MyISAM存储引擎会缓存表的索引键。这个选项用于控制MyISAM键缓存的大小。需要注意的是,MyISAM没有设置数据缓存的选项。与InnoDB不同,MyISAM希望操作系统能做好缓存工作,在Linux系统上通常确实如此。

要深入了解MyISAM键缓存的工作原理,可以查看mysys/mf_keycache.c文件。

# language

这个选项指定包含错误消息文件errmsg.sys的目录路径。不同的目录包含不同语言的文件,因此这个选项被命名为language。

我觉得这个选项很有趣,原因有几个。与许多像MySQL这样复杂程度的应用程序不同(这些应用程序通常需要一套配置文件、共享库和其他附属文件才能运行),mysqld二进制文件在复制到另一个系统并尝试运行时,并没有那么挑剔。然而,有一个外部文件是它绝对不能缺少的,那就是errmsg.sys。当你想在某个系统上使用特定的二进制文件进行一些快速而简单的测试,又不想安装所有MySQL文件时,可以将mysqld和errmsg.sys复制到某个目录,例如/tmp/mysql,然后执行类似以下的命令来启动服务器:

/tmp/mysql/mysqld --skip-grant --skip-net \
--datadir=/tmp/mysql --socket=/tmp/mysql/mysql.sock \
--language=/tmp/mysql &
1
2
3

因此,language有幸成为启动一个精简、不显眼的 “空降” 到目标系统的MySQL服务器所需的选项之一。

这个选项及其相关的MySQL功能的另一个独特之处在于,它可以作为一种创新的语言学习工具。通过使用不同的language选项启动MySQL,你可以阅读不同语言的错误消息,这有助于你构建一些基本的技术词汇,或者至少能收集一些有趣的短语来逗乐那些毫无防备的当地人。在MySQL 3.23版本时,只有190条错误消息,我曾开玩笑地向同事提议写一本名为《通过MySQL错误消息在190天内学会瑞典语》的书。在撰写本文时,错误消息的数量已经增加到了301条。随着MySQL功能的不断增加,由于错误消息数量的增多,这本书的可行性变得越来越低,因为学习瑞典语所需的时间也会随之增加。

服务器错误消息编号在include/mysqld_error.h中定义,编号从1000开始。对于每个代码,在sql/share/language_name/errmsg.txt中按数字代码顺序都有相应的消息。构建过程使用一个名为comp_err的工具(它是源代码树的一部分)从errmsg.txt创建errmsg.sys。comp_err的源代码位于extra/comp_err.c。

在5.0版本中,错误消息的维护得到了简化,现在所有错误消息数据都包含在一个文件errmsg.txt中。

要深入了解MySQL如何处理错误消息,可以查看sql/derror.cc中的init_errmessage()函数,以及sql/unireg.h中的ER()宏。

# log

这个选项用于启用通用活动日志,它会记录每一条命令。有时MySQL开发者称其为查询日志。它对调试客户端非常有帮助,但另一方面,在活动频繁的服务器上,这个日志会增长得非常快,因此使用该选项时应谨慎。

要进一步了解它,可以查看sql/sql_class.h和sql/log.cc中的MYSQL_LOG类。这种日志的实际记录发生在以下函数中:

MYSQL_LOG::write(THD  *,enum  enum_server_command  ,const  char*,...)
1

需要注意的是,日志记录代码在5.1版本中进行了重构。对于5.1及更高版本,添加了其他日志类并将其移动到sql/log.h中。写入通用日志的方法签名也已更改,变为:

bool MYSQL_LOG::write(time_t event_time,  const  char  *user_host,  uint  user_host_len, int thread_id,  const  char  *command_type,  uint  command_type_len,   const  char  *sql_text,  uint  sql_text_len)
1

# log-bin

这个选项用于启用二进制格式的更新日志(因此名称中有-bin)。它主要用于复制主服务器上的复制功能,但也可用于增量备份。日志记录发生在逻辑层面,即记录查询以及一些元信息。这个选项在3.23版本开发复制功能时引入。

要快速了解二进制日志的工作原理,可以查看MYSQL_LOG::write(Log_event*)。不过,日志格式的详细内容将在第12章讨论。

# log-isam

这个选项用于跟踪MyISAM存储引擎的底层操作,例如打开和关闭表、写入或读取记录、查询和更新索引文件状态以及其他功能。可以使用命令行工具myisamlog查看这个日志,其源代码位于myisam/myisamlog.c。

日志记录功能在myisam/mi_log.c中实现。第10章将更详细地讨论这个日志本身以及myisamlog的输出。

这个选项在MySQL的早期版本就已存在(在3.23版本之前,它记录ISAM活动,因为当时MyISAM存储引擎还不存在)。在多次调试MyISAM问题时,它都发挥了作用,是学习MyISAM的一个很好的工具。

# log-slow-queries

这个选项用于启用对优化器认为不够优化的查询的日志记录。判断标准有两个:执行时间(由long_query_time选项控制)和键的使用情况。

如果你请一位MySQL专家帮你解决性能问题,他可能首先会让你启用log-slow-queries以及log-queries-not-using-indexes(在4.1版本之前为log-long-format),并检查日志中记录的每一条查询。

日志记录是使用sql/log.cc中的标准MySQL日志类实现的。然而,对于刚开始学习MySQL开发的人来说,与这个选项相关的源代码中最有趣的部分可能是sql/sql_parse.cc中的以下代码片段:

if  ((ulong)  (thd->start_time  - thd->time_after_lock)  > thd->variables.long_query_time  ||
    ((thd->server_status &
    (SERVER_QUERY_NO_INDEX_USED   |    SERVER_QUERY_NO_GOOD_INDEX_USED)) && (specialflag  &  SPECIAL_LOG_QUERIES_NOT_USING_INDEXES)))
{
    long_query_count++;
    mysql_slow_log.write(thd, thd->query, thd->query_length,  start_of_query); 
}
1
2
3
4
5
6
7

如你所见,这段代码片段控制着哪些查询会被记录到日志中。如果你的系统运行了很多误报的查询(或者你对 “慢查询” 有自己的定义),你可能会发现修改这部分代码会有所帮助。你可以添加一个简单的测试,检查thd->examined_row_count是否超过某个阈值。

# max_allowed_packet

MySQL网络通信代码是在假设查询总是比较简短的前提下编写的,因此查询可以作为一个整体发送到服务器并由服务器进行处理,在MySQL术语中,这个整体被称为一个数据包。服务器会分配一个临时缓冲区的内存来存储数据包,并请求足够的内存以完全容纳它。这种架构需要采取预防措施,以避免服务器内存耗尽,这个选项就是用来限制数据包大小的。

与这个选项相关的代码位于sql/net_serv.cc中。查看my_net_read()函数,然后跟踪对my_real_read()的调用,并特别注意net_realloc()函数。

这个变量还限制了许多字符串函数结果的长度。详细信息可查看sql/field.cc和sql/item_strfunc.cc。

# max_connections

每个新的客户端连接都会消耗一定的系统资源。在资源有限的情况下,许多操作系统的表现并不理想,尤其是在内存方面。这个选项用于限制服务器愿意接受的最大连接数。其目的是让MySQL服务器在遇到意外的负载高峰时,在占用过多系统资源之前进行自我限制。

与这个选项相关的代码相当简单,位于sql/mysqld.cc中的create_new_thread()函数开头。

# max_heap_table_size

堆表(heap table)是MySQL中对内存表的一种说法,这个名称源于它是从程序堆中分配的。内存表速度非常快。然而,使用时需要注意,因为它们很容易被填充数据,以至于耗尽系统内存。这个选项用于限制每个内存表的最大大小。

需要注意的是,这个选项并不能阻止恶意用户进行拒绝服务攻击;恶意用户可以创建大量符合大小限制的内存表,从而耗尽内存。

要研究这个选项的实现,首先查看sql/ha_heap.cc中的ha_heap::create()函数,并跟踪到heap/hp_create.c中的heap_create()函数。然后从sql/ha_heap.cc中的ha_heap::write_row()函数开始,跟踪到heap/hp_write.c中的heap_write()函数,再跟踪到同一文件中的next_free_record_pos()函数。以下代码块实现了相关功能:

if  (info->records  >  info->max_records  &&  info->max_records)
{
    my_errno=HA_ERR_RECORD_FILE_FULL; 
    DBUG_RETURN(NULL);
}
1
2
3
4
5

# max_join_size

这个选项主要是为了防止有缺陷的应用程序和经验不足的用户使服务器崩溃。它告诉优化器,如果某个查询需要检查的记录组合数量超过给定的数量,就中止该查询。

在代码层面,在sql/sql_select.cc中的JOIN::optimize()函数内的以下代码块实现了这个功能:

if  (!(thd->options  &  OPTION_BIG_SELECTS)  &&
    best_read  >  (double)  thd->variables.max_join_size && !(select_options  &  SELECT_DESCRIBE))
{                                                                                 /*  purecov:  inspected  */
    my_message(ER_TOO_BIG_SELECT,  ER(ER_TOO_BIG_SELECT),  MYF(0));
    error=  1;                                                               /*  purecov:  inspected  */
    DBUG_RETURN(1); 
}
1
2
3
4
5
6
7

# max_sort_length

MySQL的记录排序算法(称为文件排序,filesort)使用固定大小的键值进行排序。这就需要根据给定键的最大可能大小来分配内存。如果使用二进制大对象(blob)或文本列的完整长度进行排序,可能需要分配大量内存,因为这些列的大小可能高达4GB(对于LONGBLOB类型)。为了解决这个问题,MySQL对用于排序的键前缀长度进行了限制。这样做的代价是,排序结果仅对前缀值是正确的。

这个变量用于限制排序键前缀的长度。最初,BLOB排序键的截断点是1024。然而,任意的 “魔法数字” 并不好,所以后来将其改为一个参数,由这个选项来控制。

在代码层面,关键代码位于sql/filesort.cc中的sortlength()函数内的以下几行:

if  (sortorder->field->type( )  ==  FIELD_TYPE_BLOB)
    sortorder->length= thd->variables.max_sort_length;
1
2

# myisam-recover

这个选项用于在MyISAM存储引擎发现损坏时,自动修复损坏的MyISAM表。正常情况下,不应该出现损坏的情况。然而,可能会出现电源故障、操作系统崩溃或I/O代码存在错误,MySQL本身也可能崩溃或MyISAM存储引擎存在漏洞。虽然MyISAM表在从这类崩溃中恢复的健壮性方面不如InnoDB,但大多数时候,即使是最严重的问题,通常也可以通过表修复来解决,而且往往只会丢失一条记录。

如果禁用这个选项,则必须使用REPAIR TABLE命令(在线修复)或myisamchk工具(离线修复)来进行修复。启用这个选项的好处显而易见。假设在半夜,一个小表不知为何损坏了。如果启用了这个选项,你就不用被传呼机吵醒,告知你Web应用程序因表损坏而无法运行,而你要做的只是手动修复。其缺点是,这个选项可能会在你不知情的情况下触发大量占用CPU和I/O资源的修复操作,在这段时间内给最终用户带来更糟糕的体验。

与这个选项相关的代码虽然概念上很简单,但可能有点难以理解。存储引擎类(handler的子类)可以选择定义bool auto_repair()方法。默认实现返回false;然而,如果myisam_recover_opt不为0,bool ha_myisam::auto_repair()会返回true。sql/table.cc中的openfrm()函数尝试打开表,如果打开失败,它会检查存储引擎是否报告表已损坏,然后,如果auto_repair()返回true,它会在其TABLE*参数(即表描述符)中设置crashed标志。当openfrm()返回到其调用函数sql/sql_base.cc中的open_unireg_entry()时,会检查crashed标志,如果设置了该标志,则会尝试进行修复。

对于刚开始学习MySQL开发的人来说,一个不错的实践练习是修改sql/ha_myisam.h中的bool ha_myisam::auto_repair()函数,使其仅对通过单独配置选项指定名称的表报告自动修复功能。

# query_cache_type(续)

MySQL有一个相当独特的功能:它可以缓存查询结果。有人可能会问,在数据没有变化的情况下,应用程序为什么要反复运行相同的查询呢?然而,MySQL用户报告称,自4.0版本首次出现此功能后,他们应用程序的性能平均提高了约60%。

这个选项用于设置缓存策略。可能的值有:0表示不缓存;1表示缓存除了带有SQL_NO_CACHE标志之外的所有查询;2表示仅缓存带有SQL_CACHE标志的查询。

这是众多控制查询缓存行为的选项之一。若要了解查询缓存的工作原理,可以研究sql/sql_cache.cc文件。

# read_buffer_size

虽然MyISAM存储引擎通常不会缓存数据行,但在进行顺序扫描时会使用预读缓冲区。这个选项用于控制预读缓冲区的大小。

在代码层面,有两个部分与这个选项相关,值得研究。第一部分来自sql/records.cc中的init_read_record()函数:

info->read_record=rr_sequential; 
table->file->ha_rnd_init(1);
/* 如果我们不更新动态长度的表,就可以使用记录缓存 */
if  (!table->no_cache &&
    (use_record_cache  >  0  ||
    (int) table->reginfo.lock_type <=  (int) TL_READ_HIGH_PRIORITY   || 
    !(table->db_options_in_use  &  HA_OPTION_PACK_RECORD)   ||
    (use_record_cache  <  0  &&
    !(table->file->table_flags( ) & HA_NOT_DELETE_WITH_CACHE)))) 
    VOID(table->file->extra_opt(HA_EXTRA_CACHE,
    thd->variables.read_buff_size));
1
2
3
4
5
6
7
8
9
10
11

当我们跟踪MyISAM表调用handler::extra_opt()函数时,最终会在myisam/mi_extra.c中的mi_extra()函数中。缓冲区分配发生在以下代码段:

cache_size=  (extra_arg  ?  *(ulong*)  extra_arg  :
    my_default_record_cache_size);
if  (!(init_io_cache(&info->rec_cache,info->dfile,
    (uint) min(info->state->data_file_length+1, cache_size),
    READ_CACHE,0L,(pbool)  (info->lock_type   !=  F_UNLCK), MYF(share->write_flag & MY_WAIT_IF_FULL))))
{
    info->opt_flag| =READ_CACHE_USED;
    info->update&=  ~HA_STATE_ROW_CHANGED; 
}
1
2
3
4
5
6
7
8
9

# relay-log

这个选项很少需要设置,因为默认值通常就可以满足需求。不过这里介绍它,是因为它的存在和名称有助于理解MySQL复制的内部机制。

MySQL复制采用主从模式。主服务器记录更新操作,从服务器与主服务器保持连接,并持续读取主服务器更新日志(在MySQL术语中称为二进制日志)的内容。然后,从服务器将从主服务器读取到的更新应用到自己的数据副本上,从而保持数据同步。

在3.23版本中,从服务器只有一个线程来立即应用更新。当从服务器能够跟上主服务器的节奏时,这种方式运行良好。然而,在某些情况下,从服务器会远远落后于主服务器。如果主服务器发生致命且无法恢复的崩溃,从服务器将永远无法获取尚未复制的数据。为了解决这个问题,4.0版本对从服务器算法进行了重新设计。现在,从服务器有两个线程:一个用于网络I/O,另一个用于应用SQL更新。I/O线程从主服务器读取更新内容,并将它们追加到所谓的中继日志(relay log)中。而SQL线程则读取中继日志的内容,并将其应用到从服务器的数据上。

要深入了解这个选项,实际上就是要理解从服务器上的复制是如何实现的。首先,可以查看sql/slave.cc中的handle_slave_sql()和handle_slave_io()函数。请注意,这两个函数都是用pthread_handler_decl()宏声明的,随意查看时可能容易忽略。第12章将更详细地讨论复制机制。

# server-id

这个选项为服务器分配一个数字ID,以便在网络中参与复制的对等服务器中识别该服务器。引入这个选项是基于以下情况。假设服务器A是服务器B的从服务器,而服务器B又是服务器C的从服务器,服务器C反过来又是服务器A的从服务器。服务器A上发生了一次更新,B从A的二进制日志中获取该更新,应用到自己的数据上,并将其记录到自己的二进制日志中。C从B的二进制日志中获取该更新,应用后再次记录到自己的二进制日志中。此时,A在C的二进制日志中看到了这个更新,它不应该再次应用这个更新。必须有某种方法告诉A,它在C的二进制日志中看到的更新最初来自A,因此应该被忽略。

解决方案是为每个参与复制的服务器分配一个唯一的32位ID,其概念类似于IP地址。每个二进制日志事件都会标记上产生它的服务器的ID。当从服务器应用从另一个服务器接收到的二进制日志事件时,它会使用日志事件记录中的服务器ID进行记录,而不是使用自己的ID。如果它发现事件中的ID是自己的,就不会应用该事件(除非启用了replicate-same-server-id选项)。这就打破了环形复制拓扑结构中可能出现的无限更新循环。

要了解更多关于这个选项的信息,可以在sql/log_event.h、sql/log_event.cc和sql/slave.cc中搜索server_id。

# skip-grant-tables

这个选项告诉服务器在启动时不加载访问权限表。这意味着两件事:第一,这些表无需存在;第二,由于不使用这些表,服务器会无条件地验证任何能够与它建立连接的主机提供的任何一组凭证。

当你忘记MySQL root用户密码时,这个选项特别有用。你可以使用skip-grant-tables选项启动服务器,连接到它,使用SQL语句手动编辑权限表,然后执行FLUSH PRIVILEGES命令,或者直接重启服务器。

出于安全考虑,建议你在使用skip-grant-tables选项时同时使用skip-networking选项。否则,网络上任何能够访问你MySQL端口的人都将拥有对服务器的无限制访问权限。如果你采取了这个安全预防措施,仅执行FLUSH PRIVILEGES命令不足以使服务器恢复正常模式,需要使用常规选项重启服务器才能启用网络连接。

当你想要部署一个最小化安装的服务器时,这个选项也很有用。通过消除对权限表的需求,你可以仅使用mysqld和errmsg.sys这两个文件来运行一个MySQL服务器实例。

正如预期的那样,源代码中对这个选项的处理相当简单。如果设置了该选项,在初始化时会调用sql/sql_acl.cc中的acl_init()函数,并将第二个参数设置为1,这会使它跳过读取权限表的操作。此外,sql/sql_acl.cc中的grant_init()函数根本不会被调用,这使得初始化标志(sql/sql_acl.cc中的一个静态变量)保持为0。acl_getroot()函数是用户认证查找的入口点,它会短路并返回0(表示成功),同时设置thd->master_access中的所有访问位。这使得客户端可以无限制地访问服务器功能。

# skip-stack-trace

无论你多么努力避免,崩溃总是会发生。拥有正确的调试信息对于确保同类型的崩溃不再发生至关重要。MySQL用户在收集此类信息时一直面临困难。现在已经做出了一些努力来帮助他们创建有意义的错误报告。

在x86或Alpha Linux系统上,MySQL服务器二进制文件在接收到诸如SIGSEGV之类的致命信号时,能够展开自身的堆栈并打印堆栈跟踪信息。此外,崩溃后的诊断消息还包括当时正在执行的查询以及最有可能导致崩溃的变量设置。尽管在大多数情况下这些报告的值是可靠的,但你还是应该对其持一定的怀疑态度。如果服务器已经崩溃,内存很可能已经严重损坏,导致报告的数据完全不可信。

在很多情况下,这个报告对于发现各种错误非常有帮助,包括那些只在某些生产环境中出现、在其他情况下无法复现的错误。

默认情况下,当接收到致命信号时会进行堆栈跟踪;然而,有时这并不理想(例如,当你试图在调试器中调试崩溃问题时)。这个选项可以关闭崩溃后的自我诊断功能。

堆栈跟踪代码可以在sql/stacktrace.c中找到,入口点是print_stacktrace()函数。

# slave-skip-errors

从服务器的复制算法最初设计为:如果在从服务器上执行复制的查询失败时遇到错误,就停止复制。实际上,如果一个查询在主服务器上执行成功,并且从服务器的数据与主服务器执行成功时的数据相同,那么从服务器上就不应该出现执行失败的情况。如果出现失败,理想情况下,你会希望停止复制,让数据库管理员(DBA)手动检查以验证数据的完整性。

然而,在许多情况下,这种方法并不理想。在实际应用中,大多数使用MySQL的应用程序都具有高度的记录隔离性。换句话说,尽管一个表可能包含数百万条记录,但如果其中一条记录不正确或完全缺失,这个问题可以手动修复,甚至可以直接忽略。在这些情况下,对复制来说,及时推进比从服务器数据与主服务器数据完全一致更为重要。如果这是优先考虑的因素,那么在复制继续进行时,诸如重复键错误之类的错误可以直接忽略。

这个选项告诉从服务器应该忽略哪些错误代码。可以以逗号分隔的列表形式指定要忽略的错误代码,也可以使用关键字all来忽略所有错误。

要了解这个选项的工作原理,可以查看sql/log_event.cc中的Query_log_event::exec_event()和ignored_error_code()函数。

当需要对记录进行排序时,MySQL会使用一种在MySQL术语中称为filesort的算法。记录集被分成多个块,每个块使用基数排序进行排序。如果有多个块,每个已排序的块在与已排序的集合合并时会被写入一个临时文件。通过这种方式,我们可以兼顾基数排序的速度和对大量记录进行排序的能力。

# sort_buffer_size

这个选项通过指定基数排序允许使用的内存量,间接控制在内存中使用基数排序进行排序的块的大小。

要了解更多关于filesort算法及其实现的信息,可以查看sql/filesort.cc。

# sql-mode

尽管ANSI SQL是一种标准,但大多数数据库(包括MySQL)在默认设置下运行时,对它的实现都会有一些差异,更糟糕的是,它们彼此之间还不能兼容这些差异。这在移植应用程序时可能会带来问题。

这个选项就像是一个“口音调节器”。通过将其设置为不同的值,你可以告诉MySQL:REAL是FLOAT的别名而不是DOUBLE;数据库名和表名之间允许有空格;||表示字符串连接而不是逻辑或。以及进行其他一些调整,以便在不修改代码的情况下,将应用程序从其他数据库移植到MySQL。

代码中有很多地方会受到这个选项的影响。要熟悉它的工作原理,可以做以下几件事:

  • 查看sql/mysqld.cc中sql_mode_names变量的定义。
  • 查看sql/set_var.cc中的fix_sql_mode()函数。
  • 在sql/sql_yacc.yy、sql/sql_show.cc、sql/sql_parse.cc和sql/sql_lex.cc中搜索sql_mode。

# table_cache

table_cache是MySQL代码的核心部分之一。它缓存表描述符,这大大提高了查询速度。每次在查询中引用一个表时,如果表缓存中已经有了所需的描述符,就无需进行初始化这个开销较大的操作。

这个选项用于控制可以同时缓存多少个表描述符(而不是表!)。你可以使用SHOW OPEN TABLES命令查看表缓存的内容。

要了解更多关于表缓存的信息,可以查看sql/sql_base.cc中的open_table()函数,并跟踪其执行过程。

# temp-pool

这个选项专门用于解决Linux内核(至少在2.4版本)中的一个设计缺陷。当一个进程反复创建和删除具有唯一名称的文件时,内核最终会分配大量内存,并且不会释放这些内存。MySQL有时需要创建临时文件来处理查询。在一个流量大、查询种类繁多的大型网站上,这种情况可能会频繁发生,从而引发严重问题。对于大多数用户来说,直到MySQL在一个负载极高、有大量复杂查询频繁执行的网站上使用时,这个问题才凸显出来。MySQL开发者通过添加一个选项来解决这个问题,该选项将临时表的名称可能性限制在一个较小的集合内。

要了解这个选项的工作原理,可以查看sql/select.cc中create_tmp_table()函数的开头部分。

# transaction-isolation

这个选项主要是因为InnoDB被引入MySQL代码库而产生的。当两个或多个不同的事务并行发生时,如果某些数据已被另一个事务写入但尚未提交,那么对于读操作应该返回什么数据,存在几种不同的模型或规则集。这组规则被称为事务隔离级别(transaction isolation level)。

许多事务引擎,包括InnoDB,都为用户提供了为给定事务选择所需事务隔离级别的选项。这个选项允许你为整个服务器设置一个全局事务隔离级别。

要了解更多关于这个选项的工作原理,可以研究innobase/row/row0sel.c中的row_sel_get_clust_rec_for_mysql()函数。

上次更新: 2025/04/08, 19:40:35
第四章 客户端/服务器通信
第六章 基于线程的请求处理

← 第四章 客户端/服务器通信 第六章 基于线程的请求处理→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式