monsterooo / blog

and make promises by the hours
MIT License
14 stars 1 forks source link

docker入门之什么是container #38

Open monsterooo opened 4 years ago

monsterooo commented 4 years ago

进程(process)

容器(container)只是应用了附加配置的普通Linux进程。启动以下的Redis容器,我们可以用来了解幕后的情况。

docker run -d --name=db redis:alpine

Docker容器启动一个名为redis-server的进程。我们可以查看所有正在运行的进程,包括Docker启动的进程。

ps aux | grep redis-server

Docker可以通过以下方式帮助我们识别有关进程的信息,包括进程ID(PID)、父进程ID(PPID)。

docker top db

上面命令输出如下:

$ docker top db
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
999                 1109                1094                0                   15:06               ?                   00:00:00            redis-server

父进程ID是什么呢?使用ps aux | grep <ppid>找到父进程ID,有可能是容器(Containerd)。

这里我输入ps aux | grep 1094,得到如下结果:

root      1094  0.0  0.7   8924  7860 ?        Sl   15:06   0:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0 -address /var/run/docker/containerd/docker-containerd.sock -containerd-binary /usr/bin/docker-containerd -runtime-root /var/run/docker/runtime-runc -debug
root      1197  0.0  0.0  14220   928 pts/0    S+   15:10   0:00 grep --color=auto 1094

pstree命令将会列出所有子进程,使用pstree -c -p -A $(pgrep dockerd)查看Docker进程树

输出结果如下:

dockerd(677)-+-docker-containe(718)-+-docker-containe(1094)-+-redis-server(1109)-+-{redis-server}(1139)
             |                      |                       |                    |-{redis-server}(1140)
             |                      |                       |                    `-{redis-server}(1141)
             |                      |                       |-{docker-containe}(1095)
             |                      |                       |-{docker-containe}(1096)
             |                      |                       |-{docker-containe}(1097)
             |                      |                       |-{docker-containe}(1098)
             |                      |                       |-{docker-containe}(1100)
             |                      |                       `-{docker-containe}(1101)
             |                      |-{docker-containe}(721)
             |                      |-{docker-containe}(722)
             |                      |-{docker-containe}(723)
             |                      |-{docker-containe}(730)
             |                      |-{docker-containe}(738)
             |                      |-{docker-containe}(740)
             |                      |-{docker-containe}(755)
             |                      `-{docker-containe}(757)
             |-{dockerd}(704)
             |-{dockerd}(705)
             |-{dockerd}(706)
             |-{dockerd}(717)
             |-{dockerd}(719)
             |-{dockerd}(737)
             |-{dockerd}(739)
             |-{dockerd}(741)
             |-{dockerd}(748)
             `-{dockerd}(1093)

你亲眼所见了吧,从Linux角度来看,这些是标准进程,并且与我们系统上的其他进程具有相同的属性。

进程目录(Process Directory)

Linux只是一系列不可意思的文件和内容,这使得探索了解幕后发生的事情变得很有意思。

每个进程的配置在/proc目录中定义。如果知道进程ID,则可以配置标识(identify)目录。

下面的命令将列出/proc的所有内容,并存储Redis PID已供后面使用。

DBPID=$(pgrep redis-server)
echo Redis is $DBPID
Redis is 1109
$ ls /proc
1     1227  131  169  21   25   32   462  55   62   696  768  acpi       devices      interrupts  kmsg         misc          schedstat  sysrq-trigger  version_signature
10    124   132  17   217  26   33   465  56   63   7    775  buddyinfo  diskstats    iomem       kpagecgroup  modules       scsi       sysvipc        vmallocinfo
1094  125   133  171  22   262  34   494  57   64   708  8    bus        dma          ioports     kpagecount   mounts        self       thread-self    vmstat
11    126   134  172  225  267  35   5    58   65   715  85   cgroups    driver       irq         kpageflags   mtrr          slabinfo   timer_list     zoneinfo
1109  127   135  18   23   27   36   52   59   66   718  86   cmdline    execdomains  kallsyms    loadavg      net           softirqs   timer_stats
12    129   140  19   232  28   4    53   60   675  72   87   consoles   fb           kcore       locks        pagetypeinfo  stat       tty
1205  13    15   2    233  29   455  54   61   677  720  9    cpuinfo    filesystems  keys        mdstat       partitions    swaps      uptime
1220  130   16   20   24   3    457  543  610  68   725  905  crypto     fs           key-users   meminfo      sched_debug   sys        version

