1、協程與異步編程
1、多線程和事件模型
在Web Server領域,最早像Apache Server,大家都是使用多線程模型處理併發。下圖左邊這張圖是通過多個進程去處理多個用戶不同的請求,可能在一個單核系統上也可以去創建多個進程來處理這些請求,但這實際上操作系統給大家一個假象:操作系統通過分時互動機制去不停的切換線程,表現出一種正在同時執行的假象。
實際上這個切換是非常消耗資源的,然後我們看右邊這張圖NGINX,他率先使用了事件模型,讓大家科學認知到在單個線程裡通過業務代碼去切換不同的上下文,這樣可以大大減少操作系統裡面限制切換開銷,很好的提高性能。
2、上下文切換
上下文切換會吃掉寶貴的CPU資源,大家很多情況下對上下文有誤區,進出內核和調度之間其實很大差異的。假如像剛才這種場景,我們看到多個線程來回調用,那一個線程當它資源耗盡或者比較阻塞的時候,下個線程選誰?其實操作系統需要進行調度,真正的損耗遠大於想象。
我們可以看到進出內核是上圖左邊灰色這一列,它的耗時是很小的,可能在幾十到一百納秒級別。然後假如這一次系統調用它觸發了切換,比如讀一個程序裡面有數據,信令要掛起會觸發上下文切換,如果希望有調度,開銷就會很大,會達到40倍左右。
3、使用異步編程
所以如果在編程中引發調度的切換開銷是很大的,我們應該儘量避免。怎麼避免呢?答案就是異步編程,在node.js裡面,我們可以使用大量callback區域處理業務邏輯。當使用callback以後,代碼可能會變成這樣一種三角形,因為每一個組織方式,它後面返回值都要帶callback調用,都會縮進去一層。這樣業務邏輯非常難以維護。
其次是即便我使用了異步編程,但可能還是不小心在現實裡面使用了一段阻塞代碼,下圖是NGINX官網所提供的圖片,雖然我自己去切換不同的請求處理,但是中間可能還是不小心調用了操作系統的一個阻塞方法。
為了解決這個問題,NGINX雖然是一個號稱純異步事件驅動的模型,但是它最近也引入了線程池去處理這種可能阻塞現實的情況。
4、引入協程
其實最早在操作系統裡沒有協程的概念,大家都是通過協程做邏輯上抽象來幫助我們寫併發代碼。
比如說這裡有兩段code,一段是解壓的code,一段是 parser的code。大家要從解壓數據結構裡面去解析數據,這裡對數據進行簡單的encode,如果char是普通字符,會直接返回。若是特殊字符,可能就進行一個長度encode。用協程來組織邏輯,emit() 和 parser::getchar()會切換到另一個協程,如果沒有協程需要兩個線程結合pipe來組織,但如果有協程,我們可以在 frame裡面直接控制,邏輯清晰且性能高。
我們看怎麼實現協程。協程的執行上下文其實包括這幾個部分,當前的站、局部變量、當前代碼位置,這些其實都可以通過數據表示。
與OS內的線程切換方式一致
(1) 保存pc
(2) 保存sp
(3) 保存callee-save寄存器
保存完這些後,將來想回去,只要通過反向計算器pop出來,就會回到之前上下文。協程場景下,emit和 getchar都是通過這種方式去實現的。
5、現代編程語言中的協程
左邊是VERT.X, Java裡面最近比較流行的框架,想要製作的就是Java裡的node.js的生態,我們可以看到官方所提供的連接數據庫例子。
Client.getConnection,來獲取數據庫連接,但它不是說立馬返回一個連接給到我們,而是提供callback,然後這個result裡面表示執行是否成功,如果成功的話,我們可以通過result去拿到 connection。這就是通過義務編程的方式,去讓我們在線程裡面處理大的邏輯,NGINX就是這樣的一種方式。這樣代碼其實看起來是非常難以維護的,比如在裡面需要通過result set去把數據放到緩存裡面,又是一個遠程調用需要阻塞,可能又是一種callback,這個嵌套會非常深,非常難處理,由於我們都是callback,所以這個站就沒法被維持,假如在這個地方有異常就非常難以處理。
現代編程語言是怎麼解決這個問題,我們給的答案是協程。ES7、C# 他們都提供協程來幫助解決這類問題。我們以一段Kotlin代碼為例,看協程怎麼幫助代碼改寫成非常直觀的代碼,Kotlin裡面通過suspend關鍵字來表示,函數是可以被掛起的,然後它也可以在 client上新加的方法,新的方法叫Agetconnection。裡面調用Kotlin提供的非常 medical的方法,他會獲取一個當前執行上下文的connection,讓我們getConnection直接調用。getConnection的callback是恢復當前協程的執行,並且把拿到connection作為返回值。這樣實際上不用一直佔著 CPU資源,實際上調度器會繼續去調度其他執行,一旦進行這類封裝以後,我們看到代碼可以被簡化為下面這種形式。
Conn=clinet. AGetConnection();
然後 rs= Conn .aQurerythat(“SELECT * FROM ...”)
這段代碼相比左邊這段代碼那就是大大簡化了,但我們要做對這種回調形式進行封裝。
6、Dragonwell: Wisp 原理
既然要對這麼多回調形式進行封裝,工作量是非常大的,能不能在更底層去解決,為什麼就提供了這一層幫助?因為jdk提供所有的阻塞方式都是在jdk裡面提供的。比如說Java.lang.Thread、j.u.c、java.io、synchronized這些都是有可能阻塞API。在這些API上我們都做了封裝, Wisp把這些髒活苦活全部給做掉了. Wisp還對現成模型進行一個映射。我們知道Java裡面的Java thread和操作系統pthread是1:1的映射關係,大量線程使用的話就會導致前面提到的上下切換問題。但是在Wisp下我們每一個線程都被映射到一個Wisp,wisp執行過程中可能阻塞CPU,然後這時候就可以讓pthread調動其他Wisp,調度效率非常高,可以免費提高應用的性能。
二、使用Wisp提升微服務性能
Dragonwell: Wisp demo
下面在Dragonwell下用 使用Wisp提高性能的例子
左邊這張圖是不開Wisp,使用wrk壓測工具去壓這臺機器,192.168.1.101,8080端口,平均的延遲是522微秒,QBS是不到5萬,在同個應用完全不改代碼情況下,我們調整一下界面參數把Wisp打開,然後線程就被完全意識到協程了,latency降低到270多微秒, QBS變成了6萬多,大概有20%多的性能提升,這不需要修改任何應用代碼,是一個免費的性能午餐,所以推薦大家可以通過Wisp提高我們微服務的性能表現。