Knative 基本功能深入剖析:Knative Serving 的流量灰度和版本管理
作者|冬島 阿里云技術(shù)專家
本篇主要介紹 Knative Serving 的流量灰度,通過一個 rest-api 的例子演示如何創(chuàng)建不同的 Revision、如何在不同的 Revision 之間按照流量比例灰度。
部署 rest-api v1
- 代碼
測試之前我們需要寫一段 rest-api 的代碼,并且還要能夠區(qū)分不同的版本。下面我基于官方的例子進行了修改,為了使用方便去掉了 github.com/gorilla/mux 依賴,直接使用 Golang 系統(tǒng)包 net/http 替代。這段代碼可以通過 RESOURCE 環(huán)境變量來區(qū)分不同的版本。
package mainimport ("fmt""io/ioutil""log""net/http""net/url""os""flag" )var resource stringfunc main() {flag.Parse()//router := mux.NewRouter().StrictSlash(true)resource = os.Getenv("RESOURCE")if resource == "" {resource = "NOT SPECIFIED"}root := "/" + resourcepath := root + "/{stockId}"http.HandleFunc("/", Index)http.HandleFunc(root, StockIndex)http.HandleFunc(path, StockPrice)if err := http.ListenAndServe(fmt.Sprintf(":%s", "8080"), nil); err != nil {log.Fatalf("ListenAndServe error:%s ", err.Error())} }func Index(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Welcome to the %s app! \n", resource) }func StockIndex(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "%s ticker not found!, require /%s/{ticker}\n", resource, resource) }func StockPrice(w http.ResponseWriter, r *http.Request) {stockId := r.URL.Query().Get("stockId")url := url.URL{Scheme: "https",Host: "api.iextrading.com",Path: "/1.0/stock/" + stockId + "/price",}log.Print(url)resp, err := http.Get(url.String())if err != nil {fmt.Fprintf(w, "%s not found for ticker : %s \n", resource, stockId)return}defer resp.Body.Close()body, err := ioutil.ReadAll(resp.Body)fmt.Fprintf(w, "%s price for ticker %s is %s\n", resource, stockId, string(body)) }- Dockerfile
創(chuàng)建一個叫做 Dockerfile 的文件,把下面這些內(nèi)容復制到文件中。執(zhí)行 docker build --tag registry.cn-hangzhou.aliyuncs.com/knative-sample/rest-api-go:v1 --file ./Dockerfile . 命令即可完成鏡像的編譯。
你在測試的時候請把 registry.cn-hangzhou.aliyuncs.com/knative-sample/rest-api-go:v1 換成你自己的鏡像倉庫地址。
編譯好鏡像以后執(zhí)行 docker push registry.cn-hangzhou.aliyuncs.com/knative-sample/rest-api-go:v1 把鏡像推送到鏡像倉庫。
FROM registry.cn-hangzhou.aliyuncs.com/knative-sample/golang:1.12 as builderWORKDIR /go/src/github.com/knative-sample/rest-api-go COPY . .RUN CGO_ENABLED=0 GOOS=linux go build -v -o rest-api-go FROM registry.cn-hangzhou.aliyuncs.com/knative-sample/alpine-sh:3.9 COPY --from=builder /go/src/github.com/knative-sample/rest-api-go/rest-api-go /rest-api-goCMD ["/rest-api-go"]- Service 配置
鏡像已經(jīng)有了,我們開始部署 Knative Service。把下面的內(nèi)容保存到 revision-v1.yaml 中,然后執(zhí)行 kubectl apply -f revision-v1.yaml 即可完成 Knative Service 的部署。
apiVersion: serving.knative.dev/v1alpha1 kind: Service metadata:name: stock-service-examplenamespace: default spec:template:metadata:name: stock-service-example-v1spec:containers:- image: registry.cn-hangzhou.aliyuncs.com/knative-sample/rest-api-go:v1env:- name: RESOURCEvalue: v1readinessProbe:httpGet:path: /initialDelaySeconds: 0periodSeconds: 3首次安裝會創(chuàng)建出一個叫做 stock-service-example-v1 的 Revision,并且是把 100% 的流量都打到 stock-service-example-v1 上。
驗證 Serving 的各個資源
如下圖所示,我們先回顧一下 Serving 涉及到的各種資源。接下來我們分別看一下剛才部署的 revision-v1.yaml 各個資源配置。
- Knative Service
- Knative Configuration
- Knative Revision
- Knative Route
訪問 rest-api 服務(wù)
我們部署的 Service 名稱是: stock-service-example。訪問這個 Service 需要獲取 Istio Gateway 的 IP,然后使用 stock-service-example Domain 綁定 Host 的方式發(fā)起 curl 請求。為了方便測試我寫成了一個腳本。創(chuàng)建一個 run-test.sh 文件,把下面這些內(nèi)容復制到文件內(nèi),然后賦予文件可執(zhí)行權(quán)限。執(zhí)行執(zhí)行此腳本就能得到測試結(jié)果。
#!/bin/bashSVC_NAME="stock-service-example" export INGRESSGATEWAY=istio-ingressgateway export GATEWAY_IP=`kubectl get svc $INGRESSGATEWAY --namespace istio-system --output jsonpath="{.status.loadBalancer.ingress[*]['ip']}"` export DOMAIN_NAME=`kubectl get route ${SVC_NAME} --output jsonpath="{.status.url}"| awk -F/ '{print $3}'`curl -H "Host: ${DOMAIN_NAME}" http://${GATEWAY_IP}測試結(jié)果:
從下面的命令輸出結(jié)果可以看到現(xiàn)在返回的是 v1 的信息,說明請求打到 v1 上面了。
└─# ./run-test.sh Welcome to the v1 app!灰度 50% 的流量到 v2
修改 Service 創(chuàng)建 v2 revision , 創(chuàng)建一個 revision-v2.yaml 文件,內(nèi)容如下:
apiVersion: serving.knative.dev/v1alpha1 kind: Service metadata:name: stock-service-examplenamespace: default spec:template:metadata:name: stock-service-example-v2spec:containers:- image: registry.cn-hangzhou.aliyuncs.com/knative-sample/rest-api-go:v1env:- name: RESOURCEvalue: v2readinessProbe:httpGet:path: /initialDelaySeconds: 0periodSeconds: 3traffic:- tag: v1revisionName: stock-service-example-v1percent: 50- tag: v2revisionName: stock-service-example-v2percent: 50- tag: latestlatestRevision: truepercent: 0我們對比一下 v1 版本和 v2 版本可以發(fā)現(xiàn),v2 版本的 Service 中增加了 traffic: 的配置。在 traffic 中指定了每一個 Revision。 執(zhí)行 kubectl apply -f revision-v2.yaml 安裝 v2 版本的配置。然后執(zhí)行測試腳本就能看到現(xiàn)在返回的結(jié)果中 v1 和 v2 基本上是各占 50% 的比例。下面這是我真實測試的結(jié)果。
└─# ./run-test.sh Welcome to the v2 app! └─# ./run-test.sh Welcome to the v1 app! └─# ./run-test.sh Welcome to the v2 app! └─# ./run-test.sh Welcome to the v1 app!提前驗證 Revision
上面展示的 v2 的例子,在創(chuàng)建 v2 的時候直接就把流量分發(fā)到 v2 ,如果此時 v2 有問題就會導致有 50% 的流量異常。下面我們就展示一下如何在轉(zhuǎn)發(fā)流量之前驗證新的 revision 服務(wù)是否正常。我們再創(chuàng)建一個 v3 版本。
創(chuàng)建一個 revision-v3.yaml 的文件,內(nèi)容如下:
apiVersion: serving.knative.dev/v1alpha1 kind: Service metadata:name: stock-service-examplenamespace: default spec:template:metadata:name: stock-service-example-v3spec:containers:- image: registry.cn-hangzhou.aliyuncs.com/knative-sample/rest-api-go:v1env:- name: RESOURCEvalue: v3readinessProbe:httpGet:path: /initialDelaySeconds: 0periodSeconds: 3traffic:- tag: v1revisionName: stock-service-example-v1percent: 50- tag: v2revisionName: stock-service-example-v2percent: 50- tag: latestlatestRevision: truepercent: 0執(zhí)行 kubectl apply -f revision-v3.yaml 部署 v3 版本。然后查看一下 Revision 情況:
└─# kubectl get revision NAME SERVICE NAME GENERATION READY REASON stock-service-example-v1 stock-service-example-v1 1 True stock-service-example-v2 stock-service-example-v2 2 True stock-service-example-v3 stock-service-example-v3 3 True可以看到現(xiàn)在已經(jīng)創(chuàng)建出來了三個 Revision 。
此時我們再看一下 stock-service-example 的真實生效:
└─# kubectl get ksvc stock-service-example -o yaml apiVersion: serving.knative.dev/v1beta1 kind: Service metadata:annotations: ... status: ...traffic:- latestRevision: falsepercent: 50revisionName: stock-service-example-v1tag: v1url: http://v1-stock-service-example.default.example.com- latestRevision: falsepercent: 50revisionName: stock-service-example-v2tag: v2url: http://v2-stock-service-example.default.example.com- latestRevision: truepercent: 0revisionName: stock-service-example-v3tag: latesturl: http://latest-stock-service-example.default.example.comurl: http://stock-service-example.default.example.com可以看到 v3 Revision 雖然創(chuàng)建出來了,但是因為沒有設(shè)置 traffic,所以并不會有流量轉(zhuǎn)發(fā)。此時你執(zhí)行多少次 ./run-test.sh 都不會得到 v3 的輸出。
在 Service 的 status.traffic 配置中可以看到 latest Revision 的配置:
- latestRevision: truepercent: 0revisionName: stock-service-example-v3tag: latesturl: http://latest-stock-service-example.default.example.com每一個 Revision 都有一個自己的 URL,所以只需要基于 v3 Revision 的 URL 發(fā)起請求就能開始測試了。
我已經(jīng)寫好了一個測試腳本,你可以把下面這段腳本保存在 latest-run-test.sh 文件中,然后執(zhí)行這個腳本就能直接發(fā)起到 latest 版本的請求:
#!/bin/bash export INGRESSGATEWAY=istio-ingressgateway export GATEWAY_IP=`kubectl get svc $INGRESSGATEWAY --namespace istio-system --output jsonpath="{.status.loadBalancer.ingress[*]['ip']}"` export DOMAIN_NAME=`kubectl get route ${SVC_NAME} --output jsonpath="{.status.url}"| awk -F/ '{print $3}'`export LAST_DOMAIN=`kubectl get ksvc stock-service-example --output jsonpath="{.status.traffic[?(@.tag=='latest')].url}"| cut -d'/' -f 3`curl -H "Host: ${LAST_DOMAIN}" http://${GATEWAY_IP}測試 v3 版本如果沒問題就可以把流量分發(fā)到 v3 版本了。
下面我們再創(chuàng)建一個文件 revision-v3-2.yaml , 內(nèi)容如下:
apiVersion: serving.knative.dev/v1alpha1 kind: Service metadata:name: stock-service-examplenamespace: default spec:template:metadata:name: stock-service-example-v3spec:containers:- image: registry.cn-hangzhou.aliyuncs.com/knative-sample/rest-api-go:v1env:- name: RESOURCEvalue: v3readinessProbe:httpGet:path: /initialDelaySeconds: 0periodSeconds: 3traffic:- tag: v1revisionName: stock-service-example-v1percent: 40- tag: v2revisionName: stock-service-example-v2percent: 30- tag: v3revisionName: stock-service-example-v3percent: 30- tag: latestlatestRevision: truepercent: 0用 vimdiff 看一下 revision-v3.yaml 和 revision-v3-2.yaml 的區(qū)別:
revision-v3-2.yaml 增加了到 v3 的流量轉(zhuǎn)發(fā)。此時執(zhí)行 ./run-test.sh 可以看到 v1、v2 和 v3 的比例基本是:4:3:3
└─# ./run-test.sh Welcome to the v1 app! └─# ./run-test.sh Welcome to the v2 app! └─# ./run-test.sh Welcome to the v1 app! └─# ./run-test.sh Welcome to the v2 app! └─# ./run-test.sh Welcome to the v3 app! ... ...版本回滾
Knative Service 的 Revision 是不能修改的,每次 Service Spec 的更新創(chuàng)建的 Revision 都會保留在 kube-apiserver 中。如果應(yīng)用發(fā)布到某個新版本發(fā)現(xiàn)有問題想要回滾到老版本的時候只需要指定相應(yīng)的 Revision,然后把流量轉(zhuǎn)發(fā)過去就行了。
小結(jié)
Knative Service 的灰度、回滾都是基于流量的。Workload(Pod) 是根據(jù)過來的流量自動創(chuàng)建出來的。所以在 Knative Serving 模型中流量是核心驅(qū)動。這和傳統(tǒng)的應(yīng)用發(fā)布、灰度模型是有區(qū)別的。
假設(shè)有一個應(yīng)用 app1 ,傳統(tǒng)的做法首先是設(shè)置應(yīng)用的實例個數(shù)( Kubernetes 體系中就是 Pod ),我們假設(shè)實例個數(shù)是 10 個。如果要進行灰度發(fā)布,那么傳統(tǒng)的做法就是先發(fā)布一個 Pod,此時 v1 和 v2 的分布方式是:v1 的 Pod 9個,v2 的 Pod 1 個。如果要繼續(xù)擴大灰度范圍的話那就是 v2 的 Pod 數(shù)量變多,v1 的 Pod 數(shù)量變少,但總的 Pod 數(shù)量維持 10 個不變。
在 Knative Serving 模型中 Pod 數(shù)量永遠都是根據(jù)流量自適應(yīng)的,不需要提前指定。在灰度的時候只需要指定流量在不同版本之間的灰度比例即可。每一個 Revision 的實例數(shù)都是根據(jù)流量的大小自適應(yīng),不需要提前指定。
從上面的對比中可以發(fā)現(xiàn) Knative Serving 模型是可以精準的控制灰度影響的范圍的,保證只灰度一部分流量。而傳統(tǒng)的模型中 Pod 灰度的比例并不能真實的代表流量的比例,是一個間接的灰度方法。
總結(jié)
以上是生活随笔為你收集整理的Knative 基本功能深入剖析:Knative Serving 的流量灰度和版本管理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云原生生态周报 Vol.10 | 数据库
- 下一篇: 云原生生态周报 Vol. 11 | K8