容器執行期介面串流說明

Kubernetes 容器執行期介面 (Container Runtime Interface, CRI) 作為 kubelet容器執行期 (Container Runtime) 之間的主要連線。這些執行期必須提供 gRPC 伺服器,該伺服器必須符合 Kubernetes 定義的 Protocol Buffer 介面。此 API 定義 隨著時間演進,例如當貢獻者新增功能或欄位即將被棄用時。

在這篇部落格文章中,我想深入探討三個非凡的遠端程序呼叫 (Remote Procedure Call, RPC) 的功能和歷史,它們在運作方式上確實非常出色:ExecAttachPortForward

Exec 可用於在容器內執行專用命令,並將輸出串流到用戶端,例如 kubectlcrictl。它也允許使用標準輸入 (stdin) 與該程序互動,例如,如果使用者想要在現有的工作負載中執行新的 Shell 實例。

Attach 透過 標準 I/O 將目前執行中程序的輸出從容器串流到用戶端,並允許與它們互動。如果使用者想要查看容器中正在發生的事情,並能夠與程序互動,這特別有用。

PortForward 可用於將主機的埠轉發到容器,以便能夠使用第三方網路工具與其互動。這允許它繞過特定工作負載的 Kubernetes 服務,並與其網路介面互動。

它們有什麼特別之處?

CRI 的所有 RPC 要么使用 gRPC 一元呼叫 進行通訊,要么使用 伺服器端串流 功能 (目前只有 GetContainerEvents)。這表示主要所有 RPC 都會擷取單一用戶端請求,並且必須傳回單一伺服器回應。ExecAttachPortForward 也是如此,它們的 協定定義 如下所示

// 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 具有 ExecAttachPortForward 的原生實作。在此之前,一切都綁定到 Docker 或後來被放棄的容器執行期 rkt

CRI 相關的設計文件也詳細說明了針對 exec、attach 和 port forward 使用原生 RPC 串流的選項。缺點超過了這種方法:kubelet 仍然會造成網路瓶頸,而且未來的執行期在選擇伺服器實作細節方面將不會自由。此外,Kubelet 實作可攜式、與執行期無關的解決方案的另一個選項已被最終方案放棄,因為這意味著另一個需要維護的專案,但它仍然會與執行期相關。

這表示,ExecAttachPortForward 的基本流程被提議如下所示

CRI Streaming flow

用戶端 (例如 crictl 或 kubelet (透過 kubectl)) 使用 gRPC 介面從執行期請求新的 exec、attach 或 port forward 工作階段。執行期實作串流伺服器,該伺服器也管理作用中的工作階段。此串流伺服器提供 HTTP 端點,供用戶端連線。用戶端升級連線以使用 SPDY 串流協定或 (在未來) WebSocket 連線,並開始來回串流資料。

此實作允許執行期彈性地以他們想要的方式實作 ExecAttachPortForward,並且也允許簡單的測試路徑。執行期可以變更底層實作以支援任何種類的功能,而完全無需修改 CRI。

過去幾年中,針對此整體方法的許多小型增強功能已合併到 Kubernetes 中,但一般模式始終保持不變。kubelet 原始碼轉換為 可重複使用的程式庫,如今可從容器執行期使用,以實作基本串流功能。

串流實際上如何運作?

乍看之下,這三個 RPC 似乎以相同的方式運作,但情況並非如此。可以將 ExecAttach 的功能分組,而 PortForward 遵循不同的內部協定定義。

Exec 和 Attach

Kubernetes 將 ExecAttach 定義為遠端命令,其協定定義存在於 五個不同版本

