容器与宿主机共享/私有内容

dmesg 和宿主机共享
内核 和宿主机共享
rocm-smi 和宿主机共享

Run multiple processes in a container?

在 Docker 容器中虽然理论上可以运行多个应用进程,但实际上这并不符合 Docker 设计的最佳实践。Docker 容器的核心理念是每个容器应该只包含一个主要的应用服务进程,这一理念被称为“单进程容器”模型。

一个容器只运行一个主进程。多个进程都部署在一个容器中,弊端很多。

可以通过这种方式来运行多个进程:docker/md/在一个docker容器中运行多个程序进程的Dockerfile编写.md at main · cucker0/docker

ps In container

看到的是限制之后的进程,比如 ps -A

   PID TTY          TIME CMD
     1 ?        00:02:58 light
   599 ?        00:00:00 sshd
   600 ?        00:00:00 crond
 17463 ?        00:00:01 controller.sh
 18344 ?        00:04:17 ld-linux-x86-64
 19269 ?        16:48:26 java
 90173 ?        00:00:00 syslog-ng
 90179 ?        00:00:03 systemd-journal
148290 pts/0    00:00:00 bash
148368 ?        00:00:00 sleep
148640 pts/0    00:00:00 ps
...

容器 dmesg

因为容器和 host 是共享内核的,所以容器上执行 dmesg 是可以看到 host 上的 dmesg 的,两者是一致的。

Best practice to host containers?

是应该跑在 VM 里面还是跑在 Baremetal 的机器上。

跑在 VM 里的好处有:

  • 因为 docker 对操作系统内核版本是有要求的,如果跑在 baremetal 上,只有一种 kernel 所以没有办法满足所有 docker 的需求,我们可以跑不同 VM 里面跑不同内核来满足这一个需求,kata 就是采用这种实现方式。
  • 有一些 App 本来是跑在 baremetal 上的,调度也是基于 baremetal 来调度的,如果现在直接容器化需要对原来的调度框架有很大的改动,所以不如直接跑在虚拟机(runD, kata)里,这样调度系统也不需要改。

linux - What are the benefits of running a docker container inside a VM vs running docker containers on bare metal? - Unix & Linux Stack Exchange

Container engine / Container runtime

这些名字都来自于 OCI(Open Container Initiative) 的定义。

所有的容器其实都是 kernel 里的 cgroup 而已,通过 cgroup 加了一些 CPU 计算资源/内存资源/存储上的隔离。

Container engines, runtimes and orchestrators: an overview - Stefano Alberto Russo 这里解释挺清楚的:

  • A container engine is a piece of software that accepts user requests, including command line options, pulls images, and from the end user's perspective runs the container.
  • A container runtime is a software which is in charge of managing the container lifecycle: configuring its environment, running it, stopping it, and so on. You can think about them as what's inside the engine (i.e valves and pistons). Runtimes can be further sub-divided in two types: high level and low level.
    • high level container runtimes, or container runtime interfaces (CRI) for Kubernetes.
    • low level container runtimes, or CRI runtimes for Kubernetes.
    • Also note that some engines can behave as runtimes and can be thus used from within other engines, or orchestrators.
  • Lastly, a container orchestrator is a software in charge of managing set of containers across different computing resources, handling network and storage configurations which are under the hood delegated to the container runtimes (or in some nearly legacy cases, engines).

While a container runtime is responsible for running containers, a container engine is a broader system that manages even more of the life cycle of containers.

运行时也分为低层运行时和高层运行时:

  • 低层运行时主要负责与宿主机操作系统打交道,根据指定的容器镜像在宿主机上运行容器进程,并对容器的整个生命周期进行管理,也就是负责设置容器 Namespace、Cgroups 等基础操作的组件。常见的低层运行时有:
    • runc:传统的运行时,基于 Linux Namespace 和 Cgroups 技术实现,负责实际的容器创建、隔离和执行操作。代表实现 Docker、Containerd。
    • runv:基于虚拟机管理程序的运行时,通过虚拟化 guest kernel,将容器和主机隔离开来,使得其边界更加清晰, 代表实现是 Kata Container 和 Firecracker。 (注意:虽然 kata container 更安全,但是其用在生产里的还是不多)
    • runsc:runc + safety,通过拦截应用程序的所有系统调用,提供安全隔离的轻量级容器运行时沙箱,代表实现是谷歌的 gVisor。
  • 高层运行时主要负责容器的生命周期管理、镜像管理、网络管理和存储管理等工作,为容器的运行做准备。主流的高层运行时包括 Containerd、CRI-O。高层运行时与低层运行时各司其职,容器运行时一般先由高层运行时将容器镜像下载下来,并解压转换为容器运行需要的操作系统文件,再由低层运行时启动和管理容器。
    • containerd 是一个面向容器生命周期管理的高层容器运行时。它提供了容器的生命周期管理、镜像管理、存储管理和网络管理等功能。containerd 可以与容器编排系统(如 Kubernetes)紧密集成,为容器的创建、启动、停止和销毁等操作提供 API 接口。
    • CRI-O 是一个轻量级容器运行时,专注于在 Kubernetes 环境中运行容器,符合 Kubernetes CRI 规范,使用 runc 作为底层执行引擎。