每个进程在不同文件中定义了自己的配置和安全设置 ls /proc/$DBPID 命令查看我们Redis Server的进程配置文件输出如下

attr       cgroup      comm             cwd      fd       io        map_files  mountinfo   net        oom_adj        pagemap      root       sessionid  stack  status   timers
autogroup  clear_refs  coredump_filter  environ  fdinfo   limits    maps       mounts      ns         oom_score      personality  sched      setgroups  stat   syscall  uid_map
auxv       cmdline     cpuset           exe      gid_map  loginuid  mem        mountstats  numa_maps  oom_score_adj  projid_map   schedstat  smaps      statm  task     wchan

举个例子,你可以查看和更新该进程的环境变量 cat /proc/$DBPID/environ, 输出如下

HOSTNAME=a125f186bfbeSHLVL=2REDIS_DOWNLOAD_SHA=61db74eabf6801f057fd24b590232f2f337d422280fd19486eca03be87d3a82bHOME=/home/redisPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binREDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-5.0.7.tar.gzREDIS_VERSION=5.0.7PWD=/data

docker exec -it db env 通过Docker容器查看环境变量,输出如下

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=a125f186bfbe
TERM=xterm
REDIS_VERSION=5.0.7
REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-5.0.7.tar.gz
REDIS_DOWNLOAD_SHA=61db74eabf6801f057fd24b590232f2f337d422280fd19486eca03be87d3a82b
HOME=/root

命名空间(Namespace)

容器的基本组成部分之一就是名称空间。名称空间的概念是为了限制哪些进程可以看到和访问系统的某些部分,例如其他网络接口或进程。

启动容器后,容器运行时(例如Docker)将创建新的名称空间以对进程进行沙箱处理。通过在它自己的PID 命名空间(Pid namespace)运行一个进程,它将看起来像是系统上唯一的进程。

可用的命名空间是:

查看更多Linux Namespace信息

Unshare可以启动容器的进程

在不使用诸如Docker之类的运行时的情况下,进程仍可以在其自己的名称空间中运行。一种帮助工具是unshare。

unshare --help

使用unshare,可以启动进程并创建新的名称空间,例如Pid。通过从主机 unshare Pid命名空间,bash提示似乎是机器上唯一运行的进程。看起来好像bash是计算机上唯一运行的进程。

$ sudo unshare --fork --pid --mount-proc bash
$ ps
  PID TTY          TIME CMD
    1 pts/0    00:00:00 bash
    9 pts/0    00:00:00 ps
$ exit

当我们共享一个命名空间时会发生什么?

在幕后,命名空间是磁盘上的inode位置。这允许进程共享/重用相同的名称空间,从而允许它们查看和交互。

列出所有命名空间

$ ls -lha /proc/$DBPID/ns/
total 0
dr-x--x--x 2 999 packer 0 Jan 17 15:06 .
dr-xr-xr-x 9 999 packer 0 Jan 17 15:06 ..
lrwxrwxrwx 1 999 packer 0 Jan 17 15:42 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 999 packer 0 Jan 17 15:18 ipc -> ipc:[4026532157]
lrwxrwxrwx 1 999 packer 0 Jan 17 15:18 mnt -> mnt:[4026532155]
lrwxrwxrwx 1 999 packer 0 Jan 17 15:06 net -> net:[4026532160]
lrwxrwxrwx 1 999 packer 0 Jan 17 15:18 pid -> pid:[4026532158]
lrwxrwxrwx 1 999 packer 0 Jan 17 15:18 user -> user:[4026531837]
lrwxrwxrwx 1 999 packer 0 Jan 17 15:18 uts -> uts:[4026532156]

