Alluxio导读
近期,腾讯 Alluxio 团队与 CDG 金融数据团队、TEG supersql 团队、konajdk 团队进行通力协作,解决了金融场景落地腾讯 Alluxio(DOP=Data Orchestration Platform 数据编排平台) 过程中遇到的各种问题,最终达到了性能和稳定性都大幅提升的效果。
背景
在腾讯金融场景中,我们的数据分析主要有两大入口,一个是基于sql的分析平台产品idex,另一个是图形化的分析平台产品"全民BI"。全民BI是一款类似tableau一样的可以通过拖拉拽进行数据探索分析的工具,因为不需要编写sql,所以面向人群更广,不仅包括了数据分析人员,还有产品和运营,对耗时的敏感度也更高,而这里主要介绍的是针对全民BI应用场景的落地优化。
为支持日益增加的各类分析场景,今年腾讯金融业务数据团队进行了较大的架构升级,引入了 Presto + 腾讯 Alluxio(DOP),以满足用户海量金融数据的自由探索需求。
大数据olap分析面临的挑战
| 挑战一:从可用到更快,在快速增长的数据中交互式探索数据的需求。
虽然这些年SSD不管是性能还是成本都获得了长足的进步,但是在可见的未来5年,HDD还是会以其成本的优势,成为企业中央存储层的首选硬件,以应对未来还会继续快速增长的数据。
但是对于olap分析的特点,磁盘的IO是近乎随机碎片化的,SSD显然才是更合适的选择。
下图展示的是olap分析中presto对一个ORC文件读取的视图,其中灰色竖条表示具体的分析需要读取的三列数数据在整个文件中可能的位置分布:
| 挑战二:在多种计算任务负载,olap分析的性能如何在IO瓶颈中突围
企业大数据计算常见的两种负载:
✓ ETL:数据的抽取(extract)、转换(transform)、加载(load),主要是在数据仓库、用户画像、推荐特征构建上,特点是涉及大部分的数据列。
✓ OLAP:在线联机分析处理,主要用在对数据的多维度分析上,特点是仅涉及少量的数据列,但可能涉及较大的数据范围。虽然ETL的峰值会在凌晨,但其实整个白天都会有各种任务在不断的执行,两种类型任务的IO负载的影响看起来不可避免,再加上中央存储层HDD硬盘的IO性能约束,IO很容易会成为数据探索的瓶颈。
| 一种流行的解决
方案面对这些挑战,目前很多企业会选择下面的这种架构:
将olap分析需要的热数据(比如近一年)复制到一个olap专用的存储中,这样不仅可以解决IO竞争的问题,还可以选用SSD硬盘,进一步加速olap。
但是这样的架构却又引入了新的问题:
✓ 数据的边界:因为数据需要提前复制,如果需要临时分析超出约定范围的数据(比如同比去年),就会导致只能降级到中央存储上的引擎去执行,这里不仅涉及到存储的切换,也涉及到计算引擎的切换。
✓ 数据的一致性和安全:数据复制需要面对数据一致性的拷问,另外就是这部分数据的权限和安全问题能否跟中央存储进行关联,否则就要独立管控数据的权限和数据安全,这无疑又是不小的成本,这一点在注重监管的金融行业尤其如此。
Alluxio:一种可能更优的方案
重新思考我们的olap引擎的存储需求其实是:
1)有一份独享的数据副本,最好采用SSD的存储,满足更高的性能要求
2)不需要额外的数据管理成本:数据生命周期、权限和安全
所以我们首先想到的其实是在HDFS层面解决,Hadoop在2.6.0版本的时候引入了异构存储,支持对指定的目录采取某种存储策略,但是这个特性并不能解决我们的几个问题:
✓ 不同计算负载的IO隔离:因为这部分对于olap引擎(比如presto)和etl引擎(比如spark)是透明的,无法实现让olap引擎访问某一个指定的副本(比如ONE_SSD策略的SSD副本)
✓ 数据生命周期的管理成本高:如果要根据冷热做动态策略管理还有大量的工作要做
数据副本其实可以分成物理和逻辑层面来考虑:
1)物理两套,逻辑两套:需要面对两份数据管理的问题
2)物理一套,逻辑一套:难以解决IO隔离的问题
在上面两种不可行的情况下,我们自然地想到了另一个思路:
✓ 物理两套,逻辑一套?
而Alluxio恰好在这个思路上给了我们一种可能性:Alluxio的元数据可以实现跟HDFS的同步,有比较完善的一致性保障,所以可以理解为在Alluxio中的数据跟HDFS是一份逻辑数据。而基于数据冷热驱逐的自动化机制给更灵活的数据生命周期的管理提供了一条通路。
这样,结合数据的预加载,结合Alluxio的缓存特性,不仅做到了无边界的访问中央存储的数据,同时也实现了热数据的IO隔离和SSD加速。
但区别于更流行的缓存加速的用法,我们使用Alluxio的方式更倾向于IO隔离
| Alluxio的缓存策略选择
Alluxio的两种主要缓存策略
✓ CACHE : 通过Alluxio访问后,如果不在Alluxio中,则会进行缓存,单位为block
✓ NO_CACHE:通过Alluxio访问后,如果不在Alluxio中,不进行缓存
两种策略对应两种不同的存储管理方案:
【名词解释】预加载查询:是通过olap应用系统登记注册的分析主题(对应库表),然后构造的简单聚合查询:select count() ,来触发Alluxio的数据加载。最后考虑到长期的管理和运维复杂度,我们选择了路径单一容错性更高的CACHE方案*
新的挑战
思路清晰了,但是还是有三个挑战:
1)如何让Alluxio只应用于olap引擎,而避免修改公共hive元数据中的数据location
2)如何避免一个随意的大范围查询导致其他数据被大面积驱逐?
3)异构存储机型下,我们的缓存请求分配策略怎么选择?
| 挑战一:如何让Alluxio只应用olap引擎,而无需修改hive元数据?
因为alluixo的访问schema是:Alluxio:// ,所以正常情况下使用Alluxio需要在hive中将对应表格的地址修改为Alluxio://,但如果那样做的话,其他引擎(比如spark)也会同样访问到Alluxio,这是我们不希望的。
得益于TEG 天穹presto团队的前期工作,我们采取的做法是通过在presto中增加一个Alluxio库表白名单模块解决。也就是根据用户访问的库表,我们将拿到元数据的地址前缀hdfs://hdfs_domain/user-path替换成了alluxio://allluxio_domain:port/hdfs_domain/user-path, 这样后续的list目录和获取文件操作都会走alluxio client,以此解决alluxio的独享问题。
另外对于商业版本的alluxio,还有一个Transparent URI 的特性可以解决同样的问题。
| 挑战二:如何避免随意的大范围查询导致其他数据被大面积驱逐
利用库表白名单,我们实现了对Alluxio存储的数据的横向限制,但是依然存在一个很大的风险就是用户可能突然提交一个很大范围的查询,进而导致很多其他库表的数据被evict。
因为只要采用的是CACHE策略,只要数据不在Alluxio,就会触发Alluxio的数据加载,这时候就会导致其他数据根据evict策略(比如LRU) 被清理掉。
为了解决这个问题我们采取了下面的两个关键的策略:
✓ 基于时间范围的库表白名单策略:
在库表白名单的横向限制基础上,增加纵向的基于分区时间的限制机制,所以就有了我们后面迭代的基于时间范围的库表白名单策略,我们不仅限制了库表,还限制了一定的数据范围(一般用最近N天表示)的分区数据,然后再结合用户高频使用数据的范围,就可以确定一个库表比较合理范围。下面是一个样例片段参考:
"dal_base.*",
"dal_base.*.${yyyyMMddHH:(-720h,0)}",
"dal_base.*.${yyyyMMdd:(-217d,0)}",
"dal_base.*.${yyyyMM:(-36m,0)}"
✓ 降低Alluxio worker异步缓存加载的最大线程数:
Alluxio.worker.network.async.cache.manager.threads.max 默认是2倍cpu核数,我们基本上是调整成了1/2甚至是1/4 cpu核数,这样因为查询突然增加的load cache请求就会被reject掉,降低了对存量数据的影响。
这样我们实际上就是构建了一个Alluxio的保护墙,让Alluxio在一个更合理的数据范围内(而不是全局)进行数据管理,提升了数据的有效性。
而且采用这样的策略,部分直接走HDFS的流量不管是耗时,还是对Alluxio的内存压力都会有所降低。
| 挑战三: 异构存储机型下,我们的缓存请求分配策略怎么选择?
这个也是将Alluxio当作一个存储层,可以独立扩展必须要面对的,新的机型不一定跟原来的一致。面对异构 Worker 存储的需求,Alluxio已有的块位置选取策略,都会造成热点或者不均衡的问题,不能有效利用不同worker上的存储资源。比如:
✓ RoundRobinPolicy、DeterministicHashPolicy:平均策略,将请求平均分配给所有Worker,会导致小容量的worker上的数据淘汰率更高;
✓ MostAvailableFirstPolicy:可能会导致大容量worker容易成为数据加载热点;而且因为所有worker存储最终都会达到100%,所以满了之后这个策略也就是失去意义了。
因此 我们积极参与腾讯 Alluxio 开源社区,设计并贡献了“基于容量的随机块位置选取策略 CapacityBaseRandomPolicy”。
该策略的基本思想是:在随机策略的基础上,基于不同worker的容量给予不同节点不同的分发概率。这样容量更大的worker就会接收更多的请求,配合不同worker上的参数调整,实现了均衡的数据负载。
如下图所示,是上线初期的容量情况,第一列是存储容量,第二列是使用容量,可以看到基本是按比例在增长。
除了上面的三个挑战,我们还对方案中的一个问题"presto触发查询会导致多副本问题"做了优化。因为presto的查询会将一个文件拆成以split为单位(默认64MB)进行并行处理,会在不同Worker上触发缓存,实际上会对数据产生多个副本。本来我们使用DeterministicHashPolicy来限制副本数量,但是由于切换到了CapacityBaseRandomPolicy,我们再一次对副本数失去了控制。因此我们做了如下两个优化:
✓ 预加载查询设置大split(max_initial_split_size,max_split_size):使用跟alluxio block size一致的split,比如256MB,这样避免一个文件被拆成多个split
✓ 对CapacityBaseRandomPolicy增加了缓存机制:避免了同一个worker多次请求发送到多个worker上,触发多个副本加载问题
最终架构
在落地过程中,为了满足实际存储需求,额外申请了SSD存储机型扩容了Alluxio worker,最终采用了 Presto + 腾讯 Alluxio(DOP) 混合部署以及独立部署 Alluxio Worker 的架构,即有的服务器同时部署了Presto worker和Alluxio worker,有的服务器仅部署Alluxio worker,该架构具有计算和存储各自独立扩展的能力。
线上运行效果
我们基于某一工作日随机抽取了一批历史查询,5个并发,由于完全是随机的,所以查询涉及的范围可能包含了部分一定不走Alluxio的数据(不在预设的白名单时间范围,或者没有命中),但是能更真实反映我们实际使用的效果。
测试我们选取了两个时间段:
1) 周末下午:500个查询,大部分ETL任务已经完成,HDFS大集群负载低,这时候主要看SSD加速效果。
2)工作日早上:300个查询,这个时间点还会有很多ETL,画像标签、推荐特征等任务运行,HDFS集群繁忙程度较高,这个主要看IO隔离性。测试结果如下:
闲时:
图中的横坐标是按耗时从低到高排序后的500个查询(剔除了最大值213秒),纵坐标是耗时(单位秒),其中90分位的耗时有Alluxio和无Alluxio分别是16s和27s,90分位的查询性能提升为68.75%,这里主要是SSD带来性能提升。
忙时:
图中的横纵坐标如上一个图一致,横坐标是300个按耗时排序后的查询,注意:因为查询覆盖的数据范围可能超过Alluxio的数据范围,所以会出现极端值。效果总结:
从测试结果可以看到:
✓ SSD提速:即使在闲时对50%以上的查询都有一定幅度的提升效果,在90分位达到了68%的性能提升。
✓ IO隔离优势:可以看到HDFS忙时,无Alluxio的90分位查询会明显上升,但是有Alluxio的查询非常平稳,在90分位到达了+294%的性能提升。
优化调优实践
| 采用腾讯 Konajdk + G1GC
腾讯 Alluxio(DOP) 采用 KonaJDK 和 G1GC 作为底层 JVM 和 垃圾回收器。KonaJDK 对于 G1GC 进行了持续的优化,相较于社区版本,针对腾讯内部应用特点进行了深度的优化,减少了GC暂停时间和内存使用。
| 利用腾讯 Kona-profiler 定位高并发访问 Alluxio Master FGC 问题
当出现业务海量并发查询请求场景,Alluxio Master 出现了频繁 FGC 的情况,并且内存无法大幅回收,导致 Alluxio Master 无法正常提供服务,影响业务使用。我们获取了 JVM heap dump 文件,使用 kona-profiler 进行分析。
使用 kona-profiler 快速发现问题的瓶颈在于:短时间内出现了大量未被释放的 Rocksdb 的 ReadOptions 对象,这些对象被Finalizer引用,说明ReadOptions对象可以被回收了,但是在排队做 finalizer 的函数调用,进一步定位发现,ReadOptions 对象的祖先类 AbstractNativeReference 实现了 finalizer 函数,其中的逻辑又过于复杂,因此回收较慢,这在7.x 版本的 rocksdb 已经修复。
但由于版本升级跨度过大,我们采用另一种办法解决该问题。配置腾讯 Alluxio 的alluxio.master.metastore.block=ROCKS_BLOCK_META_ONLY,支持把 blockLocation 独立放置于内存管理,而 block 信息使用 rocksdb 管理,这样从根本上避免了原本海量获取 block 位置操作,构造海量 rocksdb 的 ReadOptions 对象的问题。升级改造后。
Alluxio 侧,在压测的情况下,999分位从原来的 10ms 减少到了 0.5ms,qps 从 2.5w 提升到了6.5w;正常负载下升级前rpc排队情况:
正常负载下升级后 rpc 排队情况:
Presto 侧:对于涉及分区很多的查询场景,比如大范围的点击流漏斗分析,在一个基准测试里,从 120 秒减少到了 28 秒,提升了4 倍。
| 周期性出现50秒慢查询问题参数优化
一个查询多次执行耗时差很多。大部分在7秒左右,但是会偶尔突然增加到50秒,就是某个数据读取很慢,测试的时候集群的负载还是比较低的。
下图是慢查询时 Presto 的调用栈
结合源码,可以看出此时 Alluxio 客户端认为拿到的 BlockWorker 客户端是不健康的。
判断健康的判定标准为:不是 shutdown 状态,且两个通信 channel 都是健康的。
根据上下文,可以判断,目前不是 shutdown 的,那么只能是两个通信 channel 不健康了。进一步结合源码,定位在 closeResource 过程中,会关闭并释放 grpcConnection,这个过程中会先优雅关闭,等待超时如果未能优雅关闭则转为强制关闭。
因此,规避该问题,只需要修改调小配置项 alluxio.network.connection.shutdown.graceful.timeout 即可。
| Master data 页面卡住的问题优化
Alluxio Master 的 data 页面,在有较多 in Alluxio 文件的时候,会出现卡住的问题。这是因为,打开这个页面时,Alluxio Master 需要扫描所有文件的所有块。为了避免卡住的问题,采用限制 in Alluxio 文件个数的解决办法。可以配置最多展示的 in Alluxio 文件数量。
总结展望
✓ 腾讯 Alluxio(DOP) 支持 BlockStore 层次化,前端为缓存层,后端为持久层,同时,blockLocation 这种不需要持久化的数据,不需要实时写入后端持久层,只需要在前端缓存层失效的时候才需要溢出到后端,该功能正在内部评测。
✓ 腾讯 Alluxio(DOP) 作为一个中间组件,在大数据查询场景,遇到的性能问题,在业务侧,需要业务团队不仅对自身业务非常了解,对 Alluxio 也需要有一定的了解。在底层 JVM 侧,需要 JVM 专业的团队采用专业的技术进行协作解决,从而最大限度的优化,使得整体方案发挥最优的性能。
✓ 这是一次非常成功的跨 BG,跨团队协作,快速有效的解决腾讯 Alluxio(DOP) 落地过程中的问题,使得腾讯 Alluxio(DOP) 在金融业务场景顺利落地。