Namespace 与 Cgroups 原理

1,106 阅读7分钟

Namespace 原理

概述

Linux Namespace 是一种 Linux Kernel 提供的资源隔离方案,系统可以为进程分配不同的 Namespace,并保证不同的 Namespace 资源独立分配、进程彼此隔离,即不同的 Namespace 下的进程互不干扰。 目前 Linux 内核共实现了 6 种 Namespace:

类型系统调用参数隔离资源Kernel 版本
MountCLONE NEWNS挂载点2.4.19
PIDCLONE NEWPID进程2.6.14
IPCCLONE NEWIPCSystem V IPC 和 POSIX 消息队列2.6.19
UTSCLONE NEWUTS主机名和域名2.6.19
NetworkCLONE NEWNET网络设备、网络协议栈、网络端口等2.6.29
UserCLONE NEWUSER用户和用户组3.8

内核实现

Linux 内核代码中 Namespace 的实现:

// 进程数据结构
struct task_struct {
    // ...
    /* namespaces */
    struct nsproxy *nsproxy;
    //  ...
}

// Namespace 数据结构
struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns_for_children;
    struct net           *net_ns; 
}

对 Namespace 的操作,主要是通过 clone、setns 和 unshare 这三个系统调用来完成的:

// clone
// 在创建新进程的系统调用时,可以通过 flags 参数指定需要新建的 Namespace 类型:
// CLONE_NEWCGROUP / CLONE_NEWIPC / CLONE_NEWNET / CLONE_NEWNS / CLONE_NEWPID /  CLONE_NEWUSER / CLONE_NEWUTS
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg)

// setns
// 该系统调用可以让调用进程加入某个已经存在的 Namespace 中:  
Int setns(int fd, int nstype)

// unshare
// 该系统调用可以将调用进程移动到新的 Namespace 下:
int unshare(int flags)

注意:docker exec 命令的实现原理就是 setns。

各 Namespace 介绍

PID Namespace

不同用户的进程就是通过 PID Namespace 隔离开的,且不同 Namespace 中可以有相同 PID,有了 PID Namespace,每个 Namespace 中的 PID 能够相互隔离。

Network Namespace

网络隔离是通过 Network Namespace 实现的, 每个 Network Namespace 有独立的网络设备、IP 地址、IP 路由表、/proc/net 目录、端口号等。 Docker 默认采用 veth 的方式将 Container 中的虚拟网卡同 host 上的一个 docker bridge:docker0 连接 在一起。

IPC Namespace

Container 中进程交互还是采用 Linux 常见的进程间交互方法(interprocess communication – IPC),包 括常见的信号量、消息队列和共享内存。 Container 的进程间交互实际上还是 Host 上具有相同 PID Namespace 中的进程间交互,因此需要在 IPC 资源申请时加入 Namespace 信息 - 每个 IPC 资源有一个唯一的 32 位 ID。

Mount Namespace

Mount Namespace 允许不同 Namespace 的进程看到的文件结构不同,这样每个 Namespace 中的进程所看 到的文件目录就被隔离开了。

UTS Namespace

UTS(UNIX Time-sharing System)Namespace 允许每个 Container 拥有独立的 nodename 和 domainname,使其在网络上可以被视作一个独立的节点而非 Host 上的一个进程。

注意:这里的主机名和域名也就是 uname 系统调用使用的结构体 struct utsname 里的 nodename 和 domainname 这两个字段,UTS 这个名字也是由此而来的。

USER Namespace

每个 Container 可以有不同的 user 和 group id,也就是说可以在 Container 内部用 Container 内部的用户 执行程序而非 Host 上的用户。

常用操作

查看当前系统的 Namespace:lsns -t <type> image.png

查看某进程的 Namespace:ls -la /proc/<pid>/ns/ image.png

进入某 Namespace 运行命令:nsenter -t <pid> -n <command> image.png

实践

  1. 在新 Network Namespace 执行 sleep 命令 image.png

  2. 查看 sleep 进程信息 image.png

  3. 查看 Network Namespace image.png

  4. 进入该进程所在 Namespace 查看网络配置 image.png

Cgroups 原理

概述

Cgroups(Control Groups)是 Linux 下用于对一个或一组进程进行资源控制和监控的机制,也叫资源 QoS:

  • 可以对诸如 CPU 使用时间、内存、磁盘 I/O 等进程所需的资源进行限制;
  • 不同资源的具体管理工作由相应的 Cgroups 子系统(Subsystem)来实现;
  • 针对不同类型的资源限制,只要将限制策略在不同的的子系统上进行关联即可;
  • Cgroups 在不同的系统资源管理子系统中以层级树(Hierarchy)的方式来组织管理:每个 Cgroups 都可以包含其他的子 Cgroups,因此子 Cgroups 能使用的资源除了受本 Cgroups 配置的资源参数限制,还受到父 Cgroups 设置的资源限制。