另一个工具·NSEnter·用于将进程附加到现有的命名空间。用于调试目的

$ nsenter --help

$ nsenter --target $DBPID --mount --uts --ipc --net --pid ps aux
PID   USER     TIME  COMMAND
    1 redis     0:04 redis-server
   15 root      0:00 ps aux

使用Docker,可以使用以下语法共享这些命名空间container:<container-name>。例如,下面的命令会将nginx连接到数据库命名空间。

$ 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/
total 0
dr-x--x--x 2 systemd-network systemd-journal 0 Jan 17 15:49 .
dr-xr-xr-x 9 systemd-network systemd-journal 0 Jan 17 15:46 ..
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 ipc -> ipc:[4026532225]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 mnt -> mnt:[4026532223]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 net -> net:[4026532160]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 pid -> pid:[4026532226]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 user -> user:[4026531837]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 uts -> uts:[4026532224]

但是,两个进程的net命名空间指向相同的位置。

$ ls -lha /proc/$WEBPID/ns/ | grep net
dr-x--x--x 2 systemd-network systemd-journal 0 Jan 17 15:49 .
dr-xr-xr-x 9 systemd-network systemd-journal 0 Jan 17 15:46 ..
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 ipc -> ipc:[4026532225]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 mnt -> mnt:[4026532223]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 net -> net:[4026532160]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 pid -> pid:[4026532226]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 user -> user:[4026531837]
lrwxrwxrwx 1 systemd-network systemd-journal 0 Jan 17 15:49 uts -> uts:[4026532224]

$ ls -lha /proc/$DBPID/ns/ | grep net
lrwxrwxrwx 1 999 packer 0 Jan 17 15:06 net -> net:[4026532160]

Chroot

容器进程的重要部分是拥有独立于主机的不同文件的能力。这就是我们可以基于系统上运行的不同操作系统拥有不同的Docker映像的方式。

Chroot允许进程从不同的根目录启动到父操作系统。这允许不同的文件出现在根目录中。

Cgroups (Control Groups)

CGroup限制了进程可以消耗的资源量。这些cgroup是在/ proc目录内的特定文件中定义的值。

运行一下命令查看这些映射

$ cat /proc/$DBPID/cgroup
11:pids:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
10:perf_event:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
9:net_cls,net_prio:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
8:hugetlb:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
7:blkio:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
6:cpuset:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
5:freezer:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
4:cpu,cpuacct:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
3:devices:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
2:memory:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0
1:name=systemd:/docker/a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e

这些映射到磁盘上其他cgroup目录,位于:

$ ls /sys/fs/cgroup/
blkio  cpu  cpuacct  cpu,cpuacct  cpuset  devices  freezer  hugetlb  memory  net_cls  net_cls,net_prio  net_prio  perf_event  pids  systemd

进程的CPU统计信息是什么?

CPU统计信息和使用情况也存储在文件中!

$ cat /sys/fs/cgroup/cpu,cpuacct/docker/$DBID/cpuacct.stat
user 265
system 332

CPU份额限制也在此处定义

$ cat /sys/fs/cgroup/cpu,cpuacct/docker/$DBID/cpu.shares
1024

容器内存配置的所有Docker cgroup都存储在以下位置:

$ ls /sys/fs/cgroup/memory/docker/
085ac378ebb98d1f7d4b4cb233534bacfb057530a6e8f7e42b132faf23c5e804  memory.kmem.limit_in_bytes          memory.limit_in_bytes            memory.swappiness
a125f186bfbe2e89fccbb35829732e2ea7bcfdc216d456ed6ee2c186555ca2e0  memory.kmem.max_usage_in_bytes      memory.max_usage_in_bytes        memory.usage_in_bytes
cgroup.clone_children                                             memory.kmem.slabinfo                memory.move_charge_at_immigrate  memory.use_hierarchy
cgroup.event_control                                              memory.kmem.tcp.failcnt             memory.numa_stat                 notify_on_release
cgroup.procs                                                      memory.kmem.tcp.limit_in_bytes      memory.oom_control               tasks
memory.failcnt                                                    memory.kmem.tcp.max_usage_in_bytes  memory.pressure_level
memory.force_empty                                                memory.kmem.tcp.usage_in_bytes      memory.soft_limit_in_bytes
memory.kmem.failcnt                                               memory.kmem.usage_in_bytes          memory.stat

每个目录都根据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.clone_children  memory.kmem.failcnt             memory.kmem.tcp.limit_in_bytes      memory.max_usage_in_bytes        memory.soft_limit_in_bytes  notify_on_release
cgroup.event_control   memory.kmem.limit_in_bytes      memory.kmem.tcp.max_usage_in_bytes  memory.move_charge_at_immigrate  memory.stat                 tasks
cgroup.procs           memory.kmem.max_usage_in_bytes  memory.kmem.tcp.usage_in_bytes      memory.numa_stat                 memory.swappiness
memory.failcnt         memory.kmem.slabinfo            memory.kmem.usage_in_bytes          memory.oom_control               memory.usage_in_bytes
memory.force_empty     memory.kmem.tcp.failcnt         memory.limit_in_bytes               memory.pressure_level            memory.use_hierarchy

如何配置cgroup?

Docker的特性之一是能够控制内存限制。这是通过cgroup设置完成的,默认情况下,容器对内存没有限制。我们可以通过docker stats命令查看。

$ docker stats db --no-stream
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
a125f186bfbe        db                  0.20%               8.277MiB / 992.1MiB   0.83%               1.3kB / 0B          0B / 0B             4

内存引用存储在一个名为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
7999488

再次检查Docker Stats时,该进程的内存限制现在为7.629M

$ docker stats db --no-stream
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT    MEM %               NET I/O             BLOCK I/O           PIDS
a125f186bfbe        db                  0.19%               7.52MiB / 7.629MiB   98.57%              1.3kB / 0B          233kB / 0B          4

Seccomp / AppArmor

Linux的所有操作都是通过syscall完成的。内核有330个系统调用,它们执行诸如读取文件,关闭句柄和检查访问权限之类的操作

AppArmor是一个应用程序定义的配置文件,它描述了进程可以访问系统的哪些部分。

可以通过以下方式查看分配给流程的当前AppArmor配置文件

$ cat /proc/$DBPID/attr/current
docker-default (enforce)

Docker的默认AppArmor配置文件是docker-default (enforce)

在Docker 1.13之前,它将AppArmor配置文件存储在/etc/apparmor.d/docker-default(在Docker启动时被覆盖,因此用户无法修改它)。在v1.13之后,Docker现在在tmpfs中生成docker-default,使用apparmor_parser将其加载到内核中,然后删除该文件。

这是它的模板连接

Seccomp提供了限制可以进行哪些系统调用,阻止安装内核模块或更改文件权限等方面的功能。

默认的Docker允许调用

当分配给一个进程时,这意味着该进程将限于系统调用的一部分。如果它尝试调用被阻止的系统调用,则会收到错误“不允许操作”。

SecComp的状态也在文件中定义

$ cat /proc/$DBPID/status
$ cat /proc/$DBPID/status | grep Seccomp

该flag的含义是:0:禁用 1:严格 2:过滤

Capabilities

Capabilities是有关进程或用户有权执行的操作的分组。这些功能可能涵盖多个系统调用或操作,例如更改系统时间或主机名

状态文件还包含功能标志。一个进程可以丢弃尽可能多的功能以确保其安全性。

$ cat /proc/$DBPID/status | grep ^Cap

这些标志被存储为位掩码,可以用capsh解码

$ capsh --decode=00000000a80425fb