系統學習消息隊列分享(九) 如何使用異步設計提高系統性能?

對於開發者來講,異步是一種程序設計的思想,使用異步模式設計的程序能夠顯著減小線程等待,從而在高 吞吐量的場景中,極大提高系統的總體性能,顯著下降時延。編程

所以,像消息隊列這種須要超高吞吐量和超低時延的中間件系統,在其核心流程中,必定會大量採用異步的 設計思想。性能優化

接下來,咱們一塊兒來經過一個很是簡單的例子學習一下,使用異步設計是如何提高系統性能的。服務器

異步設計如何提高系統性能?網絡

假設咱們要實現一個轉帳的微服務Transfer( accountFrom, accountTo, amount),這個服務有三個參數: 分別是轉出帳戶、轉入帳戶和轉帳金額。框架

實現過程也比較簡單,咱們要從帳戶A中轉帳100元到帳戶B中:異步

1. 先從A的帳戶中減去100元;
2. 再給B的帳戶加上100元,轉帳完成。async

對應的時序圖是這樣的:ide

 

 

 

在這個例子的實現過程當中,咱們調用了另一個微服務Add(account, amount),它的功能是給帳戶account 增長金額amount,當amount爲負值的時候,就是扣減響應的金額。異步編程

須要特別說明的是,在這段代碼中,我爲了使問題簡化以便咱們能專一於異步和性能優化,省略了錯誤處理 和事務相關的代碼,你在實際的開發中不要這樣作。微服務

1. 同步實現的性能瓶頸

首先咱們來看一下同步實現,對應的僞代碼以下:

 Transfer(accountFrom, accountTo, amount) { 

// 先從accountFrom的帳戶中減去相應的錢數
Add(accountFrom, -1 * amount)
// 再把減去的錢數加到accountTo的帳戶中
Add(accountFrom, amount)
return OK 
}

 

上面的僞代碼首先從accountFrom的帳戶中減去相應的錢數,再把減去的錢數加到accountTo的帳戶中,這 種同步實現是一種很天然方式,簡單直接。那麼性能表現如何呢?接下來咱們就來一塊兒分析一下性能。

假設微服務Add的平均響應時延是50ms,那麼很容易計算出咱們實現的微服務Transfer的平均響應時延大約 等於執行2次Add的時延,也就是100ms。那隨着調用Transfer服務的請求愈來愈多,會出現什麼狀況呢?

在這種實現中,每處理一個請求須要耗時100ms,並在這100ms過程當中是須要獨佔一個線程的,那麼能夠得 出這樣一個結論:每一個線程每秒鐘最多能夠處理10個請求。咱們知道,每臺計算機上的線程資源並非無限 的,假設咱們使用的服務器同時打開的線程數量上限是10,000,能夠計算出這臺服務器每秒鐘能夠處理的請 求上限是: 10,000 (個線程)* 10(次請求每秒) = 100,000 次每秒。

若是請求速度超過這個值,那麼請求就不能被⻢上處理,只能阻塞或者排隊,這時候Transfer服務的響應時 延由100ms延⻓到了:排隊的等待時延 + 處理時延(100ms)。也就是說,在大量請求的狀況下,咱們的微服 務的平均響應時延變⻓了。

這是否是已經到了這臺服務器所能承受的極限了呢?其實遠遠沒有,若是咱們監測一下服務器的各項指標, 會發現不管是CPU、內存,仍是網卡流量或者是磁盤的IO都空閒的很,那咱們Transfer服務中的那10,000個 線程在幹什麼呢?對,絕大部分線程都在等待Add服務返回結果。

也就是說,採用同步實現的方式,整個服務器的全部線程大部分時間都沒有在工做,而是都在等待。

若是咱們能減小或者避免這種無心義的等待,就能夠大幅提高服務的吞吐能力,從而提高服務的整體性能。

2. 採用異步實現解決等待問題

接下來咱們看一下,如何用異步的思想來解決這個問題,實現一樣的業務邏輯。

TransferAsync(accountFrom, accountTo, amount, OnComplete()) { // 異步從accountFrom的帳戶中減去相應的錢數,而後調用OnDebit方法。
AddAsync(accountFrom, -1 * amount, OnDebit(accountTo, amount, OnAllDone(OnComplete()))) } // 扣減帳戶accountFrom完成後調用
OnDebit(accountTo, amount, OnAllDone(OnComplete())) { // 再異步把減去的錢數加到accountTo的帳戶中,而後執行OnAllDone方法
AddAsync(accountTo, amount, OnAllDone(OnComplete())) } // 轉入帳戶accountTo完成後調用 OnAllDone(OnComplete()) {
 OnComplete() }

細心的你可能已經注意到了,TransferAsync服務比Transfer多了一個參數,而且這個參數傳入的是一個回 調方法OnComplete()(雖然Java語言並不支持將方法做爲方法參數傳遞,但像JavaScript等不少語言都具 有這樣的特性,在Java語言中,也能夠經過傳入一個回調類的實例來變相實現相似的功能)。

這個TransferAsync()方法的語義是:請幫我執行轉帳操做,當轉帳完成後,請調用OnComplete()方法。調 用TransferAsync的線程沒必要等待轉帳完成就能夠當即返回了,待轉帳結束後,TransferService天然會調用 OnComplete()方法來執行轉帳後續的工做。

異步的實現過程相對於同步來講,稍微有些複雜。咱們先定義2個回調方法:

  • OnDebit():扣減帳戶accountFrom完成後調用的回調方法;
  • OnAllDone():轉入帳戶accountTo完成後調用的回調方法。
