4月13日,在CSDN主辦的“2019 Python開(kāi)發(fā)者日”大會(huì)上,阿里云數(shù)據(jù)庫(kù)專家楊群分享了《高并發(fā)場(chǎng)景下Python的性能挑戰(zhàn)》的主題演講。 以下為演講整理,文章略有刪減: 性能問(wèn)題▌(一)GIL 為什么大家都說(shuō)Python慢?最主要的原因是全局解釋器鎖。今天講的Python是官方的C版Python。CPython在創(chuàng)建變量時(shí),首先對(duì)變量分配內(nèi)存,然后開(kāi)始計(jì)數(shù)變量的數(shù)量,大家提出稱之為“引用計(jì)數(shù)”。在引用計(jì)數(shù)變?yōu)?時(shí),從系統(tǒng)中釋放變量的內(nèi)存。如果多個(gè)線程同時(shí)對(duì)這個(gè)計(jì)數(shù)做操作,線程不安全,會(huì)導(dǎo)致很多問(wèn)題。 綜合垃圾回收機(jī)制問(wèn)題,CPython引入了GIL,同一個(gè)時(shí)刻在一個(gè)進(jìn)程允許一個(gè)線程使用解釋器,意味著單進(jìn)程下Python多線程的性能沒(méi)有那么好。這樣做的好處在于能夠避免死鎖和數(shù)據(jù)用戶安全方面的問(wèn)題。 Python有三種線程狀態(tài):Idle、Running、Failed GIL Acquire。曾經(jīng)有人對(duì)GIL的性能影響做了兩個(gè)測(cè)試。第一個(gè)測(cè)試案例是兩個(gè)CPU密集線程,代碼運(yùn)行過(guò)程的大部分狀態(tài)是Failed GIL Acquire,兩個(gè)線程的運(yùn)行沒(méi)有達(dá)到雙核的效果。 第二個(gè)案例是IO密集型的線程。仔細(xì)分析發(fā)現(xiàn),IO沒(méi)有達(dá)到想象的預(yù)期效果。所以IO密集型和CPU密集型同時(shí)存在時(shí),IO密集型未必達(dá)到想要的運(yùn)算速度,我們要區(qū)分好IO密集型和CPU密集型的服務(wù)。 ▌(二)解釋器 CPython要首先生成pcy字節(jié)碼序列,之后才能被CPU理解,所以較慢。JAVA、.NET也有中間的翻譯,但因?yàn)镴AVA和.NET使用即時(shí)編輯(JIT),使用JIT可以檢測(cè)哪些代碼執(zhí)行得比較多,意味著計(jì)算機(jī)應(yīng)用程序需要重復(fù)做一件事情的時(shí)候它就會(huì)更快。 ▌(三)動(dòng)態(tài)語(yǔ)言 Python是動(dòng)態(tài)語(yǔ)言類型,我們?cè)谧鲱愋娃D(zhuǎn)化或者比較的時(shí)候比較耗時(shí),因?yàn)樽x取、寫入變量或者引用變量時(shí)會(huì)進(jìn)行檢查。靜態(tài)類型語(yǔ)言沒(méi)有這么高的靈活性,但它已經(jīng)規(guī)定好了內(nèi)存中的狀態(tài),所以很快。 Python這么慢,我們?yōu)槭裁催€要用它?一是用Python優(yōu)雅、簡(jiǎn)潔。二是大多數(shù)應(yīng)用場(chǎng)景時(shí),GIL或者解釋器帶來(lái)的性能未必是我們所擔(dān)心的,比如科學(xué)計(jì)算或者平常做一些數(shù)據(jù)分析或小應(yīng)用時(shí)不會(huì)考慮到這個(gè)問(wèn)題。 服務(wù)選型這是市面上常用的web框架針對(duì)Python的領(lǐng)域做服務(wù)選型分析的框架。無(wú)論使用什么web框架,在web服務(wù)中都會(huì)選擇多進(jìn)程。一方面考慮到服務(wù)需要一定的可用性,需要多進(jìn)程來(lái)保證減少服務(wù)可用性的影響。另外,多個(gè)進(jìn)程意味著多個(gè)解釋器,多個(gè)解釋器意味著我們盡量減少GIL帶來(lái)的性能影響。 這是常見(jiàn)web服務(wù)的方法,前端的LoadBalancer,大家可能會(huì)選擇常見(jiàn)的Nginx、apache或者云服務(wù)的SLB。 異步IO框架的選擇是大家都關(guān)心的一個(gè)問(wèn)題。GIL如果是IO密集型,我們用異步能夠做到很快。但是它有很適合的應(yīng)用場(chǎng)景,比如不想做Nginxluv插件,作為高性能的擴(kuò)展方案,那就用tornado來(lái)寫,如果內(nèi)部代碼全是異步的IO操作,它是非常好的,可以組裝自己的邏輯,比如積數(shù)之類的都可以放在tornado里來(lái)做,性能可以得到保障。 另外,PyPy是Python的Just in time 編譯器,性能一般要比CPython解釋器至少好3倍。但是它和JIT編譯器一樣有啟動(dòng)慢的特點(diǎn),所以適合對(duì)重啟不是很敏感的服務(wù)。它的問(wèn)題是不支持C擴(kuò)展的Python庫(kù)。 性能瓶頸分析在現(xiàn)實(shí)業(yè)務(wù)開(kāi)發(fā)中,最主要的是依靠業(yè)務(wù)日志分析,考慮我們的業(yè)務(wù)鏈路中是否存在網(wǎng)絡(luò)耗時(shí)。對(duì)一些任務(wù)日志可以用AWK或者unit等,去分析出來(lái)哪些接口訪問(wèn)量比較多、耗時(shí)嚴(yán)重的,使用Cprofile等工具分析問(wèn)題存在哪里,然后再找到合適的優(yōu)化方向。 這是一個(gè)簡(jiǎn)單的Cprofile例子,執(zhí)行def1、def2、def3,去分析一下它的耗時(shí)情況。 上面的代碼中有多個(gè)函數(shù)的執(zhí)行??梢钥吹?,最后一次的運(yùn)行耗時(shí)是237毫秒。當(dāng)然,對(duì)于profile也可以輸出pstat格式的數(shù)據(jù),大家能通過(guò)可視化清楚的看到自己函數(shù)耗時(shí)占比。 優(yōu)化方法▌(一)原則 第一,優(yōu)化時(shí)一定要靠數(shù)據(jù)說(shuō)話。即使需要犧牲一次迭代去更新一下,也要把數(shù)據(jù)羅列出來(lái),使之有理有據(jù)。我們優(yōu)化的原則主要有四點(diǎn):一是用數(shù)據(jù)說(shuō)話,數(shù)據(jù)不只是優(yōu)化的原因,也是優(yōu)化的方向,把指標(biāo)達(dá)到一定水準(zhǔn),目的才達(dá)到了;第二,不要過(guò)早優(yōu)化或過(guò)度優(yōu)化。否則有可能出現(xiàn)業(yè)務(wù)偏差;第三,深入理解業(yè)務(wù)。對(duì)產(chǎn)品更加負(fù)責(zé);第四,選擇好的衡量標(biāo)準(zhǔn),比如CPU利用率降到多少了。 ▌(二)IO密集型 如果是IO密集型的服務(wù),使用多線程實(shí)際比單線程的性能提高很多。但是如果大量IO操作都比較耗時(shí),它的性能未必像想象中那么好。這種情況下建議批量操作,或者改為協(xié)程,網(wǎng)絡(luò)帶寬性能會(huì)帶來(lái)很大的提升。此外,減少IO操作也是可行方案。 ▌(三)CPU密集型 多線程顯然已經(jīng)不適用于CPU密集型的服務(wù),因?yàn)轭l繁的GIL爭(zhēng)搶會(huì)導(dǎo)致序性能大幅度下降。多進(jìn)程其實(shí)很適合CPU密集型服務(wù)。對(duì)于CPU密集型的服務(wù),為了減少解釋器的損耗 ,最好可以適用C的擴(kuò)展庫(kù)來(lái)提高程序性能,能夠一定程度緩解類型轉(zhuǎn)換帶來(lái)的性能損耗 ,而且可以大幅度提高基礎(chǔ)庫(kù)的運(yùn)行速度。 ▌(四)緩存 緩存一直是系統(tǒng)性能優(yōu)化的利器,這對(duì)Python是架構(gòu)性的東西,可能跟語(yǔ)言的相關(guān)性沒(méi)有那么大。但是Python的編程方法對(duì)緩存代碼改造是非常便利的。 這是緩存的例子,這個(gè)業(yè)務(wù)邏輯很簡(jiǎn)單,在現(xiàn)有的生產(chǎn)模型里比較常用。 這是一個(gè)有緩存的函數(shù),我們?cè)谛阅苷{(diào)優(yōu)時(shí)需要?jiǎng)討B(tài)去允許開(kāi)關(guān)函數(shù)不緩存,必須按照原來(lái)的方式執(zhí)行一遍才能拿到結(jié)果。這里有一個(gè)計(jì)算緩存過(guò)程,mode是我們開(kāi)發(fā)的模式,可以在函數(shù)動(dòng)態(tài)的取mode,達(dá)到開(kāi)關(guān)的值。我們可以通過(guò)這個(gè)開(kāi)關(guān)去讓函數(shù)得到它執(zhí)行的方式。 另外,我們?cè)诖鎯?chǔ)序列化數(shù)據(jù)時(shí)最好使用高性能的庫(kù),比如cPickle,cPickle雖然比pickle,但是沒(méi)有cJSON快??梢越o存儲(chǔ)層、DB層、計(jì)算的函數(shù)層、應(yīng)用層都加上緩存,但是在Python應(yīng)用程序之外也有很多架設(shè)高速緩存的方法。 多層緩存雖然是一個(gè)架構(gòu)緩存,但是Python開(kāi)發(fā)做擴(kuò)展性應(yīng)用時(shí),用戶體驗(yàn)是非常好的,簡(jiǎn)短的代碼開(kāi)發(fā)就可以完成通用功能,而且里面的語(yǔ)言不用動(dòng)。 ▌(五)懶加載 還有一些常用的方法,比如懶加載。這是常用的Lazy單例,調(diào)用一次之后就不再調(diào)用了,以后拿到的是初始化好的。 ▌(六)一些技巧 對(duì)于generator需要謹(jǐn)慎對(duì)待。 對(duì)于循環(huán)遍歷,比如遍歷10萬(wàn)個(gè)數(shù)據(jù),generator有可能更慢一些,這種東西是需要分場(chǎng)合的。如果在循環(huán)中不需要把所有列表生成出來(lái),那么速度會(huì)稍微快一些。 這是一個(gè)命名空間問(wèn)題。第一種狀況可能更簡(jiǎn)單一些,但是它是147毫秒,第二種狀況是把循環(huán)函數(shù)里,快了1倍時(shí)間。這是因?yàn)镻ython在執(zhí)行代碼時(shí)遇到了range。對(duì)于第一種,Python首先會(huì)在本地的變量里找這個(gè)range,如果沒(méi)有找到會(huì)去gloabl變量里找range。 對(duì)于第二種,range的查找不需要再走gloabl,它走的是load-const,這是一個(gè)很快的過(guò)程。有些由于空間導(dǎo)致的性能微小的差距,執(zhí)行少量數(shù)據(jù)時(shí)看不出來(lái),但是大量數(shù)據(jù)時(shí)是非常明顯的。 總結(jié)Python這種便利的特性給我們帶來(lái)很大的開(kāi)發(fā)優(yōu)勢(shì): 數(shù)據(jù)分析是第一位的,要去優(yōu)化自己的Python服務(wù)。 第二,需要合理的測(cè)試環(huán)境,不要因?yàn)樾阅苷{(diào)優(yōu)而影響增加的服務(wù)穩(wěn)定性或者出現(xiàn)故障。 第三,要有的放矢,我們有時(shí)面對(duì)更多服務(wù)拆分或微服務(wù)化,對(duì)架構(gòu)說(shuō)不定有更多好處。比如把IO密集型服務(wù)和CPU密集型服務(wù)分開(kāi)做,在前端使用IO密集型的操作。將所有的請(qǐng)求都集中在對(duì)外的入口,這樣對(duì)外服務(wù)的性能會(huì)得到很大的提高,因?yàn)樾阅軌毫Χ挤稚⒌礁鱾€(gè)微服務(wù)里了,而同樣的性能得到了最大的保障。大家可以多鉆研一下,掌握一些技巧。 謝謝大家。 |
|