分享

阿里巴巴杨群:高并发场景下Python的性能挑战

 车厘子V 2019-08-06

4月13日,在CSDN主办的“2019 Python开发者日”大会上,阿里云数据库专家杨群分享了《高并发场景下Python的性能挑战》的主题演讲。

以下为演讲整理,文章略有删减:

性能问题

▌(一)GIL

为什么大家都说Python慢?最主要的原因是全局解释器锁。今天讲的Python是官方的C版Python。CPython在创建变量时,首先对变量分配内存,然后开始计数变量的数量,大家提出称之为“引用计数”。在引用计数变为0时,从系统中释放变量的内存。如果多个线程同时对这个计数做操作,线程不安全,会导致很多问题。

综合垃圾回收机制问题,CPython引入了GIL,同一个时刻在一个进程允许一个线程使用解释器,意味着单进程下Python多线程的性能没有那么好。这样做的好处在于能够避免死锁和数据用户安全方面的问题。

Python有三种线程状态:Idle、Running、Failed GIL Acquire。曾经有人对GIL的性能影响做了两个测试。第一个测试案例是两个CPU密集线程,代码运行过程的大部分状态是Failed GIL Acquire,两个线程的运行没有达到双核的效果。

第二个案例是IO密集型的线程。仔细分析发现,IO没有达到想象的预期效果。所以IO密集型和CPU密集型同时存在时,IO密集型未必达到想要的运算速度,我们要区分好IO密集型和CPU密集型的服务。

▌(二)解释器

CPython要首先生成pcy字节码序列,之后才能被CPU理解,所以较慢。JAVA、.NET也有中间的翻译,但因为JAVA和.NET使用即时编辑(JIT),使用JIT可以检测哪些代码执行得比较多,意味着计算机应用程序需要重复做一件事情的时候它就会更快。

▌(三)动态语言

Python是动态语言类型,我们在做类型转化或者比较的时候比较耗时,因为读取、写入变量或者引用变量时会进行检查。静态类型语言没有这么高的灵活性,但它已经规定好了内存中的状态,所以很快。

Python这么慢,我们为什么还要用它?一是用Python优雅、简洁。二是大多数应用场景时,GIL或者解释器带来的性能未必是我们所担心的,比如科学计算或者平常做一些数据分析或小应用时不会考虑到这个问题。

服务选型

640?wx_fmt=png

             这是市面上常用的web框架针对Python的领域做服务选型分析的框架。无论使用什么web框架,在web服务中都会选择多进程。一方面考虑到服务需要一定的可用性,需要多进程来保证减少服务可用性的影响。另外,多个进程意味着多个解释器,多个解释器意味着我们尽量减少GIL带来的性能影响。

640?wx_fmt=png

这是常见web服务的方法,前端的LoadBalancer,大家可能会选择常见的Nginx、apache或者云服务的SLB。

异步IO框架的选择是大家都关心的一个问题。GIL如果是IO密集型,我们用异步能够做到很快。但是它有很适合的应用场景,比如不想做Nginxluv插件,作为高性能的扩展方案,那就用tornado来写,如果内部代码全是异步的IO操作,它是非常好的,可以组装自己的逻辑,比如积数之类的都可以放在tornado里来做,性能可以得到保障。

另外,PyPy是Python的Just in time 编译器,性能一般要比CPython解释器至少好3倍。但是它和JIT编译器一样有启动慢的特点,所以适合对重启不是很敏感的服务。它的问题是不支持C扩展的Python库。

性能瓶颈分析

在现实业务开发中,最主要的是依靠业务日志分析,考虑我们的业务链路中是否存在网络耗时。对一些任务日志可以用AWK或者unit等,去分析出来哪些接口访问量比较多、耗时严重的,使用Cprofile等工具分析问题存在哪里,然后再找到合适的优化方向。

640?wx_fmt=png

这是一个简单的Cprofile例子,执行def1、def2、def3,去分析一下它的耗时情况。

640?wx_fmt=png

上面的代码中有多个函数的执行。可以看到,最后一次的运行耗时是237毫秒。当然,对于profile也可以输出pstat格式的数据,大家能通过可视化清楚的看到自己函数耗时占比。

