Knative 全鏈路流量機制探索與揭祕

引子——從自動擴縮容說起

服務接收到流量請求後,從 0 自動擴容爲 N,以及沒有流量時自動縮容爲 0,是一個 Serverless 平臺最本質的特徵。
可以說,自動擴縮容機制是那顆皇冠,戴上之後你才能被稱之爲 Serverless。
當然瞭解 Kubernetes 的人會有疑問,HPA 不就是用來幹自動擴縮容的事兒的嗎?難道我用了 HPA 就可以搖身一變成爲 Serverless 了。
這裏最關鍵的區別在於,Serverless 語義下的自動擴縮容是可以讓服務從 0 到 N 的,但是 HPA 不能。HPA 的機制是檢測服務 Pod 的 metrics 數據(例如 CPU 等)然後把 Deployment 擴容,但當你把 Deployment 副本數置爲 0 時,流量進不來,metrics 數據永遠爲 0,此時 HPA 也無能爲力。
所以 HPA 只能讓服務從 1 到 N,而從 0 到 1 的這個過程,需要額外的機制幫助 hold 住請求流量,擴容服務,再轉發流量到服務,這就是我們常說的冷啓動
可以說,冷啓動是 Serverless 皇冠中的那顆明珠,如何實現更好、更快的冷啓動,是所有 Serverless 平臺極致追求的目標。
Knative 作爲目前被社區和各大廠商如此重視和受關注的 Serverless 平臺,當然也在不遺餘力的優化自動擴縮容和冷啓動功能。
不過,本文並不打算直接介紹 Knative 自動擴縮容機制,而是先探究一下 Knative 中的流量實現機制,流量機制和自動擴容密切相關,只有瞭解其中的奧祕,才能更好的理解 Knative autoscale 功能。
由於 Knative 其實包括 Building(Tekton)、Serving 和 Eventing,這裏只專注於 Serving 部分。 另外需要提前說明的是,Knative 並不強依賴 Istio,Serverless 網關的實際選擇除了集成 Istio,還支持 Gloo、Ambassador。同時,即使使用了 Istio,也可以選擇是否使用 envoy sidecar 注入。本文介紹的時候,我們默認使用的是 Istio 和注入 sidecar 的部署方式。

簡單但是有點過時的老版流量機制

先回顧一下 Knative 官方的一個簡單的原理示意圖如下所示。用戶創建一個 Knative Service(ksv c)後,Knative 會自動創建 Route(route),Configuration(cfg)資源,然後 cfg 會創建對應的 Revision(rev)版本。rev 實際上又會創建 Deployment 提供服務,流量最終會根據 route 的配置,導入到相應的 rev 中。

這是簡單的 CRD 視角,實際上 Knative 的內部 CRD 會多一些層次結構,相對更復雜一點。下文會詳細描述。
從冷啓動和自動擴縮容的實現角度,可以參考一下下圖 。從圖中可以大概看到,有一個 Istio Route 充當網關的角色,當服務副本數爲 0 時,自動將流量轉發到 Activator 組件,Activator 會 hold 住流量,同時 Autoscaler 組件會負責將副本數擴容,之後 Activator 再將流量導入到實際的 Pod,並且在副本數不爲 0 時,Istio Route 會直接將流量負載均衡到 Pod,不再走 Activator 組件。這也是 Knative 實現冷啓動的一個基本思路。

在集成使用 Istio 部署時,Knative Route 默認採用的是 Istio Ingress Gateway 實現,大概在 Knative 0.6 版本之前,我們可以發現,Route 的流量轉發本質上是由 Istio virtualservice(vs)控制。副本數爲 0 時,vs 如下所示,其中 destination 指向的是 Activator 組件。此時 Activator 會幫助轉發冷啓動時的請求。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: route-f8c50d56-3f47-11e9-9a9a-08002715c9e6
spec:
  gateways:
  - knative-ingress-gateway
  - mesh
  hosts:
  - helloworld-go.default.example.com
  - helloworld-go.default.svc.cluster.local
  http:
  - appendHeaders:
    route:
    - destination:
        host: Activator-Service.knative-serving.svc.cluster.local
        port:
          number: 80
      weight: 100

當服務副本數不爲 0 之後,vs 會變爲如下所示,將 Ingress Gateway 的流量直接打到服務 Pod 上。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: route-f8c50d56-3f47-11e9-9a9a-08002715c9e6
spec:
 hosts:
  - helloworld-go.default.example.com
  - helloworld-go.default.svc.cluster.local
  http:
  - match:
    route:
    - destination:
        host: helloworld-go-2xxcn-Service.default.svc.cluster.local
        port:
          number: 80
      weight: 100

無非是通過修改 vs 的 destination 來實現冷啓動中的流量保持和轉發。
相信目前你在網上能找到資料,也基本上停留在該階段。不過,knative 的發展是如此的迅速,以至於,上面分析的細節已經過時。
下面以 0.9 版本爲例,我們仔細探究一下現有的實現方式,和關於 Knative 流量的真正祕密。

複雜但是更優異的新版流量機制

