关于 Linux 容器你需要知道的一切,第二部分:使用 Linux 容器 (LXC)

关于容器的深度探讨的第一部分介绍了内核控制组(或 cgroups)的概念,以及如何隔离、限制和监控选定的用户空间应用程序。在此,我将深入探讨流程隔离的下一步——即通过容器,更具体地说,是 Linux 容器 (LXC) 框架。

当运行虚拟机时,容器几乎与裸机一样接近。在托管虚拟实例时,它们几乎不产生或不产生开销。LXC 最初于 2008 年推出,其大部分功能都源自之前的 Solaris 容器(或 Solaris Zones)和 FreeBSD jails。LXC 不是创建功能齐全的虚拟机,而是启用具有自己进程和网络空间的虚拟环境。使用命名空间来强制执行进程隔离,并利用内核自身的控制组 (cgroups) 功能,该功能限制、核算和隔离一个或多个进程的 CPU、内存、磁盘 I/O 和网络使用率。可以将此用户空间框架视为 chroot 的一种非常高级的形式。

注意:LXC 使用命名空间来强制执行进程隔离,同时使用内核自身的 cgroups 来核算和限制一个或多个进程的 CPU、内存、磁盘 I/O 和网络使用率。

但容器究竟是什么?简短的答案是,容器将软件应用程序与操作系统分离,为用户提供干净且最小化的 Linux 环境,同时将其余一切都在一个或多个隔离的“容器”中运行。容器的目的是启动一组有限的应用程序或服务(通常称为微服务),并让它们在自包含的沙盒环境中运行。

注意:容器的目的是启动一组有限的应用程序或服务,并让它们在自包含的沙盒环境中运行。

""

图 1. 传统环境中运行的应用程序与容器的比较

这种隔离可防止在给定容器中运行的进程监控或影响在另一个容器中运行的进程。此外,这些容器化服务不会影响或干扰主机。能够将分散在多台物理服务器上的许多服务整合到一台服务器上的想法是数据中心选择采用该技术的众多原因之一。

容器功能包括以下内容

  • 安全性:网络服务可以在容器中运行,这限制了安全漏洞或违规造成的损害。成功利用在该容器中运行的应用程序之一上的安全漏洞的入侵者将被限制在该容器内可能执行的一组操作中。
  • 隔离性:容器允许在同一物理机上部署一个或多个应用程序,即使这些应用程序必须在不同的域下运行,每个域都需要独占访问其各自的资源。例如,在不同容器中运行的多个应用程序可以通过使用与每个容器关联的不同 IP 地址来绑定到同一物理网络接口。
  • 虚拟化和透明性:容器为系统提供了一个虚拟化环境,该环境可以隐藏或限制底层物理设备或系统配置的可见性。容器背后的一般原则是避免更改应用程序运行的环境,除非是为了解决安全或隔离问题。
使用 LXC 实用程序

对于大多数现代 Linux 发行版,内核都启用了 cgroups,但您很可能仍然需要安装 LXC 实用程序。

如果您使用的是 Red Hat 或 CentOS,则需要先安装 EPEL 存储库。对于其他发行版,例如 Ubuntu 或 Debian,只需键入


$ sudo apt-get install lxc

现在,在您开始摆弄这些实用程序之前,您需要配置您的环境。在执行此操作之前,您需要验证当前用户是否在 /etc/subuid 和 /etc/subgid 中定义了 uidgid 条目


$ cat /etc/subuid
petros:100000:65536
$ cat /etc/subgid
petros:100000:65536

如果 ~/.config/lxc 目录尚不存在,请创建它,并将 /etc/lxc/default.conf 配置文件复制到 ~/.config/lxc/default.conf。将以下两行附加到文件末尾


lxc.id_map = u 0 100000 65536
lxc.id_map = g 0 100000 65536

它应该看起来像这样


$ cat ~/.config/lxc/default.conf
lxc.network.type = veth
lxc.network.link = lxcbr0
lxc.network.flags = up
lxc.network.hwaddr = 00:16:3e:xx:xx:xx
lxc.id_map = u 0 100000 65536
lxc.id_map = g 0 100000 65536

将以下内容附加到 /etc/lxc/lxc-usernet 文件(将第一列替换为您的用户名)


petros veth lxcbr0 10

