Categories
程式開發

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


自動擴縮容是Serverless的核心特徵,更好、更快的冷啟動是所有Serverless平台的極致追求,本文基於網易杭州研究院雲計算團隊的探索,針對熱門Serverless平台Knative,解析其與自動擴容密切相關的流量實現機制,希望能夠幫助從業者更好地理解Knative autoscale功能。

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

服務接收到流量請求後,從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(ksvc)後,Knative會自動創建Route(route)、Configuration(cfg)資源,然後cfg會創建對應的Revision(rev)版本。 rev實際上又會創建Deployment提供服務,流量最終會根據route的配置,導入到相應的rev中。

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

這是簡單的CRD視角,實際上Knative的內部CRD會多一些層次結構,相對更複雜一點。下文會詳細描述。

冷啟動時的流量轉發

從冷啟動和自動擴縮容的實現角度,可以參考一下下圖 。從圖中可以大概看到,有一個Route充當網關的角色,當服務副本數為0時,自動將請求轉發到Activator組件,Activator會保持請求,同時Autoscaler組件會負責將副本數擴容,之後Activator再將請求導入到實際的Pod,並且在副本數不為0時,Route會直接將流量負載均衡到Pod,不再走Activator組件。這也是Knative實現冷啟動的一個基本思路。

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

在集成使用Istio部署時,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

我們可以很明顯的看出,Knative就是通過修改vs的destination host來實現冷啟動中的流量保持和轉發。

相信目前你在網上能找到資料,也基本上停留在該階段。不過,由於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"

virtualservice的變化

筆者的環境可簡單的認為是一個標準的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 -> kingress(ingress.networking.internal.knative.dev) -> vs

在網關這一層涉及到的CRD資源就是如上這些。這裡kingress的意義在於增加一層抽象,如果我們使用的是Gloo等其他網關,則會將kingress轉換成相應的網關資源配置。最新的版本中,負責kingress到Istio vs的控制器部分代碼已經獨立出一個項目,可見如今的Knative對Istio已經不是強依賴。

現在,我們已經了解到Serverless網關是由Knative控制器最終生成的vs生效到Istio Ingress Gateway上,為了驗證我們剛才部署的服務是否可以正常的運行,簡單的用curl命令試驗一下。

和所有的網關或者負載均衡器一樣,對於7層http訪問,我們需要在Header裡加域名Host,用於流量轉發到具體的服務。在上面的vs中已經可以看到對外域名和內部Service域名均已經配置。所以,只需要:

curl -v -H'Host:hello-go.faas.example.com'  : 

其中,IngressIP即網關實例對外暴露的IP。

對於冷啟動來說,目前的Knative需要等十幾秒,即會收到請求。根據之前老版本的經驗,這個時候vs會被更新,destination指向hello-go的Service。

不過,現在我們實際發現,vs沒有任何變化,仍然指向了服務的Service。對比老版本中服務副本數為0時,其實vs的destination指向的是Activator組件的。但現在,不管服務副本數如何變化,vs一直不變。

蹊蹺只能從destination的Service域名入手。

revision service探索

創建ksvc後,Knative會幫我們自動創建Service如下所示。

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

hello-go Service是一個ExternalName Service,作用是將hello-go的Service域名增加一個dns CNAME別名記錄,指向網關的Service域名。

根據Service的annotation我們可以發現,Knative對hello-go-fpmln、hello-go-fpmln-m9mmg 、hello-go-fpmln-metrics這​​三個Service的定位分別為public Service、private Service和metric Service(最新版本已經將private和metrics Service合併)。

private Service和metric Service其實不難理解。問題的關鍵就在這裡的public Service,仔細研究hello-go-fpmln Service,我們可以發現這是一個沒有labelSelector的Service,它的Endpoint不是kubernetes自動創建的,需要額外生成。

在服務副本數為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的副本數越多這裡也會相應的增加。並且由上面的分析可以看到,vs的destination指向的就是public Service。

輸入幾次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://: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上。

通過將流量的轉發功能內聚到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的關係如下圖所示:

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

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

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

簡單來說,服務副本數為0時,流量路徑為:

网关-> public Service -> Activator

經過冷啟動後,副本數為N時,流量路徑為:

网关-> public Service -> Pod

當然流量到Pod後,實際內部還有Envoy sidecar流量攔截,Queue-Proxy sidecar反向代理,才再到用戶的User Container。這裡的機制背後實現我們會有另外一篇文章再單獨細聊。

總結

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

作者簡介:

傅軼,網易杭州研究院雲計算技術部高級研發工程師,目前負責網易輕舟容器雲和微服務平台研發,致力於網易容器技術及其生態體系建設,對Kubernetes、Serverless有深入研究,具有豐富的雲原生分佈式架構設計開發經驗與項目實踐。