鑑於官方文檔並沒有最新的具體實現機制介紹,我們創建一個簡單的 hello-go ksvc,並以此進行分析。ksvc 如下所示:

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
  name: hello-go
  namespace: faas
spec:
  template:
    spec:
      containers:
      - image: harbor-yx-jd-dev.yx.netease.com/library/helloworld-go:v0.1
        env:
        - name: TARGET
          value: "Go Sample v1"

筆者的環境可簡單的認爲是一個標準的 Istio 部署,Serverless 網關爲 Istio Ingress Gateway,所以創建完 ksvc 後,爲了驗證服務是否可以正常運行,需要發送 http 請求至網關。由於 Gateway 資源已經在部署 Knative 的時候創建了,所以我們只需要關心一下 vs。在服務副本數爲 0 的時候,Knative 控制器創建的 vs 關鍵配置如下:

spec:
  gateways:
  - knative-serving/cluster-local-gateway
  - knative-serving/knative-ingress-gateway
  hosts:
  - hello-go.faas
  - hello-go.faas.example.com
  - hello-go.faas.svc
  - hello-go.faas.svc.cluster.local
  - f81497077928a654cf9422088e7522d5.probe.invalid
  http:
  - match:
    - authority:
        regex: ^hello-go\.faas\.example\.com(?::\d{1,5})?$
      gateways:
      - knative-serving/knative-ingress-gateway
    - authority:
        regex: ^hello-go\.faas(\.svc(\.cluster\.local)?)?(?::\d{1,5})?$
      gateways:
      - knative-serving/cluster-local-gateway
    retries:
      attempts: 3
      perTryTimeout: 10m0s
    route:
    - destination:
        host: hello-go-fpmln.faas.svc.cluster.local
        port:
          number: 80

vs 指定了已經創建好的 gw,同時 destination 指向的是一個 Service 域名。這個 Service 就是 Knative 默認自動創建的 hello-go 服務的 Service。
不過細心的我們又發現 vs 的 ownerReferences 指向了一個 Knative 的 CRD ingress.networking.internal.knative.dev:

  ownerReferences:
  - apiVersion: networking.internal.knative.dev/v1alpha1
    blockOwnerDeletion: true
    controller: true
    kind: Ingress
    name: hello-go
    uid: 4a27a69e-5b9c-11ea-ae53-fa163ec7c05f

根據名字可以看到這是一個 Knative 內部使用的 CRD,該 CRD 的內容其實和 vs 比較類似,同時 ingress.networking.internal.knative.dev 的 ownerReferences 指向了我們熟悉的 route,總結下來就是:

route -> ingress.networking.internal.knative.dev -> vs

在網關這一層體現到的 CRD 資源就是如上這些。這裏 ingress.networking.internal.knative.dev 的意義在於增加一層抽象,如果我們使用的是 Gloo 等其他網關,則會將 ingress.networking.internal.knative.dev 轉換成相應的網關資源配置。可見 Knative 正在計劃下一盤大棋。
現在,我們已經瞭解到 Serverless 網關是由 Knative 控制器最終生成的 vs 生效到 Istio Ingress Gateway 上,爲了驗證我們剛纔部署的服務是否可以正常的運行,簡單的用 curl 命令試驗一下。
和所有的網關或者負載均衡器一樣,對於 7 層 http 訪問,我們需要在 Header 里加域名 Host,用於流量轉發到具體的服務。在上面的 vs 中已經可以看到對外域名和內部 Service 域名均已經配置。所以,只需要:

curl -v -H'Host:hello-go.faas.example.com'  <IngressIP>:<Port>

其中,IngressIP 即網關實例對外暴露的 IP。
對於冷啓動來說,目前的 Knative 需要等十幾秒,即會收到請求。根據之前老版本的經驗,這個時候 vs 會被更新,destination 指向 hello-go 的 Service。
不過,現在我們實際發現,vs 沒有任何變化,仍然指向了服務的 Service。這時候,我們纔想起來,老版本中服務副本數爲 0 時,其實 vs 的 destination 指向的是 Activator 組件的。但現在,不管服務副本數如何變化,vs 一直不變。
蹊蹺只能從 destination 的 Service 域名入手。
創建 ksvc 後,Knative 會幫我們自動創建 Service 如下所示。

$ kubectl -n faas get svc
NAME                     TYPE           CLUSTER-IP     EXTERNAL-IP                                            PORT(S)      
hello-go                 ExternalName   <none>         cluster-local-gateway.istio-system.svc.cluster.local   <none>           
hello-go-fpmln           ClusterIP      10.178.4.126   <none>                                                 80/TCP             
hello-go-fpmln-m9mmg     ClusterIP      10.178.5.65    <none>                                                 80/TCP,8022/TCP  
hello-go-fpmln-metrics   ClusterIP      10.178.4.237   <none>                                                 9090/TCP,9091/TCP

