关于程序员:K8S为何杀死我的应用

38次阅读

共计 3643 个字符,预计需要花费 10 分钟才能阅读完成。

首发公众号: 二进制社区, 转载分割:binary0101@126.com

导读

“K8S 为咱们提供主动部署调度利用的能力,并通过健康检查接口主动重启失败的利用,确保服务的可用性,但这种主动运维在某些非凡状况下会造成咱们的利用陷入继续的调度过程导致业务受损,本文就生产线上一个外围的平台利用被 K8S 频繁重启调度问题开展剖解,抽丝剥茧一步步从零碎到利用的开展剖析,最初定位到代码层面解决问题 ”

景象

在搭建 devops 基础设施后, 业务曾经全盘容器化部署, 并基于 k8s 实现主动调度, 但个别业务运行一段时间后会被 k8s 主动重启, 且重启的无规律性, 有时候产生在下午, 有时产生在凌晨, 从 k8s 界面看, 有的被重启了上百次:

剖析

平台侧剖析:理解平台的重启策略

k8s 是依据 pod yaml 里定义的重启策略执行重启, 这个策略通过: .spec.restartPolicy 进行设置, 反对以下三种策略:

  • Always:当容器终止退出后,总是重启容器,默认策略。
  • Onfailure:当容器种植异样退出(退出码非 0)时, 才重启容器。
  • Never:当容器终止退出时,才不重启容器。

出问题的利用是走 CICD 主动打包公布,Yaml 也是 CD 环节主动生成, 并没有显示指定重启策略, 所以默认采纳 Always 策略,那么 k8s 在哪些状况会触发重启呢, 次要有以下场景:

    1. POD 失常退出
    1. POD 异样退出
    1. POD 应用从 CPU 超过 yaml 里设置的 CPU 下限, 或者超过容器所在 namespace 里配置的 CPU 下限
    1. POD 应用从内存超过 yaml 里设置的 Memory 下限, 或者超过容器所在 namespace 里配置的 Memory 下限
    1. 运行时宿主机的资源无奈满足 POD 的资源 (内存 CPU) 时会主动调度到其余机器, 也会呈现重启次数 +1
    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=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 自身的状态,首先看下 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 这种对问题剖析十分重要的日志,肯定要映射存储到主机目录且保障不被笼罩,不然容器销毁时很难去找到这种日志。

更多深度文章, 关注: 二进制社区

正文完
 0