0经验小白运维生产 k8s 翻车日记1 资源争夺大战

2023年 9月 30日 58.9k 0

故事背景是 一个 0 经验的运维小白,没有接触过任何 K8S 知识,在公司突如其来的云原生战略转型中临危受命,担任大规模集群的运维工作中发生的各种翻车事件...

我是小白,当听说公司采购了一些 K8S 基础设施由我运维管理时,我是兴奋的。一想到原本虚拟机上部署的业务都会迁移到我这里,我掌控着公司各种核心业务的“生死” 我是自豪的。 当 K8S 权限到手后,我先对所有集群进行了一圈巡视通过 Dashboard, 但是总觉得不够 “高级”,运维应该是通过黑色的窗口,命令行才酷,所以我立马搜了一些觉得 “可能” 会用的到的 K8S 查询命令,而且这个 Dashboard 也是一个 web 服务,万一这个 web 服务挂掉,我应该也能通过 kubectl 命令行工具来 trouble shotting。

赶快背下了这些命令:

kubectl cluster-info # 了解集群信息
kubectl get nodes # 查看集群的每个机器情况
kubectl get namespaces # 查看所有命名空间情况
kubectl get pods # 查看所有pod情况
kubectl describe pod [POD_NAME] # 看某个pod具体情况
kubectl top pods # 查看pod资源使用情况
kubectl top nodes # 查看所有机器使用资源情况

翻车了

当我熟悉了一些k8s命令后,从网上找了一些应用程序部署在k8s的模版, golang 版本,python 版本,java版本,nodejs 版本, .Net 版本,自己尝试部署了一下都没问题就约所有开发team一个“上云大会”,我将部署文件上传到 github 给大家参考,要求开发同事参考我的部署文件,将云上的程序部署在k8s上。 看着所有集群上的 pod 开始变多,我能感受到大家上云的热情,大家都在改造自己的程序,并将其部署到生产环境中。一切看起来都很正常,

image.png

直到有一天......

一天清晨,小白刚刚醒来打开电脑,邮箱像是被轰炸了,很多业务方都说他们部署在云上的应用有问题,有的访问慢,有的无法部署,有的崩溃。这时小白赶快登录到 K8S 控制台,他眼前的画面令他目瞪口呆。集群里的资源被抢得七零八落,根本满足不了所有应用程序的需求,导致CPU和内存资源耗尽。应用程序无法正常响应,服务器不堪重负,系统崩溃的危机一触即发。

小白意识到他犯下了一个严重的错误 - 没有在部署文件中提供资源限制的参考!紧急情况下,小白试图增加资源来解决故障,但很快他发现这并不是一个可行的解决方案。申请新的机器和开通网络需要等待好多天的时间,而故障已经给用户和组织带来了严重的损失。

不要慌!系统昨天还好好的,今天突发问题,事出反常必有妖,一定是某个应用导致的!之前背的命令行派上用场了,脑海中直接浮现出 kubectl top pods 我要找到这个应用占用资源异常的容器。

果然,命令巧了以后,有个应用占用的应用非常多!无论是 cpu 还是 内存,我盲猜他 内存泄漏 + 死循环!接下来通过 kubectl describe pod [POD_NAME] 得到了一些更详细的信息,发现了导致资源争夺的罪魁祸,那先把它从 delpoyment 中删除掉,然后找到业务方,并他们的开发团队紧急合作,进行了问题修复。

解决问题

小白深刻理解了资源限制的重要性,立马开始实践应用在k8s中资源限制的解决方案。

通过查阅 Kubernetes 文档,立马开始约所有业务方开发开会,来 align 我们的解决方案,作为K8S集群管理员,小白要限制每个业务方对集群的资源使用比例。

  • 使用 Resource Quotas(资源配额):通过定义 Resource Quotas,可以限制命名空间内的资源使用情况,例如限制 CPU 和内存的使用量。 当时给每个业务团队都是以 namespace 来划分权限的,一个 namespace 一个 team, 这样治理起来最省心了,给每个 team 划分这么大的资源,具体这个空间的资源怎么使用就由项目组自己分配。
apiVersion: v1
kind: ResourceQuota
metadata:
  name: example-resource-quota
  namespace: teama-namespace
spec:
  hard:
    pods: "10"
    requests.cpu: "4"
    limits.cpu: "8"
    requests.memory: 4Gi
    limits.memory: 8Gi

Resource Quotas 限制了命名空间 "teama-namespace" 内的资源使用情况。例如,最多可以创建 10 个 Pod,CPU 请求量为 4 个核心,CPU 限制量为 8 个核心,内存请求量为 4GB,内存限制量为 8GB。

