面对2千万的json,Clickhouse的表现有点扑街

来源:安瑞哥是码农


上篇文章测试了用 Doris 自带的数据导入功能 stream load 跟 broker load,将一个 2千万(20G) 的 json 数据文件给导入到表中,并且将其中的 json 数据用 variant 进行存储。


从表现来看,除了最终对 json 内部 key 的查询功能表现差点意思之外,无论是导入效率,还是写入过程的顺溜程度,都可以给个好评。


与此同时呢,好奇心驱使,我又基于同样的需求,在 Clickhouse(下称CK) 身上做了同样的测试,这不测不知道,一测才发现,CK 在外部数据源导入这块的表现,跟 Doris 相比,着实有点让人失望。

那么这篇文章,咱就来看看,一个2千万的 json 数据,是怎么经历“九九八十一难”,给倒腾到 CK 库里面的。



0. 寻找靠谱的入库方式


对于 Doris 而言,数据库本身自带的 stream load 跟 broker load 在上篇文章中已经测试验证过了,简单且都比较好用。


详情可参考上一篇文章内容:面对一个20G的json文件,Doris的解决方案有哪些?


此外,还可以借助外部的工具,比如专门的数据传输软件(如dataX),计算引擎(Spark跟Flink)等等。


而相比 Doris 丰富的数据导入方案,CK 自身所能提供的导入方式,就逊了不少。


翻遍了整个官方文档,找到了一个 CK 本身自带的,貌似还算靠谱的方法。


这是一种通过在 CK 创建外部 HDFS 表引擎的玩法。


但是,要知道,这个方法并不能直接将数据导入到 CK 里面,而只是利用 CK,构建了一个可以访问 HDFS 数据的客户端。 


想要把最终的数据给灌到 CK 内部表里,还需要进一步通过 insert into... select... from... 的方式。



1. 导入第1次尝试


虽然操作过程要比 Doris 的 stream load 以及 broker load 复杂一点,但理论上,还是要比用外部的数据导入软件安逸多了。


但理论归理论,经过我的实测,这破玩意是真的难用。


一开始,为了测试这个方法的可行性,我在 HDFS 上建了个只有区区 7 条 json 数据的测试文件,然后创建这样一张外部表(这里不能像 Doris 那样,可以自由的对字段进行映射):


CREATE TABLE json_from_hdfs
(
    `score` String,
    `whois` String,
    `sld` String
)
ENGINE = HDFS('hdfs://192.168.211.106:8020/DATA/sample.json''JSON')

按理说,这么小的数据量,查询起来是一件 so easy 的事情。


但是,你瞅瞅



这查询时间,像话吗?


要知道,我可是一会要导入2千万的数据量呢,这效率,谁能受得了,只能说 CK 对于这块的设计过于粗糙,很难用。


当我尝试用这个 insert into 的方式,把这2千万数据写入到目标表,发现,果然不靠谱。


跑了2个多小时,愣是一条数据都没写进去,完了还失败了,就问你生不生气吧。



2. 导入第2次尝试


为了避免浪费时间,果断改用万能的 Spark。


经过调试之后的代码如下:


package cn.pcl.csrc.batch

import java.sql.Date
import java.text.SimpleDateFormat
import java.util.{LocaleProperties}

import com.alibaba.fastjson.JSON
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveModeSparkSession}

/**
  * @DESC: Spark 从 HDFS 读取2千万 json 数据,写入到 CK 中
  * @Auther: Anryg
  * @Date: 2024/05/29 20:34
  */

object SparkReadHDFS2CK {
    def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf().setAppName("SparkReadHDFS2CK").setMaster("local[*]")
        val spark = SparkSession.builder().config(sparkConf).getOrCreate()

        import spark.implicits._  /**引入隐式转换对象*/ 

        val rawDF = spark.read.textFile("hdfs://192.168.211.106:8020/DATA/2kw_whois/json_file.json")

