最近处理的性能优化总结思考

按照常理,性能优化应该是属于比较高级,处于项目中后期的工作了,但是如果实现不给力,在项目初期就可以遇到了。

很多人都嫌弃Python慢,个人认为他们之中90%都没有资格这么说,一方面,需要高性能的地方并不是每个项目都需要,另一方面,他们自己写的代码烂的要死,才是罪魁祸首。

Python的代码可读性非常好,利于开发和维护,是对开发者友好的语言。但如果代码写成一团糟,没有扬长避短,导致维护困难,开发新功能无处下手,性能又遇到瓶颈,这个时候又怪罪起Python或是框架来,可以说是愚蠢至极。

我为什么要提上面这段呢?因为我在工作中不断地印证了之前听闻的一个说法。那就是,当一个项目重构后,代码量缩减,性能提升n倍,往往会被人们归功于使用了新语言或是新框架。但这样的理解是不对的,就算是使用同一种语言,同一种框架,在重构时,由于已经有了之前的积累,这些积累包括需求的深刻认识,弊端和bug的提前了解,以此为鉴,才能够在之后的重构时,搭建起较好的架构实现。
如若不然,该挖的坑还会再挖一遍,该跳的坑还得再跳一次。不从以往吸取教训,总结经验,那么永远都不会有出头之日。

最近我就做了一些性能优化工作,也正好是同一语言同一框架的优化。有些东西,虽然我们听说了,记住了,但是如果没有经历过的话,总感觉会缺少些东西。在这次的优化工作中,一些东西不断地从实际当中总结出来,又不断地和以往的知识相互验证,让我感觉受益匪浅,有点融会贯通的感觉,包括上文和下文提到的东西,我都是深有感悟啊!这个时候一定要记录下来,因为感性的认识,和最终能够形成文字表达出来的认识也有不同,后者明显对知识的掌握更深。类比做题容易,但是如果要给别人讲清楚如何做题,那对人的要求就要更高了。
综上啊,这就是经验啊,只有见的多了,才算是身经百战。。。

下面谈谈详细情况。

有些东西是可以立即采用的,比如异步、cache。

异步的作用无疑是非常大的。首页加载慢?运营人员抱怨后台大批量操作时页面卡住?那这个时候就要考虑处理流程里面都必须要用户等在那里吗,用户需要立刻就看到结果吗?在书上,我们经常看到发邮件这种操作会被作者举例,说是非常适合异步。同样,进首页的时候判断用户参与了哪些活动,是否需要发红包,运营审批一些后台数据,这些操作都可以异步实现,对结果没影响,还显著提升页面加载和后台审批速度。
并且,异步处理可以使用多个worker,进一步减少处理时间。
目前我们公司异步处理很复杂,大类可分为Redis队列和Celery处理,其中内部还有细分,我认为这部分能够重构一下,抛弃Redis,统一使用Celery。

cache,译作缓存,也译作快取,实际上这是它的一体两面。什么东西很长时间不会变,什么东西不需要立即对数据库进行写入,这些东西统统可以使用cache。
cache不单单指使用NoSql数据库。其实在代码里面就可以实现某些东西,比如有一个值是从数据库中读取的,但是基本不会改变,那这个时候可以使用@cache_property来将这个值作为类的属性缓存起来。这样的话,当程序启动后(有些场景是第一次访问请求时,比如使用odoo会在第一个访问请求到来时建立url_mapping,会将对应的endipoint存储,endpoint中即是处理对应url的类的实例),就会缓存到内存中(和NoSql同样是内存!),缺点是万一要是值有改变的话,得重启才行。
还有的,当然就是使用NoSql数据库了,我们这边用的是Redis。最近我看了一些《Redis应用实践》,发现其中对Redis的使用非常主动:并不是所有数据都要存放到关系型数据库中,NoSql只是一种辅助,而是将非常多的数据,比如说是涉及到经常改变的数据,直接放到Redis中进行处理,而永久化数据并没有提及。非常遗憾的是,我们公司目前还没有这种做法或是想法。
我们公司已经实现了两套缓存机制,但由于odoo的ORM写入机制,保存到Redis中的数据还没怎么使用呢,就经常被重写,结果导致了非常严重的死锁,所以都废弃掉了。
目前的话我们只是在代码里零星地使用Redis进行缓存,可以说不怎么正常。做优化时,我的代码实现还非常土,同样的语句写了好几遍,被CTO和一位架构师同时吐槽,但目前也没有时间去优雅处理。。。
以前一直没感觉NoSql有多神,直到最近我才深深地理解了什么叫做“保存在内存中”的含义——硬盘就算是SSD,关系型数据库也还是慢,对放在内存里的数据实在是太快了。

