来源:安瑞哥是码农
上篇文章测试了用 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 在上篇文章中已经测试验证过了,简单且都比较好用。
详情可参考上一篇文章内容:
此外,还可以借助外部的工具,比如专门的数据传输软件(如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.{Locale, Properties}
import com.alibaba.fastjson.JSON
import org.apache.spark.SparkConf
import org.apache.spark.sql.{SaveMode, SparkSession}
/**
* @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 这两个数据库,哪个更牛逼一点?
我只能说:我也不知道耶。