Namespace 原理
概述
Linux Namespace 是一种 Linux Kernel 提供的资源隔离方案,系统可以为进程分配不同的 Namespace,并保证不同的 Namespace 资源独立分配、进程彼此隔离,即不同的 Namespace 下的进程互不干扰。 目前 Linux 内核共实现了 6 种 Namespace:
| 类型 | 系统调用参数 | 隔离资源 | Kernel 版本 |
|---|---|---|---|
| Mount | CLONE NEWNS | 挂载点 | 2.4.19 |
| PID | CLONE NEWPID | 进程 | 2.6.14 |
| IPC | CLONE NEWIPC | System V IPC 和 POSIX 消息队列 | 2.6.19 |
| UTS | CLONE NEWUTS | 主机名和域名 | 2.6.19 |
| Network | CLONE NEWNET | 网络设备、网络协议栈、网络端口等 | 2.6.29 |
| User | CLONE 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>
查看某进程的 Namespace:ls -la /proc/<pid>/ns/
进入某 Namespace 运行命令:nsenter -t <pid> -n <command>
实践
-
在新 Network Namespace 执行 sleep 命令
-
查看 sleep 进程信息
-
查看 Network Namespace
-
进入该进程所在 Namespace 查看网络配置
Cgroups 原理
概述
Cgroups(Control Groups)是 Linux 下用于对一个或一组进程进行资源控制和监控的机制,也叫资源 QoS:
- 可以对诸如 CPU 使用时间、内存、磁盘 I/O 等进程所需的资源进行限制;
- 不同资源的具体管理工作由相应的 Cgroups 子系统(Subsystem)来实现;
- 针对不同类型的资源限制,只要将限制策略在不同的的子系统上进行关联即可;
- Cgroups 在不同的系统资源管理子系统中以层级树(Hierarchy)的方式来组织管理:每个 Cgroups 都可以包含其他的子 Cgroups,因此子 Cgroups 能使用的资源除了受本 Cgroups 配置的资源参数限制,还受到父 Cgroups 设置的资源限制。
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 会报错。
参考
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。