使这些设置生效的最快方法是重新启动节点或注销用户,然后重新登录。

重新登录后,验证 veth 网络驱动程序当前是否已加载


$ lsmod|grep veth
veth                   16384  0

如果未加载,请键入


$ sudo modprobe veth

现在,您可以使用 LXC 实用程序下载、运行和管理 Linux 容器。

接下来,下载容器镜像并将其命名为“example-container”。当您键入以下命令时,您将看到许多 Linux 发行版和版本下支持的容器的长列表


$ sudo lxc-create -t download -n example-container

您将获得三个提示来选择发行版、版本和架构。我选择了以下内容


Distribution: ubuntu
Release: xenial
Architecture: amd64

一旦您做出决定并按 Enter 键,rootfs 将在本地下载并配置。出于安全原因,每个容器都不附带 OpenSSH 服务器或用户帐户。也不提供默认 root 密码。为了更改 root 密码并登录,您必须运行 lxc-attachchroot 进入容器目录路径(在启动后)。

启动容器


$ sudo lxc-start -n example-container -d

-d 选项将容器守护进程化,它将在后台运行。如果您想观察启动过程,请将 -d 替换为 -F,它将在前台运行,并在登录提示符处结束。

您可能会遇到类似于以下内容的错误


$ sudo lxc-start -n example-container -d
lxc-start: tools/lxc_start.c: main: 366 The container
failed to start.
lxc-start: tools/lxc_start.c: main: 368 To get more details,
run the container in foreground mode.
lxc-start: tools/lxc_start.c: main: 370 Additional information
can be obtained by setting the --logfile and --logpriority
options.

如果遇到这种情况,您需要通过在前台运行 lxc-start 服务来调试它


$ sudo lxc-start -n example-container -F
lxc-start: conf.c: instantiate_veth: 2685 failed to create veth
 pair (vethQ4NS0B and vethJMHON2): Operation not supported
    lxc-start: conf.c: lxc_create_network: 3029 failed to
    create netdev
    lxc-start: start.c: lxc_spawn: 1103 Failed to create
    the network.
    lxc-start: start.c: __lxc_start: 1358 Failed to spawn
    container "example-container".
    lxc-start: tools/lxc_start.c: main: 366 The container failed
    to start.
    lxc-start: tools/lxc_start.c: main: 370 Additional information
    can be obtained by setting the --logfile and --logpriority
    options.

从上面的示例中,您可以看到可能未插入 veth 模块。插入后,问题得到解决。

无论如何,打开第二个终端窗口并验证容器的状态


$ sudo lxc-info -n example-container
Name:           example-container
State:          RUNNING
PID:            1356
IP:             10.0.3.28
CPU use:        0.29 seconds
BlkIO use:      16.80 MiB
Memory use:     29.02 MiB
KMem use:       0 bytes
Link:           vethPRK7YU
 TX bytes:      1.34 KiB
 RX bytes:      2.09 KiB
 Total bytes:   3.43 KiB

另一种方法是运行以下命令以列出所有已安装的容器


$ sudo lxc-ls -f
NAME         STATE   AUTOSTART GROUPS IPV4      IPV6
example-container RUNNING 0         -      10.0.3.28 -

但是有一个问题——您仍然无法登录!直接附加到正在运行的容器,创建您的用户,并使用 passwd 命令更改所有相关密码


$ sudo lxc-attach -n example-container
root@example-container:/#
root@example-container:/# useradd petros
root@example-container:/# passwd petros
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully

密码更改后,您将能够直接从控制台登录到容器,而无需 lxc-attach 命令


$ sudo lxc-console -n example-container

如果您想通过网络连接到此正在运行的容器,请安装 OpenSSH 服务器


root@example-container:/# apt-get install openssh-server

获取容器的本地 IP 地址


root@example-container:/# ip addr show eth0|grep inet
    inet 10.0.3.25/24 brd 10.0.3.255 scope global eth0
    inet6 fe80::216:3eff:fed8:53b4/64 scope link

然后从主机并在新的控制台窗口中,键入


$ ssh 10.0.3.25

瞧!现在您可以 ssh 进入正在运行的容器并键入您的用户名和密码。

在主机系统上,而不是在容器内,观察在启动容器后启动和运行的 LXC 进程很有趣


