一、灰度发布
灰度发布是一种发布方式,也叫金丝雀发布,起源是矿工在下井之前会先放一只金丝雀到井里,如果金丝雀不叫了,就代表瓦斯浓度高。原因是金丝雀对瓦斯气体很敏感。灰度发布的做法是:会在现存旧应用的基础上,启动一个新版应用,但是新版应用并不会直接让用户访问。而是先让测试同学去进行测试。如果没有问题,则可以将真正的用户流量慢慢导入到新版,在这中间,持续对新版本运行状态做观察,直到慢慢切换过去,这就是所谓的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.yamlfor ((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;sleep30;rm -rf /tmp/healthy;sleep600
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-getinstall vim -yvi /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
EXPOSE3000
CMD node index.js
node服务文件如下:
let http = require('http');
let start= Date.now();
http.createServer(function(req,res){if(req.url ==='/liveness'){
let value= req.headers['source'];if(value ==='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")});
好了今天的内容就到这里了。