我从5月中旬开始进行Kylin 2.0的升级,现在的版本是Kylin 1.5.4.1。本次升级的所有工作均由我一人完成,升级耗时和我之前估计的差不多,1个月左右,其中每天平均半天左右的时间在当“客服”(帮用户答疑,错误处理,调优,追查问题)。 本次Kylin 2.0升级已经基本完成,所以写下此文对本次Kylin 2.0升级进行总结。主要包含以下内容:

  • Kylin 2.0升级的流程
  • Kylin 2.0升级过程中遇到的一些问题
  • 可以供下次升级复用的经验
  • 如何保证系统可靠性的一点思考
  • 感谢Kylin社区
  • 对Kylin 社区的一些建议
  • 整个升级过程的总结

首先我们简要回顾下 Kylin 2.0的 升级节奏:

Kylin 2.0的升级节奏

(备注:我们的Kylin服务共有Dev, Test, Staging, Prod4个环境,这也应该是Kylin生产级别的标准配置,其中Dev和 Test环境共用一个HBase集群,Staging和Prod环境共用一个HBase集群)

  1. 5月15日 开始Kylin 2.0 代码合入。
  2. 5月17日 代码合入完成,Dev环境测试Kylin 2.0。
  3. 5月19日 Test环境升级到Kylin 2.0。
  4. 5月25日 灰度1台Prod的QueryServer到Kylin 2.0。
  5. 6月5日 16台Prod的QueryServer全部升级到Kylin 2.0。
  6. 6月6日 Staging环境2台JobServer升级到Kylin 2.0。
  7. 6月8日 Prod环境3台JobServer升级到Kylin 2.0。
  8. 6月19日 t-digest算法原理分享(非计划内,纯粹兴趣使然,在业余时间内完成)
  9. ToDo 线上的调度脚本支持多Segment并发构建。
  10. Doing Kylin 2.0 新特性调研和分享。

升级的大原则

稳定压倒一切。对稳定性有影响的功能直接禁用。稳定性达不到生产环境要求的新特性就不启用。

升级的目标

希望此次升级是一次对用户几乎透明,几乎无影响,平稳上线,无case的升级。

1 Kylin 2.0 升级流程

1.1 Kylin 2.0 代码合入

需要代码合入的原因是我们内部的代码和社区的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号

1.2 配置更新和梳理

KYLIN-2195 引入了配置的命名规范,更新了Kylin所有配置的命名。 虽然新旧配置是兼容的,但是为了和新版保持一致,我梳理了之前所有的旧配置,移除所有不必要的配置,更新了配置名。将之前的配置从100多个删减到40多个。这次升级配置相关的问题暴露出好几个,后面会提到。

1.3 兼容性测试

代码合入完成后,我首先进行了兼容性测试。Kylin的兼容性主要分3个部分: 元数据兼容性,Coprocessor兼容性, API兼容性。

元数据不兼容 意味着升级几乎无法进行

Coprocessor不兼容 意味着升级无法灰度,升级成本极高,升级难度极大

API不兼容 其实只要能提前测试发现并提前通知用户即可

1.2.1 元数据兼容性

首先Kylin 2.0的元数据整体上是兼容Kylin 1.5.4.1的, 不过存在以下问题:

  1. 定长编码的兼容性。KYLIN-2642
  2. Model元数据的兼容性。2.0版本之前由于Kylin前端有bug以及后端检查较松,当用户修改事实表后,会造成Model中dimensions字段中某一列只有table字段,但是columns列表是空;或者Model中dimensions字段有的table已经不存在。 对于前一种情况,我直接改了代码,对于后一种情况,就必须得修复元数据了。我开始是直接修改元数据的,这种做法效率低下,而且危险系数也比较高。所以就用1个多小时开发测试了 web页面直接修改model json的功能(KYLIN-2665)。
  3. Project元数据的兼容性。这个问题应该说是我自己给我挖的坑。现象是我在staging升级到2.0后,发现每个Project的配置信息都没了。原因是我当初开发这个功能的时候,project配置属性用的是overrideKylinProps,liyang Review的时候改成了override_kylin_properties,关键是我当初发现了这一点,而且我还重构了ProjectRequest,但是我当时完全忘了考虑兼容性的问题。

