《前端运维》五、k8s--3灰度发布、滚动更新与探针

一、灰度发布 灰度发布是一种发布方式,也叫金丝雀发布,起源是矿工在下井之前会先放一只金丝雀到井里,如果金丝雀不叫了,就代表瓦斯浓度高。原因是金丝雀对瓦斯气体很敏感。灰度发布的做法是:会在现存旧应用的基础上,启动一个新版应用,但是新版应用并不会直接让用户访问。而是先让测试同学去进行测试。如果没有问题,则可以将真正的用户流量慢慢导入到新版,在这中间,持续对新版本运行状态做观察,直到慢慢切换过去,这就是所谓的A/B测试。当然,你也可以招募一些灰度用户,给他们设置独有的灰度标示(Cookie,Header),来让他们可以访问到新版应用,当然,如果中间切换出现问题,也应该将流量迅速地切换到老应用上。
1)准备新版本的service
拷贝一份deployment文件:

cp deployment-user-v1.yaml deployment-user-v2.yaml

修改之前写过的内容:
apiVersion: apps/v1#API 配置版本 kind: Deployment#资源类型 metadata: +name: user-v2#资源名称 spec: selector: matchLabels: +app: user-v2 #告诉deployment根据规则匹配相应的Pod进行控制和管理,matchLabels字段匹配Pod的label值 replicas: 3 #声明一个 Pod,副本的数量 template: metadata: labels: +app: user-v2 #Pod的名称 spec:#组内创建的 Pod 信息 containers: - name: nginx #容器的名称 +image: registry.cn-beijing.aliyuncs.com/zhangrenyang/nginx:user-v2 ports: - containerPort: 80 #容器内映射的端口

然后service的文件内容是这样的:
apiVersion: v1 kind: Service metadata: +name: service-user-v2 spec: selector: +app: user-v2 ports: - protocol: TCP port: 80 targetPort: 80 type: NodePort

启动:
kubectl apply -f deployment-user-v2.yaml service-user-v2.yaml

2)根据cookie切分流量
基于 Cookie 切分流量。这种实现原理主要根据用户请求中的 Cookie 是否存在灰度标示 Cookie去判断是否为灰度用户,再决定是否返回灰度版本服务
  • nginx.ingress.kubernetes.io/canary:可选值为 true / false 。代表是否开启灰度功能
  • nginx.ingress.kubernetes.io/canary-by-cookie:灰度发布 cookie 的 key。当 key 值等于 always 时,灰度触发生效。等于其他值时,则不会走灰度环境 ingress-gray.yaml
我们创建一个ingress-gray.yaml文件:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: user-canary annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-by-cookie: "vip_user" spec: rules: - http: paths: - backend: serviceName: service-user-v2 servicePort: 80 backend: serviceName: service-user-v2 servicePort: 80

使文件生效:
kubectl apply -f ./ingress-gray.yaml

获取外部接口:
kubectl -n ingress-nginx get svc

测试:
curl http://172.31.178.169:31234/user curl http://118.190.156.138:31234/user curl --cookie "vip_user=always"http://172.31.178.169:31234/user

3)基于header切分流量
基于 Header 切分流量,这种实现原理主要根据用户请求中的 header 是否存在灰度标示 header去判断是否为灰度用户,再决定是否返回灰度版本服务。
修改下上面的ingress-gray.yml文件即可:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: user-canary annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/canary: "true" +nginx.ingress.kubernetes.io/canary-by-header: "name" +nginx.ingress.kubernetes.io/canary-by-header-value: "vip" spec: rules: - http: paths: - backend: serviceName: service-user-v2 servicePort: 80 backend: serviceName: service-user-v2 servicePort: 80

同样的:
kubectl apply -f ingress-gray.yaml curl --header "name:vip"http://172.31.178.169:31234/user

4)基于权重切分流量
这种实现原理主要是根据用户请求,通过根据灰度百分比决定是否转发到灰度服务环境中
  • nginx.ingress.kubernetes.io/canary-weight:值是字符串,为 0-100 的数字,代表灰度环境命中概率。如果值为 0,则表示不会走灰度。值越大命中概率越大。当值 = 100 时,代表全走灰度。
一样一样的,修改下配置参数罢了:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: user-canary annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/canary: "true" +nginx.ingress.kubernetes.io/canary-weight: "50" spec: rules: - http: paths: - backend: serviceName: service-user-v2 servicePort: 80 backend: serviceName: service-user-v2 servicePort: 80

