如何实现一个数据库的 UDF?图数据库 NebulaGraph UDF 功能背后的设计与思考

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 大家好,我是来自 BOSS 直聘,主要负责安全方面的图存储相关工作。作为一个从 v1.x 用到 v3.x 版本的忠实用户,在见证 NebulaGraph 发展的同时,也和它一起成长。

BOSS 直聘和 NebulaGraph
关于 NebulaGraph 在 BOSS 直聘的应用场景,大家可以看看之前文洲老师的文章(图数据库 NebulaGraph 在 BOSS 直聘的应用),从那时候文洲老师构建的行为图发展到了安全场景的业务主图、算法推理图、职位相似度图谱等业务,现在更是支持了数仓同学的数据血缘及搜索同学的实时搜索召回场景,单图的规模达到了数千亿。

在图计算方面,BOSS 直聘基于 LPA 和 Louvain 的单度团、多维团,以及基础的离线特征,在安全生产环境中广泛应用图技术。相信未来图在 BOSS 直聘还会有更为宽广的舞台。

UDF 的萌生
随着 NebulaGraph 在 BOSS 直聘业务上的广泛应用,相对应的对内部技术人员的要求也越来越高。如果技术人员仅仅停留在使用层面,就无法满足从功能到性能很多需求。所以,学习源码成为了必然。

而后迁移 Neo4j->NebulaGraph 过程中,发现业务对 Neo4j 的 UDF 包有所依赖,我本萌生了实现 NebulaGraph UDF 功能的念头。

UDF 设计和实现原理

上图是一条完整 nGQL 语句的执行过程,而 UDF 实现原理同 nGQL 的执行流程相关,大致如下:

graphd 接收到语句 -> Bison 词法解析(切词) -> Flex 语法解析创建 Sentence -> Validator 校验并生成 AstContext(抽象语法树) -> toPlan 生成执行计划 Planner -> Optimizer 优化器优化 -> Executor 执行器执行。

在词法语法解析阶段,Function 会被单独解析出来。FunctionManager 作为原生的内置函数管理者,负责函数的定义、加载、调用等操作,从而管理函数的整个生命周期。调用语句通过 FunctionManager 查找到的函数最终会被执行器调用执行。

NebulaGraph 的 UDF 实现基于函数的调用执行流程,增加了 FunctionUdfManager:

static std::unorderedmap udfFunReturnType;
static std::unorderedmap>>
udfFunInputType
;
std::unorderedmap udfFunctions;

class FunctionUdfManager {
public:
typedef GraphFunction (create_f)();
typedef void(destroy_f)(GraphFunction
);

static StatusOr getUdfReturnType(const std::string functionName,
const std::vector &argsType);

static StatusOr loadUdfFunction(
std::string functionName, size_t arity);

static FunctionUdfManager &instance();

FunctionUdfManager();

private:
static create_f getGraphFunctionClass(void func_handle);
static destroy_f deleteGraphFunctionClass(void func_handle);

void addSoUdfFunction(char funName, const char soPath, size_t i, size_t i1, bool b);
void initAndLoadSoFunction();
};
它主要做以下几件事:

和 FunctionManager 一起初始化,initAndLoadSoFunction 开启定时扫描,扫描 --udf_path 路径下文件;
loadUdfFunction 加载 .so 文件,实例化函数方法,以函数名为 key 保存在 Map 中;
在启用 UDF 功能的情况下,FunctionManager 未查找函数时,查找并调用 FunctionUdfManager Map 中的函数。
实现比较简单,可以说是取巧了,有需要的话 UDAF 也可用类似方式实现。

UDF 使用方法
下面来讲讲 NebulaGraph UDF 的具体使用,如果你是用 NebulaGraph v3.5.0+ 版本的话,就可以按照以下方式使用 UDF 功能了。如果你是 v3.4.x 及以下版本,UDF 功能是暂不支持的,你也可以 cherry-pick 这个 pr 自行编译使用 UDF 功能。

第一步,在 graphd 配置文件中开启 UDF 功能并指定包目录

enable udf, c++ only

--enable_udf=true

set the directory where the .so of udf are stored

--udf_path=/home/foobar/dev/nebula/udf/
第二步,编写自定义函数代码,继承 GraphFunction。GraphFunction 的结构如下:

class GraphFunction;

extern "C" GraphFunction create();
extern "C" void destroy(GraphFunction
function);

