暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

项目经验分享|PolarDB开源社区 乔奥:志不强者智不达!

晚星 2023-07-12
149

本期项目经验分享来自乔奥同学,承担的项目是【PolarDB-X 支持 UDF】。

一、About

关于 PolarDB 开源社区

项目介绍
PolarDB 开源社区是阿里云数据库开源产品 PolarDB 的技术交流平台。作为一款开源的数据库产品, 离不开用户和开发者的支持, 大家可以在社区针对 PolarDB 产品提问题、功能需求、交流使用心得、分享最佳实践、提交 issue、贡献代码等。
PolarDB开源社区

PolarDB-X 是一款面向超高并发、海量存储、复杂查询场景设计的云原生分布式数据库系统。其采用 Shared-nothing 与存储计算分离架构,支持水平扩展、分布式事务、混合负载等能力,具备企业级、云原生、高可用、高度兼容 MySQL 系统及生态等特点。

PolarDB-X 最初为解决阿里巴巴天猫“双十一”核心交易系统数据库扩展性瓶颈而生,之后伴随阿里云一路成长,是一款经过多种核心业务场景验证的、成熟稳定的数据库系统。

PolarDB-X 是阿里巴巴自主设计研发的高性能云原生分布式数据库产品,为用户提供高吞吐、大存储、低延时、易扩展和超高可用的云时代数据库服务。PolarDB-X 始终保持对阿里巴巴集团“双十一购物狂欢节”所有相关业务的全面支撑。历经十余年淬炼,PolarDB-X 具备了强数据一致性、高系统稳定性、快速集群弹性等核心关键特性,并在司法财税、交通物流、电力能源等公共事业领域有广泛深入应用。PolarDB-X 坚定遵循自主可控、开放生态的发展思路,持续围绕 MySQL 开源生态构建分布式能力,以求最大程度降低用户的学习使用成本。

作为全球数据库领导者,阿里云数据库坚定拥抱开源,多年来积极参与开源社区建设,为 MySQL、PostgreSQL 等社区做过多项贡献。2021 年,阿里云把数据库开源作为重要战略方向,正式开源自研核心数据库产品 PolarDB,助力开发者和客户通过开源版本快速使用阿里云数据库产品技术,并参与到技术产品的迭代过程中来。2021 年 5 月,阿里云率先开源 PolarDB for PostgreSQL 分布式版,在 10 月的云栖大会上,阿里云进一步开源了云原生分布式数据库 PolarDB-X 和 PolarDB for PostgreSQL 共享存储版,聚合社区力量,繁荣云原生分布式数据库生态,服务广大开发者,推动技术变革。

2022 年 10 月份,PolarDB-X 正式发布 2.2.0 版本,这是一个重要的里程碑版本,重点推出符合分布式数据库金融标准下的企业级和国产 ARM 适配,共包括八大核心特性,全面提升 PolarDB-X 分布式数据库在金融、通讯、政务等行业的普适性。

项目介绍

项目名称:PolarDB-X 支持 UDF

项目编号:2209e0087

项目链接:https://summer-ospp.ac.cn/#/org/prodetail/2209e0087

项目导师:玄弟

项目仓库:https://github.com/polardb
项目需求:数据库中 SQL 函数可以大致分为以下几类:内部函数、系统生成的函数、用户定义的函数(UDF),其中用户定义的函数是指由用户通过 CREATE FUNCTION 显示创建并命名的函数 ,其语义也由用户自己确定,能够在一定程度上扩展数据库的功能。本任务包含的功能点:
· PolarDB-X 支持 CREATE FUNCTION 创建 UDF
· UDF 注册到 CN(Compute Node),在 CN 端完成相应注册

·支持系统视图,可以方便查询和管理已注册 UDF

二、项目开发方案描述

方案介绍

UDF 全称用户自定义函数,用于扩展数据库中的系统内置函数,便于用户实现自定义功能。本文将从如下几个方面对 PolarDB-X 中的 Java UDF 设计实现方案进行介绍。

UDF 在其他常见数据库中的实现

目前市面上常见的数据库中,对于 UDF 的实现主要分为两类:一是用户在命令行直接输入函数代码、参数类型等等,例如 MaxCompute 以及 Snowflake;二是用户编写 UDF 后打包生成 jar 文件后上传到数据库中,例如 Hive、StarRocks 以及 Flink。下面对常见的数据库中的 UDF 的语法和相应优缺点进行简要的介绍和分析。

