新手如何調試 MySQL

前幾天看到姜老師的舊文用 VSCode 編譯和調試 MySQL,每個 DBA 都應 get 的小技能 [1], 文末留了一個思考題,如何修改源碼,自定義版本,使得 select version() 輸出自定義內容

調試過程參考 macOS VSCode 編譯調試 MySQL 5.7[2]

內部 Item 對象參考從 SQL 語句到 MySQL 內部對象 [3]

源碼面前沒有祕密,建義對 DB 感興趣的嘗試 debug 調試。本文環境爲 mac + vscode + lldb

依賴及插件

vscode 插件:

mysql 源碼:

補丁:MySQL <= 8.0.21 需要對 cmake/mysql_version.cmake 文件打補丁 (沒有嚴格測試所有版本)

tar -zxf mysql-boost-5.7.35.tar.gz
cd mysql-5.7.35
mv VERSION MYSQL_VERSION
sed -i '' 's|${CMAKE_SOURCE_DIR}/VERSION|${CMAKE_SOURCE_DIR}/MYSQL_VERSION|g' cmake/mysql_version.cmake

創建 cmake-build-debug 目錄,後續 mysql 編譯結果,以及啓動後生成的文件都在這裏

mkdir -p cmake-build-debug/{data,etc}

配置 CMake 與編譯

在 mysql 工程目錄下面創建 .vscode/settings.json 文件

{
    "cmake.buildBeforeRun": true,
    "cmake.buildDirectory""${workspaceFolder}/cmake-build-debug/build",
    "cmake.configureSettings"{
        "WITH_DEBUG""1",
        "CMAKE_INSTALL_PREFIX""${workspaceFolder}/cmake-build-debug",
        "MYSQL_DATADIR""${workspaceFolder}/cmake-build-debug/data",
        "SYSCONFDIR""${workspaceFolder}/cmake-build-debug/etc",
        "MYSQL_TCP_PORT""3307",
        "MYSQL_UNIX_ADDR""${workspaceFolder}/cmake-build-debug/data/mysql-debug.sock",
        "WITH_BOOST""${workspaceFolder}/boost",
        "DOWNLOAD_BOOST""0",
        "DOWNLOAD_BOOST_TIMEOUT""600"
    }
}

內容沒啥好說的,都是指定目錄及 boost 配置,其中 WITH_DEBUG 打開 debug 模式,會在 /tmp/debug.trace 生成 debug 信息

View -> Command Palette -> CMake: Configure 執行後生成 cmake 配置

View -> Command Palette -> CMake: Build 編譯生成最終 mysql 相關命令

發現老版本編譯很麻煩,各種報錯,mysql 5.7 代碼量遠超過 5.5, 只能硬着頭皮看 5.7

初始化數據庫

首先初始化 my.cnf 配置,簡單的就可以,共它均默認

cd cmake-build-debug
cat > etc/my.cnf <<EOF
[mysqld]
port=3307
socket=mysql.sock
innodb_file_per_table=1
log_bin = on
server-id = 10086
binlog_format = ROW
EOF

初始化數據文件,非安全模式,調試用

./build/sql/mysqld --defaults-file=etc/my.cnf --initialize-insecure
tree -L 1 data
data
├── auto.cnf
├── ca-key.pem
├── ca.pem
├── client-cert.pem
├── client-key.pem
├── ib_buffer_pool
├── ib_logfile0
├── ib_logfile1
├── ibdata1
├── mysql
├── on.000001
├── on.index
├── performance_schema
├── private_key.pem
├── public_key.pem
├── server-cert.pem
├── server-key.pem
└── sys

運行 MySQL

由於用 vscode 接管 mysql, 所以需要配置 .vscode/launch.json

{
    // 使用 IntelliSense 瞭解相關屬性。 
    // 懸停以查看現有屬性的描述。
    // 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
    "version""0.2.0",
    "configurations"[
        {
            "type""lldb",
            "request""launch",
            "name""Debug mysqld",
            "program""${workspaceFolder}/cmake-build-debug/build/sql/mysqld",
            "args"[
                "--defaults-file=${workspaceFolder}/cmake-build-debug/etc/my.cnf""--debug"
            ],
            "cwd""${workspaceFolder}"
        },
        {
            "type""lldb",
            "request""launch",
            "name""Debug mysql",
            "program""${workspaceFolder}/cmake-build-debug/build/client/mysql",
            "args"[
                "-uroot",
                "-P3307",
                "-h127.0.0.1"
            ],
            "cwd""${workspaceFolder}"
        }
    ]
}

然後點擊 run and debug mysqld

mysql 啓動,看到輸出日誌無異常,此時可以用 mysql-client 連接