runtime 有:

  • docker
  • runC
  • runD

engine 有:

  • containerd
  • Docker Engine
  • Pouch

Pouch / Containerd

这两者好像是介于 k8s 这一层和 runC / runD 这一层之间的。

特权容器

特权容器是一种具有主机的所有功能的容器,它解除了常规容器的所有限制。注意并不是虚拟机。

实际上,这意味着特权容器可以执行几乎所有可以直接在主机上执行的操作,特权容器就是提权的进程,See Linux capabilities^。

Docker

Image、Container(静态容器)和 Instance(运行时容器) 的概念需要梳理:Image 和 Container 都是文件,其中 Container 是通过 image 加上一些 docker 参数创建的。Container 在 run 的时候会成为一个 Instance,Instance 运行时写在容器层(读写层)的数据在 Instance 关闭后还是存在的,只有在删除静态容器的时候才会消失:当我们对 docker 容器执行 restart 后,其实容器中原本读写层里对临时数据还在。只有我们删除了这个容器,重新创建的容器是基于镜像的只读层然后挂载上新的空的读写层,此时临时数据是不在的。

【博客618】docker容器重启后读写层数据并不丢失的原理_docker mysql容器重启 数据还在吗-CSDN博客

Docker 与跨指令集架构 / 跨操作系统

Docker 镜像能跨平台运行;只要系统架构一样,是可以使用相同的镜像的,x86 的镜像只能在 x86 系统使用,arm 的镜像只能在 arm 系统使用。

Docker 利用宿主机内核提供了轻量级、可移植、高效的容器解决方案,但这也要求宿主机和容器之间保持内核层面的一致性和兼容性。Docker 确保容器内的应用与宿主机的内核版本和配置尽可能兼容。

如果宿主机运行的是 Linux 内核,那么它通常无法直接运行基于其他操作系统的容器,例如 Windows 容器。为了解决这个问题,Docker 使用了各种技巧和折衷方案,比如在非 Linux 系统上通过虚拟机来提供 Linux 内核环境,使 Docker 容器能够运行。

docker 镜像内核 和宿主机内核是什么关系 – PingCode

Docker 常用命令

# 基于 DOCKERFILE 来创建一个容器
docker build -t my-training-image:latest .
# 基于一个 image 地址创建一个容器
docker run <image name>
# 登入 docker shell
docker exec -it <mycontainer> sh
# 检查容器 log(失败 log)
docker logs <container_name>
# 查看所有容器状态
docker container ls --all
# 重新启动一个创建好的容器
docker start <container_name>

Docker 如何重新调整容器的启动参数?

只能通过 docker commit 来重新提交现有镜像保存更改,然后重新 run 一个参数了:

docker commit <container_name_or_id> new_image_name:tag

Docker 可以不指定任何一个 CMD / ENTRYPOINT 来运行一个进程吗?

基础镜像已定义默认命令:大多数官方镜像(如 ubuntu、nginx、alpine)已设置默认的 CMD 或 ENTRYPOINT 比如进入交互式终端。

或者直接指定一个永远不会退出的命令:

CMD ["sh", "-c", "tail -f /dev/null"]

如果要运行:

docker start

Docker kernel config

CONFIG_BPF, CONFIG_BPF_SYSCALL, CONFIG_CGROUP_BPF

Check if the kernel config is suitable for running docker:

Verify your Linux Kernel for Container Compatibility · Docker Pirates ARMed with explosive stuff

wget https://github.com/moby/moby/raw/master/contrib/check-config.sh
chmod +x check-config.sh
./check-config.sh

ADD And COPY in Dockerfile

Always use the COPY instruction instead of the ADD instruction when adding files into your Docker image.


ENTRYPOINT And CMD in Dockerfile

在写 Dockerfile 时, ENTRYPOINT 或者 CMD 命令会自动覆盖之前的 ENTRYPOINT 或者 CMD 命令。

