"K8S为咱们提供自动部署调度应用的能力,并经过健康检查接口自动重启失败的应用,确保服务的可用性,但这种自动运维在某些特殊状况下会形成咱们的应用陷入持续的调度过程致使业务受损,本文就生产线上一个核心的平台应用被K8S频繁重启调度问题展开剖解,抽丝剥茧一步步从系统到应用的展开分析,最后定位到代码层面解决问题"html
在搭建devops基础设施后,业务已经全盘容器化部署,并基于k8s实现自动调度,但个别业务运行一段时间后会被k8s自动重启,且重启的无规律性,有时候发生在下午,有时发生在凌晨,从k8s界面看,有的被重启了上百次:java
k8s是根据pod yaml里定义的重启策略执行重启,这个策略经过: .spec.restartPolicy 进行设置,支持如下三种策略:node
出问题的应用是走CICD自动打包发布,Yaml也是CD环节自动生成,并无显示指定重启策略,因此默认采用Always策略,那么k8s在哪些状况会触发重启呢,主要有如下场景:docker
出问题的应用正常运行一段时间才出现的重启,而且POD自己的Yaml文件以及所在的namespace并没设置CPU上限,那么能够排除:1 3 4 6, 业务是采用Springboot开发的,若是无端退出,JVM自己会产生dump文件,但由重启行为是K8s本身触发的,即便POD里产生里dump文件,由于运行时没有把dump文件目录映射到容器外面,因此无法去查看上次被重启时是否产生里dump文件,因此2 5都有可能致使k8s重启该业务,不过k8s提供命令能够查看POD上一次推出缘由,具体命令以下:windows
NAMESPACE=prod SERVER=dts POD_ID=$(kubectl get pods -n ${NAMESPACE} |grep ${SERVER}|awk '{print $1}') kubectl describe pod $POD_ID -n ${NAMESPACE}
命令运行结果显示POD是由于memory使用超限,被kubelet组件自动kill重启(若是reason为空或者unknown,多是上述的缘由2或者是不限制内存和CPU可是该POD在极端状况下被OS kill,这时能够查看/var/log/message进一步分析缘由),CICD在建立业务时默认为每一个业务POD设置最大的内存为2G,但在基础镜像的run脚本中,JVM的最大最小都设置为2G:后端
exec java -Xmx2g -Xms2g -jar ${WORK_DIR}/*.jar
在分析应用运行的环境和,咱们进一步分析应用使用的JVM自己的状态,首先看下JVM内存使用状况命令: jmap -heap {PID}
JVM申请的内存: (eden)675.5+(from)3.5+(to)3.5+(OldGeneration)1365.5=2048mb理论上JVM一启动就会OOMKill,但事实是业务运行一段时间后才被kill,虽然JVM声明须要2G内存,可是没有当即消耗2G内存,经过top命令查看:PS: top和free命令在docker里看到的内存都是宿主机的,要看容器内部的内存大小和使用,可使用下列命令:api
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
当配-Xmx2g -Xms2g时,虚拟机会申请2G内存,但提交的页面在首次访问以前不会消耗任何物理存储,该业务进程当时实际使用的内存为1.1g,随着业务运行,到必定时间后JVM的使用内存会逐步增长,直到达到2G被kill。内存管理相关文章推荐:Reserving and Committing MemoryJvmMemoryUsageoracle
执行命令:运维
jmap -dump:format=b,file=./dump.hprof [pid]
导入JvisualVM分析,发现里面有大量的Span对象未被回收,未被回收的缘由是被队列里item对象引用:隔断时间执行:异步
jmap -histo pid |grep Span
发现span对象个数一直在增长,span属于业务工程依赖的分布式调用链追踪系统DTS里的对象,DTS是一个透明化无侵入的基础系统,而该业务也没有显示持有Span的引用,在DTS的设计里,Span是在业务线程产生,而后放入阻塞队列,等待序列化线程异步消费,生产和消费代码以下:从以上代码看,Span在持续增长,应该就是消费者线程自己的消费速度小于了生产者的速度,消费线程执行的消费逻辑是顺序IO写盘,按照ECS普通盘30-40m的IOPS算,每一个Span经过dump看到,平均大小在150byte,理论上每秒能够写:3010241024/150=209715,因此不该该是消费逻辑致使消费率降缓,再看代码里有个sleep(50)也就是每秒最多能够写20个Span,该业务有个定时任务在运行,每次会产生较多的Span对象,且若是此时有其余业务代码在运行,也会产生大量的Span,远大于消费速度,因此出现了对象的积压,随着时间推移,内存消耗逐步增大,致使OOMKill。dump该业务的线程栈:
jstack pid >stack.txt
却发现有两个写线程,一个状态始终是waiting on condition,另外一个dump屡次为sleep:可是代码里是经过Executors.newSingleThreadExecutor(thf);起的单线程池,怎么会出现两个消费者呢? 进一步查看代码记录,原来始终11月份一次修改时把发送后端的逻辑集成到核心代码里,该功能在以前的版本里采用外部jar依赖注入的方式自动装配的,这样在如今的版本中会出现两个Sender对象,其中自动建立的Sender对象没有被DTS系统引用,他里面的队列始终未empty,致使旗下的消费者线程始终阻塞,而内置的Sender对象由于Sleep(50)致使消费速度降低从而出现堆积,Dump时是没法明确捕获到他的running状态,看上去一直在sleep,经过观察消费线程系列化写入的文件,发现数据一直在写入,说明消费线程确实是在运行的.
经过代码提交记录了解到,上上个版本业务在某些状况会产生大量的Span,Span的消费速度很是快,会致使该线程CPU飙升的比较厉害,为了缓解这种状况,因此加了sleep,实际上发现问题后业务代码已经进行优化,DTS系统是不须要修改的,DTS应是发现问题,推进业务修复和优化,基础系统的修改应该很是慎重,由于影响面很是广。 针对POD的最大内存等于虚拟机最大内存的问题,经过修改CD代码,默认会在业务配置的内存大小里加200M,为何是200M不是更多呢?由于k8s会计算当前运行的POD的最大内存来评估当前节点能够容量多少个POD,若是配置为+500m或者更多,会致使K8S认为该节点资源不足致使浪费,但也不能过少过少,由于应用除了自己的代码外,还会依赖部分第三方共享库等,也可能致使Pod频繁重启.
上述问题的根因是人为下降了异步线程的消费速度,致使消息积压引发内存消耗持续增加致使OOM,但笔者更想强调的是,当咱们把应用部署到K8S或者Docker时,**POD和Docker分配的内存须要比应用使用的最大内存适当大一些**,不然就会出现能正常启动运行,但跑着跑着就频繁重启的场景,如问题中的场景,POD指定里最大内存2G,理论上JVM启动若是当即使用里2G确定当即OOM,开发或者运维能当即分析缘由,代价会小不少,可是由于现代操做系统内存管理都是VMM(虚拟内存管理)机制,当JVM参数配置为: -Xmx2g -Xms2g时,**虚拟机会申请2G内存,但提交的页面在首次访问以前不会消耗任何物理存储,**因此就出现理论上启动就该OOM的问题延迟到应用慢慢运行直到内存达到2G时被kill,致使定位分析成本很是高。另外,对于JVM dump这种对问题分析很是重要的日志,必定要映射存储到主机目录且保证不被覆盖,否则容器销毁时很难去找到这种日志。