新开坑,计划做一系列专辑。由于 MySQL 源码太庞大,不可能面面俱到,先从丁奇《MySQL 实战 45 讲》[1] 案例开始入手,case by case 来做分享。同时强烈推荐丁奇的课,真的是受益匪浅,感谢感谢~~
最新版本己经是 MySQL 8.0 了,和我当初使用 5.5 差距非常大,增加很多实用功能也做了很多优化。对于我个人来讲,很陌生,也是重新学习的过程。
案例
本次分享一个 SQL 是如何执行的,先看这个案例,如果访问不存在的字段,那么报错是在 MySQL 哪个层呢?
select a,b from mytest.test where k =1;
另外刚毕业时面试百度,问我 mvcc
是在哪一层的,直接就蒙逼了:(
整体架构
mysql arch
提到 MySQL 架构这个图是必看的,为了支持不同的存储引擎,分为两层:Server 层和存储引擎层。做过 DBA 的都知道,redo, undo log 这些是事务相关的,都属于引擎层,具体就是 innodb 的,而 binlog 是所有引擎通用的,所以在 Server 层。目前 MySQL 大部分公司只会用过 innodb, 并且也是默认引擎。
tidb arch
同时我们也可以看下最近几年新兴的 TiDB[2] , 整体分层结构并无太大区别,但是因为底层引擎是分布式的 TiKV[3], 所以生成执行计划与执行器执行还是有很大区别的,并且执行时还要考滤 rpc 超时等等。
Server
Server 层具体包括连接器,查询缓存,分析器,优化器与执行器。具体看一下每个模块的作用,后面会用 debug 方式查看
1. 连接器
负责处理新建连接,用户名密码认证,接收发送请求等功能。在 thread_handling
默认是 one-thread-per-connection
一个新建连接会新启动一个线程。
root@myali:~/mysql# ps -T -p 10477 | wc -l
28
root@myali:~/mysql# bin/mysql -uroot -h127.0.0.1
mysql> show full processlist;
+----+------+-----------------+------+---------+------+----------+-----------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------------+------+---------+------+----------+-----------------------+
| 2 | root | localhost:58506 | NULL | Query | 0 | starting | show full processlist |
+----+------+-----------------+------+---------+------+----------+-----------------------+
1 row in set (0.00 sec)
mysql> exit
Bye
root@myali:~/mysql# ps -T -p 10477 | wc -l
29
可以看到线程个数随之变化,当然 MySQL 这么做肯定低效,所以开启线程池或是线程复用 thread_cache_size
, 为了测试我把这个参数关掉了。
以前在 ganji 的时候,业务 php 经常会有连接失败的情况,是个不错的分析 case 啊:(
2. 查询缓存
很古老的东西了,最新版本也己废弃,原因在于效率太低,Query_cache 属于表级别的,任何更新都会使之失效。
3. 分析器
以前在 ganji 做 SQL 自动上线[4] 时就用到了这块技术,使用 lex&yacc
技术将一条 sql 解析成 ast 抽象语法树,然后对之进行 sql 审核后再上线。基本大公司的上线平台也是同样套路。
至于 MySQL 这块也差不多,将 sql 识别成 MySQL 认可的语法,然后交给下一层去执行。据说 TiDB 的 sql parser 实现效率最高,很多 go 的库也都在使用,以后有机会我也试用下。
4. 优化器
经过 parser 后的 SQL,就知道要获取什么样的数据了,但是我们知道 SQL 是一种声明式语言,不会指导数据库如何去获取数据,那么在执行器执行前,就要经过优化器去分析一波,选择哪个索引合适。这一块 MySQL 和 TiDB 区别还是很大的,以后有机会再细看。
5. 执行器
MySQL 通过分析器知道 client 要做什么,通过优化器知道 MySQL 该怎么做,于是就进入了执行器阶段,开始执行语句。
这一层会对用户是否有查询权限进行校验,如果没有返回报错。有的话调用 Innodb 引擎层接口获取数据,循环获取满足条件的行数据。
案例
回到文章开头的案例,如果字段不存在肯定报错,这个属于哪层呢?
cmake . -DCMAKE_INSTALL_PREFIX=/root/my-mysql8 \
-DMYSQL_DATADIR=/root/my-mysql8/data \
-DWITH_BOOST=/root/my-mysql8/boost/boost_1_70_0 \
-DSYSCONFDIR=/etc \
-DEFAULT_CHARSET=utf8mb4 \
-DDEFAULT_COLLATION=utf8mb4_general_ci \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_DEBUG=1 \
-DSYSCONFDIR=/root/my-mysql8/etc \
-DEXTRA_CHARSETS=all
先编译 MySQL,记住一定要加 WITH_DEBUG
选项
nohup sh mysql.server start --debug &
然后启动并打开调式模式。
bin/mysql -uroot -h127.0.0.1 -e "select * from mytest.test where a=1"
trace 文件在 /tmp/mysqld.trace
, 我们直接截取 sql 执行的报错部分展示出来
T@3: | | | | >handle_query
T@3: | | | | | THD::enter_stage: 'init' /root/mysql-5.7.23/sql/sql_select.cc:121
T@3: | | | | | >PROFILING::status_change
T@3: | | | | | <:status_change>T@3: | | | | | >SELECT_LEX::prepare
T@3: | | | | | | opt: (null): starting struct
T@3: | | | | | | opt: join_preparation: starting struct
T@3: | | | | | | opt: select#: 1
T@3: | | | | | | >SELECT_LEX::setup_conds
T@3: | | | | | | | info: thd->mark_used_columns: 1
T@3: | | | | | | | >find_field_in_table_ref
T@3: | | | | | | | | enter: table: 'test' field name: 'k' item name: 'k' ref 0x7fd620007aa8
T@3: | | | | | | | | >find_field_in_table
T@3: | | | | | | | | | enter: table: 'test', field name: 'k'
T@3: | | | | | | | | T@3: | | | | | | | T@3: | | | | | | | >find_field_in_table_ref
T@3: | | | | | | | | enter: table: 'test' field name: 'k' item name: 'k' ref 0x7fd620007aa8
T@3: | | | | | | | | >find_field_in_table
T@3: | | | | | | | | | enter: table: 'test', field name: 'k'
T@3: | | | | | | | | T@3: | | | | | | | T@3: | | | | | | | >check_grant_column
T@3: | | | | | | | | enter: table: test want_privilege: 1
T@3: | | | | | | | T@3: | | | | | | | >my_error
T@3: | | | | | | | | my: nr: 1054 MyFlags: 0 errno: 2
T@3: | | | | | | | | >my_message_sql
T@3: | | | | | | | | | error: error: 1054 message: 'Unknown column 'k' in 'where clause''
T@3: | | | | | | | | | >mysql_audit_acquire_plugins
T@3: | | | | | | | | |
可以看到字段不存在的报错是在 mysql_parse
之后,在 prepare
函数里做的检查,这一块属于 optimize
优化器层前的预处理
另外非常有意思的是,字段不存在的报错竟然是在检查权限 check_grant_column
函数中。感兴趣的可以自行 debug 看下 trace 文件,可以帮助快速定位源码与流程。
select 完整流程
通过 trace 文件追踪了 mysql 源码,大致清楚了整体流程。比较关键的数据结构是 THD
, 每个连接一个实例。这个结构光定义就 3300 行代码,还不包括方法实现... 囧:(
login_connection
处理新连接,Global_THD_manager::add_thd
生成 THD,acl_authenticate
用户认证dispatch_command
分发命令mysql_parse
分析器,主要就是词法解析lex_start
和语法解析parse_sql
check_access
检查 DB 权限,open_tables_for_query
打开 tablehandle_query
主要就是prepare
和optimize
exec
调用引擎接口获取数据,然后sending_data
给客户端Global_THD_manager::remove_thd
连接退出
小结
这次分享参考了《MySQL 实战 45 讲》[1] 第一节,评论的内容也更精彩,推荐大家围观。以后面还会分享更多关于 MySQL 的内容,如果感兴趣,可以关注并转发(:
参考资料
[1]《MySQL 实战 45 讲》: https://time.geekbang.org/column/intro/139,
[2]tidb 源码分析: https://pingcap.com/blog-cn/tidb-source-code-reading-2/,
[3]tikv 是什么: https://pingcap.com/blog-cn/#TiKV,
[4]sql 自动上线: https://myslide.cn/slides/9070,