原文链接 雷鹏:ToplingDB 的 旁路插件化
背景
在 RocksDB 中,有大量
XXXXFactory 这样的类,例如
TableFactory,它用来实现 SST 文件的插件化,用户要换一种 SST,只需要配置一个相应的
TableFactory 即可,这其实已经很灵活了。
问题
这种简单的 Factory 机制存在两个问题:
- 1.要更换 Factory,用户必须修改代码,这虽然有点繁琐,但不算致命
- 2.致命的是:如果要更换的是第三方的 Factory,必须在代码中引入对第三方 Factory 的依赖!
- 3.如果通过其它语言(例如 Java)使用,还需要专门为第三方依赖实现 binding
当年在
TerarkDB 中,我为了实现无缝集成
TerarkZipTable,避免用户修改代码,使用了一种非常 Hack 的方案:在
DB::Open 中拦截配置,如果发现了相关的环境变量,就启用
TerarkZipTable。 这样就允许用户不用修改代码,只需要定义环境变量就能使用
TerarkZipTable。
这种配置方式实现了 TerarkDB 当时的预期目标,但只是一个简陋的补丁!
作为一个完备的、系统化的解决方案,我们(ToplingDB)期望的插件化,仍以
TableFactory 为例,应该让用户可以这样定义
TableFactory:
std::string table_factory_class = ReadFromSomeWhere(...);
std::string table_factory_options = ReadFromSomeWhere(...);
Options opt;
opt.table_factory = NewTableFactory(table_factory_class, table_factory_options);传统插件化方案
要在更换 Factory 时只需要修改配置而不用修改代码,我们需要将相应的配置项(例如类名)映射到 Factory(的基类)对象(的创建函数),这样就需要一个保存了这种映射关系的全局映射表。 仍以
RocksDB 的
TableFactory 为例,现有代码大致这样:
class TableFactory {
public:
virtual Status NewTableReader(...) const = 0;
virtual TableBuilder* NewTableBuilder(...) const = 0;
// more ...
};
TableFactory* NewBlockBasedTableFactory(const BlockBasedTableOptions&);
TableFactory* NewCuckooTableFactory(const CuckooTableOptions&);
TableFactory* NewPlainTableFactory(const PlainTableOptions&);我们增加一个全局
map,把类名映射到
NewXXX 函数,但首先就碰到一个问题:这几个函数的 prototype 是不同的,为了统一化,我们把这些
XXXOptions 序列化为
string:
TableFactory* NewBlockBasedTableFactoryFromString(const std::string&);
TableFactory* NewCuckooTableFactoryFromString(const std::string&);
TableFactory* NewPlainTableFactoryFromString(const std::string&);现在可以开始下一步了,定义一个全局
map,并注册这三个
Factory:
std::map table_factory_map;
table_factory_map["BlockBasedTable"] = &NewBlockBasedTableFactoryFromString;
table_factory_map["CuckooTable"] = &NewCuckooTableFactoryFromString;
table_factory_map["PlainTable"] = &NewPlainTableFactoryFromString; 大体框架是这样,但是,具体到细节,大致会是这样:
class TableFactory {
public: // 略去不相关代码 ...
using Map = std::map;
static Map& get_reg_map() { static Map m; return m; }
static TableFactory*
NewTableFactory(const std::string& clazz, const std::string& options) {
return get_reg_map()[clazz](options); // 省略错误检查
}
struct AutoReg {
AutoReg(const std::string& clazz, TableFactory*(*fn)(const std::string&))
{ get_reg_map()[clazz] = fn; }
};
};
#define REGISTER_TABLE_FACTORY(clazz, fn) \
static TableFactory::AutoReg gs_##fn(clazz, &fn) 在某 .cc 文件中的全局作用域(下面三个注册可能分散在每个 Table 各自的 .cc 文件中):
REGISTER_TABLE_FACTORY("BlockBasedTable", NewBlockBasedTableFactoryFromString);
REGISTER_TABLE_FACTORY("CuckooTable", NewCuckooTableFactoryFromString);
REGISTER_TABLE_FACTORY("PlainTable", NewPlainTableFactoryFromString);前面用户代码的调用处改成这样就可以了:
TableFactory::NewTableFactory(table_factory_class, table_factory_options);这实际上就是很多成熟系统使用的插件化机制。我们把
AutoReg 放入
TableFactory 类中,作为一个内部类,其原因是为了避免污染外层 namespace,
REGISTER_TABLE_FACTORY 用来在全局作用域定义一个
AutoReg 对象,该对象在
main 函数执行之前初始化,定义这么一个宏主要是为了方便、统一化,以及可读性,理论上,不使用
REGISTER_TABLE_FACTORY 而完全手写
AutoReg 也是可以的。
接下来的问题是,
RocksDB 有大量这样的
XXXFactory,对于每个
XXXFactory,我们都写一套这样的代码,工作量很大,很枯燥,还容易出错。于是我们抽象出一个
Factoryable 的模板类:
template
class Factoryable { // Factoryable 位于某个公共头文件如 factoryable.h
using Map = std::map;
static Map& get_reg_map() { static Map m; return m; }
static Product*
NewProduct(const std::string& clazz, const std::string& params) {
return get_reg_map()[clazz](params); // 省略错误检查
}
struct AutoReg {
AutoReg(const std::string& clazz, Product*(*fn)(const std::string&))
{ get_reg_map()[clazz] = fn; }
};
};
class TableFactory : public Factoryable {
public:
// 此处的 RocksDB 原有代码不做任何改动
};
#define REGISTER_FACTORY_PRODUCT(clazz, fn) \
static decltype(*fn(std::string())::AutoReg gs_##fn(clazz, &fn) 相应的,前面用户代码的调用处改成这样:
TableFactory::NewProduct(table_factory_class, table_factory_options);至此,我们只需要对原版
RocksDB 做少量的修改,就解决了我们的两个问题,一切似乎都很美好。但是,
RocksDB 中有很多这样的
XXXFactory,并且,很多即便不是名为 XXXFactory 的 class,也需要这样的 Factory 机制,例如
Comparator,例如
EventListener……
旁路插件化方案
对于我们(ToplingDB)来讲,RocksDB 是上游代码,如果上游能及时地接受我们的修改,传统的这种插件化方案其实已经足够好了。如果只是一两个这样的修改,我们可以努力说服上游接受这些修改,但是我们需要对 RocksDB 中大量的 class 都做这样的修改,上游就很难接受了。
所以,我们能否对原版 RocksDB 不做任何修改,就解决这两个问题呢?
其实只要从传统的思维框架“让 class 拥有 Factory 插件化功能” 转变到 ”为 class 添加 Factory 插件化功能“,前面的 Factoryable 代码都不用做任何修改,只需要改一下其中的宏定义
REGISTER_FACTORY_PRODUCT:
#define REGISTER_FACTORY_PRODUCT(clazz, fn) \
static Factoryable::AutoReg gs_##fn(clazz, &fn) 为了语义上更合逻辑,我们将 Factoryable 重命名为 PluginFactory,再增加一个全局模板函数:
template
Product* NewPluginProduct(const std::string& clazz, const std::string& params) {
return PluginFactory::NewProduct(clazz, params);
} 相应的用户代码就是:
NewPluginProduct(table_factory_class, table_factory_options); 应用
在 ToplingDB 中,我们使用了这种旁落插件化设计模式,当然,相应的实现代码比这里的 demo 代码要复杂很多。 更进一步,同样是在 ToplingDB 中,我们还支持: * 对象的旁路序列化 * 对象的 REST API 及 Web 可视化展示/修改
这两个功能完整复用了 PluginFactory,只是额外定义了两个模板类,SeDeFunc:
template struct SerDeFunc {
virtual ~SerDeFunc() {}
virtual Status Serialize(const Object&, string* output) const = 0;
virtual Status DeSerialize(Object*, const Slice& input) const = 0;
};
template
using SerDeFactory = PluginFactory > >; 以及 PluginManipFunc:
template struct PluginManipFunc {
virtual ~PluginManipFunc() {} // Repo 指 ConfigRepository
virtual void Update(Object*, const json&, const Repo&) const = 0;
virtual string ToString(const Object&, const json&, const Repo&) const = 0;
};
template
using PluginManip = PluginFactory*>;