新手如何除錯 MySQL

語言: CN / TW / HK

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

除錯過程參考 macOS VSCode 編譯除錯 MySQL 5.7

內部 Item 物件參考 從SQL語句到MySQL內部物件

原始碼面前沒有祕密,建義對 DB 感興趣的嘗試 debug 除錯。本文環境為 mac + vscode + lldb

依賴及外掛

vscode 外掛:

  • C/C++
  • C/C++ Clang Command Adapter
  • CodeLLDB
  • CMake Tools

mysql 原始碼:

  • mysql-boost-5.7.35.tar.gz

補丁:

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

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

  • select version() , select now() 這些簡單 sql 在 server 層就能得到結果,無需進入引擎層
  • version() , now() 這些函式在 yacc&lex 詞法解析時就會解析成對應的 Item
  • 最後 mysql 渲染結果時,就是由 Item::itemize 寫到 result 中
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 版本資訊

小結

MySQL 程式碼太龐大,5.1 大約 100w 行,5.5 130w 行,5.7 以後 330w 行,只能挑重點讀原始碼。最近很多群裡的人在背八股,沒必要,有那時間學著除錯下原始碼,讀讀多好

分享知識,長期輸出價值,這是我做公眾號的目標。同時寫文章不容易,如果對大家有所幫助和啟發,請幫忙點選 在看點贊分享 三連