通过clickhouse源码了解hive/spark的RoaringBitmap写入Clickhouse的bitmap

 

 先说结论:要把hive上的bitmap数据同步到clickhouse的bitmap里面

参考连接:

https://blog.csdn.net/nazeniwaresakini/article/details/108166089

https://blog.csdn.net/qq_27639777/article/details/111005838

https://zhuanlan.zhihu.com/p/351365841

https://blog.csdn.net/yizishou/article/details/78342499

https://github.com/RoaringBitmap/CRoaring

1、Clickhouse的RoaringBitmap结构

目标是将Hive的Binary类型能顺利转成Clickhouse的Bitmap类型

Hive的Binary类型是二进制数组byte[]

 

 

Clickhouse的Bitmap类型是,一般是通过groupBitmap方式构建出来的,比如:

select 
series_id, 
groupBitmapState(toUInt32(dvid))  bitmap列
FROM 
dms_pds_flow_interest_dvid_city_day_all 
group by 
series_id

其中关键sql是:groupBitmapState,源码对应位置是:AggregateFunctionGroupBitmap.cpp注册的;

 

 

 这个C++代码的关键点是:

createAggregateFunctionBitmap<AggregateFunctionGroupBitmapData>

代表通过函数:createAggregateFunctionBitmap来创建bitmap类型:AggregateFunctionGroupBitmapData

然后跟进这个AggregateFunctionGroupBitmapData类,文件(AggregateFunctionGroupBitmapData.h)

结构:

 

 

 内部:

 

 

其中bitmap的一些计算函数逻辑,就是这个AggregateFunctionGroupBitmapData.h文件实现的;比如:

select bitmapOrCardinality(bitmap_a , bitmap_b) 是取两个bitmap的并集;

那么实现就是:

 

 

 言归正传,根据Rbitmap的数据结构:

参考连接:https://zhuanlan.zhihu.com/p/351365841

1、首先,将 32bit int(无符号的)类型数据 划分为 2^16 个桶(即使用数据的前16位二进制作为桶的编号),
每个桶有一个Container(可以理解为容器也可以理解为这个桶,容器和桶在这里可以理解为一个东西,只是说法不一样而已) 来存放一个数值的低16位。

2、在存储和查询数值时,将数值 k 划分为高 16 位和低 16 位,取高 16 位值找到对应的桶,
然后在将低 16 位值存放在相应的 Container 中。这样说可能比较抽象不易理解,下面以一个例子来帮助大家理解。

 

 

 大概意思是,在clickhouse的Rbitmap里面,为了优化存储空间,会将一个32位的数据,分成高16位和低16位;

高16位会被作为key存储到short[] keys中,低16位则被看做value

比如我要存储666这个数字,需要将666划分成高16位和低16位,通过高16位定位到当前桶是5,定位到竖着排列的桶未知后,在将低16位的值存储到横着排列的数组中;

 

之前看clickhouse源码中C++里面返回的roaring和roaring64map到底是啥,在看CRoaring源码,创建Rbitmap的地方:

 

 

 其中的关键点是:

 

 

 上面意思是定义一个结构体,类型是roaring_array_t , 变量名是:high_low_container

这个就是图片里面说的高16位和低16位的存储模型,然后查看roaring_array_t的结构:

 

 

 然后查看ROARING_CONTAINER_T,也就是低16位类型是,因为clickhouse是C++编写的,因此构建的数组其实是:struct container_s {}指向的各个子类

 

 

 

返回Clickhouse的源码,要开辟的子类就是:

 

 这样就又回到了Clickhouse的Rbitmap。虽然转了一圈,但是已经知道这个Rbitmap底层存储的其实是数组

 

2、hive或者sparksql里面的RoaringBitmap

参考hive制作bitmap的连接:https://github.com/sunyaf/bitmapudf

关键就是了解UDAF里面的函数:

// 输入输出都是Object inspectors  
public  ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException;  
  
// AggregationBuffer保存数据处理的临时结果  
abstract AggregationBuffer getNewAggregationBuffer() throws HiveException;  
  
// 重新设置AggregationBuffer  
public void reset(AggregationBuffer agg) throws HiveException;  
  
// 处理输入记录  
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException;  
  
// 处理全部输出数据中的部分数据  
public Object terminatePartial(AggregationBuffer agg) throws HiveException;  
  