优化方法

▌(一)原则

第一,优化时一定要靠数据说话。即使需要牺牲一次迭代去更新一下,也要把数据罗列出来,使之有理有据。我们优化的原则主要有四点:一是用数据说话,数据不只是优化的原因,也是优化的方向,把指标达到一定水准,目的才达到了;第二,不要过早优化或过度优化。否则有可能出现业务偏差;第三,深入理解业务。对产品更加负责;第四,选择好的衡量标准,比如CPU利用率降到多少了。

▌(二)IO密集型

如果是IO密集型的服务,使用多线程实际比单线程的性能提高很多。但是如果大量IO操作都比较耗时,它的性能未必像想象中那么好。这种情况下建议批量操作,或者改为协程,网络带宽性能会带来很大的提升。此外,减少IO操作也是可行方案。

▌(三)CPU密集型

多线程显然已经不适用于CPU密集型的服务,因为频繁的GIL争抢会导致序性能大幅度下降。多进程其实很适合CPU密集型服务。对于CPU密集型的服务,为了减少解释器的损耗 ,最好可以适用C的扩展库来提高程序性能,能够一定程度缓解类型转换带来的性能损耗 ,而且可以大幅度提高基础库的运行速度。

▌(四)缓存

缓存一直是系统性能优化的利器,这对Python是架构性的东西,可能跟语言的相关性没有那么大。但是Python的编程方法对缓存代码改造是非常便利的。

640?wx_fmt=png

这是缓存的例子,这个业务逻辑很简单,在现有的生产模型里比较常用。

640?wx_fmt=png

这是一个有缓存的函数,我们在性能调优时需要动态去允许开关函数不缓存,必须按照原来的方式执行一遍才能拿到结果。这里有一个计算缓存过程,mode是我们开发的模式,可以在函数动态的取mode,达到开关的值。我们可以通过这个开关去让函数得到它执行的方式。

另外,我们在存储序列化数据时最好使用高性能的库,比如cPickle,cPickle虽然比pickle,但是没有cJSON快。可以给存储层、DB层、计算的函数层、应用层都加上缓存,但是在Python应用程序之外也有很多架设高速缓存的方法。

多层缓存虽然是一个架构缓存,但是Python开发做扩展性应用时,用户体验是非常好的,简短的代码开发就可以完成通用功能,而且里面的语言不用动。

▌(五)懒加载

640?wx_fmt=png

       还有一些常用的方法,比如懒加载。这是常用的Lazy单例,调用一次之后就不再调用了,以后拿到的是初始化好的。

▌(六)一些技巧

640?wx_fmt=png

对于generator需要谨慎对待。 对于循环遍历,比如遍历10万个数据,generator有可能更慢一些,这种东西是需要分场合的。如果在循环中不需要把所有列表生成出来,那么速度会稍微快一些。

640?wx_fmt=png

这是一个命名空间问题。第一种状况可能更简单一些,但是它是147毫秒,第二种状况是把循环函数里,快了1倍时间。这是因为Python在执行代码时遇到了range。对于第一种,Python首先会在本地的变量里找这个range,如果没有找到会去gloabl变量里找range。

对于第二种,range的查找不需要再走gloabl,它走的是load-const,这是一个很快的过程。有些由于空间导致的性能微小的差距,执行少量数据时看不出来,但是大量数据时是非常明显的。

总结

Python这种便利的特性给我们带来很大的开发优势:

数据分析是第一位的,要去优化自己的Python服务。

第二,需要合理的测试环境,不要因为性能调优而影响增加的服务稳定性或者出现故障。

第三,要有的放矢,我们有时面对更多服务拆分或微服务化,对架构说不定有更多好处。比如把IO密集型服务和CPU密集型服务分开做,在前端使用IO密集型的操作。将所有的请求都集中在对外的入口,这样对外服务的性能会得到很大的提高,因为性能压力都分散到各个微服务里了,而同样的性能得到了最大的保障。大家可以多钻研一下,掌握一些技巧。

谢谢大家。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多