如何hack边缘的kubelet修改Cgroup数值

3 阅读4分钟

之前做了一个VPA项目的需求,就是需要不重启的方式修改容器的Cgroup的值已达到垂直扩缩容的目的,项目中核心的思路如下

  1. 上游下发要VPA的结果的值写入到容器的Annotation里面
  2. Kubelet 感知到这个 annoation 的变化
  3. 我们本地运行一个 Agent,里面运行一个类似的job监听kubelet的变化,当发现annotation的值改变的时候就去修改对应容器的Cgroup的数值即可

具体步骤如下

Cgroup 是什么

简而言之,对容器技术而言,其实现资源层面上的限制和隔离,依赖于 Linux 内核提供的 Cgroup 和 namespace 技术

cgroup 的主要作用:管理资源的分配和限制;主要限制的资源是CPU、内存、网络和磁盘IO

namespace 的主要作用:封装抽象、限制、隔离,使命名空间内的进程看起来拥有他们自己的全局资源

以一个最简单的例子来校验Cgroup的限制,我们使用docker来创建以下容器

docker run --rm -d --cpus=2 --memory=2g --name=2c2g redis:alpine

查看上述容器的ID

➜ dps | grep -i 2c2g
37ad8252f8de   redis:alpine   "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes   6379/tcp   2c2g

找到 /sys/fs/cgroup/system.slice 目录对应的容器

可看到有很多容器的选项可供查看

查看CPU和内存的Max

➜  docker-37ad8252f8de50694e3321364c9ef5e94ae08f45f50e633e561feb291d1c27e9.scope cat cpu.max
200000 100000
➜  docker-37ad8252f8de50694e3321364c9ef5e94ae08f45f50e633e561feb291d1c27e9.scope cat memory.max
2147483648        # 这里的单位是Byte

我们查看容器的占用

可以看到这个2G就是我们设定的数值

那尝试直接修改这个cgroup的数值,是不是就能修改容器的内存和CPU的限制从而做到VPA的效果

我们尝试修改内存由原来的2G到现在的1G

echo "1073741824" >> memory.max

再次查看内存的占用比值,可以发现确实修改已经生效

Kubelet 的接口

首先明确一点,你在管控面 kubelet get 等信息基本都是从边缘端映射过来的,所以单机侧的 kubelet 也能获取到很多信息,比如

/configz     返回 kubelet 的相关配置信息
/metrics     暴露 kubelet 和容器的监控指标,比如 CPU 和内存的使用率,网络流量等
/pods        返回当前节点上的所有 Pod 的信息,包括 Pod 的状态以及容器信息等
/stats/summary    提供节点的资源使用统计信息,包括CPU、内存、网络、磁盘等
/exex/<pod_namespace>/<pod_name>/<container_name>     提供指定容器的日志,支持tail和follow参数
/logs        用于获取正在运行的pods和容器日志

配置服务账号访问kubelet

访问 kubelet 需要使用密钥,我们先配置服务账号访问,直接配置成最大的 clusterrole 权限

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: system:my-agent
subjects:
  - kind: ServiceAccount
    name: my-agent
    namespace: default
roleRef:
  kind: ClusterRole
  name: system:my-agent
  apiGroup: rbac.authorization.k8s.io
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: system:my-agent
rules:
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - proxy
  - apiGroups:
      - ""
    resources:
      - nodes/log
      - nodes/metrics
      - nodes/proxy
      - nodes/spec
      - nodes/stats
    verbs:
      - '*'
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-agent
  namespace: default

上面的服务账号创建完成之后就能看到下面的密钥了

kubectl get secret $(kubectl get serviceaccount my-agent -o jsonpath='{.secrets[0].name}') -o jsonpath='{.data.token}' | base64 --decode

比如我这个机器上是

➜  ~ k get secret $(kubectl get serviceaccount my-agent -o jsonpath='{.secrets[0].name}') -o jsonpath='{.data.token}' | base64 --decode
eyJhbGciOiJSUzI1NiIsImtpZCI6IlBQTDNXcGNKenVxNGZ3OHdpam5WQ0Jnd3d3NWpMbVZtSWJETTQ0WW5EZHMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6Im15LWFnZW50LXRva2VuLXE0a2hsIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Im15LWFnZW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiY2UyMzQzNGEtMTViYy00YzI1LWIwYTktZGQxM2E3MDllMGRlIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6bXktYWdlbnQifQ.UjNJxEbaLDm_19wVB4OPi-3u6fTG2noCQrLI7e7-PA1DoPc4SQJHzVXmPGwP6Ln4Odv-PrC0jkOpGF4DQQcQx589HsXN0zpmn6z-GJdzTsVxFFaJZpgZlfITGN99zQlr-AkRjgEm_9vUpijjjXOP0dWmPlHlmpxSYqpf3zSohIJsBySpXp1sQKboaeqTHbXU02xwsCjFDHeOyfESQcyVOtKQk9_w0Qvtiz5DcCClAZSCW8WLQPX9X7wie_vZCKlcNKiS5eKYCdymm6ZndXyidqb9Ga027_jhZnjvPO27rzBAL5wiTgonYAUau6tvB9KENfYAFF3pm2rTJZYujjUOfA

那用这个 token 是否可以访问到机器上的 pods 信息呢

我们试着对 /configz 接口发起访问,访问其 kubelet 的配置信息

可以看到访问成功

实操:部署pod并做VPA操作

手动修改

创建一个 nginx 的 deployment,设定资源限制为2core2G

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:latest  # 使用最新的 Nginx 镜像
        resources:
          requests:
            memory: "2Gi"  # 请求的内存
            cpu: "2"       # 请求的 CPU
          limits:
            memory: "2Gi"  # 限制的内存
            cpu: "2"       # 限制的 CPU
        ports:
        - containerPort: 80  # Nginx 默认监听的端口

创建完成之后到目录 /sys/fs/cgroup/kubepods.slice 下面就能看到我们创建的pod

这个后缀就是我们pod的uid

进入到这个目录下面

可以发现这个pod里面实际运行了两个容器,一个 sandbox 容器用来隔离容器的网络,一个业务自身的容器

分别对应的pod的信息如下

我们进入到我们的业务pod里面,修改内存的取值如下

➜  docker-567f43cdcb3c5630ffc17735d92b2895912a65a3e03fd5575262a52709eba00b.scope cat memory.max
2147483648

修改为1G

然后使用 docker stats 查看容器的负载使用情况

可以看到容器的内存使用上限已经修改为我们期望的1GB,并且是不需要在重启容器的情况下进行的

参考文档

一篇搞懂容器技术的基石: cgroup