测试下:
kubectl apply -f ingress-gray.yaml for ((i=1; i<=10; i++)); do curl http://172.31.178.169:31234/user; done

k8s 会优先去匹配 header ,如果未匹配则去匹配 cookie ,最后是 weight。
二、滚动发布 滚动发布,则是我们一般所说的无宕机发布。其发布方式如同名称一样,一次取出一台/多台服务器(看策略配置)进行新版本更新。当取出的服务器新版确保无问题后,接着采用同等方式更新后面的服务器。k8s创建副本应用程序的最佳方法就是部署(Deployment),部署自动创建副本集(ReplicaSet),副本集可以精确地控制每次替换的Pod数量,从而可以很好的实现滚动更新。k8s每次使用一个新的副本控制器(replication controller)来替换已存在的副本控制器,从而始终使用一个新的Pod模板来替换旧的pod模板
  • 创建一个新的replication controller
  • 增加或减少pod副本数量,直到满足当前批次期望的数量
  • 删除旧的replication controller
滚动发布的优缺点如下:
  • 优点
    • 不需要停机更新,无感知平滑更新。
    • 版本更新成本小,不需要新旧版本共存
  • 缺点
    • 更新时间长:每次只更新一个/多个镜像,需要频繁连续等待服务启动缓冲
    • 旧版本环境无法得到备份:始终只有一个环境存在
    • 回滚版本异常痛苦:如果滚动发布到一半出了问题,回滚时需要使用同样的滚动策略回滚旧版本
我们下面来尝试下,先扩容为10个副本:
kubectl get deploy kubectl scale deployment user-v1--replicas=10

修改deployment-user-v1.yaml文件:
apiVersion: apps/v1#API 配置版本 kind: Deployment#资源类型 metadata: name: user-v1#资源名称 spec: minReadySeconds: 1 + strategy: +type: RollingUpdate +rollingUpdate: +maxSurge: 1 +maxUnavailable: 0 + selector: +matchLabels: +app: user-v1 #告诉deployment根据规则匹配相应的Pod进行控制和管理,matchLabels字段匹配Pod的label值 replicas: 10 #声明一个 Pod,副本的数量 template: metadata: labels: app: user-v1 #Pod的名称 spec:#组内创建的 Pod 信息 containers: - name: nginx #容器的名称 +image: registry.cn-beijing.aliyuncs.com/zhangrenyang/nginx:user-v3 #使用哪个镜像 ports: - containerPort: 80 #容器内映射的端口

参数 含义
minReadySeconds 容器接受流量延缓时间:单位为秒,默认为0。如果没有设置的话,k8s会认为容器启动成功后就可以用了。设置该值可以延缓容器流量切分
strategy.type = RollingUpdate ReplicaSet 发布类型,声明为滚动发布,默认也为滚动发布
strategy.rollingUpdate.maxSurge 最多Pod数量:为数字类型/百分比。如果 maxSurge 设置为1,replicas 设置为10,则在发布过程中pod数量最多为10 + 1个(多出来的为旧版本pod,平滑期不可用状态)。maxUnavailable 为 0 时,该值也不能设置为0
strategy.rollingUpdate.maxUnavailable 升级中最多不可用pod的数量:为数字类型/百分比。当 maxSurge 为 0 时,该值也不能设置为0
启动:
kubectl apply -f ./deployment-user-v1.yaml deployment.apps/user-v1 configured

然后查看状态:
kubectl rollout status deployment/user-v1