1.2.2 Coprocessor兼容性

Coprocessor兼容性主要是指Kylin QueryServer和HBase RegionServer的Coprocessor通信时 序列化和反序列化的兼容性。 主要是KYLIN-2603和KYLIN-2212打破了兼容性。我处理方式比较简单直接,就是Revert掉相关Commit。当然,我们也可以选择让这些功能变成可配置的,或者直接让这些功能变成兼容的。显然,后两种的成本会高一些。

1.2.3 API兼容性

实际上本次升级我几乎没有测试API兼容性,这次升级只暴露出一个API兼容性的问题,是我们的用户反馈出来的。就是cube_desc的Dimension信息中的table字段内容格式被修改。2.0前table字段的格式是DBname.Tablename,2.0后table字段的格式是table的别名。其实这个合理的做法应该是新加个alias的字段来表示别名。

1.4 Cube构建测试

构建测试就是抽取了30多个Cube进行build测试,为了节省资源,快速出结果,我没有选取一些复杂的大cube,这也导致了在测试的时候没有发现Cube构建的性能问题。

1.5 Cube查询测试

查询测试主要是利用我在上次升级时开发的线上查询回放测试工具。原理是一个线程用Presto从Hive表获取某个cube某天的所有SQL,然后再用一个线程池去查Kylin。其中查Kylin的时候每100条会对新旧两个版本的查询结果进行校验。最后会统计输出每个cube查询的成功率,查询时延,失败的具体SQL和异常。但是对于PreparedStatement的查询,由于获取不到具体的SQL,我只能向用户要了10条左右的SQL进行手动验证。

1.6 web测试

测试的过程主要是把project,cube,model,job的操作过了一遍,我测试的时候没有发现什么问题。 其实web前端的问题我主要是靠Staging 环境的用户发现并反馈,因为web的具体操作很多,我不可能把所有细节都测试一遍。 而且web出问题影响也不大,因为web的问题不会影响生产,而且只要web发现问题,一般我都可以较快修复。

2 升级中暴露的问题

2.1 查询机灰度中暴露的问题

  • KYLIN-2652 现象是1台QuerServer灰度后,隔了大半天的时间,导致所有线上QuerServer查询都会随机失败。 主要原因是KYLIN-2195忽略了 kylin.query.endpoint.compression.result这条配置的特殊性以及CubeVisitService中的KylinConfig不是线程安全的,这会导致HBase返回的查询结果是压缩的,但是QuerServer按照不压缩的去反序列化。
  • KYLIN-2647
  • 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。
  • PreparedStatement的 Date类型的bug。起初我调试代码,以为是Calcite的bug,后来看了下Calcite的代码,才发现是Kylin的bug。Calcite对Date类型有特殊处理,会将Date类型的值转成epoch time,比如2015-01-01 转为 16436,所以Kylin对Date类型也有特殊处理。

对于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,应该只需改一行代码。

2.2 上线Staging和Prod后Cube构建暴露的性能问题

  • 字典的MR构建对基数超高的列性能很低下。全部禁用了字典的MR构建。
  • 构建Base cuboid时全局字典频繁换入换出。 这个也是我自己埋的坑,因为我解决这个问题时在我们内部用的配置和我最终合入社区的配置不一致。社区的配置在Review时按照建议修改了。
  • 计算列基数这一步变的异常慢。原因是kylin.engine.mr.uhc-reducer-count 默认值变成了1。我清晰的记得我当初专门把这个值改成了3,为了让IT可以cover这个feature。
  • HFile Reducer个数偏少。 我看log发现cuboid总大小估计的比较小,我开始以为是我设置的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;        
 }

