微服務的各種線程模型及其權衡

作者 Glenn Engstrand ,譯者 張衛濱 發佈於 2016年10月25日

架構師在設計微服務架構的時候,一般會關注模式、拓撲以及粒度等問題,但是有一個最爲基礎的決策是線程模型。我們現在有了很多的開源工具、編程語言和技術棧,軟件架構師所面臨的選擇要比以往更多了。

這樣的話,我們很容易就會迷失在語言的細節和/或不同庫的差異之中,從而無法分辨什麼東西纔是最重要的。

爲微服務選擇正確的線程模型並確定它將如何與數據庫連接進行關聯非常重要,這決定了你的解決方案是剛剛能用,還是會成爲一個很棒的產品。


作爲架構師,在考慮效率和複雜性之間的權衡時,關注線程模型是一種有效的方式。服務會被分解爲並行的操作,通過共享的資源來進行處理,所以應用會變得更加高效,其響應的延遲也會更短(這會在一定的範圍之內,參見Amdahl定理)。不過,並行操作和安全的資源共享會爲代碼引入更多的複雜性。

代碼越複雜,工程師完全理解起來就會越困難,這意味着在每次變更的時候更有可能引入新的bug。

架構師最爲重要的責任之一就是在效率和代碼複雜性之間找到一個平衡。

單線程單進程的線程模型

最基本的線程模型就是單線程單進程模型,按照這種方式編寫代碼是最簡單的。

單線程單進程服務同一時間無法在多個核心上執行。在現代的裸機服務器上,核心數量一般能夠達到24個。如果按照這種模型構建服務的話,它所能使用的服務器核心數量不會超過一個。如果有額外的負載的話,這些服務的吞吐量不會隨之增加,它們的CPU利用率百分比不會超過個位數。鑑於如此高的未利用率,所以有一種補償策略就是使用更大的服務器池來處理負載。

這種方式可以運行,但是它非常浪費,最終的成本會非常高昂。最爲流行的雲計算供應商都以非常便宜的價格提供單虛擬核心實例,這樣做是爲了以更細的粒度來支持這種模式,從而應對擴展性的需求。

單線程多個新進程的線程模型

在複雜性和效率方面更進一步的就是單線程多進程的線程模型,在這種方式下,會爲每個請求創建一個新的進程。編寫這種類型的微服務相對比較簡單,但是跟前面的模型相比它包含了更多的複雜性。

(點擊放大圖像)

創建進程的開銷以及持續創建和銷燬數據庫連接會佔用處理器的時間,因此會增加所有協作服務的延遲。這種線程模型之所以會創建更多的數據庫連接是因爲數據庫連接是屬於每個進程的,無法跨進程邊界共享。進程的存活時間只會在請求的時間範圍內,所以每個請求必須要重新連接數據庫。

按照這種線程模型運行的微服務應該延遲對數據庫的連接,直到需要的時候再創建連接。如果代碼路徑不需要的話,那就沒有必要耗費成本創建數據庫連接了。儘管數據庫連接無法跨進程緩存,但是有些環境支持跨進程的opcode緩存,這樣的話,我們可以將服務的配置數據存儲起來,如連接到數據庫的主機IP和憑證信息,兩個流行的opcode緩存樣例就是Zend OpCache和APC。

單線程多進程重用的線程模型

在代碼複雜性和性能方面的下一步提升就是這種線程模型,這是一種單線程多線程的模型,新的請求都會重用已有的worker進程。這與前面的線程模型有所不同,在前面的模型中,會爲每個請求都創建一個新的進程。而在這個線程模型中,在進程提供就緒之後,就不會創建新的進程了。

(點擊放大圖像)

這種服務在複雜性方面相對來說比較簡單直接,但是需要額外的代碼來管理worker進程的生命週期。這些代碼必須要正確地進行重新初始化。例如,程序員可能會維護一些靜態變量,而不是以參數的形式傳遞大量的數據。這樣的話,代碼會更加簡單,針對每個新的請求都對這些靜態變量進行重置的話,代碼就能正常運行。但是如果代碼沒有重置這些變量的話,那麼它就會基於之前的請求來進行處理,而不是基於當前的請求。在代碼複雜性方面,另外一點就是需要包含恢復失效(stale)數據庫連接的邏輯。當與數據庫的連接由於不活躍而斷掉的時候,原有的數據庫連接實例可能會出現失效的情況。

