Skip to content

Commit

Permalink
k8s oom
Browse files Browse the repository at this point in the history
  • Loading branch information
huataihuang committed Aug 30, 2023
1 parent 3c05a27 commit d6fe7d6
Show file tree
Hide file tree
Showing 17 changed files with 321 additions and 2 deletions.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,147 @@
Pod和Container的资源管理
=================================

在定义 ``pod`` 可以选择为每个 ``container`` 设置所需的资源数量:

- CPU
- 内存
- 其他资源


.. _k8s_limits_and_requests:

Kubernetes的 ``limits`` 和 ``requests``
==========================================

- ``requests`` : 当为 ``pod`` 的 ``containter`` 指定了资源( 也就是 ``request`` ), ``kube-scheduler`` 就根据这个配置信息将Pod调度到合适的节点
- ``limits`` : 当为 ``container`` 指定了资源 **限制** ( 也就是 ``limits`` ), ``kubelet`` 就能确保容器不会使用超出限制的资源,并且还会为容器预留 **请求** ( ``requests`` )数量的系统资源供其使用

注意 ``requests`` 决定了Pod的调度,如果 ``requests`` 超出了集群中所有节点中任意节点的最大可供资源,就会导致调度失败。此时 ``pods`` 会保持在 ``pending`` 状态

当集群节点资源满足 ``pods`` 的 ``requests`` 要求,则pod会被调度到合适节点。此时,如果运行的 ``pods`` 使用的内存资源超过 ``limits`` 配置,就会触发 :ref:`k8s_exit_code_137`

Kubernetes 中的资源单位
=======================

CPU 资源单位
--------------

CPU资源可以是整数或小数,其中小数 ``0.1`` 表示 ``100 millicpu`` 也就是 100毫 cpu core,也就是一个物理/虚拟 CPU 的 10% 计算能力。

CPU资源的最小设置精度是 ``1m`` ,例如,设置 ``5m`` CPU资源

内存资源单位
---------------------

内存的限制和请求以字节为单位,此时可以使用普通的整数:

- 使用 E、P、T、G、M、k 表示内存(以1000为单位跨度)
- 使用 2 的幂数: Ei、Pi、Ti、Gi、Mi、Ki ( **建议** 更为精确)

以下表示大致相同的值::

128974848、129e6、129M、128974848000m、123Mi

.. note::

注意内存资源分配的后缀是有大小写区别的, ``M`` 和 ``m`` 不同: 如果设置 ``400m`` 表示的 **不是** ``400Mi`` 字节,而是 ``0.4`` 字节( 因为在Kubernetes资源配置中,小写的 ``m`` 表示 ```` )

配置资源
=========

**案例** : 物理主机是 4 cores 16GB 内存节点

.. literalinclude:: resource_management_for_pods_containers/deployment_resources.yaml
:caption: 配置资源案例: ``limits`` 和 ``requests``

.. figure:: ../../../_static/kubernetes/concepts/configuration/k8s_limits_and_requests_figure.png

解析资源配置( ``请放大图示,非常形象`` )

- **Pod effective request** (Pod的有效请求): ``400Mi`` 内存 + ``600m`` (毫core) CPU (注意是pod中2个container的 ``requests`` 累加),也就是说集群节点必须具备这些资源才能调度上这个pod
- **CPU shares** : 由于Kubernetes总是对每个CPU core标记为 ``1024`` 个 **CPU shares** ,所以实际上上述 ``requests`` 设置的资源是 ``1024 * 0.5 cores = 512`` (redis) 和 ``1024 * 0.1 cores = 102`` (busybox)
- 注意 ``limits`` 是针对每个 ``containers`` 的:

- 当 redis 使用内存超出 ``600Mi`` ( ``limits`` )就会触发 OOM killed ( :ref:`k8s_exit_code_137` )
- 当 busybox 使用内存超出 ``200Mi`` ( ``limits`` )也会触发 OOM killed