在 docker 镜像运行时, 用户也可以在命令指定具体命令, 覆盖在 Dockerfile 里的命令。

Entrypoint 是指定容器启动时要执行的默认命令,它在运行容器时不能被覆盖。 而 Cmd 是指定容器启动时要执行的默认命令参数,它可以被覆盖

通常情况下,Entrypoint 用于指定容器启动时要运行的应用程序,而 Cmd 用于指定应用程序的默认参数。

Docker 的替代产品

   
Orbstack 和 docker 一样,需要商用。
Lima 只有一个虚拟机。
Colima 完全开源,Lima 的虚拟机,加上 docker 的容器运行时。

Kata Containers

和 Kernel Space VMM 和 Userspace VMM 是正交的,都可以用。QEMU/KVM 都不是必须的。

documentation/design/virtualization.md at master · kata-containers/documentation

  • ACRN hypervisor
  • Cloud Hypervisor/KVM
  • Firecracker/KVM
  • QEMU/KVM

可以用 Rust 写的 Dragonball kata-containers/src/dragonball at main · kata-containers/kata-containers

Kata 现在版本是 3.0,只支持默认的 Dragonball 做为 hypervisor. Dragonball Sandbox aims to provide a simple solution for the Kata Containers community. It is integrated into Kata 3.0 runtime as a built-in VMM and gives users an out-of-the-box Kata Containers experience without complex environment setup and configuration process.

KVM PV feature in Kata containers

应该默认都是打开的:请看 leaf_0x4000_0001

Transform the CPUID before exposing to the guest (KVM_SET_CPUID2)

impl CpuidTransformer for IntelCpuidTransformer {
    fn process_cpuid(&self, cpuid: &mut CpuId, vm_spec: &VmSpec) -> Result<(), Error> {
        common::use_host_cpuid_function(cpuid, cpu_leaf::leaf_0x0::LEAF_NUM, false)?;
        self.process_entries(cpuid, vm_spec)
    }

    fn entry_transformer_fn(&self, entry: &mut CpuIdEntry) -> Option<EntryTransformerFn> {
        use cpu_leaf::*;

        match entry.function {
            leaf_0x1::LEAF_NUM => Some(common::update_feature_info_entry),
            leaf_0x4::LEAF_NUM => Some(intel::update_deterministic_cache_entry),
            leaf_0x6::LEAF_NUM => Some(intel::update_power_management_entry),
            leaf_0xa::LEAF_NUM => Some(intel::update_perf_mon_entry),
            leaf_0xb::LEAF_NUM => Some(common::update_extended_topology_entry),
            leaf_0x1f::LEAF_NUM => Some(common::update_extended_topology_v2_entry),
            0x8000_0002..=0x8000_0004 => Some(common::update_brand_string_entry),
            _ => None,
        }
    }
}

impl Vcpu {
    //...
    fn set_cpuid(&mut self, vcpu_config: &VcpuConfig) -> Result<()> {
        //...
    }
    //...
}

容器存储

Container 的容器层在每次 run 的时候会被持久化吗。

UnionFS

Union 在英语里是并集的意思,正如其名字,UnionFS 相当于对各层的文件系统进行了一个并集。

Memcg / Memory Cgroup

What will happen when exceed memory cgroup limit?

当 memory cgroup 用超之后,是会发生 OOM 并 kill 掉进程还是会 swap 这个进程的内存出去?

My model of memory limits on cgroups was always “if you use more than X memory, you will get killed right away”. It turns out that that assumptions was wrong! If you use more than X memory, you can still use swap!

And apparently some kernels also support setting separate swap limits. So you could set your memory limit to X and your swap limit to 0, which would give you more predictable behavior. Swapping is weird and confusing.

怎么样去设置 swap limit 呢?

kernel.org/doc/Documentation/cgroup-v1/memory.txt 这里提到:memory+swap usage can be accounted and limited.

memory.limit_in_bytes		 # set/show limit of memory usage
memory.memsw.limit_in_bytes	 # set/show limit of memory+Swap usage

上面两个可以控制 memory limit 和 swap limit 分别是多少。

Swapping, memory limits, and cgroups

Cgroup

这篇文章讲的不错,很适合入门和看一些 high level 的东西:Linux资源管理之cgroups简介 - 美团技术团队

术语 cgroup 在不同的上下文中代表不同的意思,可以指整个 Linux 的 cgroup 技术,也可以指一个具体进程组。