class GraphFunction {
public:
virtual ~GraphFunction() = default;

virtual char *name() = 0;

virtual std::vector> inputType() = 0;

virtual nebula::Value::Type returnType() = 0;

virtual size_t minArity() = 0;

virtual size_t maxArity() = 0;

virtual bool isPure() = 0;

virtual nebula::Value body(
const std::vector> &args) = 0;
};
create、destroy 是函数的创建销毁方法;
name 调用时的函数名;
inputType、returnType 输入输出类型;
minArity、maxArity 参数数量;
isPure 函数是否有状态;
body 函数的实现。
第三步,编写好的函数打包成 (.so) 文件,放到配置文件 --udf_path 配置的对应目录下,graphd 服务会定时(5 分钟)扫描该路径下的包,加载到函数库中。之后,就可以在自己的语句中调用对应的函数了。

⚠️ 注意:由于 graphd 只扫描本地路径下的函数包,想让多个 graphd 都生效,必须都在本地路径下有相应的包。

这里要 cue 下思为老师,感谢他补充的完整使用文档和编译环境:https://github.com/vesoft-inc/nebula/pull/4804

UDF 尚未解决的问题
虽然目前 UDF 是能用,但是它还存在部分优化问题。比如:

so 包位置只支持扫描本地;
函数只在 graphd 层,无法下推到存储;
使用麻烦,需要用户编码。
当然这些问题和一开始的设计息息相关:开发 UDF 之初,其实是想兼容 C++ 的 so 包和 Java 的 jar 包,但测试了 C++ Jni 调用 Java 的性能,发现基本上无法用于大规模的生产。

下图便是当时的性能测试:

因为实现实在是性能堪忧,于是就放弃了一开始的设计。

当然还有一些未来规划上的事情,主要是希望 NebulaGraph 开发团队一起合作完成:

个别的大查询语句和深度查询,容易把 storaged 的内存打满影响集群整体性能。是否可以考虑通过查询时间超时或内存监控自动 kill 对应的查询,释放掉内存。其实对于类似的语句,基本上已经很难拿到结果了,更多的可能是想降低语句带来的影响
集群的容错性,多副本情况下某个节点的非正常下线会影响整体集群,由于环境的复杂性具体定位分析也比较困难,盼望尽可能增强集群健壮性。

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
1月前
|
SQL Java 数据库连接
深入 MyBatis-Plus 插件:解锁高级数据库功能
Mybatis-Plus 提供了丰富的插件机制,这些插件可以帮助开发者更方便地扩展 Mybatis 的功能,提升开发效率、优化性能和实现一些常用的功能。
238 26
深入 MyBatis-Plus 插件:解锁高级数据库功能
|
1月前
|
SQL 测试技术 数据库
|
1月前
|
XML 数据库 数据格式
数据库 校验名称唯一性,用于新增和修改功能
数据库 校验名称唯一性,用于新增和修改功能
39 8
|
5月前
|
SQL 数据库 微服务
微服务03,最简单的Demo,我们每个服务不能重复开发相同业务,微服务数据独立,不要访问其他微服务的数据库,微服务的特点之一是提供不能功能的数据库互相分割,微服务需要根据业务模块拆分,做到单一职责,
微服务03,最简单的Demo,我们每个服务不能重复开发相同业务,微服务数据独立,不要访问其他微服务的数据库,微服务的特点之一是提供不能功能的数据库互相分割,微服务需要根据业务模块拆分,做到单一职责,
|
1月前
|
XML 数据库 数据格式
数据库 校验名称唯一性,用于新增和修改功能
数据库 校验名称唯一性,用于新增和修改功能
37 1
|
1月前
|
XML 数据库 数据格式
数据库 校验名称唯一性,用于新增和修改功能
数据库 校验名称唯一性,用于新增和修改功能
31 0
|
3月前
|
XML 数据库 数据格式
数据库 校验名称唯一性,用于新增和修改功能
数据库 校验名称唯一性,用于新增和修改功能
60 1
|
4月前
|
存储 关系型数据库 MySQL
基于python django 医院管理系统,多用户功能,包括管理员、用户、医生,数据库MySQL
本文介绍了一个基于Python Django框架开发的医院管理系统,该系统设计了管理员、用户和医生三个角色,具备多用户功能,并使用MySQL数据库进行数据存储和管理。
187 4
基于python django 医院管理系统,多用户功能,包括管理员、用户、医生,数据库MySQL
|
5月前
|
关系型数据库 分布式数据库 数据库
PolarDB产品使用问题之数据库处于只读状态,如何恢复其读写功能
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
PolarDB产品使用问题之数据库处于只读状态,如何恢复其读写功能
|
4月前
|
存储 安全 测试技术
【计算机三级数据库技术】第4章 数据库应用系统功能设计与实现--附思维导图
重点介绍了数据库应用系统(DBAS)的功能设计和实现。
51 1