2.3 我自己代码的问题

  • 精确去重的Segment粒度的AppendTrieDict之前不支持segment并发,现在已经支持。
  • KYLIN-2606 对精准聚合的精确去重查询的优化。我之前在判断一个SQL是否是精准聚合时,遗漏了下面的情况:
SELECT MIN(A) A FROM table WHERE A = '2017-06-05'  //其中 A 是维度
解决办法就是将 维度作为指标的情况 判为 非精准聚合

3 可供下次升级复用的经验

  1. 兼容性测试时元数据兼容性,Coprocessor兼容性, API兼容性这3点都需要考虑。
  2. Cube构建测试时需要选取一些复杂的大Cube进行测试,需要重点关注构建性能。
  3. 增大线上查询回放的Cube个数,对更多的查询进行测试,重点不在于能测试多少条查询,在于能测试多少种不同类型的查询。
  4. 升级顺序可以和本次一样,可以先灰度线上QueryServer,确定查询没有问题后,再升级Staging和Prod的JobServer。
  5. 升级Staging和Prod的JobServer前,需要先确认旧版的代码是否可以构建新版的cube。如果可以,Staging和Prod的升级间隔就可以拉的很长,甚至可以在staging把Prod上全部cube构建一遍后再升级Prod。 否则,升级完Staging后就需要较快的升级Prod,因为没法切Cube,会对用户造成影响。 本次升级就属于后者,所以升级压力就会很大,必须快速解决暴露的所有问题。
  6. 应该考虑上线回滚方案,可以参考 来自 Google 的高可用架构理念与实践
  7. 我们向社区贡献代码时,应该保证合入社区的配置名称,配置默认值,元数据的属性和我们内部一致。

4 我们如何保证复杂系统的可靠性

这个问题可以近似等价于以下问题:

  • 我们如何确保我们的每次升级或者上线是一定没问题的?
  • 我们有没有可能写出没有bug的复杂系统?
  • 我们的测试到底需要测到什么程度?

首先,这个世界上没有完全没有bug的系统,也不存在100%可用的系统。我们的目标只能是提供可用性尽可能高的系统,比如3个9的可用性,4个9的可用性。关于系统可用性的概念可以参考来自 Google 的高可用架构理念与实践或者关于高可用的系统。

4.1 为什么复杂系统很难保证可靠性

我认为可能有以下几点:

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 现在的复杂系统一般都是分布式系统,而我们知道分布式系统天生就有许多难题:不可靠的网络,不可靠的时钟,进程无响应,单机挂掉等。

说了这么多,我其实一直挺好奇像神舟飞船这种完全不能出错的系统到底是怎么保障可靠性的?

4.2 航天系统是如何保证系统可靠性的

我谷歌了下,发现尽然有专门的大学专业:可靠性系统工程。 还买了两篇相关的论文读了下:《可靠性系统工程的理论与技术框架》,《航天器环境试验和航天产品的质量与可靠性保证》。结果发现这教授写的论文和我的本科论文一样水,没啥干货。最后在《握手太空的航天科技》书中谷歌到一点答案,其实发现和我们保证一个高可用的软件系统原理是一样的:

首先是大量,严密的测试确保飞船的一些组件,功能是正常的。 和软件系统一样。

其次是关键部件的备份,冗余,takeover,关键设备都是3份同时工作。 和软件系统一样。

最后是分析飞船可能出现的所有故障情况,并给出应急方案。 也和软件系统一样,我们既然不能保证不出case,那么我就尽可能保证出了case立即发现,立即处理,立即恢复。

可以发现,保证系统可靠性的原理和思路在任何领域都是一致的 除了以上几点,可靠性的系统当然需要可靠优秀的总体设计或者架构设计,也需要可靠的细节实现或者代码实现。

