天天看點

Dragonwell特性: Wisp

1、協程與異步程式設計 

1、多線程和事件模型 

在Web Server領域,最早像Apache Server,大家都是使用多線程模型處理并發。下圖左邊這張圖是通過多個程序去處理多個使用者不同的請求,可能在一個單核系統上也可以去建立多個程序來處理這些請求,但這實際上作業系統給大家一個假象:作業系統通過分時互動機制去不停的切換線程,表現出一種正在同時執行的假象。 

Dragonwell特性: Wisp

實際上這個切換是非常消耗資源的,然後我們看右邊這張圖NGINX,他率先使用了事件模型,讓大家科學認知到在單個線程裡通過業務代碼去切換不同的上下文,這樣可以大大減少作業系統裡面限制切換開銷,很好的提高性能。 

2、上下文切換 

上下文切換會吃掉寶貴的CPU資源,大家很多情況下對上下文有誤區,進出核心和排程之間其實很大差異的。假如像剛才這種場景,我們看到多個線程來回調用,那一個線程當它資源耗盡或者比較阻塞的時候,下個線程選誰?其實作業系統需要進行排程,真正的損耗遠大于想象。 

Dragonwell特性: Wisp

我們可以看到進出核心是上圖左邊灰色這一列,它的耗時是很小的,可能在幾十到一百納秒級别。然後假如這一次系統調用它觸發了切換,比如讀一個程式裡面有資料,信令要挂起會觸發上下文切換,如果希望有排程,開銷就會很大,會達到40倍左右。 

3、使用異步程式設計 

是以如果在程式設計中引發排程的切換開銷是很大的,我們應該盡量避免。怎麼避免呢?答案就是異步程式設計,在node.js裡面,我們可以使用大量callback區域處理業務邏輯。當使用callback以後,代碼可能會變成這樣一種三角形,因為每一個組織方式,它後面傳回值都要帶callback調用,都會縮進去一層。這樣業務邏輯非常難以維護。 

其次是即便我使用了異步程式設計,但可能還是不小心在現實裡面使用了一段阻塞代碼,下圖是NGINX官網所提供的圖檔,雖然我自己去切換不同的請求處理,但是中間可能還是不小心調用了作業系統的一個阻塞方法。 

Dragonwell特性: Wisp

為了解決這個問題,NGINX雖然是一個号稱純異步事件驅動的模型,但是它最近也引入了線程池去處理這種可能阻塞現實的情況。 

4、引入協程 

其實最早在作業系統裡沒有協程的概念,大家都是通過協程做邏輯上抽象來幫助我們寫并發代碼。 

Dragonwell特性: Wisp
Dragonwell特性: Wisp

比如說這裡有兩段code,一段是解壓的code,一段是 parser的code。大家要從解壓資料結構裡面去解析資料,這裡對資料進行簡單的encode,如果char是普通字元,會直接傳回。若是特殊字元,可能就進行一個長度encode。用協程來組織邏輯,emit() 和 parser::getchar()會切換到另一個協程,如果沒有協程需要兩個線程結合pipe來組織,但如果有協程,我們可以在 frame裡面直接控制,邏輯清晰且性能高。 

我們看怎麼實作協程。協程的執行上下文其實包括這幾個部分,目前的站、局部變量、目前代碼位置,這些其實都可以通過資料表示。 

Dragonwell特性: Wisp

與OS内的線程切換方式一緻 

(1) 儲存pc 

(2) 儲存sp 

(3) 儲存callee-save寄存器 

儲存完這些後,将來想回去,隻要通過反向電腦pop出來,就會回到之前上下文。協程場景下,emit和 getchar都是通過這種方式去實作的。 

5、現代程式設計語言中的協程 

Dragonwell特性: Wisp

左邊是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資源,實際上排程器會繼續去排程其他執行,一旦進行這類封裝以後,我們看到代碼可以被簡化為下面這種形式。 

Dragonwell特性: Wisp

Conn=clinet. AGetConnection(); 

然後 rs= Conn .aQurerythat(“SELECT * FROM ...”) 

這段代碼相比左邊這段代碼那就是大大簡化了,但我們要做對這種回調形式進行封裝。 

6、Dragonwell: Wisp 原理 

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提高性能的例子 

Dragonwell特性: Wisp
Dragonwell特性: Wisp

左邊這張圖是不開Wisp,使用wrk壓測工具去壓這台機器,192.168.1.101,8080端口,平均的延遲是522微秒,QBS是不到5萬,在同個應用完全不改代碼情況下,我們調整一下界面參數把Wisp打開,然後線程就被完全意識到協程了,latency降低到270多微秒, QBS變成了6萬多,大概有20%多的性能提升,這不需要修改任何應用代碼,是一個免費的性能午餐,是以推薦大家可以通過Wisp提高我們微服務的性能表現。