$ ps aux|grep lxc|grep -v grep
root       861  0.0  0.0 234772  1368 ?        Ssl  11:01
 ↪0:00 /usr/bin/lxcfs /var/lib/lxcfs/
lxc-dns+  1155  0.0  0.1  52868  2908 ?        S    11:01
 ↪0:00 dnsmasq -u lxc-dnsmasq --strict-order
 ↪--bind-interfaces --pid-file=/run/lxc/dnsmasq.pid
 ↪--listen-address 10.0.3.1 --dhcp-range 10.0.3.2,10.0.3.254
 ↪--dhcp-lease-max=253 --dhcp-no-override
 ↪--except-interface=lo --interface=lxcbr0
 ↪--dhcp-leasefile=/var/lib/misc/dnsmasq.lxcbr0.leases
 ↪--dhcp-authoritative
root      1196  0.0  0.1  54484  3928 ?        Ss   11:01
 ↪0:00 [lxc monitor] /var/lib/lxc example-container
root      1658  0.0  0.1  54780  3960 pts/1    S+   11:02
 ↪0:00 sudo lxc-attach -n example-container
root      1660  0.0  0.2  54464  4900 pts/1    S+   11:02
 ↪0:00 lxc-attach -n example-container

要停止容器,请键入(从主机)


$ sudo lxc-stop -n example-container

停止后,验证容器的状态


$ sudo lxc-ls -f
NAME         STATE   AUTOSTART GROUPS IPV4 IPV6
example-container STOPPED 0         -      -    -

$ sudo lxc-info -n example-container
Name:           example-container
State:          STOPPED

要完全销毁容器——即从主机系统中清除它——请键入


$ sudo lxc-destroy -n example-container
Destroyed container example-container

销毁后,验证它是否已被删除


$ sudo lxc-info -n example-container
example-container doesn't exist

$ sudo lxc-ls -f
$

注意:如果您尝试销毁正在运行的容器,该命令将失败并告知您容器仍在运行


$ sudo lxc-destroy -n example-container
example-container is running

容器必须先停止才能销毁。

高级配置

有时,可能需要配置一个或多个容器来完成一个或多个任务。LXC 通过让管理员修改位于 /var/lib/lxc 中的容器配置文件来简化此操作


$ sudo su
# cd /var/lib/lxc
# ls
example-container

容器的父目录将至少包含两个文件:1) 容器配置文件和 2) 容器的整个 rootfs


# cd example-container/
# ls
config  rootfs

假设您想在主机系统启动时自动启动标记为 example-container 的容器。为此,您需要将以下行附加到容器的配置文件 /var/lib/lxc/example-container/config


# Enable autostart
lxc.start.auto = 1

在您重新启动容器或重新启动主机系统后,您应该会看到类似这样的内容


$ sudo lxc-ls -f
NAME              STATE   AUTOSTART GROUPS IPV4      IPV6
example-container RUNNING 1         -      10.0.3.25 -

请注意 AUTOSTART 字段现在设置为“1”。

如果在容器启动时,您希望容器绑定挂载主机上的目录路径,请将以下行附加到同一文件


# Bind mount system path to local path
lxc.mount.entry = /mnt mnt none bind 0 0

通过上面的示例,当容器重新启动时,您将看到主机 /mnt 目录的内容可以访问容器的本地 /mnt 目录。

特权容器与非特权容器

您经常可能会偶然发现讨论特权容器和非特权容器概念的 LXC 相关内容。但这些到底是什么?这个概念非常简单明了,LXC 容器可以在任一配置中运行。

从设计上讲,非特权容器被认为比特权容器更安全。非特权容器使用容器的 root UID 到主机系统上的非 root UID 的映射运行。这使得攻击者更难通过入侵容器来获得对底层主机的 root 权限。简而言之,如果攻击者设法通过例如已知的软件漏洞来入侵您的容器,他们会立即发现自己在主机上没有任何权限。

特权容器可能会使系统遭受此类攻击。这就是为什么最好以特权模式运行少量容器。识别需要特权访问的容器,并确保额外努力定期更新并以其他方式锁定它们。

那么,Docker 呢?