以上这两点的话,只要能够理清楚实现逻辑就可以动手做改变了,但接下来的一些优化需要有能够定量的指标才行。

关于Python代码的执行时间,我推荐使用line_profiler,它可以按行标注执行时间,比较好用。其他的很多都是按函数来标注,不太符合我们的需求。
按照line_profiler提供的每行执行时间,以及每行调用次数,可以很轻松的找出瓶颈。无奈的是我司代码瓶颈太多了,我一般是抓大放小,先从最严重的开始。

一些比较好找的点是,ORM的性能,比如odoo中filtered语句就比search语句执行的慢,而search语句中,若使用a.b.c这种格式,那么ORM在翻译search语句至sql语句时,就会变成join语句,这样执行的也慢。再比如,有些功能有更好的实现,比如说是优秀的第三方库,那么对原先的代码进行替换即可。

剩下的都是需要深入场景,深入代码逻辑中去判断是否能够优化。一般而言,如果一行代码执行时间过长,那么就要进行思考,大体上,首先要想想是否需要这么计算,再考虑怎么优化。如果一行代码执行次数过多,那就要考虑是否有多次重复调用了。我在这次优化中,发现有一处调用了300多次,按照场景只需要60次即可,后来发现在代码逻辑中有一处是从上往下进行循环,在循环中调用了一个方法,这个方法又进行从下往上的递归,结果就往返执行了300多次。优化好之后,发现又有一个地方执行了3000多次,我真是百思不得其解啊,后来反复查找,才发现是有一个模型的方法中有一个自动触发的方法,当有些属性改变时就会进行调用。真可谓是一步一坑啊!

说了这么多,其实我们也没有涉及到很高级的技术,都是一些基础。但是,从性能优化的工作中,也能够看出一些问题,也就是所谓的根因:一是Model设计不合理,Model设计的好不好没有标准,是有一些比较教条的规则,但更应该看的是Model的设计是不是非常符合实际的应用场景,我司的设计估计刚开始时还是符合的,但目前已经非常不匹配了。
二是代码质量不过关,我在上文所举的那个例子,就是很好的证明,对于这点我只能说要看个人的自觉了,也没有想到或是听说过有很好的方法,code review这种东西在我司执行效果不佳。
三是极端测试不充分,否则也不会等到线上流量暴增之后,才发现这么多性能问题。
四是性能未量化,当初在写代码时,并没有考虑到哪些地方对性能有要求,只是把功能简单的实现而已。这就好比总分是100分,考了60分已经及格,但是总分突然提升到1000分时,60分就是个垃圾。虽然过早的优化是不对的,但是如果总是低要求的话,那就是挖坑不止,问题永远都解决不完。

上面提到的都是通过技术解决。下面谈谈解决问题的思路,当然是个人见解。实际上遇到问题的时候,我们首要的考虑是,出现问题的场景是真需求还是伪需求,也就是说,问题并不需要都通过技术手段来解决,而且技术手段也不应该是首选。这个过程应该是一步步来,不断地对各种取巧方法进行否定,直到无法可解,才去选择技术来解决。
而当选取技术时,也并不是说直接硬上了,像是采用异步、cache这种讨巧的技术来解决问题其实是非常漂亮的。这次有个需要优化的地方执行了20s左右,原因是会对4000+个浮点数进行加运算,在实际当中,它是一个伪需求,我们通过一个tag来规避它,而通过一个定时任务来执行计算,而计算本身我也只是引入numpy的sum来替换python的内置sum。如果它真的是一个真需求的话,我才可能会去考虑多进程/线程,Map-Reduce,C代码等等其他技术。
这样可能不太符合一个技术人员的定位,面对问题应该正面强上的啊,这样才能证明自己啊……但我恰恰喜欢这种比较“取巧”,“暴力”的方法,而且认为这样才足够优雅。

最后说点其他的,关于死锁。之前我是认为死锁这种高大上的东西得非常大的数据才会出现,没想到啊没想到。归根结底,一部分归咎于ORM的实现,一部分则是代码实现太烂。如果执行够快的话,行级锁是很难触发死锁的。但是按照我司一个事务能执行30min+的尿性,不出死锁才怪。目前我们把一些长事务给分拆,变成一个个小事务对数据库进行commit,来规避死锁的触发。

总之,写代码之前好好理清楚需求,好好设计架构,写代码的时候要对人友好,要写得优雅,写完后要进行测试,要对各种可能出现的情况提前做好判断,是否对性能有要求也要提前想好,并对实现进行验证。

软件工程,软件工程,做工程一定要考虑成本,做工程一定有前有后有各种要求标准,软件工程永远都不是代码罗列。

    原文作者:顾慎为
    原文地址: https://www.jianshu.com/p/5174c555a08a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