`
前言
我们在使用k8s部署应用的时候,虽然k8s是使用滚动升级的,先启动一个新Pod 等这个新Pod运行成功后,再干掉旧Pod;在这个过程中Pod会一直接收请求的,如果在Pod被干掉的那一刻正好有一部分请求打进来了,那么Pod被杀死了,就不会给这个请求返回结果,就会导致客户端出现请求500错误,这样就做不到平滑升级了,我们要做的就是在Pod升级的时候不能或者尽量避免这种情况;
我们公司使用的是java,中间件用的是nacos,应用在启动时会注册到nacos,然后走应用之间的内部调用,服务会不间断的向注册中心nacos发送自己的心跳(3s) 以及在每个 Pod 服务里本地也有一份缓存映射表(也有一个窗口时间更新30s),服务在停止的时候,自然也会在nacos中进行下线 但是如果有请求在应用下线的这个窗口期发起的话,就会出现K8s Pod 服务已下线,但是 Nacos 在窗口期之内注册列表未更新,导致请求达到一个根本不存在的旧服务里导致请求返回404或者旧请求已经打到旧服务里,但是高峰期时,程序处理较慢,还没来及返回响应体,服务就被关闭了返回500;
我们使用的解决方案是,在应用下线的第一时间(Pod被删除)先进行在nacos的下线操作不让其接受新的请求,然后等待Pod已接收的请求处理完成后 再进行删除Pod;
这里会使用到的知识以及需要自身考虑的点有:
- k8s的prestop钩子(容器关闭前执行操作)
- 需要判断自己应用处理的请求的时间(基本上30s内都能处理完成 如果不放心的话调整成50s 但是这样的话也会相应的增加上线时长,需要注意)
- 需要在nacos(v2.x)中配置Nacos自动清理过期服务的过期时间(删除服务的元数据信息),防止请求过多/代码问题导致Pod的cpu打满 触发Pod的健康检查后 Pod重启以后依然是下线状态(不可用) 这样就出大问题了;
一、环境描述
名称 | 版本 | 部署方式 |
---|---|---|
kubernetes | v1.20.11 | 二进制 |
nacos | v2.0.3 | 集群模式 |
二、模拟请求报错
模拟请求报错就是不加任何配置直接使用测试脚本(这里用的是jmeter)不间断的去调用我们的应用,然后发布我们的新应用会有失败的请求
##现在的deployment文件为如下##
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: data-center
name: energy-order-api
labels:
app: energy-order-api
spec:
replicas: 3
selector:
matchLabels:
app: energy-order-api
template:
metadata:
labels:
app: energy-order-api
spec:
imagePullSecrets:
- name: harbor-secret
containers:
- name: energy-order-api
image: registry.xxxx/hqt-registry-pro/energy-order-api:P-1391-2023xxxx-15.47.45
imagePullPolicy: IfNotPresent
command: ["/bin/sh"]
args: ["-c","java -jar
-Xmx2688m
-Xms2688m
-Xmn961m
-XX:MaxMetaspaceSize=512m
-XX:MetaspaceSize=512m
-Xloggc:/logs/gc-%t.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump_$MY_POD_NAME.hprof
-XX:+PrintGCDetails
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap
-XX:NativeMemoryTracking=detail
-javaagent:/data/skywalking/skywalking-agent.jar=agent.service_name=energy-order-api,agent.instance_name=$MY_POD_NAME,collector.backend_service=internal-skywalking.xxxx.xxm:11800
-Dapollo.meta=http://apollo-configservice.infrastructure.svc.cluster.local:8080
-Denv=pro /data/app.jar;/sbin/tini -s"]
env:
#获取pod实例名称,因为一个pod可能会有多个副本,所以需要根据名称来进行区分;
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
requests:
memory: "4Gi"
cpu: "2000m"
limits:
memory: "4Gi"
cpu: "2000m"
volumeMounts:
- name: energy-order-api-logs
mountPath: /data/logs
subPathExpr: $(MY_POD_NAME)
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /actuator/info
port: 80
initialDelaySeconds: 70 #pod启动多长时间后开始去探测;
periodSeconds: 5 #每隔多长时间去探测一次;
failureThreshold: 6
readinessProbe:
httpGet:
path: /actuator/info
port: 80
initialDelaySeconds: 70
periodSeconds: 5
failureThreshold: 6
affinity:
#节点亲和性
nodeAffinity:
#硬策略
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
#标签键
- key: ResourcePool
operator: In
#标签值
values:
- core
#pod反亲和性配置
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- energy-order-api
topologyKey: kubernetes.io/hostname
volumes:
- name: energy-order-api-logs
persistentVolumeClaim:
claimName: energy-order-api-logs
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: energy-order-api-logs
namespace: data-center
spec:
accessModes:
- ReadWriteMany
storageClassName: alicloud-nas-subpath
resources:
requests:
storage: 1Gi
Pod启动成功如下:
设置30个线程开始去请求我们服务的接口
可以看到目前的请求都是成功的,此时我们修改镜像apply可以模拟下我们的服务发布,当新Pod启动后 删除旧Pod时注意观察请求是否有报错!
可以发现在删除pod时的这个动作会出现错误请求,随后就会正常
以上就是发版(新Pod替换旧Pod)的过程中会出现的问题;
三、配置优雅上下线
1.修改nacos配置
[root@iZbp1iz5ayf044rk5cqq26Z ~]# vim /hqtbj/hqtwww/nacos_workspace/conf/application.properties
#打开注释并修改
### The interval to clean expired metadata, unit: milliseconds.
nacos.naming.clean.expired-metadata.interval=5000
### The expired time to clean metadata, unit: milliseconds.
nacos.naming.clean.expired-metadata.expired-time=5000
保存后重启nacos
2.修改depolyment配置
需要添加k8s的prestop钩子,以及设置强制关闭pod的时间要比sleep的时间长
terminationGracePeriodSeconds: 40
如上命令的作用:
- 使用curl将注册到nacos的实例权重设置为0,设置为0后就不会再接受请求了,也可以调用nacos的下线接口,只需要将weight=0改为enabled=false即可;
- 不接受请求后sleep睡眠30秒 用于处理已经发送过来的请求;
- 然后再kill -SIGTERM进行优雅的关闭服务;
- 等待Pod中的服务完全停止,如果在 terminationGracePeriodSeconds 40s内 (默认 30s) 还未完全停止,就发送 SIGKILL 信号强制杀死进程(kill -9)。
#添加prestop钩子
lifecycle:
preStop:
exec:
command: ["/bin/sh","-c",'curl -X PUT "http://my-nacos.xxx.com/nacos/v1/ns/instance?serviceName=energy-order-api&ip=${POD_IP}&port=80&weight=0" && sleep 30 && PID=`pidof java` && kill -SIGTERM $PID && while ps -p $PID > /dev/null; do sleep 1; done']
#设置强制杀死Pod(kill -9)的时间,默认为30s
terminationGracePeriodSeconds: 40
完整deployment内容如下
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: data-center
name: energy-order-api
labels:
app: energy-order-api
spec:
replicas: 3
selector:
matchLabels:
app: energy-order-api
template:
metadata:
labels:
app: energy-order-api
spec:
imagePullSecrets:
- name: harbor-secret
containers:
- name: energy-order-api
image: registry.xxxx/hqt-registry-pro/energy-order-api:P-1391-2023xxxx-15.47.45
imagePullPolicy: IfNotPresent
command: ["/bin/sh"]
args: ["-c","java -jar
-Xmx2688m
-Xms2688m
-Xmn961m
-XX:MaxMetaspaceSize=512m
-XX:MetaspaceSize=512m
-Xloggc:/logs/gc-%t.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump_$MY_POD_NAME.hprof
-XX:+PrintGCDetails
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap
-XX:NativeMemoryTracking=detail
-javaagent:/data/skywalking/skywalking-agent.jar=agent.service_name=energy-order-api,agent.instance_name=$MY_POD_NAME,collector.backend_service=internal-skywalking.xxxx.xxm:11800
-Dapollo.meta=http://apollo-configservice.infrastructure.svc.cluster.local:8080
-Denv=pro /data/app.jar;/sbin/tini -s"]
env:
#获取pod实例名称,因为一个pod可能会有多个副本,所以需要根据名称来进行区分;
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
requests:
memory: "4Gi"
cpu: "2000m"
limits:
memory: "4Gi"
cpu: "2000m"
volumeMounts:
- name: energy-order-api-logs
mountPath: /data/logs
subPathExpr: $(MY_POD_NAME)
ports:
- containerPort: 80
lifecycle:
preStop:
exec:
command: ["/bin/sh","-c",'curl -X PUT "http://nacos.wonxxxnk.cc/nacos/v1/ns/instance?serviceName=energy-order-api&ip=${POD_IP}&port=80&weight=0" && sleep 30 && PID=`pidof java` && kill -SIGTERM $PID && while ps -p $PID > /dev/null; do sleep 1; done']
terminationGracePeriodSeconds: 40
livenessProbe:
httpGet:
path: /actuator/info
port: 80
initialDelaySeconds: 70 #pod启动多长时间后开始去探测;
periodSeconds: 5 #每隔多长时间去探测一次;
failureThreshold: 6
readinessProbe:
httpGet:
path: /actuator/info
port: 80
initialDelaySeconds: 70
periodSeconds: 5
failureThreshold: 6
affinity:
#节点亲和性
nodeAffinity:
#硬策略
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
#标签键
- key: ResourcePool
operator: In
#标签值
values:
- core
#pod反亲和性配置
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- energy-order-api
topologyKey: kubernetes.io/hostname
volumes:
- name: energy-order-api-logs
persistentVolumeClaim:
claimName: energy-order-api-logs
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: energy-order-api-logs
namespace: data-center
spec:
accessModes:
- ReadWriteMany
storageClassName: alicloud-nas-subpath
resources:
requests:
storage: 1Gi
3.重新apply deployment后测试
如上,现在Pod已经启动成功并且在nacos中也是可用状态
开始测试
如上当开始停止旧pod时, 会先调用我们配置的prestop钩子 如下 先把nacos中旧pod的权重改为0 不让其接受请求,然后再处理已接受的请求最后彻底关闭pod
可以看到整个流程下来是没有发现有请求失败的!
这是单对这一个order服务进行的测试,接下来需要走一遍整体下单的流程,调用多个服务进行测试的同时重新发布这个order服务看是否有失败的请求
4.整体(下单)测试流程验证是否生效
已启动的pod以及nacos状态如下:
开始测试
如上可以发现在停止Pod时,跟上面的结果是一样的,都是先把nacos中旧pod的权重改为0,然后等待处理请求再彻底关闭pod;
如下测试,整体一个下单流程再发布期间也是不回受到影响的,无报错
四、期间遇到的问题
如果不配置nacos清理元数据信息的话,会导致当cpu/内存使用超过限制而导致健康检查重启时(Pod实例自身重启而不是会起一个新pod),会出现即使pod重启完nacos里注册的服务权重是0/下线,导致服务直接不可用,只能手动再去启用!!所以下面在nacos的配置一定要进行使用!
文章来源:https://www.toymoban.com/news/detail-856889.html
如下:
服务因健康检查失败开始重启
此时的请求开始报错
服务的权重变为0,不接收请求
pod重启完成
发现nacos里注册的服务权重依然为0,并没进行接收请求
请求依旧报错
文章来源地址https://www.toymoban.com/news/detail-856889.html
到了这里,关于K8s+Nacos实现应用的优雅上下线【生产实践】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!