模塊、單體和微服務

作者 | Avery Pennarun

譯者 | 平川

策劃 | 萬佳

最近,有人問我在什麼情況下使用微服務是個好主意。在 “系統設計詮釋世界” 一文中,我談到了像第二系統效應、創新者困境等宏觀問題。系統設計能回答微服務的問題嗎?

https://apenwarr.ca/log/20201227

是的,但你可能不喜歡這個答案。首先,我們需要了解一些歷史資料。

1 什麼是微服務?

你可以在網上找到各種各樣的定義。我的觀點是:微服務是對單體的最極端的抵制。

當你將整個應用程序所需要的全部東西鏈接成一個很大的程序,並將其作爲一個大型二進制包部署時,就會發生這樣的情況。單體有很長的歷史,可以追溯到像 CGI、Django、Rails 和 PHP 這樣的框架。

現在,我們要放棄這樣的假設:單體和微服務羣是僅有的兩個選項。在 “一個無所不能的巨型服務” 和“無數個幾乎什麼都不做的微型服務”之間,存在着一個廣泛而微妙的連續體。

如果你趕時髦,你至少有那麼一次已經構建了一個單體(不管是有意的,還是因爲傳統框架鼓勵你這麼做),後來發現了單體存在的一些問題,然後,你聽說微服務就是自己需要的答案,於是開始把一切都重新設計成微服務。

但是,不要趕時髦。在這兩個極端之間,還有許多點。其中一個可能很適合你。更好的方法是從你想要將接口放在哪裏開始。

2 方框和箭頭

接口是模塊之間的連接。模塊是相關代碼的集合。在系統設計中,我們討論 “方框和箭頭” 設計:模塊是方框,接口是箭頭。

更深層次的問題是:每個方框要多大?裏面放多少東西?我們如何決定什麼時候把一個大方框分成兩個小方框?連接這些方框最好的方法是什麼?有很多方法可以做到這一點。沒人知道什麼是最好的。這是軟件架構中最困難的問題之一。

在過去幾十年裏,我們經歷了許多種 “方框”。Goto 語句被 “認爲是有害的”,主要是因爲它們完全忽視了任何層次結構。然後我們添加了函數或過程;這些都是非常簡單的方框,它們之間有接口(參數和返回碼)。

https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf

根據你選擇的編程路線,你會了解到遞歸函數、選擇符(combinator)、靜態函數原型、庫(靜態鏈接或運行時鏈接)、對象(OOP)、協程、受保護的虛擬內存、進程、線程、JIT、命名空間、沙盒、chroot、Jails、容器、虛擬機、supervisor、hypervisor、微內核和 unikernel。

https://en.wikipedia.org/wiki/Unikernel

這還只是方框!一旦有了彼此隔離的方框,就需要用箭頭連接它們。爲此,我們有 ABI、API、系統調用、套接字、RPC、文件系統、數據庫、消息傳遞系統和 “虛擬化硬件”。

如果你想爲現代 Unix 系統畫一個完整的方框和箭頭圖(我不會這樣做),那太瘋狂了:函數在線程內,線程在進程內,進程在容器內,容器在用戶空間內,下層是內核,內核在 VM 裏,運行在雲提供商數據中心裏機架上的一臺硬件上,而這些硬件是通過一個編排系統連接在一起的,等等。

每個抽象層上的每個方框都以某種方式隔離開來,然後連接到同層或其他層上的其他方框。有些方框在其他方框內部。如果你想在二維空間中畫出這幅畫的真實版本,那麼其中的線條肯定縱橫交錯。

這一切都是經過幾十年的演變發展而來的。時髦的人稱之爲 “路徑依賴”。我稱之爲一個爛攤子。我們要清楚:其中的大部分已經沒有多少價值。

與其把注意力集中在那些醜陋的進化結果上,我們不妨來談談,人們在發明這些東西的時候想要達成什麼目標。

3 對模塊化的探索

模塊系統的首要目標是下面這幾個:

計算機行業花費了大量時間,試圖找出所有這些模塊化問題的完美平衡,同時保證開發過程儘可能的輕鬆愉快。

一句話,我們沒有成功。

到目前爲止,我們做得最糟糕的一個方面是隔離。如果我們能真正有效地將一部分代碼同其他部分隔離開來,那麼其他的目標也就基本可以實現了。但我們根本不知道如何做到這一點。