因爲每個進程能夠服務於多個請求,所以沒有必要針對每個請求都重新連接數據庫。數據庫連接會進行重用,這樣的話能夠規避創建連接的成本,從而減少延遲。但是,每個進程本身還是需要創建和管理自己的數據庫連接。因爲進程之間無法共享數據庫連接,所以進程間公用的數據庫會打開更多的連接。打開過多的連接將會降低數據庫的性能。這是因爲數據庫連接是有狀態的,數據庫應用必須要在自己的進程中爲每個連接分配資源。

多線程單進程的線程模型

我們有一種保護數據庫的更好方式,這就是在多線程、長期存活的單進程模型中通過可配置的連接數來使用連接池。儘管數據庫連接無法跨多進程共享,但是在同一個進程中,它可以在多個線程間共享。

(點擊放大圖像)

如下是一個樣例:如果有100個單線程的進程,每個進程有10臺服務器,那麼數據庫將會有100 X 10 = 1000個連接。如果我們的每個進程有100個線程,共有10臺服務器,每個進程在它的連接池中配置了10個連接,那麼數據庫只會有10 X 10 = 100個連接,它依然能夠實現很高的吞吐量。對於服務和數據庫來說,跨線程的連接池都是一種高效的方案。

這種連接池技術既能實現很高的吞吐量又能保護數據庫,但是它會帶來額外的代碼複雜性。因爲線程必須共享有狀態的數據庫連接,所以開發人員需要識別並修正併發相關的缺陷,比如死鎖、活鎖、線程餓死和競態條件。解決這些缺陷的方式之一就是進行序列化地訪問,但是太多的序列化訪問會降低並行性。對於初級的開發人員來說,這些類型的缺陷很難識別和修正。

多線程、長期存活的單進程模型有兩種實現風格:一種是爲請求分派一個專屬的線程,另一種是所有的請求共享一個線程。在前者的線程模型中,每個請求會有一個專門的線程與之關聯,這要限制並行處理的請求數量。太多的連接可能會導致效率低下,這是因爲在操作系統的CPU調度器中需要執行太多的任務切換。

在後者的線程模型中,我們沒有必要爲每個請求創建額外的線程,但是I/O相關的任務必須要在單獨的線程池中運行,這樣的話,能夠防止系統因爲遇到較慢的操作而hang住,請求處理器需要等待線程池的處理結果。

這種方式沒有爲每個請求創建專門的線程,對於異步操作我們可以期望有很高的吞吐量和較低的延遲,但是對於同步操作來說,相對於爲每個請求創建專屬的線程,這種方式不會帶來性能方面的改善。

小結

(點擊放大圖像)

結論

在考慮採用哪種庫和語言之前,軟件架構師應該反思哪種線程模型能夠最適合其工程文化和能力。在代碼複雜性和效率之間取得一個很好的平衡將會有助於理清這些困惑,在各種可行的技術棧之間做出選擇時,也能有一個正確的方向。因爲微服務的範圍要比單體應用更小,爲了實現更高的效率,可以在代碼複雜性方面做出更多的努力。

關於作者

Glenn Engstrand是Zoosk架構師團隊的技術領導。他的主要關注點在於服務端的應用架構,他所關注的架構需要運行在B2C Web可擴展的環境中,並且還要保證可控的運維和部署成本。在波士頓的2012 Lucene Revolution會議上,Glenn是一位知名的演講者。他的專長在於將單體應用拆分爲微服務並使其與實時通信設施進行深度集成。

 

查看英文原文:Microservice Threading Models and their Tradeoffs

轉自:http://www.infoq.com/cn/articles/engstrand-microservice-threading
發佈了1 篇原創文章 · 獲贊 29 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章