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或者解釋器帶來的性能未必是我們所擔心的,比如科學計算或者平常做一些數據分析或小應用時不會考慮到這個問題。 服務選型這是市面上常用的web框架針對Python的領域做服務選型分析的框架。無論使用什么web框架,在web服務中都會選擇多進程。一方面考慮到服務需要一定的可用性,需要多進程來保證減少服務可用性的影響。另外,多個進程意味著多個解釋器,多個解釋器意味著我們盡量減少GIL帶來的性能影響。 這是常見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等工具分析問題存在哪里,然后再找到合適的優化方向。 這是一個簡單的Cprofile例子,執行def1、def2、def3,去分析一下它的耗時情況。 上面的代碼中有多個函數的執行。可以看到,最后一次的運行耗時是237毫秒。當然,對于profile也可以輸出pstat格式的數據,大家能通過可視化清楚的看到自己函數耗時占比。 優化方法▌(一)原則 第一,優化時一定要靠數據說話。即使需要犧牲一次迭代去更新一下,也要把數據羅列出來,使之有理有據。我們優化的原則主要有四點:一是用數據說話,數據不只是優化的原因,也是優化的方向,把指標達到一定水準,目的才達到了;第二,不要過早優化或過度優化。否則有可能出現業務偏差;第三,深入理解業務。對產品更加負責;第四,選擇好的衡量標準,比如CPU利用率降到多少了。 ▌(二)IO密集型 如果是IO密集型的服務,使用多線程實際比單線程的性能提高很多。但是如果大量IO操作都比較耗時,它的性能未必像想象中那么好。這種情況下建議批量操作,或者改為協程,網絡帶寬性能會帶來很大的提升。此外,減少IO操作也是可行方案。 ▌(三)CPU密集型 多線程顯然已經不適用于CPU密集型的服務,因為頻繁的GIL爭搶會導致序性能大幅度下降。多進程其實很適合CPU密集型服務。對于CPU密集型的服務,為了減少解釋器的損耗 ,最好可以適用C的擴展庫來提高程序性能,能夠一定程度緩解類型轉換帶來的性能損耗 ,而且可以大幅度提高基礎庫的運行速度。 ▌(四)緩存 緩存一直是系統性能優化的利器,這對Python是架構性的東西,可能跟語言的相關性沒有那么大。但是Python的編程方法對緩存代碼改造是非常便利的。 這是緩存的例子,這個業務邏輯很簡單,在現有的生產模型里比較常用。 這是一個有緩存的函數,我們在性能調優時需要動態去允許開關函數不緩存,必須按照原來的方式執行一遍才能拿到結果。這里有一個計算緩存過程,mode是我們開發的模式,可以在函數動態的取mode,達到開關的值。我們可以通過這個開關去讓函數得到它執行的方式。 另外,我們在存儲序列化數據時最好使用高性能的庫,比如cPickle,cPickle雖然比pickle,但是沒有cJSON快。可以給存儲層、DB層、計算的函數層、應用層都加上緩存,但是在Python應用程序之外也有很多架設高速緩存的方法。 多層緩存雖然是一個架構緩存,但是Python開發做擴展性應用時,用戶體驗是非常好的,簡短的代碼開發就可以完成通用功能,而且里面的語言不用動。 ▌(五)懶加載 還有一些常用的方法,比如懶加載。這是常用的Lazy單例,調用一次之后就不再調用了,以后拿到的是初始化好的。 ▌(六)一些技巧 對于generator需要謹慎對待。 對于循環遍歷,比如遍歷10萬個數據,generator有可能更慢一些,這種東西是需要分場合的。如果在循環中不需要把所有列表生成出來,那么速度會稍微快一些。 這是一個命名空間問題。第一種狀況可能更簡單一些,但是它是147毫秒,第二種狀況是把循環函數里,快了1倍時間。這是因為Python在執行代碼時遇到了range。對于第一種,Python首先會在本地的變量里找這個range,如果沒有找到會去gloabl變量里找range。 對于第二種,range的查找不需要再走gloabl,它走的是load-const,這是一個很快的過程。有些由于空間導致的性能微小的差距,執行少量數據時看不出來,但是大量數據時是非常明顯的。 總結Python這種便利的特性給我們帶來很大的開發優勢: 數據分析是第一位的,要去優化自己的Python服務。 第二,需要合理的測試環境,不要因為性能調優而影響增加的服務穩定性或者出現故障。 第三,要有的放矢,我們有時面對更多服務拆分或微服務化,對架構說不定有更多好處。比如把IO密集型服務和CPU密集型服務分開做,在前端使用IO密集型的操作。將所有的請求都集中在對外的入口,這樣對外服務的性能會得到很大的提高,因為性能壓力都分散到各個微服務里了,而同樣的性能得到了最大的保障。大家可以多鉆研一下,掌握一些技巧。 謝謝大家。 |
|