微服務當下非常流行,即使在傳統的 IT 企業中也是如此。然而通常情況下微服務使用諸如 Java 之類的語言來實現,而這些語言誕生於 90 年代初,並且專爲開發單體應用而設計。你還記得舊的大型應用服務器嗎?

如果忽略近十年來發展的新的開發平臺,在採用微服務時可能導致非最優的結果以及較高的運行成本。

在過去的十年中出現了很多新的編程平臺,所有這些平臺的目的都是爲“現代分佈式計算”提供更好的支持,而“現代分佈式計算”正是微服務的基礎。此類技術有望優化基礎架構成本,並有效解決數字革命帶來的工作負載不斷增加的問題。

此外,隨着容器的出現,開發人員可以“用他們想要的任何語言編寫並在任何地方運行”,從而使最初由 Java 實現的“一次編寫,到處運行”變得不那麼重要了。

當採用基於微服務的架構時,忽略應用程序開發領域的此類進步可能會導致非最優結果。

這篇文章的重點是其中兩種技術:Node 和 Go。爲什麼是這二者?令我着迷的一個奇怪的事實是:它們的生日相同,我的意思是幾乎是同一天。這也許不是偶然。Java 發佈後的 15 年,Node和Go誕生

2009 年 11 月 8 日,Ryan Dahl 首次發佈了 Node,這是一個在服務器上運行 Javascript(現在還包括 Typescript)的開源平臺。

兩天後,Google 發佈了一種新的開源編程語言 Go。從那時起,Node 和 Go 一直穩步發展,現在已經成爲主流技術。根據 StackOverflow2020 開發人員調查,Node 是最受歡迎的平臺,而 Go 在 最受歡迎的編程語言中排名第三。

考慮到兩種技術有本質不同,這可能被視爲巧合。但是在如此接近的誕生日期之後,也許還有一些深刻的東西,而這今天仍然與我們有關。併發模式下,微服務更具伸縮性

到 2009 年,數字服務需求的指數增長已經成爲事實。但是,負載的指數增長無法通過基礎設施的類似指數增長來解決。爲了應對這一挑戰,一種新的體系結構模式出現了:基於多核處理器(例如 x86)的水平可擴展分佈式系統,而這是微服務化的基礎。

使用微服務,你可以更好地優化許多小任務的併發處理,這不是在設計 Java 和 C#等編程語言時的目標。

只要能夠“同時”處理許多小任務,這種架構就可以以可持續的成本進行擴展,從而最大程度地利用那些商用多核處理器的 CPU 和內存資源。

Java 等傳統編程語言誕生於不同的時代,其設計並未考慮到水平可擴展的分佈式體系結構。上世紀 90 年代的應用程序是單體的:單個服務器運行單個進程。

2009 年,新的需求是在許多小型計算機上同時運行許多小任務(大型併發 / 並行系統)。因此這種需求和已有技術上顯然存在着不匹配。

雖然 Node 和 Go 着眼於不同的方向,但他們都可以解決這種不匹配問題。Node 和 Go 的共同點:對併發的原生支持

Node 和 Go 同時誕生可能是一個巧合,尤其是它們有非常多的不同點。

但是,如果我們看一下它們的共同點,可能就會覺得這不是巧合了:對併發的原生支持。這就是爲什麼可以將它們視爲應對現代分佈式計算所帶來的新挑戰的兩種不同方式:對併發的強大支持。爲什麼併發在分佈式架構中如此重要

讓我們看一個典型的程序,它與數據庫、REST 服務進行交互,也可能與存儲進行交互。

這樣的程序通常做什麼?大多數情況下,它保持空閒狀態,等待其他地方執行的某些 I/O(輸入 / 輸出)操作。這稱爲 I/O 綁定,因爲實際上單位內處理的請求受 I/O 操作響應速度的限制。

由於物理原因,I/O 操作的速度比 CPU 執行的速度要慢幾個數量級。訪問主存儲器中的數據大約需要 100 納秒。同一數據中心內的數據往返大約需要 250,000 納秒,不同區域之間的數據往返則超過 2,000,000 納秒。結果是對單個請求的處理絕大多數時間是在等待 I/O 操作完成,這浪費了大部分 CPU 週期。

在分佈式體系結構中,如果要優化基礎設施的使用並使其成本最小化,那麼併發至關重要。

那麼,這對我們的計算能力意味着什麼呢?這意味着除非我們確保 CPU 可以“同時”處理多個請求,否則它將一直不能得到充分利用。而這正是併發的全部意義所在。

這不是一個新問題。傳統上,在 Java 世界中,這是應用程序服務器的任務。但是應用服務器不適用於分佈式和水平可擴展的體系結構。這就是 Node 和 Go 之類的技術可以發揮作用的地方。

併發和並行很相似,但概念不同。我在這裏使用併發,因爲併發在這種情況是真正重要的。Node 和併發

