如何用 Wasm 为数据库增加 UDF 功能

原创
03/15 18:13
阅读数 1.9K

作者: arcosx 百度高级研发工程师,负责机器学习平台研发

前言

UDF (User-defined function) 意为用户提供或定义的函数。在数据库领域,UDF 代表一种机制:通过添加一个函数来扩展数据库服务的功能。

WebAssembly (Wasm) 是一个可移植、体积小、加载快并且兼容 Web 的全新格式。近年来 Wasm 不仅仅在前端领域广泛应用,也开始在服务端大展身手。其在服务端提供了接近原生代码的性能,但是又不损失安全性。

笔者在参加 NebulaGraph 举办的 Nebula Hackathon 2021 中将上述两种技术词汇结合起来,为其实现了第一个生产级用户自定义函数引擎,荣获该项赛事的最佳项目奖。本文是笔者在比赛结束后的总结与回顾,旨在交流与分享 Wasm 在服务端作为拓展机制的潜能,抛砖引玉。

需求分析

User-defined function 广泛应用

当下,有非常多的主流数据库或数据处理系统都存在 UDF 实现,简要列举如下:

  • 关系型数据库领域,绝大部分关系型数据库,例如 MySQL,Microsoft SQL Server 等支持并且有其标准。关系型数据库标准中,将 UDF 分为标量函数 (Scalar Function) 和表值函数 (Table Function) 两类。调用标量函数只返回一个单一的值,而表值函数则返回包含多行多列的表格。
  • 非关系型数据库领域,主流 NoSQL ,例如 MongoDB、Cassandra 也支持其各具特色的 UDF,例如 MongoDB 的 UDF 将 Javascript 的函数嵌入进去Cassandra 支持上传 jar 包作为 UDF,这些特色的嵌入方式与这些数据库的实现技术密切相关。
  • 大数据计算引擎:批数据计算引擎 Spark ,流数据计算引擎 Flink 支持。这些大数据计算引擎为抽象计算模型,降低用户使用门槛,都会设计的一套符合标准 SQL 语义的开发语言,这其中也包含 UDF 函数。在 Flink 引擎中,用户可以使用 UDF 来在查询语句里调用难以用其他方式表达的频繁使用或自定义的逻辑
  • 云产品,非常多的云厂商都在发力这个方向,例如 Google BigQuery 允许用户从 SQL 查询调用以 JavaScript 编写的代码,阿里云的 MaxCompute 可以直接将 Java 或 Python 代码作为 UDF 嵌入 SQL。
  • 图数据库领域,事实上的图数据库查询语句标准 openCypher 存在 UDF 的定义,各大图数据库也都支持,例如 Tiger Graph 支持强绑定 C++ 语言的查询 UDF  ,Neo4j 支持外挂 Java 脚本式的 UDF

综上,不难得出,UDF 函数作为一种有效的拓展机制已经在各个数据系统间广泛应用。

而在 Nebula Hackathon 2021 比赛过程中,考虑到 Nebula Graph 在当时的最新版本 (2.6.1) 不支持 UDF,笔者的比赛初衷即是基于 Nebula 优秀的能力底座,为 Nebula 实现超越业界现有实现的 UDF 引擎。

Wasm-based plugin 珠玉在前

Wasm 作为前端领域诞生的技术,早已在前端广泛应用,例如 Ebay 基于 Wasm 为 Web 端提供条形码扫描功能。AutoCAD Web 通过 Wasm 将其 Native 应用移植到 Web 平台。

Wasm 在服务端的应用方兴未艾,有非常多的应用尝试在服务端使用 Wasm 技术,例如 YoMo 将 WasmEdge 嵌入到实时数据流中,然后通过 WasmEdge 进行数据的 AI 推理,也是将 Wasm 作为拓展机制使用。此外,也可以将 WasmEdge 部署在腾讯云函数上,不用运维,自动伸缩,按使用付费。

有一些开源项目选择将 Wasm 运行时嵌入程序内作为程序的拓展机制。

  • Istio 项目中的 Envoy 组件基于 Wasm 添加动态可扩展性代理过滤,大大简化了 Envoy 二次开发和功能增强的复杂度12。
  • MOSN 项目采用 Wasm 技术,给 MOSN 实现了一个安全隔离的沙箱环境,让扩展程序能够运行在隔离沙箱之中。

必须一提的是,在 TiDB 2020 Hackathon 中 ' or 0=0 or ' 团队基于 WASM 实现了 TiDB 的用户自定义函数是笔者已知的最早将 Wasm 和数据库结合的案例,也是笔者在 Nebula Hackathon 2021 上直接的灵感来源。

