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中。

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

冷啓動時的流量轉發

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

在集成使用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>:<Port> 

其中,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   <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域名。

根據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://<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上。

通過將流量的轉發功能內聚到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、Service Mesh、Serverless讓Knative充滿了魅力,但同時也導致了它的複雜性。
網絡流量的穩定保障是Serverless服務真正生產可用性的關鍵因素,Knative也還在高速的更新迭代中,相信Knative會在未來對網絡方面的性能和穩定性投入更多的優化。

作者簡介:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章