在 MariaDB 基金会进行 GSoC 实习
引言
大家好,我叫 Kartik Soneji,是孟买 Thadomal Shahani 工程学院的二年级学生。我从 13 岁开始编程。我先学习了 Java,然后学习了一些 C++,之后一头扎进了使用 HTML、CSS 和 JavaScript 进行的 Web 开发。我还用 Python 和 Rust 编写了一些代码,看看它们的魅力所在。
我为开源社区做贡献的主要动力是因为我坚信软件是自由的,可以复制、修改和学习。
今年,我非常幸运地获得了在 MariaDB 基金会作为 GSoC 实习生的机会,这篇博文总结了我的这段旅程。
项目描述
问题
问题:MDEV-12933
整理压缩库的混乱状况
随着 MariaDB 拥有越来越多的存储引擎,以及它们获得越来越多的功能,MariaDB 可以选择性地使用越来越多的压缩库来实现各种目的。
InnoDB、TokuDB、RocksDB——它们都可以使用不同的压缩库集合。将它们全部编译进去会导致大量的运行时/rpm/deb 依赖项,其中大部分用户永远不会使用。不将它们编译进去,会导致用户请求将它们编译进去。虽然大多数用户不需要所有这些库,但许多用户使用其中一些库。
在运行时动态加载压缩库将允许服务器根据库的可用性支持任何组合。
这也将允许服务器实现对更多压缩库的支持,而无需将其强制作为硬依赖项。
我们考虑的可能解决方案
我们考虑了几种替代方案,每一种都不容易实现,并且各有优缺点。
- 使用 Squash
Squash 提供了一个统一的 API 来访问许多压缩库,这使得应用程序在选择压缩算法方面具有很大的灵活性,包括将选择权交给用户的选项。
- 优点
- 一次获得对约 30 个库的访问。
- API 已经构建好了。
- 与服务相比,这是一个更稳定、更长期的解决方案。
- 缺点
- 由于 Squash 是一个抽象库,它不提供对库特定函数的访问,例如
LZ4_loadDict
LZ4_loadDictHC
ZSTD_compress_usingDict
ZSTD_compress_usingCDict
- 增加了另一个外部依赖项。
- 需要将更改传播到第三方代码中。
- 由于 Squash 是一个抽象库,它不提供对库特定函数的访问,例如
- 优点
- 编写我们自己的类似 Squash 的 API。
- 优点
- 不增加另一个外部依赖项。
- 我们可以根据我们的需求定制 API。
- 这也是一个更稳定、更容易维护的长期解决方案。
如果(众多)组件中的一个发生变化,它不太可能中断,也更容易修复。
- 缺点
- 这不是一项简单的任务。
- 仍然需要将更改传播到第三方代码中。
- 优点
- 将动态加载实现为 MariaDB 服务。
- 优点
- 可能不需要修改第三方代码。
- 服务器处理版本检查和 API/ABI 不匹配问题。
- 实现速度更快。
- 缺点
- 可能会产生意想不到的后果。
- 存储引擎或库的变化可能会破坏实现。
- 因此,如果某些引擎频繁更新 API(例如
RocksDB
),这可能会变成一个高维护项目。
尽管Connect
、InnoDB
和Mroonga
的 API 看起来相当稳定。 - 仍然有可能需要修改第三方代码,但很大程度上可能不需要。
- 优点
我们选择的解决方案
我们决定采用服务方式,原因如问题 MDEV-22895
中所述
- 使用 Squash 需要修改所有使用压缩库的存储引擎代码,包括第三方存储引擎,这是个问题,因为我们不太可能将这些更改传播到上游。
- Squash 提供的统一 API 不支持我们当前存储引擎从压缩库中使用的所有零碎部分。修改存储引擎代码以摆脱 Squash 不支持的 API 调用过于复杂,这加剧了第 1 点所述的问题。
项目内部细节
概览
MariaDB 服务是一种通用机制,用于使服务器代码可供 MariaDB 插件使用。
我们的计划是将它们用作代理,仅当主机上安装了相应的压缩库且 MariaDB 服务器在启动时可以找到它们时,才向插件提供压缩 API。
总体思路是压缩服务使用 #define
将库函数替换为函数指针调用。
也就是说,如果存储引擎的原始函数调用是
FOO_compress_buffer(src, srcLen, dst, dstLen);
那么,服务会将其转换为
compression_service_foo->FOO_compress_buffer(src, srcLen, dst, dstLen);
借助这样的预处理器定义
#define FOO_compress_buffer(...) compression_service_foo->FOO_compress_buffer_ptr(__VA_ARGS__)
理论上,通过这种方式,我们应该能够“骗过”存储引擎插件来调用我们的代理函数指针(在压缩服务中定义),而无需直接修改它们的源代码。
每个库由一个单独的压缩服务处理。
“模拟”库头文件位于 include/compression/<head name>.h
中,加载代码和哑函数(dummy functions)位于 sql/compression/<library>.h
中。
所有函数指针都放在每个压缩服务的一个结构体中。
struct compression_service_foo_st{
/* full type of function pointer */ FOO_compress_buffer_ptr;
//more functions
}
当库加载时,函数指针会调用实际函数(来自 .so
文件),但如果用户的系统上没有该库,服务就无法转发调用。
在这种情况下,指针会被初始化为返回错误代码的哑函数(dummy functions),错误代码由调用方存储引擎处理。
compression_service_foo->FOO_compress_buffer_ptr = DUMMY_FOO_compress_buffer;
哑函数(dummy function)被定义为返回一个错误代码。
foo_ret DUMMY_FOO_compress_buffer(const char *src, int srcLen, char *dst, int *dstLen){
return FOO_INTERNAL_ERROR;
}
本项目还向服务器添加了一个新选项 --use-compression
,允许在服务器启动时指定要加载的库。
例如,--use-compression=lzma,lzo
将只加载 LZMA
和 LZO
库,即使存在其他库。
默认行为是加载尽可能多的库。
服务器尝试使用 dlopen
加载指定的库。
void *library_handle = dlopen("libfoo.so");
void *FOO_compress_buffer_ptr = dlsym(library_handle, "FOO_compress_buffer");
最后,如果服务器能够成功打开库并解析所有符号,则会将哑函数(dummy functions)替换为已解析的函数。
compression_service_foo->FOO_compress_buffer_ptr = (/* typecast */) FOO_compress_buffer_ptr;
有关更详细的解释和一些示例代码,请参阅:创建一个新的压缩服务
我在编码期间遇到的最大挑战
项目的大部分进展顺利,这在很大程度上归功于 Robert 和 Sergei Golubchik 制定的出色路线图。
即便如此,有些事情并没有按计划进行。
- 我记得看着庞大的代码库(自 1995 年以来一直在开发中),心想我该如何适应它。这是我第一次处理如此大量的代码。
- 编译错误并不有趣,但链接器错误无疑是调试中最糟糕的问题。
很多时候都是靠猜测和尝试。除了基本理论外,我没有链接器经验,这也没有帮助。 - 项目的某些部分存在不当的 CMake 用法,需要采用变通方法,导致了一些延迟。
具体来说,尽管 CMake 有内置的Find<package>
函数,但检测和包含库头文件的机制是硬编码的。
项目在计划结束时的状态
\ | BZip2 | LZ4 | LZ4HC | LZMA | LZO | Snappy | ZStandard |
---|---|---|---|---|---|---|---|
Connect | * | ||||||
InnoDB | Y | Y | Y | Y | Y | ||
Mroonga | Y | Y | |||||
RocksDB | Y | ? | ? | Y | ? |
Y
→ 服务已确认工作正常,且已编写测试。*
→ 服务工作正常,但需要调整或附加测试。?
→ 服务已实现,但额外的复杂性可能不值得。
RocksDB 很难处理,因为它与 LZ4
和 ZStandard
紧密集成。
根据我对代码的理解,它在压缩失败时也没有备用机制。
这意味着它不太可能与我们的方法一起工作,我们的方法依赖于调用方(即 RocksDB)优雅地处理错误情况。
更多详情请参阅我的 GitLab 仓库。
提交我的工作并告别
所有更改都包含在 此拉取请求 中。
当前的补丁实现了对所有库的支持,以及指示库是否已加载的全局状态变量,例如 Compression_loaded_<library>
。
这四个月是一段很棒的经历,我在学习 MariaDB 服务器如何工作方面度过了愉快的时光。Robert 是一位很棒的导师,他耐心地听取了我的想法。
我学到了很多关于编写安全且高性能代码的知识。这是我第一次参与如此规模和影响力的项目,所以我有点紧张,但这段经历将帮助我成长为一名开发者。离开 GSoC 项目让我有些伤感,但我确实计划继续为 MariaDB 和其他开源项目做出贡献。
虽然我们在 GSoC 期间完成了很多工作,但项目仍未完全结束。还有一些我想探索的方法和一些我想添加的功能。
我还想修复我在过程中注意到的一些代码库问题。
干得真棒!👏
做得很好!