搭建異構 CPU 組成的邊緣計算 Kubernetes 叢集

Wi1dcard發表於2020-04-26

平時除了維護公司和私人在公有云的 Kubernetes clusters 之外,個人網路環境下還有些需要執行在本地的 workload;比如用於監控本地路由裝置( XD)的 Prometheus exporters 和一些新奇玩意兒。為了能夠執行這些應用,我在家組建了一套「邊緣計算叢集」,來看看是怎麼做的吧。

硬體準備

我手頭上目前有一臺 Raspberry Pi 3 B+,我想使用它作為 Master 節點:

Server

和兩塊 Nanopi NEO 2

Server

搭建叢集

K3s

為了能夠適應較低的計算效能,我選擇了使用 K3s 部署 Kubernetes 叢集。K3s 是一款 Rancher 開源的輕量 Kubernetes 實現,主要目標為物聯網和邊緣計算等場景。

如果你是在搭建測試叢集,不妨試試 Minikube 和 MicroK8s,它們能夠提供更加接近生產環境叢集的體驗。

不同於以上兩款產品,K3s 除了更加輕量外,還支援多節點,因此比較符合我的使用場景。

以上產品的詳細對比可參考 這篇帖子

K3sup

K3sup 是由 OpenFaaS 的創始人 Alex Ellis 開發的一款小工具,可用於快速部署 K3s 節點,例如部署 master node:

k3sup install --ip "$MASTER" --user pi

執行以上命令,K3sup 將以使用者 pi 的身份透過 SSH 連線 $MASTER(也就是作為 Master 節點的樹莓派 IP 地址),在下載並安裝 K3s 後,K3sup 會將生成的 Kubeconfig 從遠端拉取到本地的工作目錄中。

接下來部署 worker node:

k3sup join --ip "$WORKER" --server-ip "$MASTER" --user pi

隨後即可透過 kubectl 管理叢集了:

export KUBECONFIG="$(pwd)/kubeconfig"
kubectl get node

Server

具體的安裝過程限於篇幅不再詳述,可參考 Alex Ellis 的這篇部落格

使用 NodeAffinity 處理不同 CPU 架構問題

根據上圖可以發現,樹莓派的 CPU arch 是 arm,而 NanoPi 是 arm64。為了能夠將對應其架構的容器映象排程到正確的節點,使用 NodeAffinity 是解決方案之一。例如部署內網穿透專案 inlets

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inlets-arm64
spec:
  selector:
    matchLabels:
      app: inlets
  replicas: 1
  template:
    metadata:
      labels:
        app: inlets
    spec:
      containers:
        - name: inlets
          image: inlets/inlets:2.6.4-arm64 # 映象為 arm64 版本
          args: [server]
      affinity:
        nodeAffinity:
          # 在 Pod Scheduling 時強制要求
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:     # 節點選擇器陣列
              - matchExpressions:  # 匹配節點 labels
                  - key: kubernetes.io/arch  # label 名稱
                    operator: In             # 要求滿足以下任意值其一
                    values: [arm64]          # 可指定多個值

可以看到我們在 Pod Spec 內定義了 affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms 欄位,值為 NodeSelectorTerm 陣列。我們定義了一條規則為:label kubernetes.io/arch 的值必須存在於陣列 [arm64] 中。本例中只有單個值,因此等效於:必須等於 arm64

Docker Manifests

如果你想要部署的應用映象是你自己構建的話,那麼強烈推薦試試看 Docker image manifest v2 的特性 —— 可建立 manifest lists 包含多個不同 platforms 和 architectures 的 image manifests。

Docker Client 也提供了一個實驗性的命令 docker manifest 來建立、推送 manifest lists。我結合例項來說說它的用法。

由於該命令目前是「實驗性」的,首先需要透過環境變數開啟才能夠使用。我們順便配置幾個變數備用:

export DOCKER_CLI_EXPERIMENTAL=enabled # 開啟 Docker CLI 的實驗性功能
export IMAGE_REPO=vendor/app           # 映象名稱,請按需填寫
export IMAGE_TAG=v1.0.0                # 映象 tag,請按需填寫

假設你已經分別構建好針對 arm64arm 的映象,接下來先將它們推送到 registry:

docker push "${IMAGE_REPO}:${IMAGE_TAG}-arm64"
docker push "${IMAGE_REPO}:${IMAGE_TAG}-arm"

隨後建立 manifest list 指向多個 image manifests:

docker manifest create --amend \
  "${IMAGE_REPO}:${IMAGE_TAG}" \       # manifest list 名稱
  "${IMAGE_REPO}:${IMAGE_TAG}-arm64" \ # 針對 arm64 的映象
  "${IMAGE_REPO}:${IMAGE_TAG}-arm"     # 針對 arm 的映象

最關鍵的一步到了,為它們新增註解。將每個 manifest 繫結至特定的 osarch

# 註解 arm64 image manifest
docker manifest annotate \
  --os linux \                        # 系統為 Linux
  --arch arm64 \                      # 架構為 arm64
  "${IMAGE_REPO}:${IMAGE_TAG}" \      # manifest list 名稱
  "${IMAGE_REPO}:${IMAGE_TAG}-arm64"  # 被註解的 manifest

# 註解 arm image manifest
docker manifest annotate \
  --os linux \                      # 系統同為 Linux
  --arch arm \                      # 架構為 arm
  "${IMAGE_REPO}:${IMAGE_TAG}" \    # manifest list 名稱
  "${IMAGE_REPO}:${IMAGE_TAG}-arm"  # 被註解的 manifest

此時 manifest list 已經建立好了,可使用 docker manifest inspect 命令檢查一下具體資訊。確認無誤後,推送到 registry 即可:

docker manifest push --purge "${IMAGE_REPO}:${IMAGE_TAG}"

需要注意的是,此處的 --purge 引數是必不可少的。因為 Docker CLI 並沒有提供 docker manifest remove 或是 docker manifest purge 之類的命令。如果不隨著推送直接清理,那就只能到本地的 $HOME/.docker/manifests 目錄手動刪除了… 雖然早在 2018 年初就有人針對此問題提出 issue,但截止發稿前仍沒有倉庫 member 回覆。

最後,使用剛剛建立的 manifest list 名稱代替有字尾的 image manifest 名稱即可,甚至可以增加 replicas 的數量,透過 PodAntiAffinity 刻意將多個副本部署在不同 CPU 架構的節點上而無需區分 image

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inlets
spec:
  selector:
    matchLabels:
      app: inlets
  replicas: 2
  template:
    metadata:
      labels:
        app: inlets
    spec:
      containers:
        - name: inlets
          image: inlets/inlets:2.6.4 # 字尾 `arm64` 已被移除
          args: [server]
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - topologyKey: kubernetes.io/arch
              labelSelector:
                matchLabels:
                  app: inlets

Server

完。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Former WinForm and PHP engineer. Now prefer Golang and Rust, and mainly working on DevSecOps and Kubernetes.

相關文章