作者: 康凯森
日期: 2017-06-21
分类: OLAP
我从5月中旬开始进行Kylin 2.0的升级,现在的版本是Kylin 1.5.4.1。本次升级的所有工作均由我一人完成,升级耗时和我之前估计的差不多,1个月左右,其中每天平均半天左右的时间在当“客服”(帮用户答疑,错误处理,调优,追查问题)。 本次Kylin 2.0升级已经基本完成,所以写下此文对本次Kylin 2.0升级进行总结。主要包含以下内容:
首先我们简要回顾下 Kylin 2.0的 升级节奏:
(备注:我们的Kylin服务共有Dev, Test, Staging, Prod4个环境,这也应该是Kylin生产级别的标准配置,其中Dev和 Test环境共用一个HBase集群,Staging和Prod环境共用一个HBase集群)
稳定压倒一切。对稳定性有影响的功能直接禁用。稳定性达不到生产环境要求的新特性就不启用。
希望此次升级是一次对用户几乎透明,几乎无影响,平稳上线,无case的升级。
需要代码合入的原因是我们内部的代码和社区的diff已经越来越多,所以必须将我们内部的代码合入到社区的2.0 版本中。代码合入是一个十分耗神的苦力工作。因为我们早期的commit message不是很规范,所以几乎每条commit我都要仔细认真的check下,确认每条commit是否已经合入社区,除非是我自己印象很深的commit。代码合入要求我们对Kylin的代码本身必须是比较熟悉的,这样当cherry-pick出现diff时我们才能快速,合理的处理。为了减轻代码合入的成本并减少失误,dayue和我在今年2月份引入了以下规范:
1. 所有修改配置的commit,加上[CONFIG]前缀 //我们的配置和代码在一起,主要是为了实现一键代码编译,打包,部署,重启服务。这样可以明显提升开发测试效率,大幅缩短线上紧急bug修复的时间,现在从kylin的源代码到kylin的服务重启一般只需2到3分钟。
2. 所有移植社区master某个功能的commit,加上[BACKPORT]前缀,表示社区master已有该commit
3. 所有美团内部独有的commit,加上[MT]前缀
4. 需要合入社区的改动,commit中最好同时带上我们的JIRA号和社区的JIRA号
KYLIN-2195 引入了配置的命名规范,更新了Kylin所有配置的命名。 虽然新旧配置是兼容的,但是为了和新版保持一致,我梳理了之前所有的旧配置,移除所有不必要的配置,更新了配置名。将之前的配置从100多个删减到40多个。这次升级配置相关的问题暴露出好几个,后面会提到。
代码合入完成后,我首先进行了兼容性测试。Kylin的兼容性主要分3个部分: 元数据兼容性,Coprocessor兼容性, API兼容性。
元数据不兼容 意味着升级几乎无法进行
Coprocessor不兼容 意味着升级无法灰度,升级成本极高,升级难度极大
API不兼容 其实只要能提前测试发现并提前通知用户即可
首先Kylin 2.0的元数据整体上是兼容Kylin 1.5.4.1的, 不过存在以下问题:
overrideKylinProps
,liyang Review的时候改成了override_kylin_properties
,关键是我当初发现了这一点,而且我还重构了ProjectRequest,但是我当时完全忘了考虑兼容性的问题。Coprocessor兼容性主要是指Kylin QueryServer和HBase RegionServer的Coprocessor通信时 序列化和反序列化的兼容性。 主要是KYLIN-2603和KYLIN-2212打破了兼容性。我处理方式比较简单直接,就是Revert掉相关Commit。当然,我们也可以选择让这些功能变成可配置的,或者直接让这些功能变成兼容的。显然,后两种的成本会高一些。
实际上本次升级我几乎没有测试API兼容性,这次升级只暴露出一个API兼容性的问题,是我们的用户反馈出来的。就是cube_desc的Dimension信息中的table字段内容格式被修改。2.0前table字段的格式是DBname.Tablename,2.0后table字段的格式是table的别名。其实这个合理的做法应该是新加个alias的字段来表示别名。
构建测试就是抽取了30多个Cube进行build测试,为了节省资源,快速出结果,我没有选取一些复杂的大cube,这也导致了在测试的时候没有发现Cube构建的性能问题。
查询测试主要是利用我在上次升级时开发的线上查询回放测试工具。原理是一个线程用Presto从Hive表获取某个cube某天的所有SQL,然后再用一个线程池去查Kylin。其中查Kylin的时候每100条会对新旧两个版本的查询结果进行校验。最后会统计输出每个cube查询的成功率,查询时延,失败的具体SQL和异常。但是对于PreparedStatement的查询,由于获取不到具体的SQL,我只能向用户要了10条左右的SQL进行手动验证。
测试的过程主要是把project,cube,model,job的操作过了一遍,我测试的时候没有发现什么问题。 其实web前端的问题我主要是靠Staging 环境的用户发现并反馈,因为web的具体操作很多,我不可能把所有细节都测试一遍。 而且web出问题影响也不大,因为web的问题不会影响生产,而且只要web发现问题,一般我都可以较快修复。
kylin.query.endpoint.compression.result
这条配置的特殊性以及CubeVisitService中的KylinConfig不是线程安全的,这会导致HBase返回的查询结果是压缩的,但是QuerServer按照不压缩的去反序列化。case A when 0 then 0 else 1.0 * B/A end
Kylin 2.0中如果A是null查询则会失败,列出这个问题是因为这个问题影响比较大,我们的多个用户都有这种写法。原因是新版Calcite 生成的代码有问题,没有处理A是null的情况。解决办法是SQL改写为 case A when 0 then 0 else cast (B as double) /A end
,Calcite对这个SQL生成的代码对A等于null时有特殊处理,直接返回null。对于Statement,Kylin中Date的转换格式如下:
1 Date类型的2015-01-01 在OLAPFilterRel.cast中 转为 1420070400000
2 1420070400000 在请求HBase前后会有编码和解码
3 1420070400000 在Tuple的convertOptiqCellValue转为16436(epoch time)
对于PreparedStatement: Kylin的处理过程如下:
1 KylinClient的AvaticaPreparedStatement 将2015-01-01 转为 16436
2 KylinClient的KylinPreparedStatement 将16436 转为 2015-01-01。
3 KylinServer在setParam时 AvaticaPreparedStatement 再次将2015-01-01 转为 16436
4 在OLAPEnumerator.bindVariable() 中对PreparedStatement的Date类型 还有特殊转换,不过此处有问题, 因为此处Kylin认为2015-01-01的格式应该是2015-01-01,但实际上是16436。
所以我认为该问题的解决方法可以是:
在OLAPEnumerator.bindVariable() 将16436 转为1420070400000,应该只需改一行代码。
kylin.engine.mr.uhc-reducer-count
默认值变成了1。我清晰的记得我当初专门把这个值改成了3,为了让IT可以cover这个feature。kylin.cube.size-estimate-ratio
和 kylin.cube.size-estimate-countdistinct-ratio
参数有问题,或者是新版估计cuboid大小算法有变化。 我新确认了cuboid大小估计算法的diff,发现虽然有略微区别,但本质上是一样的。后来我尝试掉了几次这两个参数的大小,发现并没有明显效果。后来当我注意到cuboid总大小,Region大小,HFile的大小关系时,才发现好几个Cube HFile的大小是egion大小的一半。这时我才注意到计算HFile时的这个分支: if (hfileSizeMB > 0.0 && kylinConfig.isDevEnv()) {
hfileSizeMB = mbPerRegion / 2;
}
我开始一直以为kylinConfig.isDevEnv()是false,就忽略这段代码,结果点进去后发现kylinConfig.isDevEnv()的默认值是true,而我这次梳理配置时把kylin.env的这个配置删除了,我觉得这个参数没啥用。而实际上,估算hfileSizeMB依赖kylin.env的配置肯定是不合理的,想cover IT,直接把sandbox的Hfile的大小调小就可以。SELECT MIN(A) A FROM table WHERE A = '2017-06-05' //其中 A 是维度
解决办法就是将 维度作为指标的情况 判为 非精准聚合
这个问题可以近似等价于以下问题:
首先,这个世界上没有完全没有bug的系统,也不存在100%可用的系统。我们的目标只能是提供可用性尽可能高的系统,比如3个9的可用性,4个9的可用性。关于系统可用性的概念可以参考来自 Google 的高可用架构理念与实践或者关于高可用的系统。
我认为可能有以下几点:
1 复杂系统必然有很多模块,那么这些模块这件的相互影响就会比较复杂。 如果只写一个二分搜索或者快排函数,那么我们可以很容易确定我们的函数是没有问题的。 因为输入和输出是简单的,各种边界情况和异常情况也是有限的。 但是在复杂系统中,你有时候一个看似很简单的独立改动,也会对其他模块造成影响。 比如KYLIN-2619,我只是换个线程池,结果UT挂了,本质原因是Kylin使用的HTTPclient是不支持并发的。 比如KYLIN-2672,我只是优化了Cube迁移的缓存更新,结果没想到切完Cube后导致整个线上的查询挂了,本质原因是TblColRef在检查TableDesc一致性的时候用了 == 而不是 equals。其实我们应该使用equals。
2 复杂系统实际应用时的具体环境和参数都是不同的,而不同的context可能会导致不一致的表现。很多时候系统会出现只是某一部分模块cover了所有已知的环境,但是某些模块只cover了部分。
3 复杂系统的依赖一般比较多,越多的依赖必然引入越多的稳定性风险。比如Kylin很好的融入了Hadoop社区,这是其优点,也是其显著的缺点,比如HBase,Mapreduce,Yarn,HDFS,Hive随便一个系统出点问题或者有bug,都会给Kylin带来显著影响。新版还加入了Spark和Kafka的依赖。 此外,越多的系统依赖,也使得Kylin的日常运维成本极高,此处的运维不仅指你需要确保Kylin所依赖系统的稳定性,了解Kylin所依赖系统的原理,这都是应该的,最主要的是你还需要教给你的用户这些系统的简单原理,它们在Kylin中的作用,出了问题怎么排查。
4 现在的复杂系统一般都是分布式系统,而我们知道分布式系统天生就有许多难题:不可靠的网络,不可靠的时钟,进程无响应,单机挂掉等。
说了这么多,我其实一直挺好奇像神舟飞船这种完全不能出错的系统到底是怎么保障可靠性的?
我谷歌了下,发现尽然有专门的大学专业:可靠性系统工程。 还买了两篇相关的论文读了下:《可靠性系统工程的理论与技术框架》,《航天器环境试验和航天产品的质量与可靠性保证》。结果发现这教授写的论文和我的本科论文一样水,没啥干货。最后在《握手太空的航天科技》书中谷歌到一点答案,其实发现和我们保证一个高可用的软件系统原理是一样的:
首先是大量,严密的测试确保飞船的一些组件,功能是正常的。 和软件系统一样。
其次是关键部件的备份,冗余,takeover,关键设备都是3份同时工作。 和软件系统一样。
最后是分析飞船可能出现的所有故障情况,并给出应急方案。 也和软件系统一样,我们既然不能保证不出case,那么我就尽可能保证出了case立即发现,立即处理,立即恢复。
可以发现,保证系统可靠性的原理和思路在任何领域都是一致的
除了以上几点,可靠性的系统当然需要可靠优秀的总体设计或者架构设计,也需要可靠的细节实现或者代码实现。
当然,还有个显然的问题就是,神舟飞船在地面怎么测试太空的场景?答案是模拟
。 所以我们现在可以回答 我们的测试到底需要测到什么程度 这个问题。答案是,如果你能在线下造出和线上完全相同的环境,那么你就可以用线上真实的数据或者case进行测试,Kylin在升级时其实是完全可以做到的,只不过这种做法成本太高,所以我们就需要模拟。有人会问,网络隔离,磁盘挂掉,机器down掉,CPU持续飙高等情况可以模拟吗,答案是可以模拟的,请参考以下篇文章:
这3片文章对测试的讲解十分深入,值得大家一读,大家也可以反思自己系统的测试。
具体大家可以参考下面的参考资料,引用陈皓的话总结:高可靠的系统是一个系统化的工程,这不是一个人或者几个人可以做到的,取决于全公司的技术实力和工程素养。 比如可用性4个9以上的系统,小公司基本不太可能做的出来。
《designing-data-intensive-applications》 5星力荐,很赞的一本书。
Kylin 2.0为我们带来多Segment并发重导,TrieDictionaryForest字典,雪花模型,百分位函数,Steaming cubing,Spark cubing等实用功能和新特性,以及若干bug Fix 和性能提升。十分感谢Kylin社区,身为Kylin commiter,能够理解每位Kylin contributor的付出,因为很多时候,我们都是在自己的业余时间和假期向Kylin社区贡献。
我们每个JIRA的Assignee都应该在JIRA中描述必要的信息。 Bug类型的issue应该描述bug产生的原因,Fix bug的思路。Improve类型的issue应该描述是如何改进的,如果有性能对比则更好。New Feature类型的issue应该描述清楚背景或动机,并简要描述实现思路。 Issue Fix后应该在JIRA中给出github commit的链接。现在Kylin的大多数JIRA描述信息太过简单,要想知道基本的实现思路,必须自己去读代码,而且具体的commit信息还得根据JIRA号去找。
建议用Github的PR代替patch。 好处是首先代码阅读更方便,代码Review更方便,这样commit中就不会有那么多code review的commit。其次是Github可以和很多自动化工具集成,目前kylin中commit中经常有Fix UT和FIX IT,如果可以让每个PR自动跑UT和IT,只有通过后才允许合入Master,就不会有这个问题。
本次升级基本符合目标。
一个意外是在所有环境升级2.0的4天之后,发生了一次查询事故。事故的原因是升级2.0后,由于新版的Coprocessor会加载更多的类,所以HBase RegionServer的PermGen增加了10M左右,超过了MaxPermSize,所以PermGen就OOM了。事故的本质原因是RegionServer的PermGen配置较小和Kylin Coprocessor 中catch了OOM异常。而事实上OOM异常几乎没有理由去catch,Kylin Coprocessor中更不应该去catch OOM。