珠玉在前,可以看到 WASM 在服务端作为拓展机制的潜能无限。

业务需求

在讨论具体实现之前,笔者先结合图数据库本身的业务属性以及商业模式,来谈谈需求。

  • 复杂逻辑自举减少数据交互:Nebula 图数据库本身不支持某些复杂函数(例如 JSON Parse),或者是某些包含业务系统的逻辑处理(如 Join、 Filter)。通过 UDF 业务方可以实现一些通用逻辑的下沉,可以避免业务系统反复与数据库交互,减少网络通讯,增强性能。
  • 公有云平台增强可拓展能力:和现在非常多云厂商提供的云数据库产品一样,Nebula 图数据库存在名为 Nebula Graph Cloud 全托管数据库即服务(DBaaS)产品。作为公有云平台,为了统一管理运维的需要,一般都是部署二进制软件标品,不会对不同用户做定制。通过 UDF 可以让用户自己去完成自己的逻辑,但是这个逻辑需要能够有限控制在一个合理的空间内,既能满足用户的需求,也不会造成安全问题,比如用户不能穿透数据库到数据库所在物理机,或者是通过数据库发起一些网络攻击,但是他却可以看到自己的数据库一些内部指标,甚至把自己的业务逻辑嵌入到数据库内。
  • 私有化场景降低运维需求:作为开源软件,在 Nebula 图数据库赋能诸多企业业务腾飞的同时,不可避免会陷入到私有化维护的场景。深度使用 Nebula 的客户必然也有自己的拓展需求,这方面的拓展需求会花费 Nebula 的研发成本(不是所有的客户都具备 Nebula 的研发能力)去维护一些偏离主分支的代码,而且有些客户有自己的私密需求。Nebula 图数据库必须具备非常灵活的拓展机制来减少陷入维护深渊。在云原生趋势下,对厂商和客户扩展语言应该是包容的,不应该将用户的技术栈绑定到 Nebula 图数据库的原生语言 C++ 上。UDF 引擎允许多语言编写代码打包成 Wasm 文件,嵌入 Nebula 图数据库中执行。可以说,在私有化场景下,Nebula 图数据库仅需保证核心组件稳定,厂商私有代码不合并到 Nebula 中,应该具备按需扩展能力即可。

实现分析

痛点与期望

首先要回归本质,UDF 作为一种拓展机制, 笔者认为在讨论 UDF 设计的时候不可以忽略拓展机制需要具备的需求。

  • 安全隔离需求:代码的风险管控,运行时的程序能力限制、调用资源限制 。通过隔离沙箱避免 UDF 代码给 Nebula 运行造成的安全风险,如用户编写的 UDF 出现了问题,并不会影响 Nebula 本身的运行。
  • 弹性效率:支持动态下发、安装/卸载这些基础能力
  • 性能体积:接近原生代码 (C++) 的性能、均衡、功能和复杂度与大小成正比。
  • Write once, run anywhere,运行平台兼容 x86、ARM 等架构。

此外,作为一个以图数据库为底座的 UDF,也应该具备以下特质。

  • 可拓展性,可使用多种语言 (C/C++、Rust、Python、TypeScript) 编写函数逻辑,亦可引用其他 UDF 实现组合复用。
  • 复用现有基础设施:多语言支持的能力,允许模块化编程,封装函数组合调用,甚至直接复用社区 UDF。
  • 图数据结合:支持图数据中独有的数据结构(如顶点、边)作为函数内部可用的数据结构
  • 利用云的计算资源:利用现代云计算基础设施拓展计算能力,可对接云上函数计算 (AWS Lamda /阿里云 Function Compute),释放海量算力。
  • 更为复杂的功能:超越传统的 UDF 简单等数据聚合、字符串拼接需求,引入复杂计算逻辑,比如机器学习推理、ID-Mapping 等

实现讨论

虽然在前文已经详述了笔者的最终方案是使用 Wasm,笔者认为仍有必要对 Wasm 的使用进行探讨。

下面的图片展示了多种 UDF 方案的实现对比。笔者可以在前文列举过很多数据系统的 UDF 实现,他们都是内部实现 DSL(例如Flink)、外挂 so(例如MySQL)、内嵌脚本这几个实现方式,有其一定的局限性。

接下来我们从是否具备完备的 C++ SDK、性能体积、社区活跃度、是否好嵌入以及文档详细程度上选择具体 Wasm 虚拟机,最终我们选择了 WasmEdgeWasmtime

具体实现

总架构图

单模块介绍

编译工具链:作用是快速转换已有代码成 Wasm。这个模块比较杂乱,是我们开发调试使用 Wasm 程序,面向比赛 Demo 使用的。

