1. 程式人生 > >Python非同步IO的未來(從Web後端開發的角度)

Python非同步IO的未來(從Web後端開發的角度)

那麼Zeromq怎麼樣

首先,它不是那種多功能的軟體:

  1. 它很難拓展(hack on)(使用了複雜的C++ Actor模型)

  2. 嵌入進一些程式的效果欠佳(也就是說它沒有使用好fork)

  3. 對故障切換(failover)和服務發現(service discovery)整合欠佳

  4. 對非冪等(non-idempotent)請求和有狀態路由(stateful routing)操控欠佳

Nanomsg在(1)表現相對要好但遠未達到完美。而在當前的設計中(2)還不能解決。(3)nanoconfig庫為nanomsg解決,但卻比nanomsg本身受到了更少的關注。(4)在nanomsg中可能最終解決但現在還沒有。

  第二個大問題是工程師們還不太適應它的思考方式,例如對同樣的連線,redis協議使用釋出-訂閱(pub-sub)和請求-響應(req-rep),mongo使用push-pull和請求-響應(req-rep),而zeromq不允許。nonomsq特別想修正工程師們頭腦中的這種想法,但這條路還很長。

  別誤解我,zeromq很好。nanomsq從這個失誤中學到了很多,當它可用於生產環境時,它將是我用於服務間訊息傳遞的第一選擇。

但是微服務怎麼了?

  好吧,最簡單的原因是,僅僅使用zeromq你甚至不能構建一個很小的服務。但是如果你的DB支援HTTP,你就可以使用HTTP在客戶端和服務端兩端構建服務。聽起來很簡單(但是記住HTTP很複雜)。

  另外一個問題是I/O模型。當你的程式碼是單執行緒的,你就不能在不使用連線的情況下,依然保持連線心跳。即使你使用非同步迴圈,它也可能因做其他運算而停頓很久。

  有時你想給連線發請求,但是連線實際上已經關閉了。有種廣泛使用的方法,讀取連線資料,檢出是否可用,因為通常傳送請求後很難恢復:

?
1 2 3 4 if s.read() == b'': self.reconnect() s.write(request) response = s.read()

  這意味著連線僅當你傳送請求時才開始建立,而不是當zeromq或nanomsg中那樣只要準備好了。

  而且這樣也不好做服務發現,現在你有三種簡單的選擇:

  1. 每次請求前檢查服務名字(如解析DNS)

  2. 下次連線請求時解析服務名字

  3. 永不更新服務(即,直到程序重啟)

  大部分使用者選擇(3)。有時(2)可以直接用(work out of the box, 開箱即可用),但是它只有機器不可達後故障切換時才會發生。(1)相當低效,幾乎不可用。

I/O核心設計


(I/O核心執行緒與Python執行緒使用RPC互動)

  所以,我建議重新設計所有IO子系統,即,用C(或其他無GIL的語言)寫個庫來處理IO,這樣IO與程式主執行緒無關了。它應該與python主執行緒使用類似訊息(messaging)的機制來通訊。但是,不能傳送Python物件,也不要在I/O執行緒內持有GIL。

  I/O核心要支援多種協議,每個協議應該:(a)處理握手,(b)把流切分成訊息,這樣完整的訊息才會被轉發給主執行緒。如果可以設計連線細節,比如自動故障切換(automatic failover)的主從關係(master/slave relations),就更好了。

  I/O執行緒應該可以解析名字,處理連線請求,能夠訂閱DNS名字變化,以及其他的一些高階特性。

  注意,這些不僅僅對python有用,也適用於其他有GIL的指令碼語言。事實上,對無GIL的語言,也能很好地工作,但可能沒那個必要。

先前的做法

  這個思路部分存在於很多產品中:

  1. 上文提到的zeromq和nanomsg使用不同的執行緒來處理I/O

  2. Kazoo(python版的zookeeper)使用單獨的(python式的)執行緒處理重連、ping連線

  3. Twisted把阻塞計算轉移到執行緒池(儘管我們需要相反的東西,這已經算是工作量解除了) 

  也許還有更多的例子,我仍然沒有看到用單獨執行緒來建立統一的I/O核心的嘗試。如果你知道,告訴我。

  這種模式和最近出現的Ambassador模式很像。Ambassador是個程序,存在於每臺機器,進行服務發現,但是通過自身代理所有連線,即,所有服務都連線到localhost上Ambassador監聽的埠,然後Ambassador把連線轉發到真正的服務上去。類似的,I/O核心也應該代替主執行緒進行服務發現、與服務進行通訊(協議仍然與Ambassador使用的那個有很大不同)。