#版本注意
1channel.k8s.io初始 (未版本化) SPDY 子協定 (#13394#13395)
2v2.channel.k8s.io解決第一個版本中存在的問題 (#15961)
3v3.channel.k8s.io新增調整容器終端機大小的支援 (#25273)
4v4.channel.k8s.io新增使用 JSON 錯誤的結束代碼支援 (#26541)
5v5.channel.k8s.io新增 CLOSE 信號的支援 (#119157)

最重要的是,整體而言,有一項努力要使用 WebSocket 取代 SPDY 傳輸協定,作為 KEP #4006 的一部分。執行期必須在其生命週期內滿足這些協定,才能與 Kubernetes 實作保持同步。

讓我們假設用戶端使用最新 (v5) 版本的協定,以及透過 WebSocket 進行通訊。在這種情況下,一般流程將是

  1. 用戶端使用 CRI 請求 ExecAttach 的 URL 端點。

    • 伺服器 (執行期) 驗證請求,將其插入連線追蹤快取,並為該請求提供 HTTP 端點 URL。
  2. 用戶端連線到該 URL,升級連線以建立 WebSocket,並開始串流資料。

    • Attach 的情況下,伺服器必須將主要容器程序資料串流到用戶端。
    • Exec 的情況下,伺服器必須在容器內建立子程序命令,然後將輸出串流到用戶端。

    如果需要 stdin,則伺服器也需要監聽它,並將其重新導向到對應的程序。

解譯已定義協定的資料非常簡單:每個輸入和輸出封包的第一個位元組 定義 實際串流

第一個位元組類型描述
0標準輸入從 stdin 串流的資料
1標準輸出串流到 stdout 的資料
2標準錯誤串流到 stderr 的資料
3串流錯誤發生串流錯誤
4串流調整大小終端機調整大小事件
255串流關閉應關閉串流 (適用於 WebSocket)

執行期現在應如何使用提供的 kubelet 程式庫實作 ExecAttach 的串流伺服器方法?關鍵在於 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
}

與協定解譯相關的所有內容都已到位,執行期只需要實作實際的 ExecAttach 邏輯。例如,容器執行期 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 端點供用戶端連線,但接著容器執行期必須進入容器的網路命名空間、配置埠以及來回串流資料。沒有像 ExecAttach 那樣可用的簡單協定定義。這表示用戶端將串流純 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 ExecAttachPortForward 提供的彈性確實非常出色。然而,容器執行期必須跟上最新和最棒的實作,才能以有意義的方式支援這些功能。支援 WebSocket 的整體努力不僅僅是 Kubernetes 的事情,容器執行期以及 crictl 等用戶端也必須支援它。

例如,crictl v1.30 為子命令 execattachport-forward 新增了 --transport 旗標 (#1383#1385),以允許在 websocketspdy 之間進行選擇。

CRI-O 正嘗試一項實驗性路徑,將串流伺服器實作移至 conmon-rs (一個容器監控程式 conmon 的替代方案)。 conmon-rs 是原始容器監控程式的 Rust 實作,並允許直接使用支援的程式庫串流 WebSocket (#2070)。 此方法的 প্রধান 優勢在於,即使 CRI-O 沒有執行,conmon-rs 仍然可以保持 ExecAttachPortForward 工作階段處於活動狀態。 直接使用 crictl 時,簡化的流程將如下所示

sequenceDiagram autonumber participant crictl participant runtime as 容器執行期 participant conmon-rs Note over crictl,runtime: 容器執行期介面 (CRI) crictl->>runtime: Exec, Attach, PortForward Note over runtime,conmon-rs: Cap’n Proto runtime->>conmon-rs: 服務 Exec, Attach, PortForward conmon-rs->>runtime: HTTP 端點 (URL) runtime->>crictl: 回應 URL crictl-->>conmon-rs: 連線升級至 WebSocket conmon-rs-)crictl: 串流資料

所有這些增強功能都需要迭代式的設計決策,而最初構思完善的實作則作為這些決策的基礎。 我真心希望您喜歡這次簡短的 CRI RPC 歷史之旅。 歡迎隨時透過 官方 Kubernetes Slack 與我聯繫,提供建議或意見回饋。