mysql -uroot -S ./data/mysql.sock

調試

首先在 sql_parser.cc:5435 處打斷點

void mysql_parse(THD *thd, Parser_state *parser_state)

mysql_parse 是 sql 處理的入口,至於 tcp connection 連接先可以忽略

mysql> select version();

執行上述 sql 自動跳轉到斷點處,Step Into, Step Over, Step Out 這些調試熟悉下即可

接下來分別調用主要函數:mysql_execute_command, execute_sqlcom_select, handle_query, select->join->exec(), Query_result_send::send_data, Item::send, Item_string:val_str, Protocol_text::store, net_send_ok

修改源碼

啓動 mysql 時 init_common_variables 會初始化一堆變量,其中會調用 set_server_version 生成版本信息,修改這個就可以

static void set_server_version(void)
{
  char *end= strxmov(server_version, MYSQL_SERVER_VERSION,
                     MYSQL_SERVER_SUFFIX_STR, NullS);
......
#ifndef NDEBUG
  if (!strstr(MYSQL_SERVER_SUFFIX_STR, "-debug"))
    end= my_stpcpy(end, "-dongzerun");
#endif
  if (opt_general_log || opt_slow_log || opt_bin_log)
    end= my_stpcpy(end, "-log");          // This may slow down system
......
}

看好條件編譯的是哪塊,修改即可,重新 CMake: Build 編譯再運行

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

Item Class

這裏不做過深分析,簡單講

function_call_generic:
          IDENT_sys '(' opt_udf_expr_list ')'
          {
            $$= NEW_PTN PTI_function_call_generic_ident_sys(@1, $1$3);
          }
        | ident '.' ident '(' opt_expr_list ')'
          {
            $$= NEW_PTN PTI_function_call_generic_2d(@$, $1$3$5);
          }
        ;

sql_yacc.cc 函數 PTI_function_call_generic_ident_sys 解析 sql, 識別出 version() 是一個函數調用

  virtual bool itemize(Parse_context *pc, Item **res)
  {
    if (super::itemize(pc, res))
      return true;
......

    /*
      Implementation note:
      names are resolved with the following order:
      - MySQL native functions,
      - User Defined Functions,
      - Stored Functions (assuming the current <use> database)

      This will be revised with WL#2128 (SQL PATH)
    */
    Create_func *builder= find_native_function_builder(thd, ident);
    if (builder)
      *res= builder->create_func(thd, ident, opt_udf_expr_list);
......
    return *res == NULL || (*res)->itemize(pc, res);
  }

find_native_function_builder 查找 hash 表,找到對應 version 函數註冊的單例工廠函數

static Native_func_registry func_array[] =
{
  { { C_STRING_WITH_LEN("ABS") }, BUILDER(Create_func_abs)},
  { { C_STRING_WITH_LEN("ACOS") }, BUILDER(Create_func_acos)},
......
  { { C_STRING_WITH_LEN("VERSION") }, BUILDER(Create_func_version)},
......
  { {0, 0}, NULL}
};

mysql 啓動時調用 item_create_init 將這些函數 builder 註冊到 hash 表 native_functions_hash

Create_func_version Create_func_version::s_singleton;

Item*
Create_func_version::create(THD *thd)
{
  return new (thd->mem_root) Item_func_version(POS());
}
class Item_func_version : public Item_static_string_func
{
  typedef Item_static_string_func super;
public:
  explicit Item_func_version(const POS &pos)
    : Item_static_string_func(pos, NAME_STRING("version()"),
                              server_version,
                              strlen(server_version),
                              system_charset_info,
                              DERIVATION_SYSCONST)
  {}

  virtual bool itemize(Parse_context *pc, Item **res);
};

可以看到 Item_func_version 函數創建時傳參即爲 mysql server_version 版本信息

董澤潤的技術筆記 十一年 IT 老兵,運維架構好把式,長期分享乾貨

小結

MySQL 代碼太龐大,5.1 大約 100w 行,5.5 130w 行,5.7 以後 330w 行,只能挑重點讀源碼。最近很多羣裏的人在背八股,沒必要,有那時間學着調試下源碼,讀讀多好

參考資料

[1]

用 VSCode 編譯和調試 MySQL,每個 DBA 都應 get 的小技能: https://www.modb.pro/db/112992,

[2]

macOS VSCode 編譯調試 MySQL 5.7: https://shockerli.net/post/mysql-source-macos-vscode-debug-5-7/,

[3]

從 SQL 語句到 MySQL 內部對象: https://www.orczhou.com/index.php/2012/11/mysql-innodb-source-code-optimization-1/#21_Item,

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/lJqb0kMtnAUmqUIWCShkIQ