记一次生产K8S平滑发布问题解决方案

@[TOC]记一次K8S平滑发布问题

记一次生产K8S平滑发布问题解决方案

刚到公司不久,发现一个现象。所有正常迭代都会等到晚上10点过后发布,后来跟同事聊天发现,原来是生产环境发布不平滑,每次发布都会产生一些异常,怕影响用户体验。
问题表现:生产环境服务每次发布都会在服务端产生一些BeanCreationNotAllowedException,一小段时间后自动恢复。

问题分析阶段-通过表象看原理

通过生产日志报错以及Spring源码发现BeanCreationNotAllowedException产生条件:在spring容器处于销毁阶段,但仍然尝试从容器中获取Bean。
以下是源码截图,该标识位在容器关闭时会变成true。
异常抛出截图
容器已经销毁,但是仍然还会获取bean,结合日志,发现该表现的本质是容器关闭了,但是仍然有请求打到已经关闭的实例上

问题1:为什么容器都关闭了,仍然有请求进来

问题1原理分析

这个问题要从请求的整个网络链路来看:

  1. 客户端使用HttpClient并使用了连接池
  2. 服务调用有两种方式work内网域名serviceName(基于k8s集群的一种虚拟ip)

第一点,连接池本身是一种缓存,做一个假设,如果连接池中的连接是缓存着旧实例的连接,而且客户端一直不知道服务端已经要下线了,这时候一直有请求打到老的实例就可以解释了。
第二点,分为两种请求方式

  1. 通过.work内网域名调用最终是走到ingress(发布在k8s集群中的nginx服务),有ingress转发到具体实例。nginx是靠监听端口实现反向代理的,客户端肯定是和nginx建立连接,每次请求走nginx转发,K8S在实例下线的时候会通知所有ingress服务在负载均衡中移除实例,所以这种请求方式可以排除
  2. 通过serviceName调用,这种方式在K8s中有两种常用模式UDP和VXLAN,我们公司采用UDP,所以今天着重讨论这个。UDP模式的负载均衡是很弱的,它是通过维护iptables的出入规则来达到简单的负载,但也仅仅是在建立连接时根据iptables规则选择一个具体的实例建立连接(类似客户端负载均衡),下面是UDP网络架构图:
    UDP网络架构
    从网络架构图可以看到,UDP模式iptables和nginx最大区别一个是客户端负载均衡,一个是服务端负载均衡,而TCP连接必须是端到端,要是实体或虚拟的网卡(iptables是没有的,只有到了具体的实例上才有虚拟网卡),该结论线下我是自己搭建了建议k8s,然后进入docker容器查看了TCP连接进行过验证的,但是不是重点就不赘述。
问题1结论

实例下线后仍然有请求打过来,是因为客户端连接池使用serviceName调用导致

问题1解决方案

最简单解决方案,所有服务调用都使用.work域名就好了。但是显然这得靠人为控制,我们还是得从技术上根本解决问题1。所以接着往下分析,还有一个问题没搞定。

问题2:为什么一小段时间后异常就恢复了

按照我们问题1 的分析,连接池缓存了旧实例,那么服务应该会一直调用失败啊,至少也出现点无法连接之类的IOException吧,怎么会自动恢复了。

问题2原理分析

在研究HttpClient连接池过程中发现有这么一个配置:
连接池重试配置
连接池是支持重试机制的,我们再深入一下源码看下什么情况下会重试,我们从最里层代码往外看:
HttpClient核心execute方法
这是HttpClient核心execute方法,可以看到当发生IOException时,调用了一个abortConnection方法,这个方法的作用很关键,关闭当前连接并回收当前连接,也就是说不会放回池子里了。再来看下重试是怎么做的:
重试执行器
这是HttpClient的重试处理器,可以看到这里也是捕获了IO异常,同时去判断是否重试,结果当然就是重试,所以问题2的结论很明显了。

问题2结论

服务之后报错一小段时间就自动恢复,是因为HttpClient的重试机制。在实例被关闭之后,客户端发起的请求会受到IO异常,这时候就会很快的淘汰掉出现异常的连接,所以看起来服务自动恢复了

