UDF: 从hive中导出bitmap到clickhouse

491 阅读7分钟

使用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;
            }
        }
    }
}