一个 cgroup 在不同的子系统之间是相同的吗?比如说我这个 cgroup 有 5 个进程,希望他们在 CPU 使用上共享一个 quota,但是如果切换到内存子系统,那么可能我们希望其中两个进程和这个 cgroup 之外的进程 share memory,所以就不存在一个 cgroup 能包含这 5 个进程了,cgroup 的实现和使用支持这种方式吗,还是说不同子系统的 cgroup 其实也是不一样的?实验了一下,改一个子系统下的 cgroup,比如说在 cpu/kubepods/tasks 下加入一个 pid,memory/kubepods/tasks 下面是没有的,所以不同的树下应该是不同的 cgroup?

  • cgroup v2 下的 cgroup 应该是相同的。因为其配置结构就是 cgroup 文件下配子系统;
  • cgroup v1 下是允许不同树下有不同 cgroup 的。这是 cgroup v1 相比于 v2 更加灵活的地方

也就是说,cgroup v1 下的 cgroup 概念和 cgroup v2 是不一样的,前者是一个更小的概念,表示一个层级下面的 cgroup,而后者可以包含多个层级。

cgroup 是 Linux 下的一种将进程按组进行管理的机制,在用户层看来,cgroup 技术就是把系统中的所有进程组织成许多独立的层级树,每棵树都包含系统的所有进程,树的每个节点是一个进程组 cgroup,而每颗树又和一个或者多个 subsystem(也可以没有)关联,树的作用是将进程分组,而 subsystem 的作用就是对这些组进行操作。为了更好地理解这个概念,我们可以用一个类比来帮助理解。

  • 一个 subsystem 只能同时属于一个层级树 (hierarchy)。例如 memory 子系统不能同时属于两个层级树,也就是说 subsystem 到层级树之间是多对一的映射关系。
  • 在每个树中,一个 task 只能属于其中一个 cgroup,当添加 task 到新的 cgroup 时,会默认从旧的 cgroup 中移除。但是一个 task 可以同时存在于不同的树中。

subsystem 和资源是对应的,每个子系统都负责管理系统的某个特定资源,比如用于内存管理的 memory 子系统,用于 CPU 时间分配的 CPU 子系统等。

Linux Cgroup 入门教程:基本概念 · 云原生实验室

核心数据结构类图:

taskscgroup.proc 的区别

tasks 文件:

  • 用途: 列出当前 cgroup 中所有的进程,包括其所有的线程。
  • 使用场景: 如果你需要看到 cgroup 中的所有任务,包括每个进程的各个线程,那么应查看 tasks 文件。
  • 内容: 包含每个任务(包括主线程及其子线程)的 PID。

cgroup.procs 文件:

  • 用途: 列出当前 cgroup 中的所有进程和线程组的 leader 进程,但不包括单独的线程(只列出线程组的主进程)。
  • 使用场景: 如果你只需要看到进程级别的信息,而不需要查看每个线程的详细信息,那么应查看 cgroup.procs 文件。
  • 内容: 只包含每个进程的 PID(即线程组的 leader)。

tasks 更详细: 显示每个进程和它们的所有线程。 用于更细粒度的资源管理和监控。

Processes in container

一个容器一般来说有一个主进程,但是也有可能有一些辅助进程,比如 crond, light, sshd, ld-linux-x86-64, systemd-journal。这个在对应的 /sys/fs/cgroup/cpu/kubepods/pod…/<container hash>/cgroup.procs 能够看到对应的 pid。

一般来说如何定位哪一个是容器的主进程呢?进到容器里面直接 ps -ATIME 这一列表示使用掉的 CPU 时间,最大的那个应该就是。

cpu.shares

  • nice value 是针对一个进程的权重设置,a nice value applies to a task, that is a process or threada nice value applies to a task, that is a process or thread;
  • shares 是针对一个进程组的权重设置,a "CPU shares" value applies to a task group。

Share 值和内核里面的 Weight 值是同一个单位。

static struct cftype cpu_legacy_files[] = {
    //...
	{
		.name = "shares",
		.read_u64 = cpu_shares_read_u64,
		.write_u64 = cpu_shares_write_u64,
	},
    //...
}
// 找到对应的 task_group
sched_group_set_shares(css_tg(css), scale_load(shareval));
    sched_group_set_shares
        __sched_group_set_shares
            // 因为已经 scale_load() 了,所以和 load 是一个量纲了
            tg->shares = shares;
            for_each_possible_cpu(i)
                for_each_sched_entity(se)
                    update_cfs_group
                        shares = READ_ONCE(gcfs_rq->tg->shares);
                        shares = calc_group_shares(gcfs_rq);
                        // 会把这个 task_group 上的 share 值设置到这个
                        // task_group 对应的每一个 cfs_rq 对应的每一个 schedule entity
                        // 的 weight 值上。
                    	reweight_entity(cfs_rq_of(se), se, shares);

