欧美午夜视频一区二区在线观看_日本中文不卡影片_婷婷五月激情影院_中文字幕亚洲综合小综合片源丰富、内容全面_97中文字幕永久在线_玩弄初次红杏出墙少妇_国产野外无码理论片_国产对白刺激视频

返回全部

使用 Cgroups 限制 Kubernetes Pod 進程數

作者: 李澤璽

Kubernetes 里面的 Pod 資源是最小的計算單元,抽象了一組(一個或多個)容器。容器也是 Linux 系統上的進程,但基于 Namespace 和 Cgroups(Control groups) 等技術實現了不同程度的隔離。 簡單來說 Namespace 可以讓每個進程有獨立的 PID, IPC 和網絡空間。Cgroups 可以控制進程的資源占用,比如 CPU ,內存和允許的最大進程數等等。

今天主要介紹如何通過 Cgroups 里面的 pids 控制器限制 Kubernetes Pod 容器的最大進程數量。

場景介紹

之前遇到過這樣一個問題,我們的服務會調用執行外部的命令,每調用一次外部命令就會 fork 產生子進程。但是由于代碼上的 bug ,沒有及時對子進程回收,然后這個容器不斷 fork 產生子進程,耗盡了宿主機的進程表空間,最終導致整個系統不響應,影響了其它的服務。

這種問題除了讓開發人員修復 bug 外,也需要在系統層面對進程數量進行限制。所以,如果一個容器里面運行的服務會 fork 產生子進程,就很有必要使用 Cgroups 的 pids 控制器限制這個容器能運行的最大進程數量。

解決方法

Kubelet 開啟 PodPidsLimit 功能

Kubernetes 里面的每個節點都會運行一個叫做 Kubelet 的服務,負責節點上容器的狀態和生命周期,比如創建和刪除容器。根據 Kubernetes 的官方文檔 Process ID Limits And Reservations 內容,可以設置 Kubelet 服務的 –pod-max-pids 配置選項,之后在該節點上創建的容器,最終都會使用 Cgroups pid 控制器限制容器的進程數量。

Process ID Limits And Reservations

我們 Kubernetes 是在 CentOS 7 上使用 kubeadm 部署的 v1.15.9 版本,需要額外設置 SupportPodPidsLimit 的 feature-gate,對應操作如下(其它發行版應該也類似):

# kubelet 使用 systemd 啟動的,可以通過編輯 /etc/sysconfig/kubelet
# 添加額外的啟動參數,設置 pod 最大進程數為 1024
$ vim /etc/sysconfig/kubelet
KUBELET_EXTRA_ARGS="--pod-max-pids=1024 --feature-gates=\"SupportPodPidsLimit=true\""

# 重啟 kubelet 服務
$ systemctl restart kubelet

# 查看參數是否生效
$ ps faux | grep kubelet | grep pod-max-pids
root     104865 10.5  0.6 1731392 107368 ?      Ssl  11:56   0:30 /usr/bin/kubelet ... --pod-max-pids=10 --feature-gates=SupportPodPidsLimit=true

驗證 PodPidsLimit

通過配置 Kubelet 的 –pod-max-pids=1024 選項,限制了一個容器內允許的最大進程數為 1024 個。現在來測試下如果容器內不斷 fork 子進程,數目到達 1024 個時會觸發什么行為。

參考 Fork bomb 的內容,可以創建一個 pod,不斷 fork 子進程。

Fork bomb
# 創建普通的 nginx pod yaml
$ cat <<EOF > test-nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-nginx
spec:
  containers:
  - name: nginx
    image: nginx
EOF

# 創建到 Kubernetes 集群
$ kubectl apply -f test-nginx.yaml

# 進入 nginx 容器模擬 fork bomb 
$ kubectl exec -ti test-nginx bash
root@test-nginx:/# bash -c "fork() { fork | fork &  }; fork"
environment: fork: retry: Resource temporarily unavailable
environment: fork: retry: Resource temporarily unavailable
environment: fork: retry: Resource temporarily unavailable

通過進入一個 nginx 容器里面使用 bash 運行 fork bomb 命令,我們會發現當 fork 的子進程達到限制的上限數目后,會報retry: Resource temporarily unavailable的錯誤,這個時候再看下宿主機的 fork 進程數目。

# 通過在外部宿主機執行下面的命令,會發現 fork 的進程數目接近 1024 個
$ ps faux | grep fork | wc -l
1019

通過以上的實驗,發現能夠通過設置 Kubelet 的 –pod-max-pids 選項,限制容器類的進程數,避免容器進程數不斷上升最終耗盡宿主機資源,拖垮整個宿主機系統。

原理實現

通過之前描述的解決方法,已經能夠限制容器的進程數了。

現在從代碼的層面看下 Kubelet 如何設置 Cgroups pids 控制器。

Kubelet 代碼調用

