前言
在kubernetes环境下,无论集群再大,对应的集群资源(cpu、memory、storage)总是有上限的。而默认情况下,我们启动的pod、以及pod中运行的容器,对应的资源是不加限制的。理论上每个pod,或者是pod内运行的容器,可以无限使用资源直到把所在节点上的资源耗尽,造成节点崩溃,进而影响集群的稳定性。为了防止这种情形出现,kubernetes给我们提供了相应的机制来限制单个pod可以使用的资源,主要的对象有如下两个
- LimitRanger
- ResourceQuota
LimitRanger
- LimitRanger是一种用来限制在特定命名空间下单个pod可以消耗资源的策略,主要防止单个pod将所在命名空间的资源全部抢占完的情况,通过以下几个方面进行配置
- 限定单个pod或容器可消耗的计算资源(cpu、memory)在一定范围内(最大、最小)
- 限定单个PersistentVolumeClaim可申请的storage在一定的范围内
- 限定对特定资源的需求值(最小)和限定值(最大)的比例
- 自动对处于同一命名空间下,没有追加资源限定的pod进行限定
-
使用流程如下
- 集群管理员创建在特定namespace下创建LimitRange
- 使用者在这个namespace下创建pod、容器、PersistentVolumeClaims对象
- LimitRanger准入控制器检查待创建对象的信息,如果没有资源配置,就会用默认值来装配这些对象
- 如果有资源配置,就检查这些配置是否和LimitRanger定义的规范相冲突,如果冲突就拒绝创建相应的对象
- 如果namespace下定义了ResourceQuota,LimitRanger没有定义默认的资源限额,待创建对象也没有资源配置,则拒绝创建
服务质量(QoS)
根据创建的pod或container是否指定资源request和limit值,kubernettes将pod划分成了3个等级
- Guaranteed 当pod中的所有容器对所有资源都定义了Limits和Requests,并且所有容器的Limits值和Requests值全部相等
- BestEffort pod中的所有容器都没有定义资源配置
- Burstable 当一个pod不是Guaranteed也不是BestEffort时,该pod的QoS就是Burstable。例如 Pod中的一部分容器定义了Request值或者Limit值,或者都定义了,但是值不相等
kubernets在出现资源竞争时,会优先保证Guaranteed级别的pod正常运行,其次是Burstable,最后是BestEffort
ResourceQuota
ResourceQuota(资源配额)限定的是特定namespace下所有对象可用的资源总量,另外还能限定各种对象的创建数量
工作流程如下
- 因为ResourceQuota对整个namespace限定,所以不同的组工作在不同的namespace下方能相互不干扰
- 集群管理员对每个namespace创建相应的ResourceQuota
- 用户在自己namespace下创建pod、容器等对象
- 如果要创建对象申请的资源和ResourceQuota有冲突,则系统拒绝创建
实际需求
通过LimitRanger或者创建pod时限定对应的Resource信息,kubernetes在因为某些容器自身缺陷导致pod一直抢占系统资源时,自动帮我们发现问题,并停掉这些有问题pod,如果pod被controller控制,还会帮我们重新调度这个pod,使系统维持可用。
例如我们有如下代码块
@RequestMapping("/oom")
public String oom() {
log.info("request to oom");
PersonRepo pp = new PersonRepo();
pp.autoCreatePerson();
return "OK";
}
public static class PersonRepo {
private List<Person> repo = new ArrayList<>();
public void autoCreatePerson() {
for (long l = 0; ; l++) {
repo.add(new Person(l));
}
}
}
当调用oom方法时,由于程序编码bug,导致应用程序会持续的创建Person对象,放在内存中,如果不加限制,这个程序会不断增加内存,直到节点上分给kubernets的可用内存全部被消耗尽。
如果启动时加上resource信息
...
image: 192.168.0.107/k8s/resource-quotas-oom:0.0.4
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "582Mi"
cpu: "500m"
...
则当此容器消耗的内存达到582Mi时,k8s会直接kill掉这个容器,给出提示信息OOMKilled,并把pod重启(此pod中只有这一个容器)。
这虽然保证了系统的可用性,可是从这个信息中我们没有办法分析出到底是代码的什么地方出现了问题,正常情况下,我们启动java应用,都会在启动命令中加上如下jvm参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/src/oomdump
告诉jvm,当系统出现oom时,给我们在特定的路径下生成dump文件,之后我们根据这个dump文件分析具体出现错误的代码。如果我们想在容器被k8s kill前生成dump文件,我们还需要设置jvm的可用内存大小
-server -Xms512m -Xmx512m
并且如果jvm的最大内存(Xmx)值和k8s resource.limits.memory值一样或着小的话,实际运行时在jvm 出现oom之前,会先触发k8s的OOMKill,还是无法生成dump,所以要想生成dump文件,resource.limits.memory的值要比Xmx的值大一些(50M~100M)。
可是这样设置后,因为还没有达到resource.limits.memory这个值,虽然应用程序出现了oom,但是容器不会被k8s集群停掉,pod也不会重新启动,造成系统响应变慢(容器的内存一直被占着,访问其他请求也会变慢,并出现oom),这也不是我们想要的结果,所以在jvm中再追加一个参数,出现oom后让应用直接停止
-XX:+ExitOnOutOfMemoryError
这样,出现oom后,应用会先生成dump,之后会终止,系统日志信息
2020-02-24 10:25:15.832 INFO 6 --- [nio-8080-exec-2] c.falcon.resource.quotas.oom.RunOom : request to oom
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /usr/src/oomdump ...
Heap dump file created [625772870 bytes in 25.588 secs]
Terminating due to java.lang.OutOfMemoryError: Java heap space
从日志可以看到,系统先生成dump,然后停止掉自己
pod 变化信息
root@master:~# kubectl get pod -w
NAME READY STATUS RESTARTS AGE
jenkins-68d8b54c45-7pdhs 1/1 Running 0 2d5h
oom-deployment-64664f9454-kp65n 1/1 Running 6 26h
oom-deployment-64664f9454-kp65n 0/1 Error 6 26h
oom-deployment-64664f9454-kp65n 1/1 Running 7 26h
可以看到pod出现error,然后又自动恢复成Running状态,k8s会自动将pod中停掉的容器再启动起来,看下容器中/usr/src/oomdump目录
root@master:~# kubectl exec -it oom-deployment-64664f9454-kp65n ls /usr/src
resource-quotas-oom-0.0.1-SNAPSHOT.jar
start.sh
怎么没有我们生成的dump文件,因为容器被重新启动了,工作目录也成了新容器的工作目录,不包含已停掉的容器文件。
查看完整pod信息,有如下片段
root@master:~# kubectl get pod oom-deployment-64664f9454-kp65n -o yaml
...
containerStatuses:
- containerID: docker://70daa47c0f45c9d014e0f70b80a41ca6bc9fd219ca957cc035ee2a049d46166b
image: 192.168.0.107/k8s/resource-quotas-oom:0.0.4
imageID: docker-pullable://192.168.0.107/k8s/resource-quotas-oom@sha256:3ab65f8b5d17182abb0ca0ccc164e5f40bb1d0fcf14006c06af795d0419daa58
lastState:
terminated:
containerID: docker://95776b99ec7a27c03327e45f9e1fc1d3f058c9b814400eabafa9606534c6bc2a
exitCode: 3
...
有一个已经停掉的容器,对应的容器ID:95776b99ec7a27c03327e45f9e1fc1d3f058c9b814400eabafa9606534c6bc2a
到pod所在节点上,通过以下命令获取对应的dump
root@slave:/opt/k8s/work# docker ps -a |grep 95776b99ec7a
95776b99ec7a ec0a1d08eb22 "/bin/sh -c ./start.…" 3 hours ago Exited (3) 13 minutes ago k8s_oom_oom-deployment-64664f9454-kp65n_default_9a47dc18-14bf-4d77-9ed4-5ac2c11d2bf7_6
root@slave:/opt/k8s/work# docker cp 95776b99ec7a:/usr/src/oomdump .
root@slave:/opt/k8s/work# ls -alh oomdump
-rw------- 1 root root 597M 2月 24 18:26 oomdump
- 其中95776b99ec7a是通过kubectl获取到的容器ID的前12位(docker 默认显示12位ID)
执行过程中发现,如果多次系统出现oom,k8s只会帮我们保留最近停掉的一个容器,再往直前的停掉的容器会被自动回收掉。发现这是因为kubelet有一个垃圾回收策略,这个参数可以通过kubelet的配置参数 maximum-dead-containers-per-container来设定,默认值是1,所以只会帮我们保留一个,详情可参考Configuring kubelet Garbage Collection
疑惑
虽然通过jvm参数的设定,实现了程序oom时生成dump,之后再重启,可是这样设置后,其实k8s提供的resources信息就没有起到应有的作用了。不知道是否是用错了方法还是k8s还有别的机制,在被OOMKill之前再执行个什么动作(preStop hook),等深入研究后看有没有更优雅的实现方式