容器執行期介面串流說明
Kubernetes 容器執行期介面 (Container Runtime Interface, CRI) 作為 kubelet 和 容器執行期 (Container Runtime) 之間的主要連線。這些執行期必須提供 gRPC 伺服器,該伺服器必須符合 Kubernetes 定義的 Protocol Buffer 介面。此 API 定義 隨著時間演進,例如當貢獻者新增功能或欄位即將被棄用時。
在這篇部落格文章中,我想深入探討三個非凡的遠端程序呼叫 (Remote Procedure Call, RPC) 的功能和歷史,它們在運作方式上確實非常出色:Exec
、Attach
和 PortForward
。
Exec 可用於在容器內執行專用命令,並將輸出串流到用戶端,例如 kubectl 或 crictl。它也允許使用標準輸入 (stdin) 與該程序互動,例如,如果使用者想要在現有的工作負載中執行新的 Shell 實例。
Attach 透過 標準 I/O 將目前執行中程序的輸出從容器串流到用戶端,並允許與它們互動。如果使用者想要查看容器中正在發生的事情,並能夠與程序互動,這特別有用。
PortForward 可用於將主機的埠轉發到容器,以便能夠使用第三方網路工具與其互動。這允許它繞過特定工作負載的 Kubernetes 服務,並與其網路介面互動。
它們有什麼特別之處?
CRI 的所有 RPC 要么使用 gRPC 一元呼叫 進行通訊,要么使用 伺服器端串流 功能 (目前只有 GetContainerEvents
)。這表示主要所有 RPC 都會擷取單一用戶端請求,並且必須傳回單一伺服器回應。Exec
、Attach
和 PortForward
也是如此,它們的 協定定義 如下所示
// Exec prepares a streaming endpoint to execute a command in the container.
rpc Exec(ExecRequest) returns (ExecResponse) {}
// Attach prepares a streaming endpoint to attach to a running container.
rpc Attach(AttachRequest) returns (AttachResponse) {}
// PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
請求攜帶伺服器執行工作所需的一切資訊,例如,ContainerId
或命令 (Cmd
) (在 Exec
的情況下) 以供執行。更有趣的是,它們的所有回應都只包含一個 url
message ExecResponse {
// Fully qualified URL of the exec streaming server.
string url = 1;
}
message AttachResponse {
// Fully qualified URL of the attach streaming server.
string url = 1;
}
message PortForwardResponse {
// Fully qualified URL of the port-forward streaming server.
string url = 1;
}
為何以這種方式實作?嗯,這些 RPC 的原始設計文件 甚至早於 Kubernetes 增強提案 (KEP),最初在 2016 年概述。在將功能引入 CRI 的倡議開始之前,kubelet 具有 Exec
、Attach
和 PortForward
的原生實作。在此之前,一切都綁定到 Docker 或後來被放棄的容器執行期 rkt。
CRI 相關的設計文件也詳細說明了針對 exec、attach 和 port forward 使用原生 RPC 串流的選項。缺點超過了這種方法:kubelet 仍然會造成網路瓶頸,而且未來的執行期在選擇伺服器實作細節方面將不會自由。此外,Kubelet 實作可攜式、與執行期無關的解決方案的另一個選項已被最終方案放棄,因為這意味著另一個需要維護的專案,但它仍然會與執行期相關。
這表示,Exec
、Attach
和 PortForward
的基本流程被提議如下所示
用戶端 (例如 crictl 或 kubelet (透過 kubectl)) 使用 gRPC 介面從執行期請求新的 exec、attach 或 port forward 工作階段。執行期實作串流伺服器,該伺服器也管理作用中的工作階段。此串流伺服器提供 HTTP 端點,供用戶端連線。用戶端升級連線以使用 SPDY 串流協定或 (在未來) WebSocket 連線,並開始來回串流資料。
此實作允許執行期彈性地以他們想要的方式實作 Exec
、Attach
和 PortForward
,並且也允許簡單的測試路徑。執行期可以變更底層實作以支援任何種類的功能,而完全無需修改 CRI。
過去幾年中,針對此整體方法的許多小型增強功能已合併到 Kubernetes 中,但一般模式始終保持不變。kubelet 原始碼轉換為 可重複使用的程式庫,如今可從容器執行期使用,以實作基本串流功能。
串流實際上如何運作?
乍看之下,這三個 RPC 似乎以相同的方式運作,但情況並非如此。可以將 Exec 和 Attach 的功能分組,而 PortForward 遵循不同的內部協定定義。
Exec 和 Attach
Kubernetes 將 Exec 和 Attach 定義為遠端命令,其協定定義存在於 五個不同版本 中
# | 版本 | 注意 |
---|---|---|
1 | channel.k8s.io | 初始 (未版本化) SPDY 子協定 (#13394、#13395) |
2 | v2.channel.k8s.io | 解決第一個版本中存在的問題 (#15961) |
3 | v3.channel.k8s.io | 新增調整容器終端機大小的支援 (#25273) |
4 | v4.channel.k8s.io | 新增使用 JSON 錯誤的結束代碼支援 (#26541) |
5 | v5.channel.k8s.io | 新增 CLOSE 信號的支援 (#119157) |
最重要的是,整體而言,有一項努力要使用 WebSocket 取代 SPDY 傳輸協定,作為 KEP #4006 的一部分。執行期必須在其生命週期內滿足這些協定,才能與 Kubernetes 實作保持同步。
讓我們假設用戶端使用最新 (v5
) 版本的協定,以及透過 WebSocket 進行通訊。在這種情況下,一般流程將是
用戶端使用 CRI 請求 Exec 或 Attach 的 URL 端點。
- 伺服器 (執行期) 驗證請求,將其插入連線追蹤快取,並為該請求提供 HTTP 端點 URL。
用戶端連線到該 URL,升級連線以建立 WebSocket,並開始串流資料。
- 在 Attach 的情況下,伺服器必須將主要容器程序資料串流到用戶端。
- 在 Exec 的情況下,伺服器必須在容器內建立子程序命令,然後將輸出串流到用戶端。
如果需要 stdin,則伺服器也需要監聽它,並將其重新導向到對應的程序。
解譯已定義協定的資料非常簡單:每個輸入和輸出封包的第一個位元組 定義 實際串流
第一個位元組 | 類型 | 描述 |
---|---|---|
0 | 標準輸入 | 從 stdin 串流的資料 |
1 | 標準輸出 | 串流到 stdout 的資料 |
2 | 標準錯誤 | 串流到 stderr 的資料 |
3 | 串流錯誤 | 發生串流錯誤 |
4 | 串流調整大小 | 終端機調整大小事件 |
255 | 串流關閉 | 應關閉串流 (適用於 WebSocket) |
執行期現在應如何使用提供的 kubelet 程式庫實作 Exec 和 Attach 的串流伺服器方法?關鍵在於 kubelet 中的串流伺服器實作 概述了一個介面,稱為 Runtime
,如果實際容器執行期想要使用該程式庫,則必須滿足該介面
// Runtime is the interface to execute the commands and provide the streams.
type Runtime interface {
Exec(ctx context.Context, containerID string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
Attach(ctx context.Context, containerID string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error
PortForward(ctx context.Context, podSandboxID string, port int32, stream io.ReadWriteCloser) error
}
與協定解譯相關的所有內容都已到位,執行期只需要實作實際的 Exec
和 Attach
邏輯。例如,容器執行期 CRI-O 像這樣的虛擬碼 執行此操作
func (s StreamService) Exec(
ctx context.Context,
containerID string,
cmd []string,
stdin io.Reader, stdout, stderr io.WriteCloser,
tty bool,
resizeChan <-chan remotecommand.TerminalSize,
) error {
// Retrieve the container by the provided containerID
// …
// Update the container status and verify that the workload is running
// …
// Execute the command and stream the data
return s.runtimeServer.Runtime().ExecContainer(
s.ctx, c, cmd, stdin, stdout, stderr, tty, resizeChan,
)
}
PortForward
與從工作負載串流 IO 資料相比,將埠轉發到容器的工作方式略有不同。伺服器仍然必須提供 URL 端點供用戶端連線,但接著容器執行期必須進入容器的網路命名空間、配置埠以及來回串流資料。沒有像 Exec 或 Attach 那樣可用的簡單協定定義。這表示用戶端將串流純 SPDY 框架 (具有或不具有額外的 WebSocket 連線),可以使用 moby/spdystream 等程式庫進行解譯。
幸運的是,kubelet 程式庫已經提供 PortForward
介面方法,該方法必須由執行期實作。CRI-O 透過 (簡化) 方式執行此操作
func (s StreamService) PortForward(
ctx context.Context,
podSandboxID string,
port int32,
stream io.ReadWriteCloser,
) error {
// Retrieve the pod sandbox by the provided podSandboxID
sandboxID, err := s.runtimeServer.PodIDIndex().Get(podSandboxID)
sb := s.runtimeServer.GetSandbox(sandboxID)
// …
// Get the network namespace path on disk for that sandbox
netNsPath := sb.NetNsPath()
// …
// Enter the network namespace and stream the data
return s.runtimeServer.Runtime().PortForwardContainer(
ctx, sb.InfraContainer(), netNsPath, port, stream,
)
}
未來工作
與其他方法相比,Kubernetes 為 RPC Exec
、Attach
和 PortForward
提供的彈性確實非常出色。然而,容器執行期必須跟上最新和最棒的實作,才能以有意義的方式支援這些功能。支援 WebSocket 的整體努力不僅僅是 Kubernetes 的事情,容器執行期以及 crictl
等用戶端也必須支援它。
例如,crictl
v1.30 為子命令 exec
、attach
和 port-forward
新增了 --transport
旗標 (#1383、#1385),以允許在 websocket
和 spdy
之間進行選擇。
CRI-O 正嘗試一項實驗性路徑,將串流伺服器實作移至 conmon-rs (一個容器監控程式 conmon 的替代方案)。 conmon-rs 是原始容器監控程式的 Rust 實作,並允許直接使用支援的程式庫串流 WebSocket (#2070)。 此方法的 প্রধান 優勢在於,即使 CRI-O 沒有執行,conmon-rs 仍然可以保持 Exec、Attach 和 PortForward 工作階段處於活動狀態。 直接使用 crictl 時,簡化的流程將如下所示
所有這些增強功能都需要迭代式的設計決策,而最初構思完善的實作則作為這些決策的基礎。 我真心希望您喜歡這次簡短的 CRI RPC 歷史之旅。 歡迎隨時透過 官方 Kubernetes Slack 與我聯繫,提供建議或意見回饋。