首先來看下 Kubelet 代碼里面 –pod-max-pids 是怎么生效的,Kubernetes 的版本為 v1.15.9。

–pid-max-pids 選項是在 cmd/kubelet/app/options/options.go 里面的 AddKubeletConfigFlags 函數設置的,對應代碼如下。

// cmd/kubelet/app/options/options.go
func AddKubeletConfigFlags(mainfs *pflag.FlagSet, c *kubeletconfig.KubeletConfiguration) {
...
    // 這里定義了 '--pod-max-pids' 的選項
    // 對應參數的值通過命令行解析到 kubeletconfig.KubeletConfiguration.PodPidsLimit 里面
    fs.Int64Var(&c.PodPidsLimit, "pod-max-pids", c.PodPidsLimit, "Set the maximum number of processes per pod.  If -1, the kubelet defaults to the node allocatable pid capacity.")
...
}

PodPidsLimit配置參數解析完成后,kubelet 會在啟動的時候把值設置到 ContainerManager 里面,對應代碼在cmd/kubelet/app/server.go里面的run函數,注釋如下。

// cmd/kubelet/app/server.go
func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan struct{}) (err error) {
...
        kubeDeps.ContainerManager, err = cm.NewContainerManager(
            ...
            cm.NodeConfig{
                ...
                // 容器 runtime,默認使用 docker
                ContainerRuntime:      s.ContainerRuntime,
                // 使用 Cgroups 控制 pod 的服務質量
                CgroupsPerQOS:         s.CgroupsPerQOS,
                // 操作 Cgroups 的驅動,有 cgroupfs 和 systemd 兩種
                // 我們默認配置使用 systemd 來控制 Cgroups
                CgroupDriver:          s.CgroupDriver,
                ...
                // 這里就是 PodPidsLimit 的設置了,通過剛才 options 的解析
                // 賦值到了 ContainerManager.ExperimentalPodPidsLimit 屬性
                ExperimentalPodPidsLimit:              s.PodPidsLimit,
                // 限制容器 CPU 使用率的參數
                EnforceCPULimits:                      s.CPUCFSQuota,
                CPUCFSQuotaPeriod:                     s.CPUCFSQuotaPeriod.Duration,
            },
            ...
            ...)
...
}

初始化 ContainerManager 后,會在 pkg/kubelet/cm/container_manager_linux.go 里面調用 NewPodContainerManager 創建 PodContainerManager,代碼如下。

// pkg/kubelet/cm/container_manager_linux.go
...
func (cm *containerManagerImpl) NewPodContainerManager() PodContainerManager {
    // 默認情況下已經打開了 CgroupsPerQOS 的選項
    if cm.NodeConfig.CgroupsPerQOS {
        // 這里返回 PodContainerManager 接口的實現
        return &podContainerManagerImpl{
            qosContainersInfo: cm.GetQOSContainersInfo(),
            subsystems:        cm.subsystems,
            cgroupManager:     cm.cgroupManager,
            // 這里設置 podPidsLimit
            podPidsLimit:      cm.ExperimentalPodPidsLimit,
            enforceCPULimits:  cm.EnforceCPULimits,
            cpuCFSQuotaPeriod: uint64(cm.CPUCFSQuotaPeriod / time.Microsecond),
        }
    }
    return &podContainerManagerNoop{
        cgroupRoot: cm.cgroupRoot,
    }
}
...

從之前的代碼能發現 PodContainerManager 是一個接口,對應的實現在pkg/kubelet/cm/pod_container_manager_linux.go里面,與 Cgroup 相關的函數則是podContainerManagerImpl.EnsureExists函數。

// pkg/kubelet/cm/pod_container_manager_linux.go
...
// podContainerManagerImpl 就是實現 PodContainerManager 接口的結構體
// EnsureExists 會根據 api 里面 Pod 的定義,在當前系統創建對應容器的 cgroup 配置
func (m *podContainerManagerImpl) EnsureExists(pod *v1.Pod) error {
    // podContainerName 也會作為 cgroup name,根據 pod 的 QOS 級別和 UUID 生成
    // 查看容器是否存在
    alreadyExists := m.Exists(pod)                                                                                  
    if !alreadyExists {
        // 創建 pod 對應容器的 cgroup
        containerConfig := &CgroupConfig{
            Name:               podContainerName,
            ResourceParameters: ResourceConfigForPod(pod, m.enforceCPULimits, m.cpuCFSQuotaPeriod),
        }
        // 如果啟用了 SupportPodPidsLimit feature-gate ,并且 podPidsLimit 大于 0
        if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit) && m.podPidsLimit > 0 {
            // 這里就會配置 PidsLimit
            containerConfig.ResourceParameters.PidsLimit = &m.podPidsLimit
        }
        // 調用 cgroupManager 根據 containerConfig 創建對應的 cgroup
        if err := m.cgroupManager.Create(containerConfig); err != nil {
            return fmt.Errorf("failed to create container for %v : %v", podContainerName, err)
        }
    }
    ...
    return nil
}
...

