Knative Serving

Knative Serving

其實我在自己的筆電上已經搭了一套可以簡單跑任何應用的 K3S Cluster,今天就來介紹一下我自己的 Cluster 上這個用來跑應用程式的引擎,Knative Serving。

以下的範例大家先看一下有個感覺,先不急著安裝跟操作,文章後面會有完整的安裝教學。

甚麼是 Knative Serving ?

Home - Knative
Knative Documentation

Knative 最早是由 Google 出品現在已經被放到 CNCF 的一個開源專案,主要是讓使用者可以簡單部屬在 Kubernetes 集群裡的無伺服器服務或是事件驅動應用程式。其中 Knative Serving 提供無伺服器服務, Knative Eventing 提供事件驅動應用程式,今天我們主要介紹 Knative Serving 以及它的強大之處。

無伺服器服務

關於無伺服器服務又稱 Serverless 網路上可以搜尋到很多文章,基本上就不多解釋是了,簡單來說就是讓使用者只要專注在服務開發,不用擔心任何伺服器管理的問題,一切基礎設施的管理都交由雲廠商提供。此外,通常雲廠商會提供自動水平擴展、縮容到零以及流量切分等功能,剛好 Knative Serving 也都具備這些功能。

安裝

首先你要有一個 Kubernetes 集群,這個有很多方式,這邊推薦大家安裝 Docker Desktop,就能使用它內建的 Kubernetes 集群。

Kubernetes 集群

Deploy on Kubernetes
Deploying to Kubernetes on Docker Desktop

可以參考這篇文章,先把 Kubernetes 裝起來,這是我用來做開發用途覺得最簡單的安裝方式。

基本上安裝好 Docker Desktop 後打開設定頁,紅圈處打勾等一陣子後,就裝好了。

Ingress 網關

這一步是要準備要把流量導入集群的 Ingress 網關,這裡我們選擇的是 Knative Serving 默認支援的 Istio,很多人看到 Istio 就準備按上一頁,因為有被坑過的都知道,裝起來太複雜了。

這邊一樣,官方有出 Istio Operator 可以快速部屬,並寫我們只會用到 Istio 的 Ingress Controller 功能,其他更進階的部分,暫時還不會碰到,所以讓我們繼續裝下去。

Releases · istio/istio
Connect, secure, control, and observe services. Contribute to istio/istio development by creating an account on GitHub.

到官方 GitHub Release Page 下載符合你平台的 istioctl 執行檔,放到自己電腦上,接著執行以下的命令。

# 安裝 istio operator
istioctl operator init

# 安裝 istio
kubectl apply -f - << EOF
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  name: istio-system
  namespace: istio-system
spec:
  profile: default # 使用 default 安裝配置,只會安裝 istiod, istio-ingressgateway
EOF

# 設定 istio-ingressgateway 為集群的預設 ingress controller
# 這步驟可作可不做,主要是即使不使用 Knative Serving 部屬的應用也可以使用 istio-ingressgateway 當作入口網關
kubectl apply -f - << EOF
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: istio
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"
spec:
  controller: istio.io/ingress-controller
EOF

接著用以上的指令檢查一下 istio pod 都正常運行就沒問題了。可以看到 istio-ingressgateway 的 service type 是 LoadBalancer,位置是 localhost,基本上服務部屬起來後只要有設定 ingress,就能直接在自己電腦上 127.0.0.1 存取到。

Knative Serving

Install by using the Knative Operator - Knative
Knative Documentation

接著我們參考這份官方文件,在筆者安裝的時候試跑以下的指令,裡面的版本號,大家依照當下的最新版本來修改就好。

# 安裝 Opertor
kubectl apply -f https://github.com/knative/operator/releases/download/knative-v1.7.2/operator.yaml

# 安裝 Knative Serving
kubectl apply -f - << EOF
apiVersion: v1
kind: Namespace
metadata:
  name: knative-serving
---
apiVersion: operator.knative.dev/v1beta1
kind: KnativeServing
metadata:
  name: knative-serving
  namespace: knative-serving
spec:
  config:
    domain:
      kn.127-0-0-1.nip.io: "" # 設定 Knative Serving 頂級域名
EOF

接著運行一下上面的指令,確認 Knative Serving 組件運行都正常就可以了。

nip.io - wildcard DNS for any IP Address

