MariaDB原始碼除錯

ali清英發表於2016-03-31

作者:王成瑞 南京華泰證券資訊科技部架構師 2837796568@@qq.com
MariaDB 原始碼編譯

[root@jg-72 source]# pwd
 /data/source
 [root@jg-72 source]# ls
 mariadb-10.1.11.tar.gz

先將原始碼壓縮包解壓縮
tar -zxvf mariadb-10.1.11.tar.gz

進入到BUILD子目錄,它已經提供了一些一鍵編譯的指令碼
cd mariadb-10.1.11/BUILD

選擇執行 compile-amd64-debug-all 指令碼,因為我們要編譯X86_64平臺上帶DEBUG除錯資訊的mysql server。

[root@jg-72 BUILD]# ./compile-amd64-debug-all
You must run this script from the MySQL top-level directory

cd mariadb-10.1.11
BUILD/compile-amd64-debug-all

 

靜靜等待編譯結束,大概幾分鐘

mariadb_1

可以看到編譯生成的mysqld 檔案大小 261 M,而實際生產環境中使用的不帶除錯資訊的mysqld檔案,只有約89M。

client 和 extra目錄下的可執行檔案是編譯生成的mysql自帶的工具集,這裡我們只關注mysqld。

使用編譯的mysqld啟動mysql例項

最簡單的方法,在已安裝好的mariadb的安裝目錄下,把mysqld用編譯出來的版本替換掉即可。

但是我們不想破壞已經安裝好的用於生產的MariaDB,所以這裡我們在它的原始碼目錄下構建屬於它自己的mysql 基礎目錄(mysql basedir)

實際上就是參照安裝的mysql的目錄結構組織一下檔案即可

把編譯生成的可執行檔案都copy到建立的bin目錄下
(下面所有操作都是在MariaDB的原始碼根目錄下)
[root@jg-72 mariadb-10.1.11]# mkdir bin
cp sql/mysqld bin/
cp scripts/mysqld_safe bin/
cp sql/{add_errmsg,gen_lex_hash,gen_lex_token,mysql_tzinfo_to_sql} bin/
cp client/{async_example,mysqlbinlog,mysql,mysqladmin,mysqlcheck,mysqldump,mysql_plugin,mysqlslap,mysql_upgrade,mysqltest,mysqlshow,mysqlimport} bin/
cp extra/{comp_err,my_print_defaults,mysql_waitpid,perror,replace,resolveip,resolve_stack_dump} bin/

