關於進程、線程、協程在python中的使用問題

描述

最近在python中開發一個人工智能調度平臺,因爲計算側使用python+tensorflow,調度側爲了語言的異構安全性,也選擇了python,就涉及到了一個調度併發性能問題,因爲業務需要,需要能達到1000+個qps的業務量需求,對python調度服務的性能有很大挑戰。
具體的架構如下面所示:
關於進程、線程、協程在python中的使用問題

補充:
架構中使用的python爲cpython,解釋執行的語言,並非jpython或者pypython,cpython的社區環境比較活躍,很多開發包都是現在cpython下實現的,比如項目中計算模塊用到的tensorflow,numpy等等。
下文討論的均爲cpython語言。

問題

目前數據計算服務每個服務負責一類數據的解析,暫時還沒有問題,並且docker計算可以調度到其他機器上,暫時不構成性能瓶頸
在調度服務器rpc客戶端上需要每秒需要完成1000+次業務,一次業務包括rpc調度一次原始數據,再rpc調度給計算服務進行計算拿返回結果再異步入庫,此時在使用python調度rpc io的時候使用不同的方法會有不同的性能表現。

如下幾種方案進行項目執行和改造

順序執行

將1000多業務順序執行,假如python程序只有這麼一個進程,並且機器上其他進程不會跟他搶佔cpu資源(任何語言一個進程在不開線程的情況下最多隻能同時使用一個cpu核心,python語言一個進程就算開了線程,也只能最多同時用一個cpu核心,或者用0個,處於阻塞狀態),所有業務的代碼塊均順序循環執行,內行代碼只能用cpu一個核心依此進行順序執行,顯然不可取,任何io需要等待的地方cpu就都在那裏等待執行,執行完一次任務再循環執行下一次,性能低下,每次業務都是串行的。

多線程執行

with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WOKERS) as executor:
        for (classify, sensorLists) in classifySensors.items():
            print(f'\ncurrent classify: {classify},  current sensors list: {sensorLists}')
            try:
                executor.submit(worker,classify,sensorLists)
            except Exception as e:
                print(e)

首先簡單說一下操作系統cpu內核是如何對多線程/多進程進行調度的,多線程/多進程的作業調度都是在操作系統內核層進行調度的,操作系統根據線程/進程的優先級或者是時間片是否用完或者是否阻塞來判斷是否要將cpu時間片切換到其他進程/線程,某些進程/線程還可以根據鎖機制進行搶佔cpu資源等等,如果cpu比較空閒,那麼當前線程/進程或可以一直佔用着一個cpu核心,再討論一下當前python進程中的線程是如何被系統調度到cpu運行的,如果當前機器有多個核心,並且沒有其他任務佔用,當前調度服務進程通過多線程/線程池調度一個多線程的業務作業,由於python解釋型語言,在當前這個python進程中的各個線程執行方式如下:
1.獲取GIL鎖
2.獲取cpu時間片
3.執行代碼,等待阻塞(sleep或io阻塞或耗時操作)或其他線程搶佔了cpu時間片
4.釋放GIL鎖,切換到其他線程執行,重複從步驟1開始。
可見,python中某個線程想要執行,必須先拿到GIL鎖,在一個python進程中只有一個GIL鎖。
所以python多線程情況下因爲其解釋下語言的特徵,多了一個GIL鎖,一個進程的多線程之間也只能同時最多佔用一個cpu資源,但是一個線程io等待的時候可以切換到另一個線程進行運行,不用串行等前一個線程io完成之後再進行下一個線程,所以此時多線程就可以有併發的效果,但是不能同時佔用多個cpu核心,線程不能並行執行,只能在一個等待的時候另一個併發執行,達到一個併發效果,qps比串行執行情況要好。

協程執行

對於 進程、線程,都是有內核進行調度,有 CPU 時間片的概念,進行 搶佔式調度(有多種調度算法),對於 協程(用戶級線程),這是對內核透明的,也就是系統並不知道有協程的存在,是完全由用戶自己的程序進行調度的,因爲是由用戶程序自己控制,那麼就很難像搶佔式調度那樣做到強制的 CPU 控制權切換到其他進程/線程,通常只能進行協作式調度,需要協程自己主動把控制權轉讓出去之後,其他協程才能被執行到。
python中使用aysnc定義協程方法,方法中耗時操作定義await 關鍵字(這個由用戶自己定義),當執行到await的時候,就可以切換到其他協程去運行,這樣在這個python進程中的這個cpu核心中該協程方法如果需要多次調用,就會通過用戶自定義的aysnc+await攜程方法在需要的時候將cpu時間片讓給其他協程,同樣達到了併發的效果,由於協程的切換開銷比較小,並且不涉及系統內核維護維護線程,保存現場等操作,所以在python中協程用的好的話,併發效果會比多線程好。可以參考tornado的底層協程實現gen.coroutine裝飾器的實現源碼來理解協程的底層實現:
參考資料:
https://note.youdao.com/ynoteshare1/index.html?id=e78ce975a872b2bdd37c7bae116790b8&type=note

python的協程底層是靠迭代器和生成器的next來切換協程的,事件是靠操作系統提供的select pool實現的。不過python的協程實現還是只能在當前進程或當前線程中切換,同一時間只能用一個cpu核心,並不能實現並行,只能實現併發,用的好的話併發效果比python的多線程好。

