Java 外表插件
通常情况下,数据都是存储在数据库自己的存储空间内,而外表功能可以让OceanBase访问存放在外部的数据。
外表插件尝试通过插件的方式扩展外表访问的能力,当前使用Java语言实现了外表插件。
如何使用
环境准备
此插件支持OceanBase 4.4.1 及以上版本,并依赖 Java 运行时,使用此功能需要确保:
- OceanBase 版本不小于 4.4.1;
- JDK 不小于 11;
- OceanBase 外表插件 Jar 包;
- MySQL 客户端工具,比如 mysql、mariadb、obclient 等,或其它可以连接MySQL的客户端工具。
修改 OceanBase 配置项并重启
-- 启用 Java
alter system set ob_enable_java_env='true';
-- 设置 JDK 的 HOME 目录,通常设置为环境变量的 $JAVA_HOME 即可。
alter system set ob_java_home='`/java/home/path`';
-- 设置一些Java运行时选项,可以是一些调试选项。
-- 可以增加自己的选项,但是最少包含下面几个选项。
-- 可以使用该选项配置 CLASSPATH,比如 -Djava.class.path=/java/class/path1:/java/class/path2
alter system set ob_java_opts='-XX:-CriticalJNINatives -Djdk.lang.processReaperUseDefaultStackSize=true --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED -Darrow.allocation.manager.type=Netty';
-- 设置 OceanBase 外表插件jar包目录。只需要设置目录,不需要写jar包名称,OceanBase 会自动展开目录下的jar包。
alter system set ob_java_connector_path='/jdbc/plugin/jar/package/directory';NOTE:
- JDBC 依赖的底层是内置C++插件,不需要安装动态链接库。
- 可以添加一些额外的Java启动选项,比如一些GC调试信息 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC -XX:+PrintReferenceGC -XX:+PrintGCApplicationStoppedTime
上述步骤完成后,重启OceanBase,检查是否安装成功:
select * from oceanbase.gv$ob_plugins where type='EXTERNAL TABLE' and status='READY';最少可以看到三个插件:
+--------------+----------+-------+--------+----------------+---------+-----------------+------------------+-------------------+-----------------------+---------------+-----------------------------------------------------+
| SVR_IP | SVR_PORT | NAME | STATUS | TYPE | LIBRARY | LIBRARY_VERSION | LIBRARY_REVISION | INTERFACE_VERSION | AUTHOR | LICENSE | DESCRIPTION |
+--------------+----------+-------+--------+----------------+---------+-----------------+------------------+-------------------+-----------------------+---------------+-----------------------------------------------------+
| 127.0.0.1 | 55803 | java | READY | EXTERNAL TABLE | NULL | 0.1.0 | NULL | 0.2.0 | OceanBase Corporation | Mulan PubL v2 | This is the java external table data source plugin. |
| 127.0.0.1 | 55803 | jdbc | READY | EXTERNAL TABLE | NULL | 0.1.0 | NULL | 0.2.0 | OceanBase Corporation | Mulan PubL v2 | This is a java external data source |
| 127.0.0.1 | 55803 | mysql | READY | EXTERNAL TABLE | NULL | 0.1.0 | NULL | 0.2.0 | OceanBase Corporation | Mulan PubL v2 | This is a java external data source |
+--------------+----------+-------+--------+----------------+---------+-----------------+------------------+-------------------+-----------------------+---------------+-----------------------------------------------------+
其中 'java' 是Java外表插件的通用实现,'jdbc' 是一个Java JDBC数据源,基于 'java' 插件实现,'mysql' 是一个MySQL JDBC数据源,基于 'jdbc' 插件实现。
如何构建
首先确保你的环境上安装了Java开发环境,并且版本大于等于 11。
git clone https://github.com/oceanbase/oceanbase-plugins.git
cd oceanbase-plugins/external_table
mvn clean package执行完成后,可以找到 jar 包,这个包是我们需要用到的插件包:
./internal/target/oceanbase-external-plugin-0.1.0-SNAPSHOT-jar-with-dependencies.jar其中 0.1.0 是版本号。
如果你的环境中安装了多个版本的JDK,可以使用下面的命令指定使用的JDK版本来打包:
JAVA_HOME=/usr/lib/jvm/jdk-19-oracle-x64/ mvn clean package测试使用
我们将介绍如何通过外表访问MySQL数据库,因此需要有一个MySQL数据库(OceanBase 也有MySQL模式,因此也适用于OceanBase)。
假设你的测试环境信息是这样的:
- MySQL 数据库(测试数据源),运行机器 192.168.0.10,端口 3306。测试账号
root,密码为空; - OceanBase 数据库,运行机器 192.168.0.20,端口 55800,测试账号
root,密码为空。
那么连接 MySQL 和 OceanBase 的命令是:
# 连接MySQL的 test 数据库
mysql -h 192.168.0.10 -P 3306 -uroot -Dtest -c -A
# 连接 OceanBase 的 test 数据库
mysql -h 192.168.0.20 -P 55800 -uroot -Dtest -c -A在MySQL数据库中创建表 lineitem:
CREATE TABLE `lineitem` (
`l_orderkey` bigint(20) NOT NULL,
`l_partkey` bigint(20) NOT NULL,
`l_suppkey` bigint(20) NOT NULL,
`l_linenumber` bigint(20) NOT NULL,
`l_quantity` bigint(20) NOT NULL,
`l_extendedprice` decimal(10,2) NOT NULL,
`l_discount` decimal(10,2) NOT NULL,
`l_tax` decimal(10,2) NOT NULL,
`l_returnflag` char(1) DEFAULT NULL,
`l_linestatus` char(1) DEFAULT NULL,
`l_shipdate` date DEFAULT NULL,
`l_commitdate` date DEFAULT NULL,
`l_receiptdate` date DEFAULT NULL,
`l_shipinstruct` char(25) DEFAULT NULL,
`l_shipmode` char(10) DEFAULT NULL,
`l_comment` varchar(44) DEFAULT NULL,
PRIMARY KEY (`l_orderkey`,`l_linenumber`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;插入一些测试数据:
INSERT INTO `lineitem` (
`l_orderkey`, `l_partkey`, `l_suppkey`, `l_linenumber`, `l_quantity`, `l_extendedprice`,
`l_discount`, `l_tax`, `l_returnflag`, `l_linestatus`, `l_shipdate`, `l_commitdate`,
`l_receiptdate`, `l_shipinstruct`, `l_shipmode`, `l_comment`
) VALUES
(1, 155190, 7706, 1, 17, 21168.23, 0.04, 0.02, 'N', 'O', '1996-03-13', '1996-02-12', '1996-03-22', 'DELIVER IN PERSON', 'TRUCK', 'egular courts above the'),
(1, 67310, 7311, 2, 36, 45983.16, 0.09, 0.06, 'N', 'O', '1996-04-12', '1996-02-28', '1996-04-20', 'TAKE BACK RETURN', 'MAIL', 'ly final dependencies: slyly bold '),
(1, 63700, 3701, 3, 8, 13309.60, 0.10, 0.02, 'N', 'O', '1996-01-29', '1996-03-05', '1996-01-31', 'TAKE BACK RETURN', 'REG AIR', 'riously. regular, express dep'),
(1, 2132, 4633, 4, 28, 28955.64, 0.09, 0.06, 'N', 'O', '1996-04-21', '1996-03-30', '1996-05-16', 'NONE', 'AIR', 'lites. fluffily even de'),
(1, 24027, 1534, 5, 24, 22824.48, 0.10, 0.04, 'N', 'O', '1996-03-30', '1996-03-14', '1996-04-01', 'NONE', 'FOB', ' pending foxes. slyly re'),
(1, 15635, 638, 6, 32, 49620.16, 0.07, 0.02, 'N', 'O', '1996-01-30', '1996-02-07', '1996-02-03', 'DELIVER IN PERSON', 'MAIL', 'arefully slyly ex'),
(2, 106170, 1191, 1, 38, 44694.46, 0.00, 0.05, 'N', 'O', '1997-01-28', '1997-01-14', '1997-02-02', 'TAKE BACK RETURN', 'RAIL', 'ven requests. deposits breach a'),
(3, 4297, 1798, 1, 45, 54058.05, 0.06, 0.00, 'R', 'F', '1994-02-02', '1994-01-04', '1994-02-23', 'NONE', 'AIR', 'ongside of the furiously brave acco'),
(3, 19036, 6540, 2, 49, 46796.47, 0.10, 0.00, 'R', 'F', '1993-11-09', '1993-12-20', '1993-11-24', 'TAKE BACK RETURN', 'RAIL', ' unusual accounts. eve'),
(3, 128449, 3474, 3, 27, 39890.88, 0.06, 0.07, 'A', 'F', '1994-01-16', '1993-11-22', '1994-01-23', 'DELIVER IN PERSON', 'SHIP', 'nal foxes wake. ');在OceanBase中创建外表:
create external table lineitem(
l_orderkey bigint,
l_partkey bigint,
l_suppkey bigint,
l_linenumber bigint,
l_quantity bigint,
l_extendedprice decimal(10,2),
l_discount decimal(10,2),
l_tax decimal(10,2),
l_returnflag char(1),
l_linestatus char(1),
l_shipdate date,
l_commitdate date,
l_receiptdate date,
l_shipinstruct char(25),
l_shipmode char(10),
l_comment varchar(44)
) PROPERTIES (
TYPE='plugin',
NAME='mysql',
PARAMETERS='{"jdbc_url":"jdbc:mysql://192.168.0.10:3306/test?useSSL=false","user":"root","password":"","table":"lineitem"}'
);访问 5.7 或更低版本的MySQL,需要使用 useSSL=false 参数(如果有必要)。
这时就可以直接在 OceanBase 中直接查询MySQL中的数据了:
MySQL [test]> select * from lineitem;
+------------+-----------+-----------+--------------+------------+-----------------+------------+-------+--------------+--------------+------------+--------------+---------------+-------------------+------------+-------------------------------------+
| l_orderkey | l_partkey | l_suppkey | l_linenumber | l_quantity | l_extendedprice | l_discount | l_tax | l_returnflag | l_linestatus | l_shipdate | l_commitdate | l_receiptdate | l_shipinstruct | l_shipmode | l_comment |
+------------+-----------+-----------+--------------+------------+-----------------+------------+-------+--------------+--------------+------------+--------------+---------------+-------------------+------------+-------------------------------------+
| 1 | 155190 | 7706 | 1 | 17 | 21168.23 | 0.04 | 0.02 | N | O | 1996-03-13 | 1996-02-12 | 1996-03-22 | DELIVER IN PERSON | TRUCK | egular courts above the |
| 1 | 67310 | 7311 | 2 | 36 | 45983.16 | 0.09 | 0.06 | N | O | 1996-04-12 | 1996-02-28 | 1996-04-20 | TAKE BACK RETURN | MAIL | ly final dependencies: slyly bold |
| 1 | 63700 | 3701 | 3 | 8 | 13309.60 | 0.10 | 0.02 | N | O | 1996-01-29 | 1996-03-05 | 1996-01-31 | TAKE BACK RETURN | REG AIR | riously. regular, express dep |
| 1 | 2132 | 4633 | 4 | 28 | 28955.64 | 0.09 | 0.06 | N | O | 1996-04-21 | 1996-03-30 | 1996-05-16 | NONE | AIR | lites. fluffily even de |
| 1 | 24027 | 1534 | 5 | 24 | 22824.48 | 0.10 | 0.04 | N | O | 1996-03-30 | 1996-03-14 | 1996-04-01 | NONE | FOB | pending foxes. slyly re |
| 1 | 15635 | 638 | 6 | 32 | 49620.16 | 0.07 | 0.02 | N | O | 1996-01-30 | 1996-02-07 | 1996-02-03 | DELIVER IN PERSON | MAIL | arefully slyly ex |
| 2 | 106170 | 1191 | 1 | 38 | 44694.46 | 0.00 | 0.05 | N | O | 1997-01-28 | 1997-01-14 | 1997-02-02 | TAKE BACK RETURN | RAIL | ven requests. deposits breach a |
| 3 | 4297 | 1798 | 1 | 45 | 54058.05 | 0.06 | 0.00 | R | F | 1994-02-02 | 1994-01-04 | 1994-02-23 | NONE | AIR | ongside of the furiously brave acco |
| 3 | 19036 | 6540 | 2 | 49 | 46796.47 | 0.10 | 0.00 | R | F | 1993-11-09 | 1993-12-20 | 1993-11-24 | TAKE BACK RETURN | RAIL | unusual accounts. eve |
| 3 | 128449 | 3474 | 3 | 27 | 39890.88 | 0.06 | 0.07 | A | F | 1994-01-16 | 1993-11-22 | 1994-01-23 | DELIVER IN PERSON | SHIP | nal foxes wake. |
+------------+-----------+-----------+--------------+------------+-----------------+------------+-------+--------------+--------------+------------+--------------+---------------+-------------------+------------+-------------------------------------+
10 rows in set (21.93 sec)功能限制
- OceanBase Oracle 模式暂未实现此功能;
- Array 类型暂未支持;
- 不支持并发查询;
- 同一数据源多表 join 不会作为一条 SQL 下推到数据源;
- 聚合函数、limit等不会下推;
- Jar包不支持动态加载,在进程启动前就需要把Jar包放在指定的目录。一个目录下可以放多个jar包,都会加载;
- 无法通过插件设置查询超时时间;
- 表 PARAMETERS 属性不支持修改,必须重新建表。
常用工具下载
FAQ
show create table 中看到的 get_path(external$filerow,'a') 是什么意思?
外表中的字段都是生成的,用户不需要关心这个字符串。
get_path(external$filerow,'a') 中 external$filerow 是固定前缀,'a' 是列名。
unexpected error for jni
Java 程序以抛异常的形式返回异常,但是当前OceanBase并没有将异常信息转换为用户友好的信息。因此当Java插件抛出异常后,用户收到的错误信息并不直观,需要通过搜索日志来排查错误。
最常见的Java 插件错误信息:
ERROR 11053 (HY000): unexpected error for jni
时间戳类型怎么处理的
时间戳,timestamp 类型,与其它的日期时间字段不同,通过客户端获取相关信息时,会处理时区,而且在数据源和OceanBase侧都会处理时区信息,因此比较繁琐。
我们这里在获取时间戳信息时,将时区设置为 UTC 时区,而OceanBase侧,取决于用户的时区配置。
当前有哪些插件
当前实现了三个插件,分别叫做 java、jdbc,mysql。
其中 java 是所有 Java 插件的基础,具体插件只要实现 com.oceanbase.external.api.DataSource 即可。
jdbc 插件支持使用JDBC访问的数据源,com.oceanbase.external.jdbc.JdbcDataSource 是它的 DataSource 实现。用户使用时指定 JDBC URL 即可访问远程数据库。
而 mysql 插件是一个特殊的 jdbc 插件,对 jdbc 的一些功能属性做了定制调整,比如列名称使用 '`' 扩起来,其 DataSource 实现是 com.oceanbase.external.mysql.MysqlJdbcDataSource。
支持的插件(数据源)并没有支持动态或运行时注册的功能,未来有变更,可以通过 com.oceanbase.external.internal.DataSourceFactory#dataSources 查看支持哪些插件。
如果你对运行时注册插件感兴趣,欢迎提交PR。
如何新增插件
新增插件的方法非常简单,只需要实现接口 com.oceanbase.external.api.DataSource 即可。
DataSource 接口中最少只需要实现两个函数。
import org.apache.arrow.memory.BufferAllocator;
/**
* 构造函数
* @param allocator Arrow 中的allocator,供Arrow分配内存使用。
* @param properties 创建数据源时指定的参数,通常是用户通过 `create external table` 时传入的参数。
*/
DataSource(BufferAllocator allocator, Map<String, String> properties);
/**
* 创建遍历数据的Scanner
* 该函数返回一个 ArrowReader,它每次可以读取一批数据。
* @param scanParameters 在扫描器时的额外参数。可以结合构造函数中的 `properties`,创建一个
* `com.oceanbase.external.api.TableScanParameter` 对象。
*/
ArrowReader createScanner(Map<String, Object> scanParameters) throws IOException;如果还要实现更多的功能,可以参考接口 com.oceanbase.external.api.DataSource 的定义。
如果你想新增数据源实现,或者优化此 Java 代码,欢迎提 PR。
实现原理
OceanBase 外表相关的功能模块
OceanBase 外表涉及以下几个模块。
| 模块 | 外表插件 | 说明 |
|---|---|---|
| SQL Parser | 不相关 | 解析SQL语句 |
| Resolver | 接口 DataSource#sensitiveKeys |
Resolver 阶段对SQL语句进行校验,创建 'generated column',标记敏感字段(比如 password)。那 sensitiveKeys 接口就用来注明哪些字段是敏感字段 |
| 并行查询 | 当前未涉及 | OceanBase 并行查询框架支持并行查询,但由于JDBC与相关数据库均无法支持多个连接查询时保持数据一致性的功能,因此当前插件不支持并行查询 |
| 数据传输 | 输出 apache arrow 格式数据 | Apache Arrow 描述了高效的内存数据存储格式,并且支持了多种语言的实现、C++ 与 Java 之间零拷贝传输数据 |
| 字符编码 | 数据交互使用 utf8 编码 | 插件与OceanBase数据交互使用 apache arrow 库,该库也采用 utf8 编码,同时当前已知主流数据库均支持 utf8 编码 |
| 谓词下推 | 接口 DataSource#pushdownFilters |
谓词 (filter) ,对应 SQL 中的 WHERE 条件,可以部分下推到插件,插件自己决定支持哪些类型的谓词 |
C++ 与 Java 的交互
OceanBase Kernel(observer)是使用C++编写的,而此外表插件功能是使用Java编写的,因此OceanBase与此插件之间使用JNI来交互。
为了方便 C++ 使用,插件基础库(internal 模块)提供了一些基础能力,比如:
com.oceanbase.external.internal.DataSourceFactory#create: 根据用户指定的属性信息创建一个DataSource;com.oceanbase.external.internal.DataSourceFactory#dataSourceNames: 当前都支持哪些Java插件(数据源)。这些将会在 OCEANBASE.GV$OB_PLUGINS 表中展示出来;com.oceanbase.external.internal.JniUtils#getExceptionMessage: 输出异常信息,方便排查问题;com.oceanbase.external.internal.JniUtils#parseSqlFilterFromArrow: observer 将 filter(谓词)信息使用 arrow 格式表示,这里将 Arrow 描述的Filter表达式转换为Java的;com.oceanbase.external.internal.JniUtils#exportArrowStream: 从 Java 中导出一批行数据到 C++ 中。
C++ 与 Java 的数据传输
observer与插件传输数据时,使用Apache Arrow。
Arrow 描述了一种高效的内存列格式,对向量运算友好,并且有多种语言的实现,包括C++、Java、Python等。并且在C++与Java之间支持零拷贝。
当前OceanBase也支持向量运算,并且在内存中使用列存储保存常见的数据类型,比如 int64、double 等,因此将Arrow数据转换成OceanBase内部数据,效率也比较高。
需要指出的是,除了表数据的传输,Filter表达式也使用Arrow传输数据(不是必须的,方案也可以选择JSON或其它)。