Nived V 发布的文章

命名空间、控制组、seccomp 和 SELinux 构成了在系统上构建和运行一个容器进程的 Linux 技术基础。

 title=

在以前的文章中,我介绍过 容器镜像 及其 运行时。在本文中,我研究了容器是如何在一些特殊的 Linux 技术基础上实现的,这其中包括命名空间和控制组。

 title=

图1:对容器有贡献的 Linux 技术(Nived Velayudhan, CC BY-SA 4.0)

这些 Linux 技术构成了在系统上构建和运行容器进程的基础:

  1. 命名空间
  2. 控制组(cgroups)
  3. Seccomp
  4. SELinux

命名空间

命名空间 namespace 为容器提供了一个隔离层,给容器提供了一个看起来是独占的 Linux 文件系统的视图。这就限制了进程能访问的内容,从而限制了它所能获得的资源。

在创建容器时,Docker 或 Podman 和其他容器技术使用了 Linux 内核中的几个命名空间:

[nivedv@homelab ~]$ docker container run alpine ping 8.8.8.8
[nivedv@homelab ~]$ sudo lsns -p 29413

        NS TYPE   NPROCS   PID USER COMMAND
4026531835 cgroup    299     1 root /usr/lib/systemd/systemd --switched...
4026531837 user      278     1 root /usr/lib/systemd/systemd --switched...
4026533105 mnt         1 29413 root ping 8.8.8.8
4026533106 uts         1 29413 root ping 8.8.8.8
4026533107 ipc         1 29413 root ping 8.8.8.8
4026533108 pid         1 29413 root ping 8.8.8.8
4026533110 net         1 29413 root ping 8.8.8.8

用户

用户(user)命名空间将用户和组隔离在一个容器内。这是通过分配给容器与宿主系统有不同的 UID 和 GID 范围来实现的。用户命名空间使软件能够以 root 用户的身份在容器内运行。如果入侵者攻击容器,然后逃逸到宿主机上,他们就只能以受限的非 root 身份运行了。

挂载

挂载(mnt)命名空间允许容器有自己的文件系统层次结构视图。你可以在 Linux 系统中的 /proc/<PID>/mounts 位置找到每个容器进程的挂载点。

UTS

Unix 分时系统 Unix Timeharing System (UTS)命名空间允许容器拥有一个唯一主机名和域名。当你运行一个容器时,即使使用 - name 标签,也会使用一个随机的 ID 作为主机名。你可以使用 unshare 命令 来了解一下这个工作原理。

nivedv@homelab ~]$ docker container run -it --name nived alpine sh
/ # hostname 
9c9a5edabdd6
/ # 
nivedv@homelab ~]$ sudo unshare -u sh
sh-5.0# hostname isolated.hostname 
sh-5.0# hostname
isolated.hostname
sh-5.0# 
sh-5.0# exit
exit
[nivedv@homelab ~]$ hostname
homelab.redhat.com

IPC

进程间通信 Inter-Process Communication (IPC)命名空间允许不同的容器进程之间,通过访问共享内存或使用共享消息队列来进行通信。

[root@demo /]# ipcmk -M 10M
Shared memory id: 0
[root@demo /]# ipcmk -M 20M
Shared memory id: 1
[root@demo /]# 
[root@demo /]# ipcs
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
0xd1df416a 0          root       644        10485760   0                       
0xbd487a9d 1          root       644        20971520   0                       
------ Semaphore Arrays --------
key        semid      owner      perms      nsems

PID

进程 ID Process ID (PID)命名空间确保运行在容器内的进程与外部隔离。当你在容器内运行 ps 命令时,由于这个命名空间隔离的存在,你只能看到在容器内运行的进程,而不是在宿主机上。

网络

网络(net)命名空间允许容器有自己网络接口、IP 地址、路由表、端口号等视图。容器如何能够与外部通信?你创建的所有容器都会被附加到一个特殊的虚拟网络接口上进行通信。

[nivedv@homelab ~]$ docker container run --rm -it alpine sh
/ # ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=119 time=21.643 ms
64 bytes from 8.8.8.8: seq=1 ttl=119 time=20.940 ms
^C
[root@homelab ~]# ip link show veth84ea6fc
veth84ea6fc@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue 
master docker0 state UP mode DEFAULT group default

控制组