多進程執行

首先操作前面講的操作系統對線程/進程的調度方法,python中某個線程想要執行,必須先拿到GIL鎖,在一個python進程中只有一個GIL鎖。那麼在解釋執行語言python中,可以進行多進程的模式設計,進程運行的時候能單獨申請一部分內存空間和獨立的cpu核心,進程空間有一定的獨立性,所以每個進程單獨擁有獨立的GIL鎖,python多進程就可以實現並行的效果,在qps的運行效果上比協程和多線程效果好多了。

多進程+協程

多進程能充分利用cpu核心數,協程又能挖掘單個核心的使用率,多進程+協程方式調試得到或許會有不一樣的效果。一般在io密集型的應用中用協程方式,在計算密集型的場景中用多進程方式,在IO密集和計算密集型的場景中用多進程+協程方式。在我的該項目中,每一次任務需要兩次io,一次計算,雖然計算已經在單獨的docker中的python進程中,但是對於每一次任務中計算的時間還是串行在一起的,也會影響整體的qps的效果,所以此時用多進程+協程效果最好,一類IOT設備的業務放到獨立進程,多類IOT設備的業務就可以並行執行,該類iot設備中的多個設備可以通過協程併發,整體的qps效果就會比價不錯。

go大法goroutine

本質上,goroutine 就是協程。 不同的是,Golang 在 runtime、系統調用等多方面對 goroutine 調度進行了封裝和處理,當遇到長時間執行或者進行系統調用時,會主動把當前 goroutine 的CPU (P) 轉讓出去,讓其他 goroutine 能被調度並執行,也就是 Golang 從語言層面支持了協程。Golang 的一大特色就是從語言層面原生支持協程,在函數或者方法前面加 go關鍵字就可創建一個協程。
go的寫成定義更復雜,比python協程多的是能讓協程在操作系統的不同線程中調度。
所以如果要追求性能,將調度服務器用golang重構。
go參考資料:
《golang學習筆記二》電子書
Golang GMP調度模型 https://blog.csdn.net/qq_37858332/article/details/100689667
Golang 之協程詳解 https://blog.csdn.net/weixin_30416497/article/details/96665770

分佈式改造

如上的各種方法都是從調度服務器單節點的併發能力上進行改造,但是如果無論如何改造並且提高單臺服務器資源性能的情況下都不能滿足性能要求的話,那就必須進行分佈式改造,docker計算節點的分佈式改造比較簡單,直接上k8s進行調度到同的機器上,難點在於調度服務器上,調度服務器類似於客戶端進行rpc io的請求和分發,目前模式是:調度服務client 從client的內存數結構裏面拿服務節點信息->直連節點進行服務請求,要進行分佈式改造的話,就必須進行業務拆分,拆分成多個調度服務client進行負載承擔rpc io的請求和分發,要麼按照業務維度進行拆分,將部分類別的iot設備通過一個調度服務client,其他部分類別的iot設備通過其他調度服務client進行調度;要麼將調度服務client 精簡出來做成無狀態的的請求分發器,可以部署多個,只要在網關處先定義好路由分發規則,將rpc io請求到正確的服務端。這兩種區別是:
一種是: 多個調度服務client(業務分發路由邏輯在本client裏面)--> 多個計算服務
另一種: 多個無狀態的請求分發器(無狀態,無任何業務分發路由邏輯) --> 業務路由網關 -->多個計算服務
第一種可能調度服務client加載的配置不一樣,也即是加載的業務分發路由配置邏輯不一樣,第二種是無狀態請求分發器是一樣的,但是需要維護一個路由網關(取決於用的什麼rpc,像grpc就有官方的網關,以及Nginx配套的網關可選,可以動態向網關增加路由配置,然後自動重寫網關配置,reload)
如上即可完成分佈式的改造,改造成多個調度服務client或者多個無狀態的請求分發器+一個業務路由網關,當然兩種模式均需要一個zk或etcd進行服務註冊和服務發現,多個client或分發器就直接從zk或etcd裏拿對應計算服務器資源信息,具體改造模式如下:
第一種:
關於進程、線程、協程在python中的使用問題

第二種:

關於進程、線程、協程在python中的使用問題

總結

1.python裏面一個進程開了多個線程也最多也只能用同時用一個核心,用了協程也最多也只能用同時用一個核心
2.python多進程才能使用多個cpu核心,一個pythonf服務系統裏面不同的業務最好開processing進程去跑,要不然無法利用多核cpu性能,本身服務系統的併發能力就上不來。
3.其他語言如java,go 一個進程在開多個線程或多個協程的情況下可以同時用多個核心,go語言天生自帶協程屬性,並且時可以在操作系統的不同線程中調度的協程。
4.服務性能改造要麼提高單節點併發性能,提高性能時要進行各階段壓力測試,找出性能瓶頸的地方,在針對性的進行性能提升;要麼進行系統分佈式系統改造,用多個服務器部署多個節點去負載承擔業務以提高併發,分佈式往往會增加一些組件,造成一些組件單點故障,或者服務一致性,數據一致性等等問題需要考慮。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章