三、服务可用性探针 当 Pod 的状态为 Running 时,该 Pod 就可以被分配流量(可以访问到)了。一个后端容器启动成功,不一定不代表服务启动成功。
3.2.1 存活探针 LivenessProbe 第一种是存活探针。存活探针是对运行中的容器检测的。如果想检测你的服务在运行中有没有发生崩溃,服务有没有中途退出或无响应,可以使用这个探针。如果探针探测到错误, Kubernetes 就会杀掉这个 Pod;否则就不会进行处理。如果默认没有配置这个探针, Pod 不会被杀死。
3.2.2 可用探针 ReadinessProbe 第二种是可用探针。作用是用来检测 Pod 是否允许被访问到(是否准备好接受流量)。如果你的服务加载很多数据,或者有其他需求要求在特定情况下不被分配到流量,那么可以用这个探针。如果探针检测失败,流量就不会分配给该 Pod。在没有配置该探针的情况下,会一直将流量分配给 Pod。当然,探针检测失败,Pod 不会被杀死。
3.2.3 启动探针 StartupProbe 第三种是启动探针。作用是用来检测 Pod 是否已经启动成功。如果你的服务启动需要一些加载时长(例如初始化日志,等待其他调用的服务启动成功)才代表服务启动成功,则可以用这个探针。如果探针检测失败,该 Pod 就会被杀死重启。在没有配置该探针的情况下,默认不会杀死 Pod 。在启动探针运行时,其他所有的探针检测都会失效。
探针名称 在哪个环节触发 作用 检测失败对Pod的反应
启动探针 Pod 运行时 检测服务是否启动成功 杀死 Pod 并重启
存活探针 Pod 运行时 检测服务是否崩溃,是否需要重启服务 杀死 Pod 并重启
可用探针 Pod 运行时 检测服务是不是允许被访问到 停止Pod的访问调度,不会被杀死重启
检测方式
1、ExecAction 通过在 Pod 的容器内执行预定的 Shell 脚本命令。如果执行的命令没有报错退出(返回值为0),代表容器状态健康。否则就是有问题的
我们来新建一个文件,vi shell-probe.yaml,内容如下:
apiVersion: v1 kind: Pod metadata: labels: test: shell-probe name: shell-probe spec: containers: - name: shell-probe image: registry.aliyuncs.com/google_containers/busybox args: - /bin/sh - -c - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600 livenessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5

然后执行下面的命令,查看情况:
kubectl apply -f liveness.yaml kubectl get pods | grep liveness-exec kubectl describe pods liveness-exec

2、TCPSocketAction 这种方式是使用 TCP 套接字检测。 Kubernetes 会尝试在 Pod 内与指定的端口进行连接。如果能建立连接(Pod的端口打开了),这个容器就代表是健康的,如果不能,则代表这个 Pod 就是有问题的。
创建文件如下,tcp-probe.yaml:
apiVersion: v1 kind: Pod metadata: name: tcp-probe labels: app: tcp-probe spec: containers: - name: tcp-probe image: nginx ports: - containerPort: 80 readinessProbe: tcpSocket: port: 80 initialDelaySeconds: 5 periodSeconds: 10

类似的:
kubectl apply -f tcp-probe.yaml kubectl get pods | grep tcp-probe kubectl describe pods tcp-probe

进入到容器内部:
kubectl exec -it tcp-probe-- /bin/sh

更新apt-get并安装vim:
apt-get update apt-get install vim -y vi/etc/nginx/conf.d/default.conf

修改nginx文件配置,把80端口修改为8080,然后重载一下nginx:
nginx -s reload

看一下状态:
kubectl describe pod tcp-probe

3、HTTPGetAction 这种方式是使用 HTTP GET 请求。Kubernetes 会尝试访问 Pod 内指定的API路径。如果返回200,代表容器就是健康的。如果不能,代表这个 Pod 是有问题的。
添加http-probe.yaml文件:
apiVersion: v1 kind: Pod metadata: labels: test: http-probe name: http-probe spec: containers: - name: http-probe image: registry.cn-beijing.aliyuncs.com/zhangrenyang/http-probe:1.0.0 livenessProbe: httpGet: path: /liveness port: 80 httpHeaders: - name: source value: probe initialDelaySeconds: 3 periodSeconds: 3

然后,运行并查看状态:
kubectl apply -f ./http-probe.yaml kubectl describe pods http-probe kubectl replace --force -f http-probe.yaml

Dockerfile内容如下:
FROM node COPY ./app /app WORKDIR /app EXPOSE 3000 CMD node index.js

【《前端运维》五、k8s--3灰度发布、滚动更新与探针】node服务文件如下:
let http = require('http'); let start = Date.now(); http.createServer(function(req,res){ if(req.url === '/liveness'){ let value = https://www.it610.com/article/req.headers['source']; if(value =https://www.it610.com/article/=='probe'){ let duration = Date.now()-start; if(duration>10*1000){ res.statusCode=500; res.end('error'); }else{ res.statusCode=200; res.end('success'); } }else{ res.statusCode=200; res.end('liveness'); } }else{ res.statusCode=200; res.end('liveness'); } }).listen(3000,function(){console.log("http server started on 3000")});

好了今天的内容就到这里了。

    推荐阅读