Node 是單線程的。因此,所有操作都在一個線程中運行(嗯……幾乎所有操作,但這與我們所討論的內容無關)。那麼它如何支持併發呢?祕密在於 Node 線程也不會在 I/O 操作時阻塞。

在 Node 中,當執行 I/O 操作時,程序不會停下等待 I/O 響應。相反,它爲系統提供了一個回調函數(當 I/O 返回時必須執行的操作),然後立即執行下一個操作。I/O 完成後,將執行回調函數,恢復程序的處理邏輯。

因此,在請求 / 響應場景中,我們觸發 I/O 操作並釋放 Node 線程,以便同一 Node 實例可以立即處理另一個請求。

在上面的示例中,第一個請求 Req1 運行一些初始邏輯(第一個深綠色條),然後啓動 I/O 操作(I/O 操作 1.1),該操作指定在 I/O 完成時必須調用的回調函數(函數 cb11)。

此時,對 Req1 的處理將暫停,Node 可以開始處理另一個請求,例如 Req2。當 I/O 操作 1.1 完成時,Node 準備好恢復對 Req1 的處理,並將調用 cb11。cb11 本身將通過 cb12 作爲回調函數來啓動另一個 I/O 操作(I/O 操作 1.2),該操作將在第二個 I/O 操作完成時被調用。依此類推,直到 Req1 處理結束,並將響應 Resp1 發送回客戶端。

以這種方式,Node 可以通過單線程同時(即併發)處理多個請求。非阻塞模型是 Node 中併發的關鍵。

但是,如果使用單線程,則意味着我們不能利用多核(對於多核方案,可以使用 Node 集羣,但是沿着這條路走將不可避免地給整個解決方案增加一些複雜性)。

要注意的另一個方面是,非阻塞模型意味着使用異步編程風格,除非進行適當管理,這種編程風格一開始可能導致難以理解,並且可能導致複雜變得代碼,即所謂的“回調地獄”。Go 語言和併發

Go 併發的方法基於 goroutine,它們是 Go 運行時管理的輕量級線程,它們通過通道(channel)相互通信。

程序可以啓動許多 goroutine,而 Go 運行時將根據其優化算法在 Go 可用的 CPU 內核上調度它們。goroutine 不是操作系統任務,它們需要的資源少得多,並且可以被快速大量生成(有一些使用 Go同時運行數十萬甚至數百萬個goroutine 的例子)。

Go 也是非阻塞的,但這都是通過 Go 運行時在後臺完成的。例如,如果一個 goroutine 觸發了網絡 I/O 操作,則其狀態將從“運行”變爲“等待”,並且 Go 運行時調度器將選擇另一個 goroutine 來執行。

因此,從併發角度來看這和 Node 所做的類似,但其中有兩個主要區別:它不需要任何回調機制,並且代碼執行順序與常規的同步邏輯相同,這通常更容易實現它是多線程的,可以無縫利用 Go 運行時可用的所有 CPU 內核

在上面的示例中,Go 運行時有 2 個可用內核。所有處理器都用於處理傳入的請求。每個傳入的請求均由一個 goroutine 處理。

例如,Req1 由 Core1 上的 goroutine gr1 處理。當 gr1 進行 I/O 操作時,Go 運行時調度器會將 gr1 變爲“等待”狀態並開始處理另一個 goroutine。I/O 操作完成後,gr1 變爲“就緒”狀態,Go 調度器將盡快恢復其運行。

Core2 也發生類似的情況。因此,如果我們看一個單一的核,我們會看到它和 Node 比較類似。goroutine 狀態的切換(從“運行”到“等待”再到“就緒”再到“運行”)由 Go 運行在後臺執行,並且代碼是簡單的按順序執行的語句流,這和 Node 所使用的基於回調的機制不同。

除了上述功能外,Go 還提供了一種基於通道和互斥鎖的非常簡單而強大的通信機制,該機制可以實現不同 goroutine 之間的平滑同步和調度。

然而不止併發

既然我們已經瞭解了 Node 和 Go 如何原生支持併發,那麼我們也可以看看其他的使用這兩種高效工具的原因。Node 可以用一種編程語言來實現前端和後端

Javascript/Typescript 在前端領域占主導地位。軟件商店中的前端軟件幾乎都大量使用 Javascript/Typescript 技術。

但是,如果您還需要構建後端怎麼辦?使用 Node,您可以利用相同的編程語言,相同的結構和相同的思想(異步編程)來構建後端。甚至在無服務架構中,Node 都扮演着核心角色,它是所有主要雲提供商爲其 FaaS 產品(函數即服務,如 AWS Lambda、Google Cloud 和 Azure functions)提供支持的第一個平臺。

前端和後端之間的輕鬆切換可能是 Node 取得令人難以置信的成功的原因之一,它產生了非常多的軟件包(你幾乎可以找到任何需要的 Node 庫)和超級活躍的社區。

同時,Node 並不能有效地支持所有類型的後端程序。例如,考慮到它的單線程性質,CPU 密集型的程序不適用於 Node。因此,你不應該陷入“一種語言適合所有情況”的陷阱。