语法解析:基于 Flex 与 Bison 实现创建函数,删除函数的 SQL 语句。实现语法可以见下图,我们这里考虑到 WebAssembly 文本格式(wat),Wasm 二进制文件两种主流的格式。以及其加载方式,分为两种,第一种是方便在终端里直接输入的 wat base64 编码以及 Wasm 二进制文件 base64 编码,运行大小在 KB 级别的程序可以直接引入,第二种是 MB 级别的 Wasm 程序二进制文件,支持通过 HTTP 地址引入以及本地文件地址引入。

函数管理:负责对 Wasm 虚拟机进行统一管理,提供函数的动态更新、加载、卸载功能。也可以说是 Nebula 其他系统与 Wasm 拓展组件交互的中间胶水。

虚拟机:这里主要是引入 Wasmtime、WasmEdge 两个虚拟机的 C++ SDK,通过调用 SDK 来实现 wat 代码编译、Wasm 二进制文件编译执行、沙箱实例管理、WASI 特性管理等。

流程介绍

下图是上文所述的函数创建,调用,删除的主要流程图。

  • 当有函数创建时,将语法解析后得到的函数名称(name),函数运行参数(params),函数返回参数(return type),函数本体(code or path)。对通过 base64 解码、HTTP 下载、文件读取操作三种手段数本体进行加载,加载后调用虚拟机接口进行编译,得到编译后的运行上下文(Run context)。将函数运行参数、返回参数、运行上下文进行组合拼接成一个可以后续直接虚拟机使用的函数运行沙箱(Sandbox)。以函数名称为 Key、函数运行沙箱为 Value 存储在内存哈希表(Mem Table) 中。
  • 在函数被调用时,以函数名称为 Key 在内存哈希表找到函数运行沙箱,对传入参数进行参数校对,将Nebula 内的数据类型(C++ Data Struct)转换成虚拟机可以接受的参数(Wasm Data Struct),请求虚拟机执行函数运行沙箱中的运行上下文,得到运行结果,将返回参数转换成 Nebula 内的数据类型,返回上层系统。
  • 当有函数删除时,即删除内存哈希表中的函数运行沙箱。

碍于篇幅,本文不对模块内具体实现进行介绍,感兴趣的读者可以阅读开源代码来了解细节。

Demo

下面是比赛时候的2个 Demo 视频,充分展示了整个流程,可以看到 Wasm 作为拓展的能力。Demo 基于 WasmEdge Runtime,可以在开源代码中找到。

基于 WasmEdge Runtime 的字符串拼接程序

这个 Demo 展示的是一个非常简单的字符串拼接,这个函数并不是 Nebula 图数据库自有的。

视频 demo 见:https://www.bilibili.com/video/BV1JP4y1u7LJ/

传入一个字符串 world,返回拼接后的字符串 hello world。需要注意的是,这个拓展尝试传递字符串到Wasm虚拟机中,而 WebAssembly 规范并不支持字符串。我们这里使用 wasm_bindgen 工具编译 Rust 程序为 Wasm程序,通过将字符串映射到虚拟机的内存地址上来完成。这一部分的技巧可以参阅 WasmEdge 文档或 Demo 的开源代码。

基于 WasmEdge Runtime 的飞书聊天机器人

这个 Demo 展示如何从数据库发送消息到飞书机器人,首先我们通过飞书软件获取到聊天机器人的接口,这个接口是 HTTP Post 请求。接下来基于 WasmEdge 提供的 wasi socket 能力,将具有网络请求功能的UDF嵌入到数据库中,执行函数,可以看到程序成功请求飞书成功输出内容。

视频 demo 见:https://www.bilibili.com/video/BV1XL4y1T72X/

之所以考虑这个演示程序是因为数据库传统 UDF 的功能主要是自定义数据聚合,存储过程这些没有脱离数据库原本业务属性与能力的功能。笔者认为展示一个在功能丰富性完全超越数据库传统 UDF 的 Wasm-based plugin 机制更有实际意义。如果业务系统能够合理的在多个系统内部装载 Wasm 的拓展作为 Hook(钩子)机制,使用者就可以在不大量改动原业务系统上加入自己的 Wasm 拓展程序,可以实现非常丰富的自定义功能,我们可以考虑用这个来提高程序的可观测性,例如在数据库收到一个慢查询语句时,触发 Hook,将慢查询情况通过内部 IM 发送给数据库管理员,相信这里的想象力是非常多的。

缺陷与前景展望

缺陷

