使用 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:

  1. 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 可以看到:

  1. LAST SEEN FIRST SEEN COUNT NAME KIND SUBOBJECT TYPE REASON SOURCE MESSAGE
  2. 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:

  1. apiVersion: v1
  2. kind: ServiceAccount
  3. metadata:
  4. name: oomguard
  5. namespace: kube-system
  6. ---
  7. apiVersion: rbac.authorization.k8s.io/v1
  8. kind: ClusterRoleBinding
  9. metadata:
  10. name: system:oomguard
  11. roleRef:
  12. apiGroup: rbac.authorization.k8s.io
  13. kind: ClusterRole
  14. name: cluster-admin
  15. subjects:
  16. - kind: ServiceAccount
  17. name: oomguard
  18. namespace: kube-system
  19. ---
  20. apiVersion: apps/v1
  21. kind: DaemonSet
  22. metadata:
  23. name: oom-guard
  24. namespace: kube-system
  25. labels:
  26. app: oom-guard
  27. spec:
  28. selector:
  29. matchLabels:
  30. app: oom-guard
  31. template:
  32. metadata:
  33. annotations:
  34. scheduler.alpha.kubernetes.io/critical-pod: ""
  35. labels:
  36. app: oom-guard
  37. spec:
  38. serviceAccountName: oomguard
  39. hostPID: true
  40. hostNetwork: true
  41. dnsPolicy: ClusterFirst
  42. containers:
  43. - name: k8s-event-writer
  44. image: ccr.ccs.tencentyun.com/paas/k8s-event-writer:v1.6
  45. resources:
  46. limits:
  47. cpu: 10m
  48. memory: 60Mi
  49. requests:
  50. cpu: 10m
  51. memory: 30Mi
  52. args:
  53. - --logtostderr
  54. - --unix-socket=true
  55. env:
  56. - name: NODE_NAME
  57. valueFrom:
  58. fieldRef:
  59. fieldPath: status.hostIP
  60. volumeMounts:
  61. - name: unix
  62. mountPath: /unix
  63. - name: oomguard
  64. image: ccr.ccs.tencentyun.com/paas/oomguard:nosoft-v2
  65. imagePullPolicy: Always
  66. securityContext:
  67. privileged: true
  68. resources:
  69. limits:
  70. cpu: 10m
  71. memory: 60Mi
  72. requests:
  73. cpu: 10m
  74. memory: 30Mi
  75. volumeMounts:
  76. - name: cgroupdir
  77. mountPath: /sys/fs/cgroup/memory
  78. - name: unix
  79. mountPath: /unix
  80. - name: kmsg
  81. mountPath: /dev/kmsg
  82. readOnly: true
  83. command: ["/oom-guard"]
  84. args:
  85. - --v=2
  86. - --logtostderr
  87. - --root=/sys/fs/cgroup/memory
  88. - --walkIntervalSeconds=277
  89. - --inotifyResetSeconds=701
  90. - --port=0
  91. - --margin-ratio=0.02
  92. - --min-margin=1
  93. - --max-margin=30
  94. - --guard-ms=50
  95. - --policy=container
  96. - --openSoftLimit=false
  97. - --webhook-url=http://localhost/message
  98. env:
  99. - name: NODE_NAME
  100. valueFrom:
  101. fieldRef:
  102. fieldPath: status.hostIP
  103. volumes:
  104. - name: cgroupdir
  105. hostPath:
  106. path: /sys/fs/cgroup/memory
  107. - name: unix
  108. emptyDir: {}
  109. - name: kmsg
  110. hostPath:
  111. path: /dev/kmsg

一键部署:

  1. kubectl apply -f oom-guard.yaml

检查是否部署成功:

  1. $ kubectl -n kube-system get ds oom-guard
  2. NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
  3. oom-guard 2 2 2 2 2 <none> 6m

其中 AVAILABLE 数量跟节点数一致,说明所有节点都已经成功运行了 oom-guard

查看 oom-guard 日志

  1. kubectl -n kube-system logs oom-guard-xxxxx oomguard

查看 oom 相关事件

  1. kubectl get events |grep CgroupOOM
  2. kubectl get events |grep SystemOOM
  3. kubectl get events |grep OomGuardKillContainer
  4. kubectl get events |grep OomGuardKillProcess

卸载

  1. kubectl delete -f oom-guard.yaml

这个操作可能有点慢,如果一直不返回 (有节点 NotReady 时可能会卡住),ctrl+C 终止,然后执行下面的脚本:

  1. for pod in `kubectl get pod -n kube-system | grep oom-guard | awk '{print $1}'`
  2. do
  3. kubectl delete pod $pod -n kube-system --grace-period=0 --force
  4. done

检查删除操作是否成功

  1. kubectl -n kube-system get ds oom-guard

提示 ...not found 就说明删除成功了

关于开源

当前 oom-gaurd 暂未开源,正在做大量生产试验,后面大量反馈效果统计比较好的时候会考虑开源出来。