当然,还有个显然的问题就是,神舟飞船在地面怎么测试太空的场景?答案是模拟。 所以我们现在可以回答 我们的测试到底需要测到什么程度 这个问题。答案是,如果你能在线下造出和线上完全相同的环境,那么你就可以用线上真实的数据或者case进行测试,Kylin在升级时其实是完全可以做到的,只不过这种做法成本太高,所以我们就需要模拟。有人会问,网络隔离,磁盘挂掉,机器down掉,CPU持续飙高等情况可以模拟吗,答案是可以模拟的,请参考以下篇文章:

分布式系统测试那些事儿——理念

分布式系统测试那些事儿——错误注入

分布式系统测试那些事儿——信心的毁灭与重建

这3片文章对测试的讲解十分深入,值得大家一读,大家也可以反思自己系统的测试。

4.3 如何打造高可靠的软件系统

  • 可靠的系统设计
  • 可靠的理论基础
  • 可靠的代码实现
  • 可靠,充分,全面的测试
  • 充分的冗余,备份,多活,takeover等
  • 可靠的监控和运维系统
  • 可靠的故障恢复机制
  • 高效,自动化的工具

具体大家可以参考下面的参考资料,引用陈皓的话总结:高可靠的系统是一个系统化的工程,这不是一个人或者几个人可以做到的,取决于全公司的技术实力和工程素养。 比如可用性4个9以上的系统,小公司基本不太可能做的出来。

来自 Google 的高可用架构理念与实践

如何建设高可用系统

关于高可用的系统

TiDB 架构的演进和开发哲学

《designing-data-intensive-applications》 5星力荐,很赞的一本书。

5 感谢Kylin社区

Kylin 2.0为我们带来多Segment并发重导,TrieDictionaryForest字典,雪花模型,百分位函数,Steaming cubing,Spark cubing等实用功能和新特性,以及若干bug Fix 和性能提升。十分感谢Kylin社区,身为Kylin commiter,能够理解每位Kylin contributor的付出,因为很多时候,我们都是在自己的业余时间和假期向Kylin社区贡献。

6 给Kylin社区的建议

  1. 我们每个Kylin的contributor和commiter都应该考虑兼容性问题,具体包括元数据的兼容性,Coprocessor兼容性, API兼容性。比如像1.2.3 API兼容性中举的例子,只要我们有考虑到这一点,这个兼容性问题完全可以避免掉。
  2. 我们每个JIRA的Assignee都应该在JIRA中描述必要的信息。 Bug类型的issue应该描述bug产生的原因,Fix bug的思路。Improve类型的issue应该描述是如何改进的,如果有性能对比则更好。New Feature类型的issue应该描述清楚背景或动机,并简要描述实现思路。 Issue Fix后应该在JIRA中给出github commit的链接。现在Kylin的大多数JIRA描述信息太过简单,要想知道基本的实现思路,必须自己去读代码,而且具体的commit信息还得根据JIRA号去找。
  3. 建议用Github的PR代替patch。 好处是首先代码阅读更方便,代码Review更方便,这样commit中就不会有那么多code review的commit。其次是Github可以和很多自动化工具集成,目前kylin中commit中经常有Fix UT和FIX IT,如果可以让每个PR自动跑UT和IT,只有通过后才允许合入Master,就不会有这个问题。
  4. 每个Bug Fix后,可以加Test的话,尽量应该加上对应的test。比如KYLIN-1817在1.5.3 版本Fix后,我测试的1.5.4.1和2.0版本依旧都有这个bug。
  5. 建议Kylin参考下其他开源系统,实现自动化测试,甚至是错误注入。
  6. 建议Kylin丰富下测试集,现有的Sample cube许多性能问题都无法暴露,或者在新版本正式release前,找几个合作伙伴用生产级别的数据测试下。

7 Kylin 2.0升级总结。

本次升级基本符合目标。

一个意外是在所有环境升级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。