原文: Writing a MySQL storage engine from scratch
本文主要描述了如何寫一個MySQL 存儲引擎 - 一個可以持久保存 MySQL 表數據的插件
簡介
這篇文章總結了我在寫一個新的 MySQL 存儲引擎時學到的一些事情。MySQL 的存儲引擎是一個能夠提供 在磁盤中存儲數據,并且能夠提供對數據的鏈接的插件。存儲引擎通常被作為 key/value 數據庫實現,但是并不一定是這樣。Oracle 的 MySQL 提供了多種存儲引擎,并且所有的存儲引擎都能夠被重新編寫。默認的存儲引擎是 InnoDB,一種 key/value 庫。其他的存儲引擎有的能夠寫入 csv 文件(‘Tina’)或者專門用來歸檔數據('archive').Maria DB,作為一個分支,是一個更加先進并且提供了 Cassandra NoSQL 數據庫接口的存儲引擎,Percona 的 TokuDB key/value 庫 和 xtradb,則是 InnoDB 的改進版本。
總的來說,MySQL 有13種存儲引擎,MariaDB 有大約20 種。但是這些仍然不夠,讓我們再來添加一種。
下載源碼以及第一次接觸
你能夠在 GitHub 上面下載 MySQL 的源碼,我的工作版本是 5.7.12。
git clone https://github.com/mysql/mysql-server.git
cd mysql-server
git checkout mysql-5.7-12
讓我們簡單看一下代碼的整體結構。文件和目錄的數量有點多,但是大部分和我們的工作無關,只有少部分的目錄值得我們仔細查看:
include 包含全局 include 文件,你能在里面找到 mysql.h,其中包含了很多重要的宏和聲明。
sql 保護了 sql 相關的代碼:解析器,查詢引擎等。
sql/field.h Field class 描述了一個 MySQL 行。
sql/item.h Item class 是一個解析器生成的抽象語法樹的節點。
sql/sql_*.cc 這些文件實現了實際上的 SQL 操作。
sql/handler.h 存儲引擎的基類
storage 這個目錄包含了所有的存儲引擎。
storage/innobase InnoDB 存儲引擎
storage/example 一個存儲引擎實例項目。
通過瀏覽這些代碼,我們可以清晰地看出 mysql 代碼的基礎是相對較老的。雖然大部分的代碼是通過 C++ 實現的,但是實際上僅僅是 “c 和 類”的實現方式。更多的高級 c++ 特性,例如 模板,exceptions 或者標準庫都沒有被使用。連接列表被實現成包裹在 "void *" 中的結構體對象(my_list.h)。mysql 還有它自己的 string 實現(string.c) 而不是使用 std::string。RAII 也沒有被使用。更甚的是,你能夠找到很多用于清理內存分配資源的 goto 語句。
功能的實現會非常的長(超過 1000 行代碼,并且包含了多層的 if 語句 mysql_prepare_create_table),并且有一些類也非常的大。很多方法被存儲在基類中而不是繼承類,并且類中存儲了很多的狀態。
MySQL 用了幾種內存自動分配(memroot_allocator.h),它們不是標準的實現。很多類使用自己的內存分配來重寫了操作符 new 和 delete。
同時MySQL 的代碼沒有統一的編碼風格。首行縮進都有多種風格,結構體的命名風格也有多種方式(。。。),空格的使用也非常隨意(。。。)。
我沒有和 Oracle 或者 Mysql AB 工作的人討論過,但是我們可以基于這些代碼看出他們的合作風格。缺乏同一的代碼風格意味著每一位工程師都能選擇他最熟悉的方式。代碼風格有很大的差異,甚至在同一個文件中,每個人也可以對任意部分的代碼進行修改。我曾經在做過類似的工作,所以我明白這其中有非常多的工作需要做。
如果 MySQL希望吸引新的開發者,“c 加 類”的使用,不使用標準庫并且非常長且復雜的功能實現都是需要重構的。
編譯,安裝,運行,debug
讓我們繼續安裝。如果你運行了之前的步驟,那么接下來你需要檢查 mysql-server 目錄中的代碼。我們將在這個目錄中運行 cmake 來生成 Makefile。我們同時將會創建一個 Debug build,雖然它會有一點慢,但是在 debug 的過程中我們會用到它。
cd mysql-server
cmake -DCMAKE_BUILD_TYPE=Debug
make -j 5
sudo make install
你的編譯文件現在安裝在了 /usr/local/mysql 中。你能通過在子目錄中運行 cmake 來生成獨立的 Debug 和 Release builds,并且你能夠選擇不同的安裝目錄,運行 “cmake --help”來獲得選項的列表。在這個文章中我僅僅會保持所有事情盡可能簡單。
為了成功啟動 MySQL 客戶端,我們還需要做以下事情。我們需要創建一個數據目錄(用來存放 表)并且初始化它們(注意你可能需要改變下面代碼的文件路徑):
mkdir /home/alan/tmp/mysql-data # must be empty
cd /usr/local/mysql/bin
./mysqld --initialize --datadir=/home/alan/tmp/mysql-data
最后一個命令將會生成一個 root 密碼。記下它,接下來會用到。我將我的開發環境設置為空的 root 密碼 - 在測試時它將會更加安全。你可以使用接下來的命令重新設置密碼(用上面生成的 root 密碼來替換 <PASSWORD>):
-- start the server
./mysqld --datadir=/home/alan/tmp/mysql-data
-- in a separate terminal we can now change the password
./mysqladmin password --user=root --password=<password>
</password>
然后我們可以創建一個新的數據庫:
./mysqladmin create test --user=root --password
客戶端能夠被正常啟動,并且之后你將可以創建表,插入數據等等。
./mysql --user=root test
逐步構建一個新的存儲引擎
存儲引擎被實現為一個動態的庫(一個 .so 文件),并且他的源文件被存儲在 msyql-server/storage 目錄中。如果你僅僅希望玩一下那么你可以修改已有的 'example ' 存儲引擎。我選擇通過以下步驟創建我自己的 'upscaledb'。
cd mysql-server/storage
-- copy the 'ha_example' directory to 'ha_upscaledb'
cp -r ha_example ha_upscaledb
-- rename the files
cd ha_upscaledb
mv ha_example.h ha_upscaledb.h
mv ha_example.cc ha_upscale.cc
最有一步就是,使用你最喜歡的 IDE 來將 'example' 替換為 'upscaledb'以及 'EXAMPLE' 替換為 'UPSCALEDB'。不要忘記同樣更改你的 CMakeLists.txt 。然后進入 mysql-server 的根目錄并且再一次運行 'cmake' 和 ‘make’ 命令。新的 upscaledb 引擎現在構建好了,它的文件名是 mysql-server/storage/upscaledb/ha_upscaledb.so。
現在我們需要提示 MySQL 關于我們的新存儲引擎。首先我們創建一個 安裝目錄 到新的 .so 文件的 symbolic link(軟連接?)。通過這個連接我們的服務器將會總是使用最新版本的 .so 文件。不論我們何時修改了代碼,我們只需要簡單的重新編譯存儲引擎并且重啟 MySQL 服務器就行了。
cd /usr/local/mysql/lib/plugin
sudo ln -s ~/prj/msyql-server/storage/upscaledb/ha_upscaledb.so ha_upscaledb.so
最后的步驟就是更新 MySQL 的內部系統表。我們能夠使用 MySQL 的客戶端完成這些。同時確保 MySQL 的服務器端仍在運行。
cd /usr/local/mysql/bin
./mysql --user=root test
mysql> INSTALL PLUGIN upscaledb SONAME 'ha_upscaledb.so';
現在你能夠使用最新的存儲引擎并且嘗試構建表 (create table test(value INTEGER) ENGINE=upscaledb;)。現在我們的存儲引擎只是一個框架并且沒有任何的實現。我們將會得到一個報錯。在我們開始增添程序邏輯之前我會教你如何 Debug MySQL 服務器。接下來的代碼將會在 gdb 中啟動服務器,并且將會讓 gdb 獲取所有的信號。(i.e. ctrl-c 可以結束 debugger)。
cd /usr/local/mysql/bin
gdb --args ./mysqld --gdb --datadir=/home/alan/tmp/mysql-data
嘗試在你的存儲引擎中的 'create()' 方法中設置一個斷點,并且運行一次上述的 CREATE TABLE 命令。