題外話,上面的安裝指令裡有一段設定 Knative Serving 頂級域名,裡面用的是 nip.io 魔術 DNS,有興趣的話可以點上面的網頁進去看一下說明。

我們用 kn.127-0-0-1.nip.io 當作 Knative Serving 服務的頂級域名基本上所有服務部屬出來後都會是用這個格式的域名 "{knative service name}.{namespace}.kn.127-0-0-1.nip.io",只要把這串域名用 nslookup 解析出來都會拿到 127.0.0.1 的 IP 位置,如上圖。

Install Knative using quickstart - Knative
Knative Documentation

基本上到這邊就安裝完成了,我們到官網這個連結,安裝一下 kn 到電腦裡,就可以開始使用。

就像一開始的介紹一樣,我們跑了官網的 Hello Word 服務,並馬上得到了一個可以存取的網址。

一個簡單的使用範例

為了先讓大家知道這東西有多好用,比起打一堆字,當然是直接把一個應用跑起來最直觀,我們來看一下官方範例在一個已經安裝好的 Knative Serving 集群上跑起來的樣子。

佈署第一個服務

Deploying a Knative Service - Knative
Knative Documentation

如官網的範例,在終端機輸入指令,就可以在不到 5 秒的時間把任意一個容器在 Kubernetes 集群裡跑起來,並且得到到一個可以連線的網址。

自動擴展

Autoscaling - Knative
Knative Documentation

接著示範一下官方的教學裡的自動擴展的功能。

GitHub - rakyll/hey: HTTP load generator, ApacheBench (ab) replacement
HTTP load generator, ApacheBench (ab) replacement. Contribute to rakyll/hey development by creating an account on GitHub.

用壓力測試工具 hey 來對我們剛剛創建的服務注入一些流量。

可以看到 Knative Serving 自動幫我們把服務擴展上去,預設是用服務的併發度作為指標來擴展,預設每個服務分配 100 併發,並且在擴展過程中,Knative Serving 會自動幫你把流量 queue 住,不會有流量丟失的問題。

接著放置一段時間後,Knative Serving 會自動發現服務現在沒有流量,幫你把服務縮容到 0 ,節省你的資源。

流量拆分

Traffic splitting - Knative
Knative Documentation

試一下官方範例中流量拆分的功能。

首先我們先更新我們的服務,並且設定一半流量到新服務,一半流量到舊的服務。

接著我們試著訪問幾次我們的服務,可以發現新舊流量會以各半機率出現。

基礎概念回顧

在 Knative Serving 中每一個服務都是以上圖的這幾個 Custom Resource 被 Knative Serving 管理,以下來介紹一下它們。

Services

service.serving.knative.dev

由它自動管理工作負載的整個生命週期。它控制其他對象如 Routes, Configurations, 和 Revisions 的創建,以確保你的應用程式具有 Route、Configuration 和 Service 的每次更新的新 Revision。

並且可以將服務定義為始終將流量路由到最新修訂版或固定修訂版。

Routes

route.serving.knative.dev

這個資源將網絡端點映射到一個或多個 Revision。您可以通過多種方式管理流量,包括流量拆分和命名路由。

此外,在本篇操作的範例中,安裝的是 Istio 網路組件,會再從 Route 派生出 ingresses.networking.internal.knative.dev 然後再派生出 VirtualService 接著 Istiod 就能夠生成對應的路由配置並呼叫 istio-ingressgateway 更新路由的介面來把路由設定派發給 Envoy 網關。

Configurations

configuration.serving.knative.dev

這個資源維護部署所需的狀態。它在程式和配置之間提供了清晰的分離,並遵循十二因素應用程序方法。修改配置會創建一個新版本。

Revisions

revision.serving.knative.dev

這個資源是對工作負載進行的每次修改的代碼和配置的時間點快照。修訂是不可變的對象,只要有用就可以保留。 Revisions 可以根據傳入的流量自動放大和縮小。

而 Revisions 跟 Configurations 的關係可以理解為,每次對應用的修改,會更新 Configurations 並產生一個新的 Revisions,每個 Revision 會帶一個版本號每次遞增,並且它是不可變的作為每次更新 Configurations 的歷史紀錄,以方便我們做新舊版流量的指定,所謂的流量指定就是透過在 Service 上設定不同 Revision 流量的分布比例,並且這個設定回傳導到 Routes 在傳導到 VirtualService 整個原理大概是這樣。

官方範例的深入理解