隔離是一個超級困難的問題。天曉得,人們已經盡力了。但瀏覽器沙箱逃逸仍然經常發生,未被發現的特權升級攻擊在每一個操作系統上都存在,iOS 越獄仍然會週期性地發生,DRM 從來就無效(無論好壞),虛擬機和容器會經常被發現有漏洞,而 [在像 k8 這樣的系統中,其容器的配置默認就是不安全的。

人們甚至已經知道,通過在因特網上適時地向遠程服務器發送數據包,就可以找到加密密鑰。與此同時,近年來,最驚人的隔離事故是 Meltdown 和 Spectre 攻擊,它允許計算機上的任何程序,甚至是 Web 瀏覽器中的 JavaScript 應用程序,讀取同一臺計算機上其他程序的內存,甚至是跨沙盒或虛擬機。

每一種新的隔離技術都會經歷一個從樂觀到絕望的週期:

舉例來說,在這一點上,安全人員根本不相信以下任何一項(每一項都是當時最好的技術)是絕對安全的:

據我所知,目前最先進的隔離技術,是類似於 Chrome 沙盒或 gVisor 這樣的東西。大型瀏覽器供應商和雲計算提供商都使用這樣的工具。這些工具也不完美,但是供應商確實在儘可能快地追蹤每一個新出現的漏洞,而且新漏洞出現的速度相當緩慢。

隔離比以往任何時候都要好…… 如果你把所有的隔離都放在虛擬機(VM)級別,那麼雲提供商就可以幫你完成,因爲其他人不知道怎麼做,或者無法足夠頻繁地更新。

如果你信任雲提供商的 VM 隔離,那麼就可以希望所有已知的問題都得到了緩解;但我們有充分的理由認爲,更多的問題將被發現。

從各方面考慮,這其實很不錯。至少我們還有有效的東西。

4 很好!都用虛擬機!

等一等。爲每個小模塊創建一個孤立的 VM 是一件很痛苦的事情。一個模塊該多大?

很久以前,當 Java 第一次出現時,人們的夢想是每個對象中的每個函數的每一行都有嚴格的權限限制,甚至是在相同應用程序二進制文件中的對象之間,這樣就不需要 CPU 強制施加的內存保護。沒人再相信他們能在這方面取得成功了。除了類似 “雲函數” 這樣的市場宣傳,沒有人真的認爲你應該嘗試一下。

目前,已知的隔離方法中沒有一種是完美的,但每一種都能達到某種近似的效果。越來越有經驗的攻擊者,或者越來越有價值的目標,需要更好同時也更惱人的隔離。現在,我們所知道的最好的隔離是由一級雲提供商提供的 inter-VM 沙箱。在最壞的情況下,它的隔離效果會降到零。

假如驗證被跳過,由於大多數系統的耦合是如此之緊密,一個相當有經驗的攻擊者可以在模塊之間橫向突破。因此,舉例來說,如果有人可以將惡意庫鏈接到你的 Go 或 C++ 程序中,那麼他們可能會控制整個程序。

類似地,如果程序具有數據庫的寫入權限,那麼攻擊者可能會讓它寫入數據庫中的任何地方。如果它可以連接到網絡,那麼他們就可能連接到網絡中的任何地方。如果它可以執行任意的 Unix 命令或系統調用,那麼他們就可能獲得 Unix 根訪問權限。如果它在一個容器裏,那麼他們可能會從容器中掙脫出來,進入其他容器。如果惡意數據可以使 png 解碼器崩潰,那麼他們就可能讓它做解碼器程序可以做的任何事情,等等。

https://imagetragick.com/

一種特別強大的攻擊形式是獲得提交代碼的能力,因爲這些代碼最終將在開發人員的機器上運行,而某些開發人員或某處的生產機器可能有權執行你想要執行的操作。

以上說法可能有點過於悲觀,但是做出這些假設,可以幫助我們避免在不提高實際安全性的情況下使系統變得過於複雜。在”qmail 1.0 十年之際關於安全的一些思考 “一文中,Daniel J. Bernstein 指出,在 qmail 中添加的許多防禦措施,特別是使用 chroot 和不同的 Unix uid 隔離各個不同的組件,並不值得,而且從未得到回報。

無論如何,我們可以理所當然地認爲,具有代碼執行能力的攻擊者 “通常” 可以在耦合模塊之間橫向跳轉,這幾乎適用於任何一種模塊隔離技術。這意味着只有兩種模塊邊界:

我在這裏並沒有說什麼非常深刻的東西。現代流行的平臺已經是圍繞這一區別構建出來的。

例如,Chrome 在強隔離的沙箱虛擬機中運行任意的 Web JavaScript,因爲網頁是不可信的。

大多數操作系統僅僅以進程(沒有沙箱)的形式運行原生應用,共享文件系統、網絡名稱空間等,因爲我們曾經認爲,它們是相對可信的。(病毒就是這樣產生的。)

專家們不再信任多用戶 Unix 系統,因爲結果證明進程隔離很脆弱。雲虛擬機默認可以無密碼 sudo,因爲 root 與非 root 隔離都被證明很脆弱,所以爲什麼還要麻煩呢。

(我們仍然要求用戶在刪除所有文件或其他東西時輸入 sudo,以減少人爲錯誤的影響。)

來自多個供應商的共享庫和 DLL 被鏈接到來自其他供應商的應用程序中,因爲所有代碼都被假定是可信的。(這爲通過開源庫供應商進行供應鏈攻擊打開了方便之門。我仍然感到驚訝的是,這類攻擊並不經常發生。我有時懷疑,也許它們確實存在,只是很少被發現而已。)

手機操作系統之所以會被破解,是因爲應用商店的限制應該會讓應用沙盒足夠可信,但這種隔離總是被證明太脆弱。

Kubernetes 和 Docker 在一臺機器或 VM 中運行多個不完全隔離的容器,因爲這些容器都被默認爲可信的。強烈建議你不要嘗試運行 “多租戶”Kubernetes 集羣(不可信的應用程序代表獨立的、相互不信任的用戶),因爲事實證明,容器的隔離效果很弱。

還有,即使你對每個服務使用像 gVisor'd VM 那樣的強隔離,如果代碼本身不是使用強隔離的工具鏈構建的,也不會有什麼幫助。如果一組人可以更新一個庫,然後鏈接到一組應用程序,那麼這些應用程序並不算是真正的相互隔離,無論它們以什麼方式運行。

5 模塊邊界 vs 服務邊界

如果這麼多隔離層都很脆弱,那麼我們爲什麼還要費心使用它們呢?

主要是歷史沿革;如果我們拋棄這些層中的大部分,安全性不會受到太大影響,而簡潔性將得到提升。我預計,隨着時間的推移,這會發生。我們已經看到了這種趨勢。多用戶 Unix 系統已幾乎絕跡;“無服務器” 服務器放棄了除最強隔離類型之外的所有隔離類型,並試圖將你鎖定在你所在的雲提供商那裏。

但是,讓我們把歷史放在一邊。我必須介紹所有這些隔離概念,這樣我才能說得簡單點:你幾乎從不出於安全原因定義模塊邊界。

相反,模塊邊界通常遵循康威定律。人們分解模塊的根據是他們希望如何細分團隊中的開發工作,而模塊之間的通信則取決於團隊和團隊成員之間的溝通方式。(康威定律很吸引人,也很真實,但你可以在很多其他地方讀到它。)

https://en.wikipedia.org/wiki/Conway%27s_law

模塊邊界並不能定義部署單元的大小。

以操作系統爲例:

(人們在開 “桌面 Linux” 不可靠的玩笑時,談論的總是第二種小衆而難以測試的類型,而不是第一種主流的、容易測試的類型。我不認爲人們感知到的質量差異實際上是由企業資金與開源差異所導致的。不同之處在於部署模型。)

兩個系統都包含許多程序包(模塊),這些包是由不同團隊的許多開發人員開發的。它們的模塊之間都有接口。如果你爲每個系統畫一個方框和箭頭圖,可能看起來會非常類似:內核、驅動程序、窗口系統、沙箱、Web 瀏覽器,等等。

然而,如果是後端雲服務而不是操作系統,我們將分別稱這兩個模型爲單體和微服務,因爲它們的部署模型。一個只有一個要部署的 “服務”,而另一個有許多,每個都要單獨部署。相同的模塊架構!這是怎麼回事呢?

模塊邊界和服務邊界是兩個不同的東西。

6 服務的邊界應該在哪裏?

讓我們回顧一下最初的模塊化目標:

  1. 隔離:如果出於安全考慮,確實需要強隔離,如果你需要單獨的服務,那麼唯一的方法就是分別提供虛擬機。(不過請注意:這更多的是隔離系統的限制,而非架構目標。“基礎設施即代碼” 和藍 / 綠部署會設法讓這些服務再次同步,所以你可以有一個單體式的部署模型。)

  2. 連接:遵循康威定律。模塊邊界傾向於遵循團隊的個性化溝通模式。但與直覺相反,康威定律並不需要定義服務邊界。

  3. 兼容性保證:迫使你轉向單體。如果你的單體是用一種類型安全的語言編寫的,比如 Go、TypeScript、Rust,甚至是 C++,則尤其如此。(例如,Chrome 就是一個巨大的二進制文件。)

  4. 升級、降級和可擴展性:這些是決定服務邊界的主要因素。關於這一點,讓我們再進一步探討下。

以下是選擇服務邊界時需要考慮的一些事項:

事實上,在服務之間創建邊界時,上面的大部分理由都不是非常令人信服。它們是劃分模塊或團隊邊界的好理由!但你可以在將模塊重新組合成一個或幾個單體後推出它們。

記住,ChromeOS 是個單體,iOS 也是個單體。你的團隊可能比這兩個團隊都小得多。你根本不需要爲了得到你想要的東西而在大量的微服務上做文章。用簡單的方法設計系統,直到你不得不用困難的方法。這就是我們的工作。

英文原文:

https://tailscale.com/blog/modules-monoliths-and-microservices/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/aZ59uGcLv2SRTvGQRNdAeQ