hello-go Service 是一個 ExternalName Service,作用是將 hello-go 的 Service 域名增加一個 dns CNAME 別名記錄,指向網關的 Service 域名。
仔細研究 hello-go-fpmln Service,我們可以發現這是一個沒有 label selector 的 Headless Service,它的 Endpoint 不是 kubernetes 自動創建的,需要額外去創建。
hello-go-fpmln-m9mmg Service 和 hello-go-fpmln-metrics 則根據 revision label selector 到對應的服務 Pod。
根據這些 Service 的一些 annotation 可以看到,Knative 對 hello-go-fpmln、hello-go-fpmln-m9mmg 、hello-go-fpmln-metrics 這三個 Service 的定位分別爲 public Service、private Service 和 metric Service。
private Service 和 metric Service 其實不難理解。問題的關鍵就在這裏的 public Service,並且由上面的分析可以看到,vs 的 destination 指向的就是這裏的 public Service。
在服務副本數爲 0 時,查看一下 Service 對應的 Endpoint,如下所示:

$ kubectl -n faas get ep
NAME                     ENDPOINTS                               AGE
hello-go-fpmln           172.31.16.81:8012                       
hello-go-fpmln-m9mmg     172.31.16.121:8012,172.31.16.121:8022   
hello-go-fpmln-metrics   172.31.16.121:9090,172.31.16.121:9091

其中,public Service 的 Endpoint IP 是 Knative Activator 的 Pod IP,實際發現 Activator 的副本數越多這裏也會相應的增加。
輸入幾次 curl 命令模擬一下 http 請求,雖然副本數從 0 開始增加到 1 了,但是這裏的 Endpoint 卻沒有變化,仍然爲 Activator Pod IP。
接着使用 hey 來壓測一下:

./hey_linux_amd64 -n 1000000 -c 300  -m GET -host helloworld-go.faas.example.com http://<IngressIP>:80

果然,發現 Endpoint 變化了,通過對比服務的 Pod IP,已經變成了新啓動的服務 Pod IP,不再是 Activator Pod 的 IP。

$ kubectl -n faas get ep
NAME                     ENDPOINTS                         
helloworld-go-mpk25      172.31.16.121:8012
hello-go-fpmln-m9mmg     172.31.16.121:8012,172.31.16.121:8022   
hello-go-fpmln-metrics   172.31.16.121:9090,172.31.16.121:9091

原來,現在新版本的冷啓動流量轉發機制已經不再是通過修改 vs 來改變網關的流量轉發配置了,而是直接更新服務的 public Service 後端 Endpoint,從而實現將流量從 Activator 負載均衡到真正的服務負載 Pod 上。
這樣將流量的轉發功能內聚到 Kubernetes 本身 Service/Endpoint 層,一方面減小了網關的配置更新壓力,一方面 Knative 可以在對接各種不同的網關時的實現時更加解耦,網關層不再需要關心冷啓動時的流量轉發機制。
再深入從上述的三個 Service 入手研究,它們的 ownerReference 是 serverlessservice.networking.internal.knative.dev(sks),而 sks 的 ownerReference 是 podautoscaler.autoscaling.internal.knative.dev(kpa)。
在壓測過程中同樣發現,sks 會在冷啓動過後,會從 Proxy 模式變爲 Serve 模式:

$ kubectl -n faas get sks
NAME             MODE    SERVICENAME      PRIVATESERVICENAME     READY   REASON
hello-go-fpmln   Proxy   hello-go-fpmln   hello-go-fpmln-m9mmg   True
$ kubectl -n faas get sks
NAME             MODE    SERVICENAME      PRIVATESERVICENAME     READY   REASON
hello-go-fpmln   Serve   hello-go-fpmln   hello-go-fpmln-m9mmg   True

這也意味着,當流量從 Activator 導入的時候,sks 爲 Proxy 模式,服務真正啓動起來後會變成 Serve 模式,網關流量直接流向服務 Pod。
從名稱上也可以看到,sks 和 kpa 均爲 Knative 內部 CRD,實際上也是由於 Knative 設計上可以支持自定義的擴縮容方式和支持 Kubernetes HPA 有關,實現更高一層的抽象。
現在爲止,我們可以梳理 Knative 的絕大部分 CRD 的關係如下圖所示:

一個更復雜的實際實現架構圖如下所示。

簡單來說,服務副本數爲 0 時,流量路徑爲: 網關-> public Service -> Activator 經過冷啓動後,副本數爲 N 時,流量路徑爲: 網關->public Service -> Pod 當然流量到 Pod 後,實際內部還有 Envoy sidecar 流量攔截,Queue-Proxy sidecar 反向代理,纔再到用戶的 User Container。這裏的機制背後實現我們會有另外一篇文章再單獨細聊。

總結

Knative 本身的實現可謂是雲原生領域裏的一個集大成者,融合 Kubernetes、ServiceMesh、Serverless 讓 Knative 充滿了魅力,但同時也導致了 Knative 的複雜性。 網絡流量的穩定保障是 Serverless 服務真正生產可用性的關鍵因素,Knative 也還在高速的更新迭代中,相信 Knative 會在未來對網絡方面的性能和穩定性投入更多的優化。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://juejin.cn/post/6844904084005191693