天天看點

第29節:線程簡介和MySQL調試環境搭建

最後我想簡單說一下我的MySQL調試環境的搭建,但是在此之前不得不簡單說一下什麼是線程,因為如果不解釋一下什麼是線程,簡單的調試可能都會有阻礙,同時了解線程對我們普通DBA診斷性能問題也有極大的幫助。但是詳細解釋線程已經超出了我的能力範圍也超出了本系列讨論的範圍,具體我推薦給大家兩本書:

  • 《POSIX多線程程式設計》
  • 《Linux UNIX系統程式設計手冊》 第29到32章

第一本書很老了,但是我覺得還可以,如果有興趣可以參考一下。

一、線程簡介

我們知道MySQLD是一個單程序多線程的使用者程式,是以我們有必要了解一下什麼線程。實際上MySQL中的線程都是POSIX線程,比如我們的會話線程、DUMP線程、IO線程以及其他一些Innodb線程都是POSIX線程。

Linux中線程也叫輕量級程序(light-weight process)簡稱為LWP,一個程序中可以包含多個線程也可以隻包含一個線程,程序的第一個線程通常稱為主要線程。程序是記憶體配置設定的最小機關,線程是CPU排程的最小機關,也就是說如果CPU有足夠多核,那麼多個線程可以達到并行處理的效果,核心直接排程線程。下面是我學習Linux線程的時候看到的一張我認為比較好了解的圖,我重新畫了一下放在下面供大家參考:

一個程序内部的所有線程都擁有相同的代碼程式、堆、全局變量、共享庫等,但是每個線程擁有自己獨立棧空間和寄存器,他們共享程序的虛拟記憶體位址空間。下面我們假定是32位作業系統下的MySQLD程序,它的程序虛拟記憶體位址示意圖如下(實際上這個圖也是參考《Linux UNIX系統程式設計手冊》畫的):

我們發現線程的堆記憶體和全局變量是共享的,是以線程之間資料共享很輕松,但是要控制好這些共享記憶體就需要引入我們的線程同步技術,比如我們常說的Mutex。

如果想了解線程到底共享了哪些資源,線程和程序到底各有什麼優勢和劣勢,可執行參考上面我給出的書籍。

二、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用得不多。

下面我寫了一個簡單的測試程式僅僅用于觀察這些ID,它是通過主要線程再建立一個線程,也就是說這個程序包含了兩個線程。它們分别列印自己的PID、LWP ID、Thread TID,然後做一個循環自加操作引起高CPU消耗現象便于觀察。然後我們使用Linux的top -H和ps -eLlf指令分别進行觀察。

  1. 程式輸出

下面是程式的輸出:

[root@mysqltest2 testc]# ./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,不過多解釋了,大家可以自己去試試。

三、如何将MySQL的線程和LWP ID進行對應

在5.7中我們已經可以通過MySQL語句和LWP ID進行對應了,這讓性能診斷變得更加便捷。比如我上面的列子如果兩個高耗CPU的線程是MySQL的線程那麼我們就拿到了線程的LWP ID,然後可以通過語句找到這兩個線程到底是MySQL的什麼線程。語句如下:

mysql> select a.thd_id,b.THREAD_OS_ID,a.user ,a.conn_id,
       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     
......           

這裡的THREAD_OS_ID就是線程的LWP ID。然後我們使用剛才的ps -eLlf指令再看一下,如下:

# ps -eLlf|grep mysql3340
4 S root     16358 12736 16358  0    1  80   0 - 118443 rt_sig 09:34 pts/1  
4 S mysql    16370 16358 16370  0   48  80   0 - 1406575 poll_s 09:34 pts/1 
1 S mysql    16370 16358 17202  0   48  80   0 - 1406575 rt_sig 09:39 pts/1 
1 S mysql    16370 16358 17207  0   48  80   0 - 1406575 read_e 09:39 pts/1 
1 S mysql    16370 16358 17208  0   48  80   0 - 1406575 read_e 09:39 pts/1 
1 S mysql    16370 16358 17209  0   48  80   0 - 1406575 read_e 09:39 pts/1 
......           

我們可以發現他們是可以對應上的,這個最好自己實際試試就知道了。

四、調試環境搭建

調試環境的搭建我認為不管使用什麼方法隻要能夠起到調試的作用就可以了。這裡介紹一下我的方法。我是在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還有很多指令,可自行參考其他資料。

六、使用調試環境證明問題的一個列子

這裡我們就用一個例子來看看調試環境的使用方法。我們前面第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函數那麼調試也就不好進行了,我就經常遇到這樣的困境。是以整個系列我給出了很多這樣的接口,供有興趣的朋友調試和測試。

第29節結束

繼續閱讀