在 Node 生態系統中有大量可用的軟件包,所以在使用時必須經常檢查所導入包的質量和安全性。但這在我們利用外部庫的任何時候都可能是正確的:生態系統越廣,越要關注質量和安全性。

在許多情況下可以選擇使用 Node,特別是對於 I/O 瓶頸的併發情形,使用 Node 還可以最大限度地利用你可能已經擁有的 Javascript/Typescript 技能。Go 重新發掘了簡單性和高性能

Go 很簡單。它只有 25 個保留字,語言規範爲 84 頁,其中還包括大量示例。作爲對比,Java 規範文檔第二版超過 350 頁。

簡單性使其易於學習。簡單性有助於編寫易於理解和維護的代碼。使用 Go 時,通常只有一種方法可以完成需要做的事情,這並不是要阻礙創造力,而是使閱讀和理解代碼變得更容易。

簡單性也產生一些限制。諸如泛型或異常之類的概念根本不在語言中,因爲作者並不認爲它們是必要的(即使泛型似乎將要出現)。另一方面,一些真正有用的工具(例如垃圾收集)則是核心設計的一部分。

Go 還涉及運行時性能和資源的有效利用。Go 是一種強類型的編譯型語言,可用於構建快速高效運行的程序,特別是在我們可以利用多核併發功能的情況下。

Go 還可以生成較小的自包含二進制文件。包含 Go 可執行文件的 docker 鏡像可能比包含相同 Java 程序的 docker 鏡像小得多,這是因爲 Java 需要 JVM 才能運行,而 Go 可執行文件是獨立的(根據基準測試,對於優化後的“Hello World”的 docker 鏡像,Java 8 程序爲 85 MB,而 Go 程序僅爲 7.6MB(一個數量級的差別)。鏡像大小很重要:它可以加快鏡像構建和拉取時間,降低網絡帶寬要求,並提高對安全性的控制。其他新技術

新產生的技術並非只有 Node 和 Go,近年來產生的其他技術有可能使傳統的單體應用受益。

Rust:這是一種開源語言,由 Mozilla 支持,該語言於 2010 年推出(比 Node 和 Go 早一年)。Rust 的主要目標是使用更安全的編程模型來優化 C/C++ 的性能,即更少地陷入令人討厭的運行時錯誤。

Rust 引入的新思維方式,特別是圍繞擁有 / 借用內存的理念,通常被認爲學習曲線十分陡峭。如果性能非常關鍵,那麼 Rust 絕對是一個值得考慮的選擇。

Kotlin:它是一種在 JVM 上運行的語言,該語言由 JetBrain 開發並於 2016 年發佈(2017 年 Google 宣佈支持 Kotlin 作爲 Android 的國家通用語言)。它比 Java 更爲簡潔,並且在原始設計概念中就包含如函數式編程和協程等,這使其成爲現代編程語言的一員。可以將其視爲 Java 的自然演變,對於有 Java 編程經驗的開發人員而言,其入門門檻較低。

GraalVM:這是一種針對 Java 和其他基於 JVM 的編程語言的有前途的新方法。GraalVM 原生映像(GraalVM Native Image)允許將 Java 代碼編譯爲本機可執行的二進制文件。這樣可以生成較小的映像,並在啓動和執行時均可提高性能。該技術還很年輕(於 2019 年發佈),目前顯示出一些侷限性。鑑於 Java 的廣泛使用,該技術可能會有重大改進並逐漸走向成熟。結論

軟件世界正在快速發展。新的解決方案經常出現。有些技術從來沒有走到最前列,有些經過一段時間的炒作,然後從主流技術中被淘汰。選擇合適的解決方案並不容易,而且需要在新平臺與經過實戰考驗的穩定性和專業性之間進行權衡。

事實證明,Node 和 Go 都是可用於企業級的技術。與傳統的面向對象的編程語言相比,它們有潛力帶來顯著的收益,特別是它們提高了容器化分佈式應用程序的效率。它們得到了強大的社區支持,並擁有廣泛的軟件生態。

儘管企業必須繼續使用和支持 Java 之類的傳統平臺——畢竟他們的生產代碼庫跨越的範圍太大了,但也強烈建議他們也開始擁抱相對較新的工具(例如 Node 和 Go),因爲這些新工具可以使他們能夠獲得現代分佈式計算的所有好處。

原文鏈接:

https://hackernoon.com/microservices-deserve-modern-programming-platforms-java-may-not-be-the-best-option-1v5z3tai

延伸閱讀:

一個微服務業務系統的中臺構建之路-InfoQ

30+微服務構建的頂級工具清單-InfoQ

Helidon項目教程:如何使用Oracle輕量級Java框架構建微服務-InfoQ

關注我並轉發此篇文章,私信我“領取資料”,即可免費獲得InfoQ價值4999元迷你書,點擊文末「瞭解更多」,即可移步InfoQ官網,獲取最新資訊~

相關文章