接下來看 cgroupManager.Create 函數的實現,對應代碼實現在pkg/kubelet/cm/cgroup_manager_linux.go里面的cgroupManagerImpl.Create。

...
func (m *cgroupManagerImpl) Create(cgroupConfig *CgroupConfig) error {
...
    resources := m.toResources(cgroupConfig.ResourceParameters)
    libcontainerCgroupConfig := &libcontainerconfigs.Cgroup{
        Resources: resources,
    }
    // libcontainer consumes a different field and expects a different syntax
    // depending on the cgroup driver in use, so we need this conditional here.
    if m.adapter.cgroupManagerType == libcontainerSystemd {
        // 我們使用 systemd 管理 cgroup ,所以這里會更新下 systemd 對應 cgroup 的配置
        updateSystemdCgroupInfo(libcontainerCgroupConfig, cgroupConfig.Name)
    } else {
        libcontainerCgroupConfig.Path = cgroupConfig.Name.ToCgroupfs()
    }

    if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit) && cgroupConfig.ResourceParameters != nil && cgroupConfig.ResourceParameters.PidsLimit != nil {
        // 設置 libcontainerCgroupConfig 里面的 PidsLimit
        // 這里 PidsLimit 就是一開始參數指定的 --pod-max-pids 的值
        libcontainerCgroupConfig.PidsLimit = *cgroupConfig.ResourceParameters.PidsLimit
    }

    // 這里根據 cgroup 的配置返回 libcontainercgroups.Manager 接口的實現
    // 這里的實現是 systemd 的實現
    manager, err := m.adapter.newManager(libcontainerCgroupConfig, nil)
    if err != nil {
        return err
    }

    // 調用 libcontainer 里面的 cgroups manager Apply 接口把 pod 的 cgroup 配置應用到系統
    // 在我們的環境中,這個 Apply 函數會由 libcontainer/cgroupfs/systemd.Manager 實現
    if err := manager.Apply(-1); err != nil {
        return err
    }
    ...

    return nil
}
...

再看下最后的Apply函數,該函數會調用到vendor/github.com/opencontainers/runc/libcontainer/cgroups/systemd/apply_systemd.go里面的 systemd 實現。

// vendor/github.com/opencontainers/runc/libcontainer/cgroups/systemd/apply_systemd.go
...
func (m *Manager) Apply(pid int) error {
    // 初始化 systemd cgroup 需要的一些變量
    var (
        c          = m.Cgroups
        // systemd unit name
        unitName   = getUnitName(c)
        slice      = "system.slice"
        // systemd unit 里面的配置屬性
        properties []systemdDbus.Property
    )
...
    // Always enable accounting, this gets us the same behaviour as the fs implementation,
    // plus the kernel has some problems with joining the memory cgroup at a later time.
    properties = append(properties,
        newProp("MemoryAccounting", true),
        newProp("CPUAccounting", true),
        newProp("BlockIOAccounting", true))
    ...
    if c.Resources.Memory != 0 {
        // 設置 cgroup memory limit
        properties = append(properties,
            newProp("MemoryLimit", uint64(c.Resources.Memory)))
    }

    if c.Resources.CpuShares != 0 {
        // 設置 cgroup cpu shares
        properties = append(properties,
            newProp("CPUShares", c.Resources.CpuShares))
    }
...
    if c.Resources.BlkioWeight != 0 {
        // 設置 cgroup block io weight
        properties = append(properties,
            newProp("BlockIOWeight", uint64(c.Resources.BlkioWeight)))
    }

    if c.Resources.PidsLimit > 0 {
        // 這里設置了本文關注的 PidsLimit 參數
        // 發現會對應 systemd 里面的 TasksAccounting 和 TasksMax 屬性
        properties = append(properties,
            newProp("TasksAccounting", true),
            newProp("TasksMax", uint64(c.Resources.PidsLimit)))
    }
...
    // 通過 systemdDbus 根據之前的 cgroup 設置創建對應的 unit
    if _, err := theConn.StartTransientUnit(unitName, "replace", properties, statusChan); err == nil {
        ...
    }

    // 最后加入 Cgroups
    if err := joinCgroups(c, pid); err != nil {
        return err
    }
...
}
...

Systemd Cgroup slice

通過對 Kubelet 調用 libcontainer,最后由 systemd 創建 pod 容器對應 cgroup unit 的代碼調用分析,在這里看下對應 pod 的 systemd unit 配置。

從之前代碼看,最終生成的 systemd unit 和 cgroup 和 pod 的 uid 和 qosClass 有關系,所以先通過以下的命令拿到 pod 的 uid 和 qosClass。

