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