cp -r sql/share/ .
cp scripts/*.sql share/

建立一個 my.cnf 檔案, 其中basedir 設定為MariaDB原始碼的根目錄

[mysqld]
user=mysql
port = 3310
basedir = /data/source/mariadb-10.1.11
socket = /data/source/3310/mysql.sock
datadir = /data/source/3310/data
log-error = /data/source/3310/mysqld.err
pid-file = /data/source/3310/mysqld.pid
character-set-server=utf8
… …

然後,初始化MySQL系統資料庫,可以看到,它使用我們編譯的mysqld來做初始化

[root@jg-72mariadb-10.1.11]# scripts/mysql_install_db –defaults-file=/data/source/3310/my.cnf –user=mysql
Installing MariaDB/MySQL system tables in ‘/data/source/3310/data’ …
2016-02-25 0:58:35 139708455159584 [Note] /data/source/mariadb-10.1.11/bin/mysqld (mysqld 10.1.11-MariaDB-debug) starting as process 21624 …
OK
Filling help tables…
2016-02-25 0:59:00 140501225490208 [Note] /data/source/mariadb-10.1.11/bin/mysqld (mysqld 10.1.11-MariaDB-debug) starting as process 21664 …
OK
Creating OpenGIS required SP-s…
2016-02-25 0:59:07 140226519934752 [Note] /data/source/mariadb-10.1.11/bin/mysqld (mysqld 10.1.11-MariaDB-debug) starting as process 21703 …
OK
… …

啟動例項:
可以看到是使用我們編譯的mysqld啟動的
mariadb_2
[root@jg-72mariadb-10.1.11]# bin/mysqld_safe –defaults-file=/data/source/3310/my.cnf –user=mysql &
[1] 21808

[root@jg-72 mariadb-10.1.11]# ps -ef|grep mysql |grep 3310
root 21808 19360 0 01:04 pts/5 00:00:00 /bin/sh bin/mysqld_safe –defaults-file=/data/source/3310/my.cnf –user=mysql
mysql 22096 21808 28 01:04 pts/5 00:00:03 /data/source/mariadb-10.1.11/bin/mysqld –defaults-file=/data/source/3310/my.cnf –basedir=/data/source/mariadb-10.1.11 –datadir=/data/source/3310/data –plugin-dir=/usr/local/mysql/lib/plugin –user=mysql –log-error=/data/source/3310/mysqld.err –pid-file=/data/source/3310/mysqld.pid –socket=/data/source/3310/mysql.sock –port=3310

使用gdb除錯mysql程式

gdb 連線到要除錯的程式的命令很簡單,上面知道這個程式的id是22096

命令: gdb – 22096

Loaded symbols for /usr/lib64/libltdl.so.7
Reading symbols from /lib64/libfreebl3.so…(no debugging symbols found)…done.
Loaded symbols for /lib64/libfreebl3.so
Reading symbols from /lib64/libnss_files.so.2…(no debugging symbols found)…done.
Loaded symbols for /lib64/libnss_files.so.2
0x0000003893adf1b3 in poll () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install bzip2-libs-1.0.5-7.el6_0.x86_64 glibc-2.12-1.149.el6.x86_64 libaio-0.3.107-10.el6.x86_64 libgcc-4.4.7-11.el6.x86_64 libstdc++-4.4.7-11.el6.x86_64 libtool-ltdl-2.2.6-15.5.el6.x86_64 libxml2-2.7.6-14.el6_5.2.x86_64 nss-softokn-freebl-3.14.3-17.el6.x86_64 snappy-1.1.0-1.el6.x86_64 unixODBC-2.2.14-14.el6.x86_64 xz-libs-4.999.9-0.5.beta.20091007git.el6.x86_64 zlib-1.2.3-29.el6.x86_64
(gdb)

最後會看到如上的輸出,此時這個mysql程式已經被gdb掛起,用客戶端連線是沒有響應的。

設定函式斷點,這樣當mysql程式執行到這個函式的時候,就會被gdb捕獲到並且停在函式的入口處。

(gdb) b dict_index_too_big_for_tree
Breakpoint 1 at 0xdd291b: file /data/source/mariadb-10.1.11/storage/xtradb/dict/dict0dict.cc, line 2390.
(gdb)

輸入c 命令,讓程式正常執行
(gdb) c
Continuing.

此時在另一個終端使用mysql客戶端進行連線

[root@jg-72 mariadb-10.1.11]# ./bin/mysql -uroot -h168.168.207.72 -P3310
Welcome to the MariaDB monitor. Commands end with ; or g.
Your MariaDB connection id is 2
Server version: 10.1.11-MariaDB-debug Source distribution

Copyright (c) 2000, 2015, Oracle, MariaDB Corporation Ab and others.

Type ‘help;’ or ‘h’ for help. Type ‘c’ to clear the current input statement.

MariaDB [(none)]>

我們可以正常的執行一些SQL
MariaDB [(none)]> use test;
Database changed

MariaDB [test]> show variables like ‘char%`;
+————————–+———————————————-+
| Variable_name | Value |
+————————–+———————————————-+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /data/source/mariadb-10.1.11/share/charsets/ |
+————————–+———————————————-+
8 rows in set (0.00 sec)

嘗試建立一個表,發現hang住了,
MariaDB [test]> create table testtable ( c1 varchar(100), c2 varchar(100), c3 varchar(100));

切換到gdb所在的終端,
mariadb_3
可以看到,gdb在設定斷點的函式的入口處停了一下,等待我們單步除錯

L 命令可以看接下來10行的程式碼,後面可以跟數字
l 100就是看100行程式碼, l -10 就是看上面10行的程式碼

mariadb_4

單步除錯的命令是 n,也就是一行行執行程式碼(next)
mariadb_5

想看某個變數的值,使用 p 命令(print)

在執行到某函式呼叫處,比如上圖的2398行,也可以使用 s (step)命令,這個時候會進入被呼叫的函式dict_table_is_comp()內部,繼續單步執行
mariadb_6
comp = 1這說明,當前建立的表的格式是 compact的。

mariadb_7

我們這個表有3個nullable的列,每條記錄的長度限制,page_rec_max 是 8126,是通過
2425行的函式計算出來的。
mariadb_8
接下來進入計算每一個欄位佔用空間的計算。這裡我們看到,
這個表總共有 6 個 fields,而不是我們定義的三個,這是因為,mysql隱含的會新增三個欄位

  • Records in the clustered index contain fields for all user-defined columns. In addition, there is a 6-byte transaction ID field and a 7-byte roll pointer field.
  • If no primary key was defined for a table, each clustered index record also contains a 6-byte row ID field.

mariadb_9
我們用 p 看第一個field的資訊,確實是 ROW_ID,長度確實是6 byte,和 文件互相印證。

對於隱含列的處理,我們可能不關心,所以想直接跳過接下來的程式碼,那可以設定一個行斷點,
mariadb_10

b 命令不加引數就是在當前行設定斷點, b 就是在當前檔案的指定行設定行斷點, b : 就是在指定檔案的指定行設定行斷點
m_11

接下來 continue,我們發現它執行到 2451行後再次停下,說明迴圈進入了第二次迭代(i已經變成1)

m_13

可以看到,第二個欄位也是預設新增的 TRX_ID 列,長度是6個位元組,

m_14

第三個欄位是預設新增的ROLL_PTR列,長度為7位元組
可以預期,接下來就是我們建表的真實的欄位資訊。
如果想看當前程式的函式呼叫棧,可以使用 bt 或者where 命令

m_15

m_16

Detach命令使gdb釋放對mysql程式的連線,quit命令退出gdb

如何從MySQL的一條報錯資訊定位到原始碼

以下面這個問題舉例:
297個欄位 varchar(73)欄位的表
create table t_f73_ag_xjllb_yh
(
jydm VARCHAR(73),
rq VARCHAR(73),
cb VARCHAR(73),
khdkjdkjjse VARCHAR(73),
xyhjkjzje VARCHAR(73),
… …
xjdjwdqmye_yoy VARCHAR(73),
jxjdjwdqcye_yoy VARCHAR(73),
xjjxjdjwjzje1_yoy VARCHAR(73)
);
報如下錯誤
ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help. In current row format, BLOB prefix of 768 bytes is stored inline.

在原始碼根目錄搜尋訊息的部分內容

[root@jg-72 mariadb-10.1.11]# find * |xargs grep ‘Row size too large’

輸出很多,但是我們應該重點關注原始碼和文字檔案,所以
share/errmsg-utf8.txt sql/share/errmsg-utf8.txt
storage/innobase/handler/ha_innodb.cc
storage/xtradb/handler/ha_innodb.cc

這幾個檔案需要重點關注

因為MariaDB的INNODB儲存引擎實際上是xtraDB,所以先看
繼續尋找這個函式呼叫的地方
jydm VARCHAR(73),
rq VARCHAR(73),
cb VARCHAR(73),
khdkjdkjjse VARCHAR(73),
xyhjkjzje VARCHAR(73),
… …
xjdjwdqmye_yoy VARCHAR(73),
jxjdjwdqcye_yoy VARCHAR(73),
xjjxjdjwjzje1_yoy VARCHAR(73)
);
報如下錯誤
ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help. In current row format, BLOB prefix of 768 bytes is stored inline.

在原始碼根目錄搜尋訊息的部分內容

[root@jg-72 mariadb-10.1.11]# find * |xargs grep ‘Row size too large’

輸出很多,但是我們應該重點關注原始碼和文字檔案,所以
share/errmsg-utf8.txt sql/share/errmsg-utf8.txt
storage/innobase/handler/ha_innodb.cc
storage/xtradb/handler/ha_innodb.cc

這幾個檔案需要重點關注
m_17

因為MariaDB的INNODB儲存引擎實際上是xtraDB,所以先看
storage/xtradb/handler/ha_innodb.cc

m_18

發現兩個關鍵的巨集定義 ER_TOO_BIG_ROWSIZE 和DB_TOO_BIG_RECORD

從errmsg-utf8.txt檔案的內容ER_TOO_BIG_ROWSIZE 42000 和錯誤訊息頭 1118 (42000),可以推測出ER_TOO_BIG_ROWSIZE 應該就是錯誤訊息號 1118,但是errmsg-utf.txt中的1118錯誤的內容,不包含 (> xxx),進一步發現,包含這個錯誤內容的只有上面的 ha_innodb.cc的2058行, 函式是 convert_error_code_to_mysql()
繼續尋找這個函式呼叫的地方

find * |xargs grep ‘convert_error_code_to_mysql’
發現,全部都在ha_innodb.cc 檔案中(大概有幾十處呼叫)
通過遍歷這個檔案呼叫這個函式的地方,可以看到很多地方,error號都是通過
row_create_index_for_mysql()函式的返回值得到,我們更是找到一個函式
m_19
這裡,首先需要知道的一個概念
從文件中可以知道,InnoDB的表是一種索引組織表,也就是說,它的表實際上就是索引(clustered index),他的索引也就是表,它的clustered index實際上包含了所有使用者定義的欄位。
另外,可以在表的選定的欄位上建立二級索引,如果表中定義了主鍵,那麼二級索引將隱含的包括主鍵這一欄位。

The data in each InnoDB table is divided into pages. The pages that make up each table are arranged in a tree data structure called a B-tree index. Table data and secondary indexes both use this type of structure. The B-tree index that represents an entire table is known as the clustered index, which is organized according to the primary key columns. The nodes of the index data structure contain the values of all the columns in that row (for the clustered index) or the index columns and the primary key columns (for secondary indexes).

所以,基本可以斷定,這個函式create_clustered_index_when_no_primary()實際上就是我們建立一個不包含主鍵的InnoDB表主要要呼叫的函式。而它的主要工作,在
row_create_index_for_mysql()中完成。

同樣的方式,在storage/innobase/row/row0mysql.cc中找到 row_create_index_for_mysql() 的定義
m_20

m_21

通過大概瀏覽程式碼知道這個函式會呼叫que_run_threads()去做實際的工作。

找到它的定義處 storage/xtradb/que/que0que.cc 1183,發現它實際呼叫的是
que_run_threads_low(), 這個也定義在相同的檔案裡,進入這個函式
m_22
在同一個檔案裡,我們也找到que_thr_step()函式的定義,實際上可以推測出,它裡面會根據不同的語句呼叫不同的入口函式
m_23
很幸運,它用了老土的 if else條件判斷,而不是函式指標,所以可以知道呼叫了
dict_create_index_step()
這裡也可能是dict_create_table_step()呀? 一方面,我們是從create_index一路跟下來的,
另一方面,不放心的話,搜一下 QUE_NODE_CREATE_INDEX這個type,
發現是在storage/xtradb/dict/dict0crea.cc ind_create_graph_create()函式中賦值的,而這個函式,被 row_create_index_for_mysql()在呼叫que_run_threads()之前呼叫了,所以,可以確定,接下來執行的函式是dict_create_index_step()
m_25
在storage/innobase/dict/dict0crea.cc 中找到這個函式的定義
這個時候發現這個函式真TM長,分了好幾個階段,
它呼叫的函式有
dict_build_index_def_step()
dict_build_field_def_step()
dict_index_add_to_cache()
dict_index_get_if_in_cache_low()
dict_create_index_tree_step()

一個個函式跟進去相當於嘗試好幾條岔路,一般這種情況,每個函式都跟進去會掉進無限的呼叫陷阱裡,顯然不是個好辦法。

於是回到最開始,想辦法自底向上確定函式呼叫棧,之前是查詢ER_TOO_BIG_ROWSIZE從上往下找,現在查詢 DB_TOO_BIG_RECORD 這個關鍵字

m_24

重點關注其中返回 DB_TOO_BIG_RECORD的函式
在storage/xtradb/btr/btr0cur.cc 檔案中,幾處返回DB_TOO_BIG_RECORD的函式從名字看不是update就是insert,顯然不是我們的create table

於是在storage/xtradb/dict/dict0dict.cc 中,發現返回DB_TOO_BIG_RECORD的函式
恰好是dict_index_add_to_cache(), 是被dict_create_index_step()呼叫的函式之一。
m_25

到此,基本可以確定,最後通過函式dict_index_too_big_for_tree()來檢查建立一個innodb表是否會超出innodb表的一些長度限制。
m_26

通過前面的gdb除錯中的bt命令,也可以印證這一點,可以看到函式呼叫的堆疊
Create innodb table without primary key …
->create_clustered_index_when_no_primary()
->row_create_index_for_mysql()
->que_run_threads()
->que_run_threads_low()
->que_thr_step()
->dict_create_index_step()
->dict_index_add_to_cache()
->dict_index_too_big_for_tree()
m_27
Tip:
當然,這個查詢過程可以不通過命令列,也可以使用一些工具,比如sourceinsight把整個mariadb的原始碼匯入,然後在sourceinsight裡執行類似的搜尋過程。Source Insight的安裝使用這裡就不說了。 


相關文章