cgroup_idle

cpuset,cpu,cpuacct / CPU isolation

  • cpuset: allows you to assign individual CPUs (or groups of CPUs) and memory nodes to cgroups. This is useful for controlling which CPUs a set of processes can execute on and from which memory nodes they can allocate memory, effectively confining the processes to specific hardware resources. It's often used to improve performance for applications that benefit from being restricted to specific CPUs, especially in a multi-core system, enhancing cache locality. 指定的 cgroup 只能跑在 cpuset 分组里的 CPU 区间,但是其他 cgroup 仍然可以跑在这些 CPU 上。
  • cpu: used to share CPU time between cgroups. You can set the CPU shares for each cgroup, influencing how much CPU time a group's processes receive relative to others when the system is under full load. Additionally, you can set CPU limits and quotas to enforce absolute limits on CPU usage over defined periods, which is important for ensuring that no single group can monopolize the CPU. 这个其实就是用来限制 cgroup CPU 使用率的。
  • cpuacct: provides accounting for CPU usage in cgroups. It tracks CPU time consumed by the group's processes, helping monitor and analyze application behavior, load balancing, and performance tuning. With cpuacct, you can obtain detailed statistics about CPU usage, which can aid in making informed decisions about resource allocation.

cgroup 的 CPU set 和 task set 其实都是一样的。

Cgroup in sysfs / cgroupfs

表现在操作界面就是 sysfs 里可以进行配置。

Mount on /sys/fs/cgroup/:

CPU 子系统常见配置:

  • /sys/fs/cgroup/cpuset,cpu,cpuacct/cpu.shares: 表示一个 CPU 的资源被分成了多少份。cpu.shares 用来设置 CPU 的相对值,并且是针对所有的 CPU,默认值是 1024 等同于一个 cpu 核心。 CPU Shares 将每个核心划分为 1024 个片,并保证每个进程将按比例获得这些片的份额。如果有 1024 个片 (即 1 核),并且两个进程设置 cpu.shares 均为 1024,那么这两个进程中每个进程将获得大约一半的 cpu 可用时间。 所以这个 cpu.shares 其实也是一个保证的上限而不是下限。k8s 的一个 pod 对于 CPU 的 request 应该反映到内核 cgroup 上就是 cpu.shares.

简单的使用:控制组简介 · Linux 内核揭秘@Js中文网-免费编程书籍

Namespace in Linux

Namespaces provide isolation of system resources, and cgroups allow for fine‑grained control and enforcement of limits for those resources.

  • Cgroups = limits how much you can use;
  • namespaces = limits what you can see (and therefore use).

namespace 和 cgroup 之间并不是前辈和后继者的关系,两者是可以同时用的。

Namespaces are a feature of the Linux kernel that partition kernel resources such that one set of processes sees one set of resources, while another set of processes sees a different set of resources.

struct task_group

// Task group related information
struct task_group {
	struct cgroup_subsys_state css;

#ifdef CONFIG_FAIR_GROUP_SCHED
	/* schedulable entities of this group on each CPU */
	struct sched_entity	**se;
	/* runqueue "owned" by this group on each CPU */
	struct cfs_rq		**cfs_rq;
	unsigned long		shares;

	/* A positive value indicates that this is a SCHED_IDLE group. */
	int			idle;

#ifdef	CONFIG_SMP
	/*
	 * load_avg can be heavily contended at clock tick time, so put
	 * it in its own cacheline separated from the fields above which
	 * will also be accessed at each tick.
	 */
	atomic_long_t		load_avg ____cacheline_aligned;
#endif
#endif

#ifdef CONFIG_RT_GROUP_SCHED
	struct sched_rt_entity	**rt_se;
	struct rt_rq		**rt_rq;

	struct rt_bandwidth	rt_bandwidth;
#endif

	struct rcu_head		rcu;
	struct list_head	list;

	struct task_group	*parent;
	struct list_head	siblings;
	struct list_head	children;

#ifdef CONFIG_SCHED_AUTOGROUP
	struct autogroup	*autogroup;
#endif

