Kubernetes 集群中,业务通常采用 Deployment + LoadBalancer 类型 Service 的方式对外提供服务,其典型部署架构如图 1 所示。这种架构部署和运维都十分简单方便,但是在应用更新或者升级时可能会存在服务中断,引发线上问题。今天我们来详细分析下这种架构为何在更新应用时会发生服务中断以及如何避免服务中断;
业务部署图
Deployment 滚动更新时会先创建新 pod,等待新 pod running 后再删除旧 pod。
服务中断示意图
中断原因
Pod running后被加入到Endpoint后端,容器服务监控到Endpoint变更后将Node加入到SLB后端。 此时请求从SLB转发到Pod中,但是Pod业务代码还未初始化完毕,无法处理请求, 导致服务中断, 如上图所示。
解决办法
为Pod配置就绪检测,等待业务代码初始化完毕后再将node加入到SLB后端;
在删除旧 pod 过程中需要对多个对象(如 Endpoint、ipvs/iptables、SLB)进行状态同步,并且这些同步操作是异步执行的,整体同步流程如下图所示。
Deployment更新时序图
1 . Pod状态变更: 将Pod设置为Terminating状态,并从所有Service的Endpoints列表中删除。 此时, Pod停止获得新的流量, 但在Pod中运行容器不会受到影响;
2 . 执行preStop Hook: Pod删除时会触发preStop Hook, preStop Hook 支持bash脚本, TCP或HTTP请求;
3 . 发送SIGTERM信号: 向Pod中容器发送SIGTERM信号;
4 . 等待指定时间: terminationGracePeriodSeconds字段用于控制等待时间,默认为30s,该步骤与preStop Hook同时执行, 因此terminationGracePeriodSeconds需要大于preStop的时间,否则会出现preStop未执行完毕,Pod就被kill的情况;
5 . 发送SIGKILL信号: 等待指定时间,向Pod中的容器发送SIGKILL信号,删除Pod;
中断原因:
上述1,2,3,4步骤同时执行, 因此可能存在Pod收到SIGTERM信号并停止工作后,还未从Endpoints中移除情况,此时,请求从SLB转发到Pod中,而Pod已经停止工作,因此会出现服务中断,如图4所示;
服务中断示意图
解决办法:
为Pod配置preStop Hook,使Pod收到SIGTERM时sleep一段时间而不是立刻停止工作,从而确保SLB转发流量还可以继续被Pod处理;
中断原因: 当Pod变为termintaing状态时,会从所有service的endpoint中移除该pod,kube-proxy会清理对应的iptables/ipvs条目, 而容器服务watch到endpoint变化后,会调用slb openapi移除后端,此操作会耗费几秒。由于这两个操作是同时进行,因此有可能存在节点上iptables/ipvs条目已经被清理, 但节点并没从slb移除情况。 此时,流量从slb流入,而节点上已经没有对应的iptables/ipvs规则导致服务中断, 如图5所示;
服务中断示意图
解决方法如下:
Cluster模式:
Cluster模式下kube-proxy会把所有业务Pod写入Node的iptables/ipvs中,如果当前Node没有业务Pod,则该请求会被转发给其他Node,因此不会存在服务中断;
Cluster模式请求转发示意图
Local模式:
Local模式下,kube-proxy会把Node上的Pod写入iptables/ipvs,当Node只有一个Pod且状态变为terminating时,iptables/ipvs会将该Pod记录移除。此时请求转发到这个Node时,无对应的iptables/ipvs记录,导致请求失败。这个问题可以通过原地升级来避免,即保证更新过程中Node上至少有一个Running Pod, 原地升级可以保障Node的iptables/ipvs中总会有一条业务Pod记录,因此不会产生服务中断,如下图所示:
ENI模式Service:
ENI模式绕过kube-proxy,将Pod直接挂载到SLB后端,因此不存在因为iptables/ipvs导致的服务中断;
ENI模式请求转发示意图
服务中断示意图
中断原因:
容器服务监控到Endpoints变化后,会将Node从SLB后端移除,当节点从SLB移除后,SLB对于继续发往该节点的长连接会直接断开,导致服务中断;
解决办法:
为SLB设置长链接优雅中断(依赖具体云厂商)
避免服务中断可以从Pod和Service两类资源入手;
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: default
spec:
containers:
- name: nginx
image: nginx
# 存活检测
livenessProbe:
failureThreshold: 3
initialDelaySeconds: 30
periodSeconds: 30
successThreshold: 1
tcpSocket:
port: 800
timeoutSeconds: 1
# 就绪检测
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 30
periodSeconds: 30
successThreshold: 1
tcpSocket:
port: 800
timeoutSeconds: 1
# 优雅退出
lifecycle:
preStop:
exec:
command:
- sleep
- 30
terminationGracePeriodSeconds: 60
注意: 需要合理设置就绪检测(readinessProbe)的检测频率,延时时间,不健康阈值等数据,部分应用启动时间本身较长,如果设置过短,会导致Pod反复重启;
Cluster模式(externalTrafficPolicy: Cluster)
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
externalTrafficPolicy: Cluster
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: nginx
type: LoadBalancer
容器服务会将集群中所有节点挂载到 SLB 的后端(使用 BackendLabel 标签配置后端的除外),因此会快速消耗 SLB quota。SLB 限制了每个 ECS 上能够挂载的 SLB 的个数,默认值为 50,当 quota 消耗完后会导致无法创建新的监听及 SLB。
Cluster 模式下,如果当前节点没有业务 pod 会将请求转发给其他 Node。在跨节点转发时需要做 NAT,因此会丢失源 IP。
Local模式(externalTrafficPolicy: Local)
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
externalTrafficPolicy: Local
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: nginx
type: LoadBalancer
# 需要尽可能的让每个节点在更新的过程中有至少一个的Running的Pod
# 通过修改UpdateStrategy和利用nodeAffinity尽可能的保证在原地rolling update
# * UpdateStrategy可以设置Max Unavailable为0,保证有新的Pod启动后才停止之前的pod
# * 先对固定的几个节点打上label用来调度
# * 使用nodeAffinity+和超过相关node数量的replicas数量保证尽可能在原地建新的Pod
# 例如:
apiVersion: apps/v1
kind: Deployment
......
strategy:
rollingUpdate:
maxSurge: 50%
maxUnavailable: 0%
type: RollingUpdate
......
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: deploy
operator: In
values:
- nginx
容器服务默认会将 Service 对应的 Pod 所在的节点加入到 SLB 后端,因此 SLB quota 消耗较慢。Local 模式下请求直接转发到 pod 所在 node,不存在跨节点转发,因此可以保留源 IP 地址。Local 模式下可以通过原地升级的方式避免服务中断,yaml 文件如上。
ENI模式(阿里云特有模式)
apiVersion: v1
kind: Service
metadata:
annotations:
service.beta.kubernetes.io/backend-type: "eni"
name: nginx
spec:
ports:
- name: http
port: 30080
protocol: TCP
targetPort: 80
selector:
app: nginx
type: LoadBalancer
Terway 网络模式下,通过设置 service.beta.kubernetes.io/backend-type: "eni" annotation 可以创建 ENI 模式的 SLB。ENI 模式下,pod 会直接挂载到 SLB 后端,不经过 kube-proxy,因此不存在服务中断的问题。请求直接转发到 pod,因此可以保留源 IP 地址。
三种svc模式对比如下图所示:
选用ENI模式的svc + 设定Pod优雅终止 + 就绪检测.
如果集群中slb数量不多且不需要保留源IP: 选用cluster模式 + 设定Pod优雅中止 + 就绪检测;
如果集群中slb数量较多或需要保留源IP: 选用local模式 + 设定Pod优雅终止 + 就绪检测 + 原地升级(保证更新过程中每个节点至少一个Running Pod);
作者 | 子白(阿里云开发工程师)、溪恒(阿里云技术专家)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。