Appearance
17金丝雀发布:金丝雀部署和版本控制
我们先来回顾一下什么叫作金丝雀发布。
金丝雀发布也被称为灰度发布,实际上就是将少量的生产流量路由到线上服务的新版本中,以验证新版本的准确性和稳定性。
为什么叫金丝雀发布呢,是因为金丝雀对矿场中的毒气比较敏感,所以在矿场开工前工人们会放一只金丝雀进去,以验证矿场是否存在毒气。
在学习 Istio 如何完成金丝雀发布前,我们先来看一下 Kubernetes 是如何完成金丝雀发布的。
Kubernetes 中的金丝雀发布
我们先来看看如何在 Kubernetes 中进行版本更新。
首先启动 Minikube 环境:
java
minikube start --kubernetes-version=v1.19.2 --driver=docker
创建 controllers/nginx-deployment.yaml 的配置文件:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
可以看到:我们创建了 3 个副本数量的 Nginx,版本号为 1.14.2。
通过下面的命令创建 Deployment:
java
kubectl apply -f https://k8s.io/examples/controllers/nginx-deployment.yaml
通过命令查看 Pod 的运行状态:
java
kubectl get pods --show-labels -o wide
可以得到如下结果:
java
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES LABELS
nginx-deployment-66b6c48dd5-b92bv 1/1 Running 0 2m5s 172.18.0.3 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
nginx-deployment-66b6c48dd5-hshnm 1/1 Running 0 2m5s 172.18.0.4 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
nginx-deployment-66b6c48dd5-tfwfc 1/1 Running 0 2m5s 172.18.0.5 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
下一步,我们使用命令查看 Deployment详细信息:
java
kubectl describe deployments
可以看到 Nginx 的版本为 1.14.2:
java
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx:1.14.2
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
下面我们尝试修改 Deployment,来更新 Nginx 的版本:
java
kubectl edit deployment.v1.apps/nginx-deployment
修改 .spec.template.spec.containers[0].image 字段,从 nginx:1.14.2 改成 nginx:1.16.1,输出如下:
java
deployment.apps/nginx-deployment edited
查看滚动升级状态:
java
kubectl rollout status deployment/nginx-deployment
Waiting for deployment "nginx-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
查看 Pod 的状态,你可以看到新的副本已经启动成功,旧版本的副本已经停止:
java
kubectl get pods --show-labels -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES LABELS
nginx-deployment-559d658b74-8w29d 1/1 Running 0 2s 172.18.0.3 minikube <none> <none> app=nginx,pod-template-hash=559d658b74
nginx-deployment-559d658b74-df9rv 1/1 Running 0 77s 172.18.0.6 minikube <none> <none> app=nginx,pod-template-hash=559d658b74
nginx-deployment-559d658b74-ktzsh 1/1 Running 0 4s 172.18.0.5 minikube <none> <none> app=nginx,pod-template-hash=559d658b74
nginx-deployment-66b6c48dd5-b92bv 0/1 Terminating 0 27m 172.18.0.3 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
nginx-deployment-66b6c48dd5-hshnm 1/1 Terminating 0 27m 172.18.0.4 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
nginx-deployment-66b6c48dd5-tfwfc 0/1 Terminating 0 27m 172.18.0.5 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
我们再来看一下 Nginx 的版本信息,查看是否更新成功:
java
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx:1.16.1
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
最后删除 Deployment 资源,确保下面的示例可以正常运行:
java
kubectl delete deployment.v1.apps/nginx-deployment
至此,Kubernetes 中的版本更新已经完成了,通过滚动升级的方式,可以非常方便地进行版本更新。但是这个过程中,并没有灰度功能,这就相当于直接进行新版本全量发布了,很显然不符合生产环境的要求,下面我们就来学习如何在 Kubernetes 中结合版本更新做到金丝雀发布。
首先创建 Nginx 的应用,并部署:
java
apiVersion: v1
kind: Service
metadata:
name: my-nginx-svc
labels:
app: nginx
spec:
type: LoadBalancer
ports:
- port: 80
selector:
app: nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
创建多个资源类型:
java
kubectl apply -f https://k8s.io/examples/application/nginx-app.yaml
启动 Minikube 隧道,打通网络:
java
minikube tunnel
通过浏览器或者 curl 命令访问 minikube ip 获取 IP,可以得到如下结果:
java
curl -i http://127.0.0.1
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Fri, 12 Feb 2021 07:28:56 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 04 Dec 2018 14:44:49 GMT
Connection: keep-alive
ETag: "5c0692e1-264"
Accept-Ranges: bytes
可以看到,Nginx是可以正常访问的,从返回的 header 中,你可以看到 Nginx 版本号。
接下来,我们重复上面的 Kubernetes 版本更新时的步骤,部署一个新的版本:
java
kubectl apply -f https://k8s.io/examples/controllers/nginx-deployment.yaml
修改 nginx-deployment 中 Nginx 的版本号,并为其增加新的标签 track:canary,具体路径为 .spec.template.metadata.lables.track:canary:
java
kubectl edit deployment.v1.apps/nginx-deployment
继续查看 Pod,可以发现此时同时启动了两个版本,新启动的版本带有 track=canary 的标签:
java
kubectl get pods --show-labels -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES LABELS
my-nginx-66b6c48dd5-82v69 1/1 Running 0 38m 172.18.0.8 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
my-nginx-66b6c48dd5-clzgn 1/1 Running 0 38m 172.18.0.7 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
my-nginx-66b6c48dd5-ngbql 1/1 Running 0 38m 172.18.0.4 minikube <none> <none> app=nginx,pod-template-hash=66b6c48dd5
nginx-deployment-975fd467c-7ghzx 1/1 Running 0 100s 172.18.0.6 minikube <none> <none> app=nginx,pod-template-hash=975fd467c,track=canary
nginx-deployment-975fd467c-lx5sz 1/1 Running 0 104s 172.18.0.5 minikube <none> <none> app=nginx,pod-template-hash=975fd467c,track=canary
nginx-deployment-975fd467c-vqm4m 1/1 Running 0 102s 172.18.0.3 minikube <none> <none> app=nginx,pod-template-hash=975fd467c,track=canary
实际上,此时 Nginx 同时运行了两个版本,我们新发布的版本是 Nginx1.16.1,可以通过 curl -i 的命令查看:
java
curl -i http://127.0.0.1
HTTP/1.1 200 OK
Server: nginx/1.16.1
Date: Fri, 12 Feb 2021 07:57:30 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 13 Aug 2019 10:05:00 GMT
Connection: keep-alive
ETag: "5d528b4c-264"
Accept-Ranges: bytes
多次运行 curl -i 命令查看结果,你可以发现,返回的 header 中同时存在 1.16.1 和 1.14.2 两个 Nginx 版本。通过这样滚动升级的方式,Kubernetes 可以做到简单的金丝雀发布,这样的方式已经可以满足一些简单的场景,但是流量比例只能通过灰度单个 Pod 的方式控制。
如果线上存在两个 Pod,我们新发布一个灰度的 Pod,这样的比例也仅仅是 33%,对于线上的大流量应用来说,这个比例还是过大了。当然,我们还可以通过增加线上运行 Pod的方式进一步降低灰度的比例,但这样的方式依然达不到精准控制的目的。
这时候就需要借助 Service Mesh 中的解决方案了,下面我们来看看在 Istio 中如何做精准流量的金丝雀发布。
Istio 中的金丝雀发布
Istio和 Kubernetes 实现金丝雀发布的方式不太一样,Istio 通过 Envoy 强大的路由规则管理能力,可以灵活地控制对应版本的流量百分比。当然,你也可以通过创建其他的路由规则实现灰度,比如根据 header 中标记的特定用户进行流量路由。下面我们来看一个简单的例子,这里默认你已经安装了 Istio。
在第14讲"Istio 入门:基于最新 1.7 版本的环境搭建和介绍"中,我们已经部署过 Bookinfo 的例子,下面我们继续用这个例子做讲解。部署相关的部分你可以参考前面的章节。
具体的示意图如下,用户访问 productpage 的请求,内部流量会分别路由到 reviews-v1 和 v2 两个版本。
金丝雀发布流量路由图
我们查看 Pod,也可以看到 reviews 这个服务一共启动了三个版本:
java
kubectl get pods
NAME READY STATUS RESTARTS AGE
details-v1-b87bfc85d-nd8v7 2/2 Running 0 2m12s
productpage-v1-65576bb7bf-hj6mw 1/2 Running 0 2m11s
ratings-v1-645b477958-gtrqd 0/2 PodInitializing 0 2m12s
reviews-v1-987d495c-2x5rh 2/2 Running 0 2m12s
reviews-v2-6c5bf657cf-t66hz 2/2 Running 0 2m12s
reviews-v3-5f7b9f4f77-gcrsj 2/2 Running 0 2m12s
通过查看 samples/bookinfo/platform/kube/bookinfo.yaml 的部署配置文件,可以看到:有三个 reviews 服务的部署类型资源。通过前面 Kubernetes 灰度发布的内容我们可以知道,通过配置不同的 labels,可以让同一个服务的三个部署资源同时存在。
这三个部署资源的服务镜像指向了三个不同的地址,分别是 examples-bookinfo-reviews-v1:1.16.2、examples-bookinfo-reviews-v2:1.16.2 和 examples-bookinfo-reviews-v3:1.16.2:
java
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: reviews-v1
labels:
app: reviews
version: v1
spec:
replicas: 1
selector:
matchLabels:
app: reviews
version: v1
template:
metadata:
labels:
app: reviews
version: v1
spec:
serviceAccountName: bookinfo-reviews
containers:
- name: reviews
image: docker.io/istio/examples-bookinfo-reviews-v1:1.16.2
imagePullPolicy: IfNotPresent
env:
- name: LOG_DIR
value: "/tmp/logs"
ports:
- containerPort: 9080
volumeMounts:
- name: tmp
mountPath: /tmp
- name: wlp-output
mountPath: /opt/ibm/wlp/output
volumes:
- name: wlp-output
emptyDir: {}
- name: tmp
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: reviews-v2
labels:
app: reviews
version: v2
spec:
replicas: 1
selector:
matchLabels:
app: reviews
version: v2
template:
metadata:
labels:
app: reviews
version: v2
spec:
serviceAccountName: bookinfo-reviews
containers:
- name: reviews
image: docker.io/istio/examples-bookinfo-reviews-v2:1.16.2
imagePullPolicy: IfNotPresent
env:
- name: LOG_DIR
value: "/tmp/logs"
ports:
- containerPort: 9080
volumeMounts:
- name: tmp
mountPath: /tmp
- name: wlp-output
mountPath: /opt/ibm/wlp/output
volumes:
- name: wlp-output
emptyDir: {}
- name: tmp
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: reviews-v3
labels:
app: reviews
version: v3
spec:
replicas: 1
selector:
matchLabels:
app: reviews
version: v3
template:
metadata:
labels:
app: reviews
version: v3
spec:
serviceAccountName: bookinfo-reviews
containers:
- name: reviews
image: docker.io/istio/examples-bookinfo-reviews-v3:1.16.2
imagePullPolicy: IfNotPresent
env:
- name: LOG_DIR
value: "/tmp/logs"
ports:
- containerPort: 9080
volumeMounts:
- name: tmp
mountPath: /tmp
- name: wlp-output
mountPath: /opt/ibm/wlp/output
volumes:
- name: wlp-output
emptyDir: {}
- name: tmp
emptyDir: {}
我们在页面中,反复刷新http://127.0.0.1/productpage页面,可以看到不同的三种 reviews 的显示。
reivews v1 示例图
reviews v2 示例图
reviews v3 示例图
通过上面的请求结果,我们可以看到,Istio 确实将流量路由到了不同的 reviews 版本上,但是这里我们还没有对 Istio 的路由规则做特殊配置,所以这里的流量是均分的,你也可以理解为和 Kubernetes 的版本灰度策略是一致的。
下面我们创建一个 reviews 的路由规则,为了方便验证,这个配置将所有流量指向 reviews 的 v1 版本:
java
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reivews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
- name: v3
labels:
version: v3
EOF
此时,我们无论如何刷新页面,都会发现页面的显示内容,总是停留在 reviews 的 v1 版本。
reviews v1 示例图
这和我们的预期一致。
下面我们再做出一些改动,修改 Istio 路由配置,将 90% 的流量指向 v1 版本,10% 的流量指向 v2 版本,以达到我们最开始的需求------精准流量的金丝雀发布:
yaml
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reivews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
weight: 90
- destination:
host: reviews
subset: v2
weight: 10
EOF
成功执行上述命令,我们反复刷新 productpage 页面,可以发现大概有 10% 的流量被路由到了新的 reviews v2 版本。
reviews v2 版本示例图
至此,我们就掌握了 Istio 做金丝雀发布的原理。在现实场景中,我们可以根据需求灵活地设置 reviews v1 版本和 v2 版本的流量比例,比如设置 v2 版本流量比例为 1%,用于金丝雀发布,这样的方式相较于 Kubernetes 启动 100 个 pod 进行金丝雀发布,要灵活得多。
在金丝雀发布的过程中,通过不断增加 v2 的流量比例达到精准流量灰度发布的目的,配合 Metrics 监控指标,可以随时自动调整 v2 的比例,以减少新版本出问题时对生产环境的影响。随着 v2 版本的流量不断增大,最终彻底替代 v1 版本成为线上正式版本,整个金丝雀发布也就结束了。
当然需要注意的是,在整个过程中你应该灵活调整 v1 和 v2 版本的 Kubernetes 副本数量,或者配合 Kubernetes 的 HPA,以达到自动扩缩容的目的:
java
$ kubectl autoscale deployment reviews-v1 --cpu-percent=50 --min=1 --max=10
$ kubectl autoscale deployment reviews-v2 --cpu-percent=50 --min=1 --max=10
至此,Istio 中的金丝雀发布就讲完了,下面我们来看一下 Istio 中另一个强大的功能:针对 header 设置路由策略,以达到金丝雀测试的目的。简单来说,就是针对不同用户或不同的客户端版本(针对测试人员单独发布的测试版本),进行精准的流量路由。
Istio 金丝雀测试
首先我们创建一个 VirtualService 配置,将登录用户名为 testuser 的用户指向测试版本 v2 和 v3,其中 v3 流量占比 90%,v2 流量占比 10%。这个版本专门用于 testuser 的测试人员进行新版本的测试,其他登录用户全部路由到 v1 版本:
java
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- match:
- headers:
end-user:
exact: testuser
route:
- destination:
host: reviews
subset: v2
weight: 10
- destination:
host: reviews
subset: v3
weight: 90
- route:
- destination:
host: reviews
subset: v1
EOF
执行成功后,我们反复访问 productpage 页面,发现流量大部分都路由到了 v3 版本,少量流量路由到了 v2 版本。
reviews v3 示例图
至此,我们就完成了 Istio 的金丝雀测试。
总结
这一讲我主要介绍了 Kubernetes 和 Istio 中的版本控制和金丝雀发布。通过今天的学习,相信你已经体会到了 Istio 的金丝雀发布相较于 Kubernetes 的强大之处。
本讲内容总结如下:
利用 Envoy 强大的路由功能,我们可以实现精准流量的金丝雀发布和测试。在你的经历中,除了今天提到的内容,我们还能利用 Envoy 强大的路由规则实现哪些功能呢? 欢迎在留言区和我分享你的观点。
今天内容到这里就结束了,下一讲我将和你分享如何在 Istio 中实现微服务的可观测性,这部分内容包括 Trace、Metrics 和 Log 的落地实战。我们下一讲再见。