使用 AppSwitch 繞過依賴順序問題
使用 AppSwitch 解決應用程式啟動順序和啟動延遲問題。
我們正在經歷一個有趣的應用程式分解和重組週期。雖然微服務範例正在驅使單體應用程式分解為獨立的服務,但服務網格方法正在協助將它們重新連接在一起,形成結構良好的應用程式。因此,微服務在邏輯上是分開的,但並非獨立的。它們通常是緊密相互依賴的,將它們分開會引入許多新的問題,例如服務之間相互身份驗證的需求。Istio 直接解決了大多數這些問題。
依賴順序問題
應用程式分解產生的一個問題,而 Istio 沒有解決的是依賴順序——以確保整體應用程式快速且正確啟動的順序啟動應用程式的各個服務。在一個單體應用程式中,所有元件都是內建的,元件之間的依賴順序由內部鎖定機制強制執行。但是,在服務網格中,各個服務可能分散在整個叢集中,首先啟動服務需要檢查其依賴的服務是否已啟動且可用。
依賴順序非常微妙,涉及許多相互關聯的問題。對各個服務進行排序需要有服務的依賴圖,以便可以從葉節點開始到根節點啟動它們。建立這樣一個圖並隨著時間推移,隨著應用程式行為的演變,保持更新並不容易。即使以某種方式提供了依賴圖,強制執行排序本身也不容易。僅僅按照指定的順序啟動服務顯然是行不通的。服務可能已經啟動,但尚未準備好接受連線。這是 docker-compose 的 depends-on
標籤的問題,例如。
除了在服務啟動之間引入足夠長的睡眠時間之外,常用的一種模式是在啟動服務之前檢查依賴項是否準備就緒。在 Kubernetes 中,這可以使用等待腳本作為 Pod 的 init 容器的一部分來完成。但是,這意味著整個應用程式將被擱置,直到其所有依賴項都啟動。有時,應用程式在建立第一個出站連線之前,會在啟動時花費數分鐘來初始化自身。不允許服務啟動會給應用程式的整體啟動時間增加相當大的開銷。此外,在 init 容器上等待的策略不適用於同一 Pod 內多個相互依賴的服務的情況。
範例情境:IBM WebSphere ND
讓我們考慮 IBM WebSphere ND (一種廣泛部署的應用程式中介軟體) 來更仔細地理解這些問題。它本身是一個相當複雜的框架,由一個稱為部署管理器 (dmgr
) 的中心元件組成,該元件管理一組節點執行個體。它使用 UDP 在節點之間協商叢集成員資格,並要求部署管理器在任何節點執行個體啟動並加入叢集之前啟動並執行。
為什麼我們在現代雲原生環境中談論傳統應用程式?事實證明,透過使它們能夠在 Kubernetes 和 Istio 平台上運行,可以獲得顯著的收益。從本質上講,它是現代化旅程的一部分,允許在同一個現代平台上同時運行傳統應用程式和綠地應用程式,以促進兩者之間的互操作。實際上,WebSphere ND 是一個要求嚴苛的應用程式。它需要具有特定網路介面屬性等的穩定網路環境。AppSwitch 可以滿足這些要求。然而,就本部落格而言,我將重點關注依賴順序要求以及 AppSwitch 如何解決它。
僅僅在 Kubernetes 叢集上將 dmgr
和節點執行個體部署為 Pod 是行不通的。 dmgr
和節點執行個體恰好有一個可能需要幾分鐘的漫長初始化過程。如果它們都是共同排程的,則應用程式通常會最終處於一種奇怪的狀態。當節點執行個體啟動並發現缺少 dmgr
時,它會採用替代的啟動路徑。相反,如果它立即退出,Kubernetes 崩潰循環將接管,也許應用程式就會啟動。但即使在這種情況下,也無法保證及時啟動。
一個 dmgr
連同其節點執行個體是 WebSphere ND 的基本部署設定。在生產環境中運行的 WebSphere ND 之上建置的應用程式 (如 IBM Business Process Manager) 包括其他多個服務。在這些設定中,可能存在相互依賴的鏈。根據節點執行個體託管的應用程式,它們之間也可能存在排序要求。由於服務初始化時間長和崩潰循環重新啟動,應用程式在任何合理的長度時間內啟動的可能性很小。
Istio 中的 Sidecar 依賴
Istio 本身也受到依賴順序問題的影響。由於進入和離開 Istio 下運行的服務的連線透過其 Sidecar Proxy 重新導向,因此應用程式服務及其 Sidecar 之間建立了隱含的依賴關係。除非 Sidecar 完全可操作,否則所有來自服務和發往服務的請求都會被丟棄。
AppSwitch 的依賴順序
那麼,我們該如何解決這些問題呢?一種方法是將其推遲到應用程式,並說它們應該是「行為良好」的,並實施適當的邏輯,使其能夠免疫啟動順序問題。但是,許多應用程式 (尤其是傳統應用程式) 如果順序錯誤,則會逾時或死鎖。即使對於新的應用程式,為每個服務實施一次性邏輯也是相當大的額外負擔,最好避免。服務網格需要圍繞這些問題提供足夠的支援。畢竟,將通用模式分解到基礎框架中確實是服務網格的重點。
AppSwitch 明確解決了依賴順序問題。它位於叢集中用戶端與服務之間應用程式網路交互的控制路徑上,並且確切地知道服務何時透過發出 connect
呼叫成為用戶端,以及特定的服務何時透過發出 listen
呼叫準備好接受連線。它的服務路由器元件會在叢集中傳播有關這些事件的資訊,並仲裁用戶端和伺服器之間的交互。這就是 AppSwitch 如何以簡單有效的方式實施負載平衡和隔離等功能。利用應用程式網路控制路徑的相同策略位置,可以想像,這些服務發出的 connect
和 listen
呼叫可以以更精細的粒度排隊,而不是按照依賴圖粗略地排序整個服務。這將有效地解決多層依賴問題並加快應用程式啟動速度。
但這仍然需要一個依賴圖。存在許多產品和工具可以幫助發現服務依賴關係。但它們通常基於對網路流量的被動監視,並且無法提前為任何任意應用程式提供資訊。由於加密和通道造成的網路級混淆也使它們不可靠。發現和指定依賴關係的責任最終落到應用程式的開發人員或操作人員身上。事實上,即使一致性檢查依賴規格本身也很複雜,任何避免要求依賴圖的方法都是最理想的。
依賴圖的重點是知道哪些用戶端依賴於特定服務,以便可以使這些用戶端等待相應的服務啟動。但是,具體是哪些用戶端真的重要嗎?最終,一個始終成立的同義反覆運算是,服務的所有用戶端都對該服務具有隱含的依賴關係。這就是 AppSwitch 用來繞過此要求的方式。事實上,這完全繞過了依賴順序。應用程式的所有服務都可以共同排程,而無需考慮任何啟動順序。它們之間的相互依賴性會在個別請求和回應的粒度級別自動解決,從而實現快速且正確的應用程式啟動。
AppSwitch 模型和建構
現在我們對 AppSwitch 的高階方法有了概念性的了解,讓我們看看涉及的建構。但首先,快速總結一下使用模型是必要的。儘管它是為不同的環境編寫的,但查看我之前關於此主題的部落格也很有用。為了完整起見,我還要指出 AppSwitch 不會考慮非網路依賴關係。例如,兩個服務可以使用 IPC 機制或透過共用檔案系統進行交互。像這樣緊密聯繫的流程通常是同一個服務的一部分,不需要框架介入來進行排序。
AppSwitch 的核心建立在一個允許檢測 BSD Socket API 和其他相關呼叫 (如處理 Socket 的 fcntl
和 ioctl
) 的機制上。儘管其實施細節很有趣,但它會分散我們對主要主題的注意力,因此我只想總結一下它與其他實施不同的關鍵特性。(1) 它速度很快。它結合使用 seccomp
過濾和二進位檢測,以積極地限制干預應用程式的正常執行。AppSwitch 特別適用於服務網格和應用程式網路使用案例,因為它實施這些功能而無需實際接觸資料。相反,網路級方法會產生每個封包的成本。請查看此部落格以了解一些效能測量。(2) 它不需要任何核心支援、核心模組或修補程式,並且可在標準發行版核心上執行。(3) 它可以作為一般使用者執行 (無 root)。事實上,該機制甚至可以透過移除網路容器的 root 要求,使Docker 精靈在沒有 root 的情況下運行。(4) 它不需要對應用程式進行任何變更,並且適用於任何類型的應用程式 - 從 WebSphere ND 和 SAP 到自訂 C 應用程式到靜態連結的 Go 應用程式。目前唯一的要求是 Linux/x86。
將服務與其參考解耦
AppSwitch 的建構基於一個基本前提,即應用程式應與其參考解耦。傳統上,應用程式的識別是從它們運行的主機的識別中衍生而來的。然而,應用程式和主機是非常不同的對象,需要獨立引用。關於這個主題的詳細討論以及 AppSwitch 的概念基礎,請參閱這篇研究論文。
AppSwitch 用於實現服務物件與其識別之間解耦的核心構造是服務參考(簡稱參考)。AppSwitch 根據上述的 API 檢測機制實現服務參考。服務參考包含一個 IP:port 對(可選地還有一個 DNS 名稱)以及一個標籤選擇器,用於選擇該參考所代表的服務以及此參考適用於哪些客戶端。參考支援幾個關鍵屬性。(1) 它可以獨立於它所指的物件的名稱命名。也就是說,一個服務可能正在一個 IP 和端口上監聽,但參考允許用戶選擇任何其他 IP 和端口來訪問該服務。這使得 AppSwitch 可以運行從其原始環境捕獲的傳統應用程式,這些應用程式具有靜態 IP 設定,透過提供必要的 IP 位址和端口,使其可以在 Kubernetes 上運行,而與目標網路環境無關。(2) 即使目標服務的位置發生變化,它仍然保持不變。當參考的標籤選擇器解析到服務的新實例時,它會自動重新導向。(3) 對於本次討論最重要的一點,即使目標服務正在啟動,參考仍然有效。
為了方便發現可以透過服務參考訪問的服務,AppSwitch 提供了一個自動策劃的服務註冊表。隨著服務在集群中來來去去,該註冊表會根據 AppSwitch 跟蹤的網路 API 自動保持更新。註冊表中的每個條目都包含相應服務綁定的 IP 和端口。此外,它還包括一組標籤,指示該服務所屬的應用程式、應用程式在建立服務時透過 Socket API 傳遞的 IP 和端口,以及 AppSwitch 代表應用程式在底層主機上實際綁定服務的 IP 和端口等。此外,在 AppSwitch 下建立的應用程式帶有一組使用者傳遞的標籤,這些標籤描述了應用程式以及一些預設的系統標籤,這些標籤指示了建立應用程式的使用者以及應用程式運行的主機等。這些標籤都可以表示在服務參考所攜帶的標籤選擇器中。可以透過建立服務參考來使註冊表中的服務可供客戶端訪問。然後,客戶端將能夠以參考的名稱 (IP:port) 訪問該服務。現在讓我們看看 AppSwitch 如何保證即使在目標服務尚未啟動時,參考仍然有效。
非阻塞請求
AppSwitch 利用 BSD Socket API 的語義來確保從客戶端的角度來看,當相應的服務啟動時,服務參考看起來是有效的。當客戶端向尚未啟動的另一個服務發出阻塞的連接呼叫時,AppSwitch 會阻塞該呼叫一段時間,等待目標服務上線。由於已知目標服務是應用程式的一部分並且預計很快會啟動,因此使客戶端阻塞而不是返回諸如 ECONNREFUSED
之類的錯誤可以防止應用程式啟動失敗。如果服務在時間內沒有啟動,則會向應用程式返回錯誤,以便 Kubernetes 崩潰循環等框架級別的機制可以啟動。
如果客戶端請求標記為非阻塞,AppSwitch 會透過返回 EAGAIN
來處理該請求,以通知應用程式重試而不是放棄。再次強調,這符合 Socket API 的語義,並防止因啟動競爭而導致的故障。AppSwitch 本質上使應用程式中已經內建的重試邏輯能夠透明地重新用於支援 BSD Socket API,以用於依賴項排序。
應用程式超時
如果應用程式根據其自己的內部計時器超時會怎麼樣?事實上,如果需要,AppSwitch 也可以偽造應用程式對時間的感知,但這將是過分的,而且實際上是不必要的。應用程式決定並最清楚它應該等待多長時間,AppSwitch 不適合干預。應用程式的超時時間是保守地設定得很長,如果目標服務在時間內仍然沒有啟動,則不太可能是依賴項排序問題。一定有其他問題正在發生,不應該被掩蓋。
用於 Sidecar 依賴的萬用字元服務參考
服務參考可以用於解決先前提到過的 Istio Sidecar 依賴問題。AppSwitch 允許指定為服務參考一部分的 IP:port 為萬用字元。也就是說,服務參考 IP 位址可以是表示要捕獲的 IP 位址範圍的網路遮罩。如果服務參考的標籤選擇器指向 Sidecar 服務,則應用此服務參考的任何應用程式的所有傳出連線都將被透明地重新導向到 Sidecar。當然,當 Sidecar 仍在啟動時,服務參考仍然有效,並且消除了競爭。
使用服務參考進行 Sidecar 依賴排序也隱式地將應用程式的連線重新導向到 Sidecar,而無需 iptables 和隨之而來的權限問題。本質上,它的工作方式就像應用程式直接連線到 Sidecar 而不是目標目的地,讓 Sidecar 負責處理。AppSwitch 將使用 Sidecar 可以在傳遞連線到應用程式之前解碼的 Proxy Protocol,將有關原始目的地等的元數據插入到連線的數據流中。其中一些細節在這裡討論過。這處理了傳出連線,但傳入連線呢?由於所有服務及其 Sidecar 都在 AppSwitch 下運行,因此任何來自遠端節點的傳入連線都將重新導向到它們各自的遠端 Sidecar。因此,對於傳入連線沒有什麼特別需要做的。
總結
依賴項排序是一個棘手的問題。這主要是由於缺乏對服務間互動周圍細粒度應用程式級別事件的訪問。解決此問題通常需要應用程式實現它們自己的內部邏輯。但是 AppSwitch 使這些內部應用程式事件能夠被檢測,而無需應用程式變更。然後,AppSwitch 利用對 BSD Socket API 的廣泛支援,來迴避排序依賴項的要求。
致謝
感謝 Eric Herness 及其團隊在我們將 IBM WebSphere 和 BPM 產品現代化到 Kubernetes 平台時提供的見解和支持,以及感謝 Mandar Jog、Martin Taillefer 和 Shriram Rajagopalan 審閱了此博客的早期草稿。