为什么要做AIOps
上文提到用户在提交Impala查询时最好根据查询大小和复杂度设置Memory Limit(mem_limit参数),设置太大会浪费Pool的内存,降低并发度;设置太小会导致查询被取消。但很多时候用户并不知道如何确定mem_limit的值,往往需要大量的人工执行测试才能得到较准确的经验值。更糟糕的是,一旦集群增加或减少了机器,mem_limit也需要随之调整。
其实大多数分布式计算框架也有类似的资源参数,例如Spark需要为每个任务设置Driver和Executor的Memory和CPU资源,Hive需要为每个查询设置Map、Reduce任务的JVM内存大小。Presto也有query.max-memory,query.max-memory-per-node这些类似的参数。
当然参数调优的工作并非必需:
– 如果硬件资源充足,我们可以干脆设置很大的参数来避免执行失败,当然是以浪费资源为代价
– 如果处理的数据量不大,默认参数往往能解决问题,偶尔有数据量徒增导致失败的情况可以临时人工介入解决
– 如果机器数量比较固定,可以一开始投入研发资源对参数进行测试搜索来得到较优的参数。
但是国双作为一家大数据企业,有很多本地化部署大数据平台的场景,在本地化环境下资源是有限的,而且不可能有大量的运维或开发人员针对每个环境下做大量的人工测试和调优的工作。所以自动参数调优就成为国双必须解决的技术难题。
如何自动调优无非两种方式:
– 基于经验和规则。经验的问题在于:缺少经验的载体。载体可以是人,也可以是程序。无论是人还是程序需要在大量不同的集群环境中有成功的调优经验,并且把经验落地成规则。目前本地化部署环境和数据都是多样化的,基于经验的方式难以奏效。
– 基于历史数据,通过机器学习的方式预测最优参数,即AIOps。这也是国双选择的方式。
在Impala mem_limit参数调优的实践
mem_limit参数
对于一个Impala查询来说,用户可以设置 Memory Limit(mem_limit参数),就是这个查询在单个Impalad节点上能用到的内存上限,如果查询在某个节点超过了这个上限会被直接报错返回。如果用户不设置,默认使用Pool的Default Query Memory Limit,如果Pool没有设置,Impala会自己来估计这个值。据我们的使用经验来说,Impala自己估计的值非常不准确(如果表有统计信息会好一些,但还是很不准)。所以建议用户根据查询大小和复杂程度设置这个值。
mem_limit参数对查询的影响很大,例如一个查询set mem_limit = 10G,有30台Impalad节点,Impala就会认为这个查询会用到10G * 30 = 300G内存。如果Pool的剩余内存目前小于300G,查询就会被Queue住等待资源。所以mem_limit如果设置的太大会浪费内存,导致并发度降低;如果设置的太小会导致查询失败。
解决思路
基本思路
在Impala执行一条查询完成后,在Cloudera Manager里可以看到关于这个查询的很多信息:
有一个属性叫做”每个节点的内存使用峰值“,英文叫memory_per_node_peak,就是这个查询实际执行中的单点内存使用峰值,跟mem_limit的定义是完全一样,只是mem_limit参数是事前用户估计,memory_per_node_peak是事后实际使用值。
除了上述数据,查询的详细信息页还存储了查询的查询计划,扫描文件数量等信息。也有查询选项,包括用户当时为这个查询设置了多大的mem_limit。
有了数据在手,我们就想是否可以采用机器学习的手段训练模型,来预测mem_limit参数,让它无限接近memory_per_node_peak这样一个值呢?
评价指标
为了能够验证、衡量算法的效果,我们确定了两个指标:
– 查询通过率:对于一个查询来说,如果我们预测的mem_limit大于查询执行完成后的memory_per_node_peak,我们认为这次预测是成功的。当然这个在Impala角度并不是一个百分百正确的推论,在某些特殊类型的查询子任务中,Impala单个节点实际使用内存可能会超出memory_per_node_peak,但这只限于极个别操作,而且出现概率很低,我们用规则排除的方式来解决这个问题。对于历史数据来说
历
史
实
际
查
询
通
过
率
=
(
所
有
查
询
总
数
−
因
为
内
存
原
因
被
取
消
的
查
询
数
量
)
所
有
查
询
总
数
模
型
查
询
通
过
率
=
(
所
有
查
询
总
数
−
模
型
预
测
的
m
e
m
_
l
i
m
i
t
小
于
m
e
m
o
r
y
_
p
e
r
_
n
o
d
e
_
p
e
a
k
的
查
询
数
量
)
所
有
查
询
总
数
- 使用内存总量:前一篇文章提到过mem_limit* Impala节点数量 = Impala认为这个查询会用到的内存量。 所以对于历史数据来说:
历
史
实
际
使
用
内
存
总
量
=
∑
所
有
查
询
用
户
当
时
设
定
或
资
源
池
默
认
的
m
e
m
_
l
i
m
i
t
×
I
m
p
a
l
a
节
点
数
量
模
型
使
用
内
存
总
量
=
∑
所
有
查
询
模
型
预
测
的
m
e
m
_
l
i
m
i
t
×
I
m
p
a
l
a
节
点
数
量
如何评判我们的工作是有效的? 对于一段时间的查询历史来说
1. 模型查询通过率需要超过历史实际查询通过率,实际工作中我们要求这个对于每个子业务的查询集合都成立。模型查询通过率越大越好。
2. 模型使用内存总量要低于实际使用内存总量。模型使用内存总量越小越好。
解决步骤
数据准备
首先得有数据,既然Cloudera Manager里存储了查询历史数据(只有最近几天数据有详细信息),我们就开发了一个程序,利用Cloudera Manager的API每天将查询数据爬取下来,用Parquet文件的形式存储在Impala里,供查询和分析。
特征工程
提取特征的工作是用Spark来完成,我们从查询历史数据中提取了查询扫描文件数量和大小,查询计划中不同查询节点(例如SCAN_HDFS,HASH_JOIN,NESTED_LOOP_JOIN 等)的数量,不同聚合方式,Partition信息等。这些特征即存在于历史数据,对于新来的查询通过explain也可以得到。
算法演变
- 我们首先自然而然的考虑使用回归模型来预测mem_limit,但从评价指标来看效果不佳,特别是对于大查询,即 memory_per_node_peak 超过1G的查询。究其原因,一个查询在Impala上使用的资源不仅仅跟查询本身有关,也和当时集群状态,池当时的状态,以及和不同查询子任务节点被分配的Executor节点有关,大查询受外部因素影响尤其严重。当然集群的状态,池的状态数据我们也可以收集,但就简单起见我们还是坚持先尝试用我们已有的特征达到一个较好效果。
- 于是我们采取了一个比较激进的做法:放弃回归,直接采用二分类,即把查询分为大查询和小查询,小查询给一个固定的mem_limit参数值M1,大查询给一个固定的mem_limit参数值M2。M1和M2可以采用基于评价指标搜索的方式得到。二分类算法我们对比了Logistic Regression,RandomForest,SVM,最终采用xgboost。
算法效果
针对近三个月的查询数据
- 从通过率角度来说,算法的总体通过率99.5%,大查询通过率在96%左右,超过了历史查询真实通过率(总体大约在98%,大查询通过率90%),也超过了每个子业务的通过率。
-
从总体使用内存角度,
我们经过计算得出,在近三个月的查询历史中,实际占用的总内存量是实际需要内存的30倍左右,而使用我们的算法设置的内存,算法使用内存大约是实际需要内存的10倍左右,节约率达到了三分之二。
整体架构
下一步工作
- 目标:在保证高通过率的情况下继续降低内存的使用率,逼近memory_per_node_peak
-
方式
- 用多分类取代二分类:二分类虽然有很大优化效果,本质上是相对粗粒度的做法,我们希望采用多分类的方式把内存使用预估做的更细致。
- 分业务建模:对于一些主要业务用户,查询本身有一些相对固定的模式,在单独建模下效果更好,这个已经是经过验证的结果。
- 分时模型组合:对于3个月,1个月,近一周的数据分别建模组合预测。
- 采用更多的特征,例如集群的状态,池子的状态信息
提供辅助工具
我们对开源的Hive JDBC Driver进行了改造,把预测mem_limit的能力直接包装在JDBC Driver中。这样当国双的开发者使用Impala时,不用考虑内存预测或者以后的其他Impala参数设置,直接使用我们的Driver即可。我们未来也考虑把我们的上述工作开源出来,让更多开发者享受到AIOps的好处,也使得Impala更加易用。