image.png

Cgroups 实现了对资源的配额和度量

  • blkio:这个子系统设置限制每个块设备的输入输出控制。例如磁盘,光盘以及 USB 等等。
  • cpu:这个子系统使用调度程序为 Cgroup 任务提供 CPU 的访问。
  • cpuacct:产生 Cgroup 任务的 CPU 资源报告。
  • cpuset:如果是多核心的 CPU,这个子系统会为 Cgroup 任务分配单独的 CPU 和内存。
  • devices:允许或拒绝 Cgroup 任务对设备的访问。
  • freezer:暂停和恢复 Cgroup 任务。
  • memory:设置每个 Cgroup 的内存限制以及产生内存资源报告。
  • net_cls:标记每个网络包以供 Cgroup 方便使用。
  • ns:名称空间子系统。
  • pid:进程标识子系统。

注意:Cgroup 从 2.6.24 开始进入内核主线,在 Cgroup 出现之前,只能对一个进程做一些资源限制,例如通过 sched-setaffinity 系统调用限定一个进程的 CPU 亲和性,或者用 ulimit 限制一个进程的打开文件上限、栈大小等。另外,使用 ulimit 可以对少数资源基于用于做资源控制,例如限制一个用户能创建的进程数。

内核实现

// 进程数据结构
struct task_struct
{
    #ifdef CONFIG_CGROUPS
    struct css_set __rcu *cgroups; 
    struct list_head cg_list; 
    #endif
}

// css_set 是 cgroup_subsys_state 对象的集合数据结构
struct css_set {
    /*
    * Set of subsystem states, one for each subsystem. This array is
    * immutable after creation apart from the init_css_set during
    * subsystem registration (at boot time).
    */
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
};

各子系统介绍

cpu 子系统

cpu 子系统用于限制进程的 CPU 占用率:

  • cpu.shares:可出让的能获得 CPU 使用时间的相对值。
  • cpu.cfs_period_us:cfs_period_us 用来配置时间周期长度,单位为 us(微秒)。
  • cpu.cfs_quota_us:cfs_quota_us 用来配置当前 Cgroup 在 cfs_period_us 时间内最多能使用的 CPU 时间数,单位为 us(微秒)。
  • cpu.stat:Cgroup 内的进程使用的 CPU 时间统计。
  • nr_periods:经过 cpu.cfs_period_us 的时间周期数量。
  • nr_throttled:在经过的周期内,有多少次因为进程在指定的时间周期内用光了配额时间而受到限制。
  • throttled_time:Cgroup 中的进程被限制使用 CPU 的总用时,单位是 ns(纳秒)。

cpuacct 子系统

cpuacct 子系统用于统计 Cgroup 及其子 Cgroup 下进程的 CPU 的使用情况:

  • cpuacct.usage:包含该 Cgroup 及其子 Cgroup 下进程使用 CPU 的时间,单位是 ns(纳秒)。
  • cpuacct.stat:包含该 Cgroup 及其子 Cgroup 下进程使用的 CPU 时间,以及用户态和内核态的时间。

memory 子系统

memory 子系统用来限制 Cgroup 所能使用的内存上限:

  • memory.usage_in_bytes:Cgroup 下进程使用的内存,包含 Cgroup 及其子 Cgroup 下的进程使用的内存
  • memory.max_usage_in_bytes:Cgroup 下进程使用内存的最大值,包含子 Cgroup 的内存使用量。
  • memory.limit_in_bytes:设置 Cgroup 下进程最多能使用的内存。如果设置为 -1,表示对该 Cgroup 的内存使用不做限制。
  • memory.soft_limit_in_bytes:这个限制并不会阻止进程使用超过限额的内存,只是在系统内存足够时,会优先回收超过限额的内存,使之向限定值靠拢。
  • memory.oom_control:设置是否在 Cgroup 中使用 OOM(Out of Memory)Killer,默认为使用。当属于该 Cgroup 的进程使用的内存超过最大的限定值时,会立刻被 OOM Killer 处理。

Cgroup driver

systemd

  • 当操作系统使用 systemd 作为 init system 时,初始化进程生成一个根 Cgroup 目录结构并作为 Cgroup 管理器。
  • systemd 与 Cgroup 紧密结合,并且为每个 systemd unit 分配 Cgroup。

cgroupfs

  • docker 默认用 cgroupfs 作为 Cgroup 驱动。

存在问题

  • 在 systemd 作为 init system 的系统中,默认并存着两套 groupdriver。
  • 这会使得系统中 Docker 和 kubelet 管理的进程被 cgroupfs 驱动管,而 systemd 拉起的服务由 systemd 驱动管,让 Cgroup 管理混乱且容易在资源紧张时引发问题。
  • 因此 kubelet 会默认 --cgroup-driver=systemd,若运行时 Cgroup 不一致时,kubelet 会报错。

参考

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