Kubernetes运维之微服务生产环境采坑分享
生产环境经验
1、限制了容器资源,还经常被杀死?2、滚动更新之健康检查重要性3、滚动更新之流量的丢失
先说一下第一个问题,限制容器资源,还经常去杀死的原因?
就是说部署的java应用,不一会就重启了,其实重启就是在重建了,这就意味着你的pod是不健康的,然后k8s重新再帮你去拉取了,这样的话就要去找问题去排查了,说白了其实就是被杀死了,可以通过describe去查看一下事件,一般都会能看到,由于健康状态检查没通过,然后再去拉取的,因为是java应用,由于堆内存溢出,然后被kill掉,在日志最后一行会出现一个kill的字段,看一下它重启的原因,之前我遇到的是它的堆内存没限制住,它本身有jvm,jvm主要有内存做交换数据的,它堆内存主要是性能设计的一个体现,所以他的堆内存很容易就会超出,超出之后呢,很可能会被k8s杀死掉,为什么k8s会kill它,因为它超出了限制,默认容器会使用宿主机所有的资源,如果不做资源限制,会影响整个宿主机,然后整个宿主机资源不够会实现飘移,会转移到其他主机上,然后再异常,可能会起到一种雪崩的效应,所以一般我们都是要做资源限制的,难道这个限制,限制不了java应用吗?其他它是限制不住的,虽然k8s部署应用还是很方便的,但是部署java应用还是不兼容的,
比如它不能识别当前java容器的限制,就是不能识别我们指定的限制,也就是你在yaml去限制的,容器的堆内存没有限制,它就会超出这个限制,如果超出这个limits的限制,k8s就会把它杀掉,k8s自身它有这个策略,如果超出这个容量的限制,就会帮你杀掉,再帮你拉起。
面对java这样的堆内存稍微有一点流量突发,就可能资料利用率就上来了,所以这个幅度还是比较大的,这样就会导致k8s杀掉,然后再拉起,这样循环,可能一天几百次的这样效果。
说到这个问题,怎么来解决docker的资源限制,其实这个资源限制还是用的这个docker来做的,只不过k8s把他转换成了,只不过k8s去调用接口去做一些限制,其实就是怎么让docker去识别java堆内存的限制,来解决这个问题。
来解决这个问题有两种方案,也就是为这个java再配置堆内存的使用
配置java堆内存的使用
Java -Xmx最大的堆内存使用,一个是-Xms是初始的堆内存使用
一般都设置一个最大的堆内存使用,如果不设置超出这个设置会不断使用宿主机的一个内存,而导致物理内存不够用,而出现堆内存溢出的现象,这个很普遍,我们就用这个java -Xmx就是当快用满时,它会有一个垃圾回收,再进行循环的使用,这样能保证这个java应用稳定的运行,所有我们在yaml去配置资源限制肯定是不够的,我们必须为这个java去设置这个堆内存,我们不可能手动的在Dockerfile去写这个吧,一般在dockerfile去传入这个值,在yaml文件里设置一个变量
env: - name: JAVA_OPTS value: "-Xmx1g "
下面就是我们之前配置的容器资源的一些限制这个变量就会就会传入pod里面,也就是这个构建镜像的容器里,也就是起到容器CMD 命令下去传入$JAVA_OPTS的变量,它会调用我们系统的系统变量,这个系统变量已经赋予它值了,所以它能直接饮用这个变量了,去起到这个应用,从而设置这个堆内存大小,这个值建议要比limlts要小一点,小个10%吧,因为超过这个limits限制就会杀死,再拉取了。
一般设置好,重建一下镜像,进入容器中去查看一下进程就可以看到,其他的也是这么设置的
第二个问题,滚动更新之健康检查的重要性
滚动更新是k8s的默认策略,一般咱们部署到k8s之后第一个会使用到的,当你配置了健康检查时,滚动更新会根据probe的状态来判断是不是继续的更新允许接入流量,也就是你当前的应用是不是提供服务,这样你滚动更新的过程中,才会确保你是不是有可用的节点,保证一个平滑的升级,所以这是滚动更新设置的一个之初,那健康状态检查是很重要的
健康状态检查在滚动更新启动什么作用呢?
列入一个副本,启动之后提供业务需要一分钟才能提供服务,比如java启动比较慢,如果没有健康状态检查,来确保他是不是准备好,就直接认为它准备好了,这期间启动起来,一分钟之内它是无法提供服务的,所以新来的流量肯定是无法处理的,这是第一种情况,第二种情况,由于人为的配置错误,比如连接不上数据库了,或者连接不上其他地方了,或者配置文件哪里写错了,那触发一个滚动更新,那这个pod呢全部滚动更新完成了,结果都是由问题的,这样的话,新副本都把旧副本替换掉了,这样的话,生产环境中后果很严重的,很多服务都会无法提供了,所以在配置滚动更新的时候,健康状态检查一定要配上,那配上了健康状态检查之后,新的副本检查完成之后才会转发新的流量,如果它没有通过它不会全部替换的,也就是它不会继续更新了,因为它是有一个可用节点的限制,如果可用节点达不到这个数,就不会继续更新,健康状态检查有两种类型,readinessprobe是就绪的检查,这两种检查方式也有不同的方式,比如http,探测一个url,还有tcp socket,端口的探测,还有一个执行shell命令,执行一个shell命令,判断一个返回值,所以提供者三种健康状态检查的方法,readinessprobe就绪检查也就是你的Pod检查失败,如果是http,可以通过一个页面去探测,判断这个返回状态码,如果探测这个本地的端口不通的话,它不会让它加入service后面,因为service作为你整个的统一访问入口嘛,如果它不通过的话,新的流量也不会转发给它,这就是就绪检查,如果健康状态不通过不会给你转发新的流量,另外就是initialDelaySeconds:60,就是检查60秒,因为一般java应用启动也就是一分钟左右,还有一个periodSeconds:10,也就是没通过10秒钟在做一次,然后就是livenessProbe:存活检查。
也就是检查失败它会杀死容器,根据你重启策略,一般是重建,再给你新拉取一个容器,再判断有没有准备好,判断的方法也是根据端口的探测这些的,或者特可以用其他两种方法,http,exec,所以一般这两个都要配置上,就绪检查呢就是不为你分配新的流量,存活检查就是去帮你重新拉取。
最后一个问题滚动更新之丢失的流量
一般就是连接拒绝,响应错误,调用不到
一般滚动更新是关闭现有的pod,再起一新的pod,关闭现有的其实是就是删除了一个pod,然后apiserver会通知给kubelet,然后kubelet会关闭这个容器,然后从service后端摘掉,就不分发新的流量了,然后移除掉,然后再告诉kube-proxy,你可以处理新的转发规则了,然后调度到节点上,其实这也是一个pod的下线周期
另外再转发的过程中,转发新的pod时间段里,它是有间隔的,关闭pod之后会有一个等待时间,在这个时间呢,可能还会接入一些新的流量,但是它的服务已经不再处理新的请求了,所以会导致连接拒绝了,怎么去解决这个问题呢,实际上readiness探针在整个过程中并起到关键的作用,一旦endpoint收到pod 的删除事件后,这已经就与readiness探测结果不相关了
怎么保证它优雅的去处理呢?
其实之需要在关闭这个pod 时加个休眠的时间,其实就可以解决这个问题了,在关闭和启动都是有一个钩子存在的,所有可以在关闭容器前,执行这个钩子,钩子这个定义一个shell,y也可以定义一个http请求,也就是支持者两种类型,也就是在container同级,env这里
休眠5秒也就是你关闭的容器不会马上退出,然后休眠5秒钟,再去关闭着应用,这5秒能够足够让kube-proxy刷新这个规则,这样的话,就不会将新加入的流量转发到这个刚关闭的pod上,增加这个钩子就能暂缓你关闭pod的时间,从而让kube-proxy增加刷新规则的时间,
添加
lifecycle :preStop : exec : command : - sh - -c - "sleep 5"
这样,你不需要修改你应用的代码,这样的话,滚动更新就不会转发即将关闭的pod上了,所以也能解决这个相关的问题了。