容器之 CGroup

在 CentOS 7 中,已经通过 systemd 替换了之前的 cgroup-tools 工具,为了防止两者冲突,建议只使用 systemd ,只有对于一些不支持的,例如 net_prio ,才使用 cgroup-tools 工具。

在此,简单介绍其使用。

简介

在系统设计时,经常会遇到类似的需求,就是希望能限制某个或者某些进程的分配资源。

由此,就有了容器的概念,在容器中,有分配好的特定比例的 CPU、IO、内存、网络等资源,这就是 controller group ,简称为 cgroup ,最初由 Google 工程师提出,后来被整合进 Linux 内核中。

cgroup 本身提供了将进程进行分组化管理的功能和接口的基础结构。

使用简介

在 CentOS 7 中需要通过 yum install libcgroup libcgroup-tools 安装额外的 cgroup 工具,对系统来说,默认会挂载到 /sys/fs/cgroup/ 目录下。

----- 查看系统已经存在cgroup子系统及其挂载点
# lssubsys -am
----- 或者通过mount查看cgroup类型的挂载点
# mount -t cgroup

----- 可以命令行挂载和卸载子系统,此时再次执行上述命令将看不到memory挂载点
# umount /sys/fs/cgroup/memory/
----- 挂载cgroup的memory子系统,其中最后的cgroup参数是在/proc/mounts中显示的名称
# mount -t cgroup -o memory cgroup /sys/fs/cgroup/memory/
# mount -t cgroup -o memory none /sys/fs/cgroup/memory/

另外,在 CentOS 中有 /etc/cgconfig.conf 配置文件,该文件中可用来配置开机自动启动时挂载的条目:

mount {
    net_prio = /sys/fs/cgroup/net_prio;
}

然后,通过 systemctl restart cgconfig.service 重启服务即可,然后可以通过如下方式使用。

使用步骤

简单介绍如何通过 libcgroup-tools 创建分组并设置资源配置参数。

1. 创建控制组群

可以通过如下方式创建以及删除群组,创建后会在 cpu 挂载目录下 /sys/fs/cgroup/cpu/ 目录下看到一个新的目录 test,这个就是新创建的 cpu 子控制组群。

# cgcreate -g cpu:/test
# cgdelete -g cpu:/test

2. 设置组群参数

cpu.shares 是控制 CPU 的一个属性,更多的属性可以到 /sys/fs/cgroup/cpu 目录下查看,默认值是 1024,值越大,能获得更多的 CPU 时间。

# cgset -r cpu.shares=512 test

3. 将进程添加到控制组群

可以直接将需要执行的命令添加到分组中。

----- 直接在cgroup中执行
# cgexec -g cpu:small some-program
----- 将现有的进程添加到cgroup中
# cgclassify -g subsystems:path_to_cgroups pidlist

例如,想把 sshd 添加到一个分组中,可以通过如下方式操作。

# cgclassify -g cpu:/test `pidof sshd`
# cat /sys/fs/cgroup/cpu/test/tasks

就会看到相应的进程在这个文件中。

CPU

在 CGroup 中,与 CPU 相关的子系统有 cpusets、cpuacct 和 cpu 。

  • CPUSET 用于设置CPU、内存的亲和性,可以指定运行CPU或者不运行在某个CPU上,一般只会在一些高性能场景使用。
  • CPUACCT 显示当前cgroup所用CPU的统计信息。

这里简单介绍 cpu 子系统,包括怎么限制 cgroup 的 CPU 使用上限及与其它 cgroup 的相对值。

cpu.cfs_period_us & cpu.cfs_quota_us

其中 cfs_period_us 用来配置时间周期长度;cfs_quota_us 用来配置当前 cgroup 在设置的周期长度内所能使用的 CPU 时间数,两个文件配合起来设置 CPU 的使用上限。

两个文件单位是微秒,cfs_period_us 的取值范围为 [1ms, 1s],默认 100ms ;cfs_quota_us 的取值大于 1ms 即可,如果 cfs_quota_us 的值为 -1(默认值),表示不受 cpu 时间的限制。

下面是几个例子:

----- 1.限制只能使用1个CPU,每100ms能使用100ms的CPU时间
# echo 100000 > cpu.cfs_quota_us
# echo 100000 > cpu.cfs_period_us

------ 2.限制使用2个CPU核,每100ms能使用200ms的CPU时间,即使用两个内核
# echo 200000 > cpu.cfs_quota_us
# echo 100000 > cpu.cfs_period_us

------ 3.限制使用1个CPU的50%,每100ms能使用50ms的CPU时间,即使用一个CPU核心的50%
# echo 50000 > cpu.cfs_quota_us
# echo 100000 > cpu.cfs_period_us

cpu.shares

用于设置相对值,这里针对的是所有 CPU (多核),默认是 1024,假如系统中有两个 A(1024) 和 B(512),那么 A 将获得 1024/(1204+512)=66.67% 的 CPU 资源,而 B 将获得 33% 的 CPU 资源。

对于 shares 有两个特点:

  • 如果A不忙,没有使用到66%的CPU时间,那么剩余的CPU时间将会被系统分配给B,即B的CPU使用率可以超过33%;
  • 添加了一个新的C,它的shares值是1024,那么A和C的限额变为1024/(1204+512+1024)=40%,B的资源变成了20%;

也就是说,在空闲时 shares 基本上不起作用,只有在 CPU 忙的时候起作用。但是这里设置的值是需要与其它系统进行比较,而非设置了一个绝对值。

示例

演示一下如何控制CPU的使用率。

----- 创建并查看当前的分组
# cgcreate -g cpu:/small
# ls /sys/fs/cgroup/cpu/small

