最近在做一些性能优化工作,回想起工作这些年来,参与过的三次集中性能优化,每次都得折腾少则一个月,多则半年。这些内容既是不同视角、不同思路的比较,也是挺有趣的工作经历。
Portal 的性能优化
这已经是大概五年前了,搞了接近半年的 Portal 性能优化,后来某些内容总结在这篇文章里面。既然是 Portal,性能优化上就有它的特点。比如说:
Portal 的性能优化需要从前端和后端两个角度去思考问题,先考虑客户端和服务端之间的交互模型,然后再在客户端和服务端单独考虑分而治之。这个其实和设计的思路是一样的,交互问题需要首先考虑,定义好交互的报文形式(比如某 JSON 的具体形式)以后,包括用户触发什么行为引发什么样的数据访问,这些需要首先明确,这样才能对大概的请求模型了然于心。最怕的是那些请求乱七八糟的 Portal 要做优化,因为业务复杂,然后接口还没有统一,有的地方返回页面片段,有的地方返回一个大页面,有的地方返回一堆脚本,有的地方用 JSONP,有的地方有返回纯数据格式。互相之间还有许多重复,这种乱七八糟的客户端和服务端之间的交互,简直就没有设计,不同人开发就不同样,做起优化来简直就是噩梦。
在思考 view 这一层的时候,首先要给它分区,如果是简单的页面,就要给它分类、分块。目的只有一个,抽象出动态变化的部分和静态渲染的部分。有的前端本身解耦做得比较好的,数据和模板已经拆分得很清楚的话,模板是静态的,数据就要分析,哪些是动态的,哪些是相对静态的,可容许的不一致时间有多长。动态静态的划分主要是为了 view 层面的缓存。对于 Portal 来说,缓存是非常讨巧的,页面组件划分做得好的,性能优化才有余地。
前端的性能优化大家都会记得这流传甚广的几十条 “rules”,而后端就没有这样的统一原则可以拿来指导了。前面讲了缓存,但是缓存的引入也会引发一系列的问题。最常见的是关于数据过期造成的不一致性问题,但是其实还有一些其他的问题。比如说,如果某些原因导致缓存全部失效(例如掉电、服务重启),这个时候压力一下子全部落到后端部件上去了(比如数据库上),那么实际的性能测试要保证这种情况下依然可以保证正常服务的提供,流控正常,网站不要挂掉。
Service 的性能优化
后来工作需要维护过一个几十台机器集群的 service,接受 get 请求,从数据库里面查数据以某种 xml 形式返回给用户。这种请求处理的并发数、TPS 和 latency 都非常关键,给数据库压力很大,而相对地,计算逻辑就比较简单。
当时想了几种优化的办法。
第一种是中心化的缓存,使用 Memcached,主要是考虑总的请求重复率可能在 50% 左右,但是如果在单台机器上做缓存,这个 cache hit 比率是非常低的,但是使用一台中心缓存服务器可以提高缓存命中率。但是另一方面,如果一旦引入中心缓存,又会带来很多新问题,比如这样的缓存读写开销就不能忽略了,对于缓存没有命中的 case,性能反而是下降的;比如一旦这个中心缓存服务器挂掉怎么办,一定要设置非常短的 cache 访问超时机制;还有,写缓存的操作,完全做成 NoReply 形式的,像发送 UDP 报文一样,只管请求,不管结果,从而尽量保证总的 latency。我把 Memcached 调优的一些总结放在了这里。
第二种思路是把计算部分放到客户端去,让 service 变得很薄,以期望减小 CPU 的使用。不过这一点上效果也一般,主要是因为瓶颈毕竟主要还是在数据库查询上面。基于这一点,后来还出现了一种思路,就是异步算好这些用户可能需要的结果,等到需要的时候直接来取就好。
最后,为了减小对关系数据库的压力,增加扩展性,把数据源挪到了 DynamoDB 这个 NoSQL 数据库上面。
Spark 的性能优化
近期则是做了一些 Spark 性能优化的工作。其实性能优化的核心问题是不变的,考虑的最主要的几个因素也是不变的,无非 CPU、内存、网络、锁(本质是并行度)等等。
系统地分析和改进 Spark 性能问题的时候,大概有这么几个几件事:
测试常规数据集在不同不同 instance type,不同 memory、不同 executor number 下面的性能表现,开始是单个 EMR 任务,后来则是整个 pipeline,评估性能条件下选择的理想性价比环境。比如 R3.8 的内存是要比 C3.8 多好几倍,但是价格是 1.5 倍,这个时候则需要评估如果合理设置 executor number,再看性能能达到什么程度,比如如果整个 pipeline 的计算时间能够缩短好几倍,那么 R3.8 也是不错的选择。当然,还有一种思路是 OOM 的风险(因此通常不会配置一大堆 executor,把 CPU 使用率榨取到穷尽),而 OOM 在测试中不是必现的,因此不能光看 pipeline 的时间。另外,在评估执行状态的时候,指标不要单独地看。比如有时看到 CPU 使用率上去了,这其实不一定是计算资源利用率提高的表现,还有可能是内存太小,大量本来安安稳稳在内存里面就可以算完的东西被迫溢出到磁盘上,读写一增加 CPU 使用率自然就上去了。
第二个是需要结合代码和测试的结果去修正代码具体代码写得不好的地方。例如对于 partition 的取值,default.parallelism 的计算公式,有一些 api 不合理的使用,还有明明可以并行化的地方,却没有做到等等。这里面有一些调优的 hints 我放在了这篇文章里面。总的来说,在最基本的问题修正以后,可以分析整个 pipeline 里面最慢的几个 task,做单独和额外的优化。
第三个则是需要测试异常大的数据量,主要是遇到一些特殊的情形,例如 Q4(第四季度)业务量暴涨的时候,这种情形要在测试中覆盖到。现在刚升级到 Spark 1.5 的版本,Spark 1.6 我还没有开始使用,但是我知道它是支持动态的 executor 数量的自动调整(dynamicAllocation.enabled)的,否则给 task 设置的 executor 数量还是需要根据 input 的大小代码里面计算调整的。
这些内容我的初衷是想写一些作为系统的或者补充的内容,从而和以前写的与这三次性能调优单独记录的文章想区别,但是目前思路还理得不很清楚。以后随着思考的深入再慢慢修正。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》