控制组(cgroup)是组成一个容器的基本模块。控制组会分配和限制容器所使用的资源,如 CPU、内存、网络 I/O 等。容器引擎会自动创建每种类型的控制组文件系统,并在容器运行时为每个容器设置配额。

[root@homelab ~]# lscgroup | grep docker
cpuset:/docker
net_cls,net_prio:/docker
cpu,cpuacct:/docker
hugetlb:/docker
devices:/docker
freezer:/docker
memory:/docker
perf_event:/docker
blkio:/docker
pids:/docker

容器运行时为每个容器设置了控制组值,所有信息都存储在 /sys/fs/cgroup/*/docker。下面的命令将确保容器可以使用 50000 微秒的 CPU 时间片,并将内存的软、硬限制分别设置为 500M 和 1G。

[root@homelab ~]# docker container run -d --name test-cgroups --cpus 0.5 --memory 1G --memory-reservation 500M httpd
[root@homelab ~]# lscgroup cpu,cpuacct:/docker memory:/docker
cpu,cpuacct:/docker/
cpu,cpuacct:/docker/c3503ac704dafea3522d3bb82c77faff840018e857a2a7f669065f05c8b2cc84
memory:/docker/
memory:/docker/c3503ac704dafea3522d3bb82c77faff840018e857a2a7f669065f05c8b2cc84
[root@homelab c....c84]# cat cpu.cfs_period_us 
100000
[root@homelab c....c84]# cat cpu.cfs_quota_us 
50000
[root@homelab c....c84]# cat memory.soft_limit_in_bytes 
524288000
[root@homelab c....c84]# cat memory.limit_in_bytes 
1073741824

SECCOMP

Seccomp 意思是“ 安全计算 secure computing ”。它是一项 Linux 功能,用于限制应用程序进行的系统调用的集合。例如,Docker 的默认 seccomp 配置文件禁用了大约 44 个系统调用(总计超过 300 个)。

这里的思路是让容器只访问所必须的资源。例如,如果你不需要容器改变主机上的时钟时间,你可能不会使用 clock_adjtime 和 clock_settime 系统调用,屏蔽它们是合理的。同样地,你不希望容器改变内核模块,所以没有必要让它们使用 create_module、 delete_module 系统调用。

SELinux

SELinux 是“ 安全增强的 Linux security-enhanced Linux ”的缩写。如果你在你的宿主机上运行的是 Red Hat 发行版,那么 SELinux 是默认启用的。SELinux 可以让你限制一个应用程序只能访问它自己的文件,并阻止任何其他进程访问。因此,如果一个应用程序被破坏了,它将限制该应用程序可以影响或控制的文件数量。通过为文件和进程设置上下文环境以及定义策略来实现,这些策略将限制一个进程可以访问和更改的内容。

容器的 SELinux 策略是由 container-selinux 包定义的。默认情况下,容器以 container_t 标签运行,允许在 /usr 目录下读取(r)和执行(x),并从 /etc 目录下读取大部分内容。标签container_var_lib_t 是与容器有关的文件的通用标签。

总结

容器是当今 IT 基础设施的一个重要组成部分,也是一项相当有趣的技术。即使你的工作不直接涉及容器化,了解一些基本的容器概念和方法,也能让你体会到它们如何帮助你的组织。容器是建立在开源的 Linux 技术之上的,这使它们变得更加美好。

本文基于 techbeatly 的文章,并经授权改编。


via: https://opensource.com/article/21/8/container-linux-technology

作者:Nived V 选题:lujun9972 译者:wxy 校对:turbokernel

本文由 LCTT 原创编译,Linux中国 荣誉推出

通过深入了解容器运行时,理解容器环境是如何建立的。

 title=

在学习 容器镜像 时,我们讨论了容器的基本原理,但现在是深入研究容器 运行时 runtime 的时候了,从而了解容器环境是如何构建的。本文的部分信息摘自 开放容器计划 Open Container Initiative (OCI)的 官方文档,所以无论使用何种容器引擎,这些信息都是一致的。

容器运行机制

那么,当你运行 podman run 或 docker run 命令时,在后台到底发生了什么?一个分步的概述如下:

  1. 如果本地没有镜像,则从镜像 登记仓库 registry 拉取镜像
  2. 镜像被提取到一个写时复制(COW)的文件系统上,所有的容器层相互堆叠以形成一个合并的文件系统
  3. 为容器准备一个挂载点
  4. 从容器镜像中设置元数据,包括诸如覆盖 CMD、来自用户输入的 ENTRYPOINT、设置 SECCOMP 规则等设置,以确保容器按预期运行
  5. 提醒内核为该容器分配某种隔离,如进程、网络和文件系统( 命名空间 namespace )
  6. 提醒内核为改容器分配一些资源限制,如 CPU 或内存限制( 控制组 cgroup )
  7. 传递一个 系统调用 syscall 给内核用于启动容器
  8. 设置 SELinux/AppArmor

容器运行时负责上述所有的工作。当我们提及容器运行时,想到的可能是 runc、lxc、containerd、rkt、cri-o 等等。嗯,你没有错。这些都是容器引擎和容器运行时,每一种都是为不同的情况建立的。

容器运行时 Container runtime 更侧重于运行容器,为容器设置命名空间和控制组(cgroup),也被称为底层容器运行时。高层的容器运行时或容器引擎专注于格式、解包、管理和镜像共享。它们还为开发者提供 API。

开放容器计划(OCI)

开放容器计划 Open Container Initiative (OCI)是一个 Linux 基金会的项目。其目的是设计某些开放标准或围绕如何与容器运行时和容器镜像格式工作的结构。它是由 Docker、rkt、CoreOS 和其他行业领导者于 2015 年 6 月建立的。

它通过两个规范来完成如下任务:

1、镜像规范

该规范的目标是创建可互操作的工具,用于构建、传输和准备运行的容器镜像。

该规范的高层组件包括:

2、运行时规范

该规范用于定义容器的配置、执行环境和生命周期。config.json 文件为所有支持的平台提供了容器配置,并详细定义了用于创建容器的字段。在详细定义执行环境时也描述了为容器的生命周期定义的通用操作,以确保在容器内运行的应用在不同的运行时环境之间有一个一致的环境。

Linux 容器规范使用了各种内核特性,包括 命名空间 namespace 、 控制组 cgroup 、 权能 capability 、LSM 和文件系统 隔离 jail 等来实现该规范。

小结

容器运行时是通过 OCI 规范管理的,以提供一致性和互操作性。许多人在使用容器时不需要了解它们是如何工作的,但当你需要排除故障或优化时,了解容器是一个宝贵的优势。

本文基于 techbeatly 的文章,并经授权改编。


via: https://opensource.com/article/21/9/container-runtimes

作者:Nived V 选题:lujun9972 译者:geekpi 校对:turbokernel

本文由 LCTT 原创编译,Linux中国 荣誉推出

容器镜像包含一个打包的应用,以及它的依赖关系,还有它在启动时运行的进程信息。

 title=

容器是当今 IT 运维的一个关键部分。 容器镜像 container image 包含了一个打包的应用,以及它的依赖关系,还有它在启动时运行的进程信息。

你可以通过提供一组特殊格式的指令来创建容器镜像,可以是提交给 注册中心 Registry ,或者是作为 Dockerfile 保存。例如,这个 Dockerfile 为 PHP Web 应用创建了一个容器:

FROM registry.access.redhat.com/ubi8/ubi:8.1

RUN yum --disableplugin=subscription-manager -y module enable php:7.3 \
  && yum --disableplugin=subscription-manager -y install httpd php \
  && yum --disableplugin=subscription-manager clean all

ADD index.php /var/www/html

RUN sed -i 's/Listen 80/Listen 8080/' /etc/httpd/conf/httpd.conf \
  && sed -i 's/listen.acl_users = apache,nginx/listen.acl_users =/' /etc/php-fpm.d/www.conf \
  && mkdir /run/php-fpm \
  && chgrp -R 0 /var/log/httpd /var/run/httpd /run/php-fpm \
  && chmod -R g=u /var/log/httpd /var/run/httpd /run/php-fpm

EXPOSE 8080
USER 1001
CMD php-fpm & httpd -D FOREGROUND

这个文件中的每条指令都会在容器镜像中增加一个 层 layer 。每一层只增加与下面一层的区别,然后,所有这些堆叠在一起,形成一个只读的容器镜像。

它是如何工作的?

你需要知道一些关于容器镜像的事情,按照这个顺序理解这些概念很重要:

  1. 联合文件系统
  2. 写入时复制(COW)
  3. 叠加文件系统
  4. 快照器

联合文件系统

联合文件系统 Union File System (UnionFS)内置于 Linux 内核中,它允许将一个文件系统的内容与另一个文件系统的内容合并,同时保持“物理”内容的分离。其结果是一个统一的文件系统,即使数据实际上是以分支形式组织。

这里的想法是,如果你有多个镜像有一些相同的数据,不是让这些数据再次复制过来,而是通过使用一个叫做 层 layer 的东西来共享。

 title=

每一层都是一个可以在多个容器中共享的文件系统,例如,httpd 基础层是 Apache 的官方镜像,可以在任何数量的容器中使用。想象一下,由于我们在所有的容器中使用相同的基础层,我们节省了多少磁盘空间。

这些镜像层总是只读的,但是当我们用这个镜像创建一个新的容器时,我们会在它上面添加一个薄的可写层。这个可写层是你创建、修改、删除或进行每个容器所需的其他修改的地方。

写时复制(COW)

当你启动一个容器时,看起来好像这个容器有自己的整个文件系统。这意味着你在系统中运行的每个容器都需要自己的文件系统副本。这岂不是要占用大量的磁盘空间,而且还要花费大量的时间让容器启动?不是的,因为每个容器都不需要它自己的文件系统副本!

容器和镜像使用 写时复制 copy-on-write (COW)机制来实现这一点。写时复制策略不是复制文件,而是将同一个数据实例分享给多个进程,并且只在一个进程需要修改或写入数据时进行复制。所有其他进程将继续使用原始数据。

Docker 对镜像和容器都使用了写时复制的机制。为了做到这一点,在旧版本中,镜像和运行中的容器之间的变化是通过 图驱动 graph driver 来跟踪的,现在则是通过 快照器 snapshotter 来跟踪。

在运行中的容器中执行任何写操作之前,要修改的文件的副本被放在容器的可写层上。这就是发生 写 的地方。现在你知道为什么它被称为“写时复制”了么。

这种策略既优化了镜像磁盘空间的使用,也优化了容器启动时间的性能,并与 UnionFS 一起工作。

叠加文件系统

叠加文件系统 Overlay File System 位于现有文件系统的顶部,结合了上层和下层的目录树,并将它们作为一个单一的目录来呈现。这些目录被称为 层 layer 。下层保持不被修改。每一层只增加与下一层的差异(计算机术语为 “diff”),这种统一的过程被称为 联合挂载 union mount 。

最低的目录或镜像层被称为 下层目录 lowerdir ,上面的目录被称为 上层目录 upperdir 。最后的覆盖层或统一层被称为 合并层 merged 。

 title=

常见的术语包括这些层的定义:

  • 基础层 Base layer :是你的文件系统的文件所在的地方。就容器镜像而言,这个层就是你的基础镜像。
  • 叠加层 Overlay layer :通常被称为 容器层 container layer ,因为对运行中的容器所做的所有改变,如添加、删除或修改文件,都会写到这个可写层。对这一层所做的所有修改都存储在下一层,是基础层和差异层的联合视图。
  • 差异层 Diff layer 包含了在叠加层所作的所有修改。如果你写的东西已经在基础层了,那么叠加文件系统就会把文件复制到差异层,并做出你想写的修改。这被称为写时复制。

快照器

通过使用层和图驱动,容器可以将其更改作为其容器文件系统的一部分来构建、管理和分发。但是使用 图驱动 graph driver 的工作真的很复杂,而且容易出错。 快照器 SnapShotter 与图驱动不同,因为它们不用了解镜像或容器。

快照器的工作方式与 Git 非常相似,比如有树的概念,并跟踪每次提交对树的改变。一个 快照 snapshot 代表一个文件系统状态。快照有父子关系,使用一组目录。可以在父级和其快照之间进行差异比较(diff),以创建一个层。

快照器提供了一个用于分配、快照和挂载抽象的分层文件系统的 API。

总结

你现在对什么是容器镜像以及它们的分层方法如何使容器可移植有了很好的认识。接下来,我将介绍容器的运行机制和内部结构。

本文基于 techbeatly 的文章,经许可后改编。


via: https://opensource.com/article/21/8/container-image

作者:Nived V 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出