- CPU节流 ( **CPU throttle** )

- CPU计量单位以 ``100ms`` 为细粒度
- 配置中 redis 的 CPU limits 是 ``1`` ,表示在 ``100ms`` 内如果尝试使用完整的 ``100ms`` CPU(注意,对于4 CPU,则表示 ``每100ms`` 实际可用CPU时间片是 ``400ms`` ),就会遇到 **性能降级** (performance degradation)
- 配置中 busybox 的 CPU limitis 是 ``300m`` 表示在 ``100ms`` 内如果尝试使用 ``30ms`` CPU(对于4 CPU,则表示 ``每100ms`` 实际可用CPU时间片是 ``120ms`` ) 也会遇到 **性能降级**

requests是资源保障
---------------------

- 下图形象地表示了,如果 3 个pods 都没有设置 ``request`` 系统会平均分配CPU资源,但是如果 2个 pods配置了 ``requests`` 则系统会优先保障这两个设置了 ``requests`` 的pods运行,而导致没有配置 ``requests`` 被挤出资源:

.. figure:: ../../../_static/kubernetes/concepts/configuration/k8s_limits_and_requests_cpu.png

配置了 ``requests`` 的容器有资源保障,可以挤压没有 ``requests`` 保障的容器

- 下图形象地表示了,Pod B设置了内存 ``requests`` 保障,Pod A设置了内存 ``limits`` 限制,当资源使用上涨后,受到限制的Pod A会被OOM killed

.. figure:: ../../../_static/kubernetes/concepts/configuration/k8s_limits_and_requests_memory.png

配置了 ``requests`` 内存的容器有保障,配置了 ``limits`` 内存限制的容器在超限之后会被OOM Killed

Namespace ResourceQuota
===========================

.. note::

``Namespace ResourceQuota`` 是指租户所能分配的 ``累计`` **总的资源限制** ,非常方便给某个租户分配总的计算资源限制: 在这个资源范围内,租户可以任意分配,但是 **总量** 不能超出限制!!!

通过 ``namespaces`` ,可以以资源池方式设置不同组(也称为 **租户** ``tenants`` )的内存或CPU资源,此时创建pod到这个namespce时不能超出已有的Quota,否则会被拒绝:

.. literalinclude:: resource_management_for_pods_containers/namespace_resourcequota.yaml
:language: yaml
:caption: 限制namespace的资源配置

然后向指定namespace应用这个quota设置:

.. literalinclude:: resource_management_for_pods_containers/namespace_resourcequota_apply
:language: bash
:caption: 应用限制namespace的资源配置

可以检查namespace的ResourceQuota:

.. literalinclude:: resource_management_for_pods_containers/namespace_resourcequota_get
:language: bash
:caption: 检查限制namespace的资源配置

如果创建pod没有提供明确的资源限制配置,也会出现错误::

Error from server (Forbidden): error when creating "mypod.yaml": pods "mypod" is forbidden:
failed quota: mem-cpu-demo: must specify limits.cpu,limits.memory,requests.cpu,requests.memory

如果创建pod时 **累计总量** 超出namespace配置的ResourceQuota,会收到如下错误::

Error from server (Forbidden): error when creating "mypod.yaml": pods "mypod" is forbidden:
exceeded quota: mem-cpu-demo, requested: limits.memory=2Gi,requests.memory=2Gi,
used: limits.memory=1Gi,requests.memory=1Gi, limited: limits.memory=2Gi,requests.memory=1Gi

Namespace LimitRange
========================

``ResourceQuotas`` 是针对namespace的 **** 资源分配,那么限制单独每个对象的资源策略,就需要使用 ``Limitranges`` :

.. literalinclude:: resource_management_for_pods_containers/namespace_limitrange.yaml
:language: yaml
:caption: 限制namespace的对象的具体资源

这样,在这个namespce中的pod如果没有配置 ``requests`` 或 ``limits`` 就会自动加上这个默认配置