	struct cfs_bandwidth	cfs_bandwidth;

#ifdef CONFIG_UCLAMP_TASK_GROUP
	/* The two decimal precision [%] value requested from user-space */
	unsigned int		uclamp_pct[UCLAMP_CNT];
	/* Clamp values requested for a task group */
	struct uclamp_se	uclamp_req[UCLAMP_CNT];
	/* Effective clamp values used for a task group */
	struct uclamp_se	uclamp[UCLAMP_CNT];
#endif
};

Resource control in cgroup

配置的地方在:sys/fs/cgroup/cpu/cpu.cfs_period_us,如果要具体到某一个 cgroup,只能

  • cpu.cfs_period_us: If tasks in a cgroup should be able to access a single CPU for 0.2 seconds out of every 1 second, set cpu.cfs_quota_us to 200000 and cpu.cfs_period_us to 1000000. The upper limit of the cpu.cfs_quota_us parameter is 1 second and the lower limit is 1000 microseconds.
  • cpu.cfs_quota_us: As soon as tasks in a cgroup use up all the time specified by the quota, they are throttled for the remainder of the time specified by the period. Note that the quota and period parameters operate on a CPU basis. To allow a process to fully utilize two CPUs, for example, set cpu.cfs_quota_us to 200000 and cpu.cfs_period_us to 100000.

如何设置某一个 cgroup 对应的值呢?/sys/fs/cgroup/cpuset,cpu,cpuacct/<container>/cpu.cfs_period_us

struct task_group

Difference between cgroup and namespace in Linux

cgroup 和 namespace 类似,也是将进程进行分组,但它的目的和 namespace 不一样,namespace 是为了隔离进程组之间的资源,而 cgroup 是为了对一组进程进行统一的资源监控和限制。

In short:

  • Cgroups = limits how much you can use;
  • namespaces = limits what you can see (and therefore use)

linux - difference between cgroups and namespaces - Stack Overflow

Cgroup 常用命令 / cgcreate / cgexec / cgget / cgset

cgclassify -- cgclassify命令是用来将运行的任务移动到一个或者多个cgroup。
cgclear -- cgclear 命令是用来删除层级中的所有cgroup。
cgconfig.conf -- 在cgconfig.conf文件中定义cgroup。
cgconfigparser -- cgconfigparser命令解析cgconfig.conf文件和并挂载层级。
cgcreate -- cgcreate在层级中创建新cgroup。
cgdelete -- cgdelete命令删除指定的cgroup。
cgexec -- cgexec命令在指定的cgroup中运行任务。
cgget -- cgget命令显示cgroup参数。
cgred.conf -- cgred.conf是cgred服务的配置文件。
cgrules.conf -- cgrules.conf 包含用来决定何时任务术语某些 cgroup的规则。
cgrulesengd -- cgrulesengd 在 cgroup 中发布任务。
cgset -- cgset 命令为 cgroup 设定参数。
lscgroup -- lscgroup 命令列出层级中的 cgroup。
lssubsys -- lssubsys 命令列出包含指定子系统的层级

举个例子:

# 创建 CPU cgroup
cgcreate -g cpu:/freeze_test

# 查看刚刚创建的 cgroup
ls /sys/fs/cgroup/cpu/freeze_test

# 查看当前 cgroup 设置
cgget -g cpu:freeze_test

# 限制 CPU 使用率 10%
cgset -r cpu.cfs_period_us=100000 freeze_test
cgset -r cpu.cfs_quota_us=10000 freeze_test

# 使用设置的 CPU cgroup 来运行 python
cgexec -g cpu:freeze_test python3

# 在解析器中执行:
while True: a=1+1

lssubsys / Linux cgroup 子系统 / Linux cgroup subsystem

这个是 cgroup 专有的概念。 A subsystem is a kernel component that modifies the behavior of the processes in a cgroup. Various subsystems have been implemented, making it possible to do things such as limiting the amount of CPU time and memory available to a cgroup, accounting for the CPU time used by a cgroup, and freezing and resuming execution of the processes in a cgroup.

Subsystems are sometimes also known as resource controllers (or simply, controllers).

$lssubsys
cpuset,cpu,cpuacct
blkio
memory
devices
freezer
net_cls,net_prio
perf_event
hugetlb
pids
rdma

每个 cgroup 子系统是否被支持均与相关配置选项有关。例如,cpuset 子系统应该通过 CONFIG_CPUSETS 内核配置选项启用,io 子系统通过 CONFIG_BLK_CGROUP 内核配置选项等。所有这些内核配置选项都可以在 General setup → Control Group support 菜单。