在比赛后,笔者也有非常多的遗憾,下面的缺陷讨论实际上更像是笔者对一个完备的 Wasm 拓展机制应该具有特质的期望。

没有实现更加动态的函数以及运行时管理以及运行机制

  • 由于是在图数据库内做的 UDF,优先考虑了以 SQL 形式来实现函数的增删改。在笔者的最初设想中,整体的 Wasm UDF 功能应该是存在一个动态配置,这个配置可以限制沙箱实例个数、使用的虚拟机、虚拟机的权限(例如 WASI 提供的网络,IO 权限)等。
  • 运行机制上,在设计之初,笔者也考虑到将一部分具体函数执行放在云上进行执行(例如 AWS Lamda),将结果回传到数据库,调研发现 WasmEdge Runtime 提供了网络能力。但是到具体实现时,需要考虑的点还是比较多的,比如如何协同这种“云 UDF ”和本地 UDF 的交互、如何规划函数运行回调等,最终没有完成这个工作。

从 Wasm 函数内部调用 Nebula 图数据库的接口没有实现

  • 笔者的工作局限在 Nebula 图数据库调用 Wasm 函数,这是最直接的 Wasm 使用方式,但是 Wasm 本身可以通过接口来调用 Host Function,笔者也在 WasmEdge C SDK 的 Doc 中找到了类似的方法。
  • 但是受制于 Wasm 的安全机制,任何对外部资源的访问必须通过导入模块来间接实现,需要将 Nebula 图数据库的内部 API 的入参出参上再加一层数据转换传入到 WASM 内部,需要统一抽象好外部的数据结构在 Wasm 程序内的映射。
  • 这些工作是富有挑战的,也是现在将 Wasm UDF 落地的比较难的原因。如果可以做到这些,我们就可以不仅仅局限在 UDF 这一个小的方向,可以在上层应用的多个程序上嵌入我们的 Wasm 沙箱,做到 Hook 的机制。

性能情况的考察

  • 没有考察 Wasm 程序在处理大规模数据时候的性能情况。

缺乏更加多样复杂的功能

  • 现有应用迁移适配需要一定工作,笔者尝试将 jq (一个 C 编写的 JSON 字符串处理器)嵌入到图数据库上,来让图数据库直接支持 JSON 类型字符串的处理,但是尝试过程中发现编译较为繁琐,且导入到 VM 中运行不顺利。
  • 在考察过 WasmEdge Runtime 支持使用 Tensorflow 做机器学习推理后,笔者设想在 Wasm 内部加载一个结构化数据的模型,读入 Nebula 图数据库存储的数据,跑一个简单的聚类或者回归,做一些和上层图数据相关的推理服务,或者将一个完整的图算法编译成 Wasm。这些功能都可以体验 Wasm 作为一种拓展机制胜过传统 UDF 的能力。

前景

从技术角度考虑,面对越来越多的拓展场景,Wasm 可以提供完整可靠的隔离环境,拓展代码无法对上层应用的运行造成安全风险。虽然V8、JVM、Lua等虚拟机也具备相同的安全能力,但是性能体积、多语言支持、拓展性上没办法超越 Wasm。

虽然笔者在文中主要介绍的是图数据库 UDF 的实现,但是经验同样适用于任何想用 Wasm 做扩展的项目,得益于 WasmEdge、Wasmtime等虚拟机已经屏蔽了非常多的底层执行细节,大部分工作集中在宿主机与 Wasm 扩展程序之间的交互、函数注册删除、函数调用、数据传输等,笔者认为想要非常迅速的接入 Wasm,体验 Wasm 带来的拓展体验是非常容易的。

本文中的 Nebula 图数据库所代表的云上数据服务或者说软件服务,都可以说是一种 SaaS 平台,如果 SaaS 平台允许类似 UDF 这样的用户自定义代码直接上传到平台本身,开发者就不需要维护处理回调的中间层来减少数据链路,也不用管理基础设施,而且更好的复用 SaaS 平台现有 API,安全高性能的同时也保证了更高的可拓展性。

关于 WasmEdge

WasmEdge 是轻量级、安全、高性能、实时的软件容器与运行环境。目前是 CNCF 沙箱项目。WasmEdge 被应用在 SaaS、云原生,service mesh、边缘计算、汽车等领域。

 ✨ GitHub:https://github.com/WasmEdge/WasmEdge

 💻 官网:https://wasmedge.org/

 👨‍💻‍ Discord 群:https://discord.gg/JHxMj9EQbA

 👨‍💻‍ 文档:https://wasmedge.org/book/en

展开阅读全文
加载中

作者的其它热门文章

打赏
0
1 收藏
分享
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部