首发公众号:二进制社区,转载分割:binary0101@126.com
导读
"K8S为咱们提供主动部署调度利用的能力,并通过健康检查接口主动重启失败的利用,确保服务的可用性,但这种主动运维在某些非凡状况下会造成咱们的利用陷入继续的调度过程导致业务受损,本文就生产线上一个外围的平台利用被K8S频繁重启调度问题开展剖解,抽丝剥茧一步步从零碎到利用的开展剖析,最初定位到代码层面解决问题"
景象
在搭建devops基础设施后,业务曾经全盘容器化部署,并基于k8s实现主动调度,但个别业务运行一段时间后会被k8s主动重启,且重启的无规律性,有时候产生在下午,有时产生在凌晨,从k8s界面看,有的被重启了上百次:
剖析
平台侧剖析:理解平台的重启策略
k8s是依据pod yaml里定义的重启策略执行重启,这个策略通过: .spec.restartPolicy 进行设置,反对以下三种策略:
- Always:当容器终止退出后,总是重启容器,默认策略。
- Onfailure:当容器种植异样退出(退出码非0)时,才重启容器。
- Never:当容器终止退出时,才不重启容器。
出问题的利用是走CICD主动打包公布,Yaml也是CD环节主动生成,并没有显示指定重启策略,所以默认采纳Always策略,那么k8s在哪些状况会触发重启呢,次要有以下场景:
- POD失常退出
- POD异样退出
- POD应用从CPU超过yaml里设置的CPU下限,或者超过容器所在namespace里配置的CPU下限
- POD应用从内存超过yaml里设置的Memory下限,或者超过容器所在namespace里配置的Memory下限
- 运行时宿主机的资源无奈满足POD的资源(内存 CPU)时会主动调度到其余机器,也会呈现重启次数+1
- 创立POD时指定的image找不到或者没有node节点满足POD的资源(内存 CPU)需要,会一直重启
出问题的利用失常运行一段时间才呈现的重启,并且POD自身的Yaml文件以及所在的namespace并没设置CPU下限,那么能够排除:1 3 4 6, 业务是采纳Springboot开发的,如果无端退出,JVM自身会产生dump文件,但由重启行为是K8s本人触发的,即便POD里产生里dump文件,因为运行时没有把dump文件目录映射到容器里面,所以没法去查看上次被重启时是否产生里dump文件,所以2 5都有可能导致k8s重启该业务,不过k8s提供命令能够查看POD上一次推出起因,具体命令如下:
NAMESPACE=prodSERVER=dtsPOD_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自身的状态,首先看下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里看到的内存都是宿主机的,要看容器外部的内存大小和应用,能够应用下列命令:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
当配-Xmx2g -Xms2g时,虚构机会申请2G内存,但提交的页面在首次拜访之前不会耗费任何物理存储,该业务过程过后理论应用的内存为1.1g,随着业务运行,到肯定工夫后JVM的应用内存会逐渐减少,直到达到2G被kill。
内存治理相干文章举荐:
Reserving and Committing Memory
JvmMemoryUsage
代码级剖析: 剖解问题的本源
执行命令:
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这种对问题剖析十分重要的日志,肯定要映射存储到主机目录且保障不被笼罩,不然容器销毁时很难去找到这种日志。