• 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

  • 相关阅读:
    legend3---阿里云如何多个域名指向同一个网站
    黑马lavarel教程---1、lavarel目录结构
    modern php笔记---2.1、特性(命名空间、特性、性状)
    modern php笔记---php (性状)
    modern php笔记---1、新时代的php
    深入浅出mysql笔记---1、mysql下载安装
    深入浅出mysql笔记---0、序
    影视感悟专题---2、《大染坊》
    尚硅谷Docker---6-10、docker的安装
    legend3---Homestead常用操作代码
  • 原文地址:https://www.cnblogs.com/niutao/p/15665790.html
Copyright © 2020-2023  润新知