意義所在

難道是為了效能上能提升幾毫秒?

  對。事實上,當使用多個服務來處理單個前端請求時,毫秒級的延遲累積地相當快。而且,這種技術可以在CPU使用率接近100%時,能挽救非線性增長的延遲。

還是為了保持持久連線?如果你足夠小心地經常放棄CPU,它們在傳統的非同步I/O上也工作得很好。

  對。如果你在用非同步I/O,那你已經非常出色了,因為很多人根本沒有看到這種必要。但是服務發現需要多少非同步庫才算合理?(我的回答是:一個都不需要)

但用非同步I/O也能進行服務發現。

  當然。但是沒人這樣做。我認為應該趁機也解決這個問題。

  下面是我設想的I/O核心應該做的任務:

檢測和統計

  對CPU密集型的任務來說,定期傳送統計會比較困難。這個需要被修復。主執行緒應該遞增在某個記憶體區域的計數器,而完全與I/O執行緒無關。

  而且我們獲取請求-響應計時器的精確時間戳。通常它們對python主迴圈的工作量評估嚴重不準。

  在正確的服務發現幫助下,我們甚至可以在真正的使用者試圖在此worker上執行請求前,知道哪些必要的服務不可用。

除錯

  假設你可以讓Python在任何時間獲取狀態。首先,我們總會有一批在處理中的請求。而且,我們可以像統計一樣,貼上一些標記點。最後,我們可以使用類似錯誤處理器(faulthandler)所用的一種方法來找出主執行緒的棧。

  關鍵在於,有個執行緒可以迴應除錯請求,甚至是當主執行緒在做CPU密集型的事情,或者因為某些原因掛起時。

管道

  請求應該儘可能通過管道傳輸,即,不管哪個前端請求需要資料庫請求,我們通過一個數據庫連線傳送全部。

  這樣,db連線數就可以很少,而且允許我們統計哪份副本較慢。

名字發現

  我們不僅要解析DNS名字(不管我們選擇的是什麼名字解析方案),還要當名字變化時獲取更新,比如zookeeper中的設定watch。

  這個過程必須對應用透明,並且在應用開始請求前,連線已經存在。

統一

  既然I/O核心就位,各個python的I/O框架只需要支援核心支援的協議。所有的新協議應該在核心裡完成。這促使框架在方便上和效率上競爭,而不是協議的支援上。

節流(Throttling)

  即使在Java和Go這些可以自由使用執行緒的語言裡,也需要控制客戶端的連線數。該設計,不管到底哪個庫才是網路請求的真正執行者,允許控制應用中單個位置處的請求數目。

設計隨想

下面是關於設計I/O核心的一些隨想(沒有順序),有些可能在最終設計時會被去掉。

  1. I/O核心應該是單執行緒的。因為不太可能用Python程式碼過載用C實現的I/O執行緒。相比於nanomsg或者zeromq,設計選擇更加簡單。

  2. 所有I/O應該在使用最小互鎖(minimal interlocking)的I/O執行緒內完成。因為喚醒其他執行緒比執行python位元組碼的開銷要小(這樣設計也更簡單)。

  3. 相較於持有GIL(全域性解釋鎖,global interpreter lock),從其他python物件複製資料代價更小。但是,如果可行,應該直接分配非python的緩衝區,並直接將資料序列化進去。

  4. 所有支援的協議至少應該可以用C切分成幀。這樣不完整的包不會到達python程式碼,其他解析應該在主執行緒內用python物件直接完成。

  5. 服務發現(service discovery)應該可插拔(pluggable)。最可能的選項應該最先被實現(比如DNS名稱查詢)。

  6. 服務發現應該可以被簡單地整合到任何協議。事實上,對協議的實現者來說,使用服務發現比忽略它更簡單。

結束

  當然建立這樣的工具不是一個週末就可以完成的事情。這是一份艱苦的工作,而且是無限長的旅程。

  直到現在也適合重新思考為什麼我們使用Python操控網路。最近的工具比如穩定的libuv和Rust語言可以極大的簡化建立I/O核心。當然可以明智的使用go-python來原型化(prototype)程式碼,但這容易做但是不是長久之計。

  論及的方案,使用起來很簡潔。期望在未來我們能使用python建立高效,高效能的服務,尤其是在動態網路配置方面,從而在效能差異明顯的問題上不需要使用其他語言重寫所有內容。