使用 oom-guard 在用户态处理 cgroup OOM
背景
由于 linux 内核对 cgroup OOM 的处理,存在很多 bug,经常有由于频繁 cgroup OOM 导致节点故障(卡死, 重启, 进程异常但无法杀死),于是 TKE 团队开发了 oom-guard
,在用户态处理 cgroup OOM 规避了内核 bug。
原理
核心思想是在发生内核 cgroup OOM kill 之前,在用户空间杀掉超限的容器, 减少走到内核 cgroup 内存回收失败后的代码分支从而触发各种内核故障的机会。
threshold notify
参考文档: https://lwn.net/Articles/529927/
oom-guard
会给 memory cgroup 设置 threshold notify, 接受内核的通知。
以一个例子来说明阀值计算通知原理: 一个 pod 设置的 memory limit 是 1000M, oom-guard
会根据配置参数计算出 margin:
margin = 1000M * margin_ratio = 20M // 缺省margin_ratio是0.02
margin 最小不小于 mim_margin(缺省1M), 最大不大于 max_margin(缺省为30M)。如果超出范围,则取 mim_margin 或 max_margin。计算 threshold = limit - margin ,也就是 1000M - 20M = 980M,把 980M 作为阈值设置给内核。当这个 pod 的内存使用量达到 980M 时, oom-guard
会收到内核的通知。
在触发阈值之前,oom-gurad
会先通过 memory.force_empty
触发相关 cgroup 的内存回收。 另外,如果触发阈值时,相关 cgroup 的 memory.stat 显示还有较多 cache, 则不会触发后续处理策略,这样当 cgroup 内存达到 limit 时,会内核会触发内存回收。 这个策略也会造成部分容器内存增长太快时,还是会触发内核 cgroup OOM
达到阈值后的处理策略
通过 --policy
参数来控制处理策略。目前有三个策略, 缺省策略是 process。
process
: 采用跟内核cgroup OOM killer相同的策略,在该cgroup内部,选择一个 oom_score 得分最高的进程杀掉。 通过 oom-guard 发送 SIGKILL 来杀掉进程container
: 在该cgroup下选择一个 docker 容器,杀掉整个容器noop
: 只记录日志,并不采取任何措施
事件上报
通过 webhook reporter 上报 k8s event,便于分析统计,使用kubectl get event
可以看到:
LAST SEEN FIRST SEEN COUNT NAME KIND SUBOBJECT TYPE REASON SOURCE MESSAGE
14s 14s 1 172.21.16.23.158b732d352bcc31 Node Warning OomGuardKillContainer oom-guard, 172.21.16.23 {"hostname":"172.21.16.23","timestamp":"2019-03-13T07:12:14.561650646Z","oomcgroup":"/sys/fs/cgroup/memory/kubepods/burstable/pod3d6329e5-455f-11e9-a7e5-06925242d7ea/223d4795cc3b33e28e702f72e0497e1153c4a809de6b4363f27acc12a6781cdb","proccgroup":"/sys/fs/cgroup/memory/kubepods/burstable/pod3d6329e5-455f-11e9-a7e5-06925242d7ea/223d4795cc3b33e28e702f72e0497e1153c4a809de6b4363f27acc12a6781cdb","threshold":205520896,"usage":206483456,"killed":"16481(fakeOOM) ","stats":"cache 20480|rss 205938688|rss_huge 199229440|mapped_file 0|dirty 0|writeback 0|pgpgin 1842|pgpgout 104|pgfault 2059|pgmajfault 0|inactive_anon 8192|active_anon 203816960|inactive_file 0|active_file 0|unevictable 0|hierarchical_memory_limit 209715200|total_cache 20480|total_rss 205938688|total_rss_huge 199229440|total_mapped_file 0|total_dirty 0|total_writeback 0|total_pgpgin 1842|total_pgpgout 104|total_pgfault 2059|total_pgmajfault 0|total_inactive_anon 8192|total_active_anon 203816960|total_inactive_file 0|total_active_file 0|total_unevictable 0|","policy":"Container"}
使用方法
部署
保存部署 yaml: oom-guard.yaml
:
apiVersion: v1
kind: ServiceAccount
metadata:
name: oomguard
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:oomguard
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: oomguard
namespace: kube-system
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: oom-guard
namespace: kube-system
labels:
app: oom-guard
spec:
selector:
matchLabels:
app: oom-guard
template:
metadata:
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ""
labels:
app: oom-guard
spec:
serviceAccountName: oomguard
hostPID: true
hostNetwork: true
dnsPolicy: ClusterFirst
containers:
- name: k8s-event-writer
image: ccr.ccs.tencentyun.com/paas/k8s-event-writer:v1.6
resources:
limits:
cpu: 10m
memory: 60Mi
requests:
cpu: 10m
memory: 30Mi
args:
- --logtostderr
- --unix-socket=true
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: status.hostIP
volumeMounts:
- name: unix
mountPath: /unix
- name: oomguard
image: ccr.ccs.tencentyun.com/paas/oomguard:nosoft-v2
imagePullPolicy: Always
securityContext:
privileged: true
resources:
limits:
cpu: 10m
memory: 60Mi
requests:
cpu: 10m
memory: 30Mi
volumeMounts:
- name: cgroupdir
mountPath: /sys/fs/cgroup/memory
- name: unix
mountPath: /unix
- name: kmsg
mountPath: /dev/kmsg
readOnly: true
command: ["/oom-guard"]
args:
- --v=2
- --logtostderr
- --root=/sys/fs/cgroup/memory
- --walkIntervalSeconds=277
- --inotifyResetSeconds=701
- --port=0
- --margin-ratio=0.02
- --min-margin=1
- --max-margin=30
- --guard-ms=50
- --policy=container
- --openSoftLimit=false
- --webhook-url=http://localhost/message
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: status.hostIP
volumes:
- name: cgroupdir
hostPath:
path: /sys/fs/cgroup/memory
- name: unix
emptyDir: {}
- name: kmsg
hostPath:
path: /dev/kmsg
一键部署:
kubectl apply -f oom-guard.yaml
检查是否部署成功:
$ kubectl -n kube-system get ds oom-guard
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
oom-guard 2 2 2 2 2 <none> 6m
其中 AVAILABLE 数量跟节点数一致,说明所有节点都已经成功运行了 oom-guard
。
查看 oom-guard 日志
kubectl -n kube-system logs oom-guard-xxxxx oomguard
查看 oom 相关事件
kubectl get events |grep CgroupOOM
kubectl get events |grep SystemOOM
kubectl get events |grep OomGuardKillContainer
kubectl get events |grep OomGuardKillProcess
卸载
kubectl delete -f oom-guard.yaml
这个操作可能有点慢,如果一直不返回 (有节点 NotReady 时可能会卡住),ctrl+C
终止,然后执行下面的脚本:
for pod in `kubectl get pod -n kube-system | grep oom-guard | awk '{print $1}'`
do
kubectl delete pod $pod -n kube-system --grace-period=0 --force
done
检查删除操作是否成功
kubectl -n kube-system get ds oom-guard
提示 ...not found
就说明删除成功了
关于开源
当前 oom-gaurd
暂未开源,正在做大量生产试验,后面大量反馈效果统计比较好的时候会考虑开源出来。