----- 查看当前值,默认是1024
# cat /sys/fs/cgroup/cpu/small/cpu.shares
# cgset -r cpu.shares=512 small

----- 执行需要运行的程序,或者将正在运行程序添加到分组
# cgexec -g cpu:small ./foobar
# cgclassify -g cpu:small <PID>

----- 设置只能使用1个cpu的20%的时间
# echo 50000 > cpu.cfs_period_us
# echo 10000 > cpu.cfs_quota_us

----- 将当前bash加入到该cgroup
# echo $$
5456
# echo 5456 > cgroup.procs

----- 启动一个bash内的死循环,正常情况下应该使用100%的cpu,消耗一个核
# while :; do echo test > /dev/null; done

注意,如果是在启动进程之后添加的,实际上 CPU 资源限制的速度会比较慢,不是立即就会限制死的,而且不是严格准确。如果起了多个子进程,那么各个进程之间的资源是共享的。

其它

可以通过如下命令查看进程属于哪个 cgroup 。

# ps -O cgroup
# cat /proc/PID/cgroup

内存

相比来说,内存控制要简单的多,只需要注意物理内存和 SWAP 即可。

----- 创建并查看当前的分组
# cgcreate -g memory:/small
# ls /sys/fs/cgroup/memory/small

----- 查看当前值,默认是一个很大很大的值,设置为1M
# cat /sys/fs/cgroup/memory/small/memory.limit_in_bytes
# cgset -r memory.limit_in_bytes=10485760 small

----- 如果开启了swap之后,会发现实际上内存只限制了RSS,设置时需要确保没有进程在使用
# cgset -r memory.memsw.limit_in_bytes=104857600 small

----- 启动测试程序
# cgexec -g cpu:small -g memory:small ./foobar
# cgexec -g cpu,memory:small ./foobar

OOM

当进程试图占用的内存超过了 cgroups 的限制时,会触发 out of memory 导致进程被强制 kill 掉。

----- 关闭默认的OOM
# echo 1 > memory.oom_control
# cgset -r memory.oom_control=1 small

注意,及时关闭了 OOM,对应的进程会处于 uninterruptible sleep 状态。

systemd

CentOS 7 中默认的资源隔离是通过 systemd 进行资源控制的,systemd 内部使用 cgroups 对其下的单元进行资源管理,包括 CPU、BlcokIO 以及 MEM,通过 cgroup 可以 。

systemd 的资源管理主要基于三个单元 service、scope 以及 slice:

  • service
    通过 unit 配置文件定义,包括了一个或者多个进程,可以作为整体启停。
  • scope
    任意进程可以通过 fork() 方式创建进程,常见的有 session、container 等。
  • slice
    按照层级对 service、scope 组织的运行单元,不单独包含进程资源,进程包含在 service 和 scope 中。

常用的 slice 有 A) system.slice,系统服务进程可能是开机启动或者登陆后手动启动的服务,例如crond、mysqld、nginx等服务;B) user.slice 用户登陆后的管理,一般为 session;C) machine.slice 虚机或者容器的管理。

对于 cgroup 默认相关的参数会保存在 /sys/fs/cgroup/ 目录下,当前系统支持的 subsys 可以通过 cat /proc/cgroups 或者 lssubsys 查看。

常见命令

常用命令可以参考如下。

----- 查看slice、scope、service层级关系
# systemd-cgls

----- 当前资源使用情况
# systemd-cgtop

----- 启动一个服务
# systemd-run --unit=name --scope --slice=slice_name command
   unit   用于标示,如果不使用会自动生成一个,通过systemctl会输出;
   scope  默认使用service,该参数指定使用scope ;
   slice  将新启动的service或者scope添加到slice中,默认添加到system.slice,
          也可以添加到已有slice(systemctl -t slice)或者新建一个。
# systemd-run --unit=toptest --slice=test top -b
# systemctl stop toptest

----- 查看当前资源使用状态
$ systemctl show toptest

各服务配置保存在 /usr/lib/systemd/system/ 目录下,可以通过如下命令设置各个服务的参数。

----- 会自动保存到配置文件中做持久化
# systemctl set-property name parameter=value

----- 只临时修改不做持久化
# systemctl set-property --runtime name property=value

----- 设置CPU和内存使用率
# systemctl set-property httpd.service CPUShares=600 MemoryLimit=500M

另外,在 213 版本之后才开始支持 CPUQuota 参数,可直接修改 cpu.cfs_{quota,period}_us 参数,也就是在 /sys/fs/cgroup/cpu/ 目录下。

libcgroup

基于 libcgroup 实现一套容器的管理,详细的文档可以参考 http://libcg.sourceforge.net/html/index.html 中的相关介绍。

可以参考 https://github.com/geokat/cgfy 中的实现,该程序是通过 libcgroup 实现,功能类似于 cgexec 。

另外,也可以参考 https://github.com/vodik/clique ,是直接利用 DBus 与 Systemd 进行通讯。

参考

关于 systemd 的资源控制,详细可以通过 man 5 systemd.resource-control 命令查看帮助,或者查看 http://www.jinbuguo.com/systemd/systemd.resource-control.html 中文手册;详细的内容可以参考 https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/resource_management_guide/index

Docker 基础知识之 Namespace, Cgroup

最近工作上需要使用 Docker,在阅读「第一本 Docker 书」后了解了如何成为 Docker 的用户,但对 Docker 中用到技术却不甚了解。都说 Docker 是「新瓶装旧球」,文中笔者将学习到的 Docker 基础技术中的 Namespace,Cgroup 与 AUFS 记录如下。

Namespace

Linux Namespace 是 Linux 内核提供的一个功能,可以实现系统资源的隔离,如:PID、User ID、Network 等。Linux 中的 chroot 命令可以将当前目录设置为根目录,使得用户的操作被限制在当前目录之下而不影响其他目录。