当然,如果Container配置超过了 ``Namespace LimitRange`` 就会报错::

Error from server (Forbidden): error when creating "pods/mypod.yaml": pods "mypod" is forbidden:
maximum cpu usage per Container is 1, but limit is 1200m

参考
======

- `Resource Management for Pods and Containers <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>`_
- `Kubernetes best practices: Resource requests and limits <https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits>`_
- `A Deep Dive into Kubernetes Metrics — Part 3 Container Resource Metrics <https://blog.freshtracks.io/a-deep-dive-into-kubernetes-metrics-part-3-container-resource-metrics-361c5ee46e66>`_
- `Understanding Kubernetes Limits and Requests <https://sysdig.com/blog/kubernetes-limits-requests/>`_ 这篇sysdig的博文非常形象,提供了一个很好的图解案例,值得参考学习
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
kind: Deployment
apiVersion: extensions/v1beta1
template:
spec:
containers:
- name: redis
image: redis:5.0.3-alpine
resources:
limits:
memory: 600Mi
cpu: 1
requests:
memory: 300Mi
cpu: 500m
- name: busybox
image: busybox:1.28
resources:
limits:
memory: 200Mi
cpu: 300m
requests:
memory: 100Mi
cpu: 100m
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: LimitRange
metadata:
name: cpu-resource-constraint
spec:
limits:
- default:
cpu: 500m
defaultRequest:
cpu: 500m
min:
cpu: 100m
max:
cpu: "1"
type: Container
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: v1
kind: ResourceQuota
metadata:
name: mem-cpu-demo
spec:
hard:
requests.cpu: 2
requests.memory: 1Gi
limits.cpu: 3
limits.memory: 2Gi
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kubectl apply -f resourcequota.yaml --namespace=mynamespace
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kubectl get resourcequota -n mynamespace
4 changes: 2 additions & 2 deletions source/kubernetes/concepts/workloads/workload_resources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ workload resources(工作负载资源)
我们知道 :ref:`pods` 是一组容器的集合,而 workload resources (或者简称为 workload) 是一组Pods的集合: 之所以发明出workload resources概念,是为了简化一组pods的管理。也就是说,我们不需要分别管理一个个pods,只需要将相同的pods组合成一个workload,就可以统一管理,Kubernetes会通过 :ref:`controllers` 来确保这组Pods保持一致的运行状态以及指定数量:

- :ref:`deployment` 和 :ref:`replicaset` : ``deployment`` 适合管理集群上的无状态应用,在 ``deployment`` 汇总所有 ``pod`` 完全等价,并且在需要时被替换
- :ref:`statefulset` : 运行一个或多个需要跟踪应用状态的 :ref:`pods` 。例如,数据需要持久化的Pod就可以使用 ``StatefulSet`` ,将每个Pod和某个 :ref:`k8s_persistent_volumes` 对应起来
- :ref:`job` : 创建一个或多个Pod,并持续充实Pod的执行,直到指定数量的Pod成功终止。(通常在 :ref:`machine_learning` 大规模并行计算时使用)
- :ref:`statefulset` : 运行一个或多个需要跟踪应用状态的 :ref:`pods` 。例如,数据需要持久化的Pod就可以使用 ``StatefulSet`` ,将每个Pod和某个 :ref:`k8s_persistent_volumes` 对应起来(也就是共享存储),这样 ``StatefulSet`` 的各个Pod可以通过共享的 :ref:`k8s_persistent_volumes` 交换数据提高整体服务可靠性(你可以 ``StatefulSet`` 理解成共享NAS的WEB服务器,高可用)
- :ref:`job` : 创建一个或多个Pod,并持续重复Pod的执行,直到指定数量的Pod成功终止。也就是说定义Job可以确保一组Pod围绕一个任务来反复执行直到最终目标达成后Job终止 (通常在 :ref:`machine_learning` 大规模并行计算时使用)