$ kubectl get pods test-nginx -o yaml | grep -E 'uid|qos'
  uid: 2ac1e32c-d8d6-4533-8eab-d04d60465065
  qosClass: BestEffort

然后找到對應的 systemd unit .slice 文件。

# uid 取前 8 位,qosClass 小寫
# 找到對應的 kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice
$ systemctl | grep 2ac1e32c | grep besteffort
kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice      loaded active active    libcontainer container kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice

# 查看對應 slice 的配置
$ systemctl status kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice
● kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice - libcontainer container kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice
   Loaded: loaded (/run/systemd/system/kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice; static; vendor preset: disabled)
  Drop-In: /run/systemd/system/kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice.d
           └─50-BlockIOAccounting.conf, 50-CPUAccounting.conf, 50-CPUShares.conf, 50-DefaultDependencies.conf, 50-Delegate.conf, 50-Description.conf, 50-MemoryAccounting.conf, 50-TasksAccounting.conf, 50-TasksMax.conf, 50-Wants-kubepods-besteffort\x2eslice.conf
   Active: active since Fri 2021-06-25 16:21:25 CST; 7min ago
    Tasks: 6 (limit: 1024)
   Memory: 6.8M
   CGroup: /kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice
           ├─docker-2d151786c9985db74632c09412207fa99755473fde93d09920604e097f25a2b7.scope
           │ ├─32662 nginx: master process nginx -g daemon off;
           │ ├─32703 nginx: worker process
           │ ├─32704 nginx: worker process
           │ ├─32705 nginx: worker process
           │ └─32706 nginx: worker process
           └─docker-966047566d9e90d9ef64126b605101c174d750ec0cde3d3a83c5b313c7af9a21.scope
             └─32544 /pause

Jun 25 16:21:25 centos7-oc-dev systemd[1]: Created slice libcontainer container kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice.

# 通過 systemctl status 能發現 50-TasksMax.conf 的文件
# 從之前的代碼分析,發現 PodPidsLimit 會對應到 systemd 的 TasksMax 屬性
# 現在在看下這個文件的內容
$ cat /run/systemd/system/kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice.d/50-TasksMax.conf
[Slice]
TasksMax=1024
# TasksMax 設置為了 1024 ,限制了這個進程最大子進程(Task)數量

Cgroup FS

查看之前的 vendor/github.com/opencontainers/runc/libcontainer/cgroups/systemd/apply_systemd.go 代碼,發現在創建完 pod 容器對應的 systemd cgroup slice 后,還會調用一次 joinCgroups 這個函數。這個函數會使用 Cgroup FS 原生的方法,在 /sys/fs/cgroup 里面創建對應 pod 容器的 group 。

所以再看下 Cgroup FS 里面 pod 設置 pid limit 的配置。

# 找到 Cgroup FS pids 控制器的掛載點
$ cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)

# 看下 /sys/fs/cgroup/pids 目錄下的文件
$ ls -alh /sys/fs/cgroup/pids
...
# 發現有一個由 Kubelet 創建的 kubepods.slice
drwxr-xr-x   4 root root   0 Jun 25 04:49 kubepods.slice
...

# 再通過查看 /sys/fs/cgroup/pids/kubepods.slice 目錄
# 會發現 kubepods-besteffort.slice 和 kubepods-burstable.slice 兩個目錄
# 分別對應 pod 容器的 QOS 級別
$ ls -alh /sys/fs/cgroup/pids/kubepods.slice
...
drwxr-xr-x 42 root root 0 Jun 25 16:21 kubepods-besteffort.slice
drwxr-xr-x  8 root root 0 Jun 25 04:49 kubepods-burstable.slice
...

# 結合剛才的代碼片段,也可以想到原生 Cgroup FS 的目錄和 systemd 的應該是差不多的層級
# 現在直接用 find 命令查看 pids 控制器下面的 cgroup 設置
$ find /sys/fs/cgroup/pids/kubepods.slice -type f | grep pod2ac1e32c
...
# 能發現 pids.current 和 pids.max 兩個 cgroup 的配置
# pids.current 表示當前 pod 里面的進程(Task)數量
/sys/fs/cgroup/pids/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice/pids.current
# pids.max 則表示 pod 里面能運行的進程(Task)上限
/sys/fs/cgroup/pids/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice/pids.max
...

# 查看 pod pids.max 設置,結果為 1024
$ cat /sys/fs/cgroup/pids/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod2ac1e32c_d8d6_4533_8eab_d04d60465065.slice/pids.max
1024 

另外這篇內核文檔 Process Number Controller 對 cgroup pids 控制器的使用進行了介紹,可以了解下。

Process Number Controller
目錄
其它

技術支持

技術支持

掃碼加入技術支持微信群

掃碼加入技術支持微信群


公眾號

官方公眾號

掃碼關注獲取最新動態