使用UDF从hive中导出bitmap到clickhouse
期望通过UDF将hive中bitmap数据,输出到clickhouse中。(最终使用roaring64NavigableBitmap、roaring64Bitmap)
关于bitmap的原理与核心的RoaringBitmp知识等有空再写,具体可以先看文末参考链接,此处不再介绍。
分析
核心行为就是在hive中调用udf,将bitmap数据编码后通过jdbc clickhouse接口插入。
整个过程需要关注以下几点:
- Clickhouse(CK)与hive中bitmap的存储结构不同,两个应用之间的数据需要转化
- roaringbitmap基础是32位的,如果基础数据数值较大,需要使用64位的结构,得指定包的版本。
- CK中AggregateMergeTree可以较好地适配整个转化过程。
hive与CK的bitmap数据转化
核心参考这篇文章: www.cnblogs.com/niutao/p/15…
简单来说,通过生成bitmap的UDF源码可以看到hive中bitmap数据是java的二进制数组;而CK中的bitmap的底层结构则不同,CK中bitmap基数小于32时是SmallSet,大于时是RoaringBitmap,类型为AggregateFunction(groupBitmap, UInt*),通过SELECT bitmapBuild([1,2]) AS res, toTypeName(res);查看。
其中CK中AggregateFunction(groupBitmap, UInt*)的底层是具有特定结构的数组:
ck中
1、小于32的用smallSet存储
1):Byte(0)
2):Buffer(RoaringBitmap需要序列化的字节大小)
2、大于32的用RoaringBitmap存储
1):Byte(0)
2):VarInt(SerializedSizeInBytes) RoaringBitmap需要序列化的字节大小
3):RoaringBitmap的字节数组
那么问题就变成了: hive 二进制数组 - 字节数组 - 转化结构 - base64字符串 - CK字符串 - CK物化视图 - 实际CK中bitmap结构。
难道我要在UDF中自己手写这个类似协议的转化?
年少无知的我确实傻傻地跟着写了一次。。。直到升级了之后(roaring64Bitmap),去看了clickhouse jdbc的文档,发现里面其实已经把转化方法封装好了,能直接调用完成转化。所以可以通过自己构造出对应结构的数组,再转化为字符串,最后传给CK;也可以直接调用jdbc中的方法,将bitmap转化为CK中的格式。具体代码见后文附录。
select to_ck_udf32(array(3,4,100));
-->: AAUDAAAABAAAAGQAAAC7MwAAkW1dAw==
CK中承接bitmap数据
从CK的文档中可以直到bitmap的构造可以通过groupBitmap的方法,通过AggregatingMergeTree引擎,结合物化视图可以很方便将数据自动转化。例如:
CREATE TABLE tb (
`id` Int64,
`bmp_encode` String COMMENT 'bitmap编码数据',
`bmp` AggregateFunction(groupBitmap, UInt32) MATERIALIZED base64Decode(bmp_encode) COMMENT '实际bitmap数据'
) ENGINE = AggregatingMergeTree() ORDER BY (id) ;
注意这里 AggregateFunction的uint32与uint64是有区别的,bitmap的结构都不同了。
insert into tb values (1,'AAUDAAAABAAAAGQAAAC7MwAAkW1dAw==');
手动聚合
OPTIMIZE TABLE tb;
select bitmapToArray(bmp) from tb;
--> [3,4,100]
这样插入的时候会按照相同的id将bmp字段执行groupBitmap聚合,看文档可以知道聚合方式是OR。
关于64位的roaringBitmap
默认的roaringBitmap是32位的,意味着如果使用这个结构,则建表的时AggregateFunction(groupBitmap, UInt32)只能是Uint32,如果使用Uint64,则会读取失败。如果需要Uint64的Bitmap,需要使用Roaring64Bitmap,但是,ck中针对这个类型的兼容性存在一些问题(可能我看的文档比较旧,如有错误麻烦指出),会占用太多内存,应该换为roaring64NavigableBitmap,这个类型已经内置在RoaringBitmap的jar包中,如果没有的话需要检查版本。经过我自己测试,目前太低版本的clickhouse-jdbc或者太低版本的RoaringBitmap jar包都会在转化的时候存在某些问题,所以需要使用较高的版本。
RoaringBitmap: 1.0.5
clickhouse-jdbc: 0.4.6
clcickhouse: 23.8
如果使用32位的,则没有那么多要求,可以很容易转化;使用64位的话,就要更换bitmap类型位roaring64NavigableBitmap,并且需要使用正确版本的jar包。
验证
1
插入两条数据,让数据库自动聚合。
select to_ck32('tb',array(3L,4L,33L,222,10000));
select to_ck32('tb',array(321,43,45,32,343,2234,78,5435,345,435,34,534,534,5,345,34,534,534,534,5,345,34,543,5,66,645,5678,65,675,7,54667,45645,3455667,7897));
2
select * from tb limti 20;
观察到多条数据存在,此时还没有聚合。
3
手动聚合
OPTIMIZE TABLE tb FINAL;
4
select groupBitmapMerge(bmp) AS merged_bmp
from tb
where id=1
output: 29
5
检查数据合并方式是否为OR
select bitmapToArray(bmp)
from tb
where id=1
[3, 4, 33, 222, 10000, 5, 7, 32, 34, 43, 45, 65, 66, 78, 321, 343, 345, 435, 534, 543, 645, 675, 2234, 5435, 5678, 7897, 45645, 54667, 3455667]
to_ck_64('tb','AaQBAQAAAAAAAAAAAAAAOjAAAAYAAAAAACkAAQABAAYAAAAIAAAANAAAAGMAAAA4AAAAjAAAAJAAAACSAAAAlAAAAJYAAAAFAAcAFwAgACIAKgArAC0ANwBBAEIATgBWAFcAQQFXAVkBswEWAh8ChQKRAqMC9AL9AgAD0gO6CNUROxVYFS4WChrpHfwd2R7mIZcmTbJWsovVNfxkKEwsHXbArbO6C+E=');
[5, 7, 23, 32, 34, 42, 43, 45, 55, 65, 66, 78, 86, 87, 321, 343, 345, 435, 534, 543, 645, 657, 675, 756, 765, 768, 978, 2234, 4565, 5435, 5464, 5678, 6666, 7657, 7676, 7897, 8678, 9879, 45645, 45654, 54667, 64565, 75876, 76876, 423453, 568768, 3455667, 6545675]
错误备忘
-
执行bitmap数据插入ck的时候,std::exception. Code: 1001, type: std::runtime_error, e.what() = failed alloc while reading
本质上是数据格式不对,导致ck读取失败。注意roaringBitmap包与clickhouse-jdbc的版本。
github.com/ClickHouse/… -
java.lang.NoSuchFieldError: SERIALIZATION_MODE
代码中需要增加:Roaring64NavigableMap.SERIALIZATION_MODE=Roaring64NavigableMap.SERIALIZATION_MODE_PORTABLE。www.cnblogs.com/ramenlch/ar…
ClickHouseBitmap在序列化时会序列化为CK服务端认可的形式,其中需要把内部的Roaring64NavigableMap也序列化,他调用了一个类的静态变量.SERIALIZATION_MODE这个很重要,这个静态变量在较早的RoaringBitmap在spark使用的版本(在$SPARK_HOME/jars)0.9.0是没有的,会产生版本冲突!
参考
ck与hivewww.cnblogs.com/niutao/p/15…
JD_CDP: juejin.cn/post/732719…
RoaringBitMap原理:
https://jianshu.com/p/818ac4e90daf
www.jianshu.com/p/5f2fe1dee…
ck doc: clickhouse.com/docs/en/sql…
bitmap:
juejin.cn/post/733044…
juejin.cn/post/722698…
处理示例:
www.fblinux.com/?p=2851
www.cnblogs.com/ramenlch/ar…
java_doc:
javadoc.io/static/org.…
javadoc.io/doc/com.cli…
报错处理:
github.com/ClickHouse/…
ask.selectdb.com/questions/D…
clickhouse-java:
github.com/ClickHouse/…
附录
代码
roaring64NavigableBitmap
import org.apache.hadoop.hive.ql.exec.Description;
import org.apache.hadoop.hive.ql.exec.UDFArgumentException;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredObject;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.hive.serde2.objectinspector.ListObjectInspector;
import java.util.ArrayList;
import java.util.List;
import org.roaringbitmap.longlong.Roaring64NavigableMap;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Base64;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.data.value.ClickHouseBitmap;
import java.io.OutputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class ToCk64UDF extends GenericUDF {
@Override
public ObjectInspector initialize(ObjectInspector[] arguments) throws UDFArgumentException {
// 设置Roaring64NavigableMap的序列化模式为SERIALIZATION_MODE_PORTABLE
Roaring64NavigableMap.SERIALIZATION_MODE = Roaring64NavigableMap.SERIALIZATION_MODE_PORTABLE;
return PrimitiveObjectInspectorFactory.writableStringObjectInspector;
}
@Override
public Object evaluate(DeferredObject[] arguments) throws HiveException {
// transferCkBmpByJdbc or transferCkBmp
return new Text("");
}
@Override
public String getDisplayString(String[] children) {
return getStandardDisplayString("to_ck_udf64_v1", children);
}
public String transferCkBmpByJdbc(long[] data) {
// 使用提供的整数数组创建RoaringBitmap
Roaring64NavigableMap rb = new Roaring64NavigableMap();
String result = "";
for (long i : data) {
rb.add(i);
}
// 将Roaring64NavigableMap封装为ClickHouseBitmap
ClickHouseBitmap clickHouseBitmap = ClickHouseBitmap.wrap(rb, ClickHouseDataType.UInt64);
result = Base64.getEncoder().encodeToString(clickHouseBitmap.toBytes());
return result;
}
public String transferCkBmp(long[] data) {
// 使用提供的整数数组创建RoaringBitmap
Roaring64NavigableMap rb = new Roaring64NavigableMap();
String result = "";
for (long i : data) {
rb.add(i);
}
// System.out.println("starting with bitmap " + rb);
// 当位图的基数少于32时,仅使用SmallSet存储
if (rb.getLongCardinality() <= 32) {
// 分配缓冲区大小
ByteBuffer initBuffer = ByteBuffer.allocate(2 + 8 * (int)rb.getLongCardinality());
ByteBuffer bos = (initBuffer.order() == ByteOrder.LITTLE_ENDIAN) ? initBuffer : initBuffer.slice().order(ByteOrder.LITTLE_ENDIAN);
bos.put((byte) 0);
bos.put((byte) rb.getLongCardinality());
long[] array = rb.toArray();
for (long i : array) {
bos.putLong(i);
}
result = Base64.getEncoder().encodeToString(bos.array());
System.out.println("小于32的encode :" + result1);
} else {
// rb.serializedSizeInBytes() 需要序列化的字节数
long seriesByteSize = rb.serializedSizeInBytes();
// VarInt.varIntSize返回编码需要的长度(二进制条件下:>>>)
int varIntLen = varUIntSize(seriesByteSize);
System.out.println(varIntLen);
System.out.println(seriesByteSize);
// 初始化
ByteBuffer initBuffer = ByteBuffer.allocate(1 + varIntLen + (int)rb.serializedSizeInBytes());
ByteBuffer bos = (initBuffer.order() == ByteOrder.LITTLE_ENDIAN) ? initBuffer : initBuffer.slice().order(ByteOrder.LITTLE_ENDIAN);
bos.put((byte) 1);
try {
// VarInt.putVarInt(seriesByteSize, bos);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeVarUInt((long) seriesByteSize,baos);
rb.runOptimize();
rb.serialize(new DataOutputStream(baos));
bos.put(baos.toByteArray());
result = Base64.getEncoder().encodeToString(bos.array());
System.out.println("大于32的encode :" + result1);
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
// 用于将java类型转uint64
public static int highestBitPosition(long x) {
for (int i = 63; i >= 0; i--) {
if ((x & (1L << i)) != 0) {
return i;
}
}
return -1; // 如果x为0,返回-1
}
// 计算数值占用的字节
public static int varUIntSize(long x) {
if (x == 0) {
return 1; // 0需要一个字节来表示
}
int highestBit = highestBitPosition(x);
return highestBit / 7 + 1;
}
// 将数值写入字节
public static void writeVarUInt(long x, OutputStream ostr) throws IOException {
for (int i = 0; i < 9; ++i) {
int byteValue = (int) (x & 0x7F);
if (x > 0x7F) { // x > 0x7F indicates that x has more than 7 bits, need to mark it (set the 8th bit to 1), then do the unsigned right shift by 7 bits
byteValue |= 0x80;
}
// Write the byte to the OutputStream
ostr.write(byteValue);
// Unsigned right shift by 7 bits
x >>>= 7;
if (x == 0) {
return;
}
}
}
}