我花了很多时间谈论 Linux 容器,但 Docker 呢?它生产环境中部署最多的容器解决方案。自最初发布以来,Docker 以迅雷不及掩耳之势席卷了 Linux 计算世界。Docker 是一种获得 Apache 许可的开源容器化技术,旨在自动执行在容器内部创建和部署微服务的重复性任务。Docker 将容器视为极其轻量级和模块化的虚拟机。最初,Docker 构建在 LXC 之上,但此后已摆脱了这种依赖性,从而带来了更好的开发人员和用户体验。与 LXC 非常相似,Docker 继续使用内核 cgroup 子系统。该技术不仅仅是运行容器,它还简化了创建容器、构建镜像、共享这些构建的镜像以及对其进行版本控制的过程。

Docker 主要关注

  • 可移植性:Docker 提供基于镜像的部署模型。这种类型的可移植性允许更轻松地跨多个环境共享应用程序或一组服务(及其所有依赖项)。
  • 版本控制:单个 Docker 镜像由一系列组合层组成。每当镜像被更改时,都会创建一个新层。例如,每次用户指定命令(例如 runcopy)时,都会创建一个新层。Docker 将为新的容器构建重用这些层。分层是 Docker 自己的版本控制方法。
  • 回滚:同样,每个 Docker 镜像都有层。如果您不想使用当前正在运行的层,您可以回滚到以前的版本。这种敏捷性使软件开发人员可以更轻松地持续集成和部署他们的软件技术。
  • 快速部署:配置新硬件通常可能需要数天时间。并且,安装和配置它所需的工作量和开销非常繁重。借助 Docker,您可以避免所有这些,方法是将启动并运行镜像所需的时间缩短到几秒钟。当您完成容器后,您可以像销毁它一样轻松地销毁它。

从根本上讲,Docker 和 LXC 非常相似。它们都是用户空间和轻量级虚拟化平台,它们都实现了 cgroups 和命名空间来管理资源隔离。但是,两者之间存在许多明显的差异。

进程管理

Docker 限制容器作为单个进程运行。如果您的应用程序由 X 个并发进程组成,Docker 会希望您运行 X 个容器,每个容器都有其自己独特的进程。LXC 则不然,LXC 运行带有传统 init 进程的容器,反过来,可以在同一容器内托管多个进程。例如,如果您想托管 LAMP(Linux + Apache + MySQL + PHP)服务器,则每个应用程序的每个进程都需要跨越多个 Docker 容器。

状态管理

Docker 被设计为无状态的,这意味着它不支持持久存储。有一些解决方法,但同样,只有在进程需要时才需要。创建 Docker 镜像时,它将由只读层组成。这将不会改变。在运行时,如果容器的进程对其内部状态进行任何更改,则内部状态与镜像当前状态之间的差异将被维护,直到对 Docker 镜像进行提交(创建新层)或直到容器被删除,从而导致该差异消失。

可移植性

在讨论 Docker 时,这个词往往被过度使用——那是因为它是 Docker 优于 LXC 的最重要优势。Docker 在从应用程序中抽象出网络、存储和操作系统细节方面做得更好。这导致应用程序真正独立于配置,从而保证应用程序的环境始终保持不变,无论它在哪个机器上启用。

Docker 旨在使开发人员和系统管理员都受益。它已成为许多 DevOps(开发人员 + 运维)工具链不可或缺的一部分。开发人员可以专注于编写代码,而无需担心最终托管它的系统。使用 Docker,无需安装和配置复杂的数据库,也无需担心在不兼容的语言工具链版本之间切换。Docker 为运维人员提供了灵活性,通常减少了托管一些较小且更基本的应用程序所需的物理系统数量。Docker 简化了软件交付。新功能和错误/安全修复程序可以快速到达客户手中,而不会出现任何麻烦、意外或停机。

总结

为了基础设施安全和系统稳定性而隔离进程并不像听起来那么痛苦。Linux 内核提供了所有必要的工具,以启用易于使用的用户空间应用程序(例如 LXC(甚至 Docker))来管理操作系统及其本地服务的微实例,这些实例位于隔离的沙盒环境中。

在本系列的第三部分中,我将介绍使用 Kubernetes 的容器编排。

资源

Petros Koutoupis,LJ 特约编辑,目前是 Cray Lustre 高性能文件系统部门的高级性能软件工程师。他还是 RapidDisk 项目的创建者和维护者。Petros 在数据存储行业工作了十多年,并帮助开创了当今广泛使用的许多技术。

加载 Disqus 评论