我們在做一次官方範例接著來仔細觀察每一個步驟 Knative Serving 的資源發生的變化。

部屬 Hello World

kn service create hello --image gcr.io/knative-samples/helloworld-go --port 8080 --env TARGET=World
kubectl get ksvc hello -o yaml

我們執行上面的指令來部屬一個服務並把 Service 的 yaml 拿出來看。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: hello
  ...
spec:
  template:
    ...
    spec:
      containers:
      - name: user-container
        image: gcr.io/knative-samples/helloworld-go
        env:
        - name: TARGET
          value: World
        ...
  traffic:
  - latestRevision: true
    percent: 100
status:
  latestCreatedRevisionName: hello-00001
  latestReadyRevisionName: hello-00001
  traffic:
  - latestRevision: true
    percent: 100
    revisionName: hello-00001
  url: http://hello.default.kn.127-0-0-1.nip.io
...

可以看到容器的 image 版本跟環境變數,還有路由到哪一個 Revision 的設定都是記錄在這個資源裡。

kubectl get configuration hello -o yaml
kubectl get revision hello-00001 -o yaml
kubectl get route hello -o yaml

接著可以輸入這三行,可以觀察一下輸出。

Configuration

apiVersion: serving.knative.dev/v1
kind: Configuration
metadata:
  name: hello
  ...
spec:
  template:
    ...
    spec:
      containers:
      - name: user-container
        image: gcr.io/knative-samples/helloworld-go
        env:
        - name: TARGET
          value: World
        ...
status:
  conditions:
  - lastTransitionTime: "2022-10-17T09:31:43Z"
    status: "True"
    type: Ready
  latestCreatedRevisionName: hello-00001
  latestReadyRevisionName: hello-00001
  ...

Revision

apiVersion: serving.knative.dev/v1
kind: Revision
metadata:
  name: hello-00001
  ...
spec:
  containers:
  - name: user-container
    image: gcr.io/knative-samples/helloworld-go
    env:
    - name: TARGET
      value: World
    ...
status:
  actualReplicas: 0
  desiredReplicas: 0
  observedGeneration: 1
  ...

Route

apiVersion: serving.knative.dev/v1
kind: Route
metadata:
  name: hello
  ...
spec:
  traffic:
  - configurationName: hello
    latestRevision: true
    percent: 100
status:
  address:
    url: http://hello.default.svc.cluster.local
  traffic:
  - latestRevision: true
    percent: 100
    revisionName: hello-00001
  url: http://hello.default.kn.127-0-0-1.nip.io
  ...

可以看到在第一個版本,Revision 跟 Configuration 的內容基本上一樣,此外可以看到 Configuration 的 Status 欄位記錄了現在流量到哪一個 Revision,而 Revision 的 Status 欄位上則有紀錄了現在這個 Revision 開了幾個 Pod,當然 Configuration 的流量設定會跟 Route 看到的是一致的。

發布新版本

在一開始的範例裡我們發布新的版本其實是有一點小瑕疵的,有時候我需要先發新版,但是不要把流量放量到新版本上,如果照著前面流量拆分的範例做的話,會有一點點時間的流量會完全的在新版本上,比較正確應該是要向下面這樣操作。

kn service update hello --traffic hello-00001=100
kubectl get ksvc hello -o yaml

輸出

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  generation: 2
  name: hello
spec:
  ...
  traffic:
  - latestRevision: false # 可以發現這裡變成了 false
    percent: 100
    revisionName: hello-00001

我們先設定要全部流量導到 hello-00001 這個 Revision,觀察一下指令的輸出,默認 Knative Serving 行為是把流量自動導向最新的 Revision,而在我們下這個指令後,就是把流量全部指定到 hello-00001 這個 Revision,因此 latestRevision 欄位會變成 false。

kn service update hello --env TARGET=Knative
kubectl get ksvc hello -o yaml

輸出

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: hello
  ...
spec:
  template:
    ...
    spec:
      containers:
      - name: user-container
        ...
        env:
        - name: TARGET
          value: Knative # 剛剛更新的環境變數
        ...
  traffic:
  - latestRevision: false
    percent: 100
    revisionName: hello-00001 # 流量設定依然是我們前一步驟指定的 hello-00001 版本
status:
  ...
  latestCreatedRevisionName: hello-00002 # 可以看到最新版本 revision 是 hello-00002
  latestReadyRevisionName: hello-00002
  observedGeneration: 3
  traffic:
  - latestRevision: false
    percent: 100
    revisionName: hello-00001