参考
======
Expand Down
1 change: 1 addition & 0 deletions source/kubernetes/in_action/oom_in_k8s/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Kubernetes的Out-of-memory(OOM)解析
:maxdepth: 1

intro_oom_in_k8s.rst
k8s_exit_code_137.rst

.. only:: subproject and html

Expand Down
59 changes: 59 additions & 0 deletions source/kubernetes/in_action/oom_in_k8s/k8s_exit_code_137.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.. _k8s_exit_code_137:

==========================================
Kubernetes的pod异常退出(ExitCode:137)
==========================================

在生产环境中, ``OOM kill`` ( :ref:`linux_oom` )是非常常见的现象,对于 Kubernetes 而言,在 ``kubelet`` 日志中会记录类似:

.. literalinclude:: k8s_exit_code_137/kubelet_oom_killed
:caption: ``kubelet`` 日志中记录了 ``XXXXX`` pod被 ``OOMKilled``
:emphasize-lines: 1

Exit Code 137
================

Exit Code 137表示进程由于使用太多内存而被(意外)终止。

在Linux操作系统中,所有意外退出/杀死的进程都会返回一个退出码,以提供一个检查机制来通知用户、系统和应用程序进程为何停止。这个退出码(exit code)数值介于 ``0`` 到 ``255`` :

- ``0`` 表示shell命令成功执行完成; **非零** 退出状态表示失败
- 当命令因为编号为 ``N`` 的 **致命信号** (fatal signal) 退出时,Bash就会使用 ``128+N`` 作为退出状态

- ``OOMKilled`` 进程收到的致命信号是 ``9`` ,也就是 ``SIGKILL (signal 9)`` 强制杀死
- ``128+9 = 137`` 表示pod进程是被操作系统直接杀死的
- 如果 ``kubelet`` 记录的 ``ExitCode`` 是 ``143`` ,则表明容器是被 ``SIGTERM (signal 15)`` **温柔** 终止

- 在bash中,最后一个命令的退出状态可以在特殊参数 ``$?`` 查看( ``echo $?`` )

- 当 Kubernetes 记录了容器或Pod因为内存使用过高而终止 ``ExitCode 137`` ,就应该详细调查程序是否存在内存泄漏或者编程不佳导致资源过渡消耗

常见的内存问题
================

容器使用超出配置的内存限制
---------------------------

容器超出内存限制( :ref:`k8s_limits_and_requests` )就会触发操作系统OOM kill,如果代码检查没有出现 **内存泄漏** 以及低效代码,则可以根据业务情况调整 :ref:`resource_management_for_pods_containers` 避免触发OOM Kill

如果没有合理的容器内存限制,并且在容器内存使用达到 ``limits`` 之前及时告警和处理,有可能会触发物理服务器节点的操作系统级别的OOM kill,这种进程杀死是随机的不可预测的,有可能殃及并没有超出预期设置的正常pod被误杀。

应用程序内存泄漏
-------------------

当应用程序使用内存但是操作完成后没有释放内存,就会发生内存泄漏,导致内存逐渐填满并耗尽所有可用容量。可以尝试采用 :ref:`valgrind` 这样的诊断工具来帮助排查内存泄漏。

负载监控
----------

随着业务增长,有可能内存消耗更多(内存密集型应用)。所以要进行长期广泛的监控( :ref:`prometheus` ),以便能够观察到变化趋势以及即使收到告警,以及采用自动扩展方案



参考
=====