        val df01 = rawDF.map(line => {
                        val jsonObj = JSON.parseObject(line)
                        val domain = jsonObj.getString("sld")
                        val whoisObj = JSON.parseObject(jsonObj.getString("whois"))
                        val create_time = if (whoisObj.getString("createddate") != null) whoisObj.getString("createddate"else new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"Locale.CHINA).format(new Date(System.currentTimeMillis()))
                        (domain, whoisObj.toString, create_time)
                    }).toDF("domain""detail_json""create_time")
        
        /**CK属性配置*/
        val propertiesCK = new Properties()
        propertiesCK.put("username","default")
        propertiesCK.put("password""xxx")

        /**结果写CK*/
        df01.write.mode(SaveMode.Append)
            .jdbc("jdbc:clickhouse://ck_host:port/test",
                "whois_json01",
                propertiesCK)
    }
}

虽然... 但是,数据入库这件事对 CK 来说,好像并没有那么简单,我也不知道为什么,把一个 json 数据写入到包含有 json type 的 CK 表,居然能抛出下面的异常。


 

关键,我的表里压根就没有「数值」类型的字段呀。


写入报错前的表字段类型

如果我告诉你,还个报错,其实是 json type 惹的祸(这个时候一条数据都写不进去),你会作何感想。



3. 导入第3次尝试


当我把 detail_json 这个字段类型给换成 String 之后,


修改后的表字段类型


你猜怎么着?


它就可以了,很快数据就能写进来。


但是,这里又有个但是,数据写到这里,程序又又异常了。


从这个报错来看,你能看出异常的原因吗?我是看不出来。


但如果我告诉,是因为当前 Spark 任务写入的线程数太多导致的,你信吗?



4. 导入第4次尝试


果然,当我把 Spark 进程默认的线程数量(8个),给改小成2个后,这个问题就消失了。


因为我直接用的 Spark 本地模式跑的,本地 CPU 有8个线程,修改前的线程设置是这样的:


修改后,是这样的:


然后,它就可以了。


大概经过1个小时的本地2线程运行,2千万数据全部导入到 CK 的本地表。


有条脏数据被我删了

但还没完呢,我们最终的目的,是要把 detail_json 这个字段的值,都存储成 json type,方便后续以 json 的方式对它内部的 key 进行查询,但现在这张表,这个字段是个 string type 算怎么回事呢。


于是,我还得额外再创建一张 detail_json 字段类型为 json type 的表。


由于考虑到数据量还比较大(毕竟2千万呢),所以开始就建了一张「分片


然后,再通过 insert into... select * from... 的方式,把前面那张表的数据,给写入到当前分片表中来。


应该很简单对不对,你猜这一步,它会不会出问题?


来,报错虽然会迟到,但它绝对不会缺席。



又是一堆让人懵逼的异常,诡异的是,它并不是一条数据都写不进去,而是当数据写入了大概三十万左右,才抛的异常。


但是最后,你猜我怎么解决的。



5. 导入第5次尝试


也不知道哪来的灵感,一模一样的表结构,我最后给它换成了本地表,然后,这个导入方式,它就没有问题了。


CREATE TABLE whois_json_final01 (
  domain String,
    detail_json Json,
    create_time String
  ) ENGINE = ReplacingMergeTree
order by domain;

就问你神不神奇?



全部数据导完,耗时近半个小时。


看一眼,存进来 json type 数据的样子:



后续,就可以用 detail_json.admin_address、detail_json.admin_city 这种嵌套的方式,对 json 内部 key

进行查询了。


比如,查询数据中,域名注册人不为空的的名字,SQL 语句就可以这样来写:



但 Doris 想要达到相同的查询目的,从上次的测试结果来看,貌似做不到。



最后


从上面折腾的过程来看,将这份2千万的 json 数据,给存储到 CK 一个比较合理的表结构里,得经过一番「跋山涉水」才行。


同样的需求场景,从数据入库过程来看,Doris 提供的是多条「康壮大道」,而 CK 提供的,只有几条「羊肠小道,效率低不说,还一路磕磕碰碰,状况不断,而且只能写本地表(且针对当前带 json type 的表)


但是,从入库后的查询体验来看,由于需要处理情况比较复杂的 json,CK 终于扳回一局,挽回点颜面。


所以如果你要问我:Doris 跟 CK 这两个数据库,哪个更牛逼一点?


我只能说:我也不知道耶。

请使用浏览器的分享功能分享到微信等