Kubernetes 生产化集群的管理

计算节点相关

生产环境需要稳定性、安全性和可维护性,在硬件、kernel 上需要做一些考量。

生产化集群的考量

计算节点:

  • 如何批量安装和升级计算节点的操作系统
  • 如何管理配置计算节点的网络信息
  • 如何管理不同 SKUStock Keeping Unit)的计算节点,例如有的节点带有GPU,有的节点不带
  • 如何快速下架故障的计算节点
  • 如何快速扩缩容集群的规模

控制平面:

  • 如何在主节点上下载、安装和升级控制平面组件及其所需的配置文件
  • 如何确保集群所需的其他插件,例如 CoreDNS、监控系统等部署完成
  • 如何准备控制平面组件的各种安全证书
  • 如何快速升级或者回滚控制平面组件的版本

操作系统选择

操作系统的评估与选择

  • 通用操作系统
    • Ubuntu
    • CentOS
    • Fedora
  • 专为容器优化的操作系统
    • 最小化操作系统
      • CoreOS
      • RedHat Atomic
      • Snappy Ubuntu Core
      • RancherOS

面对市面上可选的多种操作系统,需要针对性做技术选型。

操作系统的评估与选择

  • 操作系统评估和选型的标准
    • 是否有生态系统
    • 成熟度
    • 内核版本
    • 对运行时的支持
    • Init System
    • 包管理和系统升级
    • 安全

生态系统与成熟度

  • 容器优化操作系统的游戏
      • 原子级升级和回退
      • 更高的安全性
Centos Ubuntu CoreOS Atomic* Snappy RancherOS
通用操作系统 容器优化 容器优化 容器优化 容器优化 容器优化
生态系统和成熟度 成熟 成熟 最早的容器优化操作系统,不过公司体量小,目前已被收购 Red Hat 出品,品质保证 Canonical 出品,最初为移动设备设计 相对较新,RancherOS 中运行的所有服务都是 docker 容器

最终推荐 Atomic,主要原因是不可变原则,并且可以将 rpm 包以 ostree 的方式提供,在启动的时候注入,通过 ostree 生成不同的操作系统,逻辑与 dockerfile 非常类似。