在 Kubernetes 中,CPU 和内存是两个常用的资源参数。

  • CPU(中央处理器):在 Kubernetes 中,CPU 被表示为 CPU 的核心数量或 CPU 的百分比。例如,对于一个双核心的 CPU,可以用 "2" 来表示。另外,可以使用小数点来表示 CPU 的百分比,如 "0.5" 表示使用 50% 的 CPU。
  • 内存:在 Kubernetes 中,内存被表示为字节(Byte),可以使用不同的单位来表示,如 "Ki" 表示千字节,"Mi" 表示兆字节,"Gi" 表示吉字节。例如,"8Gi" 表示 8GB 的内存。

cpu 请求量 requests.cpu 和 cpu 限制量 limits.cpu 是什么含义呢?

  • requests.cpu:表示应用程序对 CPU 的资源请求量也可以理解为最小资源运行所需量。这个值告诉 Kubernetes 集群应该预留多少 CPU 资源给应用程序。如果没有设置 requests.cpu,Kubernetes 可以根据集群的可用资源自动进行调度。

  • limits.cpu:表示应用程序对 CPU 的资源限制量。这个值告诉 Kubernetes 应用程序最多可以使用多少 CPU 资源。超过这个限制的话,Kubernetes 会限制应用程序的 CPU 使用,以避免对其他应用程序造成影响。

假设一个应用程序的 requests.cpu 设置为 0.5,limits.cpu 设置为 1。这意味着应用程序在运行时需要至少 0.5 个 CPU 的资源,但不能超过 1 个 CPU 的资源。

那 memory 也是同理。

  • 限制应用程序本身占用的资源
    也就是限制每个 POD 的资源使用率,确保 POD 在意外情况不会滥用资源
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx
    resources:
      limits:
        cpu: "1"
        memory: "512Mi"
      requests:
        cpu: "0.5"
        memory: "256Mi"

在上面的示例中,Pod的容器my-container被限制为最多使用1个CPU和512Mi的内存。同时,它至少需要0.5个CPU和256Mi的内存。

通过这种方式,我们让业务方有效地管理和控制Kubernetes集群中的Pod的资源使用情况,不会超过他们 namespace 内的总资源配额。

  • 限制容器或者pod的资源使用率
    LimitRange是Kubernetes中用来限制Pod或者Container资源使用的对象。它可以定义最小和最大的资源限制,以及默认的请求资源值。
apiVersion: v1
kind: LimitRange
metadata:
  name: example-limit-range
spec:
  limits:
  - type: Container
    max:
      cpu: "1"
      memory: 1Gi
    min:
      cpu: "0.1"
      memory: 100Mi
    default:
      cpu: "0.5"
      memory: 500Mi
    defaultRequest:
      cpu: "0.2"
      memory: 200Mi

在这个示例中,LimitRange对象限制了容器的资源使用。最大资源限制为1个CPU和1Gi内存,最小资源限制为0.1个CPU和100Mi内存。默认的资源限制为0.5个CPU和500Mi内存,而默认的资源请求为0.2个CPU和200Mi内存。

与业务方的开发同步了资源限制使用方案后,大家都积极配合,对自己的部署文件进行了修改,也协助业务方对应用的资源要求进行了评估,通过预期流量压测的方式。这样所有资源都在一个可控范围内,终于心安了!

在我们优化的过程中,我们实施了资源限制策略,就像是给每个应用程序上了"锁",让它们只能拿到自己需要的资源。我们还调整了应用程序的调度策略,就像是给它们排队买票一样,公平地分配资源。

深入问题

前面我们聊到可以通过资源限制来限制应用的资源使用率,那资源限制的原理是什么呢?

资源限制是通过Linux的cgroups和namespace机制实现的。cgroups(control groups)是Linux内核的一个功能,它允许对进程组(例如容器)分配资源,并对其资源使用进行限制。命名空间提供了一种隔离机制,使得每个容器都拥有自己独立的运行环境,包括文件系统、网络和进程空间。

通过使用cgroups和命名空间,Kubernetes可以在集群中对容器进行资源管理和隔离。它可以根据容器的资源请求和限制来分配和限制资源,确保每个容器都能够正常运行,并避免容器之间的资源争用。

cgroups

简单的来说,如果我们想限制一个应用,我们就创建一个 cgroup, 然后设置这个 cgroup 的以下属性

  • CPU:可以限制CPU使用的时间片数量或百分比。
  • 内存:可以限制使用的物理内存或虚拟内存的数量。
  • 磁盘IO:可以限制磁盘读写的速度或数量。
  • 网络带宽:可以限制网络传输的速度。

设置后之后,将我们的应用程序进程添加到这个 cgroup 就完成了应用程序的使用资源限制。

作为新手运维的我一定要操作一下才能保证我的运维信心!

# 创建一个名为myapp的cgroup
sudo cgcreate -g cpu,memory:/myapp

# 设置CPU使用率限制为50%
sudo cgset -r cpu.cfs_quota_us=50000 /myapp

