1. 程式人生 > 實用技巧 >C# 後端呼叫 Web API

C# 後端呼叫 Web API

技術標籤:演算法javapython資料庫多執行緒

1 採用非同步設計

假設使用同步實現,偽裝程式碼如下:

Transfer(accountFrom, accountTo, amount) {
  // 先從 accountFrom 的賬戶中減去相應的錢數
  Add(accountFrom, -1 * amount)
  // 再把減去的錢數加到 accountTo 的賬戶中
  Add(accountTo, 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 服務返回結果。

採用非同步處理

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()
}

由於沒有了執行緒的數量的限制,總體吞吐量上限會大大超過同步實現,並且在伺服器 CPU、網路頻寬資源達到極限之前,響應時延不會隨著請求數量增加而顯著升高,可以一直保持約 100ms 的平均響應時延。

2 序列化與反序列化資料的壓縮優化

比如我們要序列化一個 User 物件,它包含 3 個屬性,姓名 zhangsan,年齡:23,婚姻狀況:已婚。

User:
  name: "zhangsan"
  age: 23
  married: true

使用 JSON 序列化後:

{"name":"zhangsan","age":"23","married":"true"}

實現高效能的序列化,對於同樣的 User 物件,我們可以把它序列化成這樣:

03   | 08 7a 68 61 6e 67 73 61 6e | 17 | 01
User |    z  h  a  n  g  s  a  n  | 23 | true

03 表示這是一個 User 型別的物件。可以約定按照 name、age、married 這個固定順序來序列化這三個屬性。按照順序,第一個欄位是 name,不存欄位名,直接存欄位值“zhangsan”就可以了,由於名字的長度不固定,用第一個位元組 08 表示這個名字的長度是 8 個位元組,後面的 8 個位元組就是 zhangsan。第二個欄位是年齡,直接用一個位元組表示就可以了,23 的 16 進位制是 17 。最後一個欄位是婚姻狀態,用一個位元組來表示,01 表示已婚,00 表示未婚,這裡面儲存一個 01。

可以看到,同樣的一個 User 物件,JSON 序列化後需要 47 個位元組,這裡只要 12 個位元組就夠了。

3 雙工收發協議

4使用批量訊息提升服務端處理能力

5利用 PageCache 加速訊息讀寫

  PageCache 是現代作業系統都具有的一項基本特性。通俗地說,PageCache 就是作業系統在記憶體中給磁碟上的檔案建立的快取。無論我們使用什麼語言編寫的程式,在呼叫系統的 API 讀寫檔案的時候,並不會直接去讀寫磁碟上的檔案,應用程式實際操作的都是 PageCache,也就是檔案在記憶體中快取的副本。應用程式在寫入檔案的時候,作業系統會先把資料寫入到記憶體中的 PageCache,然後再一批一批地寫到磁碟上。讀取檔案的時候,也是從 PageCache 中來讀取資料,這時候會出現兩種可能情況。

  一種是 PageCache 中有資料,那就直接讀取,這樣就節省了從磁碟上讀取資料的時間;另一種情況是,PageCache 中沒有資料,這時候作業系統會引發一個缺頁中斷,應用程式的讀取執行緒會被阻塞,作業系統把資料從檔案中複製到 PageCache 中,然後應用程式再從 PageCache 中繼續把資料讀出來,這時會真正讀一次磁碟上的檔案,這個讀的過程就會比較慢。使用者的應用程式在使用完某塊 PageCache 後,作業系統並不會立刻就清除這個 PageCache,而是儘可能地利用空閒的實體記憶體儲存這些 PageCache,除非系統記憶體不夠用,作業系統才會清理掉一部分 PageCache。清理的策略一般是 LRU 或它的變種演算法,這個演算法我們不展開講,它保留 PageCache 的邏輯是:優先保留最近一段時間最常使用的那些 PageCache。

  例如Kafka 在讀寫訊息檔案的時候,充分利用了 PageCache 的特性。一般來說,訊息剛剛寫入到服務端就會被消費,按照 LRU 的“優先清除最近最少使用的頁”這種策略,讀取的時候,對於這種剛剛寫入的 PageCache,命中的機率會非常高。也就是說,大部分情況下,消費讀訊息都會命中 PageCache,帶來的好處有兩個:一個是讀取的速度會非常快,另外一個是,給寫入訊息讓出磁碟的 IO 資源,間接也提升了寫入的效能。