Druid 历史节点懒加载机制原理
声明:此方案来自于gitHub,详情可见https://github.com/apache/incubator-druid/pull/6988
1. 解决问题
Druid集群由多个节点共同提供服务,不同节点之间各司其职。其中Historical(历史节点)的作用主要是从HDFS上拉取segment到本地,提供这部分segment的查询服务和处理路由到本地的查询。Historical在启动的时候会先加载本地segment文件,加载完成之后,这台节点才可以提供服务。
在生产环境中,我们会尽量追求灰度升级。此时,我们会利用历史节点多副本机制,滚动重启。这样可以确保,一个副本能够提供服务。但是,生产环境中的一台Historical节点上存储的数据量可能达到TB级别,重启加载时间可能达到40分钟+,服务重启周期长,人工耗费大,升级成本高。
究其根本,还是因为Historical加载时间过长导致。而懒加载机制直面了这一痛点。通过延迟加载的方式,先让节点和集群进入能过提供服务的状态。大大提升了升级和重启的速度。
2. 历史节点启动加载流程
Historical 服务启动后,segment正常的加载流程如下:
加载Segments
具体流程如下:
-
获取本地
info_dir
目录下的所有文件(cachedFile
,描述segment的文件)cachedFile文件内容如下:
{
"dataSource": "lazy_load_test",
"interval": "2019-01-30T00:00:00.000+08:00/2019-02-01T00:00:00.000+08:00",
"version": "2019-02-01T20:14:53.608+08:00",
"loadSpec": {
"type": "hdfs",
"path": "/druid/segments/lazy_load_test/20190130T000000.000+0800_20190201T000000.000+0800/2019-02-01T20_14_53.608+08_00/0/index.zip"
},
"dimensions": "language,namespace,unpatrolled",
"metrics": "count,delta",
"shardSpec": {
"type": "none"
},
"binaryVersion": 9,
"size": 3553,
"identifier": "lazy_load_test_2019-01-30T00:00:00.000+08:00_2019-02-01T00:00:00.000+08:00_2019-02-01T20:14:53.608+08:00"
}
校验Segment Cahced File对应的Segment 是否已经拉取到本地的缓存目录中了。若是已经拉取到本地了,则会将对应的segment放到一个队列中。没有拉取到本地的就会把 Cached File 删除。
-
遍历已经拉取到本地的的segments进行加载,每个segment提交一个线程去处理。加载之后就会到ZK上去注册服务。
-
ZK的加载队列上也会增加一个监听器,监听加载队列的变化,若是有CHILD_ADDED事件发生,就会去加载该事件对应的segment。如果CacheFile不存在的话还会被CacheFile写到本地的info_dir目录下。具体的加载过程和本地的加载相似。
-
加载过程就是文件中对每一列进行反序列化。针对不同类型的列获取到不同的反序列器(long,float,complex,stringDictionary)。对每一列中的不同类型的值进行不同的发序列化,通过drd文件中的parts获取到不同的SerDe。
历史节点记载过程耗时比较长的就是对列进行反序列化的过程。反序列化之后的列会被放到一个Map中,作为参数创建
SimpleQueryableIndex
,该类实现了ColumnSelector
接口。在查询的过程中会用到,历史节点加载的过程中并不会用到这个类。懒加载的原理就是在返回
SimpleQueryableIndex
类的时候,不进行反序列化。而是把序列化的过程推迟到对应segment被查询的时候。
3. 懒加载原理
3.1 Supplier 接口
这个接口只有一个方法
T get()
,这个方法需要返回一个T类型的对象,且只有调用该方法的时候对象才会被创建。
Demo
Supplier<Person> supplier = ()-> new Person(); //这里并不会创建Person对象
supplier.get.getName();//到这里才会创建Person对象
3.2 Suppliers 接口
这个接口接口中的一个方法为
memoize
,这个方法可以返回一个Supplier
,区别是通过momoize创建的对象是单例的。
Demo
public static void main(String[] args) throws Exception {
Supplier<Person> s1 = Suppliers.memoize(()-> new Person());
Person p1 = s1.get();
Person p2 = s1.get();
System.out.println("p1.name = "+ p1.getName());
System.out.println("p2.name = "+ p2.getName());
p1.setName("四儿");
System.out.println("=====================\n"+"p2.name = "+ p2.getName());
}
验证输出:
p1.name = 三儿
p2.name = 三儿
=====================
p2.name = 四儿
3. Supplier实现历史节点的懒加载
3.1 Druid 原本的实现

在加载的过程中就会对列进行序列化,但是序列化之后的列只有在查询之后才会被用到。
3.2 优化之后的实现

再改写一下原本的get方法

这样只有在调用getColumn的时候才会进行序列化的操作。这种推迟操作实现了历史节点启动或重启时的懒加载。
4. 优化前后测试
segment个数:21244
segment总大小:551G
优化前,加载总时间为 21 分钟
优化后,加载总时间为 2分30秒
加载速度优化效果明显。
5. 小结
这里的懒加载,就是将真正的反序列化阶段延迟,提前返回加载成功的状态,让历史节点提供服务。但是会对第一的查询产生影响,因为第一次get会触发真正的加载。
网友评论