# 设置内存限制为200MB
sudo cgset -r memory.limit_in_bytes=200000000 /myapp

# 将进程添加到myapp cgroup中
sudo cgexec -g cpu,memory:/myapp /path/to/myapp

在这个例子中,我们创建了一个名为myapp的cgroup,并为它设置了CPU和内存的限制。CPU使用率限制为50%,表示myapp进程最多只能使用50%的CPU时间片。内存限制为200MB,表示myapp进程在运行时最多只能使用200MB的内存。

通过将进程添加到myapp cgroup中,我们可以确保myapp进程在运行时遵守这些资源限制。如果进程超过了限制,操作系统会限制其资源使用,并可能导致进程性能下降或终止。

了解了 cgroup 如何使用后,我们就看容易得的看懂 docker 命令是如何限制应用资源的, 比如下面的命令将限制容器使用的内存为 2GB。

docker run --memory=2g

再来一个更细节的例子, Docker 或者 K8S 底层是怎么调用 cgroup 的呢? 来大概演示一下

package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    // 创建一个新的进程
    cmd := exec.Command("/bin/bash")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
    }

    // 设置cgroup资源限制
    cmd.SysProcAttr.Cgroup = &syscall.Cgroup{
        Resources: syscall.CgroupResource{
            MemoryLimit:  "200M",
            CPUShares:    512,
            BlkioWeight:  500,
        },
    }

    // 启动新的进程
    if err := cmd.Start(); err != nil {
        fmt.Printf("Failed to start command: %v", err)
        os.Exit(1)
    }

    // 等待进程结束
    if err := cmd.Wait(); err != nil {
        fmt.Printf("Command finished with error: %v", err)
        os.Exit(1)
    }
}

Line 19 通过 Linux 系统调用来设置了新建进程的 cgroup,然后启动这个进程。这个进程就会被限制在我们设置的资源内,当然实际代码比这个复杂很多,但是我们通过这个例子知道一个大概原理就可以了。

namespace

除了 cgroup 外,做资源隔离的东西机制叫做 linux 的 namespace, 它用于实现进程之间的资源隔离和隔离环境。事实上 linux 本身就提供了很多种类型的命名空间比如:

  • Mount Namespace:提供文件系统隔离
  • UTS Namespace:提供主机名和域名隔离
  • IPC Namespace:提供进程间通信隔离
  • PID Namespace:提供进程隔离
  • Network Namespace:提供网络隔离
  • User Namespace:提供用户和用户组隔离

命名空间通过为每个进程提供独立的资源视图,使得每个进程看起来都在独立的环境中运行,拥有自己独立的文件系统、网络、进程等。

也就是说,不使用命名空间的话,父进程在建立子进程后,子进程是与父进程使用的同一个资源空间,如果让父子资源隔离呢?可以调用 system call clone() 并传递不同的CLONE_NEW*(上面提到的各种命名空间类型)标志,给子进程创建新的命名空间

#define _GNU_SOURCE
#include 
#include 
#include 
#include 

#define STACK_SIZE (1024 * 1024)

char child_stack[STACK_SIZE];

void child_process()
{
    printf("Child process: PID=%ld\n", (long)getpid());

    // 在子进程中进行资源隔离
    unshare(CLONE_NEWNS | CLONE_NEWNET);

    // 在子进程中进行其他自定义操作

    // 执行一个新的命令
    char *args[] = { "/bin/bash", NULL };
    execv(args[0], args);
}

int main()
{
    printf("Parent process: PID=%ld\n", (long)getpid());

    // 创建子进程,并指定子进程运行的函数
    pid_t child_pid = clone(child_process, child_stack + STACK_SIZE, CLONE_NEWPID | SIGCHLD, NULL);
    if (child_pid == -1)
    {
        perror("Failed to create child process");
        exit(1);
    }

    // 等待子进程结束
    if (waitpid(child_pid, NULL, 0) == -1)
    {
        perror("Failed to wait for child process");
        exit(1);
    }

    return 0;
}

Line 16 通过调用unshare()函数并指定相应的命名空间标志(CLONE_NEWNS和CLONE_NEWNET),创建了一个子进程,并在子进程中进行资源隔离。子进程在独立的Mount Namespace和Network Namespace中运行,拥有自己独立的文件系统和网络环境,与父进程和其他进程相互隔离。

结束语

总之呢,通过这次的事件,我与开发团队建立了“一定”的友谊,我帮助他们检查并排查可能的CPU和内存泄漏问题。尽管作为运维角色我通常不直接开发应用程序,但在我例行的监控和管理生产环境中的应用程序时,通过观察性能指标、日志和报警来发现潜在的资源泄漏,找开发团队协助修复这些问题,大家都觉得我是值得信赖的(哈哈哈哈)!, 0经验小白运维大规模集群还在继续,又将发生哪些生产事故呢? 先点个赞和关注等更新吧~

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论