整個異步實現的語義至關於:
  1.  異步從accountFrom的帳戶中減去相應的錢數,而後調用OnDebit方法;
  2. 在OnDebit方法中,異步把減去的錢數加到accountTo的帳戶中,而後執行OnAllDone方法;
  3. 在OnAllDone方法中,調用OnComplete方法。

繪製成時序圖是這樣的:

 

 

 

你會發現,異步化實現後,整個流程的時序和同步實現是徹底同樣的,區別只是在線程模型上由同步順序調 用改成了異步調用和回調的機制。

接下來咱們分析一下異步實現的性能,因爲流程的時序和同步實現是同樣,在低請求數量的場景下,平均響 應時延同樣是100ms。在超高請求數量場景下,異步的實現再也不須要線程等待執行結果,只須要個位數量的 線程,便可實現同步場景大量線程同樣的吞吐量。

因爲沒有了線程的數量的限制,整體吞吐量上限會大大超過同步實現,而且在服務器CPU、網絡帶寬資源達到極限以前,響應時延不會隨着請求數量增長而顯著升高,幾乎能夠一直保持約100ms的平均響應時延。

看,這就是異步的魔力。

 

簡單實用的異步框架: CompletableFuture

在實際開發時,咱們可使用異步框架和響應式框架,來解決一些通用的異步編程問題,簡化開發。Java 中比較經常使用的異步框架有Java8內置的CompletableFuture和ReactiveX的RxJava,我我的比較喜歡簡單實 用易於理解的CompletableFuture,可是RxJava的功能更增強大。有興趣的同窗能夠深刻了解一下。

Java 8中新增了一個很是強大的用於異步編程的類:CompletableFuture,幾乎囊獲了咱們在開發異步程序 的大部分功能,使用CompletableFuture很容易編寫出優雅且易於維護的異步代碼。

接下來,咱們來看下,如何用CompletableFuture實現的轉帳服務。 首先,咱們用CompletableFuture定義2個微服務的接口:

/** * 帳戶服務 */
public interface AccountService { /** * 變動帳戶金額 * @param account 帳戶ID * @param amount 增長的金額,負值爲減小 */ CompletableFuture<Void> add(int account, int amount); }
/** * 轉帳服務 */
public interface TransferService { /** * 異步轉帳服務 * @param fromAccount 轉出帳戶 * @param toAccount 轉入帳戶 * @param amount 轉帳金額,單位分 */ CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount); }

能夠看到這兩個接口中定義的方法的返回類型都是一個帶泛型的CompletableFeture,尖括號中的泛型類型 就是真正方法須要返回數據的類型,咱們這兩個服務不須要返回數據,因此直接用Void類型就能夠。

而後咱們來實現轉帳服務:

 /** * 轉帳服務的實現 */
public class TransferServiceImpl implements TransferService { @Inject private AccountService accountService; // 使用依賴注入獲取帳戶服務的實例
@Override public CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount) { // 異步調用add方法從fromAccount扣減相應金額
return accountService.add(fromAccount, -1 * amount) // 而後調用add方法給toAccount增長相應金額
.thenCompose(v -> accountService.add(toAccount, amount)); } }

在轉帳服務的實現類TransferServiceImpl裏面,先定義一個AccountService實例,這個實例從外部注入進 來,至於怎麼注入不是咱們關心的問題,就假設這個實例是可用的就行了。

而後咱們看實現transfer()方法的實現,咱們先調用一次帳戶服務accountService.add()方法從fromAccount 扣減響應的金額,由於add()方法返回的就是一個CompletableFeture對象,能夠用CompletableFeture的 thenCompose()方法將下一次調用accountService.add()串聯起來,實現異步依次調用兩次帳戶服務完整轉 帳。

客戶端使用CompletableFuture也很是靈活,既能夠同步調用,也能夠異步調用。

public class Client { @Inject private TransferService transferService; // 使用依賴注入獲取轉帳服務的實例 private final static int A = 1000;
private final static int B = 1001; public void syncInvoke() throws ExecutionException, InterruptedException { // 同步調用
transferService.transfer(A, B, 100).get(); System.out.println("轉帳完成!"); } public void asyncInvoke() { // 異步調用
transferService.transfer(A, B, 100) .thenRun(() -> System.out.println("轉帳完成!")); } }

在調用異步方法得到返回值CompletableFuture對象後,既能夠調用CompletableFuture的get方法,像調 用同步方法那樣等待調用的方法執行結束並得到返回值,也能夠像異步回調的方式同樣,調用 CompletableFuture那些以then開頭的一系列方法,爲CompletableFuture定義異步方法結束以後的後續操 做。好比像上面這個例子中,咱們調用thenRun()方法,參數就是將轉帳完成打印在控臺上這個操做,這樣 就能夠實如今轉帳完成後,在控制檯打印「轉帳完成!」了。

簡單的說,異步思想就是,當咱們要執行一項比較耗時的操做時,不去等待操做結束,而是給這個操做一個 命令:「當操做完成後,接下來去執行什麼。」

使用異步編程模型,雖然並不能加快程序自己的速度,但能夠減小或者避免線程等待,只用不多的線程就可 以達到超高的吞吐能力。

同時咱們也須要注意到異步模型的問題:相比於同步實現,異步實現的複雜度要大不少,代碼的可讀性和可 維護性都會顯著的降低。雖然使用一些異步編程框架會在必定程度上簡化異步開發,可是並不能解決異步模 型高複雜度的問題。

異步性能雖好,但必定不要濫用,只有相似在像消息隊列這種業務邏輯簡單而且須要超高吞吐量的場景下, 或者必須⻓時間等待資源的地方,才考慮使用異步模型。若是系統的業務邏輯比較複雜,在性能足夠知足業 務需求的狀況下,採用符合人類天然的思路且易於開發和維護的同步模型是更加明智的選擇。

相關文章
相關標籤/搜索