自動擴縮容是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有深入研究,具有豐富的雲原生分佈式架構設計開發經驗與項目實踐。