看起来子系统和有哪些 cgroups 是正交的概念。

  • cpu 子系统 cpu/kubepods/ 控制每一个 pod 的限额;
  • cpuset 子系统 cpuset/kubepods 控制每一个 pod 设置的 CPU 区间;
  • cpuacct 子系统 cpuacct/kubepods 控制每一个 pod 对应的统计信息。

可以进入到每一个的目录下面 ls

  • /sys/fs/cgroup/cpu/kubepods/besteffort
  • /sys/fs/cgroup/cpuset/kubepods/besteffort
  • /sys/fs/cgroup/cpuacct/kubepods/besteffort

会发现 pods 的 id 都是一样的。

Linux cgroup subsystem controller 联合挂载

linux 系统通常已经将多个 controller 挂载在 /sys/fs/cgroup 目录中了,下面的例子用另一个目录演示。

将多个 controller 挂载到同一个目录,如下:

mkdir -p /tmp/cgroup/cpu,cpuacct
# 同时挂载多个子系统
sudo mount -t cgroup -o cpu,cpuacct none /tmp/cgroup/cpu,cpuacct

# 可以一次挂载所有的controller
mount -t cgroup -o all cgroup /tmp/cgroup

有时候 ls /sys/fs/cgroup 可以看到三个子系统并不是分开的,比如可能会看到 cpuset,cpu,cpuacct 目录,这是因为三个子系统联合挂载了,可配置项还是不变的,比如说在这个目录下面还是可以看到 cpu., cpuset., cpuacct. 等等配置项。

联合挂载的一个好处就是直接指定一个挂载的目录就可以了,当然每一个子系统也是可以分开挂载的。

Cpuacct 子系统

CPU accounting,显示 cgroup 中任务所使用的 CPU 资源,其中包括子群组任务。报告有两大类:

  • usage: 统计 cgroup 中进程使用 CPU 的时间,单位为纳秒
  • stat: 统计 cgroup 中进程使用 CPU 的时间,单位为 USER_HZ

(注意,统计的都是此 cgroup 中所有任务以及子孙层级中的所有任务的)

usage*:

  • cpuacct.usage : 使用 CPU 的总时间(纳秒),该文件时可以写入 0 值的,用来进行重置统计信息。
  • cpuacct.usage_user: 使用用户态 CPU 的总时间(纳秒)。
  • cpuacct.usage_sys: 使用内核态 CPU 的总时间(纳秒)。
  • cpuacct.usage_percpu: 在每个 CPU 使用 CPU 的时间(纳秒)。
  • cpuacct.usage_percpu_user 在每个 CPU 上使用用户态 CPU 的时间(纳秒)。
  • cpuacct.usage_percpu_sys:在每个 CPU 上使用内核态 CPU 的时间(纳秒)。
  • cpuacct.usage_all:详细输出文件 cpuacct.usage_percpu_usercpuacct.usage_percpu_sys 的内容。

stat*

  • cpuacct.stat:使用的 user 和 sys CPU 时间,方式如下:
    • user: user 中任务使用的 CPU 时间
    • system: sys 中任务使用的 CPU 时间
    • 其单位为 USER_HZ

阿里龙蜥操作系统专有:

  • cpuacct.sched_cfs_statistics:CFS 相关数据统计,例如运行时间,等待同级/非同级 cgroup 的时间等。
  • cpuacct.wait_latency:进程在队列中等待的延迟分布。

除此之外也有 cpu.stat 有便于统计的信息:

nr_periods 0
nr_throttled 0
throttled_time 0
wait_sum 6250967972385
current_bw 18446744073709551615
nr_burst 0
burst_time 0

Cgroup之cpuacct子系统 - Notes about linux and my work

cpuacct.proc_stat: 包含与 proc 文件系统中 /proc/stat 类似的 CPU 使用统计信息,单位是 ticks,

throttled_time

Cgroup 中的进程被限制使用 CPU 的总用时。其实就是 throttle_cfs_rq() 函数到 unthrottle_cfs_rq() 函数之间的时间。

throttle_cfs_rq
    cfs_rq->throttled = 1;
    // 记录时间
	cfs_rq->throttled_clock = rq_clock(rq);

unthrottle_cfs_rq
    // 当前时间减去当时记录的时间
    cfs_b->throttled_time += rq_clock(rq) - cfs_rq->throttled_clock;

struct cgroup Kernel

表示一个 cgroup 节点,可以被不同的层级树中的对应节点指向,可以看函数 cgroup_css()

这是一个更加通用的结构体,和 cpu, mem 等等子系统无关,所以里面其实是不适合放 task_group 相关的 field 的,因为和调度的相关性太大了。

