############
学习之docker##
############
http://www.katacoda.com/courses/container-runtimes/what-is-a-container
流程
容器只是应用了额外配置的普通linux进程
创建一个容器
docker run -itd --name=db redis:alpine
docker容器启动一个名字为redis-server.从主机上,我们可以看到所有正在运行的进程,包括由docker启动的进程
ps aux |grep redis-server
docker可以通过一下方式帮助我们识别进程的信息,包括pid(进程id)和ppid(父进程)
docker top db
谁是ppid使用ps aux |grep ppid寻父进程, 可能是containerd
该命令pstree将列出所有子进程,使用查看docker进程树
pstree -c -p -A $(pgrep dockerd)
从linux的角度来看,这些是标准进程,与我们系统上的其他进程具有相同的属性
进程目录
linux只是一些列神奇的的文件和内容,这使得探索和导航查看幕后发生的事情变得有趣,并且在某些情况下,更改内容以查看结果
每个进程的配置都在/proc目录中定义,如果知道进程id,则可以识别配置目录
每个进程都有不通文件中定义的自己的配置和安全设置
命名空间
容器的基本部分之一是命令空间,命名空间的概念是限制哪些进程可以看到和访问系统的某些部分,
当容器启动时,容器运行时将创建新的命名空间来容器化进程,通过在它自己的pid命名空间中运行一个进程,它看起来像是系统上唯一的进程
可用的命名空间是:
Mount (mnt)
Process ID (pid)
Network (net)
Interprocess Communication (ipc)
UTS (hostnames)
User ID (user)
Control group (cgroup)
Unshare 可以启动“包含”的进程
不使用 Docker 等运行时,进程仍然可以在它自己的命名空间内运行。一种帮助工具是取消共享。
unshare --help
使用 unshare 可以启动一个进程并让它创建一个新的命名空间,例如 Pid。通过与主机取消共享 Pid 命名空间,看起来 bash 提示符是机器上运行的唯一进程。
unshare --fork --pid --mount-proc bash
ps
exit
当我们共享一个命名空间时会发生什么?
在幕后,命名空间是磁盘上的 inode 位置。这允许进程共享/重用相同的命名空间,允许它们查看和交互。
列出所有命名空间
ls -lha /proc/$DBPID/ns/
另一个工具 NSEnter 用于将进程附加到现有的命名空间。用于调试目的。
nsenter --help
nsenter --target $DBPID --mount --uts --ipc --net --pid ps aux
使用 Docker,可以使用语法共享这些命名空间container:<container-name>。例如,下面的命令会将 nginx 连接到 DB 命名空间。
docker run -d --name=web --net=container:db nginx:alpine
WEBPID=$(pgrep nginx | tail -n1)
echo nginx is $WEBPID
cat /proc/$WEBPID/cgroup
虽然网络已被共享,但它仍将被列为命名空间。
ls -lha /proc/$WEBPID/ns/
但是,两个进程的 net 命名空间指向相同的位置。
ls -lha /proc/$WEBPID/ns/ | grep net
ls -lha /proc/$DBPID/ns/ | grep net
chroot
容器进程的一个重要部分是能够拥有独立于主机的不同文件。这就是我们如何根据系统上运行的不同操作系统拥有不同的 Docker 镜像。
Chroot 使进程能够以与父操作系统不同的根目录启动。这允许不同的文件出现在根目录中。
Cgroups(控制组)
CGroups 限制进程可以消耗的资源量。这些 cgroup 是在 /proc 目录中的特定文件中定义的值。
要查看映射,请运行以下命令:
cat /proc/$DBPID/cgroup
这些映射到磁盘上的其他 cgroup 目录:
ls /sys/fs/cgroup/
进程的 CPU 统计信息是什么?
CPU 统计信息和使用情况也存储在一个文件中!
cat /sys/fs/cgroup/cpu,cpuacct/docker/$DBID/cpuacct.stat
CPU 份额限制也在此处定义。
cat /sys/fs/cgroup/cpu,cpuacct/docker/$DBID/cpu.shares
用于容器内存配置的所有 Docker cgroup 都存储在:
ls /sys/fs/cgroup/memory/docker/
每个目录都根据 Docker 分配的容器 ID 进行分组。
DBID=$(docker ps --no-trunc | grep 'db' | awk '{print $1}')
WEBID=$(docker ps --no-trunc | grep 'nginx' | awk '{print $1}')
ls /sys/fs/cgroup/memory/docker/$DBID
如何配置 cgroup?
Docker 的特性之一是能够控制内存限制。这是通过 cgroup 设置完成的。
默认情况下,容器对内存没有限制。我们可以通过 docker stats 命令查看。
docker stats db --no-stream
内存引用存储在一个名为memory.limit_in_bytes.
通过写入文件,我们可以更改进程的限制限制。
echo 8000000 > /sys/fs/cgroup/memory/docker/$DBID/memory.limit_in_bytes
如果您读回文件,您会注意到它已转换为 7999488。
cat /sys/fs/cgroup/memory/docker/$DBID/memory.limit_in_bytes
再次查看Docker Stats时,进程的内存限制现在为7.629M -
docker stats db --no-stream
Seccomp / AppArmor
Linux 的所有操作都是通过系统调用完成的。内核有 330 个系统调用,可以执行读取文件、关闭句柄和检查访问权限等操作。所有应用程序都使用这些系统调用的组合来执行所需的操作。
AppArmor 是一个应用程序定义的配置文件,它描述了一个进程可以访问系统的哪些部分。
可以通过以下方式查看分配给进程的当前 AppArmor 配置文件
cat /proc/$DBPID/attr/current
Docker 的默认 AppArmor 配置文件是
docker-default (enforce).
在 Docker 1.13 之前,它将 AppArmor Profile 存储在 /etc/apparmor.d/docker-default(在 Docker 启动时被覆盖,因此用户无法修改它。在 v1.13 之后,Docker 现在在 tmpfs 中生成 docker-default , 使用 apparmor_parser 将其加载到内核中,然后删除该文件
模板可以在:
https://github.com/moby/moby/blob/a575b0b1384b2ba89b79cbd7e770fbeb616758b3/profiles/apparmor/template.go找到
Seccomp 提供了限制可以进行哪些系统调用的能力,阻止安装内核模块或更改文件权限等方面。
可以在
https://github.com/moby/moby/blob/a575b0b1384b2ba89b79cbd7e770fbeb616758b3/profiles/seccomp/default.json 中找到默认允许的 Docker 调用
当分配给一个进程时,这意味着该进程将被限制为能力系统调用的一个子集。如果它尝试调用被阻止的系统调用,则会收到错误“不允许操作”。
SecComp 的状态也在文件中定义。
cat /proc/$DBPID/status
cat /proc/$DBPID/status | grep Seccomp
标志的含义是: 0:禁用 1:严格 2:过滤
能力是关于进程或用户有权做什么的分组。这些功能可能涵盖多个系统调用或操作,例如更改系统时间或主机名。
状态文件还包含 Capabilities 标志。一个进程可以删除尽可能多的 Capabilities 以确保它的安全。
cat /proc/$DBPID/status | grep ^Cap
标志存储为位掩码,可以用 capsh
capsh --decode=00000000a80425fb
#############################################################################
docker基于Namespce和Cgroups
Namespace主要用于隔离资源
Cgroups用来提供对一组进程以及将来子进程的资源限制
#############################################################################
学习之linux系统中的namespace
#namespace 的概念
namespace 是 Linux 内核用来隔离内核资源的方式。通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。
#namespace 的用途
Linux 内核实现 namespace 的一个主要目的就是实现轻量级虚拟化(容器)服务。在同一个 namespace 下的进程可以感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,认为自己置身于一个独立的系统中,从而达到隔离的目的。也就是说 linux 内核提供的 namespace 技术为 docker 等容器技术的出现和发展提供了基础条件。
我们可以从 docker 实现者的角度考虑该如何实现一个资源隔离的容器。比如是不是可以通过 chroot 命令切换根目录的挂载点,从而隔离文件系统。为了在分布式的环境下进行通信和定位,容器必须要有独立的 IP、端口和路由等,这就需要对网络进行隔离。同时容器还需要一个独立的主机名以便在网络中标识自己。接下来还需要进程间的通信、用户权限等的隔离。最后,运行在容器中的应用需要有进程号(PID),自然也需要与宿主机中的 PID 进行隔离。也就是说这六种隔离能力是实现一个容器的基础,让我们看看 linux 内核的 namespace 特性为我们提供了什么样的隔离能力:
#namespace 的发展历史
Linux 在很早的版本中就实现了部分的 namespace,比如内核 2.4 就实现了 mount namespace。大多数的 namespace 支持是在内核 2.6 中完成的,比如 IPC、Network、PID、和 UTS。还有个别的 namespace 比较特殊,比如 User,从内核 2.6 就开始实现了,但在内核 3.8 中才宣布完成。同时,随着 Linux 自身的发展以及容器技术持续发展带来的需求,也会有新的 namespace 被支持,比如在内核 4.6 中就添加了 Cgroup namespace。
Linux 提供了多个 API 用来操作 namespace,它们是 clone()、setns() 和 unshare() 函数,为了确定隔离的到底是哪项 namespace,在使用这些 API 时,通常需要指定一些调用参数:CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、CLONE_NEWUTS 和 CLONE_NEWCGROUP。如果要同时隔离多个 namespace,可以使用 | (按位或)组合这些参数。同时我们还可以通过 /proc 下面的一些文件来操作 namespace。
#####################################################################################################################
学习之linux系统中的cgroup
Cgrougps的三个组件
控制组: 一个cgroups包含一组进程,并可以在这个cgroups上增加Linux subsystem(子系统)的各种参数配置,将一组进程和一组subsystem关联起来。
subsystem: (子系统) 是一组资源控制模块,可以通过lssubsys -a命令查看当前内核支持哪些subsystem。
cpuset
cpu,cpuacct
blkio
memory
devices
freezer
net_cls,net_prio
perf_event
hugetlb
pids
rdma
subsystem作用于hierarchy的cgroup节点,并控制节点中进程的资源占用。
hierarchy(层次): (层级树) 主要功能是把cgroups串成一个树型结构,使cgruops可以做到继承。也就是说将cgroup通过树状结构串起来,通过虚拟文件系统的方式暴露给用户。
三个组件之前的关系
cgroup中的组件是相互关联的
系统创建新的hierarchy之后,系统中所有的进程都会加入这个hierarchy的cgroup的根节点,这个cgroup根节点是hierarchy默认创建的,在这个hierarchy中创建的所有cgroup都是这个cgroup根节点的子节点。
一个subsystem只能附加到一个hierarchy上
一个hierarchy可以附加多个subsystem
一个进程可以作为多个cgroup的成员,但是这些cgroup必须在不同的hierarchy下
一个进程fork出子进程时,子进程和父进程是在同一个cgroup中的,根据需要也可以移动到其他的cgroup中
使用Cgroup
我们知道kernel是通过一个虚拟树状文件系统来配置Cgroups的。我们首先需要创建并挂载一个hierarchy(cgroup树)。即先mkdir后mount。
mount -t cgroup -o none,name=cgroup-test cgroup-test cgroup-test/
可以看到挂载后系统在目录下生成了一系列文件,这些文件其实就是根节点文件的配置项。
创建子cgroup
创建cgroup-1,我们可以看到在cgroup文件夹下再创建文件夹,系统会将这个文件夹也标记为cgroup,并且它是上一个cgroup的子cgroup,它会继承父cgroup的属性
进入到cgroup-test目录下
mkdir cgroup-1
移动进程
将终端进程移动到cgroup-1(只要将进程ID写到相应的cgroup的tasks文件)
readlnh@readlnh-Inspiron-3542:~/cgroup-test/cgroup-1$ echo $$
readlnh@readlnh-Inspiron-3542:~/cgroup-test/cgroup-1$ cat /proc/10644/cgroup
readlnh@readlnh-Inspiron-3542:~/cgroup-test/cgroup-1$ sudo sh -c "echo $$ >> tasks"
readlnh@readlnh-Inspiron-3542:~/cgroup-test/cgroup-1$ cat /proc/10644/cgroup
可以看到现在终端进程已经在cgroup-1了
通过subsystem限制cgroups中的进程资源
之前我们创建的cgroup是没有和任何subsystem相关联的,所以没法通过hierarchy中的cgroup节点限制资源。实际系统中已经默认为每个subsystem创建了一个默认的hierarchy,我们这里就直接在这个hierarchy下创建cgroup
readlnh@readlnh-Inspiron-3542:~/cgroup-test/test-limit-memory$ mount | grep memory
readlnh@readlnh-Inspiron-3542:~/cgroup-test/test-limit-memory$ cd /sys/fs/cgroup/memory/
readlnh@readlnh-Inspiron-3542:/sys/fs/cgroup/memory$ sudo mkdir test-limit-memory
readlnh@readlnh-Inspiron-3542:/sys/fs/cgroup/memory$ cd test-limit-memory/
readlnh@readlnh-Inspiron-3542:/sys/fs/cgroup/memory/test-limit-memory$ ls
readlnh@readlnh-Inspiron-3542:/sys/fs/cgroup/memory/test-limit-memory$ stress --vm-bytes 200m --vm-keep -m 1
stress: info: [15357] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
用top查看,发现内存为100m,实际上这里限制内存就是一个向memory_limit_in_bytes文件中写入100m这个操作,可以说相简单了
从这里我们就可以猜测,docker实际上就是通过go语言挂载,创建cgroups再向相应文件写入相应条件来限制容器资源的。cgroup确实是一个很有意思也很方便的功能。