前提条件

kubernetes部署Java微服务时,采用滚动方式更新,当服务升级发布时,新的pod被创建Nacos上服务实例加1,旧的pod被关闭Nacos上服务实例减1,Nacos通过心跳检测机制,将旧的pod实例下线。在关闭旧的pod到下线pod之间,存在一定的时间差,导致服务消费方调用接口会发生报错的情况,对业务操作造成一定影响。上述情况就有可能导致服务请求到已经被关闭的实例上,虽然通过重试机制可以解决掉这个问题,但这种解决方案会出现重试,在一定程度上会导致用户侧请求变慢。这时就需要进行优雅的下线操作了,具体可参考k8s优雅关闭服务

Nacos心跳检测时间

Nacos目前支持临时实例使用心跳上报方式维持活性,发送心跳的周期默认是5秒,Nacos服务端会在15秒没收到心跳后将实例设置为不健康,在30秒没收到心跳时将这个临时实例摘除。这里要注意30秒这个时间

Nacos下线服务

nacos官网提供的下线接口命令如下

http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=user-center&clusterName=DEFAULT&groupName=DEFAULT_GROUP&ip=10.48.9.89&port=5000&ephemeral=true&weight=1&enabled=false&namespaceId=f3646f8c-a1c7-474c-9ea4-51adssaf54s

说明:

  • 127.0.0.1:8848 nacos注册地址

  • user-center 注册应用名称

  • 10.48.9.89 注册应用地址

  • 5000 注册应用端口号

  • enabled=false 下线 enabled=true 上线

  • namespaceId 命令空间,默认使用public命名空间则不写这个

通过分析nacos下线应用地址,需要如下参数:nacos注册地址、应用名称、应用所在主机ip、应用端口号、命令空间(public不需要),考虑到应用所在主机ip是Pod IP,这个需要从pod容器中获取,因此不能在PreStop中使用命令行的形式,也就是如下的形式

curl -x PUT http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=user-center&clusterName=DEFAULT&groupName=DEFAULT_GROUP&ip=10.48.9.489&port=5000&ephemeral=true&weight=1&enabled=false&namespaceId=f3646f8c-a1c7-474c-9ea4-51adssaf54s

原因

nacos地址、应用名称、应用端口号可以写死,应用所在主机IP也就是Pod IP没法获取

  • PreStop是配置在Daeployment中的,Pod的数量和IP都是不固定的

  • Pod IP设置成环境变量的形式,也只能是在Pod容器中使用,在PreStop中还是获取不到Pod IP

容器微服务优雅下线方式

综合以上分析,采取的办法是在构造镜像的时候新增一个preStop.sh脚本,内容写上nacos下线的命令, 然后在PreStop命令行中执行这个脚本文件,在这个过程中若是有些参数值无法从环境变量中获取,则需要增加这些参数的环境变量

preStop脚本

注意脚本中的sleep 45命令,这个是确保应用从nacos中下线使用的,默认是30秒,具体看开头nacos心跳检测时间,sleep设置时间大于30秒就可以,这里设置45秒

preStop.sh脚本中使用的变量有些是默认提供的,有些是需要提前设置环境变量的,取值是容器中的值(这里是使用命令获取需要的值,生产建议通过ENV环境变量来获取)

#!/bin/bash
NACOSADDR=nacos-headless.tools-env.svc.cluster.local:8848
NAMESPACE=a4df1666-a85f-4036-9b8f-e94d4081f76f
PODNAME=$(hostname|awk -F'-' '{for (i = 1; i <= NF - 2; i++) printf("%s-", $i); printf("\n")}'| sed 's/-\{1,\}$//')
PODIP=$(hostname -I | awk '{print $1}')
PORT=$(ss -ntl | awk '{print $4}' | grep -v Local | awk -F : '{print $4}')

result=$(curl -X PUT "http://${NACOSADDR}/nacos/v1/ns/instance?serviceName=${PODNAME}&clusterName=DEFAULT&groupName=DEFAULT_GROUP&ip=${PODIP}&port=${PORT}&enabled=false&namespaceId=${NAMESPACE}")

echo "输出curl执行结果result:${result}"

if [ ${result} = "ok" ]; then
  echo "执行成功"
  sleep 45
  exit 0
else
  echo "执行失败"
  exit 1
fi

Dockerfile镜像文件

修改Dockerfile文件,增加这个preStop脚本,设置可执行权限

...
ADD preStop.sh /home/preStop.sh
RUN chmod 777 /home/preStop.sh
...

kubernetes配置

配置服务滚动更新,添加livenessProbe、readinessProbe探针,以及添加容器生命周期钩子,然后调整优雅终止宽限期(terminationGracePeriod限定时间)由默认的30秒调整为60秒,确保这个时间大于sleep 45的时间

...
    spec:
      imagePullSecrets:
        - name: harborsecret
      terminationGracePeriodSeconds: 60
...
          lifecycle:
            preStop:
              exec:
                command: ['/bin/sh','-c','/home/preStop.sh']
...                

开始观察滚动更新时,nacos中的应用是否有一个ip状态的变成"上线"(显示这个表示应用是下线状态),等待30秒后就看不到这个ip应用了

然后观察k8s中pod的消失,等了60秒后才开始取消一个pod,最后查看事件events,发现并没有FailedPreStopHook,这是正常的,因为只有报错的情况下才会出现FailedPreStopHook,正常情况下不会出现这个

k8s-wfwyyxx-1.png

文章作者: 鲜花的主人
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱吃可爱多
Kubernetes Tools Kubernetes Tools
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