故事背景是 一个 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 开始变多,我能感受到大家上云的热情,大家都在改造自己的程序,并将其部署到生产环境中。一切看起来都很正常,
直到有一天......
一天清晨,小白刚刚醒来打开电脑,邮箱像是被轰炸了,很多业务方都说他们部署在云上的应用有问题,有的访问慢,有的无法部署,有的崩溃。这时小白赶快登录到 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经验小白运维大规模集群还在继续,又将发生哪些生产事故呢? 先点个赞和关注等更新吧~