struct cgroup {
    // 这个表示挂载在 null subsystem 上的,会重新指回这个 cgroup,不重要,可能是为了代码实现考虑?
	struct cgroup_subsys_state self;
    // 这个 cgroup 在不同子系统下的控制方案
	struct cgroup_subsys_state __rcu *subsys[CGROUP_SUBSYS_COUNT];
}

struct css_set Kernel

A css_set is a structure holding pointers to a set of cgroup_subsys_state objects.

struct cgroup_subsys Kernel

表示一个子系统。

/*
 * Control Group subsystem type.
 * See Documentation/admin-guide/cgroup-v1/cgroups.rst for details
 */
struct cgroup_subsys {
	struct cgroup_subsys_state *(*css_alloc)(struct cgroup_subsys_state *parent_css);
    //...
	int id;
    // 名字,比如 memory, cpu 等等
	const char *name;
    //...
};

struct cgroup_subsys_state Css Kernel

对应一个 cgroup,对应一个 subsytem,表示一个 subsystem 对应的某一个 cgroup,简称一个 css。

表示一个 cgroup 在某一个/几个子系统上的具像化。

所有的此结构体组成了一个树形的结构,应该就是对应层级树。

struct cgroup_subsys_state {
	/* PI: the cgroup that this css is attached to */
    // 对应哪一个 cgroup
	struct cgroup *cgroup;

	/* PI: the cgroup subsystem that this css is attached to */
    // 对应哪一个 subsystem
	struct cgroup_subsys *ss;

	/* reference count - access via css_[try]get() and css_put() */
	struct percpu_ref refcnt;

	/* siblings list anchored at the parent's ->children */
	struct list_head sibling;
	struct list_head children;

	/* flush target list anchored at cgrp->rstat_css_list */
	struct list_head rstat_css_node;

	/*
	 * PI: Subsys-unique ID.  0 is unused and root is always 1.  The
	 * matching css can be looked up using css_from_id().
	 */
	int id;

	/* number of procs under this css and its descendants */
	int nr_procs;

	unsigned int flags;

	/*
	 * Monotonically increasing unique serial number which defines a
	 * uniform order among all csses.  It's guaranteed that all
	 * ->children lists are in the ascending order of ->serial_nr and
	 * used to allow interrupting and resuming iterations.
	 */
	u64 serial_nr;

	/*
	 * Incremented by online self and children.  Used to guarantee that
	 * parents are not offlined before their children.
	 */
	atomic_t online_cnt;

	/* percpu_ref killing and RCU release */
	struct work_struct destroy_work;
	struct rcu_work destroy_rwork;

	CK_KABI_RESERVE(1)
	CK_KABI_RESERVE(2)
	CK_KABI_RESERVE(3)
	CK_KABI_RESERVE(4)

	/*
	 * PI: the parent css.	Placed here for cache proximity to following
	 * fields of the containing structure.
	 */
	struct cgroup_subsys_state *parent;
};

Cgroup v2

代码上,cgroup v2 并不是另外设计了一套代码,而是基于 cgroup v1 进行重构的。所以相当于一套代码支持了 v1 和 v2 的两套逻辑。

Cgroup v1 和 v2 的区别

v1 一般长这样(不同的子系统是不同的目录):

[root@localhost zorro]# mount
......
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)

来看一下 cgroup v2 的目录树结构:

[root@localhost zorro]# ls -p /sys/fs/cgroup/
cgroup.controllers      cgroup.stat             cpuset.cpus.effective  machine.slice/
cgroup.max.depth        cgroup.subtree_control  cpuset.mems.effective  memory.pressure
cgroup.max.descendants  cgroup.threads          init.scope/            system.slice/
cgroup.procs            cpu.pressure            io.pressure            user.slice/

主要的区别在于这个文件cgroup.controllers。这个文件显示了当前 cgoup 可以限制的相关资源有哪些?v2 之所以叫 unified,除了在内核中实现架构的区别外,体现在外在配置方法上也有变化。比如,这一个文件就可以控制当前 cgroup 都支持哪些资源的限制。而不是像 v1 一样资源分别在不同的目录下进行创建相关 cgroup。

默认创建出来的 zorro 组中的 cgroup.controllers 内容为:

[root@localhost cgroup]# cat zorro/cgroup.controllers
memory pids

还有一个文件叫做 cgroup.subtree_control,子层级的 cgroup 资源限制范围被上一级的 cgroup.subtree_control 文件内容所限制。

详解Cgroup V2 | Zorro’s Linux Book