Snowflake

create or replace function concat_varchar_2(a ARRAY)
returns varchar
language java
handler='TestFunc_2.concatVarchar2'
target_path='@~/TestFunc_2.jar'
as
$$
    class TestFunc_2 {
        public static String concatVarchar2(String[] strings) {
            return String.join(" ", strings);
        }
    }
$$;

- 无需继承,自定义类和方法即可

- 需指定 handler 以及目标路径

优缺点分析:语法简单,方便用户操作和编写;当用户函数复杂时,需要在命令行中输入更长的内容,相对繁琐。

MaxCompute

代码嵌入式 UDF:

CREATE TEMPORARY FUNCTION foo AS 'com.mypackage.Reverse' USING
#CODE ('lang'='JAVA')
package com.mypackage;
import com.aliyun.odps.udf.UDF;
public class Reverse extends UDF {
  public String evaluate(String input) {
    if (input == null) return null;
    StringBuilder ret = new StringBuilder();
    for (int i = input.toCharArray().length - 1; i >= 0; i--) {
      ret.append(input.toCharArray()[i]);
    }
    return ret.toString();
  }
}
#END CODE;

SELECT foo('abdc');

- 嵌入式代码块可以置于`USING`后或脚本末尾,置于`USING`后的代码块作用域仅为`CREATE TEMPORARY FUNCTION`语句。

- `CREATE TEMPORARY FUNCTION`创建的函数为临时函数,仅在本次执行生效,不会存入 MaxCompute 的 Meta 系统。

优缺点:对用户相对友好,语法上比较直观;但是需要输入的内容过于繁琐,包括包名、类代码等等。

HIVE

hive> add jar /home/data/HiveUDF.jar;
hive> create temporary function zodiac as 'mastercom.hive.udf.ZodiacUDF'; #注册临时函数

hive> create function hive.zodiac as 'mastercom.hive.udf.ZodiacUDF' using jar 'hdfs://192.168.1.101:8020/script/HiveUDF.jar'; #永久注册函数

- 用户代码需要继承 UDF 类。

- 实现 evaluate()方法,UDF 实现的功能在 evaluate 里实现。Hive 根据类名来创建 UDF,调用的时候根据 evaluate 参数来调用不同的方法,实现不同的功能。

优缺点分析:用户直接继承 UDF 类,实现起来相对容易。但是需要打包成 jar,操作变得复杂,同时还要考虑 jar 包的上传和储存问题,增大了用户使用的成本。

StarRocks