// 把两个部分数据聚合起来  
public void merge(AggregationBuffer agg, Object partial) throws HiveException;  
  
// 输出最终结果  
public Object terminate(AggregationBuffer agg) throws HiveException;  

我们要兼容hive的Rbitmap和Clickhouse的Rbitmap,只需要关键方法:terminate到底返回了什么

查看代码:

 

 所以关键代码就是:myagg.getPartial()

 

 hive里面返回的Rbitmap其实最终是java的二进制数组;

所以要想Hive的Rbitmap和Clickhouse的Rbitmap能够兼容,就是演变成:Hive的二进制数组如何有效的存储到Clickhouse里面

3、Clickhouse的Roaringbitmap是如何存储的

在回看Clickhouse的Rbitmap,比如看添加像Rbitmap里面添加内容。它的api是:

RoaringBitmap.add(1);

RoaringBitmap.add(2);

其源码是:

//如果基数超过32个,则会将数据存储到Rbitmap
    void toLarge()
    {
        //通过智能指针建立对象
        rb = std::make_shared<RoaringBitmap>();
        //C++ 里面的for循环,翻译成java就是:for (A x:small)
        for (const auto & x : small)
            //将smallSet的数据存储到Rbitmap里面
            rb->add(static_cast<Value>(x.getValue()));
        //清空smallSet
        small.clear();
    }

 

 

 void add(T value)
    {
        //判断存储个数是否小于32
        if (isSmall())
        {
            if (small.find(value) == small.end())
            {
                //如果插入的元素没有超过smallSet的容量,则添加到smallSet
                if (!small.full())
                    small.insert(value);
                //如果插入的元素个数超过了smallSet容量,则插入RoaringBitmap
                else
                {
                    toLarge();
                    rb->add(static_cast<Value>(value));
                }
            }
        }
        //如果超过32则按照
        else
        {
            rb->add(static_cast<Value>(value));
        }
    }

 

其中内部的写入是:

void write(DB::WriteBuffer & out) const
    {
        //判断基数,是否超过32来判断底层的存储
        UInt8 kind = isLarge() ? BitmapKind::Bitmap : BitmapKind::Small;
        //写入一个UInt8的标识到buf中,0代表使用smallset 1代表使用RoaringBitmap
        writeBinary(kind, out);
        //smallSet的写入
        if (BitmapKind::Small == kind)
        {
            small.write(out);
        }
        //Rbitmap的写入
        else if (BitmapKind::Bitmap == kind)
        {
            //得到要写入内存的Rbitmap字节大小
            auto size = rb->getSizeInBytes();
            writeVarUInt(size, out);
       //通过指针占有并管理另一对象 std::unique_ptr
<char[]> buf(new char[size]); rb->write(buf.get()); out.write(buf.get(), size); } }

 其中getSizeInBytes() 这个方法要去CRoaring里面找:

/**
     * How many bytes are required to serialize this bitmap (meant to be
     * compatible with Java and Go versions)
     *
     * Setting the portable flag to false enable a custom format that
     * can save space compared to the portable format (e.g., for very
     * sparse bitmaps).
     */
    size_t getSizeInBytes(bool portable = true) const {
        if (portable)
            return api::roaring_bitmap_portable_size_in_bytes(&roaring);
        else
            return api::roaring_bitmap_size_in_bytes(&roaring);
    }

追下去的大概意思就是:

一个header头部大小

一个Ritmao里面Container数组存储元素个数

然后header ++ Container数组元素的字节大小

 

writeVarInt(size , out)的参考连接:https://blog.csdn.net/B_e_a_u_tiful1205/article/details/106064778

所以要写入Rbitmap,需要存储结构是:

1、writeBinary(1, out)  : java中的Byte(1)
2、
auto size = rb->getSizeInBytes();
writeVarUInt(size, out);
就是像buffer中写入需要序列化的的字节大小
3、将RoaringBitmap转化成字节数组

参考一位大神的的,对应java结果就是:https://blog.csdn.net/qq_27639777/article/details/111005838

Byte(1), VarInt(SerializedSizeInBytes), ByteArray(RoaringBitmap)

 

4、将java的Rbitmap转成Clickhouse的Rbitmap

在clickhouse中构建一个bitmap:

select bitmapToArray(bitmapBuild([toUInt32(3), toUInt32(4), toUInt32(100)]));

 

 

 然后对bitmap做一个编码:

SELECT base64Encode(toString(bitmapBuild([toUInt32(3), toUInt32(4), toUInt32(100)])));

 

 

 在反过来,将编码转回bitmap

1、构建表:

CREATE TABLE test_index.spark_bitmap_test(
  dt LowCardinality(String) COMMENT '日期',
  dim_type Int32 COMMENT '维度类型',
  dim_id Int32 COMMENT '纬度值',
  encode String COMMENT '编码',
  compare_encode AggregateFunction(groupBitmap, UInt32)
      MATERIALIZED base64Decode(encode)
)
Engine = AggregatingMergeTree()
PARTITION BY toYYYYMMDD(toDate(dt))
PRIMARY KEY (dim_type, dim_id)
ORDER BY (dim_type, dim_id)
SETTINGS index_granularity = 4;

2、将编码插入到bitmap

insert into test_index.spark_bitmap_test values ('2021-12-14' , 1 , 2370 , 'AAMDAAAABAAAAGQAAAA=');

3、查询:

select 
       dt , 
       dim_type ,
       dim_id , 
       encode , 
       bitmapToArray(compare_encode) as arr , 
       bitmapCardinality(compare_encode) as encode 
from test_index.spark_bitmap_test;

 

 

 以上操作就是为了证明,如果在java中能够将bitmap进行编码,这样通过clickhouse的物化视图自动将编码字符串转成bitmap

结合之前分析的源码:

1、小于32的用smallSet存储
    1):Byte(0)
    2):Buffer(RoaringBitmap需要序列化的字节大小)

2、大于32的用RoaringBitmap存储
     1):Byte(0)
     2):VarInt(SerializedSizeInBytes)  RoaringBitmap需要序列化的字节大小
   3):RoaringBitmap的字节数组

综上转成java/scala代码:

import com.test.bitmap.VarInt
import org.roaringbitmap.RoaringBitmap
import org.roaringbitmap.buffer.{ImmutableRoaringBitmap, MutableRoaringBitmap}

import java.io.{ByteArrayOutputStream, DataOutputStream}
import java.nio.{ByteBuffer, ByteOrder}
import java.util.Base64

object TestBitmapSeries {
  def main(args: Array[String]): Unit = {
    val rb = RoaringBitmap.bitmapOf(3, 4, 100)
    println("starting with  bitmap " + rb)

    //当位图的基数少于32时,仅使用SmallSet存储
    if (rb.getCardinality <= 32) {
      //分配缓冲区大小
      val initBuffer = ByteBuffer.allocate(2 + 4 * rb.getCardinality)
      val bos = if (initBuffer.order eq ByteOrder.LITTLE_ENDIAN) initBuffer else initBuffer.slice.order(ByteOrder.LITTLE_ENDIAN)
      bos.put(new Integer(0).toByte)
      bos.put(rb.getCardinality.toByte)
      rb.toArray.foreach(i => bos.putInt(i))
      val result = Base64.getEncoder.encodeToString(bos.array())
      println("小于32的encode :"+result)
    } else {
      //rb.serializedSizeInBytes() 需要序列化的字节数
      val seriesByteSize: Int = rb.serializedSizeInBytes()
      //VarInt.varIntSize返回编码需要的长度(二进制条件下:>>>)
      val varIntLen = VarInt.varIntSize(seriesByteSize)
      //初始化
      val initBuffer: ByteBuffer = ByteBuffer.allocate(1 + varIntLen + rb.serializedSizeInBytes())
      //字节高低序列,好像意思是在内存的存储方式
      val bos = if (initBuffer.order eq ByteOrder.LITTLE_ENDIAN) initBuffer else initBuffer.slice.order(ByteOrder.LITTLE_ENDIAN)
      bos.put(new Integer(1).toByte)
      //TODO
      VarInt.putVarInt(rb.serializedSizeInBytes(), bos)
      val baos = new ByteArrayOutputStream()
      rb.serialize(new DataOutputStream(baos))
      bos.put(baos.toByteArray())
      val result: String = Base64.getEncoder.encodeToString(bos.array())
      println("大于32的encode :"+result)
    }
  }
}

 

 结果和Clickhouse的编解码一致

 

5、利用sparkSql批量序列化RoaringBitmap,然后写入clickhouse

https://github.com/niutaofan/spark_bitmap.git

 

posted @ 2021-12-14 17:25  niutao  阅读(3634)  评论(0编辑  收藏  举报