假设我们成立了一家向外售卖计算资源的公司,用户购买了一个实例在运行自己的应用。如果某些用户能够进入到其他人的实例中,修改或关闭其他实例中应用的状态,那么就会导致不同用户之间相互影响;用户的某些操作可能需要 root 权限,假如我们给每个用户都赋予了 root 权限,那么我们的机器也就没有任何安全性可言了。使用 Namespace,Linux 可以做到 UID 级别的隔离,也就是说,UID 为 n 的用户在自己的 Namespace 中是有 root 权限的,但是在真实的物理机上,他仍然是 UID 为 n 的用户。

目前 Linux 共实现了 6 种不同的 Namespace。

未分类

UTS Namespace

UTS namespaces allow a single system to appear to have different host and domain names to different processes.

UTS(UNIX Timesharing System) Namespace 可以用来隔离 nodename 和 domainname 两个系统标识。在 UTS Namespace 中,每个 Namespace 可以有自己的 hostname。

我们运行下面程序:

func main() {
    cmd := exec.Command("zsh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

这段代码主要是通过系统调用 clone,并传入 CLONE_NEWUTS 作为参数创建一个新进程,并在新进程内运行 zsh 命令。在 Ubuntu 14.04 上运行这段代码,就可以进入一个交互环境,在环境中运行 ps -af –forest 就可以看到如下的进程树:

未分类

验证下父进程和子进程是否在同一个 UTS Namespace 中:

未分类

可以看到他们的 UTS Namespace 的编号不同。因为 UTS Namespace 对 hostname 做了隔离,所以在这个环境内修改 hostname 不会影响外部主机。

在目前的 zsh 环境中我们修改 hostname 并打印:

未分类

在宿主机上打印 hostname:

未分类

可以看到,外部的 hostname 没有被内部的修改所影响。

IPC Namespace

IPC namespaces isolate processes from SysV style inter-process communication.

IPC(Interprocess Communication) Namespace 用来隔离 System V IPC 和 POSIX message queues。每一个 IPC Namespace 都有自己的 System V IPC 和 POSIX message queue。

我们在上一段代码的基础上增加 CLONE_NEWIPC 标识,表示我们要创建 IPC Namespace。

func main() {
    cmd := exec.Command("zsh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

在宿主器机查看并创建一个 message queue:

未分类

运行代码并查看 message queue:

未分类

PID Namespace

The PID namespace provides processes with an independent set of process IDs (PIDs) from other namespaces.

PID(Process ID) Namespace 可以用来隔离进程 ID。同一个进程在不同的 PID Namespace 中可以拥有不同的 PID。在 Docker Container 中,使用 ps -ef 可以看到启动容器的进程 PID 为 1,但是在宿主机上,该进程却又有不同的 PID。

继续在代码上添加 CLONE_NEWPID 为子进程创建 PID Namespace。

func main() {
    cmd := exec.Command("zsh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

运行代码,首先在宿主机上查看进程树:

未分类

可以看到 zsh 的 PID 为 11321。在 Namespace 中打印进程 PID:

未分类

可以看到,打印出的当前 Namespace 的 PID 为 1,也就是说 11321 的进程被映射到 Namespace 中后 PID 为 1。

Mount Namespace

Mount namespaces control mount points.

Mount Namespace 用来隔离各个进程看到的挂载点视图。在不同的 Namespace 中,看到的挂载点文件系统层次是不一样的。在 Mount Namespace 中调用 mount 和 unmount 仅仅会影响当前 Namespace 内的文件系统,而对全局文件系统是没有影响的。

在代码中,我们继续加入 CLONE_NEWNS 标识。

func main() {
    cmd := exec.Command("zsh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
}

首先运行代码,然后查看 /proc 的文件内容:

未分类

可以看到宿主机的 /proc 中文件较多,其中的数字是对应进程的相关信息。下面,将 /proc mount 到 Namespace 中。

未分类

可以看到现在以 PID 命名的文件夹明显减少。下面使用 ps -ef 查看系统进程:

未分类

可以看到,在当前的 Namespace 中,zsh 是 PID 为 1 的进程。这就说明当前 Namespace 中的 mount 和外部是隔离的,mount 操作没有影响到外部。Docker 的 volumn 正是利用了这个特性。

User Namespace

User namespaces are a feature to provide both privilege isolation and user identification segregation across multiple sets of processes.

User Namespace 主要是隔离用户的用户组 ID。也就是说,一个进程的 User ID 和 Group ID 在 User Namespace 内外可以是不同的。比较常用的是,在宿主机上以一个非 root 用户运行创建一个 User Namespace,然后在 User Namespace 中被映射为了 root 用户。这意味着这个进程在 User Namespace 中有 root 权限,但是在宿主机上却没有 root 权限。

继续修改代码,添加 CLONE_NEWUSER 标识。

func main() {
    cmd := exec.Command("zsh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
    Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID |
        syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
    os.Exit(1)
}

首先在宿主机上查看当前用户和用户组:

未分类

接下来运行程序,并查看用户组:

未分类

可以看到,UID 是不同的,说明 User Namespace 生效了。

Network Namespace

Network namespaces virtualize the network stack. On creation a network namespace contains only a loopback interface.

Network Namespace 用来隔离网络设置、IP 地址和端口号等网络栈的 Namespace。Network Namespace 可以让每个容器拥有自己独立的网络设备,而且容器内的应用可以绑定到自己的端口,每个 Namespace 的端口都不会有冲突。在宿主机搭建网桥后,就能很方便地实现容器之间的通信。

我们继续在代码基础上添加 CLONE_NEWNET 标识。

func main() {
    cmd := exec.Command("sh")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID |
        syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
    }
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
    os.Exit(1)
}

首先,在宿主机上查看自己的网络设备:

未分类

可以看到在宿主机上有 eth0 和 lo 等网络设备。下面,运行程序,并运行 ifconfig:

未分类

我们发现,在 Namespace 中什么网络设备都没有。这可以断定 Namespace 与宿主机之间的网络是处于隔离状态的。

## Cgroups

Linux Namespace 帮助进程隔离出自己的单独空间,而 Cgroups 则可以限制每个空间的大小。Cgroups 提供了对一组进程及将来子进程的资源限制、控制和统计的能力。

Cgroups 有三个组件:

  • cgroup 负责对进程分组管理,一个 cgroup 包含一组进程并可以设置进程参数
  • subsystem 是一组资源控制模块,可以关联到 cgroup 上,并对 cgroup 中的进程做出相应限制。
  • hierarchy 可以把一组 cgroup 串成一个树状结构,这样 cgroup 可以做到继承。

Cgroups 中的 hierarchy 是一种树状结构,Kernel 为了使得对 Cgroups 的配置更加直观,通过一个虚拟的树状文件系统配置 Cgroups 的,通过层级的目录虚拟出 cgroup 树。我们可以在系统上做实验:

1.首先,创建并挂载一个 hierarchy

未分类

  • cgroup.clone_children,cpuset 的 subsystem 会读取这个配置文件,如果这个值是 1,子 cgroup 才会继承父 cgroup 的 cputset 的配置
  • cgroup.procs 是树中当前节点 cgroup 中的进程组 ID
  • notify_on_release 和 release_agent 会一起使用。notify_on_release 标识当这个 cgroup 最后一个进程退出的时候是否执行了 release_agent;release_agent 使进程退出后自动清理掉不再使用的 cgroup
  • tasks 标识该 cgroup 下的进程 ID,将进程 ID 写入 tasks 文件中,便会将相应进程加入到这个 cgroup 中

2.在刚创建好的 hierarchy 上 cgroup 的根节点中拓展出两个子 cgroup

未分类

可以看到在 cgroup 目录下创建文件夹的时候,Kernel 会把文件夹标记为子 cgroup,她们继承父 cgroup 的属性。

3.在 cgroup 中添加和移动进程只需要将进程 ID 写到或移动到 cgroup 节点的 tasks 文件中即可

未分类

这样,我们就把当前的 3217 进程加入到 cgroup-test:/cgroup-1 中了

4.通过 subsystem 限制 cgroup 中的进程的资源。我们使用系统为每个 subsystem 默认创建的 hierarchy,如 memory 的 hierarchy 来完成实验。

未分类

未分类

可以看到系统总的内存为 2GB,其中 stess 只能占用到 5% 左右,也就是 100MB。

利用cgroup限制instance磁盘io资源使用

1、安装libcgroup软件包;

[root@pre-pool1-r6-06-08-18-01 ~]# yum install libcgroup
Loaded plugins: product-id, subscription-manager
This system is not registered to Red Hat Subscription Management. You can use subscription-manager to register.
epel                                                                                                              | 3.0 kB     00:00    
epel/primary_db                                                                                                   | 7.9 MB     00:00    
rhel-HighAvailability                                                                                             | 3.9 kB     00:00    
rhel-HighAvailability/primary_db                                                                                  |  43 kB     00:00    
rhel-LoadBalancer                                                                                                 | 3.9 kB     00:00    
rhel-LoadBalancer/primary_db                                                                                      | 7.0 kB     00:00    
rhel-ResilientStorage                                                                                             | 3.9 kB     00:00    
rhel-ResilientStorage/primary_db                                                                                  |  47 kB     00:00    
rhel-ScalableFileSystem                                                                                           | 3.9 kB     00:00    
rhel-ScalableFileSystem/primary_db                                                                                | 6.8 kB     00:00    
rhel-Server                                                                                                       | 3.9 kB     00:00    
rhel-Server/primary_db                                                                                            | 3.1 MB     00:00    
rhel-dvd                                                                                                          | 3.9 kB     00:00    
rhel-dvd/primary_db                                                                                               | 3.1 MB     00:00    
Setting up Install Process
Package libcgroup-0.40.rc1-5.el6.x86_64 already installed and latest version
Nothing to do

2、开启服务;

[root@pre-pool1-r6-06-08-18-01 ~]# service cgconfig restart
Stopping cgconfig service:                                 [  OK  ]
Starting cgconfig service:                                 [  OK  ]
[root@pre-pool1-r6-06-08-18-01 ~]# chkconfig cgconfig on

[root@pre-pool1-r6-06-08-18-01 ~]# service cgred restart
Stopping CGroup Rules Engine Daemon...                     [  OK  ]
Starting CGroup Rules Engine Daemon:                       [  OK  ]
[root@pre-pool1-r6-06-08-18-01 ~]# chkconfig cgred on

3、修改配置文件

[root@pre-pool1-r6-06-08-18-01 ~]# cat /etc/cgconfig.conf
#
#  Copyright IBM Corporation. 2007
#
#  Authors:        Balbir Singh <[email protected]>
#  This program is free software; you can redistribute it and/or modify it
#  under the terms of version 2.1 of the GNU Lesser General Public License
#  as published by the Free Software Foundation.
#
#  This program is distributed in the hope that it would be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See man cgconfig.conf for further details.
#
# By default, mount all controllers to /cgroup/<controller>

mount {
cpuset        = /cgroup/cpuset;
cpu        = /cgroup/cpu;
cpuacct        = /cgroup/cpuacct;
memory        = /cgroup/memory;
devices        = /cgroup/devices;
freezer        = /cgroup/freezer;
net_cls        = /cgroup/net_cls;
blkio        = /cgroup/blkio;
}

group bio-181 {
        blkio {
                blkio.throttle.write_iops_device="";
                blkio.throttle.read_iops_device="";
                blkio.throttle.write_bps_device="252:0 209715200";
                blkio.throttle.read_bps_device="252:0 209715200";
                blkio.reset_stats="";
                blkio.weight="500";
                blkio.weight_device="";
        }
}


[root@pre-pool1-r6-06-08-18-01 ~]# cat /etc/cgrules.conf
# /etc/cgrules.conf
#The format of this file is described in cgrules.conf(5)
#manual page.
#
# Example:
#<user>                <controllers>        <destination>
#@student        cpu,memory        usergroup/student/
#peter                cpu                test1/
#%                memory                test2/
# End of file
*               blkio           bio-181/

4、重启服务

[root@pre-pool1-r6-06-08-18-01 ~]# /etc/init.d/cgconfig restart
Stopping cgconfig service:                                 [  OK  ]
Starting cgconfig service:                                 [  OK  ]
[root@pre-pool1-r6-06-08-18-01 ~]# /etc/init.d/cgred restart
Stopping CGroup Rules Engine Daemon...                     [  OK  ]
Starting CGroup Rules Engine Daemon:                       [  OK  ]

Container技术之cgroup入门

What is Cgroup?

Cgroups 是 control groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如:cpu,memory,IO 等等)的机制。最初由 google 的工程师提出,后来被整合进 Linux 内核。Cgroups 也是 LXC 为实现虚拟化所使用的资源管理手段,可以说没有 cgroups 就没有 LXC。

What Cgroup can do?

1.限制进程组可以使用的资源数量,限制进程最大使用的内存等
2.进程组的优先级控制,比如为某个进程组分配特定的cpu share
3.记录进程组使用的资源数量,比如记录某个进程CPU的使用时间
4.进程组隔离,比如通过namespace以达到隔离的目的
5.进程组控制,比如可以将进程组挂起或恢复

Cgroup Model

进程模型

在说Cgroup的模型之前,先回顾下进程模型,在linux系统上,所有的进程都有一个共同的父进程,叫做init进程,这个进程在内核启动的时候开始执行,然后通过init进程启动其他的进程,这些进程都是init的子进程。因为所有的进程都有一个共同的父进程。那么linux的进程模型就是一个单继承层次的模型,或者称之为树状模型。除此之外每一个linux进程但是除了init进程,都继承了一些环境变量(例如PATH环境变量)

Cgroup Model

Cgroup其实和进程类似:

Cgroup也是继承体系,并且子cgroup继承其父cgroup的某些属性,两者最基本的差别在于,进程是单继承体系。而Cgroup可以存在多个不同的继承体系.(意思就是可以有多个单继承体系,每个单继承体系互不影响)

Some concepts of Cgroup

在Cgroup中有这样四个概念,可以说理解了这四个概念,那么对于如何使用cgroup,将会是如鱼得水。

  • Subsystems: 称之为子系统,一个子系统就是一个资源控制器,比如 cpu子系统就是控制cpu时间分配的一个控制器。

  • Hierarchies: 可以称之为层次体系也可以称之为继承体系,指的是Control Groups是按照层次体系的关系进行组织的。

  • Control Groups: 一组按照某种标准划分的进程。进程可以从一个Control Groups迁移到另外一个Control Groups中,同时Control Groups中的进程也会受到这个组的资源限制。

  • Tasks: 在cgroups中,Tasks就是系统的一个进程。

Subsystems

在Red_Hat_Enterprise_Linux-6系列的linux中,默认提供了如下子系统。

  • blkio这个子系统为块设备设定输入/输出限制,比如物理设备(磁盘,固态硬盘,USB 等等) 。
  • cpu这个子系统使用调度程序提供对 CPU 的 cgroup 任务访问。
  • cpuacct这个子系统自动生成 cgroup 中任务所使用的 CPU 报告。
  • cpuset这个子系统为 cgroup 中的任务分配独立 CPU(在多核系统)和内存节点。
  • devices这个子系统可允许或者拒绝 cgroup 中的任务访问设备。
  • freezer这个子系统挂起或者恢复 cgroup 中的任务。
  • memory这个子系统设定 cgroup 中任务使用的内存限制,并自动生成由那些任务使用的内存资源报告。
  • net_cls这个子系统使用等级识别符(classid)标记网络数据包,可允许 Linux 流量控制程序(tc)识别从具体 cgroup 中生成的数据包。
  • ns名称空间子系统。

Relationships Between Subsystems, Hierarchies, Control Groups and Tasks
这个部分恐怕是这篇文章的重点了吧,这个部分我主要参考红帽的关于资源管理的手册在这个部分通过引入4条规则,来帮助我们更好的去理解这个四者的关系。

  • 规则1,一个单继承体系(单层次体系)可以附加1个或者多个子系统

未分类

在上面的这个图中,Cpu和Memory两个子系统附加到了cpu_mem_cg的这个继承体系中cg1和cg2是两个Control Groups,但是需要注意的是,如果此前有Cpu或者Memory子系统附加到q其它继承体系中的,那么Cpu或者Memory就不能再次附加到cpu_mem_cg这个继承体系中了。

  • 规则2,一个子系统,不能被附加到多个继承体系中。

未分类

其实这条规则我已经在规则1中简单的进行了阐述,上面的这副图中,可以看出CPU子系统已经被附加到左侧的cpu_cg这个继承体系中了,当cpu子系统要再次被附加到cpu_mem_cg的时候,会发生附加失败。

  • 规则3,每当在系统中创建一个继承体系的时候,会默认再创建一个control groups,并且这个control groups被称之为root cgroup,此时整个系统中的tasks(进程)都属于这个root cgroup。系统中的进程,在一个继承体系中都明确的属于一个control groups,并且这个进程可以从一个control groups移动到另外一个control groups中,但是需要主要的是,在一个继承体系中一个进程是没办法同时属于两个control groups的,但是一个进程可以同时属于两个不同的继承体系中的control groups。

未分类

在上面的这副图中可以看出httpd这个进程无法同时属于A继承体系中的cg1和cg2这两个control groups,但是httpd进程却可以同时属于A继承体系中的cg1和B继承体系中的cg3。

  • 规则4,系统上的任何task(进程)通过fork创建子task(进程)的时候,这个子task(进程),自动继承其父task(进程)的control groups,成为这个control groups的一员。此后这个子task(进程)可以移动到其他control groups中,父task(进程)和子task(进程)完全独立。

未分类

在上面的这副图中,可以看出httpd进程fork出来的子进程仍然是属于cg1这个control group的。

cgroup限制进程内存大小

以限制mongodb的内存大小为例。
Cgroup限制方法:

mkdir /cgroup/memory/test/    
echo 50M > /cgroup/memory/test/memory.limit_in_bytes    
echo 50M > /cgroup/memory/test/memory.memsw.limit_in_bytes    
cgexec -g memory:test mongod -port 27017 --bind_ip 127.0.0.1 --dbpath /var/lib/mongo    

通过cgroup限制后,当内存达到限额,进程会被kill。

[root@centos mongo]# cgexec -g memory:test mongod -port 27017 --bind_ip 127.0.0.1 --dbpath /var/lib/mongo    
2014-07-18T23:20:53.228+0800 [initandlisten] MongoDB starting : pid=2529 port=27017 dbpath=/var/lib/mongo 64-bit host=centos    
2014-07-18T23:20:53.228+0800 [initandlisten] db version v2.6.3    
2014-07-18T23:20:53.228+0800 [initandlisten] git version: 255f67a66f9603c59380b2a389e386910bbb52cb    
2014-07-18T23:20:53.228+0800 [initandlisten] build info: Linux build12.nj1.10gen.cc 2.6.32-431.3.1.el6.x86_64 #1 SMP Fri Jan 3 21:39:27 UTC 2014 x86_64 BOOST_LIB_VERSION=1_49    
2014-07-18T23:20:53.228+0800 [initandlisten] allocator: tcmalloc    
2014-07-18T23:20:53.228+0800 [initandlisten] options: { net: { bindIp: "127.0.0.1", port: 27017 }, storage: { dbPath: "/var/lib/mongo" } }    
2014-07-18T23:20:53.304+0800 [initandlisten] journal dir=/var/lib/mongo/journal    
2014-07-18T23:20:53.304+0800 [initandlisten] recover : no journal files present, no recovery needed    
2014-07-18T23:20:53.374+0800 [initandlisten] waiting for connections on port 27017    
2014-07-18T23:20:57.838+0800 [initandlisten] connection accepted from 127.0.0.1:36712 #1 (1 connection now open)    
2014-07-18T23:21:15.077+0800 [initandlisten] connection accepted from 127.0.0.1:36713 #2 (2 connections now open)    
2014-07-18T23:21:52.342+0800 [conn2] getmore test.my_collection cursorid:34538199491 ntoreturn:0 keyUpdates:0 numYields:39 locks(micros) r:121572 nreturned:95052 reslen:4194299 202ms    
2014-07-18T23:21:53.376+0800 [clientcursormon] mem (MB) res:136 virt:12809    
2014-07-18T23:21:53.376+0800 [clientcursormon]  mapped (incl journal view):12508    
2014-07-18T23:21:53.376+0800 [clientcursormon]  connections:2    
2014-07-18T23:21:56.790+0800 [conn2] getmore test.my_collection cursorid:34538199491 ntoreturn:0 keyUpdates:0 numYields:88 locks(micros) r:142113 nreturned:95595 reslen:4194301 244ms    
Killed    

数据查询脚本:

[root@centos data]# cat mongotestList.py    
import pymongo    
import time    

client = pymongo.MongoClient("localhost", 27017)    
db = client.test    
print db.name    
print db.my_collection    

for item in db.my_collection.find():    
    print item    

数据插入脚本:

[root@centos data]# cat mongotest2.py    
import pymongo    
import time    

client = pymongo.MongoClient("localhost", 27017)    
db = client.test    
print db.name    
print db.my_collection    

while True:    
    db.my_collection.save({time.ctime(): time.time()})   

yarn使用cgroup隔离资源

yarn默认只管理内存资源,虽然也可以申请cpu资源,但是在没有cpu资源隔离的情况下效果并不是太好.在集群规模大,任务多时资源竞争的问题尤为严重.
还好yarn提供的LinuxContainerExecutor可以通过cgroup来隔离cpu资源

cgroup

cgroup是系统提供的资源隔离功能,可以隔离系统的多种类型的资源,yarn只用来隔离cpu资源

安装cgroup

默认系统已经安装了cgroup了,如果没有安装可以通过命令安装

CentOS 6

yum install -y libcgroup

CentOS 7

yum install -y libcgroup-tools

然后通过命令启动

CentOS 6

/etc/init.d/cgconfig start

CentOS 7

systemctl start cgconfig.service

查看/cgroup目录,可以看到里面已经创建了一些目录,这些目录就是可以隔离的资源

drwxr-xr-x 2 root root 0 3月  19 20:56 blkio
drwxr-xr-x 3 root root 0 3月  19 20:56 cpu
drwxr-xr-x 2 root root 0 3月  19 20:56 cpuacct
drwxr-xr-x 2 root root 0 3月  19 20:56 cpuset
drwxr-xr-x 2 root root 0 3月  19 20:56 devices
drwxr-xr-x 2 root root 0 3月  19 20:56 freezer
drwxr-xr-x 2 root root 0 3月  19 20:56 memory
drwxr-xr-x 2 root root 0 3月  19 20:56 net_cls

如果目录没有创建可以执行

cd /
mkdir cgroup
mount -t tmpfs cgroup_root ./cgroup
mkdir cgroup/cpuset
mount -t cgroup -ocpuset cpuset ./cgroup/cpuset/
mkdir cgroup/cpu
mount -t cgroup -ocpu cpu ./cgroup/cpu/
mkdir cgroup/memory
mount -t cgroup -omemory memory ./cgroup/memory/

通过cgroup隔离cpu资源的步骤为

1、在cpu目录创建分组

cgroup以组为单位隔离资源,同一个组可以使用的资源相同
一个组在cgroup里面体现为一个文件夹,创建分组直接使用mkdir命令即可.
组下面还可以创建下级组.最终可以形成一个树形结构来完成复杂的资源隔离方案.
每当创建了一个组,系统会自动在目录立即创建一些文件,资源控制主要就是通过配置这些文件来完成

--w--w--w- 1 root root 0 3月  19 21:09 cgroup.event_control
-rw-r--r-- 1 root root 0 3月  19 21:09 cgroup.procs
-rw-r--r-- 1 root root 0 3月  19 21:09 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 3月  19 21:09 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 3月  19 21:09 cpu.rt_period_us
-rw-r--r-- 1 root root 0 3月  19 21:09 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 3月  19 21:09 cpu.shares
-r--r--r-- 1 root root 0 3月  19 21:09 cpu.stat
-rw-r--r-- 1 root root 0 3月  19 21:09 notify_on_release
-rw-r--r-- 1 root root 0 3月  19 21:09 tasks

yarn默认使用hadoop-yarn组作为最上层,任务运行时yarn会为每个container在hadoop-yarn里面创建一个组
yarn主要使用cpu.cfs_quota_us cpu.cfs_period_us cpu.shares3个文件
yarn使用cgroup的两种方式来控制cpu资源分配

  • 严格按核数隔离资源
    可使用核数 = cpu.cfs_quota_us/cpu.cfs_period_us
    在yarn中cpu.cfs_quota_us被直接设置为1000000(这个参数可以设置的最大值)
    然后根据任务申请的core来计算出cpu.cfs_period_us

  • 按比例隔离资源
    按每个分组里面cpu.shares的比率来分配cpu
    比如A B C三个分组,cpu.shares分别设置为1024 1024 2048,那么他们可以使用的cpu比率为1:1:2

2、将进程id添加到指定组的tasks文件
创建完分组后只需要将要限制的进程的id写入tasks文件即可,如果需要解除限制,在tasks文件删除即可

yarn配置

启动cgroup需要配置几个配置文件

etc/hadoop/yarn-site.xml配置

可以参考http://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-site/NodeManagerCgroups.html 配置
这些配置大部分都是固定配置

<property>
    <name>yarn.nodemanager.container-executor.class</name>
  <value>org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor</value>
</property>
<property>
    <name>yarn.nodemanager.linux-container-executor.resources-handler.class</name>
    <value>org.apache.hadoop.yarn.server.nodemanager.util.CgroupsLCEResourcesHandler</value>
</property>
<property>
    <description>yarn使用的cgroup组,默认为/hadoop-yarn</description>
    <name>yarn.nodemanager.linux-container-executor.cgroups.hierarchy</name>
    <value>/hadoop-yarn</value>
</property>
<property>
    <description>是否自动挂载cgroup</description>
    <name>yarn.nodemanager.linux-container-executor.cgroups.mount</name>
    <value>true</value>
</property>
<property>
    <description>cgroup挂载目录, /sys/fs/cgroup 或者是 /cgroup,目录和系统有关</description>
    <name>yarn.nodemanager.linux-container-executor.cgroups.mount-path</name>
    <value>/cgroup</value>
</property>
<property>
    <name>yarn.nodemanager.linux-container-executor.group</name>
    <value>hadoop</value>
</property>
<property>
    <description>配置nodemanager使用多少物理cpu资源,比如24核服务器配置90的话,最近使用21.6核</description>
    <name>yarn.nodemanager.resource.percentage-physical-cpu-limit</name>
    <value>90</value>
</property>
<property>
    <description>是控制是否严格限制cpu,即按任务申请的core限制,还是非严格限制,即按core的比率限制</description>
    <name>yarn.nodemanager.linux-container-executor.cgroups.strict-resource-usage</name>
    <value>true</value>
</property>
<property>
    <description>非安全模式将会以这里设置的用户运行container,比如配置hadoop用户则以hadoop运行container</description>
    <name>yarn.nodemanager.linux-container-executor.nonsecure-mode.local-user</name>
    <value>hadoop</value>
</property>

etc/hadoop/container-executor.cfg配置

这个配置文件每项都需要填,要不然会报错

yarn.nodemanager.linux-container-executor.group=hadoop
banned.users=root
min.user.id=1000
allowed.system.users=hadoop

权限设置

在配置中文件的权限有特殊要求

chown root:hadoop bin/container-executor
chmod 6050 bin/container-executor

系统还要求etc/hadoop/container-executor.cfg 的所有父目录(一直到/ 目录) owner 都为 root
这个路径是默认${HADOOP_HOME}/etc/hadoop/container-executor.cfg,如果不方便修改所有父级目录为root权限,可以重新编译代码到其他目录,比如/etc/hadoop/目录

mvn clean package -Dcontainer-executor.conf.dir=/etc/hadoop/ -DskipTests -Pnative

配置好以后检测是否配置成功

./bin/container-executor --checksetup

如果没有任何输出表示配置成功
如果一切顺利就可以启动集群了

测试cgroup

可以运行测试脚本测试系统

./bin/spark-submit   
--class org.apache.spark.examples.SparkPi   
--master yarn-cluster   
--deploy-mode cluster   
--num-executors 5 
--executor-cores 3 
--executor-memory 4G 
--driver-memory 4G 
--driver-cores 2 
lib/spark-examples-1.6.0-hadoop2.6.0.jar   10000

查看系统是否生效只能登录到服务器查看
通过top查看信息

未分类

查看是否创建了cgroup分组,ll /cgroup/hadoop-yarn/

--w--w--w- 1 root root 0 3月  17 15:44 cgroup.event_control
-rw-r--r-- 1 root root 0 3月  17 15:44 cgroup.procs
drwxr-xr-x 2 root root 0 3月  17 16:06 container_1489736876249_0003_01_000011
drwxr-xr-x 2 root root 0 3月  17 16:06 container_1489736876249_0003_01_000026
drwxr-xr-x 2 root root 0 3月  17 16:06 container_1489736876249_0003_01_000051
drwxr-xr-x 2 root root 0 3月  17 16:06 container_1489736876249_0003_01_000076
drwxr-xr-x 2 root root 0 3月  17 16:06 container_1489736876249_0003_01_000101
drwxr-xr-x 2 root root 0 3月  17 16:06 container_1489736876249_0003_01_000123
drwxr-xr-x 2 root root 0 3月  17 16:06 container_1489736876249_0003_01_000136
drwxr-xr-x 2 root root 0 3月  17 16:06 container_1489736876249_0003_01_000155
drwxr-xr-x 2 root root 0 3月  17 16:30 container_1489736876249_0004_01_000008
-rw-r--r-- 1 root root 0 3月  17 15:47 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 3月  17 15:47 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 3月  17 15:44 cpu.rt_period_us
-rw-r--r-- 1 root root 0 3月  17 15:44 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 3月  17 15:44 cpu.shares
-r--r--r-- 1 root root 0 3月  17 15:44 cpu.stat
-rw-r--r-- 1 root root 0 3月  17 15:44 notify_on_release
-rw-r--r-- 1 root root 0 3月  17 15:44 tasks

查看container_*目录下 cpu.cfs_period_us,计算cpu.cfs_quota_us/cpu.cfs_period_us即可知道分配的核数

[root@- ~]# cat /cgroup/cpu/hadoop-yarn/container*/cpu.cfs_period_us
462962
462962
462962
462962
462962
462962
462962
462962
308641

问题处理

配置的过程中免不了会碰上一些问题,以下是我碰到的问题

spark任务申请了core,node manager分配不正确,都是分配1个核

这个是由于目前使用的capacity scheduler的资源计算方式只考虑了内存,没有考虑CPU
这种方式会导致资源使用情况统计不准确,比如一个saprk程序启动命令资源参数如下

--num-executors 1 --executor-cores 3 --executor-memory 4G --driver-memory 4G --driver-cores 1

DefaultResourceCalculator 统计占2核
DominantResourceCalculator 统计占4核
修改配置文件即可解决

  <property>
    <name>yarn.scheduler.capacity.resource-calculator</name>
    <value>org.apache.hadoop.yarn.util.resource.DominantResourceCalculator</value>
    <description>
      The ResourceCalculator implementation to be used to compare
      Resources in the scheduler.
      The default i.e. DefaultResourceCalculator only uses Memory while
      DominantResourceCalculator uses dominant-resource to compare
      multi-dimensional resources such as Memory, CPU etc.
    </description>
  </property>

container-executor运行时报缺少GLIBC_2.14库

container-executor: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by bin/container-executor)

这个和系统版本有关,只能通过重新编译container-executor来解决

mvn clean package -Dcontainer-executor.conf.dir=/etc/hadoop/ -DskipTests -Pnative

centos 7系统container启动报错,不能写入/cgroup/cpu

这个是yarn在centos 7下的一个bug,hadoop 2.8以后的版本才会解决
这个bug主要是因为centos 7下cgroup的目录和centos 6不一致导致,centos 7 cpu目录合并成cpu,cpuacct, 这个,导致的错误,需要打补丁后编译https://issues.apache.org/jira/browse/YARN-2194

 private String findControllerInMtab(String controller,
                                      Map<String, List<String>> entries) {
    for (Entry<String, List<String>> e : entries.entrySet()) {
//      if (e.getValue().contains(controller))
//        return e.getKey();

      if (e.getValue().contains(controller)) {
        String controllerKey = e.getKey();
        // In Redhat7, the controller is called "/sys/fs/cgroup/cpu,cpuacct"
        controllerKey = controllerKey.replace("cpu,cpuacct", "cpu");
        if (new File(controllerKey).exists()) {
          return controllerKey;
        }
      }
    }

    return null;
  }

升级的风险

由于改变了资源的隔离方式,升级可能有几个方面的影响

任务资源分配问题

升级cgroup后单个任务如果以前资源分配不合理可能会出现计算延时情况,出现资源问题时需要调整任务资源
在集群规模小的时候可能没有资源可以调整,那么可以修改为非严格模式,非严格模式不能按配置限制资源,只能保证资源不被少数进程全部占用

<property>
    <name>yarn.nodemanager.linux-container-executor.cgroups.strict-resource-usage</name>
    <value>false</value>
</property

spark driver资源问题

spark任务的driver在集群模式deploy-mode cluster时,如果没有配置driver-cores的话默认分配1核,1核在任务规模大时有可能资源会紧张.采用deploy-mode client模式的不受cgroup限制