CREATE FUNCTION MY_UDF_JSON_GET(string, string) 
RETURNS string
properties (
    "symbol" = "com.starrocks.udf.sample.UDFJsonGet", 
    "type" = "StarrocksJar",
    "file" = "http://http_host:http_port/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);

- 用户自定义类必须实现 evaluate 方法

- 无需继承

- 需将 jar 包上传至 FE 和 BE 能访问的 HTTP 服务器,并且 HTTP 服务需要一直开启。FE 会对 UDF 所在 Jar 包进行校验并计算校验值,BE 会下载 UDF 所在 Jar 包并执行。

优缺点分析:与 HIVE 类似,不但需要打包形成 jar 文件,还需要自行部署文件服务,使用成本很高。

FLINK

CREATE FUNCTION to_json AS 'com.shuyun.dpe.flink.udf.ToJsonFunction';

- 继承 ScalarFunction,实现 getName()和 eval()

- 将打包好的 jar 放到 flink 集群的 lib 目录下,重启 flink 集群

- 声明函数

优缺点:FLINK 函数与 PolarDB-X 中的 scalar 函数比较相似,处理过程会相对简单;但 jar 包的缺点与上文提到的类似,但避免了上传过程。

PolarDB-X JAVA UDF 语法设计

PolarDB-X 中的 Java UDF 在语法设计上参考了 MaxCompute 数据库以及 Snowflake 数据库,采用命令行输入用户自定义代码以及其他参数的形式。为了简化用户输入的代码,我们通过模版类的形式来辅助用户生成 UDF,用户只需要专注于编写自己的函数逻辑,在`CODE`关键字后输入相应的函数代码即可,简化了用户的操作和使用。具体语法如下:

创建 UDF

CREATE FUNCTION <函数名> RETURNTYPE <返回值类型> INPUTTYPE <输入类型1, 输入类型2...>
IMPORT <引用声明> ENDIMPORT
CODE
public Object compute(<T> args1, <T> args2, ...) {
   <用户函数代码>
}
ENDCODE
删除 UDF
DROP FUNCTION [IF EXISTS] <函数名>

显示所有 UDF

SHOW FUNCTION;
通过以上语法,用户可以对 UDF 进行创建删除和查看。

 

PolarDB-X JAVA UDF执行流程设计

在 PolarDB-X 中,存在一些系统内置的扩展函数,例如 Sin() 函数、Cos() 函数、Sub() 函数等等,这些函数由`ExtraFunctionManager`进行统一管理,并将对应的构造函数保存在缓存中,在执行过程中根据函数名进行相应的调用。

我们通过实现一个类似的`UserDefinedJavaFunction`类,将用户注册的 UDF 构造函数同样保存在缓存中,按照系统内置扩展函数的逻辑对其进行调用。

系统内置函数的执行流程

加载过程:

一个扩展函数需要继承 AbstractScalarFunction,并实现 compute()以及 getFunctionNames()函数以便后续的统一调用。在 PolarDB-X 的初始化阶段,

CobarServer.warmup()

触发

ExtraFunctionManager.getExtraFunction("warmup", null, null);

来加载 ExtraFunctionManager 类。

ExtraFunctionManager 中的静态方法 initFunctions() 将会做以下两件事:

-自动扫描 IExtraFunction 对应 Package 目录下的所有 Function 实现

-自动扫描 Extension 扩展方式下的自定义实现,比如在 META-INF/tddl 或 META-INF/services 添加扩展配置文件并通过 addFunction() 函数来进行函数签名的注册工作。在函数的执行过程中,使用了一个 HashMap 来储存函数签名以及对应的构造器,定义如下:

private static Map<FunctionSignature, Constructor<?>> functionCaches = new HashMap

 

调用过程:

Java UDF 的执行流程

加载过程:

类似的,我们为所有 UDF 定义了一个父类 UserDefinedJavaFunction,其继承了AbstractScalarFunction。用户只需实现简化后的 compute() 方法即可,降低了使用成本。我们采用 UserDefinedJavaFunctionManager 来管理所有的 Java UDF。

在 PolarDB-X 的初始化阶段,

CobarServer.warmup()

触发

 UserDefinedJavaFunctionManager.getExtraFunction("warmup", null, null);

来加载UserDefinedJavaFunctionManager类。

UserDefinedJavaFunctionManager 中的静态方法将会在对已经进行持久化的函数进行加载和注册,并通过 addFunction() 函数来进行函数签名的注册工作。这里我们同样适用了一个 HashMap 来存储函数名以及构造函数,定义如下:

private static Map<String, Constructor<?>> javaFunctionCaches = new HashMap<>();

调用过程:

PolarDB-X Java UDF创建及删除过程设计

创建过程:

对于用户输入的 UDF 创建语句,polardb-x 会对其进行解析,以获得后续执行所需的函数名、输入输出类型、引用声明以及用户逻辑代码。接着,我们会将这些关键参数进行拼接,以生成一个字符串形式的类文件,类名对应函数名。该类继承内部定义的 UserDefinedJavaFunction ,并实现了相应的 compute() 以及 getFunctionName() 方法。接着通过使用开源框架 Janino 的相应 API,把该类编译并加载到 JVM 中。这里我们使用了一个 Map 来存储函数名以及其构造方法,方便在后续调用过程中进行调用

public static Map<String, Constructor<?>> javaFunctionCaches = new HashMap<>();

最后,将其注册到相应的运算符表中,防止解析过程中无法识别函数关键字。在创建过程中我们将进行持久化的工作。

删除过程:

删除过程是创建过程的逆过程,在 javaFunctionCaches 中删除以后,还需要在 MetaDB 中删除持久化存储。

查询过程:

查询过程是基于 javaFunctionCaches 实现的逻辑查询,并不涉及 MetaDB 的查询工作。当用户输入查询语句 SHOW FUNCTION 时,将返回 javaFunctionCaches 的所有键值。

类型拦截:

在 polardb-x 中,定义了很多内部类型,而在函数的计算过程中,经常会使用到内部自定义的类型来进行计算。但是在 Java UDF 的创建过程中,用户并不了解会用到有哪些内部类型,同时也不能将这些内部类型暴露给用户。因此需要在 Java UDF 的创建过程以及执行过程中对内部类型进行拦截,并转化成为用户期望且已知的数据类型(即 Java 类型)。具体实现过程如下图所示:

对于polardb-x内置的函数而言,需要继承 AbstractScalaFunction 父类,同时实现 IscalaFunction接口的 compute() 以及 getFunctionName() 方法。参照这种思路,我们定义了一个 UDF 的父类:UserDefinedJavaFunction 类,其继承 AbstractScalaFunction 父类,在类内部实现了接口需要的 compute 方法,并添加一个入参更简洁清晰的抽象 compute 方法。在复写的 compute 方法内部,我们会对入参进行长度检验、类型转换,并将转换后的参数传递进入抽象 compute 中;用户只需要复写抽象的 compute 方法,即可获得相应的参数并实现自己的逻辑。

函数持久化

对于用户来说,一般更希望自己注册的函数能够在 polardb-x 重启后仍然存在于系统中,直至手动删除。因此,对于用户函数的持久化工作十分重要。我们使用 polardb-x 存储重要信息的 MetaDB 实现持久化工作,将用户定义的函数存储在 MetaDB 中,避免丢失。表结构具体如下:

create table if not exists `user_defined_java_code` (
              `func_name` varchar(100) not null,
              `class_name` varchar(100) not null,
              `code` text not null,
              `code_language` varchar(100) not null,
              `input_types` varchar(200) not null,
              `result_type` varchar(100) not null,
              primary key(`func_name`)
            ) charset=utf8
将函数名作为主键避免函数重复与冲突。在用户创建 UDF 时,向表中插入一条记录;删除 UDF 时,根据函数名删除对应记录;在系统的初始化阶段,从表中加载所有的数据,并将其逐一加载。

节点间同步

polardb-x 常常被用于集群化部署,如果用户在某一个节点上创建了一个 UDF,那么其他节点上也应该能够使用这个 UDF。这里通过使用节点间的通讯的手段来实现。当一个节点上创建了 UDF 时,将会通过 SyncManagerHelper.sync() 方法将函数名传递给其他节点。其他节点获取到相应函数名以后,在 MetaDB 中搜索相应表记录并加载注册。同样的道理,当某个节点删除了一个 UDF 时,函数名同样会传递给其他节点,以便进行删除操作。

三、随访

参与开源之夏

ospp:请简单介绍一下自己和你的开源经历吧。
乔奥:在大学期间我就经常使用一些开源的小工具来提升自己的电脑使用体验以及开发效率,也动手在云服务器上搭建过一些 toy 项目。在我的眼中,开源社区所在做的事情都非常的 geek,这一点深深地吸引了我,使得我一直在关注着国内外这些开源社区和项目的发展。平时我也会将自己做的课设开源分享出来,但是很遗憾,一直没有时间和机会正式参与到一个大型开源项目中。这次的 PolarDB-X 之旅算得上是我的第一次正式的开源经历。
ospp:请问你是通过什么渠道了解到开源之夏的?参与开源之夏活动的过程中你都做了哪些准备?
乔奥:在我大三的时候,看到学校有同学参加过开源之夏这个活动,觉得很有趣,便默默关注了开源之夏的官方公众号,今年开放报名以后也在第一时间提交了报名申请表。在撰写项目申请表时,我和社区的玄弟老师进行了很多次的电话沟通,逐步了解了项目的细节,这对之后的成功申请和顺利结项意义重大。

ospp:你的实习经历很丰富,请问这对于你此次所参与的项目有什么帮助吗?

乔奥:实习比较大程度上提升了我的编码能力以及解决问题的思考能力,因为在参加这个活动之前,我没有了解过 UDF 的相关知识,也没有接触过数据库内核的开发,需要我在比较短的时间内了解这些领域,并尝试给出一个初步的解决方案,这对我来说还是很大的挑战,因此我在实习过程中养成的学习能力发挥了比较大的作用。尤其是我的第二段实习,主要在做数据中间件,接触到了几十种数据库领域的很多知名产品,这也很大程度上帮助我理解了 PolarDB-X。

 

参与开源社区

ospp:介绍一下你眼中的 PolarDB 开源社区吧。

乔奥:PolarDB-X 虽然开源的时间不算很长,但是社区内部还是非常活跃的,可以看到官方每隔一段时间都会合并一部分内部代码进入开源项目中。同时虽然贡献者没有顶级开源项目那么多,但是基本上每个 issue 都有问必答,社区的 commiter 们都非常积极,各个贡献者们也在努力地发挥自己的一份力。

 

ospp:在项目进行过程中有没有印象深刻的经历?社区和导师为你提供了怎样的帮助?

乔奥:在社区的三个月以来,我从只能提交一些拼写错误 pr,到输出社区 debug-CN 文档,再到掌握系统中函数执行流程,输出自己的解决方案并实现,离不开两位老师的循循善诱和耐心指导。例如仅在实现用户函数接口这一环节,前后经历了三次设计变动。在设计初始,我并没有考虑到该接口涉及到很多内部数据类型,因此简单的认为用户可以直接操作这些数据类型,但在简单测试以后便发现了其中的问题;在越寒老师的指导下,我设计了第二版方案,即对用户函数的输入进行拦截,将传递给用户的函数参数都提前进行类型转化,转为 java 的常用类型,此时用户实现的方法为`Object compute(Object[] args)`;此时虽然解决了数据类型的问题,但输入参数类型全变成了 Object 类,用户使用起来相当繁琐,在玄弟老师的指导下,我对这部分进行了进一步的优化,通过使用反射的方式,实现对于用户实现的函数的获取,此时用户只需要编写签名为`Object compute(<T> args1, <T> args2, ...)`即可,大大简化了用户的操作。

 

ospp:在本次开源之夏项目成功结项后,你有继续投入到开源社区后续发展的打算吗?

乔奥:当然。非常感谢开源之夏给了我这个机会,能够参与到 PolarDB 这么大的社区里来,这对我来说弥足珍贵。实际上,就在上周我又帮助社区修复了一个 bug,并提交了 pull request,同时我也经常性地关注社区中的 issue,看看有什么是我能为社区做的事情。能够为社区做出贡献、和社区内的各位贡献者交流互助是一件很令我快乐的事情。

 

收获与寄语

ospp:开源之夏旨在培养和发掘更多优秀的开发者,通过这次的活动你有什么意想不到的收获和成长吗?

乔奥:经过这次开源之夏的活动,我更深一步涉猎了数据库开发这一领域,也激发了我对于基础技术研究和开发的兴趣。最近我也在持续的关注这一领域的论文和产品,并将自己研究生阶段的研究方向确定为了数据库索引相关的方向。以我目前的接触来看,我觉得学习这一领域的关键在于多看和多读:多看一些概念性、逻辑性的描述,掌握这个领域的术语和基础知识,然后就应该具体得看一看各个产品的实现代码,才能参透其中的内涵。作为一个开发者而言,对代码的探索应该是放在第一位的,但同时也要注重自己的文字表达和画图表达能力,这些也很重要。

 

ospp:对于爱好开源的学弟学妹你有什么想对他们说的吗?

乔奥:我觉得对于一个开源项目最好的了解方法就是成为它的使用者,最好的掌握方式就是成为它的开发者。但是在成为开发者之前应该专注于夯实自己的基础,例如语言、数据结构等等,这样能在参与开源项目的过程中少走很多弯路,也避免给他人带来不必要的工作量。同时,参与开源的方式不止一种,如果暂时不能进行代码开发,输出文档、维护项目官网等工作也是很不错的工作。

小结

在项目的完成过程中,我遵循项目的开发规范,和代码风格,及时与社区的导师沟通并进行 code review。同时也经常自己进行 review,保证代码风格与简洁性,积极使用⼀些语法糖并适时添加注释、规范使用函数名、变量名、类名等等,保障自己代码的易读性和可维护性。整个开发过程中,我按照预先设定的进度稳步进行,并及时输出设计文档等。总的来说,我对自己的整个项目的开发质量比较满意。
在项目的进行过程中,来自社区的两位导师(玄弟老师和越寒老师)给予了我极大的帮助,我们基本保持着每周一次线上会议的形式进行沟通,两位导师有问必答,循循善诱,给予了我非常大的帮助。不仅提供设计思路上的指导,同时也会在代码风格、实现方式上予以提示,我在与两位导师的交流中受益匪浅。
在项目的完成过程中,两位导师严谨的态度给我留下了很深的印象,使我在编写代码的过程中不自觉地加强了对自己代码风格的注意,自己的编码能力和水平也得到了长足的进步。这次开源的经历给我留下了很深的印象,不仅增长了能力和见识,同时也对未来努力的方向更加地清晰。希望自己能够继续为社区以及更多的开源项目贡献出自己的一份力。
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论