指令下完可以觀察一下上面的輸出,你會發現 Service 物件裡的環境變數已經更新,但是流量設定還是指向 Revision hello-00001,這時候你可以試著去連一下 Kantive Service 的網址。

不用懷疑連幾次都是 Hello World 這符合我們的預期,部屬新版本但是不要開放流量。

接著可以下這個指令觀察一下確實有兩個版本的 Revision 存在。

kubectl get revision hello-00001 -o yaml
kubectl get revision hello-00002 -o yaml
kubectl get configuration hello -o yaml
kubectl get route hello -o yaml

輸出 Revision
hello-00001

apiVersion: serving.knative.dev/v1
kind: Revision
metadata:
  name: hello-00001
  ...
spec:
  containers:
  - name: user-container
    image: gcr.io/knative-samples/helloworld-go
    env:
    - name: TARGET
      value: World
    ...

hello-00002

apiVersion: serving.knative.dev/v1
kind: Revision
metadata:
  name: hello-00002
  ...
spec:
  containers:
  - name: user-container
    image: gcr.io/knative-samples/helloworld-go
    env:
    - name: TARGET
      value: Knative
    ...

輸出 Configuration

apiVersion: serving.knative.dev/v1
kind: Configuration
metadata:
  name: hello
  ...
spec:
  template:
    ...
    spec:
      containers:
      - name: user-container
        image: gcr.io/knative-samples/helloworld-go@sha256:5ea96ba4b872685ff4ddb5cd8d1a97ec18c18fae79ee8df0d29f446c5efe5f50
        env:
        - name: TARGET
          value: Knative # 新的內容
        ...
status:
  conditions:
  - lastTransitionTime: "2022-10-17T10:04:58Z"
    status: "True"
    type: Ready
  latestCreatedRevisionName: hello-00002
  latestReadyRevisionName: hello-00002
  observedGeneration: 2

輸出 Route

apiVersion: serving.knative.dev/v1
kind: Route
metadata:
  name: hello
  ...
spec:
  traffic:
  - latestRevision: false
    percent: 100
    revisionName: hello-00001

可以看到 Revision hello-00001 還是擁有舊的環境變數設定,而 hello-00002 以及 Configuration 則是新的值,而 Route 則是我們指定的路由配置。

新版本放流量

接著就來把流量開放給新版本,執行以下的指令。

kn service update hello --traffic hello-00001=50 --traffic @latest=50
kubectl get ksvc hello -o yaml

輸出

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: hello
  ...
spec:
  template:
    ...
  traffic:
  - latestRevision: false
    percent: 50
    revisionName: hello-00001
  - latestRevision: true
    percent: 50
status:
...
  traffic:
  - latestRevision: false
    percent: 50
    revisionName: hello-00001
  - latestRevision: true
    percent: 50
    revisionName: hello-00002

可以看到流量設定變成了各 50%,Route 資源的部分就不帶大家看了,大家有興趣可以自己看一下。

然後嘗試存取一下 Knative 服務的網址,可以發現 Hello World 跟 Hello Knative 個半機率出現,同時也可以看到有兩個不同版本容器在提供服務。

小結

在沒有 Knative Serving 的情況下,我們要自己做到這些事情,你需要寫一個 Deployment 的 yaml 來定義你要跑甚麼容器,接著需要寫一個 Service 跟 Ingress 的 yaml 來設應甚麼域名要把流量導進來,需要做的事情就不是簡單一行指令可以達成的。

此外要自動擴展你需要使用 Kubernetes 內建的 HPA 他只支援 CPU Memory 指標的擴展,並不支援基於 QPS、Concurrency 的擴展。

而流量拆分的部分,要馬透過 Kubernetes 原生 Service 機制利用 Pod 數量比例來拆流量,或是依靠 Ingress Controller 的支援才可以做到,而使用 Knative Serving 基本上也幫你搞定了。

可以說 Knative Serving 可以讓你用一行指令執行任意你指定的 image 並設定好流量的指向,並附贈了以上提到的功能,讓你達成 image to url 的一鍵佈署。

最後如果各位覺得這篇文對你理解 Knative Serving 有幫助的話可以訂閱本站會員,然後把這文章分享給你身邊要使用 Knative Serving 的朋友。