原文: 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 命令。