云原生的原则

  • 可变基础设施的风险

    • 在灾难发生的时候,难以重新构建服务。持续过多的手工操作,缺乏记录,会导致很难由标准初始化后的服务器来重新构建起等效的服务
    • 在服务运行过程中,持续的修改服务器,就犹如程序中的可变变量的值发生变化而引入的状态不一致的并发风险。这些对于服务器的修改,同样会引入中间状态,从而导致不可预知的问题
  • 不可变基础设施(immutable infrastructure

    避免一些额外动作影响线上业务逻辑,以及一些手动操作没有覆盖生产环境导致异常问题。

    • 不可变的容器镜像
    • 不可变的主机操作系统

Atomic

  • Red Hat 支持的软件包安装系统
  • 多种 Distro
    • Fedora
    • CentOS
    • RHEL
  • 优势
    • 不可变操作系统,面向容器优化的基础设施
      • 灵活和安全性较好
      • 只有 /etc/var 可以修改,其他目录均为只读
    • 基于 rpm-ostree 管理系统包
      • rpm-ostree 是一个开源项目,使得生产系统中构建镜像非常简单
      • 支持操作系统升级和回滚的原子操作

最小化主机操作系统

原则:

  • 最小化主机操作系统
  • 只安装必要的工具
    • 必要:支持系统运行的最小工具集
    • 任何调试工具,比如性能排查,网络排查工具,均可以后期以容器形式运行
  • 意义
    • 性能
    • 稳定性
    • 安全保障

操作系统构建流程

image-20240131170900968

  • rpm 源拉取快照
  • 通过 rpm-ostreerpm 源构建出 ostree 包,提供 ostree 服务
  • 物理机在启动时候,通过 kickstart 调用 ostree 命令
  • 虚拟机(例如 OpenStack),通过 Packer Builderostree 构建成 OpenStack 支持的方式
  • 需要做调试时,在节点上通过拉起容器进行调试,避免最小化操作系统被一些工具污染

ostree

提供一个共享库(libostree)和一些列命令行(非常类似 git

提供与 git 命令行一致的体验,可以提交或者下载一个完整的可启动的文件系统数

提供将 ostree 部署进 bootloader 的机制

https://github.com/ostreedev/ostree/blob/main/src/boot/dracut/module-setup.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
install() {
dracut_install /usr/lib/ostree/ostree-prepare-root
for r in /usr/lib /etc; do
if test -f "$r/ostree/prepare-root.conf"; then
inst_simple "$r/ostree/prepare-root.conf"
fi
done
if test -f "/etc/ostree/initramfs-root-binding.key"; then
inst_simple "/etc/ostree/initramfs-root-binding.key"
fi
inst_simple "${systemdsystemunitdir}/ostree-prepare-root.service"
mkdir -p "${initdir}${systemdsystemconfdir}/initrd-root-fs.target.wants"
ln_r "${systemdsystemunitdir}/ostree-prepare-root.service" \
"${systemdsystemconfdir}/initrd-root-fs.target.wants/ostree-prepare-root.service"
}

构建 ostree

  • rpm-ostree

    基于 treefilerpm 包构建成为 ostree 管理 ostree 以及 bootloader 配置

  • treefile

    refer:分支名(版本,CPU 架构)

    reporpm package repositories

    packages:待安装组件

  • rpm 构建成 ostree

    1
    # rpm-ostree compose tree --unified-core --cachedir=cache --repo=./build-repo /path/to/treefile.json

    通过 treefile 构建最小化镜像。

treefile 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"ref": "fedora-atomic/rawhide/x86_64/base/core",

"gpg-key": "<SET KEY ID FOR GPG>",

"repos": ["fedora-rawhide"],

"selinux": true,

"packages": ["kernel", "ostree", "lvm2",
"btrfs-progs", "e2fsprogs", "xfsprogs",
"gnupg2", "selinux-policy-targeted",
"openssh-server", "openssh-clients",
"NetworkManager", "vim-minimal", "nano", "sudo"]
}

加载 ostree

  • 初始化项目

    1
    ostree admin os-init centos-atomic-host
  • 导入 ostree repo,指定 ostree

    1
    ostree remote add atomic http://ostree.svr/ostree

    image-20240131174007070

  • 拉取 ostree,将构建好的 ostree 拉到本地

    1
    ostree pull atomic centos-atomic-host/8/x86_64/standard
  • 部署 os

    1
    ostree admin deploy --os=centos-atomic-host centos-atomic-host/8/x86_64/standard --karg='root=/dev/atomicos/root'

操作系统加载

  • 物理机
    • 物理机通常需要通过 foreman 启动,foreman 通过 pxe boot,并加载 kickstart
    • kickstart 通过 ostree deploy 即可完成操作系统的部署
  • 虚拟机
    • 需要通过镜像工具将 ostree 构建成 qcow2 格式,vhdraw 等模式

生产环境遭遇过的陷阱

一些早期碰到的问题。

  • cluoud-init 0.7.7 bug

    阻止 node 在初始化过程中的静态网络配置

  • Docker 1.9.1 bug

    docker 实例日志快速输出时,会发生内存泄漏

  • Kernel panic in 4.4.6

    Cgroup 创建和销毁过程中,会产生 kernel panic

  • rootfs 分区大小

    rootfs 无空间会导致 docker 无法启动

    • 导致 rootfs 占满的情况
      • CICD 中的 Maven build 会把下载的 lib 放在 /tmp
      • 用户 Docker logs 日志过快,导致一个 log rotation 周期内日志文件撑爆硬盘
  • 需要定制化的操作系统参数

    • 比如 Elasticsearch 需要 max_map_count >= 262144,但操作系统默认值为 65535,我们需要在创建 Node 的时候就 apply 这个配置

节点资源管理

NUMA Node

Non-Uniform Memory Access (不对等内存访问)是一种内存访问方式,是为多处理器计算机设计的内存架构

image-20240131174741687

CPU 通过 FSB 总线与内存交互,但是 Core2 访问内存是通过 QPI + FSB ,相对而言性能是较低的。一些性能调优会通过判断进程正在运行的 Core 做就近迁移,避免内存远程访问。

节点资源管理

  • 状态汇报
  • 资源预留:例如预留给 systemdkebelet 运行
  • 防止节点资源耗尽的防御机制驱逐:例如 Pod 将磁盘耗尽、inode 耗尽、端口耗尽、文件句柄耗尽,节点通过防御机制驱逐这种 Pod 以自保
  • 容器和系统资源的配置

状态上报

kubelet 周期性地向 API Server 进行汇报,并更新节点的相关健康和资源使用信息。

  • 节点基础信息,包括 IP 地址、操作系统、内核、运行时、kubeletkube-proxy 版本信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # kubectl get nodes  master  -o yaml
    nodeInfo:
    architecture: amd64
    bootID: 29d745d8-b0c0-4eb7-ac29-b8734d40ef3f
    containerRuntimeVersion: docker://19.3.5
    kernelVersion: 5.15.15-1.el7.x86_64
    kubeProxyVersion: v1.16.15
    kubeletVersion: v1.16.15
    machineID: 844b4cb679ee439dab4948ec571eef48
    operatingSystem: linux
    osImage: CentOS Linux 7 (Core)
    systemUUID: 00000000-0000-0000-0000-ac1f6b145e7c
  • 节点资源信息包括 CPU、内存、HugePage、临时存储、GPU 等注册设备,以及这些资源中可以分配给容器使用的部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    allocatable:        # 可分配资源
    cpu: "56"
    ephemeral-storage: 456005072Ki
    hugepages-1Gi: "0"
    hugepages-2Mi: "0"
    memory: 131726028Ki
    pods: "110"
    capacity: # 资源总和
    cpu: "56"
    ephemeral-storage: 466490832Ki
    hugepages-1Gi: "0"
    hugepages-2Mi: "0"
    memory: 131726028Ki
    pods: "110"
  • 调度器在为 Pod 选择节点时会将机器的状态信息作为依据

    1
    2
    3
    4
    5
    6
    7
    conditions:        # 节点状态信息汇总
    - lastHeartbeatTime: "2024-02-01T03:31:37Z"
    lastTransitionTime: "2023-05-30T03:06:38Z"
    message: kubelet has sufficient memory available
    reason: KubeletHasSufficientMemory
    status: "False"
    type: MemoryPressure
状态 状态的意义
Ready 节点是否健康
MemoryPressure 节点是否存在内存压力
PIDPressure 节点是否存在比较多的进程
DiskPressure 节点是否存在磁盘压力
NetworkUnavailable 节点网络配置是否正确

Lease

在早期版本 kubelet 的状态上报直接更新 node 对象,而上报的信息包含状态信息和资源信息,因此需要传输的数据包较大,给 APIServeretcd 造成的压力较大。

后引入 Lease 对象(租约)用来保存健康信息(状态信息和资源信息通常变化不会很频繁,只有健康信息需要重点关注),在默认 40snodeLeaseDurationSeconds 周期内,若 Lease 对象没有被更新,则对应节点可以被判定为不健康

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# kubectl get leases.coordination.k8s.io -A
NAMESPACE NAME HOLDER AGE
ingress-nginx ingress-nginx-leader ingress-nginx-controller-88b784f6-s86db 43h
kube-node-lease master master 8d
kube-node-lease slave01 slave01 8d
kube-node-lease slave02 slave02 8d
kube-system apiserver-oodnacusc5bcuykzhczyk7a7zq apiserver-oodnacusc5bcuykzhczyk7a7zq_f0896458-9db4-470b-80ff-26da7d4822ba 2d2h
kube-system kube-controller-manager master_d31b90f1-a20b-4a3a-86d8-33d6550a1a26 8d
kube-system kube-scheduler master_cec8814f-23e8-474e-879a-ad93494bba9d 8d
tigera-operator operator-lock slave01_d10c5ec9-0be4-422b-a521-a162c017a4c3 2d

# kubectl get leases.coordination.k8s.io master -n kube-node-lease -o yaml
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
creationTimestamp: "2024-01-23T14:19:03Z"
name: master
namespace: kube-node-lease
ownerReferences:
- apiVersion: v1
kind: Node
name: master
uid: dcf73b81-f5f1-4efc-b1df-63838d48266e
resourceVersion: "650009"
uid: 0ff39bf2-2a71-420b-acd1-7c565d56e621
spec:
holderIdentity: master
leaseDurationSeconds: 40
renewTime: "2024-01-31T09:53:24.759121Z"

资源预留

计算节点除用户容器外,还存在很多支撑系统运行的基础服务,譬如 systemdjournaldsshddockerdContainerdkubelet 等。

为了使服务进程能够正常运行,要确保他们在任何时候都可以获取足够的系统资源,所以我们要为这些系统进程预留资源。

kubelet 可以通过众多启动参数为系统预留 CPU、内存、PID 等资源,比如 systemReservedKubeReserved 等。

Capacity 和 Allocatable

容量资源(Capacity)是指 kubelet 获取的计算节点当前的资源信息。

  • CPU 是从 /proc/cpuinfo 文件中获取的节点 CPU 核数
  • memory 是从 /proc/memoryinfo 中获取的节点内存大小
  • ephemeral-storage 是指节点根分区的大小

资源可分配额(Allocatable)是用户 Pod 可用的资源,是资源容量减去分配给系统的资源的剩余部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
allocatable:
cpu: "4"
ephemeral-storage: "18858870344"
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 3879272Ki
pods: "110"
capacity:
cpu: "4"
ephemeral-storage: 20463184Ki
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 3981672Ki
pods: "110"

节点可分配资源(allocatable) = 节点的资源容量(capacity)- kube-reserved - system-reserved - `eviction

节点的资源容量(capacity)
kube-reserved system-reserved eviction-threshold 可用资源(allocatable)

节点磁盘管理

  • 系统分区 nodefs

    默认:/var/lib/kubelet/pods/

    • 工作目录和容器日志
  • 容器运行时分区 imagefs

    docker/var/lib/docker/overlay2/

    containerd/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/

    • 用户镜像和容器可写层
    • 容器运行时分区是可选的,可以合并到系统分区中

驱逐管理

  • kubelet 会在系统资源不够时中止一些容器进程,以空出系统资源,保证节点的稳定性
  • 但由 kubelet 发起的驱逐只停止 Pod 的所有容器进程,并不会直接删除 Pod (作为审计用,后续需要手动清理)
    • Podstatus.phase 会被标记为 Failed
    • status.reason 会被设置为 Evicted
    • status.message 则会记录被驱逐的原因

资源可用额监控

  • kubelet 依赖内嵌的开源软件 cAdvisor,周期性检查节点资源使用情况(新版本会替换掉,由 kubelet 自身获取,主要是通过 Cgroup
  • CPU 是可压缩资源,根据不同进程分配时间配额和权重,CPU 可被多个进程竞相使用(针对一些无法压缩的进程,则需要针对性调优)
  • 驱逐策略是基于磁盘和内存资源用量进行的,因为两者属于不可压缩的资源,当此类资源使用耗尽时将无法再申请
检查类型 说明
memory.avaliable 节点当前的可用内存
nodefs.avaliable 节点根分区的可使用磁盘大小
nodefs.inodesFree 节点根分区的可使用 inode
imagefs.inodesFree 节点运行时分区的可使用 inode
imagefs.avaliable 节点运行时分区的可使用磁盘大小
节点如果没有运行时分区,就不会有相应的资源监控

驱逐策略

kubelet 获得节点的可用额信息后,会结合节点的容量信息来判断当前节点运行的 Pod 是否满足驱逐条件。

驱逐条件可以是绝对值或百分比,当监控资源的可使用额少于设定的数值或百分比时,kubelet 就会发起驱逐操作。

kubelet 参数 evictionMinimumReclaim 可以设置每次回收的资源的最小值,以防止小资源的多次回收。

kubelet 参数 分类 驱逐方式
evictionSoft 软驱逐 当检测到当前资源达到软驱逐的阈值时,并不会立即启动驱逐操作,而是要等待一个宽限期。
这个宽限期选取 EvictionSoftGracePeriod 和 Pod 指定的 TerminationGracePeriodSeconds 中较小的值。
evictionHard 硬驱逐 没有宽限期,一旦检测到满足硬驱逐的条件,就直接中止容器来释放紧张资源

kubectl explain pod.spec.terminationGracePeriodSeconds

1
2
3
4
5
6
7
# /usr/bin/kubelet --help
--eviction-hard mapStringString A set of eviction thresholds (e.g. memory.available<1Gi) that if met would trigger a pod eviction. (DEPRECATED: This parameter should be set via the config file specified by the Kubelet's --config flag. See https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ for more information.)
--eviction-max-pod-grace-period int32 Maximum allowed grace period (in seconds) to use when terminating pods in response to a soft eviction threshold being met. If negative, defer to pod specified value. (DEPRECATED: This parameter should be set via the config file specified by the Kubelet's --config flag. See https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ for more information.)
--eviction-minimum-reclaim mapStringString A set of minimum reclaims (e.g. imagefs.available=2Gi) that describes the minimum amount of resource the kubelet will reclaim when performing a pod eviction if that resource is under pressure. (DEPRECATED: This parameter should be set via the config file specified by the Kubelet's --config flag. See https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ for more information.)
--eviction-pressure-transition-period duration Duration for which the kubelet has to wait before transitioning out of an eviction pressure condition. (default 5m0s) (DEPRECATED: This parameter should be set via the config file specified by the Kubelet's --config flag. See https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ for more information.)
--eviction-soft mapStringString A set of eviction thresholds (e.g. memory.available<1.5Gi) that if met over a corresponding grace period would trigger a pod eviction. (DEPRECATED: This parameter should be set via the config file specified by the Kubelet's --config flag. See https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ for more information.)
--eviction-soft-grace-period mapStringString A set of eviction grace periods (e.g. memory.available=1m30s) that correspond to how long a soft eviction threshold must hold before triggering a pod eviction. (DEPRECATED: This parameter should be set via the config file specified by the Kubelet's --config flag. See https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ for more information.)

基于内存压力的驱逐

memory.avaiable 表示当前系统的可用内存情况。

kubelet 默认设置了 memory.available < 100Mi 的硬驱逐条件。

kubelet 检测到当前节点可用内存资源紧张并满足驱逐条件时,会将节点的 MemoryPressurce 状态设置为 True,调度器会阻止 BestEffort Pod 调度到内存承压的节点。(也就是禁止调度)

kubelet 启动对内存不足的驱逐操作时,会依照如下的顺序选取目标 Pod:

  1. 判断 Pod 所有容器的内存使用量总和是否超出了请求的内存量,超出请求资源的 Pod 会成为被选目标(QoS)kubectl explain pod.status.qosClass
  2. 查询 Pod 的调度优先级,低优先级的 Pod 被优先驱逐(优先级)kubectl explain pod.spec.priorityClassName
  3. 计算 Pod 所有容器的内存使用量和 Pod 请求的内存量的差值,差值越小,越不容易被驱逐。(同等优先级情况下,再按照内存使用量进行排序)

基于磁盘压力的驱逐

以下任何一项满足驱逐条件时,它会将节点的 DiskPressure 状态设置为 True,调度器不会再调度任何 Pod 到该节点上

  • nodefs.avaliable:可用空间
  • nodefs.inodesFree:判断inode 够不够
  • imagefs.available
  • iamgefs.inodesFree

驱逐行为:(按照 nodefsimagefs 是否放在一起做区分)

  • 有容器运行时分区
    • nodefs 达到驱逐阈值,那么 kubelet 删除已经退出的容器
    • imagefs 达到驱逐阈值,那么 kubelet 删除所有未使用的镜像
  • 无容器运行时分区
    • kubelet 同时删除未运行的容器和未使用的镜像

回收已经退出的容器和未使用的镜像后,如果节点依然满足驱逐条件,kubelet 就会开始驱逐正在运行的 Pod ,进一步释放磁盘空间。

  • 判断 Pod 的磁盘使用量是否超过请求的大小,超出请求资源的 Pod 会成为备选目标
  • 查询 Pod 的调度优先级,低优先级的 Pod 优先驱逐
  • 根据磁盘使用超过请求的数量进行排序,差值越小,越不容易被驱逐

容器和资源配置

Pod 的 QoS 级别是根据其容器的资源请求和限制来确定的。如果容器的资源请求和限制相等,则 Pod 被标记为 Guaranteed 级别。如果容器的资源请求小于限制,则 Pod 被标记为 Burstable 级别。如果容器没有设置资源请求和限制,则 Pod 被标记为 BestEffort 级别。

针对不同 QoS Class 的 Pod,Kubernetes 按如下 Hierarchy 组织 cgroup 中的 CPU 子系统(内存同理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# cd /sys/fs/cgroup/cpu/kubepods.slice/
root@master:/sys/fs/cgroup/cpu/kubepods.slice# ls -ltrah
total 0
dr-xr-xr-x 5 root root 0 Feb 2 02:06 ..
-r--r--r-- 1 root root 0 Feb 2 02:07 cpu.stat
-rw-r--r-- 1 root root 0 Feb 2 02:07 cpu.shares
-rw-r--r-- 1 root root 0 Feb 2 02:07 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 Feb 2 02:07 cpu.cfs_period_us
-r--r--r-- 1 root root 0 Feb 2 02:07 cpuacct.usage_percpu
-r--r--r-- 1 root root 0 Feb 2 02:07 cpuacct.usage_all
-rw-r--r-- 1 root root 0 Feb 2 02:07 cpuacct.usage
-r--r--r-- 1 root root 0 Feb 2 02:07 cpuacct.stat
-rw-r--r-- 1 root root 0 Feb 2 02:07 cgroup.clone_children
drwxr-xr-x 4 root root 0 Feb 2 02:07 .
-rw-r--r-- 1 root root 0 Feb 2 02:07 cgroup.procs
drwxr-xr-x 6 root root 0 Feb 2 02:07 kubepods-burstable.slice # 不同的 class 放入不同的目录
drwxr-xr-x 6 root root 0 Feb 2 02:07 kubepods-besteffort.slice
-rw-r--r-- 1 root root 0 Feb 2 03:30 tasks
-rw-r--r-- 1 root root 0 Feb 2 03:30 notify_on_release
-rw-r--r-- 1 root root 0 Feb 2 03:30 cpu.uclamp.min
-rw-r--r-- 1 root root 0 Feb 2 03:30 cpu.uclamp.max
-r--r--r-- 1 root root 0 Feb 2 03:30 cpuacct.usage_user
-r--r--r-- 1 root root 0 Feb 2 03:30 cpuacct.usage_sys
-r--r--r-- 1 root root 0 Feb 2 03:30 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0 Feb 2 03:30 cpuacct.usage_percpu_sys

Guaranteed 则是一个单独的目录。

image-20240202113103913

CPU CGroup 配置

CGroup 类型 参数 QoS 类型
容器的 CGroup cpu.shares BestEffort 2(最小值)
Burstable requests.cpu X 1024
Guaranteed requests.cpu X 1024
cpu.cfs_quota_us BestEffort -1(不限制)
Burstable limits.cpu X 100
Guaranteed limits.cpu X 100
Pod 的 CGroup cpu.shares BestEffort 2
Burstable Pod 所有容器(requests.cpu X 1024)之和
Guaranteed Pod 所有容器(requests.cpu X 1024)之和
cpu.cfs_quota_us BestEffort -1
Burstable Pod 所有容器(limits.cpu X 100)之和
Guaranteed Pod 所有容器(limits.cpu X 100)之和

容器和资源配置

针对不同 QoS Class 的 Pod,Kubernetes 按如下 Hierarchy 组织 cgroup 中的 Memory 子系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ls -ltra /sys/fs/cgroup/memory/kubepods.slice/
total 0
dr-xr-xr-x 5 root root 0 Feb 2 02:06 ..
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.use_hierarchy
-r--r--r-- 1 root root 0 Feb 2 02:07 memory.usage_in_bytes
-r--r--r-- 1 root root 0 Feb 2 02:07 memory.stat
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.soft_limit_in_bytes
-r--r--r-- 1 root root 0 Feb 2 02:07 memory.numa_stat
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.limit_in_bytes
-r--r--r-- 1 root root 0 Feb 2 02:07 memory.kmem.usage_in_bytes
-r--r--r-- 1 root root 0 Feb 2 02:07 memory.kmem.tcp.usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.kmem.tcp.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.kmem.tcp.limit_in_bytes
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.kmem.tcp.failcnt
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.kmem.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.kmem.limit_in_bytes
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.kmem.failcnt
-rw-r--r-- 1 root root 0 Feb 2 02:07 memory.failcnt
-rw-r--r-- 1 root root 0 Feb 2 02:07 cgroup.clone_children
drwxr-xr-x 4 root root 0 Feb 2 02:07 .
drwxr-xr-x 6 root root 0 Feb 2 02:07 kubepods-burstable.slice
drwxr-xr-x 6 root root 0 Feb 2 02:07 kubepods-besteffort.slice
-rw-r--r-- 1 root root 0 Feb 2 03:31 tasks
-rw-r--r-- 1 root root 0 Feb 2 03:31 notify_on_release
-rw-r--r-- 1 root root 0 Feb 2 03:31 memory.swappiness
---------- 1 root root 0 Feb 2 03:31 memory.pressure_level
-rw-r--r-- 1 root root 0 Feb 2 03:31 memory.oom_control
-rw-r--r-- 1 root root 0 Feb 2 03:31 memory.move_charge_at_immigrate
-r--r--r-- 1 root root 0 Feb 2 03:31 memory.kmem.slabinfo
--w------- 1 root root 0 Feb 2 03:31 memory.force_empty
-rw-r--r-- 1 root root 0 Feb 2 03:31 cgroup.procs
--w--w--w- 1 root root 0 Feb 2 03:31 cgroup.event_control

Guaranteed 则是一个单独的目录。

image-20240202113728218

内存 CGroup 配置

CGroup 类型 参数 QoS 类型
容器的 CGroup memory.limit_in_bytes BestEffort 9223372036854771712(最大值)
Burstable limits.memory
Guaranteed limits.memory
Pod 的 CGroup memory.limit_in_bytes BestEffort 9223372036854771712
Burstable Pod 所有容器(limits.memory)之和
Guaranteed Pod 所有容器(limits.memory)之和

OOM Killer 行为

  • 系统的 OOM Killer 可能会采取 OOM 的方式来中止某些容器的进程,进行必要的内存回收操作
  • 而系统根据进程的 oom_score 来进行优先级排序,选择待终止的进程,且进程的 oom_score 越高,越容易被终止
  • 进程的 oom_score 是根据当前进程使用的内存占节点总内存的比例值乘以 10,再加上 oom_score_adj 综合得到的
  • 而容器进程的 oom_score_adj 正是 kubelet 根据 memory.request 进行设置的
Pod QoS 类型 oom_score_adj
Guaranteed -997
BestEffort 1000
Burstable min(max(2, 1000-(1000 x memoryRequestBytes) / machineMemoryCapacityBytes), 999)

也就是优先杀死 BestEffort 中对内存占用较高的进程。

1
2
3
4
5
6
7
8
9
10
# crictl ps | grep nginx
c394818a94a1f b690f5f0a2d53 4 hours ago Running nginx 4 2fd8e7acb1da3 nginx-deployment-7854ff8877-qs72v
# crictl inspect c394818a94a1f | grep pid
"pid": 3414,
"pid": 1
"type": "pid"
# cat /proc/3414/oom_score
1334
# cat /proc/3414/oom_score_adj
1000

测试对 CPU 的校验和准入行为

定义一个 Pod,并将该 Pod 中的 nodeName 属性直接写成集群中的节点名

PodCPU 的资源设置为超出计算节点的 CPU 的值

创建该 Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: practice
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: test-pod
resources:
requests:
cpu: 10
dnsPolicy: ClusterFirst
nodeName: slave02
1
2
3
4
5
6
7
8
9
10
11
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning OutOfcpu 37s kubelet Node didn't have enough resource: cpu, requested: 10000, used: 200, capacity: 4000

status:
message: 'Pod was rejected: Node didn''t have enough resource: cpu, requested: 10000,
used: 200, capacity: 4000'
phase: Failed
reason: OutOfcpu
startTime: "2024-02-17T08:51:16Z"

可以看到,pod 被拒绝创建。

日志管理

节点上需要通过运行 logrotate 的定时任务对系统服务日志进行 rotate 清理,以防止系统服务日志占用大量的磁盘空间。

  • logrotate 的执行周期不能过长,以防日志短时间内大量增长
  • 同时配置日志的 rotate 条件,在日志不占用太多空间的情况下,保证有足够的日志可供查看
  • Docker
    • 出了基于系统 logrotate 管理日志,还可以依赖 Docker 自带的日志管理功能来设置容器日志的数量和每个日志文件的大小
    • Docker 写入数据之前会对日志大小进行检查和 rotate 操作,确保日志文件不会超过配置的数量和大小
  • Containerd
    • 日志的管理是通过 kubelet 定期(默认为 10s)执行 du 命令,来检查容器日志的数量和文件的大小的。
    • 每个容器日志的大小和可以保留的文件个数,可以通过 kubelet 的配置参数 container-log-max-sizecontainer-log-max-files 进行调整

Docker 卷管理

  • 在构建容器镜像时,可以在 Dockerfile 中通过 VOLUME 指令声明一个存储卷,目前 Kubernetes 尚未将其纳入管控范围,不建议使用。
  • 如果容器进程在可写层或 emptyDir 卷进行大量读写操作,就会导致磁盘 I/O 过高,从而影响其他容器进程甚至系统进程。(所有的容器都使用同一个运行时目录)
  • DockerContainerd 运行时都基于 CGroup v1。对于块设备,只支持对 Direct I/O 限速,而对于 Buffer I/O 还不具备有效的支持。因此,针对设备限速的问题,目前还没有完美的解决方案,对于特殊 I/O 需求的容器,建议使用独立的磁盘空间。(期待 CGroup v2 来解决)

网络资源

由网络插件通过 Linux Traffic ControlPod 限制带宽

可利用 CNI 社区提供的 bandwidth 插件

1
2
3
4
5
6
7
apiVersion: v1
kind: Pod
metadata:
annotations:
kubernetes.io/ingress-bandwidth: 1M
kubernetes.io/egress-bandwidth: 1M
...

进程数

kubelet 默认不限制 Pod 可以创建的子进程数量,但可以通过启动参数 podPidsLimit 开启限制,还可以由 reserved 参数为系统进程预留进程数。

  • kubelet 通过系统调用周期性地获取当前系统的 PID 的使用量,并读取 /proc/sys/kernel/pid_max,获取系统支持的 PID 上限
  • 如果当前的可用进程数少于设定阈值,那么 kubelet 会将节点对象的 PIDPressure 标记为 True
  • kube-schedule 在进行调度时,会从备选节点中对处于 NodeUnderPIDPressure 状态的节点进行过滤

节点异常检测

Kubernetes 集群可能存在的问题

  • 基础架构守护程序问题:NTP 服务关闭
  • 硬件问题:CPU,内存或磁盘损坏
  • 内核问题:内核死锁,文件系统损坏
  • 容器运行时问题:运行时守护程序无响应

当 Kubernetes 中节点发生上述问题,在整个集群中,Kubernetes 服务组件并不会感知以上问题,就会导致 Pod 仍会调度至问题节点。

node-problem-detector (NPD)

为了解决上面的问题,社区引入了守护进程 node-problem-detector,从各个守护进程收集节点问题,并使它们对上游层可见。

Kubernetes 节点诊断的工具,可以将节点的异常上报,例如:

  • Runtime 无响应
  • Linux Kernel 无响应
  • 网络插件
  • 文件描述符异常
  • 硬件问题如 CPU,内存或者磁盘故障

故障分类

Problem Daemon Types NodeCondition Description Configs
系统日志监控 KernelDeadlock ReadonlyFilesystem FrequentKubeletRestart FrequentDockerRestart FrequentContainerdRestart 通过监控系统日志来汇报问题并输出系统指标数据 filelog, kmsg, kernel abrt system
CustomPluginMonitor 按需定义 自定义插件监控允许用户自定义监控脚本,并运行这些脚本来进行监控 比如 npt 服务监控
HealthChecker KubeletUnhealthy ContainerRuntimeUnhealthy 针对 kubelet 和运行时的健康检查 kubelet
docker
containerd

问题汇报手段

node-problem-detector 通过设置 NodeCondition 或者创建 Event 对象来汇报问题

  • NodeCondition:针对永久性故障,会通过设置 NodeCondition 来改变节点状态
  • Event:临时故障通过 Event 来提醒相关对象,比如通知当前节点运行的所有 Pod

安装

按照教程:

https://helm.sh/docs/intro/install/

https://github.com/kubernetes/node-problem-detector

1
2
helm repo add deliveryhero https://charts.deliveryhero.io/
helm install --generate-name deliveryhero/node-problem-detector

或者手动安装:

1
2
3
helm repo add deliveryhero https://charts.deliveryhero.io/
helm pull deliveryhero/node-problem-detector
tar -zxvf node-problem-detector-2.3.12.tgz

改镜像

1
2
3
4
5
# vi node-problem-detector/values.yaml

image:
repository: k8s.mirror.nju.edu.cn/node-problem-detector/node-problem-detector
tag: v0.8.15

镜像源从 regisry.k8s.io 改为 k8s.mirror.nju.edu.cn

安装

1
helm install npd ./node-problem-detector
1
2
3
# kubectl get daemonsets.apps
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
npd-node-problem-detector 3 3 3 3 3 <none> 7m24s

测试,在节点的 kmsg 中刷入日志

1
sudo sh -c "echo 'kernel: BUG: unable to handle kernel NULL pointer dereference at TESTING' >> /dev/kmsg"
1
2
# kubectl logs -f npd-node-problem-detector-sv7hx --tail 10
I0202 07:00:52.076757 1 log_monitor.go:159] New status generated: &{Source:kernel-monitor Events:[{Severity:warn Timestamp:2024-02-02 07:00:50.754358117 +0000 UTC m=+138.323074338 Reason:KernelOops Message:kernel: BUG: unable to handle kernel NULL pointer dereference at TESTING}] Conditions:[{Type:KernelDeadlock Status:False Transition:2024-02-02 06:58:32.590661209 +0000 UTC m=+0.159377435 Reason:KernelHasNoDeadlock Message:kernel has no deadlock} {Type:ReadonlyFilesystem Status:False Transition:2024-02-02 06:58:32.590661356 +0000 UTC m=+0.159377577 Reason:FilesystemIsNotReadOnly Message:Filesystem is not read-only}]}

在节点的 event

1
2
3
4
5
# kubectl describe nodes master
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning KernelOops 36m kernel-monitor kernel: BUG: unable to handle kernel NULL pointer dereference at TESTING

测试,刷入 blocked 类型日志

1
sudo sh -c "echo 'kernel: INFO: task docker:20744 blocked for more than 120 seconds.' >> /dev/kmsg"

在节点的 condition

1
2
3
4
5
6
7
8
9
10
# kubectl get node master -o yaml
- lastHeartbeatTime: "2024-02-17T09:04:43Z"
lastTransitionTime: "2024-02-17T09:04:43Z"
message: 'kernel: INFO: task docker:20744 blocked for more than 120 seconds.'
reason: DockerHung
status: "True"
type: KernelDeadlock

# kubectl describe nodes master
Warning DockerHung 77s kernel-monitor Node condition KernelDeadlock is now: True, reason: DockerHung, message: "kernel: INFO: task docker:20744 blocked for more than 120 seconds."

NPD 只负责捕获异常状态,因此,还需要有监控系统做对应运维操作。

使用插件 Pod 启动 NPD

如果你使用的是自定义集群引导解决方案,不需要覆盖默认配置,可以利用插件 Pod 进一步自动化部署。

创建 node-stick-detector.yaml,并在控制平面节点上保存配置到插件 Pod 的目录 /etc/kubernetes/addons/node-problem-detector。Kubernetes 会自动去扫描这个 addons 目录安装插件。

NPD 的异常处理行为

  • NPD 只负责获取异常事件,并修改 node conditino,不会对节点状态和调度产生影响

    1
    2
    3
    4
    5
    6
    - lastHeartbeatTime: "2024-02-17T09:04:43Z"
    lastTransitionTime: "2024-02-17T09:04:43Z"
    message: 'kernel: INFO: task docker:20744 blocked for more than 120 seconds.'
    reason: DockerHung
    status: "True"
    type: KernelDeadlock
  • 需要自定义控制器,监听 NPD 汇报的 conditiontaint node,阻止 pod 调度到故障节点

  • 问题修复后,重启 NPD Pod 来清理错误事件

需要人为操作,修复好节点之后,删除 NPD Pod 。

常用节点问题排查手段

通常建议使用运维 pod 来做排查手段,并且给足够的权限

ssh 到内网节点

  • 创建一个支持 sshpod
  • 并通过负载均衡器转发 ssh 请求

查看日志

针对 systemd 拉起的服务:

  • journalctl -afu kubelet -S "2024-02-02 00:00:00"
    • -u unit:对应的 systemd 拉起的组件,如 kubelet
    • -f follow:跟踪最新日志
    • -a show all:实现所有日志列
    • -S since:从某一时间开始

对于标准的容器日志:

  • kubectl logs -f nginx-deployment-5759c4887d-gv98m -c nginx

    指定容器名 和 Pod 名称

  • kubectl logs -f --all-containers nginx-deployment-5759c4887d-gv98m

    打印 Pod 内所有容器的日志

  • kubectl logs -f nginx-deployment-5759c4887d-gv98m --previous

    查看 Pod crash 时上一个 Pod 的日志

如果容器日志被 shell 转储到文件,则需通过 exec

  • kubectl exec -it nginx-deployment-5759c4887d-gv98m -- tail -f /var/log/nginx/error.log

基于 extended resource 扩展节点资源

扩展资源

扩展资源是 kubernetes.io 域名之外的标准资源名称。它们使得集群管理员能够颁布非 Kubernetes 内置资源,而用户可以使用它们。

自定义扩展资源无法使用 kubernetes.io 作为资源域名。

管理扩展资源

  • 节点级扩展资源
    • 节点级扩展资源绑定到节点
  • 设备插件管理的资源
    • 发布在各节点上由设备插件所管理的资源,如 GPU,智能网卡等

为节点配置资源

  • 集群操作员可以向 API 服务器提交 PATCH HTTP 请求,以在集群中节点的 status.capacity 中为其配置可用数量
  • 完成此操作后,节点的 status.capacity 字段中将包含新资源
  • kubelet 会异步地对 status.allocatable 字段执行自动更新操作,使之包含新资源
  • 调度器在评估 Pod 是否适在某节点上执行时会使用节点的 status.allocatable 值,在更新节点容量使之包含新资源之后和请求该资源的第一个 Pod 被调度到该节点之间,可能会有短暂的延迟。
  • 例如注册扩展自定义的扩展资源

~/.kube/config 中的 client-key-dataclient-certificate-data 作为 keycrt

1
2
3
4
5
6
7
echo xxx | base64 -d > admin.key
echo xxx | base64 -d > admin.crt

curl --key admin.key --cert admin.crt --header "Content-Type: application/json-patch+json" \
--request PATCH -k \
--data '[{"op": "add", "path": "/status/capacity/xiaoyeshiyu.com~1reclaimed-cpu", "value": "2"}]' \
https://192.168.239.128:6443/api/v1/nodes/master/status

可以看到额外资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
allocatable:
cpu: "4"
ephemeral-storage: "18858870344"
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 3879252Ki
pods: "110"
xiaoyeshiyu.com/reclaimed-cpu: "2"
capacity:
cpu: "4"
ephemeral-storage: 20463184Ki
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 3981652Ki
pods: "110"
xiaoyeshiyu.com/reclaimed-cpu: "2"

使用扩展资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
limits:
xiaoyeshiyu.com/reclaimed-cpu: 3
requests:
xiaoyeshiyu.com/reclaimed-cpu: 3

通过 yaml 文件创建资源,可以看到由于节点资源不足的报错

1
2
3
4
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 26s default-scheduler 0/3 nodes are available: 3 Insufficient xiaoyeshiyu.com/reclaimed-cpu. preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.

reclaimed-cpu 的数量改为 2 即可。

集群层面的扩展资源

  • 可选择由默认调度器管理资源,默认调度器像管理其他资源一样管理扩展资源
    • RequestLimit 必须一致,因为 Kubernetes 无法确保扩展资源的超售
    • 而且给会给客户带来变动压力
  • 更常见的场景是,由调度器扩展程序(Scheduler Extenders)管理,这些程序处理资源消耗和资源配额
  • 修改调度器策略配置 ignoredByScheduler 字段可配置调度器不要检查自定义资源
  • 在波谷时,监控机器的负载,将资源通过扩展资源进行管理,将一些 Job 使用扩展资源进行调度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"kind" : "Policy",
"apiVersion" : "v1",
"predicates" : [
{"name" : "PodFitsHostPorts"},
{"name" : "PodFitsResources"},
{"name" : "NoDiskConflict"},
{"name" : "MatchNodeSelector"},
{"name" : "HostName"}
],
"priorities" : [
{"name" : "LeastRequestedPriority", "weight" : 1},
{"name" : "BalancedResourceAllocation", "weight" : 1},
{"name" : "ServiceSpreadingPriority", "weight" : 1},
{"name" : "EqualPriority", "weight" : 1}
],
"extenders" : [{
"urlPrefix": "http://localhost:8880",
"filterVerb": "predicates",
"prioritizeVerb": "priorities",
"preemptVerb": "preemption",
"bindVerb": "bind",
"weight": 1,
"enableHttps": false,
"nodeCacheCapable": false
"managedResources": [
{
"name": "expample.com/foo",
"ignoredByScheduler": true
}
]
}],
"hardPodAffinitySymmetricWeight" : 10
}

构建和管理高可用资源

在多节点、集群环境下管理 Kubernetes 生产系统。

Kubernetes 高可用层级

image-20240202161353373

  • 基础架构管理:提供计算资源的管理平台,在数据中心中安装部署服务器,作为计算节点、网络节点、存储节点,对应的,还需要管理操作系统,以及一些安全相关的策略,例如 iptablesfirewalldselinux ,以及容器运行时,一般选择 containerd
  • Kubernetes 本身管理:包括集群管理,每一个物理节点管理等
  • 在数据平面和控制平面有不同的插件和组件,控制平面服务于应用程序,控制平面服务与应用管理,控制平面除了 Kubernetes 自带的一些核心组件,还有有社区提供的插件例如 cni,针对一些特殊场景有自定义的用户空间控制器,以及基于告警系统的平台管理
  • 对接企业公共服务,面向企业,搭配企业 DNS 实现外部域名访问等

高可用的数据中心

  • 多地部署
  • 每个数据中心需要划分成具有独立供电、制冷、网络设备的高可用区
  • 每个高可用区管理独立的硬件资产,包括机架、计算节点、存储、负载均衡器、防火墙等硬件设备

Node 的生命周期管理

运营 Kubernetes 集群,不仅仅是集群搭建那么简单,运营需要对集群中所有节点的完整生命周期负责。

  • 集群搭建
  • 集群扩容、缩容
  • 集群销毁(很少)
  • 无论是集群搭建还是扩容,核心是 Node 的生命周期管理
    • Onboard
      • 物理资产上架
      • 操作系统安装
      • 网络配置
      • Kubernetes 组件安装
      • 创建 Node 对象
    • 故障处理
      • 临时故障:重启大法
      • 永久故障:机器下架
    • Offboard
      • 删除 Node 对象
      • 物理资产下载,送修、报废

主机管理

  • 选定哪个版本的系统内核、哪个发行版、安装哪些工具集、主机网络如何规划等
  • 日常的主机镜像升级更新也可能是造成服务不可用的因素之一
    • 主机镜像更新可以通过 A/B 系统 OTA(Over The Air) 升级方式进行
      • 分别使用 A、B 两个存储空间,共享一份用户数据。在升级过程中,OTA 更新即往其中一个存储空间写入升级包,同时保证了另一个系统可以正常运行,而不会打断用户。如果 OTA 失败,那么设备会启动到 OTA 之前的磁盘分区,并且仍然可以使用。

        更加推荐这种方式,不仅有安全保证,还可以批量进行。

生产化集群管理

  • 如何设定单个集群规模
    • 社区声明单一集群可支持 5000 节点,在如此规模的集群中,大规模部署应用是有诸多挑战的。应该更多还是更少?如何权衡?

      可以采用在 Pod 内搭建 git,然后使用 Pod 拉取代码构建。避免频繁创建 Pod

  • 如何根据地域划分集群:(etcd 集群部署,选主、心跳)
    • 不同地域的计算节点划分到同一集群
    • 将同一地域的节点划分到同一集群
  • 如何规划集群的网络
    • 企业办公环境、测试环境、预生产环境和生产环境应如何进行网络分离
    • 不同租户之间应如何进行网络隔离
  • 如何自动化搭建集群
    • 如何自动化搭建和升级集群,包括自动化部署控制平面和数据平面的核心组件
    • 如何与企业的公共服务集成

企业公共服务

  • 需要与企业认证平台集成,这样企业用户就能通过统一认证平台接入 Kubernetes 集群,而无须重新设计和管理一套用户系统
  • 集成企业的域名服务、负载均衡服务,提供集群服务对企业外发布的访问入口
  • 在与企业的公共服务集成时,需要考虑它们的服务是否可靠
  • 对于不能异步调用的请求,采用同步调用需要设置合理的超时时间
  • 过长的超时时间,会延迟结果等待时间,导致整体的链路调用时间延长,从而降低整体的 TPS
  • 有些失败是短暂的、偶然的(比如网络抖动),进行重试即可。而有些失败是必然的,重试反而会造成调用请求量放大,加重对调用系统的负担

控制平面的高可用保证

  • 针对大规模的集群,应该为控制平面组件划分单独节点,减少业务容器对控制平面容器或守护进程的干扰和资源抢占
  • 控制平面所在的节点,应确保在不同机架上,以防止因为某些机架的交换机或电源出问题,造成所有的控制面节点都无法工作
  • 保证控制平面的每个组件有足够的 CPU、内存和磁盘资源,过于严苛的资源限制会导致系统效率低下,降低集群可用性
  • 应尽可能地减少或消除外部依赖。在 Kubernetes 初期版本中存在较多 Cloud Provider API 的调用,导致在运营过程中,当 Cloud Provider API 出现故障时,会使得 Kubernetes 集群也无法正常工作
  • 应尽可能地将控制平面和数据平面解耦,确保控制平面组件出现故障时,将业务影响降到最低
  • Kubernetes 还有一些核心插件,是以普通的 Pod 形式加载运行的,可能会被调度到任意工作节点,与普通应用竞争资源。这些插件是否正常运行也决定了集群的可用性

高可用集群

image-20240202163749074

多节点选择专门的 master ,避免控制面流量被数据流量影响。

集群安装方法比较

安装工具 方法 优势 缺点
二进制 下载二进制文件,并通过设置 systemd 来管理 灵活度强 复杂,需要关心每一个组件的配置
对系统服务的依赖性过多
kubeadm kubeadm 是一个搭建集群的命令行工具
管理节点通过 kubeadm init 初始化
计算节点通过 kubeadm join 加入
相比二进制,控制面板组件的安装和配置被封装起来了
管理集群的生命周期,比如升级,证书管理等
操作系统层面的配置无自动化
运行时安装配置等复杂步骤依然是必须的
CNI 插件等需要手工安装
kubespray 通过 Ansible-playbook 完成集群搭建 自动完成操作系统层面的配置
利用了 kubeadm 作为集群管理工具
缺少基于声明式 API 的支持
KOPS 基于声明式 API 的集群管理工具 基于社区标准的 Cluster API 进行集群管理
节点的操作系统安装等全自动化
与云环境深度集成
灵活性差

综合来看,在商业化能力不够的情况下,安装大型集群推荐使用 kubespray 的方式。

目前社区推广的事 KOPS 的方式,但是需要与云环境定制开发。

用 Kubespray 搭建高可用集群

image-20240202164233706

通过 ansiblekubadm 实现自动化安装。

基于声明式 API 管理集群

集群管理不仅仅包括集群搭建,还有更多功能需要支持

  • 集群扩缩容
  • 节点健康检查和自动修复
  • Kubernetes 升级
  • 操作系统升级

云原生场景中集群应该按照我们的期望的状态运行,这意味着我们应该将集群管理建立在声明式 API 的基础之上。

Kubernetes Cluster API

image-20240202164609254

参与角色

  • 管理集群

    • 管理 workload 集群的集群,用来存放 Cluster API 对象的地方

      可同时管理多个集群

  • Workload 集群

    • 真正开放给用户用来运行应用的集群,由管理集群管理
  • Infrastructure provider

    • 提供不同云的基础架构管理,包括计算节点、网络等。目前流行的公有云多与 Cluster API 集成了
  • Bootstrap provider

    • 证书生成
    • 控制面组件安装和初始化,监控节点的创建
    • 将主节点和计算节点加入集群
  • Control plane

    • Kubernetes 控制平面组件

涉及模型

  • Machine
    • 计算节点,用来描述可以运行 Kubernetes 组件的机器对象(注意与 Kubernetes Node)的差异
    • 一个新 Machine 被创建以后,对应的控制器会创建一个计算节点,安装好操作系统并更新 Machine 的状态
    • 当一个 Machine 被删除后,对应的控制器会删除掉该节点并回收计算资源
    • Machine 属性被更新以后(比如 Kubernetes 版本更新),对应的控制器会删除旧节点并创建新节点
  • Machine ImmutabilityIn-place Upgrade vs. Replace
    • 不可变基础架构
  • MachineDeployment
    • 提供针对 MachineMachineSet 的声明式更新,类似于 Kubernetes Deployment(声明式思想统一)
  • MachineSet
    • 维护一个稳定的机器集合,类似 Kubernetes ReplicaSet
  • MachineHealthCheck
    • 定义节点应该被标记为不可用的条件

用 cluster API 管理集群

使用 kind 部署 Kubernetes on Docker

image-20240202165529854

KubeadmControlPlane

一些配置

image-20240202165552807

MachineDeployment

一些配置

image-20240202165609461

MachineHealthCheck

一些配置

image-20240202165630401

日常运营中的节点问题归类

可自动修复的问题

  • 计算节点 down
    • Ping 不通
    • TCP probe 失败
    • 节点上的所有应用都不可达

不可自动修复的问题

  • 文件系统损坏
  • 磁盘阵列故障
  • 网盘挂载问题
  • 其他硬件故障
  • kernel 出错,core dumps

其他问题

  • 软件 Bug
  • 进程锁死,或者 memory/CPU 竞争问题
  • Kubernetes 组件出问题
    • kubelet/kube-proxy/Docker/Salt

故障检测和自动恢复

当创建 Compute 节点时,允许定义 Liveness Probe

  • livenessProbe 失败时,ComputeNodeProbePassed 设置为 false

Prometheus 中,已经有 Node Levelalert,抓取 Prometheus 中的 alert

1
2
3
4
5
6
7
spec:
containers:
livenessProbe:
tcpSocket:
port: 22
initialDelaySeconds: 15
periodSeconds: 20

设定自动恢复规则

  • 大多数情况下,重启大法(Restart operator
  • 如果重启不行就重装(reprovision
  • 重装不行就重修(breakfix

探活失败示例:

image-20240202170110444

Cluster Autoscaler

每一个厂商都有自己的一套 cluster aotoscaler 方法,但是原理都差不多。

工作机制

  • 扩容:
    • 由于资源不足,pod 调度失败,即有 pod 一直处于 Pending 状态
  • 缩容
    • node 的资源利用率较低时,持续 10 分钟低于 50%
    • node 上存在的 pod 都能被重新调度到其他 node 上运行

image-20240202170219230

Cluster Autoscaler 架构

Autoscaler:核心模块,负责整体扩缩容功能

Estimator:负责评估计算扩容节点,数量以及是否支持扩容

Simulator:负责模拟调度,计算缩容节点,资源以及调度之后是否可以运行

Cloud-Provider:与云交互进行节点的增删操作,每个支持 CA 的主流厂商都实现自己的 plugin 实现动态缩放

image-20240202170409037

Cluster Autoscaler 的扩展机制

为了自动创建和初始化 NodeCluster Autoscaler 要求 Node 必须属于某个 Node Group,比如:

  • GCE/GKE 中的 Managed instance groups(MIG)
  • AWS 中的 Autoscaling Groups
  • Cluster API Node

当集群中有多个 Node Group 时,可以通过 --expander=<option> 选项配置选择 Node Group 的策略,支持如下四种方式:

  • random:随机选择
  • most-pods:选择容量最大(可以创建最多 Pod)的 Node Group
  • least-waste:以最小浪费原则选择,即选择有最少可用资源的 Node Group
  • price:选择最便宜的 Node Group

附加资料

代码:

https://github.com/kubernetes/autoscaler

Cluster APICluster Autoscaler 的整合

https://cluster-api.sigs.k8s.io/tasks/automated-machine-management/autoscaling

集群管理实践案例分享

cluster API 出现之前,一些厂商都有自己的管理方式。

image-20240202171031379

设备模型通过不同的区域、数据中心、机架。机器上架之后,注册到集群中。

设备通过集群模型进行抽象资源化,通过划分到不同的节点池,用于后续扩容缩容使用。

如果是虚拟化 Openstack 则通过 api 启动 node,是物理机则使用 kickstart ,使用 OSImage 定义操作系统

声明式集群配置

通过 yaml 文件声明节点信息,salt 安装

image-20240202171046625

声明式扩容

定义 NodePool 定义资源副本数,通过 NodePool Controller 创建 ComputeNode 描述节点信息,Provision Controllerprovider 交互创建虚拟机,安装 Kubernetes 。

image-20240202171108988

声明式持续发布

操作系统或者 Kubernetes 组件更新时,通过 ClusterDeployment 的方式一个一个节点更新节点(直接更换,而不是升级)。好处是方便,坏处是缓慢。

可以优化成只有操作系统更新时才更换节点,Kubernetes 更新时只更新部署。

image-20240202171131474

自定义插件 - 声明式集群管理对象

也就是 Kubernetes on Kubernetes

image-20240207190602635

多租户集群管理

多租户集群,核心还是基于 Cluster API

租户

  • 租户是指一组拥有访问特定软件资源权限的用户集合,在多租户环境中,它还包括共享的应用、服务、数据和各项配置等
  • 多租户集群必须将租户彼此隔离,以最大限度地减少租户与租户、租户与集群之间的影响
  • 集群须在租户之间公平地分配集群资源。通过多租户共享集群资源,可以有效地降低集群管理成本,提高整体集群的资源利用率。

认证 - 实现多租户的基础

  • 租户管理首选需要识别访问的用户是谁,因此用户身份认证是多租户的基础
  • 权限控制,如允许合法登录的用户访问、拒绝非法登录的用户访问或提供有限的匿名访问
  • Kubernetes 可管理两类用户
    • 用来标识和管理系统组件的 ServiceAccount
    • 外部用户的认证,需要通过 Kubernetes 的认证扩展来对接企业、提供商的认证服务,为用户验证、操作授权、资源隔离等提供基础

隔离

除认证、授权这些基础条件外,还要能够保证用户的工作负载彼此之间有尽可能安全的隔离,减少用户工作负载之间的影响。通常从权限、网络、数据三个方面对不同用户进行隔离。

  • 权限隔离
    • 普通用户的容器默认不具有 priviledgedsys_adminnet_admin 等高级管理权限,以阻止对宿主机及其其他用户的容器进行读取、写入等操作
  • 网络隔离
    • 不同的 Pod,运行在不同的 Network Namespace 中,拥有独立的网络协议栈。Pod 之间只能通过容器开放的端口进行通信,不能通过其他方式进行访问。例如使用 iptables
  • 数据隔离
    • 容器之间利用 Namespace 进行隔离。不同 Pod 的容器,运行在不同的 MNTUTSPIDIPC Namespace 上,相互之间无法访问对方的文件系统、进程、IPC 等信息;同一个 Pod 的容器,其 mntPID Namespace 也不共享

租户隔离手段

基于 Namespace 实现隔离

image-20240202172022775

  • NamespaceNamespace 属于且仅属于一个租户
  • 权限定义:定义内容包括命名空间中的 RoleRoleBinding。这些资源表示目前租户在归属于自己的命名空间中定义了什么权限、授权给了哪些租户的成员
  • Pod 安全策略:特殊权限指集群级的特定资源定义 - PodSecurityPolicy。它定义了一系列工作负载与基础设施之间、工作负载与工作负载之间的关联关系,并通过命名空间的 RoleBinding 完成授权
  • 网络策略:基础设施层面为保障租户网络的隔离机制提供了一系列默认策略,以及租户自己定制的用户租户应用彼此访问的策略
  • PodServicePersistentVolumeClaim 等命名空间资源:这些定义表示租户的应用落地到 Kubernetes 中的实体

权限隔离

image-20240202172424091

  • 基于 Namespace 的权限隔离
    • 创建一个 namespace-admin ClusterRole,拥有所有对象的所有权限
    • 为用户开辟新 namespace,并在该 namespace 创建 rolebinding 绑定 namespace-admin ClusterRole,用户即可拥有当前 namespace 所有对象操作权限
  • 自动化解决方案
    • Namespace 创建时,通过 mutatingwebhooknamespace 变形,将用户信息记录至 namespace annotation
    • 创建一个控制器,监控 namespace,创建 rolebinding 为该用户绑定 namespace-admin 的权限

Quota 管理

image-20240202172656160

  • 开启 ResourceQuota 准入插件

  • 在用户 namespace 创建 ResourceQuota 对象进行限额配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: ResourceQuota
metadata:
name: pods-high
spec:
hard:
cpu: "1000"
memory: 200Gi
pods: "10"
scopeSelector:
matchExpressions:
- operator : In
scopeName: PriorityClass
values: ["high"]
- apiVersion: v1
kind: ResourceQuota
metadata:
name: pods-medium
spec:
hard:
cpu: "10"
memory: 20Gi
pods: "10"
scopeSelector:
matchExpressions:
- operator : In
scopeName: PriorityClass
values: ["medium"]
- apiVersion: v1
kind: ResourceQuota
metadata:
name: pods-low
spec:
hard:
cpu: "5"
memory: 10Gi
pods: "10"
scopeSelector:
matchExpressions:
- operator : In
scopeName: PriorityClass
values: ["low"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# kubectl describe quota

Name: pods-high
Namespace: default
Resource Used Hard
-------- ---- ----
cpu 500m 1k
memory 10Gi 200Gi
pods 1 10


Name: pods-low
Namespace: default
Resource Used Hard
-------- ---- ----
cpu 0 5
memory 0 10Gi
pods 0 10


Name: pods-medium
Namespace: default
Resource Used Hard
-------- ---- ----
cpu 0 10
memory 0 20Gi
pods 0 10

节点资源隔离

通过为节点设置不同 taint 来识别不同租户的计算资源

不同租户在创建 Pod 时,增加 Toleration 关键字,确保其能调度至某个 taint 的节点。

references

Compose server

node-problem-detector