问题原因找到了,怎么解决问题

需要解决哪些问题?

  1. K8S滚动发布基本原理要了解,K8S会做什么?我们在这个过程中可以做些什么?
  2. 当一个实例要下线时,接收到的请求怎么处理完,同时如何以一个客户端可以感知到的方式拒绝新的请求
  3. 客户端如何感知服务端的拒绝,同时做出合适的处理

怎么解决上述三个问题?

第一个问题:K8S的发布流程

首先看一下我们生产的k8s配置:
生产preStop配置
preStop配置是K8S的一个钩子,可以在实例关闭之前让我们做一些自己的事情,同时有一个参数值得关注:terminationGracePeriodSeconds使用默认值30s,该参数表示K8S允许POD优雅关闭的时间周期,什么意思?请看下面的手绘图,表名发布时候实例的状态变更:
生产配置,发布时实例状态变化图
图中可以看到我们的配置其实就是让实例被强杀。这样的配置肯定是不合理的,但是为我们解决第二个问题提供了思路,我们是可以在服务关闭前做一些事情的

第二个问题:服务端如何平滑发布
平滑发布调研

我们生产环境使用的是Undertow应用服务器,搜索一下Undertow平滑发布,发现有不少现成的例子,该服务器是支持平滑发布的,而且支持自定义。请看截图:
undertow平滑发布处理器
undertow平滑发布处理器
该处理器是undertow自带的,我们只需要通过以下代码将该处理器添加到Undertow处理请求的流程中即可,注意UndertowGracefulShutdown需要实现HandlerWrapper:
在这里插入图片描述
接下来我们看下GracefulShutdownHandler的核心方法:

  1. shutdown() 改变了shutdown标识位
  2. awaitShutdown() 等待所有请求处理完成
  3. handleRequest() 处理请求的核心方法,可以看到当shutdown=true时,所有请求返回503
结论

从调研情况看Undertow的平滑发布完全满足我们第二个问题的需求,处理完已接收的请求同时新进来的请求返回503(这是一个客户端可以感知的状态)。
但是有一个重要问题,怎么触发这些核心的平滑发布方法

平滑发布方案

这时候preStop就要发挥它该有的作用了,我们将生产preStop修改成这样:sleep 10 && kill -15 1
sleep 10是干啥的?这是给K8S时间去异步移除集群中所有负载均衡和iptables规则。
kill -15 1又是干啥的?在docker容器中我们的java进程号是1,这句就是通知我们的java进程需要你处理完手上的事情释放资源并关闭。
接下来就是最终奥义,怎么触发这些核心的平滑发布方法
kill -15 1对于我们的应用程序是可感知的,我们只需要做一件事,在服务端创建一个ApplicationListener,去监听ContextCloseEvent,然后我们就可以去触发平滑发布的核心方法,从而完成对已接收请求的处理,同时拒绝新请求。

第三个问题:客户端收到503怎么办?

前面有一张截图大家可能没太注意,再看一遍:
503重试机制
一开始我以为自己需要重写一个类似RetryExec的重试处理器去处理503,不过显然我多虑了,开发者早已看透了一切。在阅读源码时比较好奇除了RetryExec和MainClientExec,还有哪些实现类实现了ClientExecChain接口,这时候就被我看到了ServiceUnavailableRetryExec,一看就是处理503 Service Unavailable的。

第三个问题解决方案

解决方案就是大家看到的,截图中设置一下503处理策略即可。


总结

一个看似简单的异常,背后却是一系列的问题、原理、技术方案,本人解决问题之后内部整理过一次文档,但是后来自己复盘之后还是觉得思路不够清晰,所以借此机会又重新梳理了一下结题思路。
没有系统的将方案代码贴出来,是因为每家公司情况不同,我们有一些自己的定制化封装,所以将排查问题的思路和关键技术点罗列出来,希望给遇到相似问题的同学一点帮助。
感谢大家来阅读我的第一篇博客!!!

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值