- `How to Fix Exit Code 137 | Kubernetes Memory Issues <https://foxutech.com/how-to-fix-exit-code-137-kubernetes-memory-issues/>`_
- `Bash: Exit Status <https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html>`_
- `SIGKILL: Fast Termination Of Linux Containers | Signal 9 <https://komodor.com/learn/what-is-sigkill-signal-9-fast-termination-of-linux-containers/>`_
- `How to fix exit code 137 <https://www.airplane.dev/blog/exit-code-137>`_
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
...ContainerStatuses:[{Name:XXXXX State:{Waiting:nil Running:nil Terminated:&ContainerStateTerminated{ExitCode:137,Signal:0,Reason:OOMKilled,
Message:,StartedAt:2023-08-29 21:51:09 +0800 CST,FinishedAt:2023-08-30 06:09:36 +0800 CST,
ContainerID:pouch://83887b90809b1d48e2c00d5e870061d29ad15d214cf90963e02903011d15e3d7,}}}]...
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ Prometheus社区官方提供了大约十几种 `xxxx_exporters <https://github.c
process-exporter.rst
amd_smi_exporter.rst
blackbox_exporter.rst
node_exporter_killed_by_sigpipe.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.. _node_exporter_killed_by_sigpipe:

===================================
Node Exporter被 ``SIGPIPE`` 杀死
===================================

.. note::

这个问题我主要是记录,尚未真正遇到,不过,我遇到过类似生产环境组件因为 ``SIGPIPE`` 退出的问题,原理和情况相似。

这个 ``SIGPIPE`` 导致 Node Exporter 退出的问题会发生在早期版本,不过现在 Node Exporter 内置了 ``SIGPIPE`` 处理,所以已经不再出现因为 ``PIPE`` 管道异常导致退出的问题。这里仅作为记录参考

Node Exproter在 :ref:`systemd` 环境下运行,有时候你可能会遇到异常退出,检查 ``systemctl status node_exporter`` 可能会看到类似:

.. literalinclude:: node_exporter_killed_by_sigpipe/systemd_service_killed_sigpipe
:caption: 服务进程被 ``PIPE`` 信号杀死的案例
:emphasize-lines: 5

这里被 **信号** ``PIPE`` 杀死的原因:

当使用已经失效的读取器写入管道时,写入器将收到 ``SIGPIPE`` 信号。默认情况下,这会终止进程。如果忽略这个信号,写入将返回错误 ``EPIPE`` 。无论 ``reader`` 是怎么死亡的,这种情况都会发生。

这里 ``node_exporter`` 服务配置是:

.. literalinclude:: node_exporter_killed_by_sigpipe/node_exporter.service
:caption: ``node_exporter.service`` 配置
:emphasize-lines: 13

默认配置是 ``Restart=on-failure`` ,早期版本没有处理 ``SIGPIPE`` 信号,所以会导致收到 ``PIPE`` 信号时候退出,但是因为没有作为Fail处理,所以也不会自动启动。

对于不能处理 ``SIGPIPE`` 信号的软件退出问题,可以修改 :ref:`systemd` 配置,修改为::

Restart=always

或者加上::

RestartForceExitStatus=SIGPIPE

参考
======

- `Is SIGPIPE signal received when reader is killed forcefully(kill -9)? <https://stackoverflow.com/questions/70648067/is-sigpipe-signal-received-when-reader-is-killed-forcefullykill-9>`_
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[Unit]
Description=node_exporter
Wants=network-online.target
After=network-online.target

StartLimitIntervalSec=500
StartLimitBurst=5

[Service]
User=prometheus
Group=prometheus
Type=simple
Restart=on-failure
RestartSec=5s
ExecStart=/usr/local/bin/node_exporter \
--collector.textfile.directory=/var/lib/node_exporter/textfile_collector

[Install]
WantedBy=multi-user.target
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
● node_exporter.service - Prometheus exporter for machine metrics, written in Go with pluggable metric collectors.
Loaded: loaded (/usr/lib/systemd/system/node_exporter.service; enabled; vendor preset: disabled)
Active: inactive (dead) since Sat 2017-05-27 04:54:39 EDT; 1 day 20h ago
Docs: https://prometheus.io
Main PID: 27203 (code=killed, signal=PIPE)

0 comments on commit d6fe7d6

Please sign in to comment.