社区投稿 | 线程简介和 MySQL 调试环境搭建

做者:高鹏 文章末尾有他著做的《深刻理解MySQL主从原理 32讲》,深刻透彻理解MySQL主从,GTID相关技术知识。html

本文节选自《深刻理解MySQL主从原理》第29节 注意:本文分为正文和附件两部分,都是图片格式,若是正文有图片不清晰能够将附件的图片保存到本地查看。mysql

背景

我想简单说一下个人 MySQL 调试环境的搭建,可是在此以前不得不简单说一下什么是线程,由于若是不解释一下什么是线程,简单的调试可能都会有阻碍,同时了解线程对咱们普通DBA诊断性能问题也有极大的帮助。可是详细解释线程已经超出了个人能力范围也超出了本系列讨论的范围。 具体我推荐给你们两本书:linux

  • 《POSIX 多线程程序设计》
  • 《Linux UNIX 系统编程手册》 第29到32章 第一本书很老了,可是我以为还能够,若是有兴趣能够参考一下。

1、线程简介

咱们知道 mysqld 是一个单进程多线程的用户程序,所以咱们有必要了解一下什么线程。实际上 MySQL 中的线程都是 POSIX 线程,好比咱们的会话线程、DUMP 线程、IO 线程以及其余一些 InnoDB 线程都是 POSIX 线程。c++

进程实际上就是运行中的程序,一个进程中能够包含多个线程,也能够只包含一个线程。在 Linux 中线程也叫轻量级进程(light-weight process)简称为 LWP,进程的第一个线程一般称为主控线程。进程是内存分配的最小单位,线程是 CPU 调度的最小单位,也就是说若是 CPU 有足够多核,那么多个线程能够达到并行处理的效果,内核直接调度线程。下面是我学习 Linux 线程的时候看到的一张我认为比较好理解的图,我从新画了一下放在下面供你们参考(图29-1,高清原图包含在文末原图中):redis

一个进程内部的全部线程都拥有相同的代码程序、堆、全局变量、共享库等,可是每一个线程拥有本身独立栈空间和寄存器,他们共享进程的虚拟内存地址空间。下面咱们假定是32位操做系统下的 mysqld 进程,它的进程虚拟内存地址示意图以下,实际上这个图也是参考《Linux UNIX 系统编程手册》画的(图29-2,高清原图包含在文末原图中):sql

咱们发现线程的堆内存和全局变量是共享的,所以线程之间数据共享很轻松,可是要控制好这些共享内存就须要引入咱们的线程同步技术,好比咱们常说的 Mutex。数据库

若是想了解线程到底共享了哪些资源,线程和进程到底各有什么优点和劣势,可执行参考上面我给出的书籍。编程

2、PID、LWP ID、Thread TID

若是要进行调试就须要了解这三种 ID,其中 PID 和 LWP ID 是比较重要,由于不论是调试和运维都会遇到它们,而 Thread TID 不作多线程开发通常不多用到。 下面是我对它们的总结:多线程

  • PID:内核分配,用于识别各个进程的 ID。这个应该是你们最熟悉的。
  • LWP ID:内核分配,用于识别各个线程的 ID,它就像是线程是‘PID’同样。同一个进程下的全部线程有相同的 PID,可是 LWP ID 却不同,主控线程的 LWP ID 就是进程 PID。
  • Thread TID:进程内部用于识别各个线程的内部 ID,这个 ID 用得很少。

下面我写了一个简单的 C 测试程序仅仅用于观察这些 ID,它是经过主控线程再建立一个线程,也就是说这个进程包含了两个线程。它们分别打印本身的 PID、LWP ID、Thread TID,而后作一个循环自加操做引发高 CPU 消耗现象便于观察。而后咱们使用 Linux 的 top -H 和 ps -eLlf 命令分别进行观察。运维

  1. 程序输出

下面是程序的输出:

# ./gaopengtest 
main thread: pid 13188 tid 2010470144 lwp 13188
new thread:  pid 13188 tid 2010461952 lwp 13189

咱们能够看到两个线程的 PID 都是 13188,可是主控线程的 LWP ID 是 13188,新创建的一个线程 LWP ID 是 13189。而后就是 Thread TID 了,不过多解释。

  1. top -H 观察以下:

咱们能够看到这两个线程都是高耗 CPU 的线程,CPU 已经处于饱和状态。这里咱们看到 top -H 命令的输出中‘PID’就是 LWP ID。这里咱们还能看它们的内存信息彻底一致,内存信息其实是整个进程的内存信息,由于进程是内存分配的最小单位嘛。

注意若是这个时候查看这个进程占用的 %cpu 就超过了 100%,接近 200%,以下:

  1. ps -eLlf 观察以下:

咱们能够看到这里包含了 PID 和 LWP ID,不过多解释了,你们能够本身去试试。

3、如何将 MySQL 的线程和 LWP ID 进行对应

在 5.7 中咱们已经能够经过 MySQL 语句和 LWP ID 进行对应了,这让性能诊断变得更加便捷。好比我上面的列子若是两个高耗 CPU 的线程是 MySQL 的线程那么咱们就拿到了线程的 LWP ID,而后能够经过语句找到这两个线程究竟是 MySQL 的什么线程。语句以下:

mysql> select a.thd_id,b.THREAD_OS_ID,a.user 
,b.TYPE from  sys.processlist 
a,performance_schema.threads  b where b.thread_id=a.thd_id;
+--------+--------------+---------------------------+---------+------------
| thd_id | THREAD_OS_ID | user                      | conn_id | TYPE       
+--------+--------------+---------------------------+---------+------------
|      1 |        16370 | sql/main                  |    NULL | BACKGROUND 
|      2 |        17202 | sql/thread_timer_notifier |    NULL | BACKGROUND 
|      3 |        17207 | innodb/io_ibuf_thread     |    NULL | BACKGROUND 
|      4 |        17208 | innodb/io_log_thread      |    NULL | BACKGROUND 
|      5 |        17209 | innodb/io_read_thread     |    NULL | BACKGROUND 
|      6 |        17210 | innodb/io_read_thread     |    NULL | BACKGROUND 
|      7 |        17211 | innodb/io_read_thread     |    NULL | BACKGROUND 
|      8 |        17212 | innodb/io_read_thread     |    NULL | BACKGROUND 
|      9 |        17213 | innodb/io_read_thread     |    NULL | BACKGROUND 
|     10 |        17214 | innodb/io_read_thread     |    NULL | BACKGROUND 
|     11 |        17215 | innodb/io_read_thread     |    NULL | BACKGROUND 
|     12 |        17216 | innodb/io_read_thread     |    NULL | BACKGROUND 
|     13 |        17217 | innodb/io_write_thread    |    NULL | BACKGROUND 
|     14 |        17218 | innodb/io_write_thread    |    NULL | BACKGROUND 
|     15 |        17219 | innodb/io_write_thread    |    NULL | BACKGROUND 
|     16 |        17220 | innodb/io_write_thread    |    NULL | BACKGROUND 
|     17 |        17221 | innodb/io_write_thread    |    NULL | BACKGROUND 
......

这里的 THREAD_OS_ID 就是线程的 LWP ID。而后咱们使用刚才的 ps -eLlf 命令再看一下,以下:

咱们能够发现他们是能够对应上的,这个最好本身实际试试就知道了。

4、调试环境搭建

调试环境的搭建我认为无论使用什么方法只要可以起到调试的做用就能够了。这里介绍一下个人方法。我是在 Linux 下面直接使用 gdb 调试的,我以为这个方法搭建很是简单而且很奏效,基本上只要会源码安装就能完成调试环境的搭建。下面来看看步骤:

1. 第一步下载 MySQL 源码包,解压。

我特地从新下载了官方版的 5.7.26 源码。

2. 使用源码安装的方法安装 MySQL,注意须要开启debug选项

下面是我使用的选项:

cmake -DCMAKE_INSTALL_PREFIX=/root/sf/mysql3312/ \
-DMYSQL_DATADIR=/root/sf/mysql3312/data/ \
-DSYSCONFDIR=/root/sf/mysql3312/ \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_ARCHIVE_STORAGE_ENGINE=1 \
-DWITH_BLACKHOLE_STORAGE_ENGINE=1 \
-DWITH_FEDERATED_STORAGE_ENGINE=1 \
-DWITH_PARTITION_STORAGE_ENGINE=1 \
-DMYSQL_UNIX_ADDR=/root/sf/mysql3312/mysql3312.sock \
-DMYSQL_TCP_PORT=3306 \
-DENABLED_LOCAL_INFILE=1 \
-DEXTRA_CHARSETS=all \
-DDEFAULT_CHARSET=utf8 \
-DDEFAULT_COLLATION=utf8_general_ci \
-DMYSQL_USER=mysql \
-DWITH_BINLOG_PREALLOC=ON \
-DWITH_BOOST=/root/sf/mysql-5.7.26/boost/boost_1_59_0 -DWITH_DEBUG=1

注意最后的 -DWITH_DEBUG=1 必须开启。

3. make && make install

编译和安装。

4. 准备参数文件和初始化 MySQL 数据库,而且确保能成功启动

这一步本身处理,注意权限。个人环境启动成功了以下:

[root@gp1 support-files]# ./mysql.server start
Starting MySQL....... SUCCESS! 
[root@gp1 support-files]# ./mysql.server stop
Shutting down MySQL.. SUCCESS!
5. 准备 gdb 命令文件

以下是我准备的命令文件:

[root@gp1 ~]# more debug.file 
break main
run --defaults-file=/root/sf/mysql3312/my.cnf --user=mysql --gdb

第一行是在 main 函数处打一个断点。第二行就是 gdb 调用 mysqld 的时候,mysqld 加什么参数了,注意 run 不要写掉了。

6.使用 gdb 启动 MySQL

使用以下命令启动调试环境:

gdb -x /root/debug.file /root/sf/mysql3312/bin/mysqld

下面就是我启动调试环境成功的记录:

# gdb -x /root/debug.file /root/sf/mysql3312/bin/mysqld
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-92.el6)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/sf/mysql3312/bin/mysqld...done.
Breakpoint 1 at 0xec7c53: file /root/sf/mysql-5.7.26/sql/main.cc, line 25.
[Thread debugging using libthread_db enabled]

Breakpoint 1, main (argc=5, argv=0x7fffffffe3b8) at /root/sf/mysql-5.7.26/sql/main.cc:25
25        return mysqld_main(argc, argv);
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.212.el6.x86_64 libaio-0.3.107-10.el6.x86_64 libgcc-4.4.7-18.el6.x86_64 libstdc++-4.4.7-18.el6.x86_64 nss-softokn-freebl-3.14.3-23.3.el6_8.x86_64
(gdb) c
Continuing.
[New Thread 0x7fffee883700 (LWP 29375)]
[New Thread 0x7fff9a9f3700 (LWP 29376)]
[New Thread 0x7fff99ff2700 (LWP 29377)]
[New Thread 0x7fff995f1700 (LWP 29378)]
[New Thread 0x7fff98bf0700 (LWP 29379)]
[New Thread 0x7fff981ef700 (LWP 29380)]
[New Thread 0x7fff977ee700 (LWP 29381)]
[New Thread 0x7fff96ded700 (LWP 29382)]
[New Thread 0x7fff963ec700 (LWP 29383)]
.....

注意到了这里的 LWP ID 了吗,前面咱们已经讨论过了。这个时候 MySQL 客户端程序已经能够链接 mysqld 了以下:

# /root/sf/mysql3312/bin/mysql -S'/root/sf/mysql3312/mysql3312.sock'
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.26-debug-log Source distribution

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select version() ;
+------------------+
| version()        |
+------------------+
| 5.7.26-debug-log |
+------------------+
1 row in set (0.00 sec)

好了到这里基本的调试环境就搭建起来了,咱们能够发现很简单。以后咱们就能够进行断点调试了。 我经常使用的 gdb 命令包含:

  • info threads:查看所有线程
  • thread n:指定某个线程
  • bt:查看某个线程栈帧
  • b:设置断点
  • c:继续执行
  • s:执行一行代码,若是代码函数调用,则进入函数
  • n:执行一行代码,函数调用不进入
  • p:打印某个变量值
  • list:打印代码的文本信息

固然 gdb 还有不少命令,可自行参考其余资料。

5、使用调试环境证实问题的一个列子

这里咱们就用一个例子来看看调试环境的使用方法。咱们前面第 15 节说过 binlog cache 是在 order commit 的 flush 阶段才写入到 binary log 的,调用的是函数 binlog_cache_data::flush。好了咱们能够将断点打到这个函数以下:

(gdb) b binlog_cache_data::flush
Breakpoint 2 at 0x1846333: file /root/sf/mysql-5.7.26/sql/binlog.cc, line 1674.

而后咱们在 MySQL 客户端执行一个事务以下,而且提交:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into gpdebug values(1);
Query OK, 1 row affected (0.03 sec)	
mysql> commit;

commit的时候已经卡主了,断点触发以下:

Breakpoint 2, binlog_cache_data::flush (this=0x7fff3c00df20...)
    at /root/sf/mysql-5.7.26/sql/binlog.cc:1674
1674      DBUG_ENTER("binlog_cache_data::flush");

咱们使用 bt 命令查看栈帧发现以下:

#0  binlog_cache_data::flush
at /root/sf/mysql-5.7.26/sql/binlog.cc:1674
#1  0x0000000001861b41 in binlog_cache_mngr::flush 
at /root/sf/mysql-5.7.26/sql/binlog.cc:967
#2  0x00000000018574ce in MYSQL_BIN_LOG::flush_thread_caches 
at /root/sf/mysql-5.7.26/sql/binlog.cc:8894
#3  0x0000000001857712 in MYSQL_BIN_LOG::process_flush_stage_queue 
at /root/sf/mysql-5.7.26/sql/binlog.cc:8957
#4  0x0000000001858d19 in MYSQL_BIN_LOG::ordered_commit 
at /root/sf/mysql-5.7.26/sql/binlog.cc:9595
#5  0x00000000018573b4 in MYSQL_BIN_LOG::commit
at /root/sf/mysql-5.7.26/sql/binlog.cc:8851
#6  0x0000000000f58de9 in ha_commit_trans 
at /root/sf/mysql-5.7.26/sql/handler.cc:1799
#7  0x000000000169e02b in trans_commit 
at /root/sf/mysql-5.7.26/sql/transaction.cc:239
......

好了看到这个栈帧,就能证实咱们的说法了,若是想深刻学习代码就能够从这个栈帧出发进行学习。可是值得注意的是,这是创建在知道函数接口功能的前提下的,若是咱们不知道写入 binary log 会调用 binlog_cache_data::flush 函数那么调试也就很差进行了,我就常常遇到这样的困境。所以整个系列我给出了不少这样的接口,供有兴趣的朋友调试和测试。


最后推荐高鹏的专栏《深刻理解MySQL主从原理 32讲》,想要透彻了解学习MySQL 主从原理的朋友不容错过。

相关文章
相关标签/搜索