spark生成roaringbitmap直接同步到clickhouse

背景

spark版本 3.1.3_2.12
clickhouse 版本 22.3.10.22

需要同步一批标签数据到达clickhouse的bitmap表
原始标签表示例为

phone label1 label2 label3 dt
131*****033 1 2024-01-01 100.10 20240801
131*****034 1 2024-08-01 100.10 20240801
... ... ... ... ...

ck-bitmap表示例为

label value phone_bitmap dt
label1 1 [B@xxx 20240801
label2 2024-01-01 [B@xxx 20240801
... ... ... ...

我们需要在spark中把原始标签表转换成ck-bitmap表的结构,并同步到clickhouse

转换结构

首先需要进行一次melt,虽然在spark没有melt函数,但是我们可以用stack实现;要注意标签的数据类型要一致,这里使用String兼容所有数据类型

def melt(df:DataFrame): DataFrame = {
	val cols = df.columns.filter(x => x != "phone" && x != "dt")
	val selectCols = "phone" +: cols.map(col => f"cast($col as String) as $col") :+ "dt"
	val stackCols = cols.map(x => s"'$x',$x")
	df.selectExpr(selectCols: _*)
		.selectExpr("phone","dt", s"stack(${stackCols.length},${stackCols.mkString(",")}) as (label,value)")
		.where("value is not null")
		.selectExpr("phone", "label", "value", "dt")
}

这样我们就能把宽表(原始结构)转换成长表了
然后我们需要把phone聚合成bitmap,所以我们需要写一个自定义聚合函数(UDAF)

import org.roaringbitmap.longlong.Roaring64NavigableMap
import org.apache.spark.sql.{Encoder, Encoders}
import org.apache.spark.sql.expressions.Aggregator

object BitmapUtil {
    def ser64NBitmap(bm: Roaring64NavigableMap): Array[Byte] = {
        val stream = new ByteArrayOutputStream()
        val dataOutput = new DataOutputStream(stream)
        bm.serializePortable(dataOutput)
        stream.toByteArray
    }

    def deSer64NBitmap(bytes: Array[Byte]): Roaring64NavigableMap = {
        val bis  = new ByteArrayInputStream(bytes)
        val dis = new DataInputStream(bis)
        val bm = Roaring64NavigableMap.bitmapOf()
        bm.deserializePortable(dis)
        bm
    }
}

class Bitmap64NGenUDAF extends Aggregator[Long, Roaring64NavigableMap, Array[Byte]] {

    override def zero: Roaring64NavigableMap = {
        // 构造一个空的bitmap
        Roaring64NavigableMap.bitmapOf()
    }

    override def reduce(b: Roaring64NavigableMap, a: Long): Roaring64NavigableMap = {
        b.add(a)
        b
    }

    override def merge(b1: Roaring64NavigableMap, b2: Roaring64NavigableMap): Roaring64NavigableMap = {
        b1.or(b2)
        b1
    }

    override def finish(reduction: Roaring64NavigableMap): Array[Byte] = {
        BitmapUtil.ser64NBitmap(reduction)
    }

    override def bufferEncoder: Encoder[Roaring64NavigableMap] = Encoders.kryo[Roaring64NavigableMap]

    override def outputEncoder: Encoder[Array[Byte]] = Encoders.BINARY
}

这个代码是参考的 https://www.cnblogs.com/baran/p/15686152.html
但是他那个代码性能太差了,进行了优化;
并且由于我的入参是11位的电话号码,所以需要转换成long,对应的bitmap实现类需要改成Roaring64NavigableMap;
注:实际上11位的电话号码是可以通过一定规则缩进UInt32的数据范围,因为电话号码有些号段是不使用的,有压缩空间.这里就不展开了.
注2:要使用Roaring64NavigableMap而不是Roaring64Bitmap,这涉及到ClickHouseBitmap的创建,如果你用Roaring64Bitmap类去创建ClickHouseBitmap就会占用大量的内存,具体原因可以看ClickHouseBitmap.wrap的源码.

melt(df).createOrReplaceTempView("cb")
spark.udf.register("gen_bitmap", udaf(new Bitmap64NGenUDAF))
var dfb = spark.sql(
            """
              |select label,value,gen_bitmap(cast(phone as Long)) phone_bitmap,dt
              |from cb
              |group by label,value,dt
              |""".stripMargin)

这样我们就得到了一张bitmap表,结果和ck上是一致的
但是此时我们不能直接同步到ck上,因为这里我们使用的是Roaring64NavigableMap类,而ck上的bitmap是有做特殊处理的;
这里参考 https://www.cnblogs.com/niutao/p/15665790.html
但是我们不需要和上述文档一样手撸一个ck-bitmap实现出来,因为clickhouse已经给我们实现好了,我们需要在maven引入clickhouse官方jdbc的all包

    <dependency>
      <groupId>com.clickhouse</groupId>
      <artifactId>clickhouse-jdbc</artifactId>
      <version>0.4.6</version>
      <!-- use uber jar with all dependencies included, change classifier to http for smaller jar -->
      <classifier>all</classifier>
<!--      <scope>provided</scope>-->
    </dependency>

这个all包不仅包含了jdbc,还包含了java-client和clickhouse-data模块;
里面的com.clickhouse.data.value.ClickHouseBitmap类就是ck-bitmap在java的实现;ClickHouseBitmap类的wrap方法允许org.roaringbitmap里面的bitmap类作为入参;
我们看下clickhouse jdbc的源码:
AbstractPreparedStatement实现类InputBasedPreparedStatement的setobject,可以看出setobject是可以接收ClickHouseValue的子类ClickHouseBitmap的

    @Override
    public void setObject(int parameterIndex, Object x) throws SQLException {
        ensureOpen();

        int idx = toArrayIndex(parameterIndex);
        values[idx].update(x);
        flags[idx] = true;
    }
// 重点是这个update方法

    default ClickHouseValue update(Object value) {
        if (value == null) {
            return resetToNullOrEmpty();
        } //省略一堆else if...
        else if (value instanceof ClickHouseValue) {
            return update((ClickHouseValue) value);
        } else {
            return updateUnknown(value);
        }
    }

所以我们只需要把spark的Roaring64NavigableMap转换成ClickHouseBitmap就可以了

    def toCKBitmap(rb: Array[Byte]): ClickHouseBitmap = {
        val bitmapB =  ClickHouseBitmap.wrap(BitmapUtil.deSer64NBitmap(rb), ClickHouseDataType.UInt64)
        bitmapB
    }

同步到clickhouse

这里使用经典的jdbc同步方法就行,细节略;
dfb.rdd.map(phone_bitmap列转换为ClickHouseBitmap).foreachPartition(jdbc批量写入ck)

还有一件事

看看ClickHouseBitmap里的这个两个方法,可以了解到一些重点

  1. ClickHouseBitmap类内部也是使用的RoaringBitmap,而入参是Roaring64Bitmap时用的Roaring64NavigableMap.
  2. ClickHouseBitmap在序列化时会序列化为CK服务端认可的形式,其中需要把内部的Roaring64NavigableMap也序列化,他调用了一个类的静态变量.SERIALIZATION_MODE这个很重要,这个静态变量在较早的RoaringBitmap在spark使用的版本(在$SPARK_HOME/jars)0.9.0是没有的,会产生版本冲突!
  3. 通过代码可知,我们在数据序列化之前(比如同步或保存),需要把SERIALIZATION_MODE配置成SERIALIZATION_MODE_PORTABLE,这个是较新的序列化方法;SERIALIZATION_MODE_LEGACY是默认的,也是老的序列化方法,如果我们使用0.9.0版本序列化为bytes并保存到hive表中,那将其读取出来时需要配置成SERIALIZATION_MODE_LEGACY读取.
    public static ClickHouseBitmap wrap(Object bitmap, ClickHouseDataType innerType) {
        final ClickHouseBitmap b;
        if (bitmap instanceof RoaringBitmap) {
            b = new ClickHouseRoaringBitmap((RoaringBitmap) bitmap, innerType);
        } else if (bitmap instanceof MutableRoaringBitmap) {
            b = new ClickHouseMutableRoaringBitmap((MutableRoaringBitmap) bitmap, innerType);
        } else if (bitmap instanceof ImmutableRoaringBitmap) {
            b = new ClickHouseImmutableRoaringBitmap((ImmutableRoaringBitmap) bitmap, innerType);
        } else if (bitmap instanceof Roaring64Bitmap) {
            b = new ClickHouseRoaring64NavigableMap(
                    Roaring64NavigableMap.bitmapOf(((Roaring64Bitmap) bitmap).toArray()), innerType);
        } else if (bitmap instanceof Roaring64NavigableMap) {
            b = new ClickHouseRoaring64NavigableMap((Roaring64NavigableMap) bitmap, innerType);
        } else {
            throw new IllegalArgumentException("Only RoaringBitmap is supported but got: " + bitmap);
        }

        return b;
    }
        @Override
        public long serializedSizeInBytesAsLong() {
            // no idea why it's implemented this way...
            // https://github.com/RoaringBitmap/RoaringBitmap/blob/fd54c0a100629bb578946e2a0bf8b62784878fa8/RoaringBitmap/src/main/java/org/roaringbitmap/longlong/Roaring64NavigableMap.java#L1371-L1380
            // TODO completely drop RoaringBitmap dependency
            if (Roaring64NavigableMap.SERIALIZATION_MODE != Roaring64NavigableMap.SERIALIZATION_MODE_PORTABLE) {
                throw new IllegalStateException(
                        "Please change Roaring64NavigableMap.SERIALIZATION_MODE to portable first");
            }
            return rb.serializedSizeInBytes();
        }

如果不重视这些问题就会导致这些报错,这里贴出来以方便其他人可以搜索到本文
报错1:

java.lang.NoSuchFieldError: SERIALIZATION_MODE

报错2:

java.lang.IllegalStateException: Please change Roaring64NavigableMap.SERIALIZATION_MODE to portable first
	at com.clickhouse.data.value.ClickHouseBitmap$ClickHouseRoaring64NavigableMap.serializedSizeInBytesAsLong(ClickHouseBitmap.java:199)
	at com.clickhouse.data.value.ClickHouseBitmap.toByteBuffer(ClickHouseBitmap.java:591)
	at com.clickhouse.data.value.ClickHouseBitmap.toBytes(ClickHouseBitmap.java:605)
	at com.clickhouse.data.format.ClickHouseRowBinaryProcessor$BitmapSerDe.serialize(ClickHouseRowBinaryProcessor.java:50)
	at com.clickhouse.data.ClickHouseDataProcessor.write(ClickHouseDataProcessor.java:593)
	at com.clickhouse.jdbc.internal.InputBasedPreparedStatement.addBatch(InputBasedPreparedStatement.java:345)

解决方法:
spark任务提交前使用--jars 指定jar包,然后配置优先使用用户jar包

--jars /path/RoaringBitmap-1.2.1.jar \
--conf spark.driver.userClassPathFirst=true \
--conf spark.executor.userClassPathFirst=true \

在同步数据前,修改SERIALIZATION_MODE的值

Roaring64NavigableMap.SERIALIZATION_MODE=Roaring64NavigableMap.SERIALIZATION_MODE_PORTABLE
posted @ 2024-08-02 14:37  RAmenLCH  阅读(227)  评论(0)    收藏  举报