Go
Go 语言又称Golang,是Google开发的一种静态强类型、编译型、并发型并具有垃圾回收功能的编程语言。Go语言在2009年第一次被披露,并在2012年发布了1.0版本,可以说是一门非常年轻的语言。
Go语言的语法虽然接近C语言,但还是有一些不同,比如两者对于变量的声明是不同的,且Go语言中的for循环和if判断语句不需要用小括号括起来。Go语言的并行模型是以东尼·霍尔的通信顺序进程(CSP)为基础的,并采取了类似模型的其他语言(包括Occam和Limbo),但它也具有Pi运算的特征,比如通道传输。
与C++相比,Go语言并不包括如异常处理、继承、泛型、断言、虚函数等功能,但增加了slice型、并发、管道、垃圾回收、接口(Interface)等特性的语言级支持。当然,Google对于泛型的态度还是很开放的,但在该语言的常见问题列表中,对于断言的存在,则持负面态度,同时也在为自己不提供类型继承辩护。不同于Java,Go语言内嵌了关联数组(也称为哈希表(Hash)或字典(Dictionary)),就像字符串类型一样。
可以在Go语言官网首页看到一个Go语言的Hello World示例,代码如下。
安装Go
这里使用 Go版本1.7.1
可以在Go语言官网https://golang.org/dl/根据操作系统下载对应的安装包。这里以Linux为例进行安装,首先下载安装包go1.7.1.linux-amd64.tar.gz(wget https://go.dev/dl/go1.7.1.linux-amd64.tar.gz),然后执行tar -C /usr/local -xzf go1.7.1.linux-amd64.tar.gz,将安装包解压到/usr/local目录下。编辑$HOME/.profile或$HOME/.bashrc,将export PATH=$PATH:/usr/local/go/bin命令添加到文件中,然后执行source $HOME/.bashrc,使修改生效。这时就可以在系统中使用Go命令了,执行go version来看一下,命令如下。
配置GOPATH
GOPATH 是真正存放代码的路径,Go寻找依赖包时会根据$GOPATH来寻找,GOPATH目录约定有如下3个子目录。
- src存放源代码。
- pkg存放编译后生成的文件。
- bin存放编译后的可执行文件。
这里以/go为GOPATH路径,编辑~/.bashrc文件,将命令export GOPATH=/go添加到文件中,然后执行source~/.bashrc,之后再执行go env看一下效果,结果如下。
可以看到,$GOPATH已经被指定了。
Linux Namespace介绍
概念
Linux Namespace是Kernel的一个功能,它可以隔离一系列的系统资源,比如PID(Process ID)、User ID、Network等。一般看到这里,很多人会想到一个命令chroot,就像chroot允许把当前目录变成根目录一样(被隔离开来的),Namespace也可以在一些资源上,将进程隔离起来,这些资源包括进程树、网络接口、挂载点等。
比如,一家公司向外界出售自己的计算资源。公司有一台性能还不错的服务器,每个用户买到一个tomcat实例用来运行它们自己的应用。有些调皮的客户可能不小心进入了别人的tomcat实例,修改或关闭了其中的某些资源,这样就会导致各个客户之间互相干扰。也许你会说,我们可以限制不同用户的权限,让用户只能访问自己名下的tomcat实例,但是,有些操作可能需要系统级别的权限,比如root权限。我们不可能给每个用户都授予root权限,也不可能给每个用户都提供一台全新的物理主机让他们互相隔离。因此,Linux Namespace在这里就派上了用场。使用Namespace,就可以做到UID级别的隔离,也就是说,可以以UID为n的用户,虚拟化出来一个Namespace,在这个Namespace里面,用户是具有root权限的。但是,在真实的物理机器上,他还是那个以UID为n的用户,这样就解决了用户之间隔离的问题。当然这只是Namespace其中的一个简单功能。
除了User Namespace,PID也是可以被虚拟的。命名空间建立系统的不同视图,从用户的角度来看,每一个命名空间应该像一台单独的Linux计算机一样,有自己的init进程(PID为1),其他进程的PID依次递增,A和B空间都有PID为1的init进程,子命名空间的进程映射到父命名空间的进程上,父命名空间可以知道每一个子命名空间的运行状态,而子命名空间与子命名空间之间是隔离的。从下图所示的PID映射关系图中可以看到,进程3在父命名空间中的PID为3,但是在子命名空间内,它的PID就是1。也就是说用户从子命名空间A内看进程3就像init进程一样,以为这个进程是自己的初始化进程,但是从整个host来看,它其实只是3号进程虚拟化出来的一个空间而已。
当前Linux一共实现了6种不同类型的Namespace。
Namespace的API主要使用如下3个系统调用。
- clone()创建新进程。根据系统调用参数来判断哪些类型的Namespace被创建,而且它们的子进程也会被包含到这些Namespace中。
- unshare()将进程移出某个Namespace。
- setns()将进程加入到Namespace中。
下面实验讲解使用 centos7 系统,内核版本 3.10,go 版本为 1.7.1(更建议使用 Ubuntu 版本)
UTS Namespace
UTS命名空间包含了运行内核的名称、版本、底层体系结构类型等信息。UTS是UNIX Timesharing System的简称,UTS Namespace主要用来隔离nodename和domainname两个系统标识。在UTS Namespace里面,每个Namespace允许有自己的hostname。
下面将使用Go来做一个UTS Namespace的例子。其实对于Namespace这种系统调用,使用C语言来描述是最好的,但是本书的目的是去实现Docker,由于Docker就是使用Go开发的,所以就整体使用Go来讲解。先来看一下如下代码,非常简单。
package main
import (
"os/exec"
"log"
"syscall"
"os"
)
func main() {
cmd := exec.Command("sh")
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)
}
}
解释一下代码,exec.Command("sh")用来指定被fork出来的新进程内的初始命令,默认使用sh来执行。使用CLONE_NEWUTS这个标识符去创建一个UTS Namespace。Go帮我们封装了对clone()函数的调用,这段代码执行后就会进入到一个sh运行环境中。
执行go run main.go命令,在这个交互式环境里,使用pstree -pl查看一下系统中进程之间的关系,然后,输出一下当前的PID,如下。
验证一下父进程和子进程是否不在同一个UTS Namespace中(验证 main 和 sh 的 ns 中 uts)
可以看到它们确实不在同一个UTS Namespace中。由于UTS Namespace对hostname做了隔离,所以在这个环境内修改hostname应该不影响外部主机,下面来做一下实验。
修改 hostname 为 bird 然后打印出来
另外启动一个shell,在宿主机上运行hostname,看一下效果。
可以看到,外部的hostname并没有被内部的修改所影响,由此可了解UTS Namespace的作用。
IPC Namespace
在linux下的多个进程间的通信机制叫做IPC(Inter-Process Communication),它是多个进程之间相互沟通的一种方法。在linux下有多种进程间通信的方法:半双工管道、命名管道、消息队列、信号、信号量、共享内存、内存映射文件,套接字等等。使用这些机制可以为linux下的网络服务器开发提供灵活而又坚固的框架。
IPC Namespace用来隔离System V IPC和POSIX message queues。每一个IPC Namespace都有自己的System V IPC和POSIX message queue。
在上一版本的基础上稍微改动了一下代码。
package main
import (
"os/exec"
"log"
"syscall"
"os"
)
func main() {
cmd := exec.Command("sh")
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)
}
}
可以看到,仅仅增加syscall.CLONE_NEWIPC代表我们希望创建IPC Namespace。下面,需要打开两个shell来演示隔离的效果。
首先在宿主机上打开一个shell。
这里,能够发现可以看到一个queue了。下面,使用另外一个shell去运行程序。
通过以上实验,可以发现,在新创建的Namespace里,看不到宿主机上已经创建的message queue,说明IPC Namespace创建成功,IPC已经被隔离。
PID Namespace
PID Namespace是用来隔离进程ID的。同样一个进程在不同的PID Namespace 里可以拥有不同的PID。这样就可以理解,在docker container 里面,使用ps -ef经常会发现,在容器内,前台运行的那个进程PID是1,但是在容器外,使用ps-ef会发现同样的进程却有不同的PID,这就是PID Namespace做的事情。
再修改一下代码,添加一个syscall.CLONE_NEWPID,代表为fork出来的子进程创建自己的PID Namespace。
package main
import (
"os/exec"
"log"
"syscall"
"os"
)
func main() {
cmd := exec.Command("sh")
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)
}
}
首先在宿主机上看一下进程树,找一下进程的真实PID :pstree -pl
可以看到,go main函数运行的PID为4499。下面,打开另外一个shell运行一下如下代码。
可以看到,该操作打印了当前Namespace的PID,其值为1。也就是说,这个4499的PID被映射到Namespace里后PID 为1。这里还不能使用ps来查看,因为ps和top等命令会使用/proc内容,具体内容在下面的Mount Namespace部分会进行讲解。
Mount Namespace
Mount Namespace用来隔离各个进程看到的挂载点视图。在不同Namespace的进程中,看到的文件系统层次是不一样的。在Mount Namespace中调用mount()和umount()仅仅只会影响当前Namespace内的文件系统,而对全局的文件系统是没有影响的。
看到这里,也许就会想到chroot()。它也是将某一个子目录变成根节点。但是,Mount Namespace不仅能实现这个功能,而且能以更加灵活和安全的方式实现。
Mount Namespace是Linux 第一个实现的Namespace 类型,因此,它的系统调用参数是NEWNS(New Namespace 的缩写)。当时人们貌似没有意识到,以后还会有很多类型的Namespace加入Linux大家庭。
继续改动代码:
package main
import (
"os/exec"
"log"
"syscall"
"os"
)
func main() {
cmd := exec.Command("sh")
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还是宿主机的,所以看到里面会比较乱,下面,将/proc mount到我们自己的Namespace下面来。
命令格式:mount -t type device dir
可以看到,在当前的Namespace中,sh 进程是PID 为1 的进程。这就说明,当前的Mount Namespace 中的mount 和外部空间是隔离的,mount 操作并没有影响到外部。Docker volume也是利用了这个特性。
User Namespace
User Namespace 主要是隔离用户的用户组ID。也就是说,一个进程的User ID 和Group ID在User Namespace内外可以是不同的。比较常用的是,在宿主机上以一个非root用户运行创建一个User Namespace,然后在User Namespace里面却映射成root 用户。这意味着,这个进程在User Namespace里面有root权限,但是在User Namespace外面却没有root的权限。从Linux Kernel 3.8开始,非root进程也可以创建User Namespace,并且此用户在Namespace里面可以被映射成root,且在Namespace内有root权限。
修改 username namespace 参数
vim /etc/sysctl.conf
user.max_user_namespaces=10000
sysctl -p
下面,继续以一个例子来描述,代码如下。
package main
import (
"os/exec"
"log"
"syscall"
"os"
)
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,
}
// cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(1)}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
os.Exit(-1)
}
在原来的基础上增加了syscall.CLONE_NEWUSER。首先,以root来运行这个程序,运行前在宿主机上看一下当前的用户和用户组,显示如下。
可以看到,它们的UID是不同的,因此说明User Namespace生效了。
Network Namespace
Network Namespace 是用来隔离网络设备、IP地址端口等网络栈的Namespace。Network Namespace可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定到自己的端口,每个Namespace内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口
代码的基础上增加syscall.CLONE_NEWNET标识符,如下。
package main
import (
"os/exec"
"log"
"syscall"
"os"
)
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)
}
首先,在宿主机上查看一下自己的网络设备,结果如下
可以看到,宿主机上有lo、ens33网络设备。下面,运行一下程序去Network Namespace里面看看。
我们发现,在Namespace里面什么网络设备都没有。这样就能断定Network Namespace与宿主机之间的网络是处于隔离状态了。
Linux Cgroups介绍
Linux容器的Namespace技术,它帮助进程隔离出自己单独的空间,但Docker是怎么限制每个空间的大小,保证它们不会互相争抢的呢?这就要用到Linux的Cgroups技术。
什么是Linux Cgroups
Linux Cgroups(Control Groups)提供了对一组进程及将来子进程的资源限制、控制和统计的能力,这些资源包括CPU、内存、存储、网络等。通过Cgroups,可以方便地限制某个进程的资源占用,并且可以实时地监控进程的监控和统计信息。
Cgroups中的3个组件
控制族群(Cgroup):
关联一组task和一组subsystem的配置参数。一个task对应一个进程, cgroup是资源分片的最小单位。cgroup是对进程分组管理的一种机制,cgroups中的资源控制都以cgroup为单位实现。cgroup表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个cgroup包含一组进程,并可以在这个cgroup上增加Linux subsystem的各种参数配置,将一组进程和一组subsystem的系统参数关联起来。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup。
子系统(Subsystem):
subsystem 是一组资源控制的模块,主要包含cpuset, cpu, cpuacct, blkio, memory, devices, freezer, net_cls, net_prio, perf_event, hugetlb, pids
- cpu 设置cgroup 中进程的CPU 被调度的策略
- cpuset 在多核机器上设置cgroup 中进程可以使用的CPU 和内存
- cpuacct 可以统计cgroup 中进程的CPU 占用
- memory 用于控制cgroup 中进程的内存占用
- blkio 设置对块设备(比如硬盘)输入输出的访问控制
- devices 控制cgroup 中进程对设备的访问
- freezer 用于挂起和恢复cgroup 中的进程
- net_cls 用于将cgroup 中进程产生的网络包分类,以便Linux 的tc (traffic contrroller)可以根据分类区分出来自某个cgroup 的包并做限流或监控
- net_prio 设置cgroup 中进程产生的网络流量的优先级
- ============================下面的熟悉即可=============================
- perf_event 增加了对每group的监测跟踪的能力,即可以监测属于某个特定的group的所有线程以及运行在特定CPU上的线程
- hugetlb 限制HugeTLB(huge translation lookaside buffer)的使用,TLB是MMU中的一块高速缓存(也是一种cache,是CPU内核和物理内存之间的cache),它缓存最近查找过的VA对应的页表项
- pids 限制cgroup及其所有子孙cgroup里面能创建的总的task数量
/sys/fs/cgroup 目录下列了各个subsystem资源管理器:
层级(hierarchy):
关联一个到多个subsystem
和一组树形结构的cgroup
。 和cgroup
不同,hierarchy
包含的是可管理的subsystem
而非具体参数把一组cgroup串成一个树状的结构,一个这样的树便是一个hierarchy。一个hierarchy可以理解为一棵cgroup树,树的每个节点就是一个进程组,每棵树都会与零到多个subsystem关联。一个系统可以有多个hierarchy。通过这种树状结构,cgroups可以做到继承。下面这个例子就是一颗hierarchy, 其中c0是其根节点,c1、c2是c0的子节点,会继承c0的限制,c1、c2可以在cgroup-test的基础上定制特殊的资源限制条件。
三个组件相互的关系
- 1、一个subsystem只能附加到一个hierarchy上面;
- 2、一个hierarcy可以附加多个subsystem;
- 3、一个进程可以作为多个cgroup的成员,但是这些cgroup必须不在同一个hierarchy上。在不同的hierarchy上可以拥有多个subsystem,进行多个条件限制;
- 4、一个进程fork出来的子进程开始时一定跟父进程在同一个cgroup中,后面可以根据需要将其移动到其他cgroup中;
- 5、创建好一个hierarchy后,系统中所有的进程都会加入这个hierarchy的根结点;这个cgroup根节点是hierarchy默认创建的
- 6、cgroups里面的task内放置遵循该cgroup的进行id(pid),同一个hierarchy里面,只能有一个。
三者概念:
内核是如何把 cgroups 对资源进行限制的配置有效的组织起来
内核使用 cgroup 结构体来表示一个控制群组对某一个或者某几个 cgroups 子系统的资源限制,cgroup 结构体可以组织成一颗树的形式,每一棵cgroup 结构体组成的树称之为一个 cgroups 层级结构。cgroups层级结构可以 attach 一个或者几个 cgroups 子系统,当前层级结构可以对其 attach 的 cgroups 子系统进行资源的限制。每一个 cgroups 子系统只能被 attach 到一个 cpu 层级结构中。
比如上图表示两个cgroups层级结构,每一个层级结构中是一颗树形结构,树的每一个节点是一个 cgroup 结构体(比如cpu_cgrp, memory_cgrp)。第一个 cgroups 层级结构 attach 了 cpu 子系统和 cpuacct 子系统, 当前 cgroups 层级结构中的 cgroup 结构体就可以对 cpu 的资源进行限制,并且对进程的 cpu 使用情况进行统计。 第二个 cgroups 层级结构 attach 了 memory 子系统,当前 cgroups 层级结构中的 cgroup 结构体就可以对 memory 的资源进行限制。
在每一个 cgroups 层级结构中,每一个节点(cgroup 结构体)可以设置对资源不同的限制权重。比如上图中 cgrp1 组中的进程可以使用60%的 cpu 时间片,而 cgrp2 组中的进程可以使用20%的 cpu 时间片。
内核是如何把进程与 cgroups 层级结构联系起来
在创建了 cgroups 层级结构中的节点(cgroup 结构体)之后,可以把进程加入到某一个节点的控制任务列表中,一个节点的控制列表中的所有进程都会受到当前节点的资源限制。
同时某一个进程也可以被加入到不同的 cgroups 层级结构的节点中,因为不同的 cgroups 层级结构可以负责不同的系统资源。
所以说进程和 cgroup 结构体是一个多对多的关系。
上面这个图从整体结构上描述了进程与 cgroups 之间的关系。最下面的P代表一个进程。
每一个进程的描述符中有一个指针指向了一个辅助数据结构css_set(cgroups subsystem set)。 指向某一个css_set的进程会被加入到当前css_set的进程链表中。一个进程只能隶属于一个css_set,一个css_set可以包含多个进程,隶属于同一css_set的进程受到同一个css_set所关联的资源限制。
上图中的”M×N Linkage”说明的是css_set通过辅助数据结构可以与 cgroups 节点进行多对多的关联。但是 cgroups 的实现不允许css_set同时关联同一个cgroups层级结构下多个节点。 这是因为 cgroups 对同一种资源不允许有多个限制配置。
一个css_set关联多个 cgroups 层级结构的节点时,表明需要对当前css_set下的进程进行多种资源的控制。而一个 cgroups 节点关联多个css_set时,表明多个css_set下的进程列表受到同一份资源的相同限制。
内核是如何通过 cgroups 文件系统把cgroups的功能暴露给用户态
Linux 使用了多种数据结构在内核中实现了 cgroups 的配置,关联了进程和 cgroups 节点,那么 Linux 又是如何让用户态的进程使用到 cgroups 的功能呢? Linux内核有一个很强大的模块叫 VFS (Virtual File System)。 VFS 能够把具体文件系统的细节隐藏起来,给用户态进程提供一个统一的文件系统 API 接口。 cgroups 也是通过 VFS 把功能暴露给用户态的,cgroups 与 VFS 之间的衔接部分称之为 cgroups 文件系统
VFS(Virtual File System)
VFS 使用了一种通用文件系统的设计,具体的文件系统只要实现了 VFS 的设计接口,就能够注册到 VFS 中,从而使内核可以读写这种文件系统。 这很像面向对象设计中的抽象类与子类之间的关系,抽象类负责对外接口的设计,子类负责具体的实现。其实,VFS本身就是用 c 语言实现的一套面向对象的接口。
通用文件模型
VFS 通用文件模型中包含以下四种元数据结构:
- 文件对象:一个文件对象表示进程内打开的一个文件,文件对象是存放在进程的文件描述符表里面的。同样这个文件中比较重要的部分是一个叫 file_operations 的结构体,这个结构体描述了具体的文件系统的读写实现。
- 目录项对象:在每个文件系统中,内核在查找某一个路径中的文件时,会为内核路径上的每一个分量都生成一个目录项对象,目录项对象一般会被缓存,从而提高内核查找速度
- 超级块对象:用于存放已经注册的文件系统的信息。比如ext2,ext3等这些基础的磁盘文件系统,还有用于读写socket的socket文件系统等
- 索引节点对象:用于存放具体文件的信息。对于一般的磁盘文件系统而言,inode 节点中一般会存放文件在硬盘中的存储块等信息;对于socket文件系统,inode会存放socket的相关属性,而对于cgroups这样的特殊文件系统,inode会存放与 cgroup 节点相关的属性信息。在此当中涉及inode_operations 的结构体,这个结构体定义了在具体文件系统中创建文件,删除文件等的具体实现
cgroups文件系统的实现
基于 VFS 实现的文件系统,都必须实现 VFS 通用文件模型定义的这些对象,并实现这些对象中定义的部分函数。cgroup 文件系统也不例外
例如:cgroups 文件系统类型的结构体:
这里面两个函数分别代表安装和卸载某一个 cgroup 文件系统所需要执行的函数。每次把某一个 cgroups 子系统安装到某一个装载点的时候,cgroup_mount 方法就会被调用,这个方法会生成一个 cgroups_root(cgroups层级结构的根)并封装成超级快对象。
由此可知:cgroups 通过实现 VFS 的通用文件系统模型,把维护 cgroups 层级结构的细节,隐藏在 cgroups 文件系统的这些实现函数中。
从另一个方面说,用户在用户态对 cgroups 文件系统的操作,通过 VFS 转化为对 cgroups 层级结构的维护。通过这样的方式,内核把 cgroups 的功能暴露给了用户态的进程。
cgroups使用方法
cgroups文件系统挂载
Linux中,用户可以使用mount命令挂载 cgroups 文件系统,格式为: mount -t cgroup -o subsystems name /cgroup/name,其中 subsystems 表示需要挂载的 cgroups 子系统, /cgroup/name 表示挂载点,这条命令同时在内核中创建了一个cgroups 层级结构。
比如挂载 cpuset, cpu, cpuacct, memory 4个subsystem到/cgroup/cpu_and_mem 目录下,就可以使用 mount -t cgroup -o remount,cpu,cpuset,memory cpu_and_mem /cgroup/cpu_and_mem
在centos下面,在使用yum install libcgroup安装了cgroups模块之后,在 /etc/cgconfig.conf 文件中会自动生成 cgroups 子系统的挂载点:
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;
}
上面的每一条配置都等价于展开的 mount 命令,例如mount -t cgroup -o cpuset cpuset /cgroup/cpuset。这样系统启动之后会自动把这些子系统挂载到相应的挂载点上。
子节点和进程
挂载某一个 cgroups 子系统到挂载点之后,就可以通过在挂载点下面建立文件夹或者使用cgcreate命令的方法创建 cgroups 层级结构中的节点。比如通过命令cgcreate -t sankuai:sankuai -g cpu:test就可以在 cpu 子系统下建立一个名为 test 的节点。
然后可以通过写入需要的值到 test 下面的不同文件,来配置需要限制的资源。每个子系统下面都可以进行多种不同的配置,需要配置的参数各不相同,详细的参数设置需要参考 cgroups 手册。使用 cgset 命令也可以设置 cgroups 子系统的参数,格式为 cgset -r parameter=value path_to_cgroup。
当需要删除某一个 cgroups 节点的时候,可以使用 cgdelete 命令,比如要删除上述的 test 节点,可以使用 cgdelete -r cpu:test命令进行删除
把进程加入到 cgroups 子节点也有多种方法,可以直接把 pid 写入到子节点下面的 task 文件中。也可以通过 cgclassify 添加进程,格式为 cgclassify -g subsystems:path_to_cgroup pidlist,也可以直接使用 cgexec 在某一个 cgroups 下启动进程,格式为gexec -g subsystems:path_to_cgroup command arguments.
cgroup 子资源参数详解
1、blkio:限制设备 IO 访问
限制磁盘 IO 有两种方式:权重(weight)和上限(limit)。权重是给不同的应用(或者 cgroup)一个权重值,各个应用按照百分比来使用 IO 资源;上限是直接写死应用读写速率的最大值。
设置 cgroup 访问设备的权重:
设置的权重并不能保证什么,当只有某个应用在读写磁盘时,不管它权重多少,都能使用磁盘。只有当多个应用同时读写磁盘时,才会根据权重为应用分配读写的速率。
blkio.weight:设置 cgroup 读写设备的权重,取值范围在 100-1000
blkio.weight_device:设置 cgroup 使用某个设备的权重。当访问该设备时,它会使用当前值,覆盖 blkio.weight 的值。内容的格式为 major:minor weight,前面是设备的 major 和 minor 编号,用来唯一表示一个设备,后面是 100-1000 之间的整数值。设备号的分配可以参考:https://www.kernel.org/doc/html/v4.11/admin-guide/devices.html
设置 cgroup 访问设备的限制:
除了设置权重之外,还能设置 cgroup 磁盘的使用上限,保证 cgroup 中的进程读写磁盘的速率不会超过某个值。
blkio.throttle.read_bps_device:最多每秒钟从设备读取多少字节
blkio.throttle.read_iops_device:最多每秒钟从设备中执行多少次读操作
blkio.throttle.write_bps_device:最多每秒钟可以往设备写入多少字节
blkio.throttle.write_iops_device:最多每秒钟可以往设备执行多少次写操作
读写字节数的限制格式一样 major:minor bytes_per_second,前面两个数字代表某个设备,后面跟着一个整数,代表每秒读写的字节数,单位为比特,如果需要其他单位(KB、MB等)需要自行转换。比如要限制 /dev/sda 读速率上线为 10 Mbps,可以运行:
echo "8:0 10485760" > /sys/fs/cgroup/blkio/mygroup/blkio.throttle.read_bps_device
iops 代表 IO per second,是每秒钟执行读写的次数,格式为 major:minor operations_per_second。比如,要限制每秒只能写 10 次,可以运行:
echo "8:0 10" > /sys/fs/cgroup/blkio/mygroup/blkio.throttle.write_iops_device
除了限制磁盘使用之外,blkio 还提供了 throttle 规则下磁盘使用的统计数据。
blkio.throttle.io_serviced:cgroup 中进程读写磁盘的次数,文件中内容格式为 major:minor operation number,表示对磁盘进行某种操作(read、write、sync、async、total)的次数
- blkio.throttle.io_service_bytes:和上面类似,不过这里保存的是操作传输的字节数
- blkio.reset_stats:重置统计数据,往该文件中写入一个整数值即可
- blkio.time:统计 cgroup 对各个设备的访问时间,格式为 major:minor milliseconds
- blkio.io_serviced:CFQ 调度器下,cgroup 对设备的各种操作次数,和 blkio.throttle.io_serviced 刚好相反,所有不是 throttle 下的请求
- blkio.io_services_bytes:CFQ 调度器下,cgroup 对各种设备的操作字节数
- blkio.sectors:cgroup 中传输的扇区次数,格式为 major:minor sector_count
- blkio.queued:cgroup IO 请求进队列的次数,格式为 number operation
- blkio.dequeue:cgroup 的 IO 请求被设备出队列的次数,格式为 major:minor number
- blkio.avg_queue_size:
- blkio.merged:cgroup 把 BIOS 请求合并到 IO 操作请求的次数,格式为 number operation
- blkio.io_wait_time:cgroup 等待队列服务的时间
- blkio.io_service_time:CFQ 调度器下,cgroup 处理请求的时间(从请求开始调度,到 IO 操作完成)
2、cpu:限制进程组 CPU 使用
CPU 子资源可以管理 cgroup 中任务使用 CPU 的行为,任务使用 CPU 资源有两种调度方式:完全公平调度(CFS,Completely Fair Scheduler)和 实时调度(RT,Real-Time Scheduler)。前者可以根据权重为任务分配响应的 CPU 时间片,后者能够限制使用 CPU 的核数。
CFS 调优参数:
CFS 调度下,每个 cgroup 都会分配一个权重,但是这个权重并不能保证任务使用 CPU 的具体数据。如果只有一个进程在运行(理论上,现实中机器上不太可能只有一个进程),不管它所在 cgroup 对应的 CPU 权重是多少,都能使用所有的 CPU 资源;在 CPU 资源紧张的情况,内核会根据 cgroup 的权重按照比例分配给任务各自使用 CPU 的时间片。
CFS 调度模式下,也可以给 cgroup 分配一个使用上限,限制任务能使用 CPU 的核数。
设置 CPU 数字的单位都是微秒(microsecond),用 us 表示。
- cpu.cfs_quota_us:每个周期 cgroup 中所有任务能使用的 CPU 时间,默认为 -1,表示不限制 CPU 使用。需要配合 cpu.cfs_period_us 一起使用,一般设置为 100000(docker 中设置的值)
- cpu.cfs_period_us:每个周期中 cgroup 任务可以使用的时间周期,如果想要限制 cgroup 任务每秒钟使用 0.5 秒 CPU,可以在 cpu.cfs_quota_us 为 100000 的情况下把它设置为 50000。如果它的值比 cfs_quota_us 大,表明进程可以使用多个核 CPU,比如 200000 表示进程能够使用 2.0 核
- cpu.stat:CPU 使用的统计数据,nr_periods 表示已经过去的时间周期;nr_throttled 表示 cgroup 中任务被限制使用 CPU 的次数(因为超过了规定的上限);throttled_time 表示被限制的总时间
- cpu.shares:cgroup 使用 CPU 时间的权重值。如果两个 cgroup 的权重都设置为 100,那么它们里面的任务同时运行时,使用 CPU 的时间应该是一样的;如果把其中一个权重改为 200,那么它能使用的 CPU 时间将是对方的两倍。
RT 调度模式下的参数:
RT 调度模式下和 CFS 中上限设置类似,区别是它只是限制实时任务的 CPU。
- cpu.rt_period_us:设置一个周期时间,表示多久 cgroup 能够重新分配 CPU 资源
- cpu.rt_runtime_us:设置运行时间,表示在周期时间内 cgroup 中任务能访问 CPU 的时间。这个限制是针对单个 CPU 核数的,如果是多核,需要乘以对应的核数
3、cpuacct(CPU accounting): 任务使用 CPU 情况统计
cpuacct 不做任何资源限制,它的功能是资源统计,自动地统计 cgroup 中任务对 CPU 资源的使用情况,统计数据也包括子 cgroup 中的任务。
- cpuacct.usage:该 cgroup 中所有任务(包括子 cgroup 中的任务,下同)总共使用 CPU 的时间,单位是纳秒(ns)。往文件中写入 0 可以重置统计数据
- cpuacct.stat:该 cgroup 中所有任务使用 CPU 的user 和 system 时间,也就是用户态 CPU 时间和内核态 CPU 时间
- cpuacct.usage_percpu:该 cgroup 中所有任务使用各个 CPU 核数的时间,单位为纳秒(ns)
4、cpuset: cpu 绑定
除了限制 CPU 的使用量,cgroup 还能把任务绑定到特定的 CPU,让它们只运行在这些 CPU 上,这就是 cpuset 子资源的功能。除了 CPU 之外,还能绑定内存节点(memory node)。在把任务加入到 cpuset 的 task 文件之前,用户必须设置 cpuset.cpus 和 cpuset.mems 参数。
- cpuset.cpus:设置 cgroup 中任务能使用的 CPU,格式为逗号(,)隔开的列表,减号(-)可以表示范围。比如,0-2,7 表示 CPU 第 0,1,2,和 7 核。
- cpuset.mems:设置 cgroup 中任务能使用的内存节点,和 cpuset.cpus 格式一样
上面两个是最常用的参数,cpuset 中有很多其他参数,需要对 CPU 调度机制有深入的了解,很少用到
5、memory:限制内存使用
控制内存使用:
- memory.limit_in_bytes:cgroup 能使用的内存上限值,默认为字节;也可以添加 k/K、m/M 和 g/G 单位后缀。往文件中写入 -1 来移除设置的上限,表示不对内存做限制
- memory.memsw.limit_in_bytes:cgroup 能使用的内存加 swap 上限,用法和上面一样。写入 -1 来移除上限
- memory.failcnt:任务使用内存量达到 limit_in_bytes 上限的次数
- memory.memsw.failcnt:任务使用内存加 swap 量达到 memsw.limit_in_bytes 上限的次数
- memory.soft_limit_in_bytes:设置内存软上限。如果内存充足, cgroup 中的任务可以用到 memory.limit_in_bytes 设定的内存上限;当时当内存资源不足时,内核会让任务使用的内存不超过 soft_limit_in_bytes 中的值。文件内容的格式和 limit_in_bytes 一样
- memory.swappiness:设置内核 swap out 进程内存(而不是从 page cache 中回收页) 的倾向。默认值为 60,低于 60 表示降低倾向,高于 60 表示增加倾向;如果值高于 100,表示允许内核 swap out 进程地址空间的页。如果值为 0 表示倾向很低,而不是禁止该行为。
OOM 操作:
OOM 是 out of memory 的缩写,可以翻译成内存用光。cgroup 可以控制内存用完之后应该怎么处理进程,默认情况下,用光内存的进程会被杀死。
memory.oom_control:是否启动 OOM killer,如果启动(值为 0,是默认情况)超过内存限制的进程会被杀死;如果不启动(值为 1),使用超过限定内存的进程不会被杀死,而是被暂停,直到它释放了内存能够被继续使用。
统计内存使用情况:
- memory.stat:汇报内存的使用情况,里面的数据包括:
cache:页缓存(page cache)字节数,包括 tmpfs(shmem)
rss:匿名和 swap cache 字节数,不包括 tmpfs
mapped_file:内存映射(memory-mapped)的文件大小,包括 tmpfs,单位是字节
pgpgin: paged into 内存的页数
pgpgout:paged out 内存的页数
swap:使用的 swap 字节数
active_anon:活跃的 LRU 列表中匿名和 swap 缓存的字节数,包括 tmpfs
inactive_anon:不活跃的 LRU 列表中匿名和 swap 缓存的字节数,包括 tmpfs
active_file:活跃 LRU 列表中文件支持的(file-backed)的内存字节数
inactive_file:不活跃列表中文件支持的(file-backed)的内存字节数
unevictable:不可以回收的内存字节数
- memory.usage_in_bytes:cgroup 中进程当前使用的总内存字节数
- memory.memsw.usage_in_bytes:cgroup 中进程当前使用的总内存加上总 swap 字节数
- memory.max_usage_in_bytes:cgroup 中进程使用的最大内存字节数
- memory.memsw.max_usage_in_bytes:cgroup 中进程使用的最大内存加 swap 字节数
5、net_cls:为网络报文分类
cgroup 本身并不提供对网络资源的使用控制,只能添加简单的标记和优先级,具体的控制需要借助 linux 的 TC 模块来实现。
net_cls 子资源能够给网络报文打上一个标记(classid),这样内核的 tc(traffic control)模块就能根据这个标记做流量控制。
net_cls.classid:包含一个整数值。从文件中读取的是十进制,写入的时候需要是十六进制。比如,0x100001 写入到文件中,读取的将是 1048577, ip 命令操作的形式为 10:1。
这个值的格式为 0xAAAABBBB,一共 32 位,分成前后两个部分,前置的 0 可以忽略,因此 0x10001 和 0x00010001 一样,表示为 1:1。
6、net_prio:网络报文优先级
net_prio(Network Priority)子资源能够动态设置 cgroup 中应用在网络接口的优先级。网络优先级是报文的一个属性值,tc可以设置网络的优先级,socket 也可以通过 SO_PRIORITY 选项设置它(但是很少应用会这么做)。
- net_prio.prioidx:只读文件,里面包含了一个整数值,内核用来标识这个 cgroup
- net_prio.ifpriomap:网络接口的优先级,里面可以包含很多行,用来为从网络接口中发出去的报文设置优先级。每行的格式为 network_interface priority,比如 echo "eth0 5" > /sys/fs/cgroup/net_prio/mycgroup/net_prio.ifpriomap
7、devices:设备黑白名单
device 子资源可以允许或者阻止 cgroup 中的任务访问某个设备,也就是黑名单和白名单的作用。
- devices.allow:cgroup 中的任务能够访问的设备列表,格式为 type major:minor access,
type 表示类型,可以为 a(all), c(char), b(block)
major:minor 代表设备编号,两个标号都可以用* 代替表示所有,比如 *:* 代表所有的设备
accss 表示访问方式,可以为 r(read),w(write), m(mknod) 的组合
- devices.deny:cgroup 中任务不能访问的设备,和上面的格式相同
- devices.list:列出 cgroup 中设备的黑名单和白名单
8、freezer
freezer 子资源比较特殊,它并不和任何系统资源相关,而是能暂停和恢复 cgroup 中的任务。
- freezer.state:这个文件值存在于非根 cgroup 中(因为所有的任务默认都在根 cgroup 中,停止所有的任务显然是错误的行为),里面的值表示 cgroup 中进程的状态:
FROZEN:cgroup 中任务都被挂起(暂停)
FREEZING:cgroup 中任务正在被挂起的过程中
THAWED:cgroup 中的任务已经正常恢复
- 要想挂起某个进程,需要先把它移动到某个有 freezer 的 cgroup 中,然后 Freeze 这个 cgroup。
- 如果某个 cgroup 处于挂起状态,不能往里面添加任务。用户可以写入 FROZEN 和 THAWED 来控制进程挂起和恢复,FREEZING 不受用户控制。
实验
基本操作
1. 首先,要创建并挂载一个hierarchy(cgroup树)
创建一颗cgroup树关联所有subsystem,并挂载在/sys/fs/cgroup下(xxx为cgroup树名称)
sudo mount -t cgroup xxx /sys/fs/cgroup
也可以不关联任何subsystem,挂载其他目录也可以,比如名叫demo树挂载在~/test_aa/demo目录:
sudo mount -t cgroup -o none,name=demo demo ~/demo/
- cgroup.clone_children,cpuset的subsystem会读取这个配置文件,如果这个值是1(默认是0),子cgroup才会继承父cgroup的cpuset的配置。
- notify_on_release和release_agent会一起使用。notify_on_release标识当这个cgroup最后一个进程退出的时候是否执行了release_agent;release_agent则是一个路径,通常用作进程退出之后自动清理掉不再使用的cgroup。
- cgroup.procs :当前cgroup中的所有进程ID
- tasks :当前cgroup中的所有线程ID,如果把一个进程ID写到tasks文件中,便会将相应的进程加入到这个cgroup中。
2. 建立子Cgroup
然后,创建刚刚创建好的hierarchy上cgroup根节点中扩展出的两个子cgroup。
hierarchy上cgroup根节点中扩展出的两个子cgroup
可以看到,在一个cgroup的目录下创建文件夹时,Kernel会把文件夹标记为这个cgroup的子cgroup,它们会继承父cgroup的属性。
3. 添加进程进入Cgroup
一个进程在一个Cgroups的hierarchy中,只能在一个cgroup节点上存在,系统的所有进程都会默认在根节点上存在,可以将进程移动到其他cgroup节点,只需要将进程ID写到移动到的cgroup节点的tasks文件中即可。
sh -c "echo $$ >> tasks" # 将我所在的终端进程移动到cgroup-1中
可以看到,当前的53642进程已经被加到demo:/cgroup-1中了。
4.通过subsystem限制cgroup中进程的资源
在上面创建hierarchy的时候,这个hierarchy并没有关联到任何的subsystem,所以没办法通过那个hierarchy中的cgroup节点限制进程的资源占用,其实系统默认已经为每个subsystem创建了一个默认的hierarchy,比如memory的hierarchy。
可以看到,/sys/fs/cgroup/memory目录便是挂在了memory subsystem的hierarchy上。下面,就通过在这个hierarchy中创建cgroup,限制如下进程占用的内存。
首先,在不做限制的情况下启动一个占用内存的 stress 进程,top/htop监控内存使用情况
创建一个 cgroup
cd /sys/fs/cgroup/memory
mkdir test-limit-memory && cd test-limit-memory
设置最大 cgroup 的最大内存占用为 100MB
sh -c "echo "100m" > memory.limit_in_bytes"
将当前进程移动到这个 cgroup 中
再次运行占用内存 200MB 的 stress 进程
stress --vm-bytes 200m --vm-keep -m 1
由此可知,虽然申请的虚拟内存(VIRT)为200M,但是实际内存(RES)只有 100M,通过cgroup,我们成功地将stress进程的最大内存占用限制到了100MB。
日常CPU内存限制用法
1. 限制CPU使用率
创建一个控制组,并设置CPU使用进行限制
sudo cgcreate -g cpu:cpu_limit
echo 10000 > /sys/fs/cgroup/cpu/cpu_limit/cpu.cfs_quota_us
echo 200000 > /sys/fs/cgroup/cpu/cpu_limit/cpu.cfs_period_us
(上述表示当前组可占用cpu 10000微秒内核时间,然后让出时间并等待200000微秒,等内核时间过后再占用,以达到限制CPU使用的目地)
将进程添加到控制组中:
sudo cgclassify -g cpu:cpu_limit PID
2. 限制内存使用量
创建一个控制组,并设置内存使用量限制为300MB:
sudo cgcreate -g memory:mem_limit
echo $((300 * 1024 * 1024)) > /sys/fs/cgroup/memory/mem_limit/memory.limit_in_bytes
将进程添加到控制组中:
sudo cgclassify -g memory:mem_limit PID
限制磁盘I/O速率
blkio cgroup的虚拟文件系统挂载点/sys/fs/cgroup/blkio/,包含以下参数:
- blkio.throttle.read_iops_device:读IOPS限制;
- blkio.throttle.read_bps_device:读吞吐量限制;
- blkio.throttle.write_iops.device:写IOPS限制;
- blkio.throttle.write_bps_device:写吞吐量限制;
创建一个控制组,并设置磁盘I/O限制为10MB/s:
sudo cgcreate -g blkio:io_limit
echo "8:0 $((10 *1024 * 1024))" > /sys/fs/cgroup/blkio/io_limit/blkio.throttle.read_bps_device
将进程添加到控制组中:
sudo cgclassify -g blkio:io_limit PID
限制网络带宽
创建一个控制组,并设置网络带宽限制为1Mbps:
sudo cgcreate -g net_cls:net_limit
echo 0x10000 > /sys/fs/cgroup/net_cls/net_limit/net_cls.classid
使用tc命令配置网络限制:
sudo tc qdisc add dev eth0 root handle 1: htb
sudo tc class add dev eth0 parent 1: classid 1:1 htb rate 1mbit
sudo tc filter add dev eth0 parent 1: protocol ip prio 1 handle 1: cgroup
将进程添加到控制组中:
sudo cgclassify -g net_cls:net_limit PID
Cgroup嵌套使用
Cgroup支持嵌套使用,即在一个控制组内创建子控制组。例如,可以创建一个名为parent_group的控制组,并在其中创建两个子控制组child_group1和child_group2:
sudo cgcreate -g cpu,memory:parent_group
sudo cgcreate -g cpu,memory:parent_group/child_group1
sudo cgcreate -g cpu,memory:parent_group/child_group2
可以为每个子控制组分别设置资源限制和优先级
注意事项
使用Cgroup时,避免过度限制资源,否则可能导致进程性能下降或无法正常运行。
删除控制组之前,确保控制组内的所有进程已经退出,否则可能导致资源泄露。
使用Cgroup进行资源监控时,可以定期读取状态文件,以便及时发现和处理潜在的问题。
使用Cgroup进行优先级调整时,注意权衡各个控制组之间的资源分配,避免出现资源竞争等相关情况。
Docker是如何使用Cgroups的
我们知道Docker是通过Cgroups实现容器资源限制和监控的,下面以一个实际的容器实例来看一下Docker是如何配置Cgroups的。
docker run -m 设置内存限制
docker run -it -m 128m ubuntu
docker会为每个容器在系统的 hierarchy 中创建 cgroup
如果运行在后台,宿主机查看
可以看到,Docker通过为每个容器创建cgroup,并通过cgroup去配置资源限制和资源监控。
用Go语言实现通过cgroup限制容器的资源
下面,在上节Namespace容器demo的基础上,加上cgroup的限制,实现一个demo,使其能够具有限制容器内存的功能。
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
"syscall"
)
const (
// 挂载了 memory subsystem的hierarchy的根目录位置
cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
)
func main() {
// /proc 文件夹下放置着系统运行时的信息。并不存在于磁盘上,而只是存在于内存中。只不过以文件系统的形式展现出来。
if os.Args[0] == "/proc/self/exe" { // os.Args[0] 当前程序名 "/proc/self/exe"链接到进程的执行命令,代表当前程序
//容器进程
fmt.Printf("current pid %d \n", syscall.Getpid())
cmd := exec.Command("sh", "-c", "stress --vm-bytes 200m --vm-keep -m 1")
// sh -c 命令,它可以让 bash 将一个字串作为完整的命令来执行,这样就可以将 sudo 的影响范围扩展到整条命令。
// stress linux压力测试工具 --vm-bytes B malloc B bytes per vm worker (default is 256MB)
// --vm-keep redirty memory instead of freeing and reallocating
cmd.SysProcAttr = &syscall.SysProcAttr{} // syscall.SysProcAttr用于控制进程的相关属性
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
cmd := exec.Command("/proc/self/exe")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
panic(err)
}
// 得到 fork出来进程映射在外部命名空间的pid
fmt.Printf("pid:%+v", cmd.Process.Pid)
fmt.Println()
// 创建子cgroup
newCgroup := path.Join(cgroupMemoryHierarchyMount, "cgroup-demo-memory")
if err := os.Mkdir(newCgroup, 0755); err != nil {
panic(err)
}
// 将容器进程放到子cgroup中
if err := ioutil.WriteFile(path.Join(newCgroup, "tasks"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644); err != nil {
panic(err)
}
// 限制cgroup的内存使用
if err := ioutil.WriteFile(path.Join(newCgroup, "memory.limit_in_bytes"), []byte("100m"), 0644); err != nil {
panic(err)
}
cmd.Process.Wait()
}
pstree -p
通过对Cgroups虚拟文件系统的配置,将容器中stress进程的内存占用限制到了100MB
发布者:LJH,转发请注明出处:https://www.ljh.cool/39251.html