LXC-容器化手册-全-

LXC 容器化手册(全)

原文:annas-archive.org/md5/383dd4e9fd9e44b47991846bf91de59b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

不久前,我们通常在单一服务器上部署应用程序,通过增加更多硬件资源来进行扩展——我们称之为“单体架构”。实现高可用性通常是通过在负载均衡器后面增加更多的单用途服务器/单体来完成,往往最终形成一个资源利用率低的系统集群。编写和部署应用程序也遵循这种单体架构——软件通常是一个大型二进制文件,提供了大部分甚至是全部的功能。我们要么从源代码编译它并使用某种安装程序,要么将其打包并发送到仓库。

随着虚拟机和容器的出现,我们摆脱了服务器单体架构,通过在隔离且资源受限的实例中运行应用程序,充分利用了可用的计算资源。应用程序的扩展变成了在服务器集群上添加更多虚拟机或容器,然后找到一种方法自动部署它们。我们还将单一的二进制应用拆分成微服务,它们通过消息总线/队列相互通信,充分利用容器所提供的低开销。部署完整的应用堆栈现在只是将服务打包到各自的容器中,创建一个单一的、完全隔离、依赖完整的工作单元,准备好进行部署。使用持续集成模式和工具(如 Jenkins)使我们能够进一步自动化构建和部署过程。

本书讲解的是 LXC 容器以及如何在其中运行应用程序。与其他容器解决方案(如 Docker)不同,LXC 旨在运行整个 Linux 系统,而不仅仅是单个进程,尽管后者也是可能的。即使 LXC 容器可以包含整个 Linux 文件系统,底层的宿主内核仍然是共享的,无需虚拟化层。

本书采用直接而实用的方式介绍 LXC。你将学习如何安装、配置和操作 LXC 容器,并通过多个示例讲解如何在 LXC 中运行高度可扩展且高可用的应用程序。你将使用监控和部署应用程序以及其他第三方工具。你还将学习如何编写自己的工具,扩展 LXC 及其各种库提供的功能。最后,你将看到一个完整的 OpenStack 部署,它为管理一组计算资源提供了智能化,使得在 LXC 容器中轻松部署你的应用程序。

本书内容涵盖的内容

第一章,Linux 容器简介,深入探讨了 Linux 内核中容器的历史,并介绍了一些基本术语。在了解基本内容后,你将详细了解内核命名空间和控制组(cgroups)的实现,并能够尝试一些 C 系统调用。

第二章,在 Linux 系统上安装和运行 LXC,涵盖了在 Ubuntu 和 Red Hat 系统上安装、配置和运行 LXC 所需的所有内容。你将学习所需的软件包和工具,并了解不同的 LXC 配置方法。通过本章的学习,你将拥有一个运行 LXC 容器的 Linux 系统。

第三章,使用原生工具和 Libvirt 工具进行命令行操作,专注于如何在命令行中运行和操作 LXC。本章将涵盖各种工具和软件包,并演示与容器化应用程序交互的不同方式。重点将放在 libvirt 和原生 LXC 库提供的功能上,控制 LXC 容器的整个生命周期。

第四章,LXC 与 Python 代码集成,将展示如何使用 Python 库编写工具并自动化 LXC 的配置和管理。你还将学习如何使用 Vagrant 和 LXC 创建开发环境。

第五章,使用 Linux 桥接和 Open vSwitch 进行 LXC 网络配置,将深入探讨容器化世界中的网络配置——将 LXC 连接到 Linux 桥接,使用直接连接、NAT 和其他多种方法。它还将演示使用 Open vSwitch 进行流量管理的更高级技巧。

第六章,使用 LXC 进行集群和横向扩展,在前面章节的基础上构建,展示如何构建 Apache 容器集群,并演示如何通过使用 Open vSwitch 的 GRE 隧道将它们连接起来。本章还展示了如何在最小化根文件系统容器中运行单进程应用程序的示例。

第七章,容器化世界中的监控与备份,讨论如何备份你的 LXC 应用容器并部署监控解决方案来进行警报和触发操作。我们将看到使用 Sensu 和 Monit 进行监控的示例,以及使用 iSCSI 和 GlusterFS 创建热备份和冷备份的示例。

第八章,将 LXC 与 OpenStack 结合使用,演示如何使用 OpenStack 配置 LXC 容器。首先介绍 OpenStack 的各个组件,以及如何使用 LXC nova 驱动程序在计算资源池中自动配置 LXC 容器。

附录,LXC 与 Docker 和 OpenVZ 的替代方案,通过展示其他流行的容器解决方案如 Docker 和 OpenVZ 的由来,以及它们之间的相似性和差异,结束本书。它还探讨了如何在 LXC 旁边安装、配置和运行这些解决方案的实际示例。

本书所需的内容

对 Linux 和命令行有初学者水平的知识应该足以跟随并运行示例代码。虽然本书并非关于软件开发,但要完全理解和实验代码片段需要一定的 Python 和 C 语言知识。如果不感兴趣,你可以跳过第四章,LXC 与 Python 的代码集成

在硬件和软件要求方面,本书中的大多数示例已在虚拟机中测试过,虚拟机利用了各种云服务提供商,如 Amazon AWS 和 Rackspace Cloud。考虑到 Canonical 公司参与 LXC 项目,我们推荐使用最新版本的 Ubuntu,但在安装/操作方法有所不同的情况下,我们也提供了 CentOS 的示例。

本书适合谁阅读

本书适合任何对 Linux 容器感兴趣的人,从寻求深入理解 LXC 如何工作的 Linux 管理员,到需要在没有完整虚拟机开销的隔离环境中快速简便地原型化代码的软件开发人员。想要从头到尾阅读本书的最合适职位是 DevOps 工程师。

约定

本书中会有多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“手动使用debootstrapyum等工具构建根文件系统和配置文件。”

一段代码如下所示:

#define _GNU_SOURCE
#include<stdlib.h>
#include<stdio.h>
#include<signal.h>
#include<sched.h>

staticintchildFunc(void *arg)
{
  printf("UID inside the namespace is %ld\n", (long) geteuid());
  printf("GID inside the namespace is %ld\n", (long) getegid());
}

当我们希望引起你对代码块中特定部分的注意时,相关行或项会以粗体显示:

<head> 
#define _GNU_SOURCE
#include
#include
#include
#include

staticintchildFunc(void *arg)
{
 printf("UID inside the namespace is %ld\n", (long) geteuid());  printf("GID inside the namespace is %ld\n", (long) getegid());
}

任何命令行输入或输出如下所示:

root@ubuntu:~# lsb_release -dc
Description:   	Ubuntu 14.04.5 LTS
Codename:      	trusty
root@ubuntu:~#

新术语重要词汇以粗体显示。你在屏幕上看到的词汇,例如菜单或对话框中的内容,会以如下方式出现在文本中:“导航到网络支持 | 网络选项 | 802.1d 以太网桥接,并选择Y以在内核中编译桥接功能,或选择M将其作为模块编译。”

注意

警告或重要提示会显示在像这样的框中。

提示

提示和技巧如这样显示。

读者反馈

我们非常欢迎读者的反馈。告诉我们您对本书的看法——您喜欢或不喜欢的部分。读者的反馈对我们非常重要,因为它帮助我们开发出您真正能从中受益的书籍。如果您有某个领域的专长,并且有兴趣撰写或参与编写书籍,请查看我们的作者指南:www.packtpub.com/authors

客户支持

现在,作为一本 Packt 书籍的骄傲拥有者,我们提供了一些帮助您充分利用购买的资源。

下载示例代码

您可以从www.packtpub.com的账户中下载此书的示例代码文件。如果您是在其他地方购买的此书,可以访问www.packtpub.com/support并注册,以便直接通过电子邮件将文件发送给您。

您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的官方网站。

  2. 将鼠标指针悬停在顶部的SUPPORT标签上。

  3. 点击Code Downloads & Errata

  4. Search框中输入书名。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的地方。

  7. 点击Code Download

一旦文件下载完成,请确保使用以下最新版本解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublishing/Containerization-with-LXC。我们还有其他书籍和视频的代码包,您可以在github.com/PacktPublishing/上找到。赶快去看看吧!

下载本书的彩色图片

我们还为您提供了一份包含本书中使用的截图/图表的彩色图片的 PDF 文件。这些彩色图片将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/ContainerizationwithLXC_ColorImages.pdf下载该文件。

勘误

尽管我们已经尽力确保内容的准确性,但错误仍然可能发生。如果您在我们的书籍中发现任何错误——无论是文本还是代码错误——我们将非常感激您向我们报告。这样,您可以帮助其他读者避免困扰,并且帮助我们改进后续版本。如果您发现任何勘误,请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并填写勘误的详细信息。一旦您的勘误得到验证,您的提交将被接受,并且勘误将会被上传到我们的网站或添加到该书的勘误部分的现有列表中。

若要查看之前提交的勘误,请访问 www.packtpub.com/books/content/support,在搜索框中输入书名。相关信息将出现在勘误部分。

盗版

网络上版权材料的盗版问题是一个持续存在的全球性问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现任何形式的非法复制作品,请立即向我们提供其位置地址或网站名称,以便我们采取措施解决。

如果你发现疑似盗版的内容,请通过电子邮件联系我们,邮箱地址为 copyright@packtpub.com,并提供相关链接。

我们感谢您帮助保护我们的作者,并支持我们继续为您提供有价值的内容。

问题

如果你对本书的任何内容有疑问,可以通过电子邮件联系 questions@packtpub.com,我们会尽力解决问题。

第一章. Linux 容器简介

如今,在某种形式的 Linux 容器中部署应用程序已成为一种广泛采用的做法,主要得益于工具的演变和其提供的易用性。尽管 Linux 容器或操作系统级虚拟化在某种形式上已经存在超过十年,但这项技术需要一段时间才能成熟并进入主流应用。其原因之一是,基于虚拟机监控程序的技术(如 KVM 和 Xen)在那段时间内能够解决 Linux 内核的大多数限制,且它所带来的开销并未被视为一个问题。然而,随着内核命名空间和控制组cgroups)的出现,通过使用容器实现的轻量级虚拟化才变得可行。

本章将涵盖以下主题:

  • 操作系统内核的演变及其早期的限制

  • 容器与平台虚拟化之间的区别

  • 与命名空间和控制组相关的概念和术语

  • 使用网络命名空间和控制组进行进程资源隔离与管理的示例

操作系统内核及其早期的限制

当前 Linux 容器的状态是早期操作系统设计者试图解决的问题的直接结果——以最有效的方式管理内存、I/O 和进程调度。

在过去,只有单个进程可以被调度执行,如果在进行 I/O 操作时发生阻塞,会浪费宝贵的 CPU 周期。解决这个问题的方法是开发更好的 CPU 调度器,以便能够以公平的方式分配更多的工作,从而最大限度地利用 CPU。尽管现代的调度器,比如 Linux 中的完全公平调度器CFS),在为每个进程分配公平的时间方面做得非常好,但仍然有很强的需求能够对进程及其子进程进行更高或更低的优先级控制。传统上,这可以通过 nice() 系统调用或者实时调度策略来实现,但这也存在粒度或控制程度上的限制。

同样,在虚拟内存出现之前,多个进程会从共享的物理内存池中分配内存。虚拟内存提供了一种每个进程的内存隔离形式,意味着进程会有自己的地址空间,并通过交换空间扩展可用内存,但仍然没有一个好的方式来限制每个进程及其子进程可以使用的内存量。

更加复杂的是,在同一物理服务器上运行不同的工作负载通常会对所有正在运行的服务产生负面影响。内存泄漏或内核恐慌可能导致某个应用程序使整个操作系统崩溃。例如,内存密集型的 Web 服务器与 I/O 密集型的数据库服务一起运行时,会变得非常有问题。为了避免这种情况,系统管理员通常会将不同的应用程序分配到一组服务器上,使得一些机器处于低利用率状态,尤其是在一天中的某些时段,当工作量不大时。这与单个运行中的进程因 I/O 操作阻塞而浪费 CPU 和内存资源是类似的问题。

解决这些问题的方法是使用基于虚拟机监控程序的虚拟化、容器技术,或者两者的结合。

Linux 容器的案例

虚拟机监控程序作为操作系统的一部分,负责管理虚拟机的生命周期,自上世纪 60 年代末期的主机时代起便存在。大多数现代虚拟化实现,如 Xen 和 KVM,都可以追溯到那个时期。大约在 2005 年,这些虚拟化技术的广泛应用的主要原因是需要更好地控制和利用日益增长的计算资源集群。虚拟机与宿主操作系统之间增加一层安全性也是一个好的卖点,尤其对注重安全性的人来说,尽管像所有新技术一样,仍然出现了一些安全事件。

尽管如此,完全虚拟化和半虚拟化的采用显著改善了服务器的利用方式和应用的配置方式。事实上,像 KVM 和 Xen 这样的虚拟化技术至今仍被广泛使用,尤其是在多租户云和像 OpenStack 这样的云技术中。

在之前所述问题的背景下,虚拟机监控程序提供了以下好处:

  • 能够在同一物理服务器上运行不同的操作系统

  • 更精细的资源分配控制

  • 进程隔离——虚拟机上的内核恐慌不会影响宿主操作系统

  • 独立的网络栈以及按虚拟机控制流量的能力

  • 通过简化数据中心管理并更好地利用可用服务器资源,从而降低资本和运营成本。

今天反对使用任何形式的虚拟化技术的主要原因可以说是同一操作系统中使用多个内核所带来的开销。如果宿主操作系统能够提供这种隔离级别,而不需要 CPU 的硬件扩展,或不使用如 QEMU 之类的仿真软件,甚至不需要像 KVM 这样的内核模块,那么从复杂性角度来看,效果会更好。仅为了为单个 Web 服务器实现隔离而运行整个操作系统在虚拟机上,并不是最有效的资源分配方式。

在过去十年里,Linux 内核进行了多次改进,以实现类似功能,但减少了开销——最显著的变化就是内核命名空间和 cgroups。LXC 是首批利用这些变化的技术之一,自内核版本 2.6.24 和 2008 年左右开始。尽管 LXC 并不是最早的容器技术,但它帮助推动了我们今天所看到的容器革命。

使用 LXC 的主要优点包括:

  • 比运行虚拟机监控程序的开销和复杂性要小

  • 每个容器占用更小的系统资源

  • 启动时间在毫秒范围内

  • 原生内核支持

值得一提的是,容器的安全性本身并不如在虚拟机和宿主操作系统之间使用虚拟机监控程序那样强。然而,近年来,通过使用 强制访问控制 (MAC) 技术,如 SELinux 和 AppArmor、内核能力和 cgroups,已经取得了显著进展,大大缩小了这一差距,后续章节将展示这些技术的应用。

Linux 命名空间——LXC 的基础

命名空间是轻量级进程虚拟化的基础。它们使得进程及其子进程可以对底层系统有不同的视图。这是通过增加 unshare()setns() 系统调用,以及为 clone()unshare()setns() 系统调用传递的六个新的常量标志来实现的:

  • clone():这会创建一个新进程并将其附加到一个新的指定命名空间

  • unshare():这将当前进程附加到一个新的指定命名空间

  • setns():这将一个进程附加到已存在的命名空间

当前 LXC 使用六种命名空间,并且还在开发更多:

  • 挂载命名空间,通过 CLONE_NEWNS 标志指定

  • UTS 命名空间,通过 CLONE_NEWUTS 标志指定

  • IPC 命名空间,通过 CLONE_NEWIPC 标志指定

  • PID 命名空间,通过 CLONE_NEWPID 标志指定

  • 用户命名空间,通过 CLONE_NEWUSER 标志指定

  • 网络命名空间,通过 CLONE_NEWNET 标志指定

让我们更详细地看一下每个优点,并通过一些用户空间的示例帮助我们更好地理解系统背后的运作原理。

挂载命名空间

挂载命名空间首次出现在 2002 年的内核 2.4.19 版本中,为进程及其子进程提供了一个独立的文件系统挂载点视图。当挂载或卸载文件系统时,所有进程都会察觉到变化,因为它们共享同一个默认命名空间。当 CLONE_NEWNS 标志传递给 clone() 系统调用时,新进程会获得调用进程挂载树的副本,并可以对其进行修改,而不会影响父进程。从那时起,默认命名空间中的所有挂载和卸载操作将会在新命名空间中可见,但在每个进程的挂载命名空间中的变化不会被外部进程察觉。

clone() 原型如下:

#define _GNU_SOURCE 
#include <sched.h> 
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg); 

一个示例调用,在新的挂载命名空间中创建一个子进程,代码如下:

child_pid = clone(childFunc, child_stack + STACK_SIZE, CLONE_NEWNS | SIGCHLD, argv[1]); 

当子进程被创建时,它会执行 childFunc 函数,该函数将在新的挂载命名空间中执行其工作。

util-linux 包提供了实现 unshare() 调用的用户空间工具,这些工具有效地将指定的命名空间从父进程中分离出来。

举个例子:

  1. 首先打开一个终端,并在 /tmp 下创建一个目录,如下所示:

    root@server:~# mkdir /tmp/mount_ns
    root@server:~#
    
    
  2. 接下来,通过向 unshare 传递挂载标志,将当前的 bash 进程移动到自己的挂载命名空间中:

    root@server:~# unshare -m /bin/bash
    root@server:~#
    
    
  3. bash 进程现在处于一个独立的命名空间中。让我们检查该命名空间的关联 inode 编号:

    root@server:~# readlink /proc/$$/ns/mnt
    mnt:[4026532211]
    root@server:~#
    
    
  4. 接下来,创建一个临时的挂载点:

    root@server:~# mount -n -t tmpfs tmpfs /tmp/mount_ns
    root@server:~#
    
    
  5. 同时,确保你能在新创建的命名空间中看到挂载点:

    root@server:~# df -h | grep mount_ns
    tmpfs           3.9G     0  3.9G   0% /tmp/mount_ns 
    root@server:~# cat /proc/mounts | grep mount_ns
    tmpfs /tmp/mount_ns tmpfs rw,relatime 0 0
    root@server:~#
    
    

    正如预期的那样,挂载点可见,因为它是我们创建的命名空间的一部分,当前的 bash 进程正是从这个命名空间中运行的。

  6. 接下来,启动一个新的终端会话并显示该会话中的命名空间 inode ID:

    root@server:~# readlink /proc/$$/ns/mnt
    mnt:[4026531840]
    root@server:~#
    
    

    注意它与另一个终端上的挂载命名空间的区别。

  7. 最后,检查新的终端中挂载点是否可见:

    root@server:~# cat /proc/mounts | grep mount_ns
    root@server:~# df -h | grep mount_ns
    root@server:~#
    
    

毫不奇怪,挂载点在默认命名空间中不可见。

注意

在 LXC 的上下文中,挂载命名空间非常有用,因为它们提供了一种不同文件系统布局的方式,允许它存在于容器内部。值得一提的是,在挂载命名空间之前,可以使用 chroot() 系统调用实现类似的进程隔离,但 chroot 并没有像挂载命名空间那样提供每个进程的独立隔离。

UTS 命名空间

Unix 时间共享UTS)命名空间为主机名和域名提供隔离,使得每个 LXC 容器都能保持其自己的标识符,正如 hostname -f 命令返回的那样。大多数依赖于正确设置主机名的应用程序都需要这个功能。

要在新的 UTS 命名空间中创建一个 bash 会话,可以再次使用 unshare 工具,该工具使用 unshare() 系统调用来创建命名空间,并使用 execve() 系统调用来执行 bash

root@server:~# hostname
server
root@server:~# unshare -u /bin/bash
root@server:~# hostname uts-namespace
root@server:~# hostname
uts-namespace
root@server:~# cat /proc/sys/kernel/hostname
uts-namespace
root@server:~#

正如前面的输出所示,命名空间中的主机名现在是 uts-namespace

接下来,从另一个终端检查主机名,确保它没有发生变化:

root@server:~# hostname
server
root@server:~#

正如预期的那样,主机名仅在新的 UTS 命名空间中发生了变化。

要查看 unshare 命令实际使用的系统调用,可以运行 strace 工具:

root@server:~# strace -s 2000 -f unshare -u /bin/bash
...
unshare(CLONE_NEWUTS)                   = 0
getgid()                                = 0
setgid(0)                               = 0
getuid()                                = 0
setuid(0)                               = 0
execve("/bin/bash", ["/bin/bash"], [/* 15 vars */]) = 0
...

从输出中我们可以看到,unshare 命令确实使用了 unshare()execve() 系统调用,并使用 CLONE_NEWUTS 标志来指定新的 UTS 命名空间。

IPC 命名空间

进程间通信IPC)命名空间为一组 IPC 和同步设施提供隔离。这些设施提供了一种在线程和进程之间交换数据和同步操作的方式。它们提供诸如信号量、文件锁和互斥锁等原语,是容器中实现真正进程隔离所必需的。

PID 命名空间

进程 IDPID)命名空间为进程提供了一个 ID,允许该 ID 在默认命名空间中已存在,例如 ID 为 1。这使得初始化系统可以在容器中运行,并与其他进程一起运行,而不会与同一操作系统上的其他 PID 冲突。

为了演示这个概念,打开 pid_namespace.c 文件:

#define _GNU_SOURCE 
#include <stdlib.h> 
#include <stdio.h> 
#include <signal.h> 
#include <sched.h> 

static int childFunc(void *arg) 
{ 
    printf("Process ID in child  = %ld\n", (long) getpid()); 
} 

首先,我们包含头文件并定义childFunc函数,clone()系统调用将使用该函数。该函数通过getpid()系统调用打印子进程的 PID:

static char child_stack[1024*1024]; 

int main(int argc, char *argv[]) 
{ 
    pid_t child_pid; 

    child_pid = clone(childFunc, child_stack + 
    (1024*1024),      
    CLONE_NEWPID | SIGCHLD, NULL); 

    printf("PID of cloned process: %ld\n", (long) child_pid); 
    waitpid(child_pid, NULL, 0); 
    exit(EXIT_SUCCESS); 
} 

main() 函数中,我们指定栈的大小并调用 clone(),传递子函数 childFunc、栈指针、CLONE_NEWPID 标志和 SIGCHLD 信号。CLONE_NEWPID 标志指示 clone() 创建一个新的 PID 命名空间,SIGCHLD 标志通知父进程其子进程何时终止。如果子进程尚未终止,父进程将会阻塞在 waitpid() 调用上。

编译并使用以下命令运行程序:

root@server:~# gcc pid_namespace.c -o pid_namespace
root@server:~# ./pid_namespace
PID of cloned process: 17705
Process ID in child  = 1
root@server:~#

从输出中,我们可以看到子进程在其命名空间内的 PID 为 1,在其他地方为 17705

注意

请注意,代码示例中省略了错误处理以简化内容。

用户命名空间

用户命名空间允许一个命名空间中的进程拥有与默认命名空间中不同的用户和组 ID。在 LXC 的上下文中,这使得一个进程可以在容器内以root身份运行,而在外部拥有一个非特权 ID。这增加了一层薄薄的安全性,因为一旦从容器中逃逸,进程将变为一个非特权用户。这是可能的,因为内核 3.8 引入了非特权进程创建用户命名空间的能力。

要在非特权用户下创建一个新的用户命名空间并在其中拥有root,我们可以使用unshare工具。让我们从源代码安装最新版本:

root@ubuntu:~# cd /usr/src/
root@ubuntu:/usr/src# wget https://www.kernel.org/pub/linux/utils/util-linux/v2.28/util-linux-2.28.tar.gz
root@ubuntu:/usr/src# tar zxfv util-linux-2.28.tar.gz
root@ubuntu:/usr/src# cd util-linux-2.28/
root@ubuntu:/usr/src/util-linux-2.28# ./configure
root@ubuntu:/usr/src/util-linux-2.28# make && make install
root@ubuntu:/usr/src/util-linux-2.28# unshare --map-root-user --user sh -c whoami
root
root@ubuntu:/usr/src/util-linux-2.28#

我们还可以使用带有 CLONE_NEWUSER 标志的 clone() 系统调用,在用户命名空间中创建一个进程,如下程序所示:

#define _GNU_SOURCE 
#include <stdlib.h> 
#include <stdio.h> 
#include <signal.h> 
#include <sched.h> 

static int childFunc(void *arg) 
{ 
    printf("UID inside the namespace is %ld\n", (long) 
    geteuid()); 
    printf("GID inside the namespace is %ld\n", (long) 
    getegid()); 
} 

static char child_stack[1024*1024]; 

int main(int argc, char *argv[]) 
{ 
    pid_t child_pid; 

    child_pid = clone(childFunc, child_stack +  
    (1024*1024),        
    CLONE_NEWUSER | SIGCHLD, NULL); 

    printf("UID outside the namespace is %ld\n", (long)       
    geteuid()); 
    printf("GID outside the namespace is %ld\n", (long)      
    getegid()); 
    waitpid(child_pid, NULL, 0); 
    exit(EXIT_SUCCESS); 
} 

编译和执行后,作为root运行时,输出看起来类似于这个 - UID 为 0

root@server:~# gcc user_namespace.c -o user_namespace
root@server:~# ./user_namespace
UID outside the namespace is 0
GID outside the namespace is 0
UID inside the namespace is 65534
GID inside the namespace is 65534
root@server:~#

网络命名空间

网络命名空间提供网络资源的隔离,例如网络设备、地址、路由和防火墙规则。这有效地创建了网络栈的逻辑副本,允许多个进程在多个命名空间中监听同一个端口。这是 LXC 网络的基础,并且在很多其他用例中也能派上用场。

iproute2 包提供了非常有用的用户空间工具,我们可以用它来实验网络命名空间,并且几乎所有 Linux 系统默认都安装了它。

总是存在默认的网络命名空间,称为根命名空间,所有网络接口最初都会分配到该命名空间。要列出属于默认命名空间的网络接口,可以运行以下命令:

root@server:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
 link/ether 0e:d5:0e:b0:a3:47 brd ff:ff:ff:ff:ff:ff
root@server:~#

在这种情况下,有两个接口——loeth0

要列出它们的配置,我们可以运行以下命令:

root@server:~# ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP group default qlen 1000
 link/ether 0e:d5:0e:b0:a3:47 brd ff:ff:ff:ff:ff:ff
inet 10.1.32.40/24 brd 10.1.32.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::cd5:eff:feb0:a347/64 scope link
 valid_lft forever preferred_lft forever
root@server:~#

同时,要列出根网络命名空间中的路由,可以执行以下命令:

root@server:~# ip r s
default via 10.1.32.1 dev eth0
10.1.32.0/24 dev eth0  proto kernel  scope link  src 10.1.32.40
root@server:~#

让我们创建两个新的网络命名空间,分别命名为 ns1ns2,并列出它们:

root@server:~# ip netns add ns1
root@server:~# ip netns add ns2
root@server:~# ip netns
ns2
ns1
root@server:~#

现在我们有了新的网络命名空间,可以在其中执行命令:

root@server:~# ip netns exec ns1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
root@server:~#

上述输出显示,在 ns1 命名空间中,只有一个网络接口——回环接口 lo,且处于 DOWN 状态。

我们还可以在命名空间内启动一个新的 bash 会话,并以类似的方式列出接口:

root@server:~# ip netns exec ns1 bash
root@server:~# ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
root@server:~# exit
root@server:~#

这样比逐个指定每个命令要更方便。两个网络命名空间如果没有连接到任何东西,将不会有太多用处,所以让我们将它们连接起来。为此,我们将使用一个名为 Open vSwitch 的软件桥接器。

Open vSwitch 的工作原理就像一个普通的网络桥接器,然后它会在我们定义的虚拟端口之间转发帧。虚拟机,如 KVM、Xen、LXC 或 Docker 容器,可以通过它进行连接。

大多数基于 Debian 的发行版(如 Ubuntu)都提供该软件包,所以让我们安装它:

root@server:~# apt-get install -y openvswitch-switch
root@server:~#

这将安装并启动 Open vSwitch 守护进程。现在是时候创建桥接器了,我们将它命名为 OVS-1

root@server:~# ovs-vsctl add-br OVS-1
root@server:~# ovs-vsctl show
0ea38b4f-8943-4d5b-8d80-62ccb73ec9ec
Bridge "OVS-1"
 Port "OVS-1"
 Interface "OVS-1"
 type: internal
ovs_version: "2.0.2"
root@server:~#

注意

如果你想尝试最新版本的 Open vSwitch,可以从 openvswitch.org/download/ 下载源代码并编译。

新创建的桥接器现在可以在根命名空间中看到:

root@server:~# ip a s OVS-1
4: OVS-1: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
link/ether 9a:4b:56:97:3b:46 brd ff:ff:ff:ff:ff:ff
inet6 fe80::f0d9:78ff:fe72:3d77/64 scope link
 valid_lft forever preferred_lft forever
root@server:~#

为了连接这两个网络命名空间,我们首先为每个命名空间创建一对虚拟接口:

root@server:~# ip link add eth1-ns1 type veth peer name veth-ns1
root@server:~# ip link add eth1-ns2 type veth peer name veth-ns2
root@server:~#

上述两个命令创建了四个虚拟接口——eth1-ns1eth1-ns2veth-ns1veth-ns2。这些名称是任意的。

要列出属于根网络命名空间的所有接口,请运行:

root@server:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 0e:d5:0e:b0:a3:47 brd ff:ff:ff:ff:ff:ff
3: ovs-system: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default
link/ether 82:bf:52:d3:de:7e brd ff:ff:ff:ff:ff:ff
4: OVS-1: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether 9a:4b:56:97:3b:46 brd ff:ff:ff:ff:ff:ff
5: veth-ns1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 1a:7c:74:48:73:a9 brd ff:ff:ff:ff:ff:ff
6: eth1-ns1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 8e:99:3f:b8:43:31 brd ff:ff:ff:ff:ff:ff
7: veth-ns2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 5a:0d:34:87:ea:96 brd ff:ff:ff:ff:ff:ff
8: eth1-ns2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether fa:71:b8:a1:7f:85 brd ff:ff:ff:ff:ff:ff
root@server:~#

让我们将 eth1-ns1eth1-ns2 接口分配到 ns1ns2 命名空间:

root@server:~# ip link set eth1-ns1 netns ns1
root@server:~# ip link set eth1-ns2 netns ns2

同时,确认它们是否能从每个网络命名空间内部看到:

root@server:~# ip netns exec ns1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
6: eth1-ns1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 8e:99:3f:b8:43:31 brd ff:ff:ff:ff:ff:ff
root@server:~#
root@server:~# ip netns exec ns2 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
8: eth1-ns2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether fa:71:b8:a1:7f:85 brd ff:ff:ff:ff:ff:ff
root@server:~#

请注意,现在每个网络命名空间都分配了两个接口——loopbacketh1-ns*

如果我们从根命名空间列出设备,应该会看到我们刚刚移动到 ns1ns2 命名空间的接口不再可见:

root@server:~# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 0e:d5:0e:b0:a3:47 brd ff:ff:ff:ff:ff:ff
3: ovs-system: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default
link/ether 82:bf:52:d3:de:7e brd ff:ff:ff:ff:ff:ff
4: OVS-1: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether 9a:4b:56:97:3b:46 brd ff:ff:ff:ff:ff:ff
5: veth-ns1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 1a:7c:74:48:73:a9 brd ff:ff:ff:ff:ff:ff
7: veth-ns2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 5a:0d:34:87:ea:96 brd ff:ff:ff:ff:ff:ff
root@server:~#

现在是时候将两个虚拟管道的另一端——veth-ns1veth-ns2 接口连接到桥接器了:

root@server:~# ovs-vsctl add-port OVS-1 veth-ns1
root@server:~# ovs-vsctl add-port OVS-1 veth-ns2
root@server:~# ovs-vsctl show
0ea38b4f-8943-4d5b-8d80-62ccb73ec9ec
Bridge "OVS-1"
 Port "OVS-1"
 Interface "OVS-1"
 type: internal
 Port "veth-ns1"
 Interface "veth-ns1"
 Port "veth-ns2"
 Interface "veth-ns2"
ovs_version: "2.0.2"
root@server:~#

从上述输出中可以看出,桥接器现在有两个端口——veth-ns1veth-ns2

最后要做的就是启用网络接口并分配 IP 地址:

root@server:~# ip link set veth-ns1 up
root@server:~# ip link set veth-ns2 up
root@server:~# ip netns exec ns1 ip link set dev lo up
root@server:~# ip netns exec ns1 ip link set dev eth1-ns1 up
root@server:~# ip netns exec ns1 ip address add 192.168.0.1/24 dev eth1-ns1
root@server:~# ip netns exec ns1 ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
6: eth1-ns1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 8e:99:3f:b8:43:31 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.1/24 scope global eth1-ns1
 valid_lft forever preferred_lft forever
inet6 fe80::8c99:3fff:feb8:4331/64 scope link
 valid_lft forever preferred_lft forever
root@server:~#

同样,对于 ns2 命名空间:

root@server:~# ip netns exec ns2 ip link set dev lo up
root@server:~# ip netns exec ns2 ip link set dev eth1-ns2 up
root@server:~# ip netns exec ns2 ip address add 192.168.0.2/24 dev eth1-ns2
root@server:~# ip netns exec ns2 ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
8: eth1-ns2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether fa:71:b8:a1:7f:85 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.2/24 scope global eth1-ns2
 valid_lft forever preferred_lft forever
inet6 fe80::f871:b8ff:fea1:7f85/64 scope link
 valid_lft forever preferred_lft forever
root@server:~#

Network namespaces

这样,我们通过 Open vSwitch 桥接器建立了 ns1ns2 网络命名空间之间的连接。为了确认,让我们使用 ping 命令:

root@server:~# ip netns exec ns1 ping -c 3 192.168.0.2
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.414 ms
64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.027 ms
64 bytes from 192.168.0.2: icmp_seq=3 ttl=64 time=0.030 ms
--- 192.168.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.027/0.157/0.414/0.181 ms
root@server:~#
root@server:~# ip netns exec ns2 ping -c 3 192.168.0.1
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.150 ms
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=0.025 ms
64 bytes from 192.168.0.1: icmp_seq=3 ttl=64 time=0.027 ms
--- 192.168.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.025/0.067/0.150/0.058 ms
root@server:~#

Open vSwitch 允许为网络接口分配 VLAN 标签,从而实现命名空间之间的流量隔离。这在你有多个命名空间并希望它们之间实现连接时非常有用。

以下示例演示如何为 ns1ns2 命名空间标记虚拟接口,以便这两个网络命名空间之间的流量不可见:

root@server:~# ovs-vsctl set port veth-ns1 tag=100
root@server:~# ovs-vsctl set port veth-ns2 tag=200
root@server:~# ovs-vsctl show
0ea38b4f-8943-4d5b-8d80-62ccb73ec9ec
Bridge "OVS-1"
 Port "OVS-1"
 Interface "OVS-1"
 type: internal
 Port "veth-ns1"
 tag: 100
 Interface "veth-ns1"
 Port "veth-ns2"
 tag: 200
 Interface "veth-ns2"
ovs_version: "2.0.2"
root@server:~#

现在,两个命名空间应当被隔离在各自的 VLAN 中,且 ping 命令应当失败:

root@server:~# ip netns exec ns1 ping -c 3 192.168.0.2
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
--- 192.168.0.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 1999ms
root@server:~# ip netns exec ns2 ping -c 3 192.168.0.1
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
--- 192.168.0.1 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 1999ms
root@server:~#

我们还可以使用我们在挂载和 UTC 命名空间示例中看到的 unshare 工具来创建一个新的网络命名空间:

root@server:~# unshare --net /bin/bash
root@server:~# ip a s
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
root@server:~# exit
root@server

使用 cgroups 进行资源管理

Cgroups 是内核特性,允许对单个进程或一组进程(称为 任务)的资源分配进行细粒度控制。在 LXC 环境中,这非常重要,因为它使得为任何给定容器分配内存、CPU 时间或 I/O 限制成为可能。

我们最关心的 cgroups 在下表中描述:

子系统 描述 定义在
cpu 为任务分配 CPU 时间 kernel/sched/core.c
cpuacct 记录 CPU 使用情况 kernel/sched/core.c
cpuset 为任务分配 CPU 核心 kernel/cpuset.c
memory 为任务分配内存 mm/memcontrol.c
blkio 限制设备的 I/O 访问 block/blk-cgroup.c
devices 允许/拒绝对设备的访问 security/device_cgroup.c
freezer 挂起/恢复任务 kernel/cgroup_freezer.c
net_cls 标记网络数据包 net/sched/cls_cgroup.c
net_prio 优先级网络流量 net/core/netprio_cgroup.c
hugetlb 限制 HugeTLB mm/hugetlb_cgroup.c

Cgroups 以层次结构的形式组织,表示为虚拟文件系统VFS)中的目录。类似于进程层次结构,每个进程都是 initsystemd 进程的后代,cgroups 会继承其父级的一些属性。系统上可以存在多个 cgroups 层次结构,每个层次表示单个或一组资源。可以拥有组合两个或更多子系统的层次结构,例如,内存和 I/O,分配给某个组的任务将对这些资源应用限制。

注意

如果你对内核中不同子系统的实现感兴趣,可以安装内核源代码并查看表格第三列中显示的 C 文件。

以下图示有助于可视化一个包含两个子系统——CPU 和 I/O——的单一层次结构:

使用 cgroups 进行资源管理

Cgroups 可以通过两种方式使用:

  • 通过手动操作挂载的 VFS 上的文件和目录

  • 使用各种软件包提供的用户空间工具,如 Debian/Ubuntu 上的 cgroup-bin 和 RHEL/CentOS 上的 libcgroup

让我们来看几个实际的例子,了解如何使用 cgroups 来限制资源。这将帮助我们更好地理解容器是如何工作的。

限制 I/O 吞吐量

假设我们有两个应用程序在一台服务器上运行,且它们是高度 I/O 绑定的:app1app2。我们希望白天将更多的带宽分配给 app1,而晚上则分配给 app2。这种 I/O 吞吐量优先级调整可以通过使用 blkio 子系统来实现。

首先,让我们通过挂载 cgroup VFS 来附加 blkio 子系统:

root@server:~# mkdir -p /cgroup/blkio
root@server:~# mount -t cgroup -o blkio blkio /cgroup/blkio
root@server:~# cat /proc/mounts | grep cgroup
blkio /cgroup/blkio cgroup rw, relatime, blkio, crelease_agent=/run/cgmanager/agents/cgm-release-agent.blkio 0 0
root@server:~#

接下来,创建两个优先级组,这些组将属于同一个 blkio 层级:

root@server:~# mkdir /cgroup/blkio/high_io
root@server:~# mkdir /cgroup/blkio/low_io
root@server:~#

我们需要获取 app1app2 进程的 PID,并将它们分配到 high_iolow_io 组:

root@server:~# pidof app1 | while read PID; do echo $PID >> /cgroup/blkio/high_io/tasks; done 
root@server:~# pidof app2 | while read PID; do echo $PID >> /cgroup/blkio/low_io/tasks; done
root@server:~#

限制 I/O 吞吐量

我们创建的 blkio 层级

tasks 文件是我们定义应应用限制的进程/任务的地方。

最后,让我们为 high_iolow_io cgroups 设置 10:1 的比例。这些 cgroups 中的任务将立即仅使用分配给它们的资源:

root@server:~# echo 1000 > /cgroup/blkio/high_io/blkio.weight
root@server:~# echo 100 > /cgroup/blkio/low_io/blkio.weight
root@server:~#

blkio.weight 文件定义了进程或进程组可用的 I/O 访问权重,值的范围从 100 到 1000。在这个例子中,1000100 的值创建了 10:1 的比例。

通过这样设置,低优先级的应用程序 app2 将仅使用大约 10% 的 I/O 操作,而高优先级的应用程序 app1 将使用大约 90%。

如果你在 Ubuntu 上列出 high_io 目录的内容,你将看到以下文件:

root@server:~# ls -la /cgroup/blkio/high_io/
drwxr-xr-x 2 root root 0 Aug 24 16:14 .
drwxr-xr-x 4 root root 0 Aug 19 21:14 ..
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_merged
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_merged_recursive
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_queued
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_queued_recursive
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_service_bytes
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_service_bytes_recursive
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_serviced
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_serviced_recursive
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_service_time
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_service_time_recursive
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_wait_time
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.io_wait_time_recursive
-rw-r--r-- 1 root root 0 Aug 24 16:14 blkio.leaf_weight
-rw-r--r-- 1 root root 0 Aug 24 16:14 blkio.leaf_weight_device
--w------- 1 root root 0 Aug 24 16:14 blkio.reset_stats
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.sectors
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.sectors_recursive
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.throttle.io_service_bytes
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.throttle.io_serviced
-rw-r--r-- 1 root root 0 Aug 24 16:14 blkio.throttle.read_bps_device
-rw-r--r-- 1 root root 0 Aug 24 16:14 blkio.throttle.read_iops_device
-rw-r--r-- 1 root root 0 Aug 24 16:14 blkio.throttle.write_bps_device
-rw-r--r-- 1 root root 0 Aug 24 16:14 blkio.throttle.write_iops_device
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.time
-r--r--r-- 1 root root 0 Aug 24 16:14 blkio.time_recursive
-rw-r--r-- 1 root root 0 Aug 24 16:49 blkio.weight
-rw-r--r-- 1 root root 0 Aug 24 17:01 blkio.weight_device
-rw-r--r-- 1 root root 0 Aug 24 16:14 cgroup.clone_children
--w--w--w- 1 root root 0 Aug 24 16:14 cgroup.event_control
-rw-r--r-- 1 root root 0 Aug 24 16:14 cgroup.procs
-rw-r--r-- 1 root root 0 Aug 24 16:14 notify_on_release
-rw-r--r-- 1 root root 0 Aug 24 16:14 tasks
root@server:~#

从前面的输出可以看到,只有某些文件是可写的。这取决于各种操作系统设置,例如正在使用的 I/O 调度器。

我们已经看到 tasksblkio.weight 文件的用途。以下是 blkio 子系统中最常用文件的简短描述:

文件 描述
blkio.io_merged 总共合并到请求中的读/写、同步或异步操作数
blkio.io_queued 在任何给定时间排队的读/写、同步或异步请求的总数
blkio.io_service_bytes 传输到或从指定设备的字节数
blkio.io_serviced 发往指定设备的 I/O 操作数量
blkio.io_service_time 请求调度与请求完成之间的总时间(单位:纳秒),针对指定设备
blkio.io_wait_time 指定设备的 I/O 操作在调度器队列中等待的总时间
blkio.leaf_weight 类似于 blkio.weight,可以应用于 完全公平队列 (CFQ) I/O 调度器
blkio.reset_stats 向此文件写入一个整数将重置所有统计信息
blkio.sectors 传输到或从指定设备的扇区数
blkio.throttle.io_service_bytes 传输到或从磁盘的字节数
blkio.throttle.io_serviced 向指定磁盘发出的 I/O 操作次数
blkio.time 分配给设备的磁盘时间(以毫秒为单位)
blkio.weight 为 cgroup 层级指定权重
blkio.weight_device blkio.weight相同,但指定一个块设备来应用限制
tasks 将任务附加到 cgroup

提示

需要记住的一点是,直接写入这些文件进行更改不会在服务器重启后保持不变。在本章的后面,您将学习如何使用用户空间工具生成持久化配置文件。

限制内存使用

memory子系统控制分配给进程的内存量以及进程可用的内存。这在多租户环境中尤其有用,因为在这种环境下,需要更好的控制每个用户进程可以使用多少内存,或者限制内存消耗较大的应用程序。像 LXC 这样的容器化解决方案可以使用memory子系统来管理实例的大小,而无需重启整个容器。

memory子系统执行资源会计,例如跟踪匿名页面、文件缓存、交换缓存和一般层级会计的利用率,这些都带来了一定的开销。因此,某些 Linux 发行版默认禁用memory cgroup。如果下面的命令失败,您需要通过指定以下 GRUB 参数并重启来启用它:

root@server:~# vim /etc/default/grub
RUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory"
root@server:~# grub-update && reboot

首先,让我们挂载memory cgroup:

root@server:~# mkdir -p /cgroup/memory
root@server:~# mount -t cgroup -o memory memory /cgroup/memory
root@server:~# cat /proc/mounts | grep memory
memory /cgroup/memory cgroup rw, relatime, memory, release_agent=/run/cgmanager/agents/cgm-release-agent.memory 0 0
root@server:~#

然后将app1的内存设置为 1GB:

root@server:~# mkdir /cgroup/memory/app1
root@server:~# echo 1G > /cgroup/memory/app1/memory.limit_in_bytes
root@server:~# cat /cgroup/memory/app1/memory.limit_in_bytes
1073741824
root@server:~# pidof app1 | while read PID; do echo $PID >> /cgroup/memory/app1/tasks; done
root@server:~#

限制内存使用

app1 进程的内存层级

类似于blkio子系统,tasks文件用于指定我们要添加到 cgroup 层级中的进程的 PID,memory.limit_in_bytes指定要分配的内存大小(以字节为单位)。

app1的内存层级包含以下文件:

root@server:~# ls -la /cgroup/memory/app1/
drwxr-xr-x 2 root root 0 Aug 24 22:05 .
drwxr-xr-x 3 root root 0 Aug 19 21:02 ..
-rw-r--r-- 1 root root 0 Aug 24 22:05 cgroup.clone_children
--w--w--w- 1 root root 0 Aug 24 22:05 cgroup.event_control
-rw-r--r-- 1 root root 0 Aug 24 22:05 cgroup.procs
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.failcnt
--w------- 1 root root 0 Aug 24 22:05 memory.force_empty
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.failcnt
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.limit_in_bytes
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.max_usage_in_bytes
-r--r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.slabinfo
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.tcp.failcnt
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.tcp.limit_in_bytes
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.tcp.max_usage_in_bytes
-r--r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.tcp.usage_in_bytes
-r--r--r-- 1 root root 0 Aug 24 22:05 memory.kmem.usage_in_bytes
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.limit_in_bytes
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.move_charge_at_immigrate
-r--r--r-- 1 root root 0 Aug 24 22:05 memory.numa_stat
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.oom_control
---------- 1 root root 0 Aug 24 22:05 memory.pressure_level
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.soft_limit_in_bytes
-r--r--r-- 1 root root 0 Aug 24 22:05 memory.stat
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.swappiness
-r--r--r-- 1 root root 0 Aug 24 22:05 memory.usage_in_bytes
-rw-r--r-- 1 root root 0 Aug 24 22:05 memory.use_hierarchy
-rw-r--r-- 1 root root 0 Aug 24 22:05 tasks
root@server:~#

内存子系统中的文件及其功能如下表所示:

文件 描述
memory.failcnt 显示内存限制命中的总次数
memory.force_empty 如果设置为0,则释放任务占用的内存
memory.kmem.failcnt 显示内核内存限制命中的总次数
memory.kmem.limit_in_bytes 设置或显示内核内存硬限制
memory.kmem.max_usage_in_bytes 显示最大内核内存使用量
memory.kmem.tcp.failcnt 显示 TCP 缓冲区内存限制命中的次数
memory.kmem.tcp.limit_in_bytes 设置或显示 TCP 缓冲区内存的硬限制
memory.kmem.tcp.max_usage_in_bytes 显示最大 TCP 缓冲区内存使用量
memory.kmem.tcp.usage_in_bytes 显示当前 TCP 缓冲区内存
memory.kmem.usage_in_bytes 显示当前的内核内存
memory.limit_in_bytes 设置或显示内存使用限制
memory.max_usage_in_bytes 显示最大内存使用量
memory.move_charge_at_immigrate 设置或显示移动费用的控制
memory.numa_stat 显示每个 NUMA 节点的内存使用情况
memory.oom_control 设置或显示 OOM 控制
memory.pressure_level 设置内存压力通知
memory.soft_limit_in_bytes 设置或显示内存使用的软限制
memory.stat 显示各种统计信息
memory.swappiness 设置或显示交换的级别
memory.usage_in_bytes 显示当前内存使用情况
memory.use_hierarchy 设置从子进程回收内存
tasks 将任务附加到 cgroup 中

限制进程可用的内存可能会触发 内存不足OOM)杀手,进而杀死正在运行的任务。如果这不是期望的行为,并且你希望进程被挂起,等待内存释放,则可以禁用 OOM 杀手:

root@server:~# cat /cgroup/memory/app1/memory.oom_control
oom_kill_disable 0
under_oom 0
root@server:~# echo 1 > /cgroup/memory/app1/memory.oom_control
root@server:~#

memory cgroup 在 memory.stat 文件中提供了广泛的记账统计信息,这些信息可能会引起关注:

root@server:~# head /cgroup/memory/app1/memory.stat
cache 43325     # Number of bytes of page cache memory
rss 55d43       # Number of bytes of anonymous and swap cache memory
rss_huge 0      # Number of anonymous transparent hugepages
mapped_file 2   # Number of bytes of mapped file
writeback 0     # Number of bytes of cache queued for syncing
pgpgin 0        # Number of charging events to the memory cgroup
pgpgout 0       # Number of uncharging events to the memory cgroup
pgfault 0       # Total number of page faults
pgmajfault 0    # Number of major page faults
inactive_anon 0 # Anonymous and swap cache memory on inactive LRU list

如果你需要在app1内存层级中启动一个新任务,你可以将当前的 shell 进程移动到tasks文件中,所有在这个 shell 中启动的其他进程将成为其直接后代,并继承相同的 cgroup 属性:

root@server:~# echo $$ > /cgroup/memory/app1/tasks
root@server:~# echo "The memory limit is now applied to all processes started from this shell"

cpu 和 cpuset 子系统

cpu 子系统将 CPU 时间调度到 cgroup 层级及其任务中。与 CFS 的默认行为相比,它提供了对 CPU 执行时间的更细粒度控制。

cpuset 子系统允许将 CPU 核心分配给一组任务,类似于 Linux 中的 taskset 命令。

cpucpuset 子系统提供的主要好处是为高度依赖 CPU 的应用提供更好的每个处理器核心利用率。它们还允许在某些时间段内将负载分配给通常空闲的核心。在多租户环境中,运行许多 LXC 容器时,cpucpuset cgroup 允许创建不同的实例大小和容器类型,例如每个容器仅暴露一个核心,并分配 40% 的调度工作时间。

作为示例,假设我们有两个进程 app1app2,我们希望 app1 使用 60% 的 CPU 时间,app2 仅使用 40%。我们首先挂载 cgroup VFS:

root@server:~# mkdir -p /cgroup/cpu
root@server:~# mount -t cgroup -o cpu cpu /cgroup/cpu
root@server:~# cat /proc/mounts | grep cpu
cpu /cgroup/cpu cgroup rw, relatime, cpu, release_agent=/run/cgmanager/agents/cgm-release-agent.cpu 0 0

然后我们创建两个子层级:

root@server:~# mkdir /cgroup/cpu/limit_60_percent
root@server:~# mkdir /cgroup/cpu/limit_40_percent

同时为每个任务分配 CPU 份额,其中 app1 将获得 60% 的调度时间,app2 将获得 40%:

root@server:~# echo 600 > /cgroup/cpu/limit_60_percent/cpu.shares
root@server:~# echo 400 > /cgroup/cpu/limit_40_percent/cpu.shares

最后,我们将 PID 移动到tasks文件中:

root@server:~# pidof app1 | while read PID; do echo $PID >> /cgroup/cpu/limit_60_percent/tasks; done
root@server:~# pidof app2 | while read PID; do echo $PID >> /cgroup/cpu/limit_40_percent/tasks; done
root@server:~#

cpu 子系统包含以下控制文件:

root@server:~# ls -la /cgroup/cpu/limit_60_percent/
drwxr-xr-x 2 root root 0 Aug 25 15:13 .
drwxr-xr-x 4 root root 0 Aug 19 21:02 ..
-rw-r--r-- 1 root root 0 Aug 25 15:13 cgroup.clone_children
--w--w--w- 1 root root 0 Aug 25 15:13 cgroup.event_control
-rw-r--r-- 1 root root 0 Aug 25 15:13 cgroup.procs
-rw-r--r-- 1 root root 0 Aug 25 15:13 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 Aug 25 15:13 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 Aug 25 15:14 cpu.shares
-r--r--r-- 1 root root 0 Aug 25 15:13 cpu.stat
-rw-r--r-- 1 root root 0 Aug 25 15:13 notify_on_release
-rw-r--r-- 1 root root 0 Aug 25 15:13 tasks
root@server:~#

以下是每个项的简要说明:

文件 描述
cpu.cfs_period_us 以微秒为单位的 CPU 资源重新分配
cpu.cfs_quota_us 任务在一个cpu.cfs_period_us周期内的运行时长,以微秒为单位
cpu.shares 任务可用的 CPU 时间的相对份额
cpu.stat 显示 CPU 时间统计信息
tasks 将任务附加到 cgroup 中

cpu.stat 文件特别重要:

root@server:~# cat /cgroup/cpu/limit_60_percent/cpu.stat
nr_periods 0        # number of elapsed period intervals, as specified in
 # cpu.cfs_period_us
nr_throttled 0      # number of times a task was not scheduled to run
 # because of quota limit
throttled_time 0    # total time in nanoseconds for which tasks have been
 # throttled
root@server:~#

为了演示cpuset子系统的工作方式,假设我们创建一个名为app1cpuset层次结构,其中包含 CPU 0 和 1,app2 cgroup 将只包含 CPU 1:

root@server:~# mkdir /cgroup/cpuset
root@server:~# mount -t cgroup -o cpuset cpuset /cgroup/cpuset
root@server:~# mkdir /cgroup/cpuset/app{1..2}
root@server:~# echo 0-1 > /cgroup/cpuset/app1/cpuset.cpus
root@server:~# echo 1 > /cgroup/cpuset/app2/cpuset.cpus
root@server:~# pidof app1 | while read PID; do echo $PID >> /cgroup/cpuset/app1/tasks limit_60_percent/tasks; done
root@server:~# pidof app2 | while read PID; do echo $PID >> /cgroup/cpuset/app2/tasks limit_40_percent/tasks; done
root@server:~#

要检查app1进程是否绑定到 CPU 0 和 1,我们可以使用:

root@server:~# taskset -c -p $(pidof app1)
pid 8052's current affinity list: 0,1
root@server:~# taskset -c -p $(pidof app2)
pid 8052's current affinity list: 1
root@server:~#

cpuset app1层次结构包含以下文件:

root@server:~# ls -la /cgroup/cpuset/app1/
drwxr-xr-x 2 root root 0 Aug 25 16:47 .
drwxr-xr-x 5 root root 0 Aug 19 21:02 ..
-rw-r--r-- 1 root root 0 Aug 25 16:47 cgroup.clone_children
--w--w--w- 1 root root 0 Aug 25 16:47 cgroup.event_control
-rw-r--r-- 1 root root 0 Aug 25 16:47 cgroup.procs
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.cpu_exclusive
-rw-r--r-- 1 root root 0 Aug 25 17:57 cpuset.cpus
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.mem_exclusive
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.memory_migrate
-r--r--r-- 1 root root 0 Aug 25 16:47 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.memory_spread_page
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.mems
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 Aug 25 16:47 cpuset.sched_relax_domain_level
-rw-r--r-- 1 root root 0 Aug 25 16:47 notify_on_release
-rw-r--r-- 1 root root 0 Aug 25 17:13 tasks
root@server:~#

控制文件的简要描述如下:

文件 描述
cpuset.cpu_exclusive 检查是否其他cpuset层次结构共享当前组中定义的设置
cpuset.cpus 允许在该cpuset中的进程执行的 CPU 物理编号列表
cpuset.mem_exclusive cpuset是否应独占其内存节点
cpuset.mem_hardwall 检查每个任务的用户分配是否保持独立
cpuset.memory_migrate 检查当cpuset.mems中的值发生变化时,内存中的页是否应迁移到新的节点
cpuset.memory_pressure 包含由进程创建的内存压力的运行平均值
cpuset.memory_spread_page 检查文件系统缓冲区是否应均匀分布在内存节点之间
cpuset.memory_spread_slab 检查内核的文件 I/O 操作的 slab 缓存是否应均匀分布在cpuset
cpuset.mems 指定此 cgroup 中的任务可以访问的内存节点
cpuset.sched_load_balance 检查内核是否应通过将进程从过载的 CPU 迁移到较少使用的 CPU 来平衡cpuset中的 CPU 负载
cpuset.sched_relax_domain_level 包含内核应尝试平衡负载的 CPU 范围的宽度
notify_on_release 检查释放后层次结构是否应接收特殊处理,且没有进程在使用它
tasks 将任务附加到 cgroup

cgroup 冻结子系统

freezer子系统可用于挂起当前运行任务的状态,以便对其进行分析,或创建一个可以用于将进程迁移到其他服务器的检查点。另一个使用场景是当某个进程对系统产生负面影响时,需要暂时暂停该进程,同时不丢失当前的状态数据。

下一个示例展示了如何挂起top进程的执行,检查其状态,然后恢复它。

首先,挂载freezer子系统并创建新的层次结构:

root@server:~# mkdir /cgroup/freezer
root@server:~# mount -t cgroup -o freezer freezer /cgroup/freezer
root@server:~# mkdir /cgroup/freezer/frozen_group
root@server:~# cat /proc/mounts | grep freezer
freezer /cgroup/freezer cgroup rw,relatime,freezer,release_agent=/run/cgmanager/agents/cgm-release-agent.freezer 0 0
root@server:~#

在一个新的终端中,启动top进程并观察它如何定期刷新。回到原始终端,将top的 PID 添加到frozen_group任务文件中,并观察其状态:

root@server:~# echo 25731 > /cgroup/freezer/frozen_group/tasks
root@server:~# cat /cgroup/freezer/frozen_group/freezer.state
THAWED
root@server:~#

要冻结进程,请执行以下命令:

root@server:~# echo FROZEN > /cgroup/freezer/frozen_group/freezer.state
root@server:~# cat /cgroup/freezer/frozen_group/freezer.state
FROZEN
root@server:~# cat /proc/25s731/status | grep -i state
State:      D (disk sleep)
root@server:~#

请注意,top进程的输出不再刷新,并且在检查其状态文件时,您可以看到它现在处于阻塞状态。

要恢复执行,执行以下命令:

root@server:~# echo THAWED > /cgroup/freezer/frozen_group/freezer.state
root@server:~# cat /proc/29328/status  | grep -i state
State:  S (sleeping)
root@server:~#

检查frozen_group层次结构会得到以下文件:

root@server:~# ls -la /cgroup/freezer/frozen_group/
drwxr-xr-x 2 root root 0 Aug 25 20:50 .
drwxr-xr-x 4 root root 0 Aug 19 21:02 ..
-rw-r--r-- 1 root root 0 Aug 25 20:50 cgroup.clone_children
--w--w--w- 1 root root 0 Aug 25 20:50 cgroup.event_control
-rw-r--r-- 1 root root 0 Aug 25 20:50 cgroup.procs
-r--r--r-- 1 root root 0 Aug 25 20:50 freezer.parent_freezing
-r--r--r-- 1 root root 0 Aug 25 20:50 freezer.self_freezing
-rw-r--r-- 1 root root 0 Aug 25 21:00 freezer.state
-rw-r--r-- 1 root root 0 Aug 25 20:50 notify_on_release
-rw-r--r-- 1 root root 0 Aug 25 20:59 tasks
root@server:~#

以下表格描述了几个感兴趣的文件:

文件 描述
freezer.parent_freezing 显示父状态。如果 cgroup 的祖先中没有一个是FROZEN则显示0;否则显示1
freezer.self_freezing 显示自身状态。如果自身状态为THAWED则显示0;否则显示1
freezer.state 将 cgroup 的自身状态设置为THAWEDFROZEN
tasks 将任务附加到 cgroup。

使用用户空间工具管理 cgroups 并持久化更改

通过直接操作目录和文件来处理 cgroup 子系统是原型设计和测试更改的快速便捷方式,但这也有一些缺点,即所做的更改不会在服务器重启后持续,且错误报告或处理不多。

为了解决这个问题,有一些提供用户空间工具和守护程序的软件包非常易于使用。让我们看几个例子。

要在 Debian/Ubuntu 上安装工具,请运行以下命令:

root@server:~# apt-get install -y cgroup-bin cgroup-lite libcgroup1
root@server:~# service cgroup-lite start

在 RHEL/CentOS 上,执行以下操作:

root@server:~# yum install libcgroup
root@server:~# service cgconfig start

要挂载所有子系统,请运行以下命令:

root@server:~# cgroups-mount
root@server:~# cat /proc/mounts | grep cgroup
cgroup /sys/fs/cgroup/memory cgroup rw,relatime,memory,release_agent=/run/cgmanager/agents/cgm-release-agent.memory 0 0
cgroup /sys/fs/cgroup/devices cgroup rw,relatime,devices,release_agent=/run/cgmanager/agents/cgm-release-agent.devices 0 0
cgroup /sys/fs/cgroup/freezer cgroup rw,relatime,freezer,release_agent=/run/cgmanager/agents/cgm-release-agent.freezer 0 0
cgroup /sys/fs/cgroup/blkio cgroup rw,relatime,blkio,release_agent=/run/cgmanager/agents/cgm-release-agent.blkio 0 0
cgroup /sys/fs/cgroup/perf_event cgroup rw,relatime,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event 0 0
cgroup /sys/fs/cgroup/hugetlb cgroup rw,relatime,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb 0 0
cgroup /sys/fs/cgroup/cpuset cgroup rw,relatime,cpuset,release_agent=/run/cgmanager/agents/cgm-release-agent.cpuset,clone_children 0 0
cgroup /sys/fs/cgroup/cpu cgroup rw,relatime,cpu,release_agent=/run/cgmanager/agents/cgm-release-agent.cpu 0 0
cgroup /sys/fs/cgroup/cpuacct cgroup rw,relatime,cpuacct,release_agent=/run/cgmanager/agents/cgm-release-agent.cpuacct 0 0

注意从前面的输出中 cgroups 的位置 - /sys/fs/cgroup。这是许多 Linux 发行版上的默认位置,在大多数情况下各个子系统已经被挂载。

要验证正在使用的 cgroup 子系统,可以使用以下命令检查:

root@server:~# cat /proc/cgroups
#subsys_name  hierarchy  num_cgroups  enabled
cpuset  7  1  1
cpu  8  2  1
cpuacct  9  1  1
memory  10  2  1
devices  11  1  1
freezer  12  1  1
blkio  6  3  1
perf_event  13  1  1
hugetlb  14  1  1

接下来,让我们创建一个blkio层级,并使用cgclassify将一个已运行的进程添加到其中。这与之前手动创建目录和文件的操作类似:

root@server:~# cgcreate -g blkio:high_io
root@server:~# cgcreate -g blkio:low_io
root@server:~# cgclassify -g blkio:low_io $(pidof app1)
root@server:~# cat /sys/fs/cgroup/blkio/low_io/tasks
8052
root@server:~# cgset -r blkio.weight=1000 high_io
root@server:~# cgset -r blkio.weight=100 low_io
root@server:~# cat /sys/fs/cgroup/blkio/high_io/blkio.weight
1000
root@server:~#

现在,我们已经定义了high_iolow_io cgroups,并向它们添加了一个进程,让我们生成一个配置文件,以便稍后重新应用这个设置:

root@server:~# cgsnapshot -s -f /tmp/cgconfig_io.conf
cpuset = /sys/fs/cgroup/cpuset;
cpu = /sys/fs/cgroup/cpu;
cpuacct = /sys/fs/cgroup/cpuacct;
memory = /sys/fs/cgroup/memory;
devices = /sys/fs/cgroup/devices;
freezer = /sys/fs/cgroup/freezer;
blkio = /sys/fs/cgroup/blkio;
perf_event = /sys/fs/cgroup/perf_event;
hugetlb = /sys/fs/cgroup/hugetlb;
root@server:~# cat /tmp/cgconfig_io.conf
# Configuration file generated by cgsnapshot
mount {
 blkio = /sys/fs/cgroup/blkio;
}
group low_io {
 blkio {
 blkio.leaf_weight="500";
 blkio.leaf_weight_device="";
 blkio.weight="100";
 blkio.weight_device="";
 blkio.throttle.write_iops_device="";
 blkio.throttle.read_iops_device="";
 blkio.throttle.write_bps_device="";
 blkio.throttle.read_bps_device="";
 blkio.reset_stats="";
 }
}
group high_io {
blkio {
 blkio.leaf_weight="500";
 blkio.leaf_weight_device="";
 blkio.weight="1000";
 blkio.weight_device="";
 blkio.throttle.write_iops_device="";
 blkio.throttle.read_iops_device="";
 blkio.throttle.write_bps_device="";
 blkio.throttle.read_bps_device="";
 blkio.reset_stats="";
 }
}
root@server:~#

要在high_io组中启动一个新进程,我们可以使用cgexec命令:

root@server:~# cgexec -g blkio:high_io bash
root@server:~# echo $$
19654
root@server:~# cat /sys/fs/cgroup/blkio/high_io/tasks
19654
root@server:~#

在上面的例子中,我们在high_io cgroup中启动了一个新的bash进程,通过查看tasks文件确认。

要将一个已运行的进程移动到memory子系统中,首先我们创建high_priolow_prio组,并使用cgclassify移动任务:

root@server:~# cgcreate -g cpu,memory:high_prio
root@server:~# cgcreate -g cpu,memory:low_prio
root@server:~# cgclassify -g cpu,memory:high_prio 8052
root@server:~# cat /sys/fs/cgroup/memory/high_prio/tasks
8052
root@server:~# cat /sys/fs/cgroup/cpu/high_prio/tasks
8052
root@server:~#

要设置内存和 CPU 限制,可以使用cgset命令。与此相反,记住我们使用echo命令手动将 PID 和内存限制移动到tasksmemory.limit_in_bytes文件中:

root@server:~# cgset -r memory.limit_in_bytes=1G low_prio
root@server:~# cat /sys/fs/cgroup/memory/low_prio/memory.limit_in_bytes
1073741824
root@server:~# cgset -r cpu.shares=1000 high_prio
root@server:~# cat /sys/fs/cgroup/cpu/high_prio/cpu.shares
1000
root@server:~#

要查看 cgroup 层级的外观,可以使用lscgroup实用工具:

root@server:~# lscgroup
cpuset:/
cpu:/
cpu:/low_prio
cpu:/high_prio
cpuacct:/
memory:/
memory:/low_prio
memory:/high_prio
devices:/
freezer:/
blkio:/
blkio:/low_io
blkio:/high_io
perf_event:/
hugetlb:/
root@server:~#

上述输出确认了存在blkiomemorycpu层级及其子层级。

完成后,可以使用cgdelete删除层级结构,这将删除 VFS 上相应的目录:

root@server:~# cgdelete -g cpu,memory:high_prio
root@server:~# cgdelete -g cpu,memory:low_prio
root@server:~# lscgroup
cpuset:/
cpu:/
cpuacct:/
memory:/
devices:/
freezer:/
blkio:/
blkio:/low_io
blkio:/high_io
perf_event:/
hugetlb:/
root@server:~#

要完全清除 cgroups,可以使用cgclear实用工具,它将卸载 cgroup 目录:

root@server:~# cgclear
root@server:~# lscgroup
cgroups can't be listed: Cgroup is not mounted
root@server:~#

使用 systemd 管理资源

随着 systemd 作为初始化系统的广泛采用,引入了新的方法来操作 cgroups。例如,如果内核启用了 CPU 控制器,systemd 会默认为每个服务创建一个 cgroup。通过在 systemd 配置文件中添加或移除 cgroup 子系统,可以更改这种行为,配置文件通常位于 /etc/systemd/system.conf

如果服务器上运行着多个服务,默认情况下,CPU 资源会被平均分配给它们,因为 systemd 对每个服务分配相等的权重。要改变这种行为,我们可以编辑其服务文件,定义 CPU 配额、分配的内存和 I/O。

以下示例演示了如何为 nginx 进程更改 CPU 配额、内存和 I/O 限制:

root@server:~# vim /etc/systemd/system/nginx.service
.include /usr/lib/systemd/system/httpd.service
[Service]
CPUShares=2000
MemoryLimit=1G
BlockIOWeight=100

要应用这些更改,首先重新加载 systemd,然后重新启动 nginx:

root@server:~#  systemctl daemon-reload
root@server:~#  systemctl restart httpd.service
root@server:~# 

这将创建并更新 /sys/fs/cgroup/systemd 中所需的控制文件,并应用限制。

总结

内核命名空间和 cgroups 的出现使得将进程组隔离到一个自包含的轻量级虚拟化包中成为可能;我们称之为容器。在本章中,我们看到容器提供了与其他基于完整虚拟化技术(如 KVM 和 Xen)相同的功能,而无需在同一操作系统中运行多个内核。LXC 充分利用了 Linux 的 cgroups 和命名空间,达到了这种隔离和资源控制的水平。

通过本章所获得的基础知识,你将能更好地理解底层的运作原理,这将使得排查故障和支持 Linux 容器的整个生命周期变得更加容易,正如我们在接下来的章节中所做的那样。

第二章:在 Linux 系统上安装和运行 LXC

LXC 利用内核命名空间和 cgroups 创建我们称之为容器的进程隔离,正如我们在前一章中看到的那样。因此,LXC 不是 Linux 内核中的一个独立软件组件,而是一组用户空间工具、liblxc库和各种语言绑定。

本章将涵盖以下主题:

  • 使用发行版包在 Ubuntu 和 CentOS 上安装 LXC

  • 从源代码编译和安装 LXC

  • 使用提供的模板和配置文件构建和启动容器

  • 使用debootstrapyum等工具手动构建根文件系统和配置文件

安装 LXC

在写本书时,LXC 有两个长期支持版本:1.0 和 2.0。它们提供的用户空间工具在命令行标志和弃用功能上有一些小差异,我会在使用时指出这些差异。

使用 apt 在 Ubuntu 上安装 LXC

让我们首先在 Ubuntu 14.04.5(Trusty Tahr)上安装 LXC 1.0:

  1. 安装主要的 LXC 包、工具和依赖项:

    root@ubuntu:~# lsb_release -dc
    Description:       Ubuntu 14.04.5 LTS
    Codename:          trusty 
    root@ubuntu:~# apt-get -y install -y lxc bridge-utils 
          debootstrap libcap-dev 
          cgroup-bin libpam-systemdbridge-utils
    root@ubuntu:~#
    
    
  2. Trusty Tahr 当前提供的包版本是 1.0.8:

    root@ubuntu:~# dpkg --list | grep lxc | awk '{print $2,$3}'
    liblxc1 1.0.8-0ubuntu0.3
    lxc 1.0.8-0ubuntu0.3
    lxc-templates 1.0.8-0ubuntu0.3
    python3-lxc 1.0.8-0ubuntu0.3
    root@ubuntu:~# 
    
    

要安装 LXC 2.0,我们需要使用 Backports 仓库:

  1. apt源文件中添加以下两行:

    root@ubuntu:~# vim /etc/apt/sources.list
    deb http://archive.ubuntu.com/ubuntu trusty-backports main 
          restricted universe multiverse
    deb-src http://archive.ubuntu.com/ubuntu trusty-backports 
          main restricted universe multiverse
    
    
  2. 从源站点重新同步软件包索引文件:

    root@ubuntu:~# apt-get update
    
    
  3. 安装主要的 LXC 包、工具和依赖项:

    root@ubuntu:~# apt-get -y install -y 
          lxc=2.0.3-0ubuntu1~ubuntu14.04.1 
          lxc1=2.0.3-0ubuntu1~ubuntu14.04.1 
          liblxc1=2.0.3-0ubuntu1~ubuntu14.04.1 python3-
          lxc=2.0.3-0ubuntu1~ubuntu14.04.1 cgroup-
          lite=1.11~ubuntu14.04.2 
          lxc-templates=2.0.3-0ubuntu1~ubuntu14.04.1bridge-utils
    root@ubuntu:~#
    
    
  4. 确保软件包版本在 2.x 分支上,本例中为 2.0.3:

    root@ubuntu:~# dpkg --list | grep lxc | awk '{print $2,$3}'
    liblxc1 2.0.3-0ubuntu1~ubuntu14.04.1
    lxc2.0.3-0ubuntu1~ubuntu14.04.1
    lxc-common    2.0.3-0ubuntu1~ubuntu14.04.1
    lxc-templates 2.0.3-0ubuntu1~ubuntu14.04.1
    lxc1          2.0.3-0ubuntu1~ubuntu14.04.1
    lxcfs         2.0.2-0ubuntu1~ubuntu14.04.1
    python3-lxc   2.0.3-0ubuntu1~ubuntu14.04.1
    root@ubuntu:~#
    
    

从源代码在 Ubuntu 上安装 LXC

要使用最新版的 LXC,您可以从上游的 GitHub 仓库下载源代码并进行编译:

  1. 首先,安装git并克隆仓库:

    root@ubuntu:~# apt-get install git
    root@ubuntu:~# cd /usr/src
    root@ubuntu:/usr/src# git clone https://github.com/lxc/lxc.git
    Cloning into 'lxc'...
    remote: Counting objects: 29252, done.
    remote: Compressing objects: 100% (156/156), done.
    remote: Total 29252 (delta 101), reused 0 (delta 0), 
          pack-reused 29096
    Receiving objects: 100% (29252/29252), 11.96 MiB | 12.62 
          MiB/s, done.
    Resolving deltas: 100% (21389/21389), done.
    root@ubuntu:/usr/src#
    
    
  2. 接下来,让我们安装构建工具和各种依赖项:

    root@ubuntu:/usr/src# apt-get install -y dev-utils 
          build-essential aclocal automake pkg-config git bridge-utils 
          libcap-dev libcgmanager-dev cgmanager
    root@ubuntu:/usr/src#
    
    
  3. 现在,生成configure脚本,该脚本将尝试为编译过程中使用的不同系统相关变量猜测正确的值:

    root@ubuntu:/usr/src# cd lxc
    root@ubuntu:/usr/src/lxc#./autogen.sh
    
    
  4. configure脚本提供了可以启用或禁用的选项,具体取决于您希望编译哪些功能。要了解可用的选项,并查看每个选项的简短描述,请运行以下命令:

    root@ubuntu:/usr/src/lxc# ./configure -help
    
    
  5. 现在是运行configure的时间了。在这个例子中,我将启用 Linux 能力和cgmanager,它将管理每个容器的 cgroup:

    root@ubuntu:/usr/src/lxc# ./configure --enable-capabilities 
          --enable-cgmanager
    ...
    ----------------------------
    Environment:
     - compiler: gcc
     - distribution: ubuntu
     - init script type(s): upstart,systemd
     - rpath: no
     - GnuTLS: no
     - Bash integration: yes
    Security features:
     - Apparmor: no
     - Linux capabilities: yes
     - seccomp: no
     - SELinux: no
     - cgmanager: yes
    Bindings:
     - lua: no
     - python3: no
    Documentation:
     - examples: yes
     - API documentation: no
     - user documentation: no
    Debugging:
     - tests: no
     - mutex debugging: no
    Paths:
    Logs in configpath: no
    root@ubuntu:/usr/src/lxc#
    
    

    从前面的简短输出中,我们可以看到编译后将会有哪些选项可用。请注意,我们现在没有启用任何安全功能,比如Apparmor

  6. 接下来,使用make进行编译:

    root@ubuntu:/usr/src/lxc# make
    
    
  7. 最后,安装二进制文件、库和模板:

    root@ubuntu:/usr/src/lxc# make install
    
    

    截至本文写作时,LXC 的二进制文件将其库文件搜索路径与安装路径不同。要修复此问题,只需将其复制到正确的位置:

    root@ubuntu:/usr/src/lxc# cp /usr/local/lib/liblxc.so* 
          /usr/lib/x86_64-linux-gnu/
    
    
  8. 要检查已编译并安装的版本,请执行以下代码:

    root@ubuntu:/usr/src/lxc# lxc-create --version
    2.0.0
    root@ubuntu:/usr/src/lxc# 
    
    

使用 yum 在 CentOS 上安装 LXC

CentOS 7 当前在其上游仓库中提供 LXC 1.0.8 版本。以下说明应该适用于 RHEL 7 和 CentOS 7:

  1. 安装主包和发行版模板:

    root@centos:~# cat /etc/redhat-release
    CentOS Linux release 7.2.1511 (Core)
    root@centos:~# yum install -y lxc lxc-templates
    root@centos:~#
    
    
  2. 检查已安装的包版本:

    root@centos:~# rpm -qa | grep lxc
    lua-lxc-1.0.8-1.el7.x86_64
    lxc-templates-1.0.8-1.el7.x86_64
    lxc-libs-1.0.8-1.el7.x86_64
    lxc-1.0.8-1.el7.x86_64
    root@centos:~#
    
    

从源代码在 CentOS 上安装 LXC

要安装最新版本的 LXC,我们需要从 GitHub 下载并编译它,类似于我们在 Ubuntu 上所做的:

  1. 安装构建工具、git 和各种依赖项:

    root@centos:# cd /usr/src
    root@centos:/usr/src# yum install -y libcap-devel libcgroup 
          bridge-utils git
    root@centos:/usr/src# yum groupinstall "Development tools"
    root@centos:/usr/src#
    
    
  2. 接下来,克隆仓库:

    root@centos:/usr/src# git clone https://github.com/lxc/lxc.git
    root@centos:/usr/src# cd lxc/
    root@centos:/usr/src/lxc#
    
    
  3. 生成配置文件:

    root@centos:/usr/src/lxc# ./autogen.sh
    root@centos:/usr/src#
    
    
  4. 准备软件进行编译:

    root@centos:/usr/src/lxc# ./configure
    ...
    ----------------------------
    Environment:
     - compiler: gcc
     - distribution: centos
     - init script type(s): sysvinit
     - rpath: no
     - GnuTLS: no
     - Bash integration: yes
    Security features:
     - Apparmor: no
     - Linux capabilities: yes
     - seccomp: no
     - SELinux: no
     - cgmanager: no
    Bindings:
     - lua: no
     - python3: no
    Documentation:
     - examples: yes
     - API documentation: yes
     - user documentation: no
    Debugging:
     - tests: no
     - mutex debugging: no
    Paths:
    Logs in configpath: no
    root@centos:/usr/src/lxc#
    
    
  5. 编译并安装二进制文件、库文件和发行版模板:

     root@centos:/usr/src/lxc# make && make install
    
    
  6. 将库文件复制到二进制文件预期的位置:

    root@centos:/usr/src/lxc# cp /usr/local/lib/liblxc.so* 
          /usr/lib64/
    
    
  7. 最后,要检查已编译和安装的版本,请执行以下代码:

    root@centos:/usr/src/lxc# lxc-create --version
    2.0.0
    root@centos:/usr/src/lxc#
    
    

    CentOS 7 使用 systemd 作为其初始化系统。要启动 LXC 服务,请运行以下命令:

    root@centos:/usr/src/lxc# systemctl start lxc.service
    root@centos:/usr/src/lxc# systemctl status lxc.service
     * lxc.service - LXC Container Initialization and Autoboot 
          Code
    Loaded: loaded (/usr/lib/systemd/system/lxc.service; 
          disabled; vendor preset: disabled)
    Active: active (exited) since Tue 2016-08-30 20:03:58 
          UTC; 6min ago
    Process: 10645 ExecStart=/usr/libexec/lxc/lxc-autostart-
          helper start (code=exited, status=0/SUCCESS)
    Process: 10638 ExecStartPre=/usr/libexec/lxc/lxc-devsetup 
          (code=exited, status=0/SUCCESS)
    Main PID: 10645 (code=exited, status=0/SUCCESS)
    CGroup: /system.slice/lxc.service
    Aug 30 20:03:28 centos systemd[1]: Starting LXC Container 
          Initialization and Autoboot Code...
    Aug 30 20:03:28 centos lxc-devsetup[10638]: Creating 
          /dev/.lxc
    Aug 30 20:03:28 centos lxc-devsetup[10638]: /dev is devtmpfs
    Aug 30 20:03:28 centos lxc-devsetup[10638]: Creating 
          /dev/.lxc/user
    Aug 30 20:03:58 centos lxc-autostart-helper[10645]: Starting 
          LXC autoboot containers:  [  OK  ]
    Aug 30 20:03:58 nova systemd[1]: Started LXC Container 
          Initialization and Autoboot Code.
    root@centos:/usr/src/lxc#
    
    

    为了确保 LXC 在安装过程中配置正确,请运行以下命令:

    root@centos:/usr/src/lxc# lxc-checkconfig
    Kernel configuration found at /boot/config-
          3.10.0-327.28.2.el7.x86_64
    --- Namespaces ---
    Namespaces: enabled
    Utsname namespace: enabled
    Ipc namespace: enabled
    Pid namespace: enabled
    User namespace: enabled
    Network namespace: enabled
    Multiple /dev/pts instances: enabled
    --- Control groups ---
    Cgroup: enabled
    Cgroup clone_children flag: enabled
    Cgroup device: enabled
    Cgroup sched: enabled
    Cgroup cpu account: enabled
    Cgroup memory controller: enabled
    Cgroup cpuset: enabled
    --- Misc ---
    Veth pair device: enabled
    Macvlan: enabled
    Vlan: enabled
    Bridges: enabled
    Advanced netfilter: enabled
    CONFIG_NF_NAT_IPV4: enabled
    CONFIG_NF_NAT_IPV6: enabled
    CONFIG_IP_NF_TARGET_MASQUERADE: enabled
    CONFIG_IP6_NF_TARGET_MASQUERADE: enabled
    CONFIG_NETFILTER_XT_TARGET_CHECKSUM: enabled
    --- Checkpoint/Restore ---
    checkpoint restore: enabled
    CONFIG_FHANDLE: enabled
    CONFIG_EVENTFD: enabled
    CONFIG_EPOLL: enabled
    CONFIG_UNIX_DIAG: enabled
    CONFIG_INET_DIAG: enabled
    CONFIG_PACKET_DIAG: enabled
    CONFIG_NETLINK_DIAG: enabled
    File capabilities: enabled
     Note : Before booting a new kernel, you can check its 
     configuration:
    usage : CONFIG=/path/to/config /usr/bin/lxc-checkconfig
    root@centos:/usr/src/lxc#
    
    

LXC 目录安装布局

以下表格显示了在包和源代码安装后创建的 LXC 目录布局。目录的设置因发行版和安装方法而异:

Ubuntu 包 CentOS 包 源代码安装 描述
/usr/share/lxc /usr/share/ lxc /usr/local/share/ lxc LXC 基础目录
/usr/share/lxc/ config /usr/share/lxc/ config /usr/local/share/lxc/ config 基于发行版的 LXC 配置文件集合
/usr/share/lxc/ templates /usr/share/lxc/ templates /usr/local/share/lxc/ templates 容器模板脚本集合
/usr/bin /usr/bin /usr/local/bin 大多数 LXC 二进制文件的位置
/usr/lib/x86_64-linux-gnu /usr/lib64 /usr/local/lib liblxc 库的位置
/etc/lxc /etc/lxc /usr/local/etc/ lxc 默认 LXC 配置文件的位置
/var/lib/ lxc/ /var/lib/ lxc/ /usr/local/var/ lib/lxc/ 创建的容器的根文件系统和配置位置
/var/log/lxc /var/log/lxc /usr/local/var/ log/lxc LXC 日志文件

在构建、启动和终止 LXC 容器时,我们将探索大多数目录。

提示

在从源代码构建 LXC 时,您可以通过传递参数给配置脚本(例如 configure --prefix)来更改默认的安装路径。

构建和操作 LXC 容器

与手动创建命名空间并通过 cgroups 应用资源限制相比,使用提供的用户空间工具来管理容器生命周期非常方便。本质上,这正是 LXC 工具所做的:创建和操作我们在第一章,Linux 容器简介 中看到的命名空间和 cgroups。LXC 工具实现了在第四章,LXC 与 Python 的代码集成 中将要看到的 liblxc API 中定义的功能。

LXC 附带了多种模板,用于为不同的 Linux 发行版构建根文件系统。我们可以使用这些模板来创建各种类型的容器。例如,在 CentOS 主机上运行 Debian 容器。我们也可以选择使用debootstrapyum等工具来构建自己的根文件系统,稍后我们将探讨这一点。

构建我们的第一个容器

我们可以使用模板创建我们的第一个容器。lxc-download文件像templates目录中的其他模板一样,是一个用 bash 编写的脚本:

root@ubuntu:~# ls -la /usr/share/lxc/templates/
drwxr-xr-x 2 root root  4096 Aug 29 20:03 .
drwxr-xr-x 6 root root  4096 Aug 29 19:58 ..
-rwxr-xr-x 1 root root 10557 Nov 18  2015 lxc-alpine
-rwxr-xr-x 1 root root 13534 Nov 18  2015 lxc-altlinux
-rwxr-xr-x 1 root root 10556 Nov 18  2015 lxc-archlinux
-rwxr-xr-x 1 root root  9878 Nov 18  2015 lxc-busybox
-rwxr-xr-x 1 root root 29149 Nov 18  2015 lxc-centos
-rwxr-xr-x 1 root root 10486 Nov 18  2015 lxc-cirros
-rwxr-xr-x 1 root root 17354 Nov 18  2015 lxc-debian
-rwxr-xr-x 1 root root 17757 Nov 18  2015 lxc-download
-rwxr-xr-x 1 root root 49319 Nov 18  2015 lxc-fedora
-rwxr-xr-x 1 root root 28253 Nov 18  2015 lxc-gentoo
-rwxr-xr-x 1 root root 13962 Nov 18  2015 lxc-openmandriva
-rwxr-xr-x 1 root root 14046 Nov 18  2015 lxc-opensuse
-rwxr-xr-x 1 root root 35540 Nov 18  2015 lxc-oracle
-rwxr-xr-x 1 root root 11868 Nov 18  2015 lxc-plamo
-rwxr-xr-x 1 root root  6851 Nov 18  2015 lxc-sshd
-rwxr-xr-x 1 root root 23494 Nov 18  2015 lxc-ubuntu
-rwxr-xr-x 1 root root 11349 Nov 18  2015 lxc-ubuntu-cloud
root@ubuntu:~#

如果你仔细检查这些脚本,你会注意到它们大多数是创建chroot环境,在这些环境中会安装包和各种配置文件,以便为选定的发行版创建根文件系统。

让我们首先使用lxc-download模板来构建一个容器,这会要求提供发行版、版本和架构,然后使用适当的模板为我们创建文件系统和配置:

root@ubuntu:~# lxc-create -t download -n c1
Setting up the GPG keyring
Downloading the image index
---
DIST       RELEASE    ARCH       VARIANT    BUILD
---
centos    6          amd64      default    20160831_02:16
centos    6          i386       default    20160831_02:16
centos    7          amd64      default    20160831_02:16
debian    jessie     amd64      default    20160830_22:42
debian    jessie     arm64      default    20160824_22:42
debian    jessie     armel      default    20160830_22:42
... 
ubuntu    trusty     amd64      default    20160831_03:49
ubuntu    trusty     arm64      default    20160831_07:50
ubuntu    yakkety    s390x      default    20160831_03:49
---
Distribution: ubuntu
Release: trusty
Architecture: amd64
Unpacking the rootfs
---
You just created an Ubuntu container (release=trusty, arch=amd64, variant=default)
To enable sshd, run: apt-get install openssh-server
For security reason, container images ship without user accounts
and without a root password.
Use lxc-attach or chroot directly into the rootfs to set a root password
or 
create user accounts.
root@ubuntu:~#

让我们列出所有容器:

root@ubuntu:~# lxc-ls -f
NAME                  STATE    IPV4  IPV6  AUTOSTART
----------------------------------------------------
c1                    STOPPED  -     -     NO
root@nova-perf:~#

提示

根据 LXC 的版本,一些命令选项可能会有所不同。如果遇到错误,请阅读每个工具的手册页面。

我们的容器目前没有运行,让我们在后台启动它并将日志级别提高到DEBUG

root@ubuntu:~# lxc-start -n c1 -d -l DEBUG

提示

在某些发行版中,LXC 在构建第一个容器时不会创建宿主桥接,这会导致错误。如果发生这种情况,可以通过运行brctl addbr virbr0命令来创建它。

执行以下命令列出所有容器:

root@ubuntu:~# lxc-ls -f
NAME                  STATE    IPV4        IPV6  AUTOSTART
----------------------------------------------------------
c1                    RUNNING  10.0.3.190  -     NO
root@ubuntu:~# 

要获取有关容器的更多信息,请运行以下命令:

root@ubuntu:~# lxc-info -n c1
Name:           c1
State:          RUNNING
PID:            29364
IP:             10.0.3.190
CPU use:        1.46 seconds
BlkIO use:      112.00 KiB
Memory use:     6.34 MiB
KMem use:       0 bytes
Link:           vethVRD8T2
 TX bytes:      4.28 KiB
 RX bytes:      4.43 KiB
 Total bytes:   8.70 KiB
root@ubuntu:~#

新的容器现在已连接到宿主机的桥接lxcbr0

root@ubuntu:~# brctl show
bridge name        bridge id              STP enabled        interfaces
lxcbr0         8000.fea50feb48ac          no             vethVRD8T2 
root@ubuntu:~# ip a s lxcbr0
4: lxcbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether fe:a5:0f:eb:48:ac brd ff:ff:ff:ff:ff:ff
inet 10.0.3.1/24 brd 10.0.3.255 scope global lxcbr0
valid_lft forever preferred_lft forever
inet6 fe80::465:64ff:fe49:5fb5/64 scope link
valid_lft forever preferred_lft forever 
root@ubuntu:~# ip a s vethVRD8T2
8: vethVRD8T2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master lxcbr0 state UP group default qlen 1000
link/ether fe:a5:0f:eb:48:ac brd ff:ff:ff:ff:ff:ff
inet6 fe80::fca5:fff:feeb:48ac/64 scope link
valid_lft forever preferred_lft forever
root@ubuntu:~#

使用下载模板并未指定任何网络设置时,容器会从dnsmasq服务器获取 IP 地址,这个服务器在私有网络10.0.3.0/24上运行。宿主机通过iptables中的 NAT 规则允许容器连接到其他网络和互联网:

root@ubuntu:~# iptables -L -n -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  10.0.3.0/24         !10.0.3.0/24
root@ubuntu:~#

连接到桥接的其他容器可以相互访问并访问宿主机,只要它们都连接到同一个桥接且未标记为不同的 VLAN ID。

让我们看看容器启动后进程树的样子:

root@ubuntu:~# ps axfww
...
1552 ?        S      0:00 dnsmasq -u lxc-dnsmasq --strict-order --bind-interfaces --pid-file=/run/lxc/dnsmasq.pid --conf-file= --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
29356 ?        Ss     0:00 lxc-start -n c1 -d -l DEBUG
29364 ?        Ss     0:00  \_ /sbin/init
29588 ?        S      0:00      \_ upstart-udev-bridge --daemon
29597 ?        Ss     0:00      \_ /lib/systemd/systemd-udevd --daemon
29667 ?        Ssl    0:00      \_ rsyslogd
29688 ?        S      0:00      \_ upstart-file-bridge --daemon
29690 ?        S      0:00      \_ upstart-socket-bridge --daemon
29705 ?        Ss     0:00      \_ dhclient -1 -v -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases eth0
29775 pts/6    Ss+    0:00      \_ /sbin/getty -8 38400 tty4
29777 pts/1    Ss+    0:00      \_ /sbin/getty -8 38400 tty2
29778 pts/5    Ss+    0:00      \_ /sbin/getty -8 38400 tty3
29787 ?        Ss     0:00      \_ cron
29827 pts/7    Ss+    0:00      \_ /sbin/getty -8 38400 console
29829 pts/0    Ss+    0:00      \_ /sbin/getty -8 38400 tty1
root@ubuntu:~#

注意新生成的init子进程,它是从lxc-start命令克隆出来的。它在实际容器中的 PID 是1

接下来,让我们运行attach命令与容器交互,列出所有进程和网络接口,并检查连接性:

root@ubuntu:~# lxc-attach -n c1
root@c1:~# ps axfw
PID TTY      STAT   TIME COMMAND
1 ?        Ss     0:00 /sbin/init
176 ?        S      0:00 upstart-udev-bridge --daemon
185 ?        Ss     0:00 /lib/systemd/systemd-udevd --daemon
255 ?        Ssl    0:00 rsyslogd
276 ?        S      0:00 upstart-file-bridge --daemon
278 ?        S      0:00 upstart-socket-bridge --daemon
293 ?        Ss     0:00 dhclient -1 -v -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases eth0
363 lxc/tty4 Ss+    0:00 /sbin/getty -8 38400 tty4
365 lxc/tty2 Ss+    0:00 /sbin/getty -8 38400 tty2
366 lxc/tty3 Ss+    0:00 /sbin/getty -8 38400 tty3
375 ?        Ss     0:00 cron
415 lxc/console Ss+   0:00 /sbin/getty -8 38400 console
417 lxc/tty1 Ss+    0:00 /sbin/getty -8 38400 tty1
458 ?        S      0:00 /bin/bash
468 ?        R+     0:00 ps ax 
root@c1:~# ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
7: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:16:3e:b2:34:8a brd ff:ff:ff:ff:ff:ff
inet 10.0.3.190/24 brd 10.0.3.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::216:3eff:feb2:348a/64 scope link
valid_lft forever preferred_lft forever 
root@c1:~# ping -c 3 google.com
PING google.com (216.58.192.238) 56(84) bytes of data.
64 bytes from ord30s26-in-f14.1e100.net (216.58.192.238): icmp_seq=1 ttl=52 time=1.77 ms
64 bytes from ord30s26-in-f14.1e100.net (216.58.192.238): icmp_seq=2 ttl=52 time=1.58 ms
64 bytes from ord30s26-in-f14.1e100.net (216.58.192.238): icmp_seq=3 ttl=52 time=1.75 ms
--- google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.584/1.705/1.779/0.092 ms 
root@c1:~# exit
exit
root@ubuntu:~#

提示

在某些发行版中,如 CentOS,或者如果是从源代码安装的,dnsmasq服务器默认未配置和启动。你可以手动安装并配置它,或者按我在本章后面展示的那样,给容器配置 IP 地址和默认网关。

注意,一旦我们连接到容器,终端的主机名就发生了变化。这是 LXC 如何使用 UTS 命名空间的一个例子,正如我们在第一章,Linux 容器简介中看到的。

让我们检查一下构建 c1 容器后创建的目录:

root@ubuntu:~# ls -la /var/lib/lxc/c1/
total 16
drwxrwx---  3 root root 4096 Aug 31 20:40 .
drwx------  3 root root 4096 Aug 31 21:01 ..
-rw-r--r--  1 root root  516 Aug 31 20:40 config
drwxr-xr-x 21 root root 4096 Aug 31 21:00 rootfs
root@ubuntu:~#

rootfs 目录看起来像一个普通的 Linux 文件系统。您可以通过直接修改那里文件或使用 chroot 来操作容器。

为了演示这一点,让我们通过 chroot 到其 rootfs 来更改 c1 容器的根密码,而不是通过连接到容器来实现:

root@ubuntu:~# cd /var/lib/lxc/c1/
root@ubuntu:/var/lib/lxc/c1# chroot rootfs
root@ubuntu:/# ls -al
total 84
drwxr-xr-x 21 root root 4096 Aug 31 21:00 .
drwxr-xr-x 21 root root 4096 Aug 31 21:00 ..
drwxr-xr-x  2 root root 4096 Aug 29 07:33 bin
drwxr-xr-x  2 root root 4096 Apr 10  2014 boot
drwxr-xr-x  4 root root 4096 Aug 31 21:00 dev
drwxr-xr-x 68 root root 4096 Aug 31 22:12 etc
drwxr-xr-x  3 root root 4096 Aug 29 07:33 home
drwxr-xr-x 12 root root 4096 Aug 29 07:33 lib
drwxr-xr-x  2 root root 4096 Aug 29 07:32 lib64
drwxr-xr-x  2 root root 4096 Aug 29 07:31 media
drwxr-xr-x  2 root root 4096 Apr 10  2014 mnt
drwxr-xr-x  2 root root 4096 Aug 29 07:31 opt
drwxr-xr-x  2 root root 4096 Apr 10  2014 proc
drwx------  2 root root 4096 Aug 31 22:12 root
drwxr-xr-x  8 root root 4096 Aug 31 20:54 run
drwxr-xr-x  2 root root 4096 Aug 29 07:33 sbin
drwxr-xr-x  2 root root 4096 Aug 29 07:31 srv
drwxr-xr-x  2 root root 4096 Mar 13  2014 sys
drwxrwxrwt  2 root root 4096 Aug 31 22:12 tmp
drwxr-xr-x 10 root root 4096 Aug 29 07:31 usr
drwxr-xr-x 11 root root 4096 Aug 29 07:31 var
root@ubuntu:/# passwd
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully 
root@ubuntu:/# exit
exit
root@ubuntu:/var/lib/lxc/c1#

注意,当我们使用 chroot 并在退出受限环境后,控制台上的路径发生了变化。

为了测试根密码,我们首先连接到容器,然后使用 ssh 连接并安装 安全外壳 (SSH) 服务器:

root@ubuntu:~# lxc-attach -n c1
root@c1:~# apt-get update&&apt-get install -y openssh-server
root@c1:~# sed -i 's/without-password/yes/g' /etc/ssh/sshd_config
root@c1:~#service ssh restart
root@c1:/# exit
exit 
root@ubuntu:/var/lib/lxc/c1# ssh 10.0.3.190
root@10.0.3.190's password:
Welcome to Ubuntu 14.04.5 LTS (GNU/Linux 3.13.0-91-generic x86_64)
* Documentation:  https://help.ubuntu.com/
Last login: Wed Aug 31 22:25:39 2016 from 10.0.3.1
root@c1:~# exit
logout
Connection to 10.0.3.190 closed.
root@ubuntu:/var/lib/lxc/c1#

我们成功地通过 ssh 连接到容器,并使用之前手动设置的根密码。

在 Ubuntu 上使用 debootstrap 创建自定义容器

使用提供的发行版模板脚本和配置文件是最迅速的 LXC 配置方法。然而,要完全控制根文件系统的布局——应该包含哪些软件包、块设备和网络设置——则需要更为手动的方法。

为此,我们可以使用 debootstrap 工具来创建容器的 rootfs,然后手动创建描述容器属性的配置文件。这在 Ubuntu 和 RHEL/CentOS 发行版上有效,并且提供了一种在 RHEL/CentOS 和 Debian/Ubuntu 系统上运行 Debian 和 Ubuntu 容器的方式。

首先安装debootstrap,如果尚未安装。在 Ubuntu 上,运行以下命令:

root@ubuntu:~# apt-get install -y debootstrap

类似地,在 CentOS 上,运行以下命令:

root@centos:~# yum install -y debootstrap

为了创建稳定的 Debian 发行版文件系统,我们可以提供以下参数:

root@ubuntu:~# debootstrap --arch=amd64 --include="openssh-server vim" stable ~/container http://httpredir.debian.org/debian/
W: Cannot check Release signature; keyring file not available /usr/share/keyrings/debian-archive-keyring.gpg
I: Retrieving Release
I: Retrieving Packages
I: Validating Packages
...
I: Configuring libc-bin...
I: Configuring systemd...
I: Base system installed successfully.
root@ubuntu:~#

我们指定了架构、要安装的软件包、操作系统的发行版以及 rootfs 将创建的位置,在本例中是 ~/container

接下来,我们需要为容器准备一个 config 文件。指定各种 LXC 属性有许多可选项。让我们从一个相对简单的配置开始:

root@ubuntu:~# vim ~/config
lxc.devttydir = lxc
lxc.pts = 1024
lxc.tty = 4
lxc.pivotdir = lxc_putold
lxc.cgroup.devices.deny = a
lxc.cgroup.devices.allow = c *:* m
lxc.cgroup.devices.allow = b *:* m
lxc.cgroup.devices.allow = c 1:3 rwm
lxc.cgroup.devices.allow = c 1:5 rwm
lxc.cgroup.devices.allow = c 1:7 rwm
lxc.cgroup.devices.allow = c 5:0 rwm
lxc.cgroup.devices.allow = c 5:1 rwm
lxc.cgroup.devices.allow = c 5:2 rwm
lxc.cgroup.devices.allow = c 1:8 rwm
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 136:* rwm
lxc.cgroup.devices.allow = c 10:229 rwm
lxc.cgroup.devices.allow = c 254:0 rm
lxc.cgroup.devices.allow = c 10:200 rwm
lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
lxc.mount.entry = /sys/fs/fuse/connections sys/fs/fuse/connections none bind,optional 0 0
lxc.mount.entry = /sys/kernel/debug sys/kernel/debug none bind,optional 0 0
lxc.mount.entry = /sys/kernel/security sys/kernel/security none bind,optional 0 0
lxc.mount.entry = /sys/fs/pstore sys/fs/pstore none bind,optional 0 0
lxc.mount.entry = mqueue dev/mqueue mqueue rw,relatime,create=dir,optional 0 0
# Container specific configuration
lxc.arch = x86_64
lxc.rootfs = /root/container
lxc.rootfs.backend = dir
lxc.utsname = manual_container
# Network configuration
lxc.network.type = veth
lxc.network.link = lxcbr0
lxc.network.flags = up
lxc.network.hwaddr = 00:16:3e:e4:68:91
lxc.network.ipv4 = 10.0.3.151/24 10.0.3.255
lxc.network.ipv4.gateway = 10.0.3.1
# Limit the container memory to 512MB
lxc.cgroup.memory.limit_in_bytes = 536870912

注意

上述配置是在 LXC 2.0 上测试的,可能与 1.0 版本分支上的版本不兼容。

以下表格简要总结了我们正在使用的最重要的选项:

Option Description
lxc.devttydir 控制台设备在 /dev/ 中的位置
lxc.tty 为容器提供的 TTY 数量
lxc.cgroup.devices.allow 容器中允许的设备列表
lxc.mount 要挂载的设备
lxc.arch 容器的架构
lxc.rootfs 根文件系统的位置
lxc.rootfs.backend 后端存储的类型
lxc.utsname 容器的主机名
lxc.network.type 网络虚拟化的类型
lxc.network.link 容器将连接的主机桥接器的名称
lxc.network.flags 启动网络接口
lxc.network.hwaddr 网络接口的 MAC 地址
lxc.network.ipv4 如果不使用 DHCP,网络接口的 IP 地址
lxc.network.ipv4.gateway 容器内的默认网关
lxc.cgroup 设置 cgroup 参数,如memorycpublkio

提示

欲了解所有可用的配置选项,请参阅lxc.container.conf手册页。

配置完成后,让我们创建容器:

root@ubuntu:~# lxc-create --name manual_container -t none -B dir --dir ~/container -f ~/config

注意,这次我们没有指定模板,而是提供了之前用debootstrap创建的rootfs

让我们通过将日志设置为DEBUG并将其重定向到一个文件来启动容器,以防我们需要排查错误:

root@ubuntu:~# lxc-start -n manual_container -l DEBUG -o container.log
root@ubuntu:~# lxc-ls -f
NAME             STATE   AUTOSTART GROUPS IPV4       IPV6
manual_container RUNNING 0         -      10.0.3.151 - 
root@ubuntu:~# lxc-info -n manual_container
Name:           manual_container
State:          RUNNING
PID:            4283
IP:             10.0.3.151
CPU use:        0.21 seconds
BlkIO use:      0 bytes
Memory use:     11.75 MiB
KMem use:       0 bytes
Link:           vethU29DXE
TX bytes:      690 bytes
RX bytes:      840 bytes
Total bytes:   1.49 KiB
root@ubuntu:~#

为了测试,我们可以像往常一样运行attach

root@ubuntu:~# lxc-attach -n manual_container
[root@manual_container ~]# exit
exit
root@ubuntu:~#

使用 yum 在 CentOS 上制作自定义容器

在 CentOS 7 上,我们可以使用debootstrap工具创建基于 Debian 和 Ubuntu 的根文件系统,并像前一节中描述的那样构建容器。

然而,要构建 RHEL、Fedora 或 CentOS 容器,我们需要使用像rpmyumdownloaderyum这样的工具。让我们看一个稍微复杂一点的例子,构建一个新的 CentOS rootfs,容器将使用它:

  1. 首先,创建一个将包含根文件系统的目录:

    root@centos:~# mkdir container
    
    
  2. 在创建container目录后,我们需要创建并初始化rpm软件包数据库:

    root@centos:~# rpm --root /root/container -initdb
    root@centos:~# ls -la container/var/lib/rpm/
    total 108
    drwxr-xr-x. 2 root root  4096 Sep  1 19:13 .
    drwxr-xr-x. 3 root root    16 Sep  1 19:13 ..
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Basenames
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Conflictname
    -rw-r--r--. 1 root root     0 Sep  1 19:13 .dbenv.lock
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Dirnames
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Group
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Installtid
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Name
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Obsoletename
    -rw-r--r--. 1 root root 12288 Sep  1 19:13 Packages
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Providename
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Requirename
    -rw-r--r--. 1 root root     0 Sep  1 19:13 .rpm.lock
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Sha1header
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Sigmd5
    -rw-r--r--. 1 root root  8192 Sep  1 19:13 Triggername
    root@centos:~#
    
    
  3. 数据库初始化完成后,我们可以下载 CentOS 发行版的发布文件。如果你更倾向于构建一个 Fedora 容器,可以在命令行中将centos-release替换为fedora-release。发布文件包含yum仓库和yumrpm使用的其他重要文件:

    root@centos:~# yumdownloader --destdir=/tmp centos-release
    Loaded plugins: fastestmirror
    Loading mirror speeds from cached hostfile
    * base: mirrors.evowise.com
     * extras: mirror.netdepot.com
     * updates: mirror.symnds.com
    centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm   
          | 23 kB  00:00:00
    root@centos:~# ls -la /tmp/centos-release
          -7-2.1511.el7.centos.2.10.x86_64.rpm
    -rw-r--r--. 1 root root 23516 Dec  9  2015 /tmp/centos-release-
          7-2.1511.el7.centos.2.10.x86_64.rpm
    root@centos:~# 
    
    
  4. 接下来,安装containerroot目录下的rpm软件包的发布文件:

    root@centos:~# rpm --root /root/container -ivh --nodeps 
          /tmp/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm
    warning: /tmp/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm: 
          Header V3 
          RSA/SHA256 Signature, key ID f4a80eb5: NOKEY
    Preparing...    
          ################################# [100%]
    Updating / installing...
     1:centos-release-7-
          2.1511.el7.cento#################################[100%]
     root@centos:~# ls -la container/
    total 8
    drwxr-xr-x. 5 root root   36 Sep  1 19:19 .
    dr-xr-x---. 4 root root 4096 Sep  1 19:13 ..
    drwxr-xr-x. 6 root root 4096 Sep  1 19:19 etc
    drwxr-xr-x. 4 root root   28 Sep  1 19:19 usr
    drwxr-xr-x. 3 root root   16 Sep  1 19:13 var
    root@centos:~#
    
    

    从前面的输出可以看出,container文件系统开始成形。它目前包含了使用包管理器和安装其余系统文件所需的所有文件。

  5. 现在,到了在rootfs中安装最小化的 CentOS 发行版的时刻;这与debootstrap在 Debian 和 Ubuntu 中所做的类似:

    root@centos:~# yum --installroot=/root/container -y group install 
          "Minimal install"
    Loaded plugins: fastestmirror
    There is no installed groups file.
    Maybe run: yum groups mark convert (see man yum)
    Determining fastest mirrors
     * base: centos.aol.com
     * extras: mirror.lug.udel.edu
     * updates: mirror.symnds.com
    Resolving Dependencies
    --> Running transaction check
    ---> Package NetworkManager.x86_64 1:1.0.6-30.el7_2 will be installed
    --> Processing Dependency: ppp = 2.4.5 for package: 1:NetworkManager-
          1.0.6-30.el7_2.x86_64
    --> Processing Dependency: NetworkManager-libnm(x86-64) = 
          1:1.0.6-30.el7_2 for 
          package: 1:NetworkManager-1.0.6-30.el7_2.x86_64
    --> Processing Dependency: wpa_supplicant >= 1:1.1 for 
          package: 1:NetworkManager-
          1.0.6-30.el7_2.x86_64
    --> Processing Dependency: libnl3 >= 3.2.21-7 for 
          package: 1:NetworkManager-
          1.0.6-30.el7_2.x86_64
    --> Processing Dependency: glib2 >= 2.32.0 for 
          package: 1:NetworkManager-
          1.0.6-30.el7_2.x86_64
    ...
    Complete!
    root@centos:~# 
    
    
  6. 现在,~/container目录包含了完整的 CentOS 7 发行版的根文件系统:

    root@centos:~# ls -la container/
    total 32
    dr-xr-xr-x. 17 root root 4096 Sep  1 19:20 .
    dr-xr-x---.  4 root root 4096 Sep  1 19:13 ..
    lrwxrwxrwx.  1 root root    7 Sep  1 19:20 bin -> usr/bin
    dr-xr-xr-x.  3 root root   43 Sep  1 19:21 boot
    drwxr-xr-x.  2 root root   17 Sep  1 19:21 dev
    drwxr-xr-x. 74 root root 8192 Sep  1 19:21 etc
    drwxr-xr-x.  2 root root    6 Aug 12  2015 home
    lrwxrwxrwx.  1 root root    7 Sep  1 19:20 lib -> usr/lib
    lrwxrwxrwx.  1 root root    9 Sep  1 19:20 lib64 -> usr/lib64
    drwxr-xr-x.  2 root root    6 Aug 12  2015 media
    drwxr-xr-x.  2 root root    6 Aug 12  2015 mnt
    drwxr-xr-x.  2 root root    6 Aug 12  2015 opt
    dr-xr-xr-x.  2 root root    6 Aug 12  2015 proc
    dr-xr-x---.  2 root root   86 Sep  1 19:21 root
    drwxr-xr-x. 16 root root 4096 Sep  1 19:21 run
    lrwxrwxrwx.  1 root root    8 Sep  1 19:20 sbin -> usr/sbin
    drwxr-xr-x.  2 root root    6 Aug 12  2015 srv
    dr-xr-xr-x.  2 root root    6 Aug 12  2015 sys
    drwxrwxrwt.  7 root root   88 Sep  1 19:21 tmp
    drwxr-xr-x. 13 root root 4096 Sep  1 19:20 usr
    drwxr-xr-x. 19 root root 4096 Sep  1 19:21 var
    root@centos:~#
    
    

    如果你希望在容器构建之前安装更多的包或进行更改,可以通过chroot进入~/containers并像往常一样进行操作。

  7. 接下来,如果桥接器尚未存在,创建它,并编写容器配置文件:

    root@centos:~# brct addbr lxcbr0
    root@centos:~# brct show
    bridge name    bridge id          STP enabled       interfaces
    lxcbr0         8000.000000000000          no 
    root@centos:~# vim config
    lxc.devttydir = lxc
    lxc.pts = 1024
    lxc.tty = 4
    lxc.pivotdir = lxc_putold
    lxc.cgroup.devices.deny = a
    lxc.cgroup.devices.allow = c *:* m
    lxc.cgroup.devices.allow = b *:* m
    lxc.cgroup.devices.allow = c 1:3 rwm
    lxc.cgroup.devices.allow = c 1:5 rwm
    lxc.cgroup.devices.allow = c 1:7 rwm
    lxc.cgroup.devices.allow = c 5:0 rwm
    lxc.cgroup.devices.allow = c 5:1 rwm
    lxc.cgroup.devices.allow = c 5:2 rwm
    lxc.cgroup.devices.allow = c 1:8 rwm
    lxc.cgroup.devices.allow = c 1:9 rwm
    lxc.cgroup.devices.allow = c 136:* rwm
    lxc.cgroup.devices.allow = c 10:229 rwm
    lxc.cgroup.devices.allow = c 254:0 rm
    lxc.cgroup.devices.allow = c 10:200 rwm
    lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
    lxc.mount.entry = /sys/fs/fuse/connections sys/fs/fuse/connections 
          none bind,optional 0 0
    lxc.mount.entry = /sys/kernel/debug sys/kernel/debug none 
          bind,optional 0 0
    lxc.mount.entry = /sys/kernel/security sys/kernel/security none 
          bind,optional 0 0
    lxc.mount.entry = /sys/fs/pstore sys/fs/pstore none bind,optional 0 0
    lxc.mount.entry = mqueue dev/mqueue mqueue 
          rw,relatime,create=dir,optional 0 0
    # Container specific configuration
    lxc.arch = x86_64
    lxc.rootfs = /root/container
    lxc.rootfs.backend = dir
    lxc.utsname = c1
    # Network configuration
    lxc.network.type = veth
    lxc.network.link = lxcbr0
    lxc.network.flags = up
    lxc.network.hwaddr = 00:16:3e:e4:68:92
    lxc.network.ipv4 = 10.0.3.152/24 10.0.3.255
    lxc.network.ipv4.gateway = 10.0.3.1
    
    
  8. 配置好rootfsconfig后,让我们创建并启动容器:

    root@centos:~# lxc-create --name c1 -t none -B dir --dir 
          ~/container -f ~/config
    root@centos:~# lxc-ls -f
    NAME STATE   AUTOSTART GROUPS IPV4       IPV6
    c1   RUNNING 0         -      10.0.3.151 -
    root@centos:~#
    
    
  9. 最后,我们可以像往常一样运行attach

    root@centos:~# lxc-attach -n c1
    [root@c1 ~]# exit
    exit
    root@centos:~# 
    
    

总结

LXC 提供了一套工具,使得构建、启动和操作容器变得非常简单和方便。使用包含的模板和配置文件可以进一步简化这一过程。在本章中,我们展示了如何在 Ubuntu 和 CentOS 发行版上安装、配置和启动 LXC 的实际示例。你学习了如何创建容器根文件系统,以及如何编写简单的配置文件。

在下一章,我们将了解如何在 LXC 中配置系统资源,并通过使用libvirt工具包和库,探索与 LXC 协同工作的替代方法。

第三章:使用原生和 Libvirt 工具的命令行操作

LXC 支持多种后备存储用于其根文件系统。在 第二章, 在 Linux 系统上安装和运行 LXC 中,我们使用了默认的 dir 类型,该类型在 /var/lib/lxc/containername/rootfs 下创建一个目录。使用默认存储在某些情况下可能足够,但为了利用更多高级功能,如容器快照和备份,还可以使用其他类型,如 LVM、Btrfs 和 ZFS。

除了容器快照,LXC 还提供了通过 cgroups 控制资源使用的工具,能够在容器启动前、启动时和启动后执行程序,并冻结/挂起正在运行的 LXC 实例的状态。

作为 LXC 工具的替代方案,我们还将查看一组不同的用户空间工具和库,用于创建和管理容器,特别是 libvirt 提供的工具。

本章将涵盖以下主题:

  • 使用 LVM 作为后备存储构建容器

  • LXC 在 Btrfs 上

  • 使用 ZFS 后备存储

  • 自动启动容器

  • 添加容器钩子

  • 从主机访问文件并探索实例的运行文件系统

  • 冻结正在运行的容器

  • 限制容器资源使用

  • 使用 libvirt 库和工具构建容器

使用 LVM 后备存储

逻辑卷管理器LVM)使用 Linux 内核中的设备映射框架,该框架允许将物理块设备映射到更抽象的虚拟块设备上。这种抽象允许将各种块设备聚合到逻辑卷中,从而更好地控制资源。通过 LVM,可以通过向称为 物理卷PVs)的资源池中添加更多块设备来扩展文件系统的大小。PVs 包含块设备。然后可以从 PV 中切出 卷组VGs)。VG 可以在 PV 之间拆分、合并或移动,如果 PV 中有足够的块,则可以在线调整其大小。VG 可以拥有一个或多个 逻辑卷LVs)。LV 可以跨多个磁盘并承载文件系统。如果需要添加更多磁盘空间,可以向 PV 中添加新的块设备,然后扩展 VG 和 LV。

LVM 允许创建快照,这是 LXC 利用的功能,它创建一个 LV 作为原始 LV 的克隆。通过此功能,我们可以快速克隆容器,正如我们接下来所看到的那样。

下图展示了 LVM 布局以及用于管理卷的用户空间工具:

使用 LVM 后备存储

首先让我们从安装 LVM 包开始。在 Ubuntu 上,可以通过以下命令来完成:

root@ubuntu:~# apt-get install lvm2

在 CentOS 上同样运行:

[root@centos ~]# yum install lvm2

接下来,让我们检查一下我们将要使用的块设备,在本例中为 xvdb

root@ubuntu:~# fdisk -l /dev/xvdb 
Disk /dev/xvdb: 80.5 GB, 80530636800 bytes
255 heads, 63 sectors/track, 9790 cylinders, total 157286400 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000
Disk /dev/xvdb doesn't contain a valid partition table
root@ubuntu:~#

要创建 LVM 类型的分区,我们将使用fdisk工具,按照这里的步骤进行:

  1. n键创建一个新分区。

  2. 然后选择p为主分区。

  3. 接下来,选择分区号1

  4. 只需按两次Enter键使用默认值。

  5. 接下来,按p打印出定义的分区。

  6. L列出所有可用的类型。

  7. 输入t来选择分区。

  8. 选择8e作为 Linux LVM 类型,并按Enter键应用。

  9. 再次使用p打印出更改。

  10. 最后,按w保存更改:

     root@ubuntu:~# fdisk /dev/xvdb 
    Device contains neither a valid DOS partition table, nor 
          Sun, SGI or OSF disklabel 
    Building a new DOS disklabel with disk identifier    
          0x115573cb. 
    Changes will remain in memory only, until you decide to 
          write them. 
    After that, of course, the previous content won't be 
          recoverable. 
    Warning: invalid flag 0x0000 of partition table 4 will be  
          corrected by w(rite) 
    Command (m for help): p 
    Disk /dev/xvdb: 80.5 GB, 80530636800 bytes 
    255 heads, 63 sectors/track, 9790 cylinders, total 
          157286400 sectors 
    Units = sectors of 1 * 512 = 512 bytes 
    Sector size (logical/physical): 512 bytes / 512 bytes 
    I/O size (minimum/optimal): 512 bytes / 512 bytes 
    Disk identifier: 0x115573cb 
    Device Boot Start End Blocks Id System 
    Command (m for help): n 
    Partition type: 
    p primary (0 primary, 0 extended, 4 free) 
    e extended 
    Select (default p): p 
    Partition number (1-4, default 1): 1 
    First sector (2048-157286399, default 2048): 
    Using default value 2048 
    Last sector, +sectors or +size{K,M,G} (2048-157286399,    
          default 157286399): 
    Using default value 157286399 
    Command (m for help): t 
    Selected partition 1 
    Hex code (type L to list codes): 8e 
    Changed system type of partition 1 to 8e (Linux LVM) 
    Command (m for help): p 
    Disk /dev/xvdb: 80.5 GB, 80530636800 bytes 
    255 heads, 63 sectors/track, 9790 cylinders, total 
          157286400 sectors 
    Units = sectors of 1 * 512 = 512 bytes 
    Sector size (logical/physical): 512 bytes / 512 bytes 
    I/O size (minimum/optimal): 512 bytes / 512 bytes 
    Disk identifier: 0x115573cb 
    Device Boot Start  End      Blocks    Id  System 
    /dev/xvdb1  2048  157286399 78642176  8e  Linux LVM 
    Command (m for help): w 
    The partition table has been altered! 
    Calling ioctl() to re-read partition table. 
    Syncing disks. 
    root@ubuntu:~#
    
    

定义好 LVM 分区后,我们来创建 PV:

root@ubuntu:~# pvcreate /dev/xvdb1 
Physical volume "/dev/xvdb1" successfully created 
root@ubuntu:~#

这样,xvdb1分区现在成为了 LVM 的一部分。接下来是创建 VG,我们将其命名为lxc,因为这是 LXC 使用的默认 VG:

root@ubuntu:~# vgcreate lxc /dev/xvdb1 
Volume group "lxc" successfully created 
root@ubuntu:~#

使用 LVM 存储后端创建 LXC 容器

要使用 LVM 存储后端创建容器,我们需要在命令行中指定它,并指定所需的根文件系统大小:

root@ubuntu:~# lxc-create --bdev lvm --fssize 10G --name lvm_container --template ubuntu 
Logical volume "lvm_container" created 
Checking cache download in /var/cache/lxc/trusty/rootfs-amd64 ... 
Installing packages in template: apt-transport-https,ssh,vim,language-pack-en 
Downloading ubuntu trusty minimal ... 
... 
[root@centos ~]# lxc-create --bdev lvm --fssize 3G --name lvm_container --template centos 
...

从前面截断的输出中可以看到,lxc-create命令创建了一个名为lvm_container的新 LV,并在其上构建了容器文件系统。

我们通过pvsvgslvs命令列出创建的容器和 LVM 卷:

root@ubuntu:~# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
lvm_container STOPPED 0 - - - 
root@ubuntu:~# pvs 
PV VG Fmt Attr PSize PFree 
/dev/xvdb1 lxc lvm2 a-- 75.00g 65.00g 
root@ubuntu:~# vgs 
VG #PV #LV #SN Attr VSize VFree 
lxc 1 1 0 wz--n- 75.00g 65.00g 
root@ubuntu:~# lvs 
LV VG Attr LSize Pool Origin Data% Move Log Copy% Convert 
lvm_container lxc -wi-a---- 10.00g 
root@ubuntu:~#

如预期所示,我们可以看到之前创建的 PV 和 VG,以及 LXC 添加的 LV。

让我们启动容器,并确保它正在运行:

root@ubuntu:~# lxc-start -n lvm_container 
root@ubuntu:~# lxc-ls -f 
NAME          STATE  AUTOSTART GROUPS IPV4 IPV6 
lvm_container RUNNING 0 - - - 
root@ubuntu:~#

检查容器配置文件时,我们可以看到根文件系统的后端存储被设置为lvm

root@ubuntu:~# cat /var/lib/lxc/lvm_container/config | grep -vi ^# | grep lxc 
lxc.include = /usr/share/lxc/config/ubuntu.common.conf 
lxc.rootfs = /dev/lxc/lvm_container 
lxc.rootfs.backend = lvm 
lxc.utsname = lvm_container 
lxc.arch = amd64 
lxc.network.type = veth 
lxc.network.link = lxcbr0 
lxc.network.flags = up 
lxc.network.hwaddr = 00:16:3e:60:96:a2 
root@ubuntu:~#

查看设备映射器在/dev/lxc/dev/mapper中创建的新块设备,它们是/dev/dm-0的链接:

root@ubuntu:~# ls -la /dev/lxc/ 
total 0 
drwxr-xr-x 2 root root 60 Sep 13 18:14 . 
drwxr-xr-x 15 root root 4020 Sep 13 18:25 .. 
lrwxrwxrwx 1 root root 7 Sep 13 18:14 lvm_container -> ../dm-0 
root@ubuntu:~# ls -la /dev/mapper/ 
total 0 
drwxr-xr-x 2 root root 100 Sep 13 18:35 . 
drwxr-xr-x 15 root root 4040 Sep 13 18:35 .. 
crw------- 1 root root 10, 236 Aug 30 15:17 control 
lrwxrwxrwx 1 root root 7 Sep 13 18:14 lxc-lvm_container -> ../dm-0 
root@ubuntu:~# ls -la /dev/mapper/lxc-lvm_container 
lrwxrwxrwx 1 root root 7 Sep 13 18:14 /dev/mapper/lxc-lvm_container -> ../dm-0 
root@ubuntu:~#

让我们创建一个第二个较小的容器,并观察设备映射器的效果:

root@ubuntu:~# lxc-create --bdev lvm --fssize 5G --name lvm_container_2 --template debian 
Logical volume "lvm_container_2" created 
debootstrap is /usr/sbin/debootstrap 
Checking cache download in /var/cache/lxc/debian/rootfs-jessie-amd64 ... 
... 
root@ubuntu:~# lvs 
LV VG Attr LSize Pool Origin Data% Move Log Copy% Convert 
lvm_container lxc -wi-ao--- 10.00g 
lvm_container_2 lxc -wi-a---- 5.00g 
root@ubuntu:~# 
root@ubuntu:~# ls -la /dev/lxc/ 
total 0 
drwxr-xr-x 2 root root 80 Sep 13 18:35 . 
drwxr-xr-x 15 root root 4040 Sep 13 18:35 .. 
lrwxrwxrwx 1 root root 7 Sep 13 18:14 lvm_container -> ../dm-0 
lrwxrwxrwx 1 root root 7 Sep 13 18:35 lvm_container_2 -> ../dm-1 
root@ubuntu:~# ls -la /dev/mapper/ 
total 0 
drwxr-xr-x 2 root root 100 Sep 13 18:35 . 
drwxr-xr-x 15 root root 4040 Sep 13 18:35 .. 
crw------- 1 root root 10, 236 Aug 30 15:17 control 
lrwxrwxrwx 1 root root 7 Sep 13 18:14 lxc-lvm_container -> ../dm-0 
lrwxrwxrwx 1 root root 7 Sep 13 18:35 lxc-lvm_container_2 -> ../dm-1 
root@ubuntu:~#

注意到这次容器的构建速度更快,因为根文件系统已经被缓存到磁盘中,并且存在两个块设备dm-0dm-1

让我们获取更多关于 LXC 为两个容器创建的 LV 的信息:

root@ubuntu:~# lvdisplay 
--- Logical volume --- 
LV Path             /dev/lxc/lvm_container 
LV Name             lvm_container 
VG Name             lxc 
LV UUID             oBiCEC-mHE7-FHGY-ikUI-tg10-rouD-NBWHVt 
LV Write Access     read/write 
LV Creation host, time ubuntu, 2016-09-13 18:14:42 +0000 
LV Status            available 
# open               1 
LV Size              10.00 GiB 
Current LE           2560 
Segments             1 
Allocation           inherit 
Read ahead sectors   auto 
- currently set to   256 
Block device         252:0 
--- Logical volume --- 
LV Path              /dev/lxc/lvm_container_2 
LV Name              lvm_container_2 
VG Name              lxc 
LV UUID              ED06VK-xzWv-lGPL-Myff-gN3P-AD2l-8zTRko 
LV Write Access      read/write 
LV Creation host, time ubuntu, 2016-09-13 18:35:52 +0000 
LV Status            available 
# open               0 
LV Size              5.00 GiB 
Current LE           1280 
Segments             1 
Allocation           inherit 
Read ahead sectors   auto 
- currently set to   256 
Block device         252:1 
root@ubuntu:~#

注意到有两个 LV 及其各自的属性。

在 LVM 存储后端上创建容器快照

现在我们有支持快照的存储后端容器,接下来我们来试验一下lxc-copy工具。lxc-copy工具可以创建现有容器的副本,这些副本可以是原始容器的完整克隆,意味着整个根文件系统都会被复制到新容器中,或者是快照,采用写时复制COW)。

让我们从创建第二个容器的快照开始,并观察 LV 的变化:

root@ubuntu:~# lxc-copy --snapshot --name lvm_container_2 --newname container_2_copy
Logical volume "container_2_copy" created
root@ubuntu:~# lxc-ls -f
NAME             STATE AUTOSTART GROUPS IPV4 IPV6
container_2_copy STOPPED 0 - - -
lvm_container    RUNNING 0 - 10.0.3.129 -
lvm_container_2  STOPPED 0 - - -
root@ubuntu:~# lvs
LV               VG Attr LSize Pool Origin Data% Move Log Copy% Convert
container_2_copy lxc swi-a-s-- 5.00g lvm_container_2 0.00
lvm_container    lxc -wi-ao--- 10.00g
lvm_container_2  lxc owi-a-s-- 5.00g
root@ubuntu:~#

从前面的输出中可以注意到s属性,表示该容器是一个快照,而Origin列显示了lvm_container_2,这是我们克隆的容器。COW(写时复制)是一种快速创建容器快照的好方法,能够节省磁盘空间,仅记录快照后发生的新变化。

相反,如果我们不为 lxc-copy 指定 --snapshot 属性,我们可以创建原始容器文件系统的完整副本:

root@ubuntu:~# lxc-copy --name lvm_container_2 --newname container_2_hard 
Logical volume "container_2_hard" created 
root@ubuntu:~# lvs 
LV               VG  Attr LSize Pool Origin Data% Move Log Copy% Convert 
container_2_copy lxc swi-a-s-- 5.00g lvm_container_2 0.11 
container_2_hard lxc -wi-a---- 5.00g 
lvm_container    lxc -wi-ao--- 10.00g 
lvm_container_2  lxc owi-a-s-- 5.00g 
root@ubuntu:~#

观察新克隆容器没有 s 属性,并且 Origin 列现在为空,这表示它是完整的副本,而不是快照。

使用 truncate、dd 和 losetup 创建块设备

对于本章中的示例,最好使用诸如 Rackspace 或 Amazon 之类的云服务提供商,因为它们提供免费套餐,并且能够按需添加或阻塞设备。然而,如果无法使用块设备进行测试,您可以借助一些工具创建逻辑块设备并使用它。无需多言,这仅适用于测试,并且由于此类抽象所带来的额外开销,不应在生产环境中实施。

让我们展示如何创建一个块设备,并在其上创建一个 PV 供 LVM 使用:

  1. 首先,让我们创建一个文件,作为新软件块设备的基础,可以使用 truncatedd 命令,并指定大小为 5 GB:

     root@ubuntu:~# truncate --size 5G xvdz.img 
     root@ubuntu:~#
    
    

    上述命令在磁盘上创建了一个常规的 5 GB 文件:

     root@ubuntu:~# ls -la xvdz.img 
     -rw-r--r-- 1 root root 5368709120 Sep 13 19:01 xvdz.img 
     root@ubuntu:~#
    
    
  2. 接下来,我们将使用 loop 内核模块和 losetup 工具,通过将环回设备与之前创建的常规文件关联,来创建一个新的块设备。

    首先,我们加载内核模块:

     root@ubuntu:~# modprobe loop
    
    
  3. 然后,找到第一个未使用的可用环回设备:

     root@ubuntu:~# losetup --find 
     /dev/loop0 
     root@ubuntu:~#
    
    
  4. 将环回设备与映像文件关联:

     root@ubuntu:~# losetup /dev/loop0 xvdz.img 
     root@ubuntu:~# losetup --all 
     /dev/loop0: [ca01]:30352 (/root/xvdz.img) 
     root@ubuntu:~#
    
    
  5. 现在,我们可以将 /dev/loop0 作为常规块设备使用。让我们在其上创建 LVM PV:

     root@ubuntu:~# pvcreate /dev/loop0 
     Physical volume "/dev/loop0" successfully created 
     root@ubuntu:~#
    
    
  6. 或者,我们可以使用 dd 命令:

     root@ubuntu:~# dd if=/dev/zero of=/block_device bs=1k 
          count=500000 
     500000+0 records in 
     500000+0 records out 
     512000000 bytes (512 MB) copied, 2.33792 s, 219 MB/s 
     root@ubuntu:~# losetup --find 
     /dev/loop1 
     root@ubuntu:~#
    
    

    请注意,loop1 现在是下一个可用的环回设备:

     root@ubuntu:~# losetup /dev/loop1 /block_device 
     root@ubuntu:~# pvcreate /dev/loop1 
     Physical volume "/dev/loop1" successfully created 
     root@ubuntu:~#
    
    
  7. 现在列出环回设备,显示其中两个与我们通过 truncatedd 创建的常规文件关联:

     root@ubuntu:~# losetup --all 
     /dev/loop0: [ca01]:30352 (/root/xvdz.img) 
     /dev/loop1: [ca01]:38291 (/block_device) 
     root@ubuntu:~#
    
    
  8. 最后,若要移除环回设备,请运行以下命令:

     root@ubuntu:~# losetup -d /dev/loop1
     root@ubuntu:~# losetup -d /dev/loop0
     root@ubuntu:~# losetup --all
     root@ubuntu:~#
    
    

使用 Btrfs 后备存储

B 树文件系统Btrfs)是一个 COW 文件系统,提供现代功能,如动态 inode 分配、压缩和在线文件系统碎片整理——最重要的是,适用于本书目的,可写和只读的快照。

不深入讨论 Btrfs 的设计,下面的图示展示了文件系统的主要组件:

使用 Btrfs 后备存储

每个 Btrfs 文件系统包含一个 Btrfs 根树,它记录 Extent TreeSubvolume Tree 的根块。根块指针会随着每次事务的进行而更新,指向由事务创建的新根。前面图示中的 Extent Tree 管理磁盘空间,并包含设备上块的信息。Subvolume Tree 记录快照,这些快照是子卷。

请注意,子卷与 LVM 中的 LV 是不同的,因为 Btrfs 子卷并不是一个实际的块设备。

让我们看几个例子,了解如何在 LXC 中使用 Btrfs 后备存储:

  1. 首先,让我们安装 Btrfs 支持工具。

    在 Ubuntu 上,运行以下命令:

     root@ubuntu:~# apt-get -y install btrfs-tools
    
    

    在 CentOS 上,通过以下命令进行安装:

     [root@centos ~]# yum install -y btrfs-progs
    
    
  2. 如果尚未加载,请加载内核模块:

     root@ubuntu:~# modprobe btrfs 
     root@ubuntu:~# lsmod | grep btrfs 
     btrfs       840205  0 
     raid6_pq    97812   1     btrfs 
     xor         21411   1     btrfs 
     libcrc32c   12644   2     xfs,btrfs 
     root@ubuntu:~#
    
    
  3. 使用 Btrfs 时,我们不需要在块设备上创建分区,因此可以直接创建文件系统:

     root@ubuntu:~# mkfs -t btrfs /dev/xvdd 
     fs created label (null) on /dev/xvdd 
     nodesize 16384 leafsize 16384 sectorsize 4096 size 75.00GiB 
     Btrfs v3.12 
     root@ubuntu:~#
    
    

    注意 xvdd 块设备上的文件系统类型:

     root@ubuntu:~# file -s /dev/xvdd 
     /dev/xvdd: BTRFS Filesystem sectorsize 4096, nodesize 16384, 
          leafsize 16384,   
          UUID=58ef810a-c009-4302-9579-a2a9ed7f7ced, 114688/80530636800 
          bytes used, 
          1 devices 
     root@ubuntu:~#
    
    
  4. 要获取有关文件系统的更多信息,我们可以使用 btrfs 工具:

     root@ubuntu:~# btrfs filesystem show 
     Label: none uuid: 9c84a092-4791-4031-ad16-2cb8488c5633 
     Total devices 1 FS bytes used 331.25MiB 
     devid 1 size 75.00GiB used 3.04GiB path /dev/xvdb 
     Btrfs v3.12 
     root@ubuntu:~#
    
    
  5. 让我们挂载块设备,以便实际使用它:

     root@ubuntu:~# mkdir btrfs_c1 
     root@ubuntu:~# mount /dev/xvdd btrfs_c1 
     root@ubuntu:~# cat /proc/mounts | grep btrfs 
     /dev/xvdd /root/btrfs_c1 btrfs rw,relatime,ssd,space_cache 0 0 
     root@ubuntu:~#
    
    
  6. 挂载设备后,让我们展示子卷和磁盘空间使用情况:

     root@ubuntu:~# btrfs subvolume show /root/btrfs_c1/ 
     /root/btrfs_c1 is btrfs root 
     root@ubuntu:~# 
     root@ubuntu:~# btrfs filesystem df /root/btrfs_c1 
     Data, single: total=1.01GiB, used=309.14MiB 
     System, DUP: total=8.00MiB, used=16.00KiB 
     System, single: total=4.00MiB, used=0.00 
     Metadata, DUP: total=1.00GiB, used=22.09MiB 
     Metadata, single: total=8.00MiB, used=0.00 
     root@ubuntu:~#
    
    

使用 Btrfs 后端存储创建 LXC 容器

现在我们已经准备好后端存储,让我们通过指定 Btrfs 后端存储和根文件系统位置来创建一个新的 LXC 容器:

root@ubuntu:~# lxc-create --bdev btrfs --lxcpath=btrfs_c1 --name btrfs_container --template ubuntu 
Checking cache download in /var/cache/lxc/trusty/rootfs-amd64 ... 
Copy /var/cache/lxc/trusty/rootfs-amd64 to btrfs_c1/btrfs_container/rootfs ... 
Copying rootfs to btrfs_c1/btrfs_container/rootfs ... 
... 
root@ubuntu:~# lxc-ls --lxcpath=/root/btrfs_c1 -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
btrfs_container STOPPED 0 - - - 
root@ubuntu:~#

请注意,我们为容器的根文件系统使用了与 /var/lib/lxc 默认路径不同的路径。我们通过 --lxcpath 参数指定它指向 Btrfs 卷。每次运行 LXC 命令时,我们需要传递相同的路径,或者可以通过 lxc-config 命令更新容器的默认路径:

root@ubuntu:~# lxc-config lxc.lxcpath 
/var/lib/lxc 
root@ubuntu:~# echo "lxc.lxcpath = /root/btrfs_c1" >> /etc/lxc/lxc.conf 
root@ubuntu:~# lxc-config lxc.lxcpath 
/root/btrfs_c1 
root@ubuntu:~#

现在我们可以列出所有正在运行的 Btrfs 容器,而无需显式指定路径:

root@ubuntu:~# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
btrfs_container STOPPED 0 - - - 
root@ubuntu:~#

容器的根文件系统和配置文件位于 Btrfs 卷上:

root@ubuntu:~# ls -la btrfs_c1/btrfs_container/ 
total 20 
drwxrwx--- 1 root root 24 Sep 13 20:08 . 
drwxr-xr-x 1 root root 30 Sep 13 20:07 .. 
-rw-r--r-- 1 root root 714 Sep 13 20:08 config 
drwxr-xr-x 1 root root 132 Sep 13 18:18 rootfs 
root@ubuntu:~#

为了确保容器的根目录位于 Btrfs 文件系统上,让我们列出所有子卷:

root@ubuntu:~# btrfs subvolume list /root/btrfs_c1/ 
ID 257 gen 9 top level 5 path btrfs_container/rootfs 
root@ubuntu:~#

在 Btrfs 后端存储上创建容器快照

使用 Btrfs 创建 COW 快照与 LVM 类似:我们需要指定后端存储、容器根文件系统的位置以及新容器的名称:

root@ubuntu:~# lxc-copy --lxcpath=/root/btrfs_c1 -s -n btrfs_container -N btrfs_cow_clone 
root@ubuntu:~# lxc-ls --lxcpath=/root/btrfs_c1 -f 
NAME            STATE AUTOSTART GROUPS IPV4 IPV6 
btrfs_container STOPPED 0 - - - 
btrfs_cow_clone STOPPED 0 - - - 
root@ubuntu:~#

让我们看看克隆操作后对 Btrfs 文件系统的影响:

root@ubuntu:~# btrfs subvolume list /root/btrfs_c1/ 
ID 257 gen 12 top level 5 path btrfs_container/rootfs 
ID 259 gen 12 top level 5 path btrfs_cow_clone/rootfs 
root@ubuntu:~# ls -la btrfs_c1/ 
total 20 
drwxr-xr-x 1 root root 60 Sep 13 20:23 . 
drwx------ 6 root root 4096 Sep 13 19:58 .. 
drwxrwx--- 1 root root 24 Sep 13 20:08 btrfs_container 
drwxrwx--- 1 root root 24 Sep 13 20:23 btrfs_cow_clone 
root@ubuntu:~#

从前面的输出中可以看到,我们现在在 Btrfs 文件系统上有两个子卷和目录。

在启动容器之前,确保 lxc.rootfs 配置选项指向正确的根文件系统:

root@ubuntu:~# cat btrfs_c1/btrfs_container/config | grep -vi ^# | grep lxc 
lxc.include = /usr/share/lxc/config/ubuntu.common.conf 
lxc.rootfs = /root/btrfs_c1/btrfs_container/rootfs 
lxc.rootfs.backend = btrfs 
lxc.utsname = btrfs_container 
lxc.arch = amd64 
lxc.network.type = veth 
lxc.network.link = lxcbr0 
lxc.network.flags = up 
lxc.network.hwaddr = 00:16:3e:f2:a9:04 
root@ubuntu:~#

提示

在某些 Linux 发行版和 LXC 版本中,lxc.rootfs 可能指向错误的容器文件系统位置,从而导致启动失败。如果是这种情况,请更改路径并重新启动容器。

如果一切正常,我们启动容器并确保它们正在运行:

root@ubuntu:~# lxc-start --lxcpath=/root/btrfs_c1 -n btrfs_cow_clone 
root@ubuntu:~# lxc-start --lxcpath=/root/btrfs_c1 -n btrfs_container 
root@ubuntu:~# lxc-ls -f 
NAME            STATE AUTOSTART GROUPS IPV4 IPV6 
btrfs_container RUNNING 0 - 10.0.3.137 - 
btrfs_cow_clone RUNNING 0 - 10.0.3.136 - 
root@ubuntu:~#

为了进行清理,首先停止容器,卸载 Btrfs 块设备,并恢复 LXC 默认路径:

root@ubuntu:~# lxc-stop --lxcpath=/root/btrfs_c1 -n btrfs_cow_clone 
root@ubuntu:~# lxc-stop --lxcpath=/root/btrfs_c1 -n btrfs_container 
root@ubuntu:~# umount /root/btrfs_c1 
root@ubuntu:~# echo "lxc.lxcpath = /var/lib/lxc" > /etc/lxc/lxc.conf 
root@ubuntu:~# lxc-config lxc.lxcpath 
/var/lib/lxc 
root@ubuntu:~#

使用 ZFS 后端存储

ZFS 既是一个文件系统,又是一个 LVM。它由一个存储池组成,管理多个块设备,并为文件系统提供虚拟存储接口,之后可以轻松扩展。以下图示展示了 ZFS 的总体结构及其组件:

使用 ZFS 后端存储

与 LVM 类似,多个块设备可以聚合成一个 存储池,从中可以划分出不同的目录。

ZFS 提供的主要功能包括由于透明校验和的实现而带来的数据可靠性、自动压缩和去重、并行常数时间目录操作,以及在 LXC 环境下最重要的功能:COW 快照和克隆。

让我们在 Ubuntu 上安装用户空间工具:

root@ubuntu:~# apt-add-repository ppa:zfs-native/daily 
root@ubuntu:~# apt-get update 
root@ubuntu:~# apt-get install ubuntu-zfs

在 CentOS 上,软件包名称不同:

[root@centos ~]# yum update 
[root@centos ~]# reboot 
[root@centos ~]# yum localinstall --nogpgcheck http://archive.zfsonlinux.org/epel/zfs-release.el7.noarch.rpm 
[root@centos ~]# yum install kernel-devel 
[root@centos ~]# yum install zfs

接下来,加载内核模块并确保它正在使用:

root@ubuntu:~# modprobe zfs 
root@ubuntu:~# lsmod | grep zfs 
zfs      3089200 0 
zunicode 331170  1 zfs 
zcommon  66797   1 zfs 
znvpair  89131   2 zfs,zcommon 
spl      106271  3 zfs,zcommon,znvpair 
zavl     15236   1 zfs 
root@ubuntu:~#

让我们创建一个名为lxc的池,因为这是 LXC 在创建容器根文件系统时使用的 ZFS 后端的默认名称:

root@ubuntu:~# zpool create -f lxc xvdb

xvdb替换为你想要使用的块设备名称。

让我们列出新创建的池并检查其状态:

root@ubuntu:~# zpool list 
NAME SIZE ALLOC FREE EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT 
lxc 74.5G 51.5K 74.5G - 0% 0% 1.00x ONLINE - 
root@ubuntu:~# zpool status 
pool: lxc 
state: ONLINE 
scan: none requested 
config: 
NAME STATE READ WRITE CKSUM 
lxc ONLINE 0 0 0 
xvdb ONLINE 0 0 0 
errors: No known data errors 
root@ubuntu:~#

一切看起来不错,让我们查看挂载点:

root@ubuntu:~# zfs list 
NAME USED AVAIL REFER MOUNTPOINT 
lxc 56.5K 72.2G 19K /lxc 
root@ubuntu:~# 
root@ubuntu:~# df -h | grep -w lxc 
lxc 73G 0 73G 0% /lxc 
root@ubuntu:~#

到此为止,我们已经准备好将 ZFS 作为 LXC 的后端存储来使用。

使用 ZFS 后端存储创建 LXC 容器

让我们通过指定后端存储和根文件系统路径来创建一个新的 LXC 容器,就像我们在 LVM 和 Btrfs 中做的那样:

root@ubuntu:~# lxc-create --bdev zfs --lxcpath=/lxc --name zfs_container --template ubuntu
Checking cache download in /var/cache/lxc/trusty/rootfs-amd64 ...
Installing packages in template: apt-transport-https,ssh,vim,language-pack-en
Downloading ubuntu trusty minimal ...
... 
root@ubuntu:~# lxc-ls --lxcpath=/lxc -f
NAME          STATE AUTOSTART GROUPS IPV4 IPV6
zfs_container STOPPED 0 - - -
root@ubuntu:~#

注意

请注意,由于我们使用的是自定义路径作为容器的根文件系统,每个 LXC 命令都需要传递--lxcpath参数。通过在系统范围的 LXC 配置文件中指定新的路径,可以避免这种情况,就像我们在本章的 Btrfs 部分中看到的那样。

请注意,根文件系统位于 ZFS 卷上:

root@ubuntu:~# ls -la /lxc/
total 5
drwxr-xr-x 3 root root 3 Sep 14 13:57 
drwxr-xr-x 23 root root 4096 Sep 14 13:54 ..
drwxrwx--- 3 root root 4 Sep 14 14:02 zfs_container
root@ubuntu:~#

在 ZFS 后端存储上创建容器快照

让我们基于刚刚创建的容器做一个快照。确保原始容器已停止,并指定根文件系统的位置:

root@ubuntu:~# lxc-copy --lxcpath=/lxc -s -n zfs_container -N zfs_cow_clone 
root@ubuntu:~# lxc-ls --lxcpath=/lxc -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
zfs_container STOPPED 0 - - - 
zfs_cow_clone STOPPED 0 - - - 
root@ubuntu:~#

  1. 克隆目录位于 ZFS 文件系统上:

     root@ubuntu:~# ls -la /lxc 
     total 6 
     drwxr-xr-x 4 root root 4 Sep 14 14:03 . 
     drwxr-xr-x 23 root root 4096 Sep 14 13:54 .. 
     drwxrwx--- 3 root root 4 Sep 14 14:02 zfs_container 
     drwxrwx--- 3 root root 4 Sep 14 14:03 zfs_cow_clone 
     root@ubuntu:~#
    
    
  2. 接下来,启动两个容器并确保它们处于运行状态:

     root@ubuntu:~# lxc-start --lxcpath=/lxc --name zfs_container 
     root@ubuntu:~# lxc-start --lxcpath=/lxc --name zfs_cow_clone 
     root@ubuntu:~# lxc-ls --lxcpath=/lxc -f 
     NAME STATE AUTOSTART GROUPS IPV4 IPV6 
     zfs_container RUNNING 0 - 10.0.3.238 - 
     zfs_cow_clone RUNNING 0 - 10.0.3.152 - 
     root@ubuntu:~#
    
    
  3. 最后,让我们清理一下。首先,停止容器:

     root@ubuntu:~# lxc-stop --lxcpath=/lxc --name zfs_cow_clone 
     root@ubuntu:~# lxc-stop --lxcpath=/lxc --name zfs_container 
     root@ubuntu:~#
    
    
  4. 接下来,尝试销毁两个容器:

     root@ubuntu:~# lxc-destroy --lxcpath=/lxc --name zfs_cow_clone 
     Destroyed container zfs_cow_clone 
     root@ubuntu:~# lxc-destroy --lxcpath=/lxc --name zfs_container 
     cannot destroy 'lxc/zfs_container': filesystem has children 
     use '-r' to destroy the following datasets: 
     lxc/zfs_container@zfs_cow_clone 
     lxc-destroy: lxccontainer.c: container_destroy: 2376 Error destroying 
          rootfs for    
          zfs_container 
     Destroying zfs_container failed 
     root@ubuntu:~#
    
    
  5. 克隆被销毁了,但原始容器失败了。这是因为我们首先需要清理在快照原始容器时创建的子 ZFS 数据集:

     root@ubuntu:~# zfs destroy lxc/zfs_container@zfs_cow_clone 
     root@ubuntu:~# lxc-destroy --lxcpath=/lxc --name zfs_container 
     Destroyed container zfs_container 
     root@ubuntu:~# lxc-ls --lxcpath=/lxc -f 
     root@ubuntu:~#
    
    
  6. 最后,删除 ZFS 池:

     root@ubuntu:~# zpool destroy lxc 
     root@ubuntu:~# zpool status 
     no pools available 
     root@ubuntu:~# zfs list 
     no datasets available 
     root@ubuntu:~#
    
    

到此为止,ZFS 中没有任何数据集或池。

自动启动 LXC 容器

默认情况下,LXC 容器在服务器重启后不会启动。要更改这一点,我们可以使用lxc-autostart工具和容器配置文件:

  1. 为了演示这一点,首先让我们创建一个新的容器:

     root@ubuntu:~# lxc-create --name autostart_container --template 
          ubuntu 
     root@ubuntu:~# lxc-ls -f 
     NAME STATE AUTOSTART GROUPS IPV4 IPV6 
     autostart_container STOPPED 0 - - - 
     root@ubuntu:~#
    
    
  2. 接下来,将lxc.start.auto段添加到其配置文件中:

     root@ubuntu:~# echo "lxc.start.auto = 1" >> 
          /var/lib/lxc/autostart_container/config 
     root@ubuntu:~#
    
    
  3. 列出所有配置为自动启动的容器:

     root@ubuntu:~# lxc-autostart --list 
     autostart_container 
     root@ubuntu:~#
    
    
  4. 现在,我们可以再次使用lxc-autostart命令来启动所有配置为自动启动的容器,在这种情况下只有一个容器:

     root@ubuntu:~# lxc-autostart --all
     root@ubuntu:~# lxc-ls -f
     NAME                STATE AUTOSTART GROUPS IPV4 IPV6
     autostart_container RUNNING 1 - 10.0.3.98 -
     root@ubuntu:~#
    
    
  5. 另两个有用的自动启动配置参数是给启动添加延迟,以及定义一个组,使多个容器可以作为一个单元启动。停止容器并将以下内容添加到配置选项中:

     root@ubuntu:~# lxc-stop --name autostart_container 
     root@ubuntu:~# echo "lxc.start.delay = 5" >> 
          /var/lib/lxc/autostart_container/config 
     root@ubuntu:~# echo "lxc.group = high_priority" >> 
          /var/lib/lxc/autostart_container/config 
     root@ubuntu:~#
    
    
  6. 接下来,让我们再次列出配置为自动启动的容器:

     root@ubuntu:~# lxc-autostart --list 
     root@ubuntu:~#
    
    

    注意,在前面的输出中没有显示任何容器。这是因为我们的容器现在属于一个自动启动组。让我们指定它:

     root@ubuntu:~# lxc-autostart --list --group high_priority
     autostart_container 5
     root@ubuntu:~#
    
    
  7. 同样,启动属于给定自动启动组的所有容器:

     root@ubuntu:~# lxc-autostart --group high_priority 
     root@ubuntu:~# lxc-ls -f 
     NAME                STATE AUTOSTART GROUPS IPV4 IPV6 
     autostart_container RUNNING 1 high_priority 10.0.3.98 - 
     root@ubuntu:~#
    
    

    要使lxc-autostart在服务器重启后自动启动容器,首先需要启动它。可以通过将前述命令添加到crontab中,或者创建一个初始化脚本来实现。

  8. 最后,为了清理,运行以下命令:

     root@ubuntu:~# lxc-destroy --name autostart_container 
     Destroyed container autostart_container 
     root@ubuntu:~# lxc-ls -f 
     root@ubuntu:~#
    
    

LXC 容器钩子

LXC 提供了一种方便的方式,在容器生命周期内执行程序。以下表格总结了可用于启用此功能的各种配置选项:

选项 描述
lxc.hook.pre-start 在容器的终端、控制台或挂载点加载之前,在主机命名空间中运行的钩子
lxc.hook.pre-mount 在容器的文件系统命名空间中运行的钩子,但在设置 rootfs 之前
lxc.hook.mount 在挂载完成后,但在 pivot_root 之前,容器中运行的钩子
lxc.hook.autodev 在挂载完成且所有挂载钩子运行之后,但在 pivot_root 之前,容器中运行的钩子
lxc.hook.start 在容器初始化之前,容器内运行的钩子
lxc.hook.stop 在容器关闭后,在主机的命名空间中运行的钩子
lxc.hook.post-stop 在容器关闭后,在主机命名空间中运行的钩子
lxc.hook.clone 在容器被克隆时运行的钩子
lxc.hook.destroy 在容器被销毁时运行的钩子

为了演示这一点,让我们创建一个新的容器,并编写一个简单的脚本,当容器启动时,将四个 LXC 变量的值输出到文件中:

  1. 首先,创建容器并将 lxc.hook.pre-start 选项添加到其配置文件中:

     root@ubuntu:~# lxc-create --name hooks_container --template 
          ubuntu 
     root@ubuntu:~# echo "lxc.hook.pre-start =   
          /var/lib/lxc/hooks_container/pre_start.sh" >> /var/lib
          /lxc/hooks_container/config 
     root@ubuntu:~#
    
    
  2. 接下来,创建一个简单的 bash 脚本并使其可执行:

     root@ubuntu:~# vi /var/lib/lxc/hooks_container/pre_start.sh 
     #!/bin/bash 
     LOG_FILE=/tmp/container.log 
     echo "Container name: $LXC_NAME" | tee -a $LOG_FILE 
     echo "Container mounted rootfs: $LXC_ROOTFS_MOUNT" | tee -a 
          $LOG_FILE 
     echo "Container config file $LXC_CONFIG_FILE" | tee -a $LOG_FILE 
     echo "Container rootfs: $LXC_ROOTFS_PATH" | tee -a $LOG_FILE 
     root@ubuntu:~# 
     root@ubuntu:~# chmod u+x /var/lib/lxc/hooks_container
          /pre_start.sh 
     root@ubuntu:~#
    
    
  3. 启动容器并检查 bash 脚本应写入的文件的内容,确保脚本已被触发:

     root@ubuntu:~# lxc-start --name hooks_container 
     root@ubuntu:~# lxc-ls -f 
     NAME            STATE AUTOSTART GROUPS IPV4 IPV6 
     hooks_container RUNNING 0 - 10.0.3.237 - 
     root@ubuntu:~# cat /tmp/container.log 
     Container name: hooks_container 
     Container mounted rootfs: /usr/lib/x86_64-linux-gnu/lxc 
     Container config file /var/lib/lxc/hooks_container/config 
     Container rootfs: /var/lib/lxc/hooks_container/rootfs 
     root@ubuntu:~#
    
    

从前面的输出中,我们可以看到,当我们启动容器时,脚本被触发了,并且 LXC 变量的值被写入了临时文件。

从主机操作系统附加目录并探索容器的运行文件系统

LXC 容器的根文件系统在主机操作系统中作为常规目录树可见。我们可以直接通过在该目录中进行更改来操作运行中的容器中的文件。LXC 还允许通过绑定挂载将主机操作系统中的目录挂载到容器内部。绑定挂载是对目录树的另一种视图,它通过在不同的挂载点下复制现有的目录树来实现。

  1. 为了演示这一点,让我们创建一个新的容器、目录,并在主机上创建一个文件:

     root@ubuntu:~# mkdir /tmp/export_to_container 
     root@ubuntu:~# hostname -f > /tmp/export_to_container/file 
     root@ubuntu:~# lxc-create --name mount_container --template 
          ubuntu 
     root@ubuntu:~#
    
    
  2. 接下来,我们将使用容器配置文件中的 lxc.mount.entry 选项,告诉 LXC 从主机挂载哪个目录,并指定容器内的挂载点:

     root@ubuntu:~# echo "lxc.mount.entry = /tmp/export_to_container/   
          /var/lib/lxc/mount_container/rootfs/mnt none ro,bind 0 0" >>    
          /var/lib/lxc/mount_container/config 
     root@ubuntu:~#
    
    
  3. 一旦容器启动,我们可以看到容器内的 /mnt 现在包含了我们之前在主机操作系统 /tmp/export_to_container 目录中创建的文件:

     root@ubuntu:~# lxc-start --name mount_container
     root@ubuntu:~# lxc-attach --name mount_container
     root@mount_container:~# cat /mnt/file
     ubuntu
     root@mount_containerr:~# exit
     exit
     root@ubuntu:~#
    
    
  4. 当 LXC 容器处于运行状态时,一些文件仅能在宿主操作系统的/proc中看到。要查看容器的运行目录,首先获取其 PID:

     root@ubuntu:~# lxc-info --name mount_container 
     Name:        mount_container 
     State:       RUNNING 
     PID:         8594 
     IP:          10.0.3.237 
     CPU use:     1.96 seconds 
     BlkIO use:   212.00 KiB 
     Memory use:  8.50 MiB 
     KMem use:    0 bytes 
     Link:        vethBXR2HO 
     TX bytes:    4.74 KiB 
     RX bytes:    4.73 KiB 
     Total bytes: 9.46 KiB 
     root@ubuntu:~#
    
    

    拿到 PID 后,我们可以检查容器的运行目录:

     root@ubuntu:~# ls -la /proc/8594/root/run/ 
     total 44 
     drwxr-xr-x 10 root root 420 Sep 14 23:28 . 
     drwxr-xr-x 21 root root 4096 Sep 14 23:28 .. 
     -rw-r--r-- 1 root root 4 Sep 14 23:28 container_type 
     -rw-r--r-- 1 root root 5 Sep 14 23:28 crond.pid 
     ---------- 1 root root 0 Sep 14 23:28 crond.reboot 
     -rw-r--r-- 1 root root 5 Sep 14 23:28 dhclient.eth0.pid 
     drwxrwxrwt 2 root root 40 Sep 14 23:28 lock 
     -rw-r--r-- 1 root root 112 Sep 14 23:28 motd.dynamic 
     drwxr-xr-x 3 root root 180 Sep 14 23:28 network 
     drwxr-xr-x 3 root root 100 Sep 14 23:28 resolvconf 
     -rw-r--r-- 1 root root 5 Sep 14 23:28 rsyslogd.pid 
     drwxr-xr-x 2 root root 40 Sep 14 23:28 sendsigs.omit.d 
     drwxrwxrwt 2 root root 40 Sep 14 23:28 shm 
     drwxr-xr-x 2 root root 40 Sep 14 23:28 sshd 
     -rw-r--r-- 1 root root 5 Sep 14 23:28 sshd.pid 
     drwxr-xr-x 2 root root 80 Sep 14 23:28 udev 
     -rw-r--r-- 1 root root 5 Sep 14 23:28 upstart-file-bridge.pid 
     -rw-r--r-- 1 root root 4 Sep 14 23:28 upstart-socket-bridge.pid 
     -rw-r--r-- 1 root root 5 Sep 14 23:28 upstart-udev-bridge.pid 
     drwxr-xr-x 2 root root 40 Sep 14 23:28 user 
     -rw-rw-r-- 1 root utmp 2688 Sep 14 23:28 utmp 
     root@ubuntu:~#
    
    

    提示

    确保将 PID 替换为宿主机上 lxc-info 输出的 PID,因为它与前面的示例不同。

为了在容器的根文件系统中进行持久化更改,可以修改 /var/lib/lxc/mount_container/rootfs/ 中的文件。

冻结一个运行中的容器

LXC 利用 freezer cgroup 来冻结容器内运行的所有进程。这些进程将处于阻塞状态,直到被解冻。冻结容器在系统负载高时很有用,可以释放一些资源,而不需要实际停止容器并保持其运行状态。

确保你有一个正在运行的容器,并通过freezer cgroup 检查其状态:

root@ubuntu:~# lxc-ls -f 
NAME            STATE AUTOSTART GROUPS IPV4 IPV6 
hooks_container RUNNING 0 - 10.0.3 
root@ubuntu:~# cat /sys/fs/cgroup/freezer/lxc/hooks_container/freezer.state 
THAWED 
root@ubuntu:~#

注意当前正在运行的容器显示为已解冻。让我们冻结它:

root@ubuntu:~# lxc-freeze -n hooks_container 
root@ubuntu:~# lxc-ls -f 
NAME            STATE AUTOSTART GROUPS IPV4 IPV6 
hooks_container FROZEN 0 - 10.0.3.237 - 
root@ubuntu:~#

容器状态显示为冻结状态,让我们检查 cgroup 文件:

root@ubuntu:~# cat /sys/fs/cgroup/freezer/lxc/hooks_container/freezer.state 
FROZEN 
root@ubuntu:~#

要解冻容器,运行以下命令:

root@ubuntu:~# lxc-unfreeze --name hooks_container 
root@ubuntu:~# lxc-ls -f 
NAME            STATE AUTOSTART GROUPS IPV4 IPV6 
hooks_container RUNNING 0 - 10.0.3.237 - 
root@ubuntu:~# cat /sys/fs/cgroup/freezer/lxc/hooks_container/freezer.state 
THAWED 
root@ubuntu:~#

我们可以通过在另一个控制台上运行 lxc-monitor 命令,同时冻结和解冻容器来监控状态变化。容器状态的变化将如下所示:

root@ubuntu:~# lxc-monitor --name hooks_container 
'hooks_container' changed state to [FREEZING] 
'hooks_container' changed state to [FROZEN] 
'hooks_container' changed state to [THAWED]

限制容器资源使用

在第一章,Linux 容器介绍中,我们了解了如何通过直接操作 cgroup 层次结构中的文件或使用用户空间工具轻松地限制进程资源。

类似地,LXC 也提供了同样简便易用的工具。

让我们从设置容器的可用内存为 512 MB 开始:

root@ubuntu:~# lxc-cgroup -n hooks_container memory.limit_in_bytes 536870912 
root@ubuntu:~#

我们可以通过直接检查容器的 memory cgroup 来验证新设置是否已应用:

root@ubuntu:~# cat /sys/fs/cgroup/memory/lxc/hooks_container/memory.limit_in_bytes
536870912
root@ubuntu:~#

更改该值只需要再次运行相同的命令。让我们将可用内存更改为 256 MB,并通过附加到容器并运行 free 工具来检查容器:

root@ubuntu:~# lxc-cgroup -n hooks_container memory.limit_in_bytes 268435456 
root@ubuntu:~# cat /sys/fs/cgroup/memory/lxc/hooks_container/memory.limit_in_bytes 
268435456 
root@ubuntu:~# lxc-attach --name hooks_container 
root@hooks_container:~# free -m 
 total used free shared buffers cached 
Mem: 256   63   192  0      0       54 
-/+ buffers/cache: 9 246 
Swap: 0 0 0 
root@hooks_container:~# exit 
root@ubuntu:~#

如前所示,容器的可用内存只有 256 MB。

我们还可以将一个 CPU 核心绑定到容器。下一个示例中,我们的测试服务器有两个核心。让我们允许容器只在核心 0 上运行:

root@ubuntu:~# cat /proc/cpuinfo | grep processor 
processor : 0 
processor : 1 
root@ubuntu:~# 
root@ubuntu:~# lxc-cgroup -n hooks_container cpuset.cpus 0 
root@ubuntu:~# cat /sys/fs/cgroup/cpuset/lxc/hooks_container/cpuset.cpus 
0 
root@ubuntu:~# lxc-attach --name hooks_container 
root@hooks_container:~# cat /proc/cpuinfo | grep processor 
processor : 0 
root@hooks_container:~# exit 
exit 
root@ubuntu:~#

通过附加到容器并检查可用的 CPU,我们看到只有一个呈现出来,正如预期的那样。

为了使更改在服务器重启后生效,我们需要将它们添加到容器的配置文件中:

root@ubuntu:~# echo "lxc.cgroup.memory.limit_in_bytes = 536870912" >> /var/lib/lxc/hooks_container/config 
root@ubuntu:~#

设置其他各种 cgroup 参数的方法类似。例如,我们来查看容器的 CPU 份额和块 IO:

root@ubuntu:~# lxc-cgroup -n hooks_container cpu.shares 512 
root@ubuntu:~# lxc-cgroup -n hooks_container blkio.weight 500 
root@ubuntu:~# lxc-cgroup -n hooks_container blkio.weight 
500 
root@ubuntu:~#

有关所有可用 cgroup 选项的完整列表,请参阅第一章,Linux 容器介绍,或者探索挂载的 cgroup 层次结构。

使用 libvirt 构建和运行 LXC 容器

Libvirt 是一组库和语言绑定,用于以标准和统一的方式与各种虚拟化技术进行交互。这些技术包括 KVM、XEN、QEMU、OpenVZ,当然还有 LXC。Libvirt 使用 XML 文件来定义虚拟化实体(如 LXC 容器),并描述其属性,如可用内存、块设备、网络、初始化系统以及其他元数据。它支持多种存储驱动程序,如 LVM、本地和网络文件系统、iSCSI 等。

Libvirt 提供了一种与主流 LXC 项目以及我们目前看到并使用的工具集完全独立的方式来处理 Linux 容器。它实现了构成 LXC 的内核特性,并暴露了自己的工具和库,用于与容器进行交互,而无需安装其他包。在第四章,LXC 代码与 Python 的集成 中,我们将看到如何使用 libvirt API 编写 Python 程序,但现在我们先来探索它提供的工具集。

在 Debian 和 CentOS 上从包安装 libvirt

Ubuntu 和 CentOS 都提供 libvirt 包,尽管版本相比上游版本较旧。为了获取最新版本,我们将在下一节从源代码编译它。

提示

为了充分利用本章中的示例并避免冲突和错误,我建议您使用一个全新的虚拟机或新的云实例。

要在 Ubuntu 上安装包,请运行以下命令:

root@ubuntu:~# apt-get update && apt-get -y install libvirt-bin python-libvirt virtinst cgmanager cgroup-lite 
root@ubuntu:~#

安装完成后,确保 libvirt 守护进程正在运行:

root@ubuntu:~# /etc/init.d/libvirt-bin status 
libvirt-bin start/running, process 15987 
root@ubuntu:~#

在 CentOS 上,包的名称不同:

[root@centos ~]# yum install libvirt 
[root@centos ~]# 

安装包完成后,启动 libvirt 服务并确保其正在运行:

[root@centos ~]# service libvirtd start
Redirecting to /bin/systemctl start libvirtd.service
[root@centos ~]# systemctl status libvirtd
  libvirtd.service - Virtualization daemon
Loaded: loaded (/usr/lib/systemd/system/libvirtd.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2016-09-15 19:42:50 UTC; 59s ago
Docs: man:libvirtd(8)
http://libvirt.org
Main PID: 10578 (libvirtd)
CGroup: /system.slice/libvirtd.service
|-10578 /usr/sbin/libvirtd
|-10641 /sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/libexec/libvi...
|-10642 /sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/libexec/libvi...
Sep 15 19:42:50 centos systemd[1]: Started Virtualization daemon.
Sep 15 19:42:51 centos dnsmasq[10641]: started, version 2.66 cachesize 150
Sep 15 19:42:51 centos dnsmasq[10641]: compile time options: IPv6 GNU-getopt DBus no-i18n IDN DHCP DHCPv6 no-Lua TFTP no-co...et auth
Sep 15 19:42:51 centos dnsmasq-dhcp[10641]: DHCP, IP range 192.168.122.2 -- 192.168.122.254, lease time 1h
Sep 15 19:42:51 centos dnsmasq[10641]: reading /etc/resolv.conf
Sep 15 19:42:51 centos dnsmasq[10641]: using nameserver 173.203.4.8#53
Sep 15 19:42:51 centos dnsmasq[10641]: using nameserver 173.203.4.9#53
Sep 15 19:42:51 centos dnsmasq[10641]: read /etc/hosts - 5 addresses
Sep 15 19:42:51 centos dnsmasq[10641]: read /var/lib/libvirt/dnsmasq/default.addnhosts - 0 addresses
Sep 15 19:42:51 centos dnsmasq-dhcp[10641]: read /var/lib/libvirt/dnsmasq/default.hostsfile
Hint: Some lines were ellipsized, use -l to show in full.
[root@centos ~]#

在本章的其余示例中,我们将使用从源代码编译的最新版本的 libvirt 在 Ubuntu 上进行操作。

从源代码安装 libvirt

在 Ubuntu 上,确保您的系统是最新的:

root@ubuntu:~# apt-get update && apt-get upgrade && reboot

接下来,安装必要的前提包,以便我们能够从 git 获取源代码并进行构建:

root@ubuntu:~# apt-get install build-essential automake pkg-config git bridge-utils libcap-dev libcgmanager-dev cgmanager git cgmanager cgroup-lite
root@ubuntu:~# apt-get install libtool libxml2 libxml2-dev libxml2-utils autopoint xsltproc libyajl-dev libpciaccess-dev libdevmapper-dev libnl-dev gettext libgettextpo-dev ebtables dnsmasq dnsmasq-utils

安装所有包之后,克隆主 git 分支中的源代码:

root@ubuntu:~# cd /usr/src/ 
root@ubuntu:/usr/src# git clone git://libvirt.org/libvirt.git 
root@ubuntu:/usr/src# cd libvirt/ 
root@ubuntu:/usr/src/libvirt#

生成配置文件,编译并安装二进制文件:

root@ubuntu:/usr/src/libvirt# ./autogen.sh 
root@ubuntu:/usr/src/libvirt# make 
root@ubuntu:/usr/src/libvirt# make install 
root@ubuntu:/usr/src/libvirt# 

更新必要的链接和缓存,以获取最新的共享库:

root@ubuntu:/usr/src/libvirt# ldconfig 
root@ubuntu:/usr/src/libvirt# 

启动 libvirt 守护进程并检查其版本:

root@ubuntu:~# libvirtd -d 
root@ubuntu:~# libvirtd --version 
libvirtd (libvirt) 2.3.0 
root@ubuntu:~#

使用 libvirt 定义 LXC 容器

要构建一个容器,我们需要在 libvirt 将使用的 XML 文件中定义其属性。一旦定义,libvirt 会启动一个名为 libvirt_lxc 的辅助进程,负责创建实际的容器,启动第一个进程并处理 I/O。根据安装类型、版本和发行版的不同,其位置可能有所不同,我们来查找它。

在 Ubuntu 上运行:

root@ubuntu:~# find / -name libvirt_lxc 
/usr/lib/libvirt/libvirt_lxc 
root@ubuntu:~#

在 CentOS 上,位置会有所不同:

[root@centos ~]# find / -name libvirt_lxc
/usr/libexec/libvirt_lxc
[root@centos ~]#

如前所述,libvirt 支持许多不同的虚拟化管理程序,并为适配所有虚拟化管理程序使用了虚拟化管理程序的标准 URI。URI 指向 libvirt 将与之通信的虚拟化管理程序。要列出当前配置的 URI,请运行以下命令:

root@ubuntu:~# virsh uri 
qemu:///system 
root@ubuntu:~#

从前面的输出可以看到,默认的 URI 是 QEMU。要与 LXC 配合使用,我们需要将其更改。为此,请运行以下命令:

root@ubuntu:~# export LIBVIRT_DEFAULT_URI=lxc:/// 
root@ubuntu:~# virsh uri 
lxc:/// 
root@ubuntu:~#

在会话期间,默认的虚拟化程序将是 LXC。你可以通过命令行选项显式告诉 libvirt 使用什么。例如,要列出所有 LXC 容器,可以运行以下命令:

root@ubuntu:~# virsh --connect lxc:/// list -all 
Id Name State 
---------------------------------------------------- 
root@ubuntu:~#

在我们定义并构建容器之前,我们需要一个根文件系统。让我们使用 debootstrap 来构建一个:

root@ubuntu:~# apt-get install debootstrap 
root@ubuntu:~# debootstrap --arch=amd64 --include="openssh-server vim" stable /root/container http://httpredir.debian.org/debian/ 
I: Retrieving Release 
I: Retrieving Packages 
I: Validating Packages 
... 
I: Configuring systemd... 
I: Base system installed successfully. 
root@ubuntu:~#

根文件系统准备好后,让我们定义容器属性:

root@ubuntu:~# vim libvirt_container1.xml 
<domain type='lxc'> 
 <name>libvirt_container1</name> 
 <uuid>902b56ed-969c-458e-8b55-58daf5ae97b3</uuid> 
 <memory unit='KiB'>524288</memory> 
 <currentMemory unit='KiB'>524288</currentMemory> 
 <vcpu placement='static'>1</vcpu> 
 <os> 
 <type arch='x86_64'>exe</type> 
 <init>/sbin/init</init> 
 </os> 
 <features> 
 <capabilities policy='allow'> 
 </capabilities> 
 </features> 
 <clock offset='utc'/> 
 <on_poweroff>destroy</on_poweroff> 
 <on_reboot>restart</on_reboot> 
 <on_crash>destroy</on_crash> 
 <devices> 
 <emulator>/usr/local/libexec/libvirt_lxc</emulator> 
 <filesystem type='mount' accessmode='passthrough'> 
 <source dir='/root/container'/> 
 <target dir='/'/> 
 </filesystem> 
 <interface type='bridge'> 
 <mac address='00:17:4e:9f:36:f8'/> 
 <source bridge='lxcbr0'/> 
 <link state='up'/> 
 </interface> 
 <console type='pty' /> 
 </devices> 
</domain>

该文件大部分内容不言自明且有很好的文档说明。我们将域类型定义为 lxc,内存和 CPU 属性,根文件系统的位置,最重要的是,libvirt_lxc 辅助进程的位置。在网络部分,我们定义了一个 MAC 地址和主机桥接的名称。并非所有选项都需要指定,如果省略,libvirt 会创建合理的默认值。在本章后面,我将演示如何从使用 LXC 工具构建的 LXC 容器生成此文件。

文件准备好后,让我们定义容器:

root@ubuntu:~# virsh define libvirt_container1.xml 
Domain libvirt_container1 defined from libvirt_container1.xml 
root@ubuntu:~# virsh list --all 
Id Name State 
---------------------------------------------------- 
- libvirt_container1 shut off 
root@ubuntu:~#

我们看到容器已经定义,但处于关机状态:

使用 libvirt 启动和连接 LXC 容器

我们之前定义的容器指定了 lxcbr0 桥接。我们需要先创建它,才能启动容器:

root@ubuntu:~# brctl addbr lxcbr0 && ifconfig lxcbr0 up

现在我们有了定义好的容器和桥接,来连接它,让我们启动它:

root@ubuntu:~# virsh start libvirt_container1 
Domain libvirt_container1 started 
root@ubuntu:~# virsh list --all 
Id Name State 
---------------------------------------------------- 
2618 libvirt_container1 running 
root@ubuntu:~#

在我们可以连接到容器的控制台之前,我们需要将其添加到允许的终端中。为此,我们可以 chroot 进入容器的文件系统并编辑 securetty 文件:

root@ubuntu:~# chroot container
root@ubuntu:/# echo "pts/0" >> /etc/securetty
root@ubuntu:/# exit
exit
root@ubuntu:~#

让我们连接到控制台:

root@ubuntu:~# virsh console libvirt_container1 
Connected to domain libvirt_container1 
Escape character is ^] 
systemd 215 running in system mode. (+PAM +AUDIT +SELINUX +IMA +SYSVINIT +LIBCRYPTSETUP +GCRYPT +ACL +XZ -SECCOMP -APPARMOR) 
Detected virtualization 'lxc-libvirt'. 
Detected architecture 'x86-64'. 
Welcome to Debian GNU/Linux 8 (jessie)! 
... 
[ OK ] Reached target Graphical Interface. 
Starting Update UTMP about System Runlevel Changes... 
[ OK ] Started Update UTMP about System Runlevel Changes. 
Debian GNU/Linux 8 server-33 console 
ubuntu login:

使用 libvirt 将块设备附加到正在运行的容器

Libvirt 提供了一种便捷的方式将块设备附加到已经运行的容器上。为了演示这一点,让我们从一个常规文件创建一个块设备,就像本章前面所演示的,使用 truncatelosetup 命令:

root@ubuntu:~# truncate --size 5G xvdz.img 
root@ubuntu:~# modprobe loop 
root@ubuntu:~# losetup --find 
/dev/loop0 
root@ubuntu:~# losetup /dev/loop0 xvdz.img 
root@ubuntu:~# losetup --all 
/dev/loop0: [ca01]:1457 (/root/xvdz.img) 
root@ubuntu:~#

我们的新块设备现在是 /dev/loop0。让我们在上面创建文件系统,挂载它,并创建一个测试文件:

root@ubuntu:~# mkfs.ext4 /dev/loop0 
root@ubuntu:~# mount /dev/loop0 /mnt/ 
root@ubuntu:~# echo test > /mnt/file 
root@ubuntu:~# umount /mnt 

现在是定义块设备的时候了:

root@ubuntu:~# vim new_disk.xml 
<disk type='block' device='disk'> 
 <driver name='lxc' cache='none'/> 
 <source dev='/dev/loop0'/> 
 <target dev='vdb' bus='virtio'/> 
</disk>

在前面的配置文件中,我们定义了驱动程序为 lxc,我们刚刚创建的块设备的路径,以及将在容器内呈现的设备名称,这里是 vdb。现在是时候附加设备了:

root@ubuntu:~# virsh attach-device libvirt_container1 new_disk.xml 
Device attached successfully 
root@ubuntu:~#

让我们连接到容器并确保块设备存在:

root@ubuntu:~# virsh console libvirt_container1 
Connected to domain libvirt_container1 
Escape character is ^] 
root@ubuntu:~# ls -la /dev/vdb 
brwx------ 1 root root 7, 0 Sep 22 15:13 /dev/vdb 
root@ubuntu:~# mount /dev/vdb /mnt 
root@ubuntu:~# cat /mnt/file 
test 
root@ubuntu:~# exit 
exit 
root@ubuntu:~#

要分离块设备,我们可以同样运行以下命令:

root@ubuntu:~# virsh detach-device libvirt_container1 new_disk.xml --live 
Device detached successfully 
root@ubuntu:~#

使用 libvirt LXC 进行网络配置

Libvirt 提供了一个默认网络,使用 dnsmasq 并配置为自动启动。要列出所有网络,请运行以下命令:

root@ubuntu:~# virsh net-list --all 
Name State Autostart Persistent 
---------------------------------------------------------- 
default active yes yes 
root@ubuntu:~#

为了检查默认网络的配置,让我们将其导出为 XML 格式:

root@ubuntu:~# virsh net-dumpxml default 
<network> 
 <name>default</name> 
 <uuid>9995bf85-a6d8-4eb9-887d-656b7d44de6d</uuid> 
 <forward mode='nat'> 
 <nat> 
 <port start='1024' end='65535'/> 
 </nat> 
 </forward> 
 <bridge name='virbr0' stp='on' delay='0'/> 
 <mac address='52:54:00:9b:72:83'/> 
 <ip address='192.168.122.1' netmask='255.255.255.0'> 
 <dhcp> 
 <range start='192.168.122.2' end='192.168.122.254'/> 
 </dhcp> 
 </ip> 
</network> 
root@ubuntu:~#

从输出结果来看,很明显默认网络会创建一个名为 virbr0 的桥接,并显示 dnsmasq 中配置的网络范围。如果我们更愿意使用自己的网络,可以在一个新的 XML 文件中描述其属性:

root@ubuntu:~# vim lxc_net.xml 
<network> 
 <name>lxc</name> 
 <bridge name="br0"/> 
 <forward/> 
 <ip address="192.168.0.1" netmask="255.255.255.0"> 
 <dhcp> 
 <range start="192.168.0.2" end="192.168.0.254"/> 
 </dhcp> 
 </ip> 
</network> 
root@ubuntu:~#

在这个示例中,桥接的名称将是 br0,并将使用不同的子网。让我们定义它并列出网络:

root@ubuntu:~# virsh net-define lxc_net.xml 
Network lxc defined from lxc_net.xml: 
root@ubuntu:~# virsh net-list --all 
Name State Autostart Persistent 
---------------------------------------------------------- 
default active yes yes 
lxc active no no 
root@ubuntu:~#

我们可以通过运行以下命令来确认两个网络已创建桥接:

root@ubuntu:~# brctl show 
bridge name bridge id STP enabled interfaces 
br0 8000.525400628c5e yes br0-nic 
lxcbr0 8000.76ccc8474468 no vnet0 
virbr0 8000.5254009b7283 yes virbr0-nic 
root@ubuntu:~#

请注意,lxcbr0 是我们之前手动创建的那个。每个网络将启动一个 dnsmasq 进程:

root@ubuntu:~# pgrep -lfa dnsmasq 
22521 /sbin/dnsmasq --conf-file=/usr/local/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/local/libexec/libvirt_leaseshelper 
22522 /sbin/dnsmasq --conf-file=/usr/local/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/local/libexec/libvirt_leaseshelper 
22654 /sbin/dnsmasq --conf-file=/usr/local/var/lib/libvirt/dnsmasq/lxc.conf --leasefile-ro --dhcp-script=/usr/local/libexec/libvirt_leaseshelper 
22655 /sbin/dnsmasq --conf-file=/usr/local/var/lib/libvirt/dnsmasq/lxc.conf --leasefile-ro --dhcp-script=/usr/local/libexec/libvirt_leaseshelper 
root@ubuntu:~#

为了构建一个将成为某个已定义网络一部分的容器,我们需要在 XML 文件中更改桥接的名称,登录容器并配置网络接口。

从现有 LXC 容器生成配置文件并使用 libvirt

Libvirt 提供了一种方法,将我们在本章开始时使用的 LXC 工具构建的现有 LXC 容器的配置文件转换为 libvirt 可以使用的格式。

为了演示这一点,让我们使用 lxc-create 创建一个新的 LXC 容器:

root@ubuntu:~# apt-get install -y lxc=2.0.3-0ubuntu1~ubuntu14.04.1 lxc1=2.0.3-0ubuntu1~ubuntu14.04.1 liblxc1=2.0.3-0ubuntu1~ubuntu14.04.1 python3-lxc=2.0.3-0ubuntu1~ubuntu14.04.1 cgroup-lite=1.11~ubuntu14.04.2 lxc-templates=2.0.3-0ubuntu1~ubuntu14.04.1 bridge-utils debootstrap 
root@ubuntu:~# lxc-create --name lxc-container --template ubuntu 
root@ubuntu:~# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
lxc-container STOPPED 0 - - - 
root@ubuntu:~#

在新的容器准备好后,让我们将其配置文件转换为 libvirt 支持的 XML 规范:

root@ubuntu:~# virsh domxml-from-native lxc-tools /var/lib/lxc/lxc-container/config | tee -a lxc-container.xml 
<domain type='lxc'> 
 <name>lxc-container</name> 
 <uuid>1ec0c47d-0399-4f8c-b2a4-924834e369e7</uuid> 
 <memory unit='KiB'>65536</memory> 
 <currentMemory unit='KiB'>65536</currentMemory> 
 <vcpu placement='static'>1</vcpu> 
 <os> 
 <type arch='x86_64'>exe</type> 
 <init>/sbin/init</init> 
 </os> 
 <features> 
 <capabilities policy='allow'> 
 </capabilities> 
 </features> 
 <clock offset='utc'/> 
 <on_poweroff>destroy</on_poweroff> 
 <on_reboot>restart</on_reboot> 
 <on_crash>destroy</on_crash> 
 <devices> 
 <emulator>/usr/local/libexec/libvirt_lxc</emulator> 
 <filesystem type='mount' accessmode='passthrough'> 
 <source dir='/var/lib/lxc/lxc-container/rootfs'/> 
 <target dir='/'/> 
 </filesystem> 
 <interface type='bridge'> 
 <mac address='00:16:3e:0f:dc:ed'/> 
 <source bridge='lxcbr0'/> 
 <link state='up'/> 
 </interface> 
 </devices> 
</domain> 
root@ubuntu:~#

命令的输出是容器的新配置文件,保存为 XML 格式的 lxc-container.xml 文件。我们可以使用这个文件通过 virsh 启动容器,而不是使用 lxc-start。在此之前,我们需要先指定控制台类型:

root@ubuntu:~# sed -i "/<\/devices>/i <console type='pty' \/>" lxc-container.xml 
root@ubuntu:~#

定义新的容器:

root@ubuntu:~# virsh define lxc-container.xml 
Domain lxc-container defined from lxc-container.xml 
root@ubuntu:~#

并启动它:

root@ubuntu:~# virsh start lxc-container 
Domain lxc-container started 
root@ubuntu:~# virsh list --all 
Id Name State 
---------------------------------------------------- 
18062 lxc-container running 
22958 libvirt_container1 running 
24893 libvirt_container2 running 
root@ubuntu:~#

使用 libvirt 停止和删除 LXC 容器

让我们停止所有正在运行的 libvirt 容器:

root@ubuntu:~# virsh destroy lxc-container 
Domain lxc-container destroyed 
root@ubuntu:~# virsh destroy libvirt_container1 
Domain libvirt_container1 destroyed 
root@ubuntu:~# virsh destroy libvirt_container2 
Domain libvirt_container2 destroyed 
root@ubuntu:~# virsh list --all 
Id Name State 
---------------------------------------------------- 
- libvirt_container1 shut off 
- libvirt_container2 shut off 
- lxc-container shut off 
root@ubuntu:~#

要完全删除容器,我们需要取消定义它们:

root@ubuntu:~# virsh undefine lxc-container 
Domain lxc-container has been undefined 
root@ubuntu:~# virsh undefine libvirt_container1 
Domain libvirt_container1 has been undefined 
root@ubuntu:~# virsh undefine libvirt_container2 
Domain libvirt_container2 has been undefined 
root@ubuntu:~# virsh list --all 
Id Name State 
---------------------------------------------------- 
root@ubuntu:~#

总结

LXC 支持多种文件系统后端存储。在本章中,我们探讨了如何使用 LVM、Btrfs 和 ZFS 后端存储来创建 COW 快照。我们还研究了如何从常规文件中创建块设备以进行测试。

我们展示了如何设置容器自启动、创建生命周期中执行程序的钩子,以及如何将主机操作系统中的目录和文件暴露给 LXC。

LXC 使用 cgroup 机制来控制和分配资源给容器。对这些资源的更改会存储在配置文件中,并可以根据需要进行持久化。我们探索了如何使用提供的工具集来实现这一点。

最后,我们介绍了另一种使用 libvirt 和 virsh 命令来创建和管理 LXC 的方法。

在下一章中,您将学习如何使用 LXC API 和 libvirt 的 Python 绑定来创建和管理容器。

第四章:LXC 与 Python 的代码集成

本章将介绍 LXC 和 libvirt API 提供的 Python 绑定。我们将探索哪些容器功能是可能的,哪些是不可能的,使用 Ubuntu 上的上游 lxc-devpython-libvirt 包以及 CentOS 上的 lxc-devellibvirt-python 包。

为了充分利用本章内容,需要具备一定的 Python 知识。如果你是开发者,本章可能是最重要的一章。

本章将按以下顺序涵盖以下主题:

  • 使用 lxc Python 绑定构建和管理容器

  • 使用 libvirt Python 绑定创建和编排容器

  • 使用 LXC 作为 Vagrant 开发和测试的后端

  • 开发一个简单的前端 RESTful API 来与 LXC 交互,使用 Bottle 微框架和 lxc

LXC Python 绑定

LXC 提供稳定的 C API 和 Python 绑定,支持 Python 2.x 和 3.x 版本。让我们通过编写一个代码来探索一些功能,这些功能覆盖了我们在前几章中看到的用户空间工具所提供的大多数功能,使用 Python 2.7.6 版本。

在 Ubuntu 和 CentOS 上安装 LXC Python 绑定并准备开发环境

让我们从安装所有必需的包开始,这些包将使我们能够编写功能完整的 Python 代码,包括 LXC API 库和带有 ipythonvirtualenv 的 Python 开发环境。

  1. 要准备一个 Ubuntu 主机,请运行以下命令:

    root@ubuntu:~# apt-get update && apt-get upgrade && reboot
    root@ubuntu:~# apt-get install python-pip python-dev ipython
    root@ubuntu:~# apt-get install lxc-dev=2.0.3-0ubuntu1~ubuntu14.04.1 
    liblxc1=2.0.3-0ubuntu1~ubuntu14.04.1 cgroup-lite=1.11~ubuntu14.04.2 
    
    root@ubuntu:~# apt-get install 
    lxc-templates=2.0.3-0ubuntu1~ubuntu14.04.1
    lxc1=2.0.3-0ubuntu1~ubuntu14.04.1 
    python3-lxc=2.0.3-0ubuntu1~ubuntu14.04.1
    
    

    上述命令将确保我们运行的是最新的 Ubuntu 包,并包含诸如 pip 这样的工具,用于安装和管理 Python 包,以及用于 Python 交互式编程的 ipython 工具。在 CentOS 上,安装以下软件包以提供相同的功能:

    [root@centos ~]# yum update && reboot
    [root@centos ~]# yum install python-devel python-pip lxc 
    lxc-devel lxc-templates libcgroup-devel ipython
    
    
  2. 在此阶段,让我们为容器创建一个 Linux 桥接,以便后续连接:

    [root@centos ~]# brctl addbr virbr0
    
    

    对于本章中的示例,我们将使用一个单独的 Python 虚拟环境,以保持项目的依赖项要求独立。我们可以通过使用 virtualenv 包来实现。

  3. 让我们首先通过 pip 安装它:

    root@ubuntu:~# pip install virtualenv
    
    
  4. 接下来,让我们为项目创建一个工作目录并激活虚拟环境:

    root@ubuntu:~# mkdirlxc_python
    root@ubuntu:~# virtualenv lxc_python
    New python executable in /root/lxc_python/bin/python
    Installing setuptools, pip, wheel...done.
    root@ubuntu:~# 
    root@ubuntu:~# source lxc_python/bin/activate
    (lxc_python) root@ubuntu:~# cd lxc_python
    (lxc_python) root@ubuntu:~/lxc_python#
    
    
  5. 激活虚拟环境后,让我们安装 Python LXC API 绑定包,并使用 pip 列出开发环境中的内容:

    (lxc_python) root@ubuntu:~/lxc_python# pip install lxc-python2
    (lxc_python) root@ubuntu:~/lxc_python# pip freeze
    lxc-python2==0.1
    (lxc_python) root@ubuntu:~/lxc_python#
    
    

至此,我们已经具备了所有必需的包、库和工具,可以使用 Python 创建和使用 LXC 容器。让我们写点 Python 代码,享受乐趣吧!

用 Python 构建我们的第一个容器

让我们启动 ipython 工具并导入之前安装的 LXC 库:

(lxc_python) root@ubuntu:~/lxc_python# ipython
In [1]: import lxc

接下来,我们需要使用 Container 类并指定一个名称来创建 container 对象:

In [2]: container = def build():Container("python_container")
In [3]: type(container)
Out[3]: lxc.Container

现在我们已经有了 container 对象,可以使用 create 方法来构建我们的第一个容器。

container.create() 方法的定义以及每个参数的解释如下:

定义container.create(self, template=None, flags=0, args=()),为容器创建一个新的 rootfs。以下是各个参数的说明:

  • template:此参数必须是一个有效的模板名称,才能被传递。

  • flags:这是可选的。它是一个整数,表示要传递的可选创建标志。

  • args:这是可选的。它是一个元组,包含要传递给模板的参数。也可以提供为字典形式。

创建一个 Ubuntu 容器就像运行以下代码一样简单:

In [4]: container.create("ubuntu")

Checking cache download in /var/cache/lxc/trusty/rootfs-amd64 ... 
Copy /var/cache/lxc/trusty/rootfs-amd64 to /var/lib/lxc/python_container/rootfs ...
Copying rootfs to /var/lib/lxc/python_container/rootfs ...
Generating locales...
en_US.UTF-8... up-to-date
Generation complete.
Creating SSH2 RSA key; this may take some time ...
Creating SSH2 DSA key; this may take some time ...
Creating SSH2 ECDSA key; this may take some time ...
Creating SSH2 ED25519 key; this may take some time ...
update-rc.d: warning: default stop runlevel arguments (0 1 6) do not match ssh Default-Stop values (none)
invoke-rc.d: policy-rc.d denied execution of start.
Current default time zone: 'Etc/UTC'
Local time is now: Tue Sep 20 16:30:31 UTC 2016.
Universal Time is now: Tue Sep 20 16:30:31 UTC 2016.
##
# The default user is 'ubuntu' with password 'ubuntu'!
# Use the 'sudo' command to run tasks as root in the container.
##
Out[4]: True

输出 True 表示操作成功地定义了容器。

使用 Python 收集容器信息

现在我们已经构建了第一个 LXC 容器,让我们来检查它的一些属性。

首先,检查一下容器的名称:

In [5]: container.name
Out[5]: u'python_container'

还可以检查其状态:

In [6]: container.state
Out[6]: u'STOPPED'

列出当前主机操作系统上所有的容器:

In [7]: lxc.list_containers()
Out[7]: (u'python_container')

上面的 containers() 方法返回一个包含容器名称的元组。在此例中,就是我们刚刚构建的唯一一个容器。

默认情况下,当我们使用用户空间工具如 lxc-create 构建 LXC 容器时,根文件系统和配置文件位于 /var/lib/lxc/containername/。让我们通过调用 get_config_path()get_config_item() 方法来查看我们构建的容器的根文件系统位置:

In [8]: container.get_config_path()
Out[8]: u'/var/lib/lxc'
In [9]: container.get_config_item('lxc.rootfs')
Out[9]: u'/var/lib/lxc/python_container/rootfs'

get_config_path() 方法的输出中,我们可以观察到默认的 LXC 配置位置与使用 lxc-create 命令构建容器时的配置位置相同。

在前面的代码示例中,我们还将 lxc.rootfs 配置选项传递给 get_config_item() 方法,以获取根文件系统的位置,这也与默认设置一致,假如使用命令行工具的话。

我们可以传递不同的配置参数给 get_config_item() 方法,以获取容器的当前设置。让我们查询一下 memory.limit_in_bytes 选项:

In [10]: 
container.get_config_item('lxc.cgroup.memory.limit_in_bytes')
Out[10]: u''

注意

要列出我们创建的 container 对象上所有可用的方法和变量,在 ipython 中,输入 container 然后按一次 Tab 键。要获取关于方法、函数或变量的更多信息,可以输入其名称后加上问号,例如 container.get_ips?

你可以进一步尝试,打开容器的配置文件,如前面输出所示,并将其作为参数传递给 get_config_item() 方法。

要获取容器的 IP 配置信息,我们可以调用 get_ips() 方法,无需任何参数,如下所示:

In [11]: container.get_ips()
Out[11]: ()

由于容器没有运行,并且没有应用内存限制,因此输出分别是空字符串和空元组。与停止的容器一起工作不是特别有趣;让我们探索一下如何在 Python 中操作一个正在运行的容器。

启动容器、应用更改并使用 Python 列出配置选项

让我们通过打印 container 对象上运行布尔值的值来检查容器是否正在运行:

In [12]: container.running
Out[12]: False

要启动容器,我们可以使用 start() 方法。该方法的文档字符串如下所示:

start(useinit = False, daemonize=True, close_fds=False, cmd = (,)) ->boolean

启动容器并在成功时返回 True。当设置 useinit 时,LXC 会使用 lxc-init 启动容器。容器可以通过 daemonize=False 在前台启动。所有 fds 也可以通过传递 close_fds=True 来关闭。看起来很简单。让我们通过守护进程方式启动容器,并且不使用 lxc-init 管理器,而是使用 Python 解释器:

In [13]: container.start(useinit = False, daemonize = True)
Out[13]: True

和之前一样,True 的输出表示操作成功执行。让我们使用 wait() 方法来等待容器达到 RUNNING 状态,或者在 5 秒内超时:

In [14]: container.wait("RUNNING", 5)
Out[14]: True

输出表明容器现在正在运行。让我们通过打印运行状态和状态变量的值来再次确认:

In [15]: container.running
Out[15]: True
In [16]: container.state
Out[16]: u'RUNNING'

在一个独立的终端中,让我们使用 LXC 用户空间工具来检查我们用 Python 库构建的容器:

root@ubuntu:~# lxc-ls -f
NAME             STATE   AUTOSTART GROUPS IPV4      IPV6
python_container RUNNING 0         -      10.0.3.29 -
root@ubuntu:~#

lxc-ls 命令的输出确认了 container.state 变量的返回值。

让我们在 Python shell 中获取容器的 PID:

In [17]: container.init_pid
Out[17]: 4688L

此时的 PID 是 4688;我们可以通过执行以下命令来确认它是否与当前在主机系统上运行的进程匹配:

root@ubuntu:~# psaxfw
...
4683 ?Ss     0:00 /usr/bin/python /usr/bin/ipython
4688 ?Ss     0:00  \_ /sbin/init
5405 ?        S      0:00      \_ upstart-socket-bridge --daemon
6224 ?        S      0:00      \_ upstart-udev-bridge --daemon
6235 ?Ss     0:00      \_ /lib/systemd/systemd-udevd --daemon
6278 ?        S      0:00      \_ upstart-file-bridge --daemon
6280 ?Ssl    0:00      \_ rsyslogd
6375 ?Ss     0:00      \_ dhclient -1 -v -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases eth0
6447 pts/3    Ss+    0:00      \_ /sbin/getty -8 38400 tty4
6450 pts/1    Ss+    0:00      \_ /sbin/getty -8 38400 tty2
6451 pts/2    Ss+    0:00      \_ /sbin/getty -8 38400 tty3
6462 ?Ss     0:00      \_ /usr/sbin/sshd -D
6468 ?Ss     0:00      \_ cron
6498 pts/4    Ss+    0:00      \_ /sbin/getty -8 38400 console
6501 pts/0    Ss+    0:00      \_ /sbin/getty -8 38400 tty1
root@ubuntu:~#

这里没有什么惊讶的。注意,启动容器 init 系统的主进程是 python 而不是 lxc-init,因为我们在 start() 方法中传递的就是 Python。

现在我们的容器已经启动,我们可以从中获取更多信息。让我们从获取其 IP 地址开始:

In [18]: container.get_ips()
Out[18]: (u'10.0.3.29',)

结果是一个元组,包含容器所有接口的 IP 地址,在本例中只有一个 IP 地址。

我们可以像在第三章,使用本地和 Libvirt 工具的命令行操作中看到的那样,通过调用 attach_wait() 方法,使用 lxc-attach 命令以编程方式附加到容器并运行命令,代码如下:

In [19]: container.attach_wait(lxc.attach_run_command, ["ifconfig", "eth0"])
eth0      Link encap:EthernetHWaddr 00:16:3e:ea:1c:38
inet addr:10.0.3.29  Bcast:10.0.3.255  Mask:255.255.255.0
inet6addr: fe80::216:3eff:feea:1c38/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
 RX packets:53 errors:0 dropped:0 overruns:0 frame:0
 TX packets:52 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
 RX bytes:5072 (5.0 KB)  TX bytes:4892 (4.8 KB)
Out[19]: 0L

attach_wait() 方法将一个函数作为参数,在前面的示例中是内置的 lxc.attach_run_command,但它也可以是你编写的任何其他 Python 函数。我们还指定了一个包含我们要执行的命令及其参数的列表。

我们还可以指定命令应在哪个命名空间上下文中运行。例如,要列出容器挂载命名空间中所有的文件(该命名空间由 CLONE_NEWNS 标志指定),我们可以传递 namespaces 参数:

In [20]: container.attach_wait(lxc.attach_run_command, ["ls", "-la"], namespaces=(lxc.CLONE_NEWNS))
total 68
drwxr-xr-x  21 root root 4096 Sep 20 18:51 .
drwxr-xr-x  21 root root 4096 Sep 20 18:51 ..
drwxr-xr-x   2 root root 4096 Sep 14 15:20 bin
drwxr-xr-x   2 root root 4096 Apr 10  2014 boot
drwxr-xr-x   7 root root 1140 Sep 20 18:51 dev
drwxr-xr-x  65 root root 4096 Sep 20 18:51 etc
drwxr-xr-x   3 root root 4096 Sep 20 16:30 home
drwxr-xr-x  12 root root 4096 Sep 14 15:19 lib
drwxr-xr-x   2 root root 4096 Sep 14 15:19 lib64
drwxr-xr-x   2 root root 4096 Sep 14 15:18 media
drwxr-xr-x   2 root root 4096 Apr 10  2014 mnt
drwxr-xr-x   2 root root 4096 Sep 14 15:18 opt
dr-xr-xr-x 143 root root    0 Sep 20 18:51 proc
drwx------   2 root root 4096 Sep 14 15:18 root
drwxr-xr-x  10 root root  420 Sep 20 18:51 run
drwxr-xr-x   2 root root 4096 Sep 14 15:20 sbin
drwxr-xr-x   2 root root 4096 Sep 14 15:18 srv
dr-xr-xr-x  13 root root    0 Sep 20 18:51 sys
drwxrwxrwt   2 root root 4096 Sep 20 19:17 tmp
drwxr-xr-x  10 root root 4096 Sep 14 15:18 usr
drwxr-xr-x  11 root root 4096 Sep 14 15:18 var
Out[20]: 0L

我们可以通过指定多个 namespaces 标志来运行命令。在下一个示例中,我们通过明确指定挂载和进程命名空间(分别使用 CLONE_NEWNSCLONE_NEWPID 标志),列出容器中的所有进程:

In [21]: container.attach_wait(lxc.attach_run_command, ["ps", "axfw"], namespaces=(lxc.CLONE_NEWNS + lxc.CLONE_NEWPID))
PID TTY      STAT   TIME COMMAND
1751 pts/0    R+     0:00psaxfw
1 ?Ss     0:00 /sbin/init
670 ?        S      0:00 upstart-socket-bridge --daemon
1487 ?        S      0:00 upstart-udev-bridge --daemon
1498 ?Ss     0:00 /lib/systemd/systemd-udevd --daemon
1541 ?        S      0:00 upstart-file-bridge --daemon
1543 ?Ssl    0:00 rsyslogd
1582 ?Ss     0:00 dhclient -1 -v -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases eth0
 1654 lxc/tty4 Ss+    0:00 /sbin/getty -8 38400 tty4
 1657 lxc/tty2 Ss+    0:00 /sbin/getty -8 38400 tty2
 1658 lxc/tty3 Ss+    0:00 /sbin/getty -8 38400 tty3
1669 ?Ss     0:00 /usr/sbin/sshd -D
1675 ?Ss     0:00 cron
 1705 lxc/console Ss+   0:00 /sbin/getty -8 38400 console
 1708 lxc/tty1 Ss+    0:00 /sbin/getty -8 38400 tty1
Out[21]: 0L

使用 set_config_item()get_config_item() 方法,我们可以对运行中的容器应用配置更改并查询它们。为了演示这一点,让我们为容器指定一个内存限制,然后获取新设置的值:

In [22]: container.set_config_item("lxc.cgroup.memory.limit_in_bytes", "536870912")
Out[22]: True
In [23]: container.get_config_item('lxc.cgroup.memory.limit_in_bytes')
Out[23]: [u'536870912']

上述更改在容器重启时不会持久化;为了使更改永久生效,我们可以使用 append_config_item()save_config() 方法将它们写入配置文件:

In [24]: container.append_config_item("lxc.cgroup.memory.limit_in_bytes", "536870912")
Out[24]: True
In [25]: container.save_config()
Out[25]: True

为了验证这一点,lxc.cgroup.memory.limit_in_bytes 参数已经保存在配置文件中;让我们检查它:

root@ubuntu:~# cat /var/lib/lxc/python_container/config
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
# Container specific configuration
lxc.rootfs = /var/lib/lxc/python_container/rootfs
lxc.rootfs.backend = dir
lxc.utsname = python_container
lxc.arch = amd64
# Network configuration
lxc.network.type = veth
lxc.network.link = lxcbr0
lxc.network.flags = up
lxc.network.hwaddr = 00:16:3e:ea:1c:38
lxc.cgroup.memory.limit_in_bytes = 536870912
root@ubuntu:~#

注意

配置文件中的最后一行是我们用两个 Python 调用追加的那一行。

除了 set_config_item() 方法外,Python 绑定还提供了 set_cgroup_item()get_cgroup_item() 方法,用于专门操作 cgroup 参数。让我们使用这两个方法设置并获取相同的 memory.limit_in_bytes 选项:

In [26]: container.get_cgroup_item("memory.limit_in_bytes")
Out[26]: u'536870912'
In [27]: container.set_cgroup_item("memory.limit_in_bytes", "268435456")
Out[27]: True
In [28]: container.get_cgroup_item("memory.limit_in_bytes")
Out[28]: u'268435456'

使用 Python 更改容器状态

在第三章,使用原生和 Libvirt 工具进行命令行操作中,我们看到了如何使用 lxc-freezelxc-unfreeze 命令冻结和解冻 LXC 容器,以保存其状态。我们可以使用 freeze()unfreeze() 方法做到这一点。要冻结容器,执行以下命令:

In [29]: container.freeze()
Out[29]: u'FROZEN'

按如下方式检查状态:

In [30]: container.state
Out[30]: u'FROZEN'

我们也可以检查 cgroup 文件,以确认更改已发生:

root@ubuntu:~# cat /sys/fs/cgroup/freezer/lxc/python_container/freezer.state
FROZEN
root@ubuntu:~#

要解冻容器并检查新状态,请调用 unfreeze() 方法:

In [31]: container.unfreeze()
Out[31]: True
In [32]: container.state
Out[32]: u'RUNNING'
root@ubuntu:~# cat /sys/fs/cgroup/freezer/lxc/python_container/freezer.state
THAWED
root@ubuntu:~#

使用 Python 停止容器

Python 绑定提供了一个方便的方式来停止容器,使用 stop() 方法。让我们停止容器并检查其状态:

In [33]: container.stop()
Out[33]: True
In [34]: container.state
Out[34]: u'STOPPED'

最后,列出主机上的所有容器:

root@ubuntu:~# lxc-ls -f
NAME             STATE   AUTOSTART GROUPS IPV4 IPV6
python_container STOPPED 0         -      -    -
root@ubuntu:~#

使用 Python 克隆容器

容器处于 STOPPED 状态时,让我们运行 clone() 方法并创建一个副本:

In [35]: cloned_container = container.clone("cloned_container")
In [35]: True

使用 list_containers() 方法列出 lxc 对象上的可用容器,我们得到一个元组:

In [36]: lxc.list_containers()
Out[36]: (u'cloned_container', u'python_container')

要在主机操作系统上确认,请执行以下操作:

root@ubuntu:~# lxc-ls -f
NAME             STATE   AUTOSTART GROUPS IPV4 IPV6
cloned_container STOPPED 0         -      -    -
python_container STOPPED 0         -      -    -
root@ubuntu:~#

要找到克隆容器的根文件系统位置,我们可以在新的 container 对象上调用 get_config_item() 方法:

In [37]: cloned_container.get_config_item('lxc.rootfs')
Out[37]: u'/var/lib/lxc/cloned_container/rootfs'

现在在默认的容器路径下存在两个目录:

root@ubuntu:~# ls -la /var/lib/lxc
total 20
drwx------  5 root root 4096 Sep 20 19:51 .
drwxr-xr-x 47 root root 4096 Sep 16 13:40 ..
drwxrwx---  3 root root 4096 Sep 20 19:51 cloned_container
drwxrwx---  3 root root 4096 Sep 20 16:30 python_container
root@ubuntu:~#

最后,让我们启动克隆的容器并确保它正在运行:

In [38]: cloned_container.start()
Out[38]: True
In [39]: cloned_container.state
Out[39]: u'RUNNING'

使用 Python 销毁容器并清理虚拟环境

在我们能够在 Python 中移除或销毁容器之前,像命令行工具一样,我们需要先停止它们:

In [40]: cloned_container.stop()
Out[40]: True
In [41]: container.stop()
Out[41]: True

container 对象上调用 destroy() 方法,以删除根文件系统并释放它所使用的所有资源:

In [42]: cloned_container.destroy()
Out[42]: True
In [43]: container.destroy()
Out[43]: True

通过 list_containers() 方法列出容器,此时返回一个空元组:

In [44]: lxc.list_containers()
Out[44]: ()

最后,让我们停用之前创建的 Python 虚拟环境——注意,文件仍然会保留在磁盘上:

(lxc_python) root@ubuntu:~/lxc_python# deactivate
root@ubuntu:~/lxc_python# cd ..
root@ubuntu:~#

Libvirt Python 绑定

在 第三章,使用原生和 Libvirt 工具的命令行操作,我们探讨了通过使用 libvirt 用户空间工具处理 LXC 的替代方法。Libvirt 提供了 Python 绑定,我们可以用它来编写应用程序,主要的好处是与其他虚拟化技术的一致性。使用一个通用的库来编写 KVM、XEN 和 LXC 的 Python 应用程序非常方便。

在本节中,我们将探讨 libvirt 库提供的一些 Python 方法,用于创建和控制 LXC 容器。

安装 libvirt Python 开发包

让我们首先安装所需的包并启动服务。

在 Ubuntu 上,运行以下命令:

root@ubuntu:~# apt-get install python-libvirt debootstrap
root@ubuntu:~# service libvirt-bin start

在 CentOS 上,库和服务的名称不同:

[root@centos ~]# yum install libvirt libvirt-python debootstrap
[root@centos ~]# service libvirtd start

由于 libvirt 不提供模板来使用,我们需要创建自己的根文件系统:

root@ubuntu:~# debootstrap --arch=amd64 --include="openssh-server vim" stable ~/container http://httpredir.debian.org/debian/ 
...
root@ubuntu:~#

激活 Python 虚拟环境并启动解释器:

root@ubuntu:~# source lxc_python/bin/activate
(lxc_python) root@ubuntu:~# ipython
In [1]:

使用 libvirt Python 构建 LXC 容器

现在是导入库并调用 open() 方法以创建与 LXC 驱动程序的连接的时候了。我们传递给 open() 方法的参数应该是熟悉的——我们在 第三章,使用原生和 Libvirt 工具的命令行操作,中使用过它,在导出 LIBVIRT_DEFAULT_URI 环境变量时,告诉 libvirt LXC 将是默认的虚拟化驱动程序:

In [1]: import libvirt
In [2]: lxc_conn = libvirt.open('lxc:///')

在指定默认虚拟化驱动程序和 URI 后,我们可以使用接下来的两个方法来返回我们设置的驱动程序的名称和路径:

In [3]: lxc_conn.getType()
Out[3]: 'LXC'
In [4]: lxc_conn.getURI()
Out[4]: 'lxc:///'

要获取我们之前创建的 lxc_conn 对象上的可用方法和变量列表,请输入 lxc_conn,然后按 Tab 键。要获取有关方法、函数或变量的更多信息,请键入其名称后加上问号,例如,lxc_conn.getURI?

我们可以使用 getInfo() 方法提取主机节点的硬件信息:

In [5]: lxc_conn.getInfo()
Out[5]: ['x86_64', 1996L, 2, 3000, 1, 2, 1, 1]

结果是一个包含以下值的列表:

成员 描述
list[0] 指示 CPU 型号的字符串
list[1] 以兆字节为单位的内存大小
list[2] 活跃 CPU 的数量
list[3] 预期的 CPU 频率(单位:MHz)
list[4] NUMA 节点的数量,1 表示统一内存访问
list[5] 每个节点的 CPU 插槽数
list[6] 每个插槽的核心数
list[7] 每个核心的线程数

要构建容器,我们需要先在 XML 文件中定义它。让我们使用以下示例并将其分配给 domain_xml 字符串变量:

In [6]: domain_xml = '''
<domain type='lxc'>
 <name>libvirt_python</name>
 <memory unit='KiB'>524288</memory>
 <currentMemory unit='KiB'>524288</currentMemory>
 <vcpu placement='static'>1</vcpu>
 <os>
 <type arch='x86_64'>exe</type>
 <init>/sbin/init</init>
 </os>
 <clock offset='utc'/>
 <on_poweroff>destroy</on_poweroff>
 <on_reboot>restart</on_reboot>
 <on_crash>destroy</on_crash>
 <devices>
 <emulator>/usr/lib/libvirt/libvirt_lxc</emulator>
 <filesystem type='mount' accessmode='passthrough'>
 <sourcedir='/root/container/'/>
 <targetdir='/'/>
 </filesystem>
 <interface type='bridge'>
 <mac address='00:17:3e:9f:33:f7'/>
 <source bridge='lxcbr0'/>
 <link state='up'/>
 </interface>
 <console type='pty' />
 </devices>
</domain>
'''

使用之前分配给变量的 XML 配置,我们可以使用 defineXML() 方法来定义容器。此方法将 XML 定义作为参数并定义容器,但不会启动容器:

In [7]: container = lxc_conn.defineXML(domain_xml)

让我们验证容器是否已成功在主机上定义:

root@ubuntu:~# virsh --connect lxc:/// list --all
Id    Name                           State
----------------------------------------------------
-     libvirt_python                 shut off
root@ubuntu:~#

我们可以使用listDefinedDomains()方法列出所有已定义但未运行的域,它返回一个列表:

In [8]: lxc_conn.listDefinedDomains()
Out[8]: ['libvirt_python']

使用 libvirt Python 启动容器并运行基本操作

要启动之前定义的容器,我们需要调用create()方法:

In [9]: container.create()
Out[9]: 07

为了验证容器是否在主机上运行,在调用create()方法后,我们将执行以下操作:

root@ubuntu:~# virsh --connect lxc:/// list --all
Id    Name                           State
----------------------------------------------------
23749 libvirt_python                 running
root@ubuntu:~#

有许多方法可以获取容器的信息。我们可以通过在container对象上调用XMLDesc()方法来获取 XML 定义:

In [10]: container.XMLDesc()
Out[10]: "<domain type='lxc' id='25535'>\n  <name>libvirt_python</name>\n  <uuid>6a46bd23-f0df-461b-85e7-19fd36be90df</uuid>\n  <memory unit='KiB'>524288</memory>\n  <currentMemory unit='KiB'>524288</currentMemory>\n  <vcpu placement='static'>1</vcpu>\n  <resource>\n    <partition>/machine</partition>\n  </resource>\n  <os>\n    <type arch='x86_64'>exe</type>\n    <init>/sbin/init</init>\n  </os>\n  <clock offset='utc'/>\n  <on_poweroff>destroy</on_poweroff>\n  <on_reboot>restart</on_reboot>\n  <on_crash>destroy</on_crash>\n  <devices>\n    <emulator>/usr/lib/libvirt/libvirt_lxc</emulator>\n    <filesystem type='mount' accessmode='passthrough'>\n      <source dir='/root/container/'/>\n      <target dir='/'/>\n    </filesystem>\n    <interface type='bridge'>\n      <mac address='00:17:3e:9f:33:f7'/>\n      <source bridge='lxcbr0'/>\n      <target dev='vnet0'/>\n      <link state='up'/>\n    </interface>\n    <console type='pty' tty='/dev/pts/1'>\n      <source path='/dev/pts/1'/>\n      <target type='lxc' port='0'/>\n      <alias name='console0'/>\n    </console>\n  </devices>\n  <seclabel type='none'/>\n</domain>\n"

让我们通过调用isAlive()函数来验证容器是否正在运行,该函数返回一个布尔值:

In [11]: lxc_conn.isAlive()
Out[11]: 1

我们可以获取容器 ID,该 ID 应与通过运行前述virsh命令返回的 ID 匹配:

In [12]: lxc_conn.listDomainsID()
Out[12]: [23749]

毫无惊讶,ID 是相同的。

下一个代码片段遍历已定义容器的列表,并通过调用listAllDomains()方法返回域对象列表,从中打印出它们的名称:

In [13]: domains = lxc_conn.listAllDomains(0)
In [13]: for domain in domains:
....:     print('  '+domain.name())
....:
libvirt_python

API 提供了两种查找容器的方法,通过名称和 ID,并将其赋值给对象变量:

In [14]: container = lxc_conn.lookupByName("libvirt_python")
In [15]: container = lxc_conn.lookupByID(23749)

这在我们想要操作已存在的容器时非常有用。现在可以像平常一样使用container对象,通过调用其方法。

使用 libvirt Python 收集容器信息

让我们收集容器的内存信息。maxMemory()方法返回容器配置的最大内存:

In [16]: container.maxMemory()
Out[16]: 524288L

收集内存统计信息是通过memoryStats()方法完成的,该方法返回一个字典对象:

In [17]: container.memoryStats()
Out[17]: {'actual': 524288L, 'rss': 1388L, 'swap_in': 1388L}

当我们在 XML 文件中定义容器时,我们指定了域的操作系统类型为exe,这意味着容器将执行指定的二进制文件。要在运行中的容器上获取该信息,可以调用OSType()方法:

In [18]: container.OSType()
Out[18]: 'exe'

最后,为了获取更多关于容器的信息,我们可以调用info()函数:

In [19]: container.info()
Out[19]: [1, 524288L, 1352L, 1, 8080449759L]

结果是一个包含以下值的列表:

成员 描述
list[0] 表示容器状态的字符串
list[1] 容器的最大内存
list[2] 当前内存利用率
list[3] CPU 数量
list[4] CPU 时间

容器启动后,让我们看看接下来如何停止它并清理环境。

使用 libvirt Python 停止和删除 LXC 容器

在销毁容器之前,让我们验证一下它的状态和名称。

In [20]: container.isActive()
Out[20]: 1
In [21]: container.name()
Out[21]: 'libvirt_python'

要停止容器,调用container对象上的destroy()方法:

In [22]: container.destroy()
Out[22]: 0

在删除容器之前,让我们验证容器在主机上没有运行:

root@ubuntu:~# virsh --connect lxc:/// list --all
Id    Name                           State
----------------------------------------------------
-     libvirt_python                 shut off
root@ubuntu:~#

要删除容器,我们调用undefine()方法:

In [23]: container.undefine()
Out[23]: 0
root@ubuntu:~# virsh --connect lxc:/// list --all
Id    Name                           State
----------------------------------------------------
root@ubuntu:~#

注意

需要注意的是,并非所有的方法、函数和变量都可以在 libvirt LXC 驱动程序中使用,即使它们在导入 libvirt 库后可以在 ipython 解释器中列出。这是由于 libvirt 对多个虚拟化管理程序(如 KVM 和 XEN)的支持。在探索 API 调用时请记住这一点。

Vagrant 和 LXC

Vagrant 是一个优秀的开源项目,通过使用插件提供了构建隔离开发环境的方式,支持各种虚拟化技术,如 KVM 和 LXC。

在本节中,我们将简要介绍如何使用 LXC 进行隔离,设置 Vagrant 开发环境。

让我们从在 Ubuntu 上下载并安装 Vagrant 开始:

root@ubuntu:~# cd /usr/src/
root@ubuntu:/usr/src# wget https://releases.hashicorp.com/vagrant/1.8.5/vagrant_1.8.5_x86_64.deb
--2016-09-26 21:11:56-- https://releases.hashicorp.com/vagrant/1.8.5/vagrant_1.8.5_x86_64.deb
Resolving releases.hashicorp.com (releases.hashicorp.com)... 151.101.44.69
Connecting to releases.hashicorp.com (releases.hashicorp.com)|151.101.44.69|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 76325224 (73M) [application/x-debian-package]
Saving to: 'vagrant_1.8.5_x86_64.deb'
100%[============================================================
================================>] 76,325,224   104MB/s   in 0.7s
2016-09-26 21:11:57 (104 MB/s) - 'vagrant_1.8.5_x86_64.deb' saved [76325224/76325224]
root@ubuntu:/usr/src#

安装这个软件包非常简单:

root@ubuntu:/usr/src# dpkg --install vagrant_1.8.5_x86_64.deb
Selecting previously unselected package vagrant.
(Reading database ... 60326 files and directories currently installed.)
Preparing to unpack vagrant_1.8.5_x86_64.deb ...
Unpacking vagrant (1:1.8.5) ...
Setting up vagrant (1:1.8.5) ...
root@ubuntu:/usr/src#

在 CentOS 上,步骤如下:

  1. 执行以下命令以下载并安装 Vagrant:

     [root@centos ~]# cd /usr/src/
     [root@centossrc]# wget   
          https://releases.hashicorp.com/vagrant/1.8.5/
          vagrant_1.8.5_x86_64.rpm
    --2016-09-26 21:16:06-- 
          https://releases.hashicorp.com/vagrant/1.8.5/
          vagrant_1.8.5_x86_64.rpm
     Resolving releases.hashicorp.com (releases.hashicorp.com)...     
          151.101.44.69
     Connecting to releases.hashicorp.com       
          (releases.hashicorp.com)|151.101.44.69|:443... connected.
     HTTP request sent, awaiting response... 200 OK
     Length: 75955433 (72M) []
     Saving to: 'vagrant_1.8.5_x86_64.rpm'
     100%   
          [========================================================
          =====================================>] 
          75,955,433  104MB/s   in 0.7s
     2016-09-26 21:16:07 (104 MB/s) - 'vagrant_1.8.5_x86_64.rpm' saved 
          [75955433/75955433]
     [root@centossrc]# rpm --install vagrant_1.8.5_x86_64.rpm
    
    
  2. 接下来,如果 LXC 尚未附加到新的桥接设备,创建一个新的桥接设备:

     [root@centossrc]# brctladdbr lxcbr0
    
    
  3. 现在是时候安装 LXC 插件了:

     root@ubuntu:/usr/src# vagrant plugin install vagrant-lxc
     Installing the 'vagrant-lxc' plugin. This can take a few minutes...
     Installed the plugin 'vagrant-lxc (1.2.1)'!
     root@ubuntu:/usr/src#
    
    
  4. 如果一切顺利,请列出已安装的 Vagrant 插件:

     root@ubuntu:/usr/src# vagrant plugin list
     vagrant-lxc (1.2.1)
     vagrant-share (1.1.5, system)
     root@ubuntu:/usr/src#
    
    
  5. 安装了 LXC 插件后,创建一个新的项目目录:

     root@ubuntu:/usr/src# mkdirmy_project
     root@ubuntu:/usr/src# cd my_project/
    
    
  6. 接下来,通过指定我们将使用的盒子类型或虚拟机镜像,初始化一个新的 Vagrant 环境。在以下示例中,我们将使用来自 fgremh 仓库的 Ubuntu Precise LXC 镜像:

    root@ubuntu:/usr/src/my_project# vagrant init fgrehm/precise64-lxc
    A `Vagrantfile` has been placed in this directory. You are now
    ready to `vagrant up` your first virtual environment! Please read
    the comments in the Vagrantfile as well as documentation on
    `vagrantup.com` for more information on using Vagrant.
    root@ubuntu:/usr/src/my_project#
    
    

    如输出所示,项目目录中已创建了一个新的 Vagrantfile

    root@ubuntu:/usr/src/my_project# ls -alh
    total 12K
    drwxr-xr-x 2 root root 4.0K Sep 26 21:45 .
    drwxr-xr-x 5 root root 4.0K Sep 26 21:18 ..
    -rw-r--r-- 1 root root 3.0K Sep 26 21:45 Vagrantfile
    root@ubuntu:/usr/src/my_project#
    
    

    注意

    要查看 Vagrant 盒子列表,请访问:atlas.hashicorp.com/boxes/search

    让我们看看 Vagrantfile

     root@ubuntu:/usr/src/my_project# cat Vagrantfile | grep -v "#" 
          | sed '/^$/d'
    Vagrant.configure("2") do |config|
    config.vm.box = "fgrehm/precise64-lxc"
    end
    root@ubuntu:/usr/src/my_project#
    
    
  7. 配置非常简洁,仅指定 Vagrant 虚拟机将使用的镜像。我们通过明确指定提供程序来启动容器:

    root@ubuntu:/usr/src/my_project# vagrant up --provider=lxc
    Bringing machine 'default' up with 'lxc' provider...
    ==>default: Importing base box 'fgrehm/precise64-lxc'...
    ==>default: Checking if box 'fgrehm/precise64-lxc' is up to date...
    ==>default: Setting up mount entries for shared folders...
    default: /vagrant => /usr/src/my_project
    ==>default: Starting container...
    ==>default: Waiting for machine to boot. This may take a few 
          minutes...
    default: SSH address: 10.0.3.181:22
    default: SSH username: vagrant
    default: SSH auth method: private key
    default:
    default: Vagrant insecure key detected. Vagrant will automatically 
          replace
    default: this with a newly generated keypair for better security.
    default:
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH 
          key...
    ==>default: Machine booted and ready!
    root@ubuntu:/usr/src/my_project#
    
    
  8. 要验证是否有正在运行的 LXC 容器,请在命令行中执行以下命令:

    root@ubuntu:/usr/src/my_project# lxc-ls -f
    NAME                      STATE   AUTOSTART GROUPS IPV4      IPV6
    my_project_default_1474926399170_41712 RUNNING 0         -      
          10.0.3.80 -
    root@ubuntu:/usr/src/my_project#
    
    
  9. 让我们检查 Vagrant 虚拟机的状态:

    root@ubuntu:/usr/src/my_project# vagrant status
    Current machine states:
    default                   running (lxc)
    ...
    root@ubuntu:/usr/src/my_project#
    
    
  10. 要连接到 LXC 容器,请运行以下命令:

    root@ubuntu:/usr/src/my_project# vagrant ssh
    Welcome to Ubuntu 12.04.5 LTS (GNU/Linux 3.13.0-91-generic x86_64)
     * Documentation:  https://help.ubuntu.com/
    vagrant@vagrant-base-precise-amd64:~$ ps ax
     PID TTY      STAT   TIME COMMAND
    1 ?Ss     0:00 /sbin/init
    194 ?        S      0:00 upstart-socket-bridge --daemon
    2630 ?        S      0:00 upstart-udev-bridge --daemon
    2653 ?Ss     0:00 /sbin/udevd --daemon
    2672 ?Sl     0:00 rsyslogd -c5
    2674 ?Ss     0:00 rpcbind -w
    2697 ?Ss     0:00 rpc.statd -L
    2830 ?Ss     0:00 dhclient3 -e IF_METRIC=100 -pf 
          /var/run/dhclient.eth0.pid -lf 
          /var/lib/dhcp/dhclient.eth0.leases -1 eth0
    2852 ?Ss     0:00 /usr/sbin/sshd -D
    2888 ?Ss     0:00 cron
    3259 ?Ss     0:00 /sbin/getty -8 38400 tty4
    3260 ?Ss     0:00 /sbin/getty -8 38400 tty2
    3261 ?Ss     0:00 /sbin/getty -8 38400 tty3
    3262 ?Ss     0:00 /sbin/getty -8 38400 console
    3263 ?Ss     0:00 /sbin/getty -8 38400 tty1
    3266 ?Ss     0:00 sshd: vagrant [priv]
    3278 ?        S      0:00 sshd: vagrant@pts/9
     3279 pts/9    Ss     0:00 -bash
     3305 pts/9    R+     0:00ps ax
    vagrant@vagrant-base-precise-amd64:~$ exit
    logout
    Connection to 10.0.3.181 closed.
    root@ubuntu:/usr/src/my_project#
    
    

配置 Vagrant LXC

Vagrantfile 有很好的文档说明,以下是如何通过指定可用于 LXC 容器的内存量来定制 Vagrant 虚拟机的简要示例:

  1. 停止正在运行的 Vagrant 虚拟机:

    root@ubuntu:/usr/src/my_project# vagrant halt
    ==>default: Attempting graceful shutdown of VM...
    root@ubuntu:/usr/src/my_project#
    
    
  2. 编辑 Vagrantfile 并设置 cgroup.memory.limit_in_bytes cgroup 限制。新的配置应该如下所示:

    root@ubuntu:/usr/src/my_project# vim Vagrantfile
    Vagrant.configure("2") do |config|
    config.vm.box = "fgrehm/trusty64-lxc"
    config.vm.provider :lxc do |lxc|
    lxc.customize 'cgroup.memory.limit_in_bytes', '1024M'
    end
    end
    root@ubuntu:/usr/src/my_project#
    
    
  3. 保存文件并重新启动 Vagrant 虚拟机:

    root@ubuntu:/usr/src/my_project# vagrant up --provider=lxc
    Bringing machine 'default' up with 'lxc' provider...
    ==>default: Setting up mount entries for shared folders...
    default: /vagrant => /usr/src/my_project
    ==>default: Starting container...
    ==>default: Waiting for machine to boot. This may take a few 
          minutes...
    default: SSH address: 10.0.3.181:22
    default: SSH username: vagrant
    default: SSH auth method: private key
    ==>default: Machine booted and ready!
    ==>default: Machine already provisioned. Run `vagrant provision` or 
          use the `--provision`
    ==>default: flag to force provisioning. Provisioners marked to run 
          always will still run.
    root@ubuntu:/usr/src/my_project#
    
    
  4. 验证是否应用了 cgroup 限制:

    root@ubuntu:/usr/src/my_project# cat /sys/fs/cgroup/memory/lxc/ 
          my_project_default_1474926399170_41712/memory.limit_in_bytes
    1073741824
    root@ubuntu:/usr/src/my_project#
    
    
  5. 最后,让我们通过删除所有 Vagrant 虚拟机残留文件来清理:

    root@ubuntu:/usr/src/my_project# vagrant destroy
    default: Are you sure you want to destroy the 'default' VM? [y/N] y
    ==>default: Forcing shutdown of container...
    ==>default: Destroying VM and associated drives...
    root@ubuntu:/usr/src/my_project# lxc-ls -f
    NAME                    STATE   AUTOSTART GROUPS IPV4 IPV6
    root@ubuntu:/usr/src/my_project#
    
    

将所有内容结合起来 – 使用 Python 构建一个简单的 LXC RESTful API

通过之前实验 Python 的 LXC 绑定所掌握的所有知识,我们可以编写一个简单的 RESTful API,来构建、管理和销毁 LXC 容器。

注意

为了保持代码尽可能简单,我们将跳过所有错误和异常处理以及程序中的任何输入验证。

用于构建 API 的最简单 Python Web 框架之一是 Bottle。我们先安装它:

root@ubuntu:~# source lxc_python/bin/activate
(lxc_python) root@ubuntu:~# pip install bottle
Collecting bottle
 Downloading bottle-0.12.9.tar.gz (69kB)
 100% |████████████████████████████████| 71kB 7.1MB/s
Building wheels for collected packages: bottle
 Running setup.py bdist_wheel for bottle ... done
 Stored in directory: 
/root/.cache/pip/wheels/6e/87/89/f7ddd6721f4a208d44f2dac02f281b2403a314dd735d2b0e61
Successfully built bottle
Installing collected packages: bottle
Successfully installed bottle-0.12.9
(lxc_python) root@ubuntu:~#

在继续之前,请确保安装了 bottlelxc-python2 库:

(lxc_python) root@ubuntu:~# pip freeze
bottle==0.12.9
lxc-python2==0.1
(lxc_python) root@ubuntu:~#

打开新创建的 lxc_api.py 文件并编写以下代码:

import lxc
from bottle import run, request, get, post

@get('/list')
def list():
     container_list = lxc.list_containers()

     return "List of containers: {0}\n".format(container_list)
run(host='localhost', port=8080, debug=True)

run类提供了run()调用,用于启动内置服务器。在我们的示例中,服务器将监听本地主机上的8080端口。get()装饰器将下方函数中的代码链接到一个 URL 路径。在前面的代码中,/list路径与list()函数绑定。当然,您已经熟悉了list_containers()方法。

要测试这个简单的 API 前端,保存文件并执行程序:

(lxc_python) root@ubuntu:~# python lxc_api.py
Bottle v0.12.9 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl+C to quit. 

提示

如果你遇到socket.error: [Errno 98] Address already in use错误,说明另一个进程已经绑定了8080端口。要解决此问题,只需在run()方法中更改 Python 应用程序监听的端口。

在一个独立的终端窗口中,执行以下操作:

root@ubuntu:~# curl localhost:8080/list
List of containers: ()
root@ubuntu:~#

就是这么简单;我们创建了一个 API 调用来列出 LXC 容器!

用于构建和配置 LXC 容器的 API 调用

让我们稍微扩展一下功能,添加构建容器的能力。编辑文件并添加以下函数:

@post('/build')
def build():
     name = request.headers.get('X-LXC-Name')
     template = str(request.headers.get('X-LXC-Template'))

     container = lxc.Container(name)
     container.create(template)
     return "Building container {0} using the {1} template\n".format(
    name, template)

在这种情况下,我们将使用@post装饰器和request类提供的headers.get()方法来获取包含容器和模板名称的自定义头信息。

注意

有关 Bottle 框架的完整 API 参考,请参阅bottlepy.org/docs/dev/api.html

保存更新的文件并重启程序。让我们在第二个终端中测试新的调用:

root@ubuntu:~# curl -XPOST --header "X-LXC-Name: api_container" --header "X-LXC-  
Template: ubuntu" localhost:8080/build 
Building container api_container using the ubuntu template
root@ubuntu:~#

我们使用了--header标志通过curl将容器和模板名称作为头信息传递,使用POST动词。如果你检查正在运行的应用程序的终端,你可以看到容器构建的日志,以及 HTTP 路由和错误代码:

Bottle v0.12.9 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.
Copying rootfs to /var/lib/lxc/api_container/rootfs ...
Generating locales...
en_US.UTF-8... up-to-date
Generation complete.
Creating SSH2 RSA key; this may take some time ...
Creating SSH2 DSA key; this may take some time ...
Creating SSH2 ECDSA key; this may take some time ...
Creating SSH2 ED25519 key; this may take some time ...
update-rc.d: warning: default stop runlevel arguments (0 1 6) do not match ssh  
Default-Stop values (none)
invoke-rc.d: policy-rc.d denied execution of start.
...
127.0.0.1 - - [06/Oct/2016 21:36:10] "POST /build HTTP/1.1" 200 59

让我们使用之前定义的/list路由来列出所有容器:

root@ubuntu:~# curl localhost:8080/list
List of containers: (u'api_container',)
root@ubuntu:~#

太好了!现在我们可以构建和列出容器了。让我们创建一个新路由来启动 LXC。将以下函数添加到lxc_api.py文件中:

@post('/container/<name>/start')
def container_start(name):
     container = lxc.Container(name)
     container.start(useinit = False, daemonize = True)

     return "Starting container {0}\n".format(name)

我们再次使用了POST装饰器和动态路由。动态路由由一个名称组成,在我们的示例中为<name>,它将保存我们通过curl命令传递给路由的字符串值。绑定到container_start(name)路由的方法也接受一个同名的变量。保存更改,重启应用程序,并执行以下操作:

root@ubuntu:~# curl -XPOST localhost:8080/container/api_container/start
Starting container api_container
root@ubuntu:~#

我们在 URL 中传递了api_container,并且我们定义的路由能够匹配它,并将其作为变量传递给container_start函数。

我们的简单 API 尚未提供获取容器状态的路由,所以让我们确保它确实在运行:

root@ubuntu:~# lxc-ls -f
NAME          STATE   AUTOSTART GROUPS IPV4       IPV6
api_container RUNNING 0         -      10.0.3.198 -
root@ubuntu:~#

让我们向 API 添加一个state调用:

@get('/container/<name>/state')
def container_status(name):
     container = lxc.Container(name)
     state = container.state

     return "The state of container {0} is {1}\n".format(name, state)

这次我们使用了@get装饰器,并在container对象上调用了state()方法。让我们测试一下新路由:

root@ubuntu:~# curl localhost:8080/container/api_container/state
The state of container api_container is RUNNING
root@ubuntu:~#

现在我们有了一个正在运行的容器,让我们添加列出其 IP 地址的功能:

@get('/container/<name>/ips')
def container_status(name):
     container = lxc.Container(name)
     ip_list = container.get_ips()
     return "Container {0} has the following IP's {1}\n".format(
     name, ip_list)

这里没有什么新的需要注意的内容,接下来让我们看看返回的结果:

root@ubuntu:~# curl localhost:8080/container/api_container/ips
Container api_container has the following IP's (u'10.0.3.198',)
root@ubuntu:~#

我们之前已经在本章中看到如何冻结和解冻容器;现在让我们把这个功能添加到我们的 API 中:

@post('/container/<name>/freeze')
def container_start(name):
     container = lxc.Container(name)
     container.freeze()
     return "Freezing container {0}\n".format(name)

@post('/container/<name>/unfreeze')
def container_start(name):
     container = lxc.Container(name)
     container.unfreeze()
     return "Unfreezing container {0}\n".format(name)

这里使用 POST 方法更为合适,因为我们正在修改容器的状态。现在让我们冻结容器并检查其状态:

root@ubuntu:~# curl -XPOST localhost:8080/container/api_container/freeze
Freezing container api_container
root@ubuntu:~# curl localhost:8080/container/api_container/state
The state of container api_container is FROZEN
root@ubuntu:~# lxc-ls -f
NAME          STATE  AUTOSTART GROUPS IPV4       IPV6
api_container FROZEN 0         -      10.0.3.198 -
root@ubuntu:~#

最后,让我们用我们刚才创建的新的 API 调用解冻它:

root@ubuntu:~# curl -XPOST localhost:8080/container/api_container/unfreeze
Unfreezing container api_container
root@ubuntu:~# curl localhost:8080/container/api_container/state
The state of container api_container is RUNNING
root@ubuntu:~# lxc-ls -f
NAME          STATE   AUTOSTART GROUPS IPV4       IPV6
api_container RUNNING 0         -      10.0.3.198 -
root@ubuntu:~#

接下来,作为结论,我们将编写两个新函数来停止和删除容器:

@post('/container/<name>/stop')
def container_start(name):
     container = lxc.Container(name)
     container.stop()

     return "Stopping container {0}\n".format(name)

@post('/container/<name>/destroy')
def container_start(name):
     container = lxc.Container(name)
     container.destroy() 

     return "Destroying container {0}\n".format(name)

到目前为止,我们使用的所有方法,都已经在本章中进行过测试;如有需要,请随时参考它们的描述。

使用 API 调用进行清理

现在是清理的时候了,通过调用 stop API 路由:

root@ubuntu:~# curl -XPOST localhost:8080/container/api_container/stop
Stopping container api_container 
root@ubuntu:~# curl localhost:8080/container/api_container/state
The state of container api_container is STOPPED
root@ubuntu:~#

在执行完之前的所有 API 调用后,控制台中的运行应用程序应显示类似如下内容:

127.0.0.1 - - [06/Oct/2016 22:00:08] "GET /container/api_container/state HTTP/1.1" 200 48
127.0.0.1 - - [06/Oct/2016 22:02:45] "GET /container/api_container/ips HTTP/1.1" 200 64
127.0.0.1 - - [06/Oct/2016 22:08:14] "POST /container/api_container/freeze HTTP/1.1" 200 33
127.0.0.1 - - [06/Oct/2016 22:08:19] "GET /container/api_container/state HTTP/1.1" 200 47
127.0.0.1 - - [06/Oct/2016 22:08:30] "POST /container/api_container/unfreeze HTTP/1.1" 200 35
127.0.0.1 - - [06/Oct/2016 22:08:32] "GET /container/api_container/state HTTP/1.1" 200 48
127.0.0.1 - - [06/Oct/2016 22:18:39] "POST /container/api_container/stop HTTP/1.1" 200 33
127.0.0.1 - - [06/Oct/2016 22:18:41] "GET /container/api_container/state HTTP/1.1" 200 48

最后,让我们销毁容器:

root@ubuntu:~# curl -XPOST localhost:8080/container/api_container/destroy
Destroying container api_container
root@ubuntu:~# lxc-ls -f
root@ubuntu:~#

我们可以轻松地添加之前实验过的所有 LXC Python 方法,只需遵循相同的模式 —— 只需记住捕获所有异常并验证输入。

这是整个程序:

import lxc
from bottle import run, request, get, post
@post('/build')
def build():
 name = request.headers.get('X-LXC-Name')
 memory = request.headers.get('X-LXC-Memory')
 template = str(request.headers.get('X-LXC-Template'))

 container = lxc.Container(name)
 container.create(template)

 return "Building container {0} using the {1} template\n".format(
    name, template)

@post('/container/<name>/start')
def container_start(name):
 container = lxc.Container(name)
 container.start(useinit=False, daemonize=True)

 return "Starting container {0}\n".format(name)

@post('/container/<name>/stop')
def container_start(name):
 container = lxc.Container(name)
 container.stop()

 return "Stopping container {0}\n".format(name)

@post('/container/<name>/destroy')
def container_start(name):
 container = lxc.Container(name)
 container.destroy()

 return "Destroying container {0}\n".format(name)

@post('/container/<name>/freeze')
def container_start(name):
 container = lxc.Container(name)
 container.freeze()

 return "Freezing container {0}\n".format(name)

@post('/container/<name>/unfreeze')
def container_start(name):
 container = lxc.Container(name)
 container.unfreeze()

 return "Unfreezing container {0}\n".format(name)

@get('/container/<name>/state')
def container_status(name):
 container = lxc.Container(name)
 state = container.state

return "The state of container {0} is {1}\n".format(name, state)

@get('/container/<name>/ips')
def container_status(name):
 container = lxc.Container(name)
 ip_list = container.get_ips()

return "Container {0} has the following IP's {1}\n".format(
    name, ip_list)

 @get('/list')
def list():
 container_list = lxc.list_containers()

 return "List of containers: {0}\n".format(container_list)

run(host='localhost', port=8080, debug=True)

总结

LXC 和 libvirt API 提供的 Python 绑定是编程创建和管理 LXC 容器的一个好方法。

在本章中,我们通过编写简单的代码片段,探索了两组 Python 绑定,这些代码实现了用户空间工具提供的大部分功能。事实上,了解这些 API 的最佳方法是查看命令行工具的源代码,尽管它们是用 C 语言实现的。

我们简要介绍了如何使用 Vagrant 配置 LXC,以便在隔离的环境中测试代码。我们在本章的结尾实现了一个简单的 RESTful API,该 API 使用我们之前探索的一些方法来配置、管理和销毁 LXC。在第五章,在 LXC 中使用 Linux 桥接和 Open vSwitch 进行网络配置,我们将探讨 LXC 的网络方面,使用 Linux 桥接、NAT 中的 Open vSwitch 和直接路由模式,并查看如何互联容器与宿主操作系统的示例。

第五章:使用 Linux 桥接和 Open vSwitch 在 LXC 中的网络

要为新建的容器启用网络连接,我们需要一种方法将容器网络命名空间中的虚拟网络接口连接到主机,并在需要时为其他容器或互联网提供路由。Linux 提供了一个软件桥接,允许我们以多种方式将 LXC 容器连接在一起,正如我们将在本章中探讨的那样。

有两种流行的软件桥接实现——bridge-utils包提供的 Linux 桥接和 Open vSwitch 项目。这些通过将交换机的控制平面和管理平面分离,进一步扩展了 Linux 桥接的基本功能,从而实现了流量控制并提供硬件集成等功能。

默认情况下,当我们从提供的模板构建容器时,模板脚本会通过在主机操作系统上使用iptables中的网络地址转换NAT)规则来配置网络桥接。在这种模式下,容器从 LXC 启动的dnsmasq服务器获取 IP 地址。然而,我们可以通过容器的配置文件完全控制我们希望使用的桥接、模式或路由。

在本章中,我们将探讨以下主题:

  • 安装和配置 Linux 桥接

  • 安装和创建 Open vSwitch 交换机

  • 使用 NAT、直连、VLAN 等模式在 LXC 中配置网络

Linux 中的软件桥接

连接 LXC 或任何其他类型的虚拟机(如 KVM 或 Xen)、虚拟化管理层,或者在 LXC 的情况下,主机操作系统,要求能够在容器/虚拟机与外部世界之间桥接流量。Linux 中的软件桥接自内核版本 2.4 起便已被支持。为了利用这一功能,必须通过设置网络支持 | 网络选项 | 802.1d 以太网桥接为“是”来启用桥接,或者在配置内核时作为内核模块进行启用。

要检查内核中编译了哪些桥接选项,或作为模块提供哪些选项,请运行以下命令:

root@host:~# cat /boot/config-`uname -r` | grep -ibridge
# PC-card bridges
CONFIG_BRIDGE_NETFILTER=y
CONFIG_NF_TABLES_BRIDGE=m
CONFIG_BRIDGE_NF_EBTABLES=m
CONFIG_BRIDGE_EBT_BROUTE=m
CONFIG_BRIDGE_EBT_T_FILTER=m
CONFIG_BRIDGE_EBT_T_NAT=m
CONFIG_BRIDGE_EBT_802_3=m
CONFIG_BRIDGE_EBT_AMONG=m
CONFIG_BRIDGE_EBT_ARP=m
CONFIG_BRIDGE_EBT_IP=m
CONFIG_BRIDGE_EBT_IP6=m
CONFIG_BRIDGE_EBT_LIMIT=m
CONFIG_BRIDGE_EBT_MARK=m
CONFIG_BRIDGE_EBT_PKTTYPE=m
CONFIG_BRIDGE_EBT_STP=m
CONFIG_BRIDGE_EBT_VLAN=m
CONFIG_BRIDGE_EBT_ARPREPLY=m
CONFIG_BRIDGE_EBT_DNAT=m
CONFIG_BRIDGE_EBT_MARK_T=m
CONFIG_BRIDGE_EBT_REDIRECT=m
CONFIG_BRIDGE_EBT_SNAT=m
CONFIG_BRIDGE_EBT_LOG=m
# CONFIG_BRIDGE_EBT_ULOG is not set
CONFIG_BRIDGE_EBT_NFLOG=m
CONFIG_BRIDGE=m
CONFIG_BRIDGE_IGMP_SNOOPING=y
CONFIG_BRIDGE_VLAN_FILTERING=y
CONFIG_SSB_B43_PCI_BRIDGE=y
CONFIG_DVB_DDBRIDGE=m
CONFIG_EDAC_SBRIDGE=m
# VME Bridge Drivers
root@host:~#

在 Ubuntu 和 CentOS 系统上,桥接作为内核模块提供。要验证它们是否已加载,请运行以下命令:

root@ubuntu:~# lsmod | grep bridge
bridge                110925  0
stp                    12976  1 bridge
llc                    14552  2 stp,bridge
root@ubuntu:~#

要获取更多关于bridge内核模块的信息,请执行以下命令:

[root@centos ~]# modinfo bridge
filename:       /lib/modules/3.10.0-327.28.3.el7.x86_64/kernel/net/bridge/bridge.ko
alias:          rtnl-link-bridge
version:        2.3
license:        GPL
rhelversion:    7.2
srcversion:     905847C53FF43DEFAA0EB3C
depends:        stp,llc
intree:         Y
vermagic:       3.10.0-327.28.3.el7.x86_64 SMP mod_unloadmodversions
signer:         CentOS Linux kernel signing key
sig_key:        15:64:6F:1E:11:B7:3F:8C:2A:ED:8A:E2:91:65:5D:52:58:05:6E:E9
sig_hashalgo:   sha256
[rootcentos ~]# 

如果你使用的发行版没有将桥接编译进内核,或者没有作为模块提供,或者如果你想尝试不同的内核选项,你需要首先获取内核源代码。

要在 Ubuntu 上安装它,可以通过运行以下命令来完成:

root@ubuntu:~# cd /usr/src/
root@ubuntu:/usr/src# apt-get install linux-source ncurses-dev
root@ubuntu:/usr/src# cd linux-source-3.13.0/
root@ubuntu:/usr/src/linux-source-3.13.0# tar jxfv linux-source-3.13.0.tar.bz2
root@ubuntu:/usr/src/linux-source-3.13.0# cd linux-source-3.13.0/

在 CentOS 上,使用yum安装内核源代码:

[root@bridge ~]# yum install kernel-devel ncurses-devel

要使用ncurses菜单配置内核,请运行以下命令:

root@ubuntu:/usr/src/linux-source-3.13.0/linux-source-3.13.0# make menuconfig

转到网络支持 | 网络选项 | 802.1d 以太网桥接,并选择Y以将桥接功能编译到内核中,或选择M将其编译为模块。

内核配置菜单如下所示:

Linux 中的软件桥接

选择完成后,构建新的内核包并安装:

root@bridge:/usr/src/linux-source-3.13.0/linux-source-3.13.0# make deb-pkg
 CHK     include/config/kernel.release
make KBUILD_SRC=
SYSHDR  arch/x86/syscalls/../include/generated/uapi/asm/unistd_32.h
SYSHDR  arch/x86/syscalls/../include/generated/uapi/asm/unistd_64.h
...
dpkg-deb: building package `linux-firmware-image-3.13.11-ckt39' 
in `../linux-firmware-image-3.13.11-ckt39_3.13.11-ckt39-1_amd64.deb'.
dpkg-deb: building package `linux-headers-3.13.11-ckt39' 
in `../linux-headers-3.13.11-ckt39_3.13.11-ckt39-1_amd64.deb'.
dpkg-deb: building package `linux-libc-dev' 
in `../linux-libc-dev_3.13.11-ckt39-1_amd64.deb'.
dpkg-deb: building package `linux-image-3.13.11-ckt39' 
in `../linux-image-3.13.11-ckt39_3.13.11-ckt39-1_amd64.deb'.
dpkg-deb: building package `linux-image-3.13.11-ckt39-dbg' 
in `../linux-image-3.13.11-ckt39-dbg_3.13.11-ckt39-1_amd64.deb'.
root@ubuntu:/usr/src/linux-source-3.13.0/linux-source-3.13.0# ls -la ../*.deb
-rw-r--r-- 1 root root    803530 Oct 19 18:04 ../linux-firmware-image-3.13.11-ckt39_3.13.11-ckt39-1_amd64.deb
-rw-r--r-- 1 root root   6435516 Oct 19 18:04 ../linux-headers-3.13.11-ckt39_3.13.11-ckt39-1_amd64.deb
-rw-r--r-- 1 root root  39640242 Oct 19 18:07 ../linux-image-3.13.11-ckt39_3.13.11-ckt39-1_amd64.deb
-rw-r--r-- 1 root root 363727500 Oct 19 18:37 ../linux-image-3.13.11-ckt39-dbg_3.13.11-ckt39-1_amd64.deb
-rw-r--r-- 1 root root    768606 Oct 19 18:04 ../linux-libc-dev_3.13.11-ckt39-1_amd64.deb
root@ubuntu:/usr/src/linux-source-3.13.0/linux-source-3.13.0#

要使用新内核,安装这些包并重启。

注意

有关如何从源代码编译和安装 Linux 内核的更多信息,请参考您的发行版文档。

Linux 桥接

内置的 Linux 桥接是一个软件层 2 设备。OSI 层 2 设备提供了一种将多个以太网段连接在一起并基于 MAC 地址转发流量的方式,实质上创建了独立的广播域。

让我们从源代码开始在 Ubuntu 上安装最新版本:

root@ubuntu:~# cd /usr/src/
root@ubuntu:/usr/src# apt-get update && apt-get install build-essential automakepkg-config git
root@ubuntu:/usr/src# git clone git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/bridge-utils.git
Cloning into 'bridge-utils'...
remote: Counting objects: 654, done.
remote: Total 654 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (654/654), 131.72 KiB | 198.00 KiB/s, done.
Resolving deltas: 100% (425/425), done.

Checking connectivity... done. 
root@ubuntu:/usr/src# cd bridge-utils/
root@ubuntu:/usr/src/bridge-utils# autoconf
root@ubuntu:/usr/src/bridge-utils# ./configure && make && make install
root@ubuntu:/usr/src/bridge-utils# brctl --version
bridge-utils, 1.5
root@ubuntu:/usr/src/bridge-utils#

从前面的输出中,我们可以看到我们首先克隆了git仓库中的bridge-utils项目,然后编译了源代码。

在 CentOS 上编译桥接软件的过程与我们在上一节中做的类似;首先我们安装所需的先决包,然后进行配置和编译,如下所示:

[root@centos ~]# cd /usr/src/
[root@centossrc]#
[root@centossrc]# yum groupinstall "Development tools"
[root@centossrc]# git clone git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/bridge-utils.git
Cloning into 'bridge-utils'...
remote: Counting objects: 654, done.
remote: Total 654 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (654/654), 131.72 KiB | 198.00 KiB/s, done.
Resolving deltas: 100% (425/425), done.
Checking connectivity... done. 

[root@centossrc]# cd bridge-utils
[root@centos bridge-utils]# autoconf
[root@centos bridge-utils]# ./configure && make && make install
[root@centos bridge-utils]# brctl --version
bridge-utils, 1.5
[root@centos bridge-utils]#

调用brctl命令而不带任何参数可以显示可用的操作:

root@ubuntu:/usr/src/bridge-utils# brctl
Usage: brctl [commands]
commands:
 addbr  <bridge>    add bridge
 delbr  <bridge>    delete bridge
 addif  <bridge><device>  add interface to bridge
 delif  <bridge><device>  delete interface from bridge
 hairpin     <bridge><port> {on|off}  turn hairpin on/off
 setageing  <bridge><time>    set ageing time
 setbridgeprio  <bridge><prio>    set bridge priority
 setfd  <bridge><time>    set bridge forward delay
 sethello  <bridge><time>    set hello time
 setmaxage  <bridge><time>    set max message age
 setpathcost  <bridge><port><cost>  set path cost
 setportprio  <bridge><port><prio>  set port priority
 show  [ <bridge> ]    show a list of bridges
 showmacs  <bridge>    show a list of mac addrs
 showstp  <bridge>    show bridge stp info
 stp  <bridge> {on|off}  turn stp on/off
root@ubuntu:/usr/src/bridge-utils#

Linux 桥接和 Ubuntu 上的 LXC 包

让我们安装 LXC 包和依赖项。要检查系统中配置的存储库中可用的最新包版本,请运行以下命令:

root@ubuntu:~# apt-cache madison lxc
 lxc | 2.0.4-0ubuntu1~ubuntu14.04.1 | http://us-east-1.ec2.archive.ubuntu.com/ubuntu/ trusty-backports/main amd64 Packages
lxc | 1.0.8-0ubuntu0.3 | http://us-east-1.ec2.archive.ubuntu.com/ubuntu/ trusty-updates/main amd64 Packages
lxc | 1.0.7-0ubuntu0.7 | http://security.ubuntu.com/ubuntu/ trusty-security/main amd64 Packages
lxc | 1.0.3-0ubuntu3 | http://us-east-1.ec2.archive.ubuntu.com/ubuntu/ trusty/main amd64 Packages
lxc | 1.0.3-0ubuntu3 | http://us-east-1.ec2.archive.ubuntu.com/ubuntu/ trusty/main Sources
lxc | 1.0.8-0ubuntu0.3 | http://us-east-1.ec2.archive.ubuntu.com/ubuntu/ trusty-updates/main Sources
lxc | 2.0.4-0ubuntu1~ubuntu14.04.1 | http://us-east-1.ec2.archive.ubuntu.com/ubuntu/ trusty-backports/main Sources
lxc | 1.0.7-0ubuntu0.7 | http://security.ubuntu.com/ubuntu/ trusty-security/main Sources
root@ubuntu:~#

输出来自一个 Amazon EC2 实例,显示最新的 LXC 版本为2.0.4-0ubuntu1~ubuntu14.04.1。让我们安装它并观察输出:

root@ubuntu:/usr/src/bridge-utils# apt-get install -y lxc=2.0.4-0ubuntu1~ubuntu14.04.1 lxc1=2.0.4-0ubuntu1~ubuntu14.04.1 liblxc1=2.0.4-0ubuntu1~ubuntu14.04.1 python3-lxc=2.0.4-0ubuntu1~ubuntu14.04.1 cgroup-lite=1.11~ubuntu14.04.2 lxc-templates=2.0.4-0ubuntu1~ubuntu14.04.1
...
Setting up lxc1 (2.0.4-0ubuntu1~ubuntu14.04.1) ...
lxc-net start/running
Setting up lxc dnsmasq configuration.
...
root@ubuntu:/usr/src/bridge-utils#

请注意,包安装并配置了dnsmasq服务,这是由于lxc包中lxc1.postinst脚本部分的包依赖关系。这在 Ubuntu 上非常方便,但如果您运行的发行版不支持此功能,或者您是从源代码编译 LXC,您始终可以手动安装它。只有在您希望dnsmasq动态分配 IP 地址给容器时,才需要这样做。您始终可以为 LXC 容器配置静态 IP 地址。

从前面的输出中,我们还可以观察到该包启动了lxc-net服务。让我们通过检查其状态来深入了解:

root@ubuntu:/usr/src/bridge-utils# service lxc-net status
lxc-net start/running
root@ubuntu:/usr/src/bridge-utils#

此外,还请查看init配置文件:

root@ubuntu:/usr/src/bridge-utils# cat /etc/init/lxc-net.conf
description "lxc network"
author "Serge Hallyn<serge.hallyn@canonical.com>"
start on starting lxc
stop on stopped lxc
pre-start exec /usr/lib/x86_64-linux-gnu/lxc/lxc-net start
post-stop exec /usr/lib/x86_64-linux-gnu/lxc/lxc-net stop
root@ubuntu:/usr/src/bridge-utils#

我们可以看到init脚本启动了lxc-net服务。让我们看看它提供了什么:

root@ubuntu:/usr/src/bridge-utils# head -24 /usr/lib/x86_64-linux-gnu/lxc/lxc-net
#!/bin/sh -
distrosysconfdir="/etc/default"
varrun="/run/lxc"
varlib="/var/lib"
# These can be overridden in /etc/default/lxc
#   or in /etc/default/lxc-net
USE_LXC_BRIDGE="true"
LXC_BRIDGE="lxcbr0"
LXC_ADDR="10.0.3.1"
LXC_NETMASK="255.255.255.0"
LXC_NETWORK="10.0.3.0/24"
LXC_DHCP_RANGE="10.0.3.2,10.0.3.254"
LXC_DHCP_MAX="253"
LXC_DHCP_CONFILE=""
LXC_DOMAIN=""
LXC_IPV6_ADDR=""
LXC_IPV6_MASK=""
LXC_IPV6_NETWORK=""
LXC_IPV6_NAT="false"
root@bridge:/usr/src/bridge-utils#

从前面脚本的前几行中,我们可以看到它设置了 LXC 网络的默认值,例如 Linux 桥接的名称和将由dnsmasq服务分配的子网。它还指向了我们可以用来覆盖这些选项的默认 LXC 网络文件。

让我们来看看默认的lxc-net文件:

root@ubuntu:/usr/src/bridge-utils# cat /etc/default/lxc-net  | grep -vi ^#
USE_LXC_BRIDGE="true"
LXC_BRIDGE="lxcbr0"
LXC_ADDR="10.0.3.1"
LXC_NETMASK="255.255.255.0"
LXC_NETWORK="10.0.3.0/24"
LXC_DHCP_RANGE="10.0.3.2,10.0.3.254"
LXC_DHCP_MAX="253"
root@ubuntu:/usr/src/bridge-utils#

Ubuntu 上的 LXC 被打包成这样的方式,它也为我们创建了桥接:

root@ubuntu:/usr/src/bridge-utils# brctl show
bridge name  bridge id           STP enabled  interfaces
lxcbr0       8000.000000000000   no
root@ubuntu:/usr/src/bridge-utils#

注意,桥接的名称—lxcbr0—是/etc/default/lxc-net文件和/usr/lib/x86_64-linux-gnu/lxc/lxc-net脚本中指定的名称。

CentOS 上的 Linux 桥接和 LXC 包

不幸的是,并非所有 Linux 发行版都在打包 LXC 时附带了构建桥接、配置和启动dnsmasq的额外功能,正如我们在之前的 Ubuntu 部分中看到的那样。从源代码构建 LXC,如第二章所述,在 Linux 系统上安装和运行 LXC,或使用 CentOS 的包,将不会自动创建 Linux 桥接,或配置并启动dnsmasq服务。

让我们在 CentOS 上更详细地探讨这个问题,通过使用centos模板构建一个名为bridge的 LXC 容器:

[root@centos ~]# yum install lxc lxc-templates
[root@centos ~]# lxc-create --name bridge -t centos
Host CPE ID from /etc/os-release: cpe:/o:centos:centos:7
Checking cache download in /var/cache/lxc/centos/x86_64/7/rootfs ...
Downloading centos minimal ...
Loaded plugins: fastestmirror, langpacks
...
Container rootfs and config have been created.
Edit the config file to check/enable networking setup.
...
[root@centos ~]#

让我们检查在安装lxc包并构建容器后是否创建了桥接:

[root@centos ~]# brctl show
[root@centos ~]#

如果我们尝试启动容器,将会遇到以下错误:

[root@centos ~]# lxc-ls -f
bridge 
[root@centos ~]# lxc-start --name bridge
lxc-start: conf.c: instantiate_veth: 3105 failed to attach 'veth5TVOEO' to the bridge 'virbr0': No such device
lxc-start: conf.c: lxc_create_network: 3388 failed to create netdev
lxc-start: start.c: lxc_spawn: 841 failed to create the network
lxc-start: start.c: __lxc_start: 1100 failed to spawn 'bridge'
lxc-start: lxc_start.c: main: 341 The container failed to start.
lxc-start: lxc_start.c: main: 345 Additional information can be obtained by setting the --logfile and --logpriority options.
[root@centos ~]#

上述输出显示容器正在尝试连接一个名为virbr0的桥接,但该桥接并不存在。该名称在以下文件中定义,并随后分配给容器的配置:

[root@centos ~]# cat /etc/lxc/default.conf
lxc.network.type = veth
lxc.network.link = virbr0
lxc.network.flags = up 
[root@centos ~]# cat /var/lib/lxc/bridge/config  | grep -vi "#" | sed '/^$/d' | grep network
lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = virbr0
lxc.network.hwaddr = fe:6e:b6:af:24:2b 
[root@centos ~]#

为了成功启动容器,我们必须首先创建 LXC 期望的桥接:

[root@centos ~]# brctl addbr virbr0

然后重新启动容器并检查桥接:

[root@centos~]# lxc-start --name bridge
[root@centos~]# brctl show
bridge name    bridge id             STP enabled   interfaces
virbr0         8000.fe1af1cb0b2e     no            vethB6CRLW
[root@centos ~]#

vethB6CRLW接口是 LXC 容器呈现给主机的虚拟接口,并连接到virbr0桥接端口:

[root@centos ~]# ifconfig vethB6CRLW
vethB6CRLW: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>mtu 1500
inet6 fe80::fc1a:f1ff:fecb:b2e  prefixlen 64  scopeid 0x20<link>
ether fe:1a:f1:cb:0b:2e  txqueuelen 1000  (Ethernet)
 RX packets 14  bytes 2700 (2.6 KiB)
 RX errors 0  dropped 0  overruns 0  frame 0
 TX packets 8  bytes 648 (648.0 B)
 TX errors 0  dropped 0 overruns 0  carrier 0  collisions 
        0
[root@centos ~]#

上述输出显示了我们之前构建的bridge容器的虚拟接口的当前配置,如主机操作系统所看到的。

使用dnsmasq服务在容器中获取 IP 地址

在 Ubuntu 上安装lxc包后启动的dnsmasq服务应类似于以下内容:

root@bridge:/usr/src/bridge-utils# pgrep -lfaww dnsmasq
12779 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@bridge:/usr/src/bridge-utils#

注意

请注意,dhcp-range参数与/etc/default/lxc-net文件中定义的内容相匹配。

让我们创建一个新的容器并探索其网络设置:

root@bridge:/usr/src/bridge-utils# lxc-create -t ubuntu --name br1
Checking cache download in /var/cache/lxc/trusty/rootfs-amd64 ...
Installing packages in template: apt-transport-https,ssh,vim,language-pack-en
Downloading ubuntu trusty minimal ...
...
##
# The default user is 'ubuntu' with password 'ubuntu'!
# Use the 'sudo' command to run tasks as root in the container.
##
root@bridge:/usr/src/bridge-utils# lxc-start --name br1
root@bridge:/usr/src/bridge-utils# lxc-info --name br1
Name:           br1
State:          RUNNING
PID:            1773
IP:             10.0.3.65
CPU use:        1.66 seconds
BlkIO use:      160.00 KiB
Memory use:     7.27 MiB
KMem use:       0 bytes
Link:           veth366R6F
 TX bytes:      1.85 KiB
RX bytes:      1.59 KiB
Total bytes:   3.44 KiB 
root@bridge:/usr/src/bridge-utils#

请注意,在主机操作系统上创建的虚拟接口名称—veth366R6F,这是lxc-info命令输出中的名称。该接口应作为端口添加到桥接中。让我们使用brctl工具确认这一点:

root@bridge:/usr/src/bridge-utils# brctl show
bridge name  bridge id          STP enabled   interfaces
lxcbr0       8000.fe7a39cee87c  no            veth366R6F
root@bridge:/usr/src/bridge-utils#

列出主机上的所有接口可以显示与容器关联的桥接和虚拟接口:

root@bridge:~# ifconfig
eth0      Link encap:EthernetHWaddr bc:76:4e:10:6a:31
inet addr:10.208.131.214  Bcast:10.208.255.255  Mask:255.255.128.0
inet6addr: fe80::be76:4eff:fe10:6a31/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
 RX packets:322 errors:0 dropped:0 overruns:0 frame:0
 TX packets:337 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:14812 (14.8 KB)  TX bytes:14782 (14.7 KB)
lo        Link encap:Local Loopback
inet addr:127.0.0.1  Mask:255.0.0.0
inet6addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING  MTU:65536  Metric:1
 RX packets:9 errors:0 dropped:0 overruns:0 frame:0
 TX packets:9 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0
 RX bytes:668 (668.0 B)  TX bytes:668 (668.0 B)
lxcbr0    Link encap:EthernetHWaddr fe:7a:39:ce:e8:7c
inet addr:10.0.3.1  Bcast:0.0.0.0  Mask:255.255.255.0
inet6addr: fe80::fc7a:39ff:fece:e87c/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
 RX packets:53 errors:0 dropped:0 overruns:0 frame:0
 TX packets:57 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0
 RX bytes:4268 (4.2 KB)  TX bytes:5450 (5.4 KB)
veth366R6F Link encap:EthernetHWaddr fe:7a:39:ce:e8:7c
inet6addr: fe80::fc7a:39ff:fece:e87c/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
 RX packets:53 errors:0 dropped:0 overruns:0 frame:0
 TX packets:58 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:5010 (5.0 KB)  TX bytes:5528 (5.5 KB)
root@bridge:~#

注意分配给lxcbr0接口的 IP 地址—它与传递给dnsmasq进程的listen-address参数相同。让我们首先通过连接到容器来检查容器内的网络接口和路由:

root@bridge:~# lxc-attach --name br1
root@br1:~# ifconfig
eth0      Link encap:EthernetHWaddr 00:16:3e:39:cf:e9
inet addr:10.0.3.65  Bcast:10.0.3.255  Mask:255.255.255.0
inet6addr: fe80::216:3eff:fe39:cfe9/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
 RX packets:58 errors:0 dropped:0 overruns:0 frame:0
 TX packets:53 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:5528 (5.5 KB)  TX bytes:5010 (5.0 KB)
lo        Link encap:Local Loopback
inet addr:127.0.0.1  Mask:255.0.0.0
inet6addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING  MTU:65536  Metric:1
 RX packets:0 errors:0 dropped:0 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0
 RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
root@br1:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.0.3.1        0.0.0.0         UG    0      0        0 eth0
10.0.3.0        0.0.0.0         255.255.255.0   U     0      0        0 eth0
root@br1:~#

dnsmasqeth0接口分配的 IP 地址位于10.0.3.0/24子网中,默认网关是主机上桥接接口的 IP 地址。容器之所以自动获得 IP 地址,是因为其接口配置文件:

root@br1:~# cat /etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).
# The loopback network interface
auto lo
iface lo inet loopback
auto eth0
iface eth0 inetdhcp
root@br1:~#

正如前面的输出所示,eth0 配置为使用 DHCP。如果我们宁愿使用静态分配的地址,只需更改该文件并指定我们想要使用的任何 IP 地址。使用 dnsmasq 配合 DHCP 进行 LXC 网络连接并非必需,但它可以提供便利。

让我们通过不分配 /etc/default/lxc-net 文件中的前一百个 IP 来更改 dnsmasq 提供的 IP 范围:

root@bridge:/usr/src/bridge-utils# sed -i 's/LXC_DHCP_RANGE="10.0.3.2,10.0.3.254"/LXC_DHCP_RANGE="10.0.3.100,10.0.3.254"/g' /etc/default/lxc-net 
root@bridge:/usr/src/bridge-utils# grep LXC_DHCP_RANGE /etc/default/lxc-net
LXC_DHCP_RANGE="10.0.3.100,10.0.3.254"
root@bridge:/usr/src/bridge-utils#

在重新启动 dnsmasq 后,观察作为 dhcp-range 参数传递的新 DHCP 范围:

root@bridge:/usr/src/bridge-utils# pgrep -lfaww dnsmasq
4215 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.100,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@bridge:/usr/src/bridge-utils#

下次我们使用 Ubuntu 模板构建容器时,分配给容器的 IP 地址将从第四个八位组的 100 开始。如果我们想将前 100 个 IP 用于手动分配,使用这种方式很方便,正如我们接下来所看到的那样。

在 LXC 容器中静态分配 IP 地址

在容器内部分配 IP 地址与配置常规 Linux 服务器没有什么不同。在容器内运行以下命令:

root@br1:~# ifconfig eth0 10.0.3.10 netmask 255.255.255.0
root@br1:~# route add default gw 10.0.3.1
root@br1:~# ping -c 3 10.0.3.1
PING 10.0.3.1 (10.0.3.1) 56(84) bytes of data.
64 bytes from 10.0.3.1: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 10.0.3.1: icmp_seq=2 ttl=64 time=0.073 ms
64 bytes from 10.0.3.1: icmp_seq=3 ttl=64 time=0.074 ms 
--- 10.0.3.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.053/0.066/0.074/0.013 ms
root@br1:~#

为使更改持久化,按如下方式编辑文件,然后停止并启动容器:

root@br1:~# cat /etc/network/interfaces
auto lo
iface lo inet loopback
auto eth0
 iface eth0 inet static
 address 10.0.3.10
 netmask 255.255.255.0
 gateway 10.0.3.1
root@br1:~# exit
root@ubuntu:~#lxc-stop --name br1 && lxc-start --name br1

让我们使用 brctl 工具查看桥接了解哪些 MAC 地址:

root@ubuntu:~# brctl showmacs lxcbr0
port no   mac addr           is local?   ageing timer
1         fe:7a:39:ce:e8:7c   yes         0.00
root@ubuntu:~#

请注意,这是 veth366R6F 虚拟接口和桥接的 MAC 地址,正如之前在主机上通过 ifconfig 输出所列示的那样。

LXC 网络配置选项概览

让我们查看之前构建的 br1 容器的网络配置:

root@ubuntu:~# cat /var/lib/lxc/br1/config | grep -vi "#" | sed  '/^$/d' | grep -i network
lxc.network.type = veth
lxc.network.link = lxcbr0
lxc.network.flags = up
lxc.network.hwaddr = 00:16:3e:39:cf:e9
root@ubuntu:~#

需要注意的是 lxc.network.hwaddr 选项。它是容器内部的 eth0 的 MAC 地址,是动态生成的。所有配置选项都可以在容器创建之前或之后进行更改。

下表简要描述了容器可用的网络配置选项:

选项 描述
lxc.network.type 要使用的网络虚拟化类型
lxc.network.link 主机上的接口
lxc.network.flags 对接口执行的操作
lxc.network.hwaddr 设置容器接口的 MAC 地址
lxc.network.mtu 设置容器接口的 最大传输单元 (MTU)
lxc.network.name 指定接口名称
lxc.network.ipv4 分配给容器的 IPv4 地址
lxc.network.ipv4.gateway 用作默认网关的 IPv4 地址
lxc.network.ipv6 分配给容器的 IPv6 地址
lxc.network.ipv6.gateway 用作默认网关的 IPv6 地址
lxc.network.script.up 指定在创建和配置网络后执行的脚本
lxc.network.script.down 指定在销毁网络之前执行的脚本

表 5.1

我们将在本章稍后更详细地探讨此表中的大多数选项。

手动操作 Linux 桥接

让我们通过展示一些如何手动操作 Linux 桥接的例子,完成对它的探索。

我们可以通过运行以下命令,首先显示主机上的桥接器:

root@ubuntu:~# brctl show
bridge name    bridge id          STP enabled  interfaces
lxcbr0         8000.000000000000   no
root@ubuntu:~#

要删除桥接器,首先需要将接口关闭:

root@ubuntu:~# ifconfig lxcbr0 down
root@ubuntu:~# brctl delbr lxcbr0
root@ubuntu:~# brctl show
root@ubuntu:~#

接下来,让我们创建一个新的桥接器,并将主机操作系统中暴露的容器接口之一veth366R6F添加到该桥接器中:

root@ubuntu:~# brctl addbr lxcbr0
root@ubuntu:~# brctl show
bridge name   bridge id          STP enabled  interfaces
lxcbr0        8000.000000000000   no 
root@ubuntu:~# brctl addif lxcbr0 veth366R6F
root@ubuntu:~# brctl show
bridge name    bridge id          STP enabled  interfaces
lxcbr0         8000.fe7a39cee87c   no          veth366R6F
root@ubuntu:~#

桥接器已创建,但与之关联的接口需要启动,如下所示错误:

root@ubuntu:~# ifconfig lxcrb0
lxcrb0: error fetching interface information: Device not found 
root@ubuntu:~# ifconfig lxcbr0 up
root@ubuntu:~# ifconfig lxcbr0
lxcbr0    Link encap:EthernetHWaddr fe:7a:39:ce:e8:7c
inet6addr: fe80::fc7a:39ff:fece:e87c/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
 RX packets:0 errors:0 dropped:0 overruns:0 frame:0
 TX packets:6 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0
 RX bytes:0 (0.0 B)  TX bytes:508 (508.0 B)
root@ubuntu:~#

最后,让我们给容器可以用作默认网关的桥接器分配一个 IP 地址:

root@bridge:~# ifconfig lxcbr0 10.0.3.1 netmask 255.255.255.0
root@bridge:~#

Open vSwitch

Open vSwitch (OVS) 是一种软件交换机,允许更高级的网络配置,如策略路由、访问控制列表 (ACLs)、服务质量 (QoS) 管控、流量监控、流管理、VLAN 标签、GRE 隧道等。OVS 可以作为 Linux 桥接器的替代品。在下一章中,我们将使用 OVS 和 GRE 隧道构建一个软件定义网络,将一组 LXC 容器隔离开,但现在让我们演示如何以类似于 Linux 桥接器的方式安装和配置它。

让我们首先在 Ubuntu 上安装这个包:

root@ubuntu:~# apt-get install openvswitch-switch
...
Setting up openvswitch-common (2.0.2-0ubuntu0.14.04.3) ...
Setting up openvswitch-switch (2.0.2-0ubuntu0.14.04.3) ...
openvswitch-switch start/running
... 
root@ubuntu:~# ovs-vsctl --version
ovs-vsctl (Open vSwitch) 2.0.2
Compiled Dec  9 2015 14:08:08
root@ubuntu:~#

OVS 使用一个内核模块,该模块应该被加载:

root@ubuntu:~# lsmod | grep switch
openvswitch            70989  0
vxlan                  37611  1 openvswitch
gre                    13796  1 openvswitch
libcrc32c              12644  1 openvswitch
root@ubuntu:~#

接下来,让我们确认没有配置任何交换机:

root@bridge:~# ovs-vsctl show
4cf17055-87a7-4b01-a8eb-4a551c842342
ovs_version: "2.0.2"
root@bridge:~#

安装包后启动以下进程:

root@bridge:~# pgrep -lfa switch
9424 ovsdb-server /etc/openvswitch/conf.db -vconsole:emer 
-vsyslog:err -vfile:info --remote=punix:/var/run/openvswitch/db.sock 
--private-key=db:Open_vSwitch,SSL,private_key 
--certificate=db:Open_vSwitch,SSL,certificate 
--bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert --no-chdir 
--log-file=/var/log/openvswitch/ovsdb-server.log 
--pidfile=/var/run/openvswitch/ovsdb-server.pid --detach --monitor
9433 ovs-vswitchd: monitoring pid 9434 (healthy)
9434 ovs-vswitchdunix:/var/run/openvswitch/db.sock -vconsole:emer 
-vsyslog:err -vfile:info --mlockall --no-chdir 
--log-file=/var/log/openvswitch/ovs-vswitchd.log 
--pidfile=/var/run/openvswitch/ovs-vswitchd.pid --detach --monitor
root@bridge:~#

ovsdb-server 是一个数据库引擎,使用 JSON RPC,并且可以独立于 OVS 运行。ovsdb-server 接受来自 ovs-vswitchd 守护进程的连接,后者可以创建和修改桥接、端口、网络流等。作为一个快速示例,我们可以列出 ovsdb-server 进程管理的数据库以及它包含的各种表:

root@ubuntu:~# ovsdb-client list-dbs
Open_vSwitch
root@ubuntu:~# ovsdb-client dump | grep table
Bridge table
Controller table
Flow_Sample_Collector_Set table
Flow_Table table
IPFIX table
Interface table
Manager table
Mirror table
NetFlow table
Open_vSwitch table
Port table
QoS table
Queue table
SSL table
sFlow table
root@ubuntu:~#

ovs-vswitchd 进程是主要的 OVS 应用程序,控制主机操作系统上的所有交换机。

现在是时候创建一个交换机并命名为lxcovs0

root@ubuntu:~# ovs-vsctl add-br lxcovs0
root@ubuntu:~# ovs-vsctl show
4cf17055-87a7-4b01-a8eb-4a551c842342
 Bridge "lxcovs0"
 Port "lxcovs0"
 Interface "lxcovs0"
 type: internal
ovs_version: "2.0.2"
root@ubuntu:~#

接下来,分配一个 IP 地址并将容器的虚拟接口连接到它,方法是创建一个端口:

root@ubuntu:~# ifconfig lxcovs0 192.168.0.1 netmask 255.255.255.0
root@ubuntu:~# ovs-vsctl add-port lxcovs0 veth366R6F
root@ubuntu:~# ovs-vsctl show
4cf17055-87a7-4b01-a8eb-4a551c842342
Bridge "lxcovs0"
 Port "lxcovs0"
 Interface "lxcovs0"
 type: internal
 Port "veth366R6F"
 Interface "veth366R6F"
ovs_version: "2.0.2"
root@ubuntu:~# 

veth366R6F 接口属于我们在本章早些时候创建的 br1 容器。为了测试连接性,进入容器,并将 IP 地址和默认网关更改为 OVS 网络的端口:

root@ubuntu:~# lxc-attach --name br1
root@br1:~# ifconfig eth0 192.168.0.10 netmask 255.255.255.0
root@br1:~# route add default gw 192.168.0.1

为避免与 Linux 桥接器发生冲突,请确保销毁桥接器并卸载内核模块:

root@ubuntu:~# ifconfig lxcbr0 down
root@ubuntu:~# brctl delbr lxcbr0
root@ubuntu:~# modprobe -r bridge
root@ubuntu:~#

连接 LXC 到主机网络

连接 LXC 容器到主机网络有三种主要模式:

  • 使用主机操作系统上的物理网络接口,这需要为每个容器分配一个物理接口:

  • 使用一个虚拟接口,通过 NAT 连接到主机软件桥接器

  • 与主机共享相同的网络命名空间,在容器中使用主机网络设备

容器配置文件提供了lxc.network.type选项,如我们之前在表 5.1 中看到的。让我们来看一下该配置选项的可用参数:

参数 描述
none 容器将共享主机的网络命名空间。
empty LXC 只会创建回环接口。
veth 在主机上创建一个虚拟接口,并连接到容器网络命名空间中的接口。
vlan 创建一个 VLAN 接口,该接口与通过 lxc.network.link 指定的设备连接。VLAN ID 通过 lxc.network.vlan.id 选项指定。
macvlan 允许一个物理接口与多个 IP 地址和 MAC 地址关联。
phys 使用 lxc.network.link 选项将主机上的物理接口分配给容器。

让我们更详细地探讨网络配置。

使用 none 网络模式配置 LXC

在此模式下,容器将与主机共享相同的网络命名空间。让我们配置本章开始时创建的 br1 容器以使用该模式:

root@ubuntu:~# vim /var/lib/lxc/br1/config
# Common configuration
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
# Container specific configuration
lxc.rootfs = /var/lib/lxc/br1/rootfs
lxc.rootfs.backend = dir
lxc.utsname = br1
lxc.arch = amd64
# Network configuration
lxc.network.type = none
lxc.network.flags = up

停止并重新启动容器以使新的网络选项生效,并附加到容器:

root@ubuntu:~# lxc-stop --name br1 && lxc-start --name br1
root@ubuntu:~# lxc-attach --name br1
root@br1:~#

让我们检查容器内部的接口配置和网络路由:

root@br1:~# ifconfig
eth0      Link encap:EthernetHWaddr bc:76:4e:10:6a:31
inet addr:10.208.131.214  Bcast:10.208.255.255  Mask:255.255.128.0
inet6addr: fe80::be76:4eff:fe10:6a31/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
 RX packets:35205 errors:0 dropped:0 overruns:0 frame:0
 TX packets:35251 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:1000
 RX bytes:1621243 (1.6 MB)  TX bytes:1486058 (1.4 MB)
lo        Link encap:Local Loopback
inet addr:127.0.0.1  Mask:255.0.0.0
inet6addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING  MTU:65536  Metric:1
 RX packets:9 errors:0 dropped:0 overruns:0 frame:0
 TX packets:9 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0
 RX bytes:668 (668.0 B)  TX bytes:668 (668.0 B)
lxcovs0   Link encap:EthernetHWaddr b2:e1:c6:c8:e6:40
inet addr:192.168.0.1  Bcast:192.168.0.255  Mask:255.255.255.0
inet6addr: fe80::b0e1:c6ff:fec8:e640/64 Scope:Link
 UP BROADCAST RUNNING  MTU:1500  Metric:1
 RX packets:18557 errors:0 dropped:0 overruns:0 frame:0
 TX packets:678 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0
 RX bytes:6190048 (6.1 MB)  TX bytes:5641977 (5.6 MB) 
root@br1:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.208.128.1    0.0.0.0         UG    0      0        0 eth0
10.208.0.0      10.208.128.1    255.240.0.0     UG    0      0        0 eth0
10.208.128.0    0.0.0.0         255.255.128.0   U     0      0        0 eth0
root@br1:~#

不出所料,容器内部的网络接口和路由与主机操作系统上的相同,因为两者共享相同的根网络命名空间。

让我们在附加到容器时检查网络连接:

root@br1:~# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=48 time=11.7 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=48 time=11.8 ms
root@br1:~# exit
root@ubuntu:~#

提示

在此模式下停止容器将导致主机操作系统关闭,因此请小心。

使用空网络模式配置 LXC

empty 模式只会在容器中创建回环接口。配置文件看起来类似于以下输出:

root@ubuntu:~# vim /var/lib/lxc/br1/config
# Common configuration
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
# Container specific configuration
lxc.rootfs = /var/lib/lxc/br1/rootfs
lxc.rootfs.backend = dir
lxc.utsname = br1
lxc.arch = amd64
# Network configuration
lxc.network.type = empty
lxc.network.flags = up

重新启动容器并附加到它,以便我们验证回环接口是唯一存在的设备:

root@ubuntu:~# lxc-stop --name br1 && sleep 5 && lxc-start --name br1
root@ubuntu:~# lxc-attach --name br1
root@br1:~#

让我们检查容器内部的接口配置和网络路由:

root@br1:~# ifconfig
lo        Link encap:Local Loopback
inet addr:127.0.0.1  Mask:255.0.0.0
inet6addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING  MTU:65536  Metric:1
 RX packets:0 errors:0 dropped:0 overruns:0 frame:0
 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
 collisions:0 txqueuelen:0
 RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
root@br1:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
root@br1:~# exit
root@ubuntu:~#

如预期的那样,只有回环接口存在,且未配置任何路由。

使用 veth 模式配置 LXC

NAT 模式是使用 LXC 模板脚本或 libvirt 用户空间工具创建容器时的默认网络模式。在此模式下,容器可以使用主机上应用的 iptables 规则通过 IP 伪装访问外部世界。我们在前几章看到的所有示例都使用了 veth 模式。

在此模式下,LXC 会在主机上创建一个虚拟接口,命名类似 veth366R6F。这是容器虚拟连接的一端,应该连接到软件桥接器。连接的另一端是容器内部的接口,默认名为 eth0

以下示意图有助于可视化网络配置:

使用 veth 模式配置 LXC

LXC 的 veth 模式

这里显示的是容器的配置:

root@ubuntu:~# cat /var/lib/lxc/br1/config
# Common configuration
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
# Container specific configuration
lxc.rootfs = /var/lib/lxc/br1/rootfs
lxc.rootfs.backend = dir
lxc.utsname = br1
lxc.arch = amd64
# Network configuration
lxc.network.type = veth
lxc.network.link = lxcovs0
lxc.network.flags = up
lxc.network.hwaddr = 00:16:3e:39:cf:e9
root@ubuntu:~#

lxc.network.link 选项指定虚拟接口应该连接的主机网络设备,在这种情况下是先前创建的 lxcovs0 OVS 交换机。

请注意,在主机上应用的 iptables 伪装规则:

root@ubuntu:~# iptables -L -n -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  10.0.3.0/24         !10.0.3.0/24
root@ubuntu:~#

提示

如果由于某种原因iptables规则不存在,或者你希望构建一个位于不同子网中的容器,可以使用iptables -t nat -A POSTROUTING -s 10.3.0.0/24 -o eth0 -j MASQUERADE命令添加新规则。

使用物理模式配置 LXC

在此模式下,我们通过lxc.network.link配置选项指定主机的物理接口,该接口将分配给容器的网络命名空间,并使主机无法使用该接口。

以下图表帮助可视化网络配置:

使用物理模式配置 LXC

物理模式下的 LXC

让我们看看配置文件:

root@ubuntu:~# vim /var/lib/lxc/br1/config
# Common configuration
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
# Container specific configuration
lxc.rootfs = /var/lib/lxc/br1/rootfs
lxc.rootfs.backend = dir
lxc.utsname = br1
lxc.arch = amd64
# Network configuration
lxc.network.type = phys
lxc.network.link = eth1
lxc.network.ipv4 = 10.208.131.214/24
lxc.network.hwaddr = bc:76:4e:10:6a:31
lxc.network.flags = up
root@ubuntu:~#

我们指定phys作为网络模式,eth1作为将被移动到容器命名空间中的主机接口,以及eth1的 IP 和 MAC 地址。让我们先查看主机上的所有网络接口:

root@ubuntu:~# ip a s
1: lo: <LOOPBACK,UP,LOWER_UP>mtu 65536 qdiscnoqueue state UNKNOWN group default
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
 inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscpfifo_fast state UP group default qlen 1000
 link/ether bc:76:4e:10:57:b8 brdff:ff:ff:ff:ff:ff
 inet 192.168.167.122/24 brd 192.237.167.255 scope global eth0
 valid_lft forever preferred_lft forever
 inet6 fe80::be76:4eff:fe10:57b8/64 scope link
 valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscpfifo_fast state UP group default qlen 1000
 link/ether bc:76:4e:10:6a:31 brdff:ff:ff:ff:ff:ff
 inet 10.208.131.214/17 brd 10.208.255.255 scope global eth1
 valid_lft forever preferred_lft forever
 inet6 fe80::be76:4eff:fe10:6a31/64 scope link
 valid_lft forever preferred_lft forever
4: lxcbr0: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscnoqueue state UNKNOWN group default
 link/ether 52:26:ad:1d:1a:7f brdff:ff:ff:ff:ff:ff
 inet 10.0.3.1/24 scope global lxcbr0
 valid_lft forever preferred_lft forever
 inet6 fe80::5026:adff:fe1d:1a7f/64 scope link
 valid_lft forever preferred_lft forever
5: ovs-system: <BROADCAST,MULTICAST>mtu 1500 qdiscnoop state DOWN group default
 link/ether c2:05:a1:f4:7f:89 brdff:ff:ff:ff:ff:ff
6: lxcovs0: <BROADCAST,UP,LOWER_UP>mtu 1500 qdiscnoqueue state UNKNOWN group default
 link/ether b2:e1:c6:c8:e6:40 brdff:ff:ff:ff:ff:ff
 inet6 fe80::b0e1:c6ff:fec8:e640/64 scope link
 valid_lft forever preferred_lft forever
root@ubuntu:~# 

注意,eth1接口现在出现在主机上。让我们重启br1容器,并再次列出主机上的接口:

root@ubuntu:~# lxc-stop --name br1 && lxc-start --name br1
root@ubuntu:~# ip a s
1: lo: <LOOPBACK,UP,LOWER_UP>mtu 65536 qdiscnoqueue state UNKNOWN group default
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
 inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscpfifo_fast state UP group default qlen 1000
 link/ether bc:76:4e:10:57:b8 brdff:ff:ff:ff:ff:ff
 inet 192.168.167.122/24 brd 192.237.167.255 scope global eth0
 valid_lft forever preferred_lft forever
 inet6 fe80::be76:4eff:fe10:57b8/64 scope link
 valid_lft forever preferred_lft forever
4: lxcbr0: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscnoqueue state UNKNOWN group default
 link/ether 52:26:ad:1d:1a:7f brdff:ff:ff:ff:ff:ff
 inet 10.0.3.1/24 scope global lxcbr0
 valid_lft forever preferred_lft forever
 inet6 fe80::5026:adff:fe1d:1a7f/64 scope link
 valid_lft forever preferred_lft forever
5: ovs-system: <BROADCAST,MULTICAST>mtu 1500 qdiscnoop state DOWN group default
 link/ether c2:05:a1:f4:7f:89 brdff:ff:ff:ff:ff:ff
6: lxcovs0: <BROADCAST,UP,LOWER_UP>mtu 1500 qdiscnoqueue state UNKNOWN group default
 link/ether b2:e1:c6:c8:e6:40 brdff:ff:ff:ff:ff:ff
 inet6 fe80::b0e1:c6ff:fec8:e640/64 scope link
 valid_lft forever preferred_lft forever
root@ubuntu:~#

eth1接口不再显示在主机上。让我们连接到容器并检查其接口:

root@ubuntu:~# lxc-attach --name br1
root@br1:~# ip a s
1: lo: <LOOPBACK,UP,LOWER_UP>mtu 65536 qdiscnoqueue state UNKNOWN group default
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
 inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscpfifo_fast state UP group default qlen 1000
 link/ether bc:76:4e:10:6a:31 brdff:ff:ff:ff:ff:ff
 inet 10.208.131.214/24 brd 10.208.131.255 scope global eth1
 valid_lft forever preferred_lft forever
 inet6 fe80::be76:4eff:fe10:6a31/64 scope link
 valid_lft forever preferred_lft forever
root@br1:~#

现在,eth1接口成为容器的一部分,具有与主机原始eth1接口相同的 IP 和 MAC 地址,因为我们在容器的配置文件中显式指定了它们。

如果我们需要多个容器使用phys模式,那么我们就需要那么多物理接口,但这并不总是可行的。

使用 vlan 模式配置 LXC

vlan网络模式允许我们在容器的网络命名空间内创建一个虚拟局域网VLAN)标记接口。VLAN 是一个广播域,在数据链路层与网络的其余部分隔离。

lxc.network.link配置选项指定容器应从主机连接到的接口,而lxc.network.vlan.id是容器接口将应用于网络流量的标签。

当主机上运行多个容器,并且需要在容器子集之间隔离流量时,这种模式非常有用,从而实现逻辑上的网络分离。我们在第一章中通过 VLAN 标签演示了类似的概念,Linux 容器简介

要创建一个将标记以太网数据包的容器,配置应类似于以下内容:

root@ubuntu:~# cat /var/lib/lxc/br1/config
# Common configuration
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
# Container specific configuration
lxc.rootfs = /var/lib/lxc/br1/rootfs
lxc.rootfs.backend = dir
lxc.utsname = br1
lxc.arch = amd64
# Network configuration
lxc.network.type = vlan
lxc.network.vlan.id = 100
lxc.network.link = eth1
lxc.network.flags = up
root@ubuntu:~#

我们指定 VLAN ID 100eth1作为容器将配对的接口。重启容器并连接到它:

root@ubuntu:~# lxc-stop --name br1 && lxc-start --name br1
root@ubuntu:~# lxc-attach --name br1
root@br1:~#

让我们检查容器中的eth0接口,确保它已配置为使用 VLAN ID 100标记数据包:

root@br1:~# ip -d link show eth0
10: eth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscnoqueue state UP mode DEFAULT group default
 link/ether bc:76:4e:10:6a:31 brdff:ff:ff:ff:ff:ff promiscuity 0
vlan protocol 802.1Q id 100 <REORDER_HDR>
root@br1:~#

注意eth0接口被命名为eth0@if3。这里,if3意味着容器的接口与主机接口 ID 为3的接口配对,在我们的例子中是eth1。我们可以通过在主机上运行以下命令来查看这一点:

root@br1:~# exit
root@ubuntu:~# ip -d link show eth1
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
 link/ether bc:76:4e:10:6a:31 brd ff:ff:ff:ff:ff:ff promiscuity 0
root@ubuntu:~#

现在,你可以配置容器内的eth0接口,使其属于与主机上eth1接口相同的子网。

配置 LXC 使用 macvlan 模式

macvlan网络模式允许将主机上的单个物理接口与多个具有不同 IP 和 MAC 地址的虚拟接口关联。macvlan可以在以下三种模式下操作:

  • 私有:此模式不允许 LXC 容器之间进行通信

  • 虚拟以太网端口聚合器 (VEPA):此模式不允许 LXC 容器之间进行通信,除非有一个作为反射中继的交换机

  • 桥接:此模式创建一个简单的桥接(与 Linux 桥接或 OVS 不同),允许容器之间相互通信,但将它们与主机隔离开来。

让我们配置br1容器,使用macvlanbridge模式:

root@ubuntu:~# cat /var/lib/lxc/br1/config
# Common configuration
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
# Container specific configuration
lxc.rootfs = /var/lib/lxc/br1/rootfs
lxc.rootfs.backend = dir
lxc.utsname = br1
lxc.arch = amd64
# Network configuration
lxc.network.type = macvlan
lxc.network.macvlan.mode = bridge
lxc.network.link = lxcmacvlan0
lxc.network.flags = up

我们通过lxc.network.link指定的设备是我们接下来要创建的桥接接口:

root@ubuntu:~# iplink add lxcmacvlan0 link eth1 type macvlan mode bridge
root@ubuntu:~# ifconfig lxcmacvlan0 up
root@ubuntu:~# ip -d link show lxcmacvlan0
12: lxcmacvlan0@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscnoqueue state UNKNOWN mode DEFAULT group default
 link/ether ae:de:56:01:2f:b6 brdff:ff:ff:ff:ff:ff promiscuity 0
macvlan  mode bridge
root@ubuntu:~#

macvlan桥接接口启用后,我们可以启动容器并检查其网络接口:

root@ubuntu:~# lxc-stop --name br1 && lxc-start --name br1
root@ubuntu:~# lxc-attach --name br1
root@br1:~# ip -d link show eth0
13: eth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP>mtu 1500 qdiscnoqueue state UNKNOWN mode DEFAULT group default
 link/ether aa:6e:cb:74:0c:fc brdff:ff:ff:ff:ff:ff promiscuity 0
macvlan  mode bridge
root@br1:~#

请注意,容器中eth0接口的if3 ID 与主机上的eth1 ID 相匹配。现在我们可以为eth0接口分配一个与主机上eth1相同子网的 IP 地址,并能够访问同一子网中的其他容器。此模式类似于使用 Linux 桥接或 OVS,但没有它们各自的开销。同时请注意,容器将无法直接与主机通信。

总结

在本章中,你熟悉了 Linux 桥接,并学会了如何将 LXC 容器连接到它。我们还了解了 Open vSwitch 作为 Linux 桥接的替代方案。接着我们探索了 LXC 提供的各种网络配置选项,并查看了几个示例。

本章结束时,我们演示了如何使用 NAT、VLAN、直接连接以及更先进的节点(如 MAC VLAN)将 LXC 连接到主机网络和其他容器。

在下一章中,我们将把到目前为止获得的所有知识付诸实践,构建一个使用 LXC 和 HAProxy 的高可用、可扩展的应用程序部署。

第六章:使用 LXC 进行集群化和横向扩展

在 LXC 容器中运行应用程序提供了一种方便的资源分配和限制方式,正如我们在前面的章节中所看到的那样。LXC 也非常适合创建应用程序集群,例如,一个可以横向或纵向扩展的 Web 服务器集群。

横向扩展是向集群或执行共同任务的资源组中添加更多计算能力的一种方式。通常,这是通过添加更多的服务器、虚拟机,或在 LXC 的情况下,添加更多容器来运行应用程序来实现的。相反,纵向扩展是通过为物理服务器、虚拟机或容器添加更多硬件或虚拟资源(如 CPU 和内存)来实现的。

在本章中,我们将利用你迄今为止所学到的所有知识,做以下事情:

  • 创建一个简单的 Apache 集群,在最小根文件系统上运行 LXC,并使用 libvirt

  • 使用 Open vSwitch 和 GRE 隧道网格实现带有 Apache 和 HAProxy 的多节点 Web 集群

  • 演示如何通过重用现有 LXC 实例的文件系统来添加更多容器

使用 LXC 扩展应用程序

LXC 非常适合替代虚拟机,因为它可以容纳一个完整的 Linux 发行版根文件系统,在这种情况下,唯一与宿主操作系统共享的组件是内核。应用程序可以安装在容器的根文件系统中,使宿主机或其他容器无法共享它们。如果我们想运行相同应用程序及其依赖项的不同版本,或完全不同的 Linux 发行版,这种隔离是非常有用的。

另一方面,libvirt LXC 允许从一个二进制文件执行单个进程或一组进程,这个二进制文件由所有容器共享,并来自宿主操作系统。在这种情况下,容器共享宿主文件系统,并且只抽象出某些目录。这对于某些应用场景非常有帮助,比如当应用程序可能不需要自己的专用文件系统时,例如,如果容器中的 Linux 发行版与宿主操作系统相同。扩展此类应用程序的关键是确保服务已安装在宿主机上,并且必要的配置文件在容器的最小根文件系统中存在。然后,我们可以复制容器的配置文件和最小根文件系统,并在没有太多更改的情况下启动它。

在接下来的两节中,我们将探讨这两种情况。我们将首先通过 libvirt 构建最小根文件系统的 Apache 容器,并使用 HAProxy 进行负载均衡,然后转向使用 LXC 构建 Apache 集群,并通过 Open vSwitch 与 GRE 隧道网格实现专用文件系统和网络隔离。

在最小根文件系统中使用 libvirt LXC 扩展 Apache

在这一部分,我们将展示如何使用 libvirt LXC 和为每个容器创建最小的根文件系统,在同一主机上运行多个 Apache 服务器。Apache 的二进制文件和库将被容器共享。尽管这种方法可能不适用于 Apache,而更适合简单的单线程进程,但它将帮助我们以更实际的方式展示这一概念。

在这个例子中,我们将使用 Ubuntu,但相同的指令适用于 CentOS,正如我们在第二章,在 Linux 系统上安装和运行 LXC中所展示的那样。以下是该示例的步骤:

  1. 让我们从更新操作系统开始,确保它运行的是最新的内核:

    root@ubuntu:~# apt-get update && apt-get upgrade --yes && reboot
    root@lxc:~# lsb_release -a 2>/dev/null
    Distributor ID:   Ubuntu
    Description:      Ubuntu 16.04.1 LTS
    Release:    16.04
    Codename:   xenial
    root@ubuntu:~# 
    root@ubuntu:~# uname -r
    4.4.0-38-generic
    root@ubuntu:~#
    
    
  2. 截至本写作时,Ubuntu Xenial 上的最新 libvirt 包如下所示:

    root@ubuntu:~# apt-cache policy libvirt-bin
    libvirt-bin:
     Installed: 
     Candidate: 1.3.1-1ubuntu10.5
     Version table:
     *** 1.3.1-1ubuntu10.5 500
     500 http://rackspace.clouds.archive.ubuntu.com/ubuntu  
                xenial-updates/main amd64 Packages
     100 /var/lib/dpkg/status
     1.3.1-1ubuntu10 500
     500 http://rackspace.clouds.archive.ubuntu.com/ubuntu 
                xenial/main amd64 Packages
    root@ubuntu:~#
    
    
  3. 接下来,安装 libvirt 包:

    root@ubuntu:~# apt-get install libvirt-bin virtinst
    root@ubuntu:~# dpkg --list | grep libvirt
    ii  libvirt-bin      1.3.1-1ubuntu10.5       amd64         
          programs for the libvirt library
    ii  libvirt0:amd64     1.3.1-1ubuntu10.5     amd64        
          library for interfacing with different virtualization systems
    ii  python-libvirt      1.3.1-1ubuntu1       amd64        
          libvirt Python bindings
    root@ubuntu:~#
    
    
  4. 方便的是,libvirt 为我们创建了桥接:

     root@ubuntu:~# brctl show
    bridge name      bridge id          STP enabled  interfaces
    virbr0           8000.5254003d3c43  yes          virbr0-nic
    root@ubuntu:~#
    
    
  5. 我们将使用 default libvirt 网络;让我们确保它存在:

    root@ubuntu:~# export LIBVIRT_DEFAULT_URI=lxc:///
    root@ubuntu:~# virsh net-list --all
     Name              State      Autostart     Persistent
    ----------------------------------------------------------
     default           active     yes           yes
    root@ubuntu:~#
    
    
  6. 要检查 libvirt 使用的 default 网络和网关,请运行以下命令:

    root@ubuntu:~# virsh net-dumpxml default
    <network>
    <name>default</name>
    <uuid>6585ac5b-3d81-4071-bb61-3aa22007834e</uuid>
    <forward mode='nat'>
    <nat>
    <port start='1024' end='65535'/>
    </nat>
    </forward>
    <bridge name='virbr0' stp='on' delay='0'/>
    <mac address='52:54:00:3d:3c:43'/>
    <ip address='192.168.122.1' netmask='255.255.255.0'>
    <dhcp>
    <range start='192.168.122.2' end='192.168.122.254'/>
    </dhcp>
    </ip>
    </network>
    root@ubuntu:~#
    
    
  7. libvirt 工具包还启动了 dnsmasq,如果我们配置它们使用 DHCP,它将为 LXC 容器分配网络设置:

    root@ubuntu:~# ps axfww
    5310 ?        Ssl    0:00 /usr/sbin/libvirtd
     5758 ?        S      0:00 /usr/sbin/dnsmasq 
          --conf-file=/var/lib/libvirt/dnsmasq/default.conf 
          --leasefile-ro 
          --dhcp-script=/usr/lib/libvirt/libvirt_leaseshelper
     5759 ?        S      0:00  \_ /usr/sbin/dnsmasq 
          --conf-file=/var/lib/libvirt/dnsmasq/default.conf 
          --leasefile-ro 
          --dhcp-script=/usr/lib/libvir/libvirt_leaseshelper
    
    
  8. 我们将使用默认的 dnsmasq 配置,但让我们确保 DHCP 范围与 libvirt-net 在前面的输出中所知道的范围匹配:

    root@ubuntu:~# cat /var/lib/libvirt/dnsmasq/default.conf 
          | grep -vi "#"
    strict-order
    user=libvirt-dnsmasq
    pid-file=/var/run/libvirt/network/default.pid
    except-interface=lo
    bind-dynamic
    interface=virbr0
    dhcp-range=192.168.122.2,192.168.122.254
    dhcp-no-override
    dhcp-lease-max=253
    dhcp-hostsfile=/var/lib/libvirt/dnsmasq/default.hostsfile
    addn-hosts=/var/lib/libvirt/dnsmasq/default.addnhosts
    root@ubuntu:~#
    
    

为容器创建最小的根文件系统

对于这个例子,我们不会使用提供的模板,也不会使用 debootstrap 命令来构建完整的文件系统,而是创建一个最小的目录结构来托管 Apache 配置文件和容器。其他部分将与主机操作系统位于相同的 mount 命名空间中,除了几个我们将绑定到容器的目录。

按照以下步骤为容器创建最小的根文件系统:

  1. 首先,创建目录;复制必要的文件并在主机上安装 Apache:

    root@ubuntu:~# cd /opt/
    root@ubuntu:/opt# mkdir -p containers/http1/etc
    root@ubuntu:/opt# mkdir -p containers/http1/var/www/html
    root@ubuntu:/opt# apt-get install --yes apache2
    ...
    root@ubuntu:/opt# cp -r /etc/apache2/ /opt/containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/passwd  /opt//containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/shadow  /opt//containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/group  /opt//containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/mime.types /opt//containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/init.d/ /opt//containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/resolv.conf /opt/containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/fstab /opt/containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/apache2/ /opt/containers/http1/etc/
    root@ubuntu:/opt# cp -r /etc/network/ /opt/containers/http1/etc/
    
    
  2. 接下来,创建 Apache 的 index.html 页面,并配置 Web 服务器使用自己独特的 PID 文件,这将使我们以后能够启动多个 Apache 进程:

    root@ubuntu:/opt# echo "Apache in LXC http1" > 
          /opt/containers/http1/var/www/html/index.html
    root@ubuntu:/opt# cd containers/
    root@ubuntu:/opt/containers# sed -i 
          's/\${APACHE_PID_FILE}/\/var\/run\/apache2\/apache2_http1.pid/g'    
          http1/etc/apache2/apache2.conf
    
    
  3. 配置网络接口文件以使用 DHCP,这样我们就可以利用之前 libvirtd 启动的 dnsmasq 服务器:

    root@ubuntu:/opt/containers# cat http1/etc/network/interfaces
    auto lo
    iface lo inet loopback
    auto eth0
    iface eth0 inet dhcp
    root@ubuntu:/opt/containers#
    
    

定义 Apache libvirt 容器

为了使用 libvirt 构建 LXC 容器,我们需要创建一个包含容器属性的配置文件。根据前面步骤创建的目录结构以及主机上安装的 Apache,我们可以定义容器的配置文件。当我们在第三章,使用原生和 libvirt 工具的命令行操作中谈到 libvirt 时,已经看到过类似的配置。要查看配置,请运行以下命令:

root@ubuntu:/opt/containers# cat http1.xml
<domain type='lxc'>
<name>http1</name>
<memory>102400</memory>
<os>
<type>exe</type>
<init>/opt/containers/startup.sh</init>
</os>
<vcpu>1</vcpu>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<devices>
<emulator>/usr/lib/libvirt/libvirt_lxc</emulator>
<filesystem type='mount'>
<source dir='/opt/containers/http1/etc/apache2/'/>
<target dir='/etc/apache2'/>
</filesystem>
<filesystem type='mount'>
<source dir='/opt/containers/http1/var/www/html/'/>
<target dir='/var/www/html'/>
</filesystem>
<filesystem type='mount'>
<source dir='/opt/containers/http1/etc/'/>
<target dir='/etc'/>
</filesystem>
<interface type='network'>
<source network='default'/>
</interface>
<console type='pty'/>
</devices>
</domain>
root@ubuntu:/opt/containers#

前面配置中的新变化是,我们不再指定/sbin/init作为初始化系统的类型,而是配置 libvirt 使用自定义脚本—startup.sh。该脚本可以是任何我们喜欢的脚本;在此情况下,它将启动容器中的网络配置,配置 shell,执行dhclientdnsmasq获取网络设置,然后启动 Apache 和 bash:

root@ubuntu:/opt/containers# cat startup.sh
#!/bin/bash
export PATH=$PATH:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin
export PS1="[\u@\h \W]\\$"
echo "Starting Networking" >> /var/log/messages
/etc/init.d/networking start
/sbin/dhclient eth0
echo "Starting httpd" >> /var/log/messages
/etc/init.d/apache2 start
/bin/bash
root@ubuntu:/opt/containers#

接下来,设置脚本为可执行:

root@ubuntu:/opt/containers# chmod u+x startup.sh
root@ubuntu:/opt/containers#

容器的根目录应该如下所示:

root@ubuntu:/opt/containers# ls -la http1
total 16
drwxr-xr-x 4 root root 4096 Oct 31 17:25 .
drwxr-xr-x 3 root root 4096 Oct 31 17:32 ..
drwxr-xr-x 4 root root 4096 Oct 31 17:27 etc
drwxr-xr-x 3 root root 4096 Oct 31 17:25 var
root@ubuntu:/opt/containers#

只有两个目录!我们现在准备定义容器:

root@ubuntu:/opt/containers# virsh define http1.xml
Domain http1 defined from http1.xml
root@ubuntu:/opt/containers#

启动 Apache libvirt 容器

所有必要的组件就位后,让我们启动容器并确认它正在运行:

root@ubuntu:/opt/containers# virsh start http1
Domain http1 started
root@ubuntu:/opt/containers# virsh list --all
Id    Name                           State
----------------------------------------------------
19032 http1                          running
root@ubuntu:/opt/containers# ps axfww
...
10592 ?      S     0:00 /usr/lib/libvirt/libvirt_lxc --name http1 
--console 23 --security=apparmor --handshake 26 --veth vnet1
10594 ?      S     0:00  \_ /bin/bash /opt/containers/startup.sh
10668 ?      Ss    0:00      \_ /sbin/dhclient eth0
10694 ?      Ss    0:00      \_ /usr/sbin/apache2 -k start
10698 ?      Sl    0:00      |   \_ /usr/sbin/apache2 -k start
10699 ?      Sl    0:00      |   \_ /usr/sbin/apache2 -k start
10697 ?      S     0:00      \_ /bin/bash
root@ubuntu:/opt/containers#

在列出宿主上的进程时,注意容器是如何从libvirt_lxc脚本启动的,它是startup.sh脚本的父进程,后者又启动了 Apache。

连接到容器并确保它能够从dnsmasq获取 IP 地址和默认网关:

root@ubuntu:/opt/containers# virsh console http1
[root@ubuntu /]#ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
 inet6 ::1/128 scope host
 valid_lft forever preferred_lft forever
14: eth0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
 link/ether 52:54:00:92:cf:12 brd ff:ff:ff:ff:ff:ff link-netnsid 0
 inet 192.168.122.216/24 brd 192.168.122.255 scope global eth0
 valid_lft forever preferred_lft forever
 inet6 fe80::5054:ff:fe92:cf12/64 scope link
 valid_lft forever preferred_lft forever
[root@ubuntu /]#ip r s
default via 192.168.122.1 dev eth0
192.168.122.0/24 dev eth0  proto kernel  scope link  src 192.168.122.10
[root@ubuntu /]# Ctrl + ]

让我们从宿主操作系统连接到 Apache:

root@ubuntu:/opt/containers# curl 192.168.122.216
Apache in LXC http1
root@ubuntu:/opt/containers#

注意

192.168.122.70dnsmasq分配给容器的 IP 地址;你可能需要将其替换为系统上的正确地址。

使用 libvirt LXC 和 HAProxy 扩展 Apache

扩展 Apache 与 libvirt LXC 和 HAProxy 时,请按照以下步骤进行:

  1. 只运行一个 Apache 容器时,让我们通过复制http1容器的简单目录结构和 libvirt 配置来快速创建第二个容器:

    root@ubuntu:/opt/containers# cp -r http1 http2
    root@ubuntu:/opt/containers# cp http1.xml http2.xml
    root@ubuntu:/opt/containers#
    
    
  2. 我们需要更改的只有容器的名称、Apache 的 PID 文件和其索引文件:

    root@ubuntu:/opt/containers# sed -i 's/http1/http2/g' 
          http2.xml 
    root@ubuntu:/opt/containers# sed -i 's/http1/http2/g' 
          http2/etc/apache2/apache2.conf 
    root@ubuntu:/opt/containers# echo "Apache in LXC http2" >    
          /opt/containers/http2/var/www/html/index.html
    
    
  3. 定义新容器并检查包含根文件系统和两个容器配置文件的目录结构:

    root@ubuntu:/opt/containers# virsh define http2.xml
    Domain http2 defined from http2.xml 
    root@ubuntu:/opt/containers# ls -la
    total 28
    drwxr-xr-x 4 root root 4096 Oct 31 19:57 .
    drwxr-xr-x 3 root root 4096 Oct 31 17:32 ..
    drwxr-xr-x 4 root root 4096 Oct 31 17:25 http1
    -rw-r--r-- 1 root root  868 Oct 31 17:31 http1.xml
    drwxr-xr-x 4 root root 4096 Oct 31 19:56 http2
    -rw-r--r-- 1 root root  868 Oct 31 19:57 http2.xml
    -rwxr--r-- 1 root root  418 Oct 31 19:44 startup.sh
    root@ubuntu:/opt/containers#
    
    
  4. 让我们启动新容器并确保两个实例都在运行:

    root@ubuntu:/opt/containers# virsh start http2
    Domain http2 started 
    root@ubuntu:/opt/containers# virsh list --all
    Id    Name                           State
    ----------------------------------------------------
     10592 http1                          running
     11726 http2                          running
    root@ubuntu:~#
    
    
  5. 要获取有关 Apache 容器的更多信息,请运行以下命令:

    root@ubuntu:~# virsh dominfo http1
    Id:             15720
    Name:           http1
    UUID:           defd17d7-f220-4dca-9be9-bdf40b4d9164
    OS Type:        exe
    State:          running
    CPU(s):         1
    CPU time:       35.8s
    Max memory:     102400 KiB
    Used memory:    8312 KiB
    Persistent:     yes
    Autostart:      disable
    Managed save:   no
    Security model: apparmor
    Security DOI:   0
    Security label: libvirt-defd17d7-f220-4dca-9be9-bdf40b4d9164
          (enforcing) 
    root@ubuntu:~# virsh dominfo http2
    Id:             16126
    Name:           http2
    UUID:           a62f9e9d-4de3-415d-8f2d-358a1c8bc0bd
    OS Type:        exe
    State:          running
    CPU(s):         1
    CPU time:       36.5s
    Max memory:     102400 KiB
    Used memory:    8300 KiB
    Persistent:     yes
    Autostart:      disable
    Managed save:   no
    Security model: apparmor
    Security DOI:   0
    Security label: libvirt-a62f9e9d-4de3-415d-8f2d-358a1c8bc0bd 
          (enforcing)
    root@ubuntu:~#
    
    

    注意

    dominfo输出提供了关于容器内存和 CPU 利用率的有用信息,我们可以用来进行监控、警报和自动扩展,正如我们在第七章,容器化世界中的监控与备份中所看到的那样。请注意,OS 类型设置为exe,因为容器的初始化系统是一个脚本。

  6. 让我们测试新容器中 Apache 的连通性;根据需要替换实例的 IP:

    root@ubuntu:/opt/containers# curl 192.168.122.242
    Apache in LXC http2
    root@ubuntu:/opt/containers#
    
    
  7. 由于所有容器都连接到相同的桥接网络,宿主操作系统可以访问这两个 Apache 进程。为了从宿主操作系统外部访问它们,我们可以在服务器上安装 HAProxy,并将容器的 IP 地址作为其后端服务器:

    root@ubuntu:~# echo "nameserver 8.8.8.8" > /etc/resolv.conf
    root@ubuntu:~# apt-get install --yes haproxy
    root@ubuntu:~# cat /etc/haproxy/haproxy.cfg
    global
     log /dev/log  local0
     log /dev/log  local1 notice
     chroot /var/lib/haproxy
     stats socket /run/haproxy/admin.sock mode 660 level admin
     stats timeout 30s
     user haproxy
     group haproxy
     daemon
     ca-base /etc/ssl/certs
     crt-base /etc/ssl/private
     ssl-default-bind-ciphers 
                ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128 
                :DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES
                :RSA+3DES:!aNULL:!MD5:!DSS
     ssl-default-bind-options no-sslv3
    defaults
     log  global
     mode  http
     option  httplog
     option  dontlognull
    timeout connect 5000
     timeout client  50000
     timeout server  50000
    frontend http
     bind :80
     reqadd X-Forwarded-Proto:\ http
     default_backend http_nodes
    backend http_nodes
     mode http
     balance roundrobin
     option httpclose
     option forwardfor
     option redispatch
     option httpchk GET /
     cookie JSESSIONID prefix
     server http1 192.168.122.216:80 check inter 5000
     server http1 192.168.122.242:80 check inter 5000
    root@ubuntu:~# 
    
    

    在 HAProxy 配置的backend部分的服务器行中指定的 IP 地址是 libvirt LXC 容器的地址。根据需要更新文件。

    在配置的 frontend 部分,我们告诉 HAProxy 监听端口 80 并绑定到所有接口。在 backend 部分,我们指定了两个 LXC 容器的 IP 地址。您可能需要将容器的 IP 地址替换为 dnsmasq 在您的系统上提供的那些地址。

  8. 重新启动 HAProxy,因为在 Ubuntu 上,包安装后它会自动启动:

    root@ubuntu:~# service haproxy restart
    root@ubuntu:~#
    
    
  9. 然后,确保 HAProxy 正在运行,并且在主机上监听端口80

    root@ubuntu:~# pgrep -lfa haproxy
    1957 /usr/sbin/haproxy-systemd-wrapper -f /etc/haproxy
          /haproxy.cfg -p /run/haproxy.pid
    1958 /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -p   
          /run/haproxy.pid -Ds
    1960 /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -p    
          /run/haproxy.pid -Ds
    root@ubuntu:~# 
    root@ubuntu:~# netstat -antup | grep -i listen | grep -w 80
    tcp      0     0 0.0.0.0:80      0.0.0.0:*                  
          LISTEN      1960/haproxy
    tcp6       0      0 :::80        :::*                    
          LISTEN      9693/apache2
    root@ubuntu:~# 
    
    
  10. 我们配置了 HAProxy 使用轮询法来选择后端节点;现在,让我们连接几次并确认每次都连接到了每个 LXC 容器中的 Apache:

    root@ubuntu:~# curl localhost
    Apache in LXC http1 
    root@ubuntu:~# curl localhost
    Apache in LXC http2
    root@ubuntu:~#
    
    
  11. 最后,我们可以停止其中一个容器,并确保 HAProxy 将其从轮询中移除:

    root@ubuntu:~# virsh destroy http2
    Domain http2 destroyed
    root@ubuntu:~# virsh list --all
     Id    Name                           State
    ----------------------------------------------------
     15720 http1                          running
     -     http2                          shut off
    root@ubuntu:~# curl localhost
    Apache in LXC http1
    root@ubuntu:~# curl localhost
    Apache in LXC http1
    root@ubuntu:~#
    
    

Apache 可能不是在同一主机上运行多个容器的最佳应用程序。不过,它有助于展示如何在最小的 LXC 容器中扩展应用程序,如何在代理后使用 libvirt LXC,或如何构建一个多租户环境。使用来自所有容器的共享二进制文件的附加好处是,升级这些文件时无需对每个 LXC 实例进行更改,而只需对主机操作系统进行更改,这些更改将在该服务器的所有容器中可见。前述设置可能看起来很简单,但它提供了一种强大的方法来扩展轻量级 LXC 容器中的服务,而不会占用太多磁盘空间。

通过完整的 LXC 根文件系统和 OVS GRE 隧道扩展 Apache

在某些场景下,在同一主机上运行多个容器,并为每个容器提供最小文件系统是很好的,但接下来我们将重点介绍一个更复杂的多服务器部署示例。下图展示了我们将在本节中构建的部署结构:

通过完整的 LXC 根文件系统和 OVS GRE 隧道扩展 Apache

使用 LXC 和 GRE 隧道的多服务器 LXC 部署

我们将使用三台服务器——lxc-lblxc-node-01lxc-node-02。每台服务器都会安装 LXC 和 OVS。lxc-lb 主机将托管一个运行 HAProxy 的容器,稍后还会在服务器本身上运行 HAProxy。lxc-node-01lxc-node-02 服务器将有容器运行 Apache。所有 LXC 实例将在一个专用的私有网络上通过连接到 OVS 的 GRE 隧道网状结构进行通信。OVS GRE 网状结构将在容器与主机之间,以及潜在的其他容器和它们的网络之间创建网络隔离。所有容器将从在 lxc-lb 主机上运行的 dnsmasq 获取网络配置。

对于这个部署,我们将使用来自 AWS 的三台 EC2 实例,运行最新的 Ubuntu Xenial 版本。

配置负载均衡主机

配置负载均衡主机,请按照以下步骤操作:

  1. 让我们从 lxc-lb 服务器开始。检查可用的 LXC 版本并安装最新版本:

    root@lxc-lb:~# apt-get update && apt-get upgrade --yes && reboot
    root@lxc-lb:~# apt-cache policy lxc
    lxc:
     Installed: (none)
     Candidate: 2.0.5-0ubuntu1~ubuntu16.04.2
     Version table:
     2.0.5-0ubuntu1~ubuntu16.04.2 500
     500 http://us-east-1.ec2.archive.ubuntu.com/ubuntu 
                    xenial-updates/main amd64 Packages
     2.0.0-0ubuntu2 500
     500 http://us-east-1.ec2.archive.ubuntu.com/ubuntu   
                    xenial/main amd64 Packages
    root@lxc-lb:~# 
    root@lxc-lb:~# apt-get install --yes lxc
    ... 
    root@lxc-lb:~# dpkg --list | grep lxc
    ii          liblxc1      2.0.5-0ubuntu1~ubuntu16.04.2    
          amd64       Linux Containers userspace tools (library)
    ii          lxc          2.0.5-0ubuntu1~ubuntu16.04.2    
          all         Transitional package for lxc1
    ii          lxc-common   2.0.5-0ubuntu1~ubuntu16.04.2    
          amd64       Linux Containers userspace tools (common tools)
    ii          lxc-templates  2.0.5-0ubuntu1~ubuntu16.04.2    
          amd64       Linux Containers userspace tools (templates)
    ii          lxc1           2.0.5-0ubuntu1~ubuntu16.04.2    
          amd64       Linux Containers userspace tools
    ii          lxcfs          2.0.4-0ubuntu1~ubuntu16.04.1    
          amd64       FUSE based filesystem for LXC
    ii          python3-lxc    2.0.5-0ubuntu1~ubuntu16.04.2    
    
          amd64         
          Linux Containers userspace tools (Python 3.x bindings)
    root@lxc-lb:~# 
    root@lxc-lb:~# lxc-create --version
    2.0.5
    root@lxc-lb:~#
    
    
  2. 安装 LXC 包和模板后,我们也得到了 Linux 桥接,但我们不会使用它:

    root@lxc-lb:~# brctl show
    bridge name       bridge id           STP enabled interfaces
    lxcbr0            8000.000000000000   no
    root@lxc-lb:~#
    
    
  3. 接下来,安装 OVS 并创建一个名为 lxcovsbr0 的新桥接:

    root@lxc-lb:~# apt-get install --yes openvswitch-switch
    ...
    root@lxc-lb:~# ovs-vsctl add-br lxcovsbr0
    root@lxc-lb:~# ovs-vsctl show
    482cf359-a59e-4482-8a71-02b0884d016d
     Bridge "lxcovsbr0"
     Port "lxcovsbr0"
     Interface "lxcovsbr0"
     type: internal
     ovs_version: "2.5.0"
    root@lxc-lb:~#
    
    
  4. 默认的 LXC 网络使用10.0.3.0/24子网;我们将其替换为192.168.0.0/24。这样可以在已有 LXC 网络的情况下启动一个新的网络并隔离某些容器集合,同时也有助于展示这一概念:

    root@lxc-lb:~# cat /etc/default/lxc-net | grep -vi "#"
    USE_LXC_BRIDGE="true"
    LXC_BRIDGE="lxcbr0"
    LXC_ADDR="10.0.3.1"
    LXC_NETMASK="255.255.255.0"
    LXC_NETWORK="10.0.3.0/24"
    LXC_DHCP_RANGE="10.0.3.2,10.0.3.254"
    LXC_DHCP_MAX="253" 
    root@lxc-lb:~# cat /etc/lxc/default.conf
    lxc.network.type = veth
    lxc.network.link = lxcbr0
    lxc.network.flags = up
    lxc.network.hwaddr = 00:16:3e:xx:xx:xx
    root@lxc-lb:~#
    
    
  5. 将默认的 Linux 桥接名称替换为我们刚刚创建的 OVS 桥接,并更改网络:

    root@lxc-lb:~# sed -i 's/lxcbr0/lxcovsbr0/g' /etc/default/lxc-net
    root@lxc-lb:~# sed -i 's/10.0.3/192.168.0/g' /etc/default/lxc-net
    root@lxc-lb:~# sed -i 's/lxcbr0/lxcovsbr0/g' /etc/lxc/default.conf
    
    
  6. dnsmasq服务配置为10.0.3.0/24网络,但重启后,它应该会监听我们之前指定的新的子网。让我们重启服务器以确保更改会生效:

    root@lxc-lb:~# pgrep -lfa dnsmasq
    10654 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@lxc-lb:~#
    root@lxc-lb:~# reboot
    
    
  7. 如预期所示,dnsmasq现在将提供来自192.168.0.0/24子网的 IP 地址:

    root@lxc-lb:~# pgrep -lfa dnsmasq
    1354 dnsmasq -u lxc-dnsmasq --strict-order --bind-interfaces 
          --pid-file=/run/lxc/dnsmasq.pid --listen-address 192.168.0.1 
          --dhcp-range 192.168.0.2,192.168.0.254 --dhcp-lease-max=253 
          --dhcp-no-override --except-interface=lo --interface=lxcovsbr0 
          --dhcp-leasefile=/var/lib/misc/dnsmasq.lxcovsbr0.leases 
          --dhcp-authoritative
    root@lxc-lb:~#
    
    
  8. 检查 OVS 桥接;它应该已经启动并配置了 IP 地址:

    root@lxc-lb:~# ip a s lxcovsbr0
    4: lxcovsbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc 
          noqueue state UNKNOWN group default qlen 1
     link/ether ee:b0:a2:42:22:4e brd ff:ff:ff:ff:ff:ff
     inet 192.168.0.1/24 scope global lxcovsbr0
     valid_lft forever preferred_lft forever
     inet6 fe80::ecb0:a2ff:fe42:224e/64 scope link
     valid_lft forever preferred_lft forever
    root@lxc-lb:~#
    
    

创建负载均衡器容器

创建负载均衡器容器,请按照以下步骤进行:

  1. 我们将使用 Ubuntu 模板来创建 HAProxy 容器的根文件系统:

    root@lxc-lb:~# lxc-create --name haproxy --template ubuntu
    root@lxc-lb:~# lxc-start --name haproxy
    root@lxc-lb:~#
    
    
  2. OVS 桥接现在应该已经将容器接口添加为端口——在本例中为vethUY97FY

    root@lxc-lb:~# ovs-vsctl show
    482cf359-a59e-4482-8a71-02b0884d016d
     Bridge "lxcovsbr0"
     Port "lxcovsbr0"
     Interface "lxcovsbr0"
     type: internal
     Port "vethUY97FY"
     Interface "vethUY97FY"
     ovs_version: "2.5.0"
    root@lxc-lb:~# 
    root@lxc-lb:~# ip a s vethUY97FY
    6: vethUY97FY@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc 
          noqueue master ovs-system state UP group default qlen 1000
     link/ether fe:d1:f3:ca:9e:83 brd ff:ff:ff:ff:ff:ff link-netnsid 0
     inet6 fe80::fcd1:f3ff:feca:9e83/64 scope link
     valid_lft forever preferred_lft forever
    root@lxc-lb:~#
    
    
  3. 连接到新容器,确保它已从同一主机上运行的 DHCP 服务器获得 IP 地址:

    root@lxc-lb:~# lxc-attach --name haproxy
    root@haproxy:~# ifconfig
    eth0      Link encap:Ethernet  HWaddr 00:16:3e:76:92:0a
     inet addr:192.168.0.26  Bcast:192.168.0.255  
                    Mask:255.255.255.0
     inet6 addr: fe80::216:3eff:fe76:920a/64 Scope:Link
     UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
     RX packets:16 errors:0 dropped:0 overruns:0 frame:0
     TX packets:12 errors:0 dropped:0 overruns:0 carrier:0
     collisions:0 txqueuelen:1000
     RX bytes:1905 (1.9 KB)  TX bytes:1716 (1.7 KB)
    lo        Link encap:Local Loopback
     inet addr:127.0.0.1  Mask:255.0.0.0
     inet6 addr: ::1/128 Scope:Host
     UP LOOPBACK RUNNING  MTU:65536  Metric:1
     RX packets:0 errors:0 dropped:0 overruns:0 frame:0
     TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
     collisions:0 txqueuelen:1
     RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
    root@haproxy:~# route -n
    Kernel IP routing table
    Destination   Gateway       Genmask       Flags Metric Ref  Use Iface
    0.0.0.0       192.168.0.1   0.0.0.0       UG    0      0     0  eth0
    192.168.0.0   0.0.0.0       255.255.255.0 U     0      0     0  eth0
    root@haproxy:~#
    
    
  4. 容器应该能够连接到主机和互联网。我们在继续之前先测试一下:

    root@haproxy:~# ping -c 3 192.168.0.1
    PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
    64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.218 ms
    64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=0.045 ms
    64 bytes from 192.168.0.1: icmp_seq=3 ttl=64 time=0.046 ms
    --- 192.168.0.1 ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 2000ms
    rtt min/avg/max/mdev = 0.045/0.103/0.218/0.081 ms
     root@haproxy:~# ping google.com -c 3
    PING google.com (216.58.217.110) 56(84) bytes of data.
    64 bytes from iad23s42-in-f14.1e100.net (216.58.217.110): icmp_seq=1 
          ttl=48 time=2.55 ms
    64 bytes from iad23s42-in-f14.1e100.net (216.58.217.110): icmp_seq=2 
          ttl=48 time=2.11 ms
    64 bytes from iad23s42-in-f14.1e100.net (216.58.217.110): icmp_seq=3 
          ttl=48 time=2.39 ms
    --- google.com ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 2002ms
    rtt min/avg/max/mdev = 2.113/2.354/2.555/0.191 ms
     root@haproxy:~# exit
    exit
    root@lxc-lb:~#
    
    

如果连接不可用,请确保dnsmasq服务器正确分配了 IP 地址,并且容器连接到了 OVS 桥接,且桥接接口本身已经启动并配置了 IP 地址。

构建 GRE 隧道

通用路由封装GRE)是一种隧道协议,允许通过互联网协议IP)建立虚拟点对点网络。我们可以使用它在三个主机上的 OVS 交换机之间创建一个网络网状结构,从而将 LXC 容器连接到一个隔离的网络。每个服务器(或者在这个例子中的 EC2 实例)将彼此连接。OVS 提供了一种方便的方式来建立 GRE 隧道。

仍然在负载均衡器主机上,创建两个到其他两台服务器的 GRE 隧道,并根据需要替换 IP 地址:

root@lxc-lb:~# ovs-vsctl add-port lxcovsbr0 gre0 -- set interface gre0 type=gre options:remote_ip=10.1.34.124
root@lxc-lb:~# ovs-vsctl add-port lxcovsbr0 gre1 -- set interface gre1 type=gre options:remote_ip=10.1.34.57
root@lxc-lb:~#

注意

请注意,前面提到的 IP 地址是实际服务器的地址,而不是容器的地址。

现在,列出桥接上的所有端口将显示 GRE 端口:

root@lxc-lb:~# ovs-vsctl show
482cf359-a59e-4482-8a71-02b0884d016d
Bridge "lxcovsbr0"
 Port "gre1"
 Interface "gre1"
 type: gre
 options: {remote_ip="10.1.34.57"}
 Port "vethRIC2BJ"
 Interface "vethRIC2BJ"
 Port "lxcovsbr0"
 Interface "lxcovsbr0"
 type: internal
 Port "gre0"
 Interface "gre0"
 type: gre
 options: {remote_ip="10.1.34.124"}
ovs_version: "2.5.0"
root@lxc-lb:~#

由于我们正在 OVS 之间创建网络网状结构,可能会发生数据包循环。为了防止拓扑循环,我们需要在 OVS 上启用生成树协议STP)。STP 是一种二层协议,在创建冗余和互联的交换机连接时防止网络环路。要在 OVS 交换机上启用它,请执行以下命令:

root@lxc-lb:~# ovs-vsctl set bridge lxcovsbr0 stp_enable=true
root@lxc-lb:~#

完成所有前述步骤后,第一个主机已经配置完成。在接下来的部分,我们将以类似的方式配置其余的服务器。

配置 Apache 节点

配置 Apache 节点,请按照以下步骤进行:

  1. 在第一个 Apache 节点上,安装 LXC 和 OVS,并创建桥接:

    root@lxc-node-01:~# apt-get update && apt-get --yes upgrade && reboot
    root@lxc-node-01:~# apt-get install --yes lxc
    root@lxc-node-01:~# apt-get install --yes openvswitch-switch
    root@lxc-node-01:~# ovs-vsctl add-br lxcovsbr0
    root@lxc-node-01:~# ifconfig lxcovsbr0 up
    
    
  2. 替换桥接名称并更改子网:

    root@lxc-node-01:~# sed -i 's/lxcbr0/lxcovsbr0/g' 
          /etc/lxc/default.conf
    root@lxc-node-01:~# sed -i 's/lxcbr0/lxcovsbr0/g' 
          /etc/default/lxc-net
    root@lxc-node-01:~# sed -i 's/10.0.3/192.168.0/g' 
          /etc/default/lxc-net
    
    
  3. 创建到另外两个服务器的 GRE 隧道,并根据需要替换 IP:

    root@lxc-node-01:~# ovs-vsctl add-port lxcovsbr0 gre0 -- set 
          interface gre0 type=gre options:remote_ip=10.1.34.23
    root@lxc-node-01:~# ovs-vsctl add-port lxcovsbr0 gre1 -- set 
          interface gre1 type=gre options:remote_ip=10.1.34.57
     root@lxc-node-01:~# ovs-vsctl show
    625928b0-b57a-46b2-82fe-77d541473f29
     Bridge "lxcovsbr0"
     Port "gre0"
     Interface "gre0"
     type: gre
     options: {remote_ip="10.1.34.23"}
     Port "gre1"
     Interface "gre1"
     type: gre
     options: {remote_ip="10.1.34.57"}
     Port "lxcovsbr0"
     Interface "lxcovsbr0"
     type: internal
     ovs_version: "2.5.0"
    root@lxc-node-01:~#
    
    
  4. 在桥接器上启用 STP:

    root@lxc-node-01:~# ovs-vsctl set bridge lxcovsbr0 stp_enable=true
    root@lxc-node-01:~#
    
    
  5. 接下来,让我们创建一个名为apache的 Ubuntu 容器:

    root@lxc-node-01:~# lxc-create --name apache --template ubuntu
    root@lxc-node-01:~# lxc-start --name apache
    
    
  6. 是时候以类似方式配置最后一个节点了:

    root@lxc-node-02:~# apt-get update && apt-get upgrade --yes && reboot
    root@lxc-node-02:~# apt-get install --yes lxc
    root@lxc-node-02:~# apt-get install --yes openvswitch-switch
    root@lxc-node-02:~# ovs-vsctl add-br lxcovsbr0
    root@lxc-node-02:~# ifconfig lxcovsbr0 up
    root@lxc-node-02:~# sed -i 's/lxcbr0/lxcovsbr0/g' 
          /etc/lxc/default.conf
    root@lxc-node-02:~# sed -i 's/lxcbr0/lxcovsbr0/g' 
          /etc/default/lxc-net
    root@lxc-node-02:~# sed -i 's/10.0.3/192.168.0/g' 
          /etc/default/lxc-net
    
    
  7. 创建 GRE 隧道:

    root@lxc-node-02:~# ovs-vsctl add-port lxcovsbr0 gre0 -- set 
          interface gre0 type=gre options:remote_ip=10.1.34.23
     root@lxc-node-02:~# ovs-vsctl add-port lxcovsbr0 gre1 -- set 
          interface gre1 type=gre options:remote_ip=10.1.34.124
     root@lxc-node-02:~# ovs-vsctl show
    7b8574ce-ed52-443e-bcf2-6b1ddbedde4c
     Bridge "lxcovsbr0"
     Port "gre0"
     Interface "gre0"
     type: gre
     options: {remote_ip="10.1.34.23"}
     Port "gre1"
     Interface "gre1"
     type: gre
     options: {remote_ip="10.1.34.124"}
     Port "lxcovsbr0"
     Interface "lxcovsbr0"
     type: internal
     ovs_version: "2.5.0"
    root@lxc-node-02:~#
    
    
  8. 同时,在交换机上启用 STP:

    root@lxc-node-02:~# ovs-vsctl set bridge lxcovsbr0 stp_enable=true
    root@lxc-node-02:~#
    
    
  9. 最后,创建并启动 Apache 容器:

    root@lxc-node-02:~# lxc-create --name apache --template ubuntu
    root@lxc-node-02:~# lxc-start --name apache
    
    

安装 Apache 和 HAProxy,并测试连接性

配置好所有服务器,启动容器,建立 GRE 隧道后,让我们测试每个 LXC 实例之间的连接性。由于所有容器都属于同一网络,并通过 GRE 隧道与 OVS 交换机互联,它们应该能够相互通信。最重要的是,Apache 容器将通过在lxc-lb服务器上运行的dnsmasq服务通过 DHCP 获取网络配置。

要验证每个容器是否接收到租约,我们可以通过执行以下命令检查dnsmasq租约文件:

root@lxc-lb:~# cat /var/lib/misc/dnsmasq.lxcovsbr0.leases
1478111141 00:16:3e:84:cc:f3 192.168.0.41 apache *
1478111044 00:16:3e:74:b3:8c 192.168.0.165 * *
1478110360 00:16:3e:76:92:0a 192.168.0.26 haproxy *
root@lxc-lb:~#

  1. 获取租约可能需要几秒钟;你可能需要多次检查该文件,才能看到任何 IP 记录。一旦所有容器都分配了 IP,我们应该能够在列出每个服务器的容器时看到它们:

    root@lxc-lb:~# lxc-ls -f
    NAME    STATE   AUTOSTART GROUPS IPV4         IPV6
    haproxy RUNNING 0         -      192.168.0.26 -
    root@lxc-lb:~#
    root@lxc-node-01:~# lxc-ls -f
    NAME   STATE   AUTOSTART GROUPS IPV4          IPV6
    apache RUNNING 0         -      192.168.0.165 -
    root@lxc-node-01:~#
    root@lxc-node-02:~# lxc-ls -f
    NAME   STATE   AUTOSTART GROUPS IPV4         IPV6
    apache RUNNING 0         -      192.168.0.41 -
    root@lxc-node-02:~#
    
    
  2. 接下来,让我们在lxc-lb服务器上的haproxy容器中安装 HAProxy,并测试容器之间的连接性:

    root@lxc-lb:~# lxc-attach --name haproxy
    root@haproxy:~# ping -c3 192.168.0.165
    PING 192.168.0.165 (192.168.0.165) 56(84) bytes of data.
    64 bytes from 192.168.0.165: icmp_seq=1 ttl=64 time=0.840 ms
    64 bytes from 192.168.0.165: icmp_seq=2 ttl=64 time=0.524 ms
    64 bytes from 192.168.0.165: icmp_seq=3 ttl=64 time=0.446 ms
    --- 192.168.0.165 ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 1999ms
    rtt min/avg/max/mdev = 0.446/0.603/0.840/0.171 ms
     root@haproxy:~# ping -c3 192.168.0.41
    PING 192.168.0.41 (192.168.0.41) 56(84) bytes of data.
    64 bytes from 192.168.0.41: icmp_seq=1 ttl=64 time=1.26 ms
    64 bytes from 192.168.0.41: icmp_seq=2 ttl=64 time=0.939 ms
    64 bytes from 192.168.0.41: icmp_seq=3 ttl=64 time=1.05 ms
    --- 192.168.0.41 ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 2001ms
    rtt min/avg/max/mdev = 0.939/1.086/1.269/0.142 ms
    root@haproxy:~#
    root@haproxy:~# apt-get update && apt-get install haproxy
    ...
    
    

    注意

    如果你在云提供商上构建这个示例部署,并且apt-get update挂起,请尝试将 LXC 容器中eth0接口的 MTU 设置减少如下:ifconfig eth0 mtu 1400

  3. 让我们查看haproxy.cfg配置文件:

     root@haproxy:~# cat /etc/haproxy/haproxy.cfg 
     global 
     log /dev/log local0 
     log /dev/log local1 notice 
     chroot /var/lib/haproxy 
     stats socket /run/haproxy/admin.sock mode 660 level admin 
     stats timeout 30s 
     user haproxy 
     group haproxy 
     daemon 
     ca-base /etc/ssl/certs 
     crt-base /etc/ssl/private 
     ssl-default-bind-ciphers
                ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+
                AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:
                RSA+3DES:!aNULL:!MD5:!DSS 
     ssl-default-bind-options no-sslv3 
     defaults 
     log global 
     mode http 
     option httplog 
     option dontlognull 
     timeout connect 5000 
     timeout client 50000 
     timeout server 50000 
     frontend http 
     bind :80 
     reqadd X-Forwarded-Proto:\ http 
     default_backend http_nodes 
     backend http_nodes 
     mode http 
     balance roundrobin 
     option httpclose 
     option forwardfor 
     option redispatch 
     option httpchk GET / 
     cookie JSESSIONID prefix 
     server http1 192.168.0.165:80 check inter 5000 
     server http1 192.168.0.41:80 check inter 5000 
     root@haproxy:~#
    
    

    HAProxy 的配置几乎与我们在本章前面使用的配置相同。

    注意

    请注意,HAProxy 配置文件中backend部分的 IP 地址是运行在lxc-node-01/02服务器上的 Apache 容器的 IP。

  4. 重启 HAProxy 并确保其正常运行:

    root@haproxy:~# service haproxy restart
    root@haproxy:~# ps axfww
     PID TTY      STAT   TIME COMMAND
     1 ?         Ss     0:00 /sbin/init
     38 ?        Ss     0:00 /lib/systemd/systemd-journald
     59 ?        Ss     0:00 /usr/sbin/cron -f
     62 ?        Ssl    0:00 /usr/sbin/rsyslogd -n
     143 ?        Ss     0:00 /sbin/dhclient -1 -v -pf 
            /run/dhclient.eth0.pid 
            -lf /var/lib/dhcp/dhclient.eth0.leases -I -df   
            /var/lib/dhcp/dhclient6.eth0.leases eth0
     167 ?        Ss     0:00 /usr/sbin/sshd -D
     168 pts/2    Ss+    0:00 /sbin/agetty --noclear 
            --keep-baud pts/2 115200 38400 9600 vt220
     169 lxc/console Ss+   0:00 /sbin/agetty --noclear 
            --keep-baud console 115200 38400 9600 vt220
     412 ?        Ss     0:00 /usr/sbin/haproxy-systemd-wrapper -f  
            /etc/haproxy/haproxy.cfg -p /run/haproxy.pid
     413 ?        S      0:00  \_ /usr/sbin/haproxy -f 
            /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -Ds
     414 ?        Ss     0:00      \_ /usr/sbin/haproxy -f 
            /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -Ds
    root@haproxy:~#
    
    

    请注意,从前面的输出中可以看到,与本章前面看到的 libvirt LXC 示例不同,现在容器中的初始化进程是父进程。

  5. 在另外两个容器中安装 Apache,并为每个容器创建index.html页面:

    root@lxc-node-01:~# lxc-attach --name apache
    root@apache:~# apt-get install --yes apache2
    root@apache:~# echo "Apache on LXC container on host lxc-node-01" > 
          /var/www/html/index.html
    root@lxc-node-02:~# lxc-attach --name apache
    root@apache:~# apt-get install --yes apache2
    root@apache:~# echo "Apache on LXC container on host lxc-node-02" > 
          /var/www/html/index.html
    
    
  6. haproxy容器内,连接到 HAProxy 正在监听的端口80,负载均衡器应该将请求转发到 Apache 容器:

    root@haproxy:~# apt-get install --yes curl
    root@haproxy:~# curl localhost
    Apache on LXC container on host lxc-node-01
    root@haproxy:~# curl localhost
    Apache on LXC container on host lxc-node-02
    root@haproxy:~# exit
    exit
    root@lxc-lb:~#
    
    
  7. 我们应该也能够从lxc-lb主机连接到 HAProxy,因为主机操作系统可以通过 OVS 交换机与容器通信:

    root@lxc-lb:~# apt-get install curl
    root@lxc-lb:~# curl 192.168.0.26
    Apache on LXC container on host lxc-node-01
    root@lxc-lb:~# curl 192.168.0.26
    Apache on LXC container on host lxc-node-02
    root@lxc-lb:~#
    
    

    前面的192.168.0.26地址是haproxy容器的 IP 地址;请根据系统中dnsmasq分配的 IP 地址进行替换。

  8. 最后,我们可以在lxc-lb服务器本身上安装 HAProxy,这将允许我们从外部世界连接到 Apache 服务器,例如,如果lxc-lb主机具有公共 IP。在这种情况下,我们完全不需要在容器中运行 HAProxy,尽管我们可以重用相同的配置:

    root@lxc-lb:~# apt-get install haproxy
    ...
    root@lxc-lb:~#
    
    

    从容器中复制配置(如本节前面所列)并重启 HAProxy:

    root@lxc-lb:~# service haproxy restart
    root@lxc-lb:~# curl 10.1.34.23
    Apache on LXC container on host lxc-node-01
    root@lxc-lb:~# curl 10.1.34.23
    Apache on LXC container on host lxc-node-02
    root@lxc-lb:~#
    
    

    注意

    请注意,10.1.34.23 IP 地址是此示例中 lxc-lb 服务器的地址。如果您的服务器有多个 IP 地址或公共 IP 地址,可以使用其中任何一个,因为我们已经配置了 HAProxy 绑定到所有接口。

通过这样做,我们建立了一个简单的设置,可以在生产环境中使用,通过添加更多的服务器和容器,并通过 HAProxy 或 Nginx 等负载均衡器来水平扩展服务。

扩展 Apache 服务

像前面那样的设置可以通过创建容器文件系统和配置文件的快照来完全自动化,这些快照已经安装了所需的服务;然后使用这些副本按需启动新的容器。

为了演示如何通过添加更多容器来手动扩展 Apache,请按照以下步骤操作:

  1. 首先,停止其中一个 Apache 实例:

    root@lxc-node-01:~# lxc-ls -f
    NAME   STATE   AUTOSTART GROUPS IPV4 IPV6
    apache RUNNING 0         -      -    -
    root@lxc-node-01:~# lxc-stop --name apache
    
    
  2. 接下来,复制其根文件系统和 LXC 配置文件:

    root@lxc-node-01:~# cd /var/lib/lxc
    root@lxc-node-01:/var/lib/lxc# ls -alh
    total 12K
    drwx------  3 root root 4.0K Nov  2 16:53 .
    drwxr-xr-x 43 root root 4.0K Nov  2 15:02 ..
    drwxrwx---  3 root root 4.0K Nov  2 16:53 apache
    root@lxc-node-01:/var/lib/lxc# cp -rp apache/ apache_new
    root@lxc-node-01:/var/lib/lxc#
    
    
  3. 更改新容器的名称,并从配置文件中删除 MAC 地址。LXC 会在容器启动时动态分配一个新的地址:

    root@lxc-node-01:/var/lib/lxc# sed -i 's/apache/apache_new/g' 
          apache_new/config
    root@lxc-node-01:/var/lib/lxc# sed -i '/lxc.network.hwaddr/d' 
          apache_new/config
    root@lxc-node-01:/var/lib/lxc#
    
    
  4. 现在,我们在主机上有两个容器:

    root@lxc-node-01:/var/lib/lxc# lxc-ls -f
    NAME       STATE   AUTOSTART GROUPS IPV4 IPV6
    apache     STOPPED 0         -      -    -
    apache_new STOPPED 0         -      -    -
    root@lxc-node-01:/var/lib/lxc#
    
    
  5. 让我们启动这两个容器:

    root@lxc-node-01:/var/lib/lxc# lxc-start --name apache
    root@lxc-node-01:/var/lib/lxc# lxc-start --name apache_new
    root@lxc-node-01:/var/lib/lxc# lxc-ls -f
    NAME       STATE   AUTOSTART GROUPS IPV4 IPV6
    apache     RUNNING 0         -      -    -
    apache_new RUNNING 0         -      -    -
    root@lxc-node-01:/var/lib/lxc#
    
    
  6. 由于我们已经从原始容器复制了整个根文件系统,因此 Apache 服务已在新实例中安装。现在,启动它并确保它正在运行:

    root@lxc-node-01:/var/lib/lxc# lxc-attach --name apache_new
    root@apache:/# /etc/init.d/apache2 start
    [ ok ] Starting apache2 (via systemctl): apache2.service.
    root@apache:/# ps ax
     PID TTY      STAT   TIME COMMAND
     1 ?        Ss     0:00 /sbin/init
     37 ?        Ss     0:00 /lib/systemd/systemd-journald
     50 ?        Ss     0:00 /sbin/ifup -a --read-environment
     60 ?        Ss     0:00 /usr/sbin/cron -f
     62 ?        Ssl    0:00 /usr/sbin/rsyslogd -n
     97 ?        S      0:00 /bin/sh -c /sbin/dhclient -1 -v -pf 
             /run/dhclient.eth0.pid -lf 
             /var/lib/dhcp/dhclient.eth0.leases -I 
             -df /var/lib/dhcp/dhclient6.eth0.leases eth0 ?
     98 ?        S      0:00 /sbin/dhclient -1 -v -pf 
             /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases 
             -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
     125 pts/3    Ss     0:00 /bin/bash
     168 ?        Ss     0:00 /usr/sbin/apache2 -k start
     171 ?        Sl     0:00 /usr/sbin/apache2 -k start
     172 ?        Sl     0:00 /usr/sbin/apache2 -k start
     246 pts/3    R+     0:00 ps ax
    root@apache:/#
    
    

这个过程类似的操作可以通过 Jenkins 等服务完全自动化,并分布到不同的服务器上,从而实现服务的自动扩展。我们将在 第七章中更详细地探讨这个设置,容器化世界中的监控与备份

概述

使用 LXC 扩展各种工作负载需要一个代理服务,如 HAProxy 或 Nginx,并且实际的服务运行在容器中。通过使用软件定义网络(OVS 和 GRE 隧道),可以实现网络连接和分段。

在本章中,我们介绍了如何在简单的基于 libvirt 的 LXC 容器中运行 Apache,这些容器不需要整个根文件系统,而只需包含主机操作系统中共享的二进制文件和库的最小目录集。我们还在多个服务器上创建了一个负载均衡器后面的 Apache 集群,并演示了通过复制 LXC 容器来简单而有效地扩展它的方法。

在下一章中,我们将继续你所学到的内容,展示如何监控和备份 LXC,并使用 Jenkins 和 Sensu 创建一个自动扩展服务。

第七章。容器化世界中的监控与备份

在上一章中,我们看了一些如何通过创建多个实例并放置在代理服务(如 HAProxy)后面,来扩展在 LXC 容器中运行的应用程序的示例。这确保了应用程序有足够的资源并能承受故障,从而实现一定程度的高可用性。

对于运行在单个 LXC 实例中的应用程序,通常希望定期备份容器,包括根文件系统和容器的配置文件。根据后台存储的不同,有一些可用的选项,我们将在本章中探讨。

使用高度可用或共享的后台存储可以帮助快速从失败的主机恢复,或者在需要在服务器之间迁移 LXC 容器时提供帮助。我们将看看如何在互联网小型计算机系统接口iSCSI)目标上创建容器,并在服务器之间迁移 LXC。我们还将给出一个示例,展示如何使用 GlusterFS 作为共享文件系统来托管容器的根文件系统,从而创建活动-被动 LXC 部署。

我们还将介绍如何使用各种工具(如 Monit 和 Sensu)监控 LXC 容器及其内部运行的服务;我们将在本章结束时给出一个基于监控触发器创建简单自动扩展解决方案的示例。

在本章中,我们将探讨以下主题:

  • 使用 tarrsync 备份 LXC 容器

  • 使用 lxc-copy 工具备份 LXC 容器

  • 使用 iSCSI 目标作为后台存储创建 LXC 容器,并演示当主服务器故障时如何迁移容器

  • 演示如何使用 GlusterFS 作为容器根文件系统的共享文件系统,并部署活动-被动 LXC 节点

  • 了解 LXC 暴露了哪些度量指标,以及如何监控、警报和处理这些指标

备份和迁移 LXC

创建 LXC 实例的备份可以确保我们在服务器崩溃或后台存储损坏等事件发生时能够恢复数据。备份还为在主机之间迁移实例或通过更改配置文件快速启动多个相似容器提供了一种便捷的方式。

使用 tar 和 rsync 创建 LXC 备份

在大多数使用场景中,我们从模板构建容器,或使用如 debootstrap 等工具,这些工具会为实例创建完整的根文件系统。在这种情况下,创建备份的方法是停止容器,归档其配置文件以及实际的根文件系统,然后将它们存储在远程服务器上。让我们通过一个简单的示例来演示这个概念:

  1. 首先更新你的 Ubuntu 发行版并安装 LXC。在本示例中,我们将使用 Ubuntu 16.04,但该说明适用于任何其他 Linux 发行版:

     root@ubuntu:~# apt-get update && apt-get -y upgrade && reboot 
     root@ubuntu:~# lsb_release -cd 
     Description: Ubuntu 16.04.1 LTS 
     Codename: xenial 
     root@ubuntu:~# 
     root@ubuntu:~# apt-get install -y lxc
    
    
  2. 接下来,使用默认目录后台存储创建一个容器,并启动它:

     root@ubuntu:~# lxc-create --name dir_container --template ubuntu 
     root@ubuntu:~# lxc-start --name dir_container
    
    
  3. 在 LXC 内安装一个应用程序,本例中是 Nginx,并创建一个自定义的索引文件:

     root@ubuntu:~# lxc-attach --name dir_container 
     root@dir_container:~# apt-get install -y nginx 
     root@dir_container:~# echo "Original container" > 
          /var/www/html/index.nginx-debian.html 
     root@dir_container:~# exit 
     exit 
     root@ubuntu:~#
    
    
  4. 在容器运行时,确保我们能够访问 HTTP 服务:

     root@ubuntu:~# lxc-ls -f 
     NAME STATE AUTOSTART GROUPS IPV4 IPV6 
     dir_container RUNNING 0 - 10.0.3.107 - 
     root@ubuntu:~# curl 10.0.3.107 
     Original container 
     root@ubuntu:~#
    
    
  5. 请注意,容器的文件系统及其配置文件都自包含在 /var/lib/lxc 目录中:

     root@ubuntu:~# cd /var/lib/lxc 
     root@ubuntu:/var/lib/lxc# ls -lah 
     total 12K 
     drwx------ 3 root root 4.0K Nov 15 16:28 . 
     drwxr-xr-x 42 root root 4.0K Nov 15 16:17 .. 
     drwxrwx--- 3 root root 4.0K Nov 15 16:33 dir_container 
     root@ubuntu:/var/lib/lxc# ls -la dir_container/ 
     total 16 
     drwxrwx--- 3 root root 4096 Nov 15 16:33 . 
     drwx------ 3 root root 4096 Nov 15 16:28 .. 
     -rw-r--r-- 1 root root 712 Nov 15 16:33 config 
     drwxr-xr-x 21 root root 4096 Nov 15 16:38 rootfs 
     root@ubuntu:/var/lib/lxc#
    
    
  6. 使用标准 Linux 工具(如 tarrsync)创建备份非常方便。在备份之前,请先停止容器:

     root@ubuntu:/var/lib/lxc# lxc-stop --name dir_container 
     root@ubuntu:/var/lib/lxc# lxc-ls -f 
     NAME STATE AUTOSTART GROUPS IPV4 IPV6 
     dir_container STOPPED 0 - - - 
     root@ubuntu:/var/lib/lxc#
    
    
  7. 创建根文件系统和配置文件的 bzip 归档文件,确保保留数字所有者 ID,这样我们就可以在不同的服务器上启动它而不会发生用户 ID 冲突:

     root@ubuntu:/var/lib/lxc# tar --numeric-owner -jcvf 
          dir_container.tar.bz2 dir_container/ 
     ... 
     root@ubuntu:/var/lib/lxc# file dir_container.tar.bz2 
     dir_container.tar.bz2: bzip2 compressed data, block size = 900k 
     root@ubuntu:/var/lib/lxc#
    
    
  8. 接下来,将归档文件复制到另一台服务器,在本例中是 ubuntu-backup,并从原始服务器删除该归档文件:

     root@ubuntu:/var/lib/lxc# rsync -vaz dir_container.tar.bz2 
          ubuntu-backup:/tmp 
     sending incremental file list 
     dir_container.tar.bz2 
     sent 148,846,592 bytes received 35 bytes 9,603,008.19 bytes/sec 
     total size is 149,719,493 speedup is 1.01 
     root@ubuntu:/var/lib/lxc# rm -f dir_container.tar.bz2
    
    

在目标服务器上有了归档文件后,我们现在可以在需要时恢复它。

从归档备份恢复

要从 bz2 备份中恢复容器,在目标服务器上,提取 /var/lib/lxc 目录中的归档文件:

root@ubuntu-backup:~# cd /var/lib/lxc 
root@ubuntu-backup:/var/lib/lxc# ls -la 
total 8 
drwx------ 2 root root 4096 Oct 21 16:56 . 
drwxr-xr-x 42 root root 4096 Nov 15 16:48 .. 
root@ubuntu-backup:/var/lib/lxc# 
root@ubuntu-backup:/var/lib/lxc# tar jxfv /tmp/dir_container.tar.bz2 
... 
root@ubuntu-backup:/var/lib/lxc# ls -la 
total 12 
drwx------ 3 root root 4096 Nov 15 16:59 . 
drwxr-xr-x 42 root root 4096 Nov 15 16:48 .. 
drwxrwx--- 3 root root 4096 Nov 15 16:33 dir_container 
root@ubuntu-backup:/var/lib/lxc#

注意,在提取根文件系统和配置文件后,列出所有容器将显示我们刚刚恢复的容器,尽管它处于停止状态:

root@ubuntu-backup:/var/lib/lxc# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
dir_container STOPPED 0 - - - 
root@ubuntu-backup:/var/lib/lxc#

我们开始并访问 HTTP 端点,确保我们得到与原始 LXC 实例相同的结果:

root@ubuntu-backup:/var/lib/lxc# lxc-start --name dir_container 
root@ubuntu-backup:/var/lib/lxc# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
dir_container RUNNING 0 - 10.0.3.107 - 
root@ubuntu-backup:/var/lib/lxc# curl 10.0.3.107 
Original container 
root@ubuntu-backup:/var/lib/lxc#

要清理,请运行以下命令:

root@ubuntu-backup:/var/lib/lxc# lxc-stop --name dir_container 
root@ubuntu-backup:/var/lib/lxc# lxc-destroy --name dir_container 
Destroyed container dir_container 
root@ubuntu-backup:/var/lib/lxc# ls -la 
total 8 
drwx------ 2 root root 4096 Nov 15 17:01 . 
drwxr-xr-x 42 root root 4096 Nov 15 16:48 .. 
root@ubuntu-backup:/var/lib/lxc#

使用 lxc-copy 创建容器备份

无论容器的后端存储是什么,我们都可以使用 lxc-copy 工具来创建 LXC 实例的完整副本。按照以下步骤创建容器备份:

  1. 我们首先指定要备份的原始主机上的容器名称,以及副本的名称:

     root@ubuntu:/var/lib/lxc# lxc-copy --name dir_container --newname 
          dir_container_backup 
     root@ubuntu:/var/lib/lxc#
    
    

    该命令在主机上创建了一个新的根文件系统和配置文件:

     root@ubuntu:/var/lib/lxc# ls -la 
     total 16 
     drwx------ 4 root root 4096 Nov 15 17:07 . 
     drwxr-xr-x 42 root root 4096 Nov 15 16:17 .. 
     drwxrwx--- 3 root root 4096 Nov 15 16:33 dir_container 
     drwxrwx--- 3 root root 4096 Nov 15 17:07 dir_container_backup 
     root@ubuntu:/var/lib/lxc# lxc-ls -f 
     NAME STATE AUTOSTART GROUPS IPV4 IPV6 
     dir_container STOPPED 0 - - - 
     dir_container_backup STOPPED 0 - - - 
     root@ubuntu:/var/lib/lxc#
    
    
  2. 创建完整副本时,会更新新容器的配置文件,指定新的容器名称和 rootfs 的位置:

     root@ubuntu:/var/lib/lxc# cat dir_container_backup/config | egrep 
          "rootfs|utsname" 
     lxc.rootfs = /var/lib/lxc/dir_container_backup/rootfs 
     lxc.rootfs.backend = dir 
     lxc.utsname = dir_container_backup 
     root@ubuntu:/var/lib/lxc#
    
    

    请注意,容器的名称和目录已从原始实例更改。现在,我们可以像上一节中展示的那样使用 tarrsync 归档并远程存储。如果我们需要确保容器的名称和 rootfs 位置不同于原始实例,这种方法非常方便,尤其是在我们希望将备份保存在同一台服务器上,或在具有相同 LXC 名称的主机上时。

  3. 最后,要清理,请执行以下命令:

     root@ubuntu:/var/lib/lxc# lxc-destroy --name dir_container_backup 
     Destroyed container dir_container_backup 
     root@ubuntu:/var/lib/lxc# lxc-destroy --name dir_container 
     Destroyed container dir_container 
     root@ubuntu:/var/lib/lxc#
    
    

在 iSCSI 目标上迁移 LXC 容器

将容器从一台主机迁移到另一台主机在进行服务器维护或重新分配服务器负载时非常有用。像 OpenStack 这样的云平台利用调度程序来选择 LXC 实例创建的位置,并基于某些标准(如实例密度、资源利用率等)迁移它们,正如我们在下一章中将看到的那样。不过,了解如何在需要时手动迁移主机间的实例是很有帮助的。

当我们使用默认的 dir 存储后端与 LXC 时,创建容器的备份或副本更加容易;然而,当使用其他类型的存储(如 LVM、Btrfs 或 ZFS)时,除非使用共享存储(如 GlusterFS 或 iSCSI 块设备),否则这并不那么简单,我们将接下来探讨这一点。

iSCSI 协议已经存在了一段时间,围绕它有许多存储区域网络SAN)解决方案。它是通过 TCP/IP 网络提供块设备访问的绝佳方式。

设置 iSCSI 目标

暴露块设备的端点被称为目标。在 Linux 上创建 iSCSI 目标是相当简单的。我们所需要的仅仅是一个块设备。在下面的示例中,我们将使用两台服务器;一台将作为 iSCSI 目标,暴露一个块设备,另一台将是发起者服务器,它将连接到目标并将提供的块设备作为 LXC 容器的根文件系统和配置文件的存储位置。

设置 iSCSI 目标的步骤如下:

  1. 让我们先在 Ubuntu 上安装必要的软件包,然后在 CentOS 上演示相同的操作:

     root@ubuntu-backup:~# apt-get install -y iscsitarget 
     root@ubuntu-backup:~#
    
    
  2. 安装软件包后,启用目标功能:

     root@ubuntu-backup:~# sed -i   
          's/ISCSITARGET_ENABLE=false/ISCSITARGET_ENABLE=true/g' 
          /etc/default/iscsitarget 
     root@ubuntu-backup:~#
    
    
  3. 接下来,让我们创建目标配置文件。该文件有良好的注释说明;我们将使用一些可用配置选项中的一个小子集。我们从指定一个任意标识符开始,在此示例中为 iqn.2001-04.com.example:lxc,用户名和密码,最重要的是,我们将暴露的块设备 /dev/xvdb。iSCSI 标识符的格式如下:

          iqn.yyyy-mm.naming-authority:unique name
    

    该标识符的描述如下:

    • yyyy-mm : 这是命名机构成立的年份和月份

    • naming-authority:通常是命名机构的互联网域名的反向语法,或者是服务器的域名

    • unique name:这是你希望使用的任何名称

    考虑到这些,最小的工作目标配置文件如下:

     root@ubuntu-backup:~# cat /etc/iet/ietd.conf 
     Target iqn.2001-04.com.example:lxc 
     IncomingUser lxc secret 
     OutgoingUser 
     Lun 0 Path=/dev/xvdb,Type=fileio 
     Alias lxc_lun 
     root@ubuntu-backup:~#
    
    
  4. 接下来,指定哪些主机或发起者可以连接到 iSCSI 目标;将 IP 地址替换为将连接到目标服务器的主机的 IP 地址:

     root@ubuntu-backup:~# cat /etc/iet/initiators.allow 
     iqn.2001-04.com.example:lxc 10.208.129.253 
     root@ubuntu-backup:~#
    
    
  5. 现在,启动 iSCSI 目标服务:

     root@ubuntu-backup:~# /etc/init.d/iscsitarget start 
     [ ok ] Starting iscsitarget (via systemctl): iscsitarget.service. 
     root@ubuntu-backup:~#
    
    
  6. 在 CentOS 上,过程和配置文件稍有不同。要安装软件包,请执行以下命令:

     [root@centos ~]# yum install scsi-target-utils 
     [root@centos ~]#
    
    
  7. 配置文件的格式如下:

     [root@centos ~]# cat /etc/tgt/targets.conf 
     default-driver iscsi 
     <target iqn.2001-04.com.example:lxc> 
     backing-store /dev/xvdb 
     initiator-address 10.208.5.176 
     incominguser lxc secret 
     </target> 
     [root@centos ~]#
    
    
  8. 要启动服务,请运行以下命令:

     [root@centos ~]# service tgtd restart
    
    
  9. 让我们通过运行以下命令列出暴露的目标:

     [root@centos ~]# tgt-admin --show 
     Target 1: iqn.2001-04.com.example:lxc 
     System information: 
     Driver: iscsi 
     State: ready 
     I_T nexus information: 
     LUN information: 
     ... 
     LUN: 1 
     Type: disk 
     SCSI ID: IET 00010001 
     SCSI SN: beaf11 
     Size: 80531 MB, Block size: 512 
     Online: Yes 
     Removable media: No 
     Prevent removal: No 
     Readonly: No 
     SWP: No 
     Thin-provisioning: No 
     Backing store type: rdwr 
     Backing store path: /dev/xvdb 
     Backing store flags: 
     Account information: 
     ACL information: 
     10.208.5.176 
     [root@centos ~]#
    
    

设置 iSCSI 发起者

发起者是将连接到目标并通过 iSCSI 协议访问暴露的块设备的服务器。

  1. 要在 Ubuntu 上安装发起者工具,请运行以下命令:

     root@ubuntu:~# apt-get install -y open-iscsi
    
    

    在 CentOS 上,软件包名称有所不同:

     [root@centos ~]# yum install iscsi-initiator-utils
    
    
  2. 接下来,启用服务并启动 iSCSI 守护进程:

     root@ubuntu:~# sed -i 's/node.startup = manual/node.startup = 
          automatic/g'   /etc/iscsi/iscsid.conf 
     root@ubuntu:~# /etc/init.d/open-iscsi restart 
     [ ok ] Restarting open-iscsi (via systemctl): open-iscsi.service. 
     root@ubuntu:~#
    
    
  3. iscsiadm 命令是我们可以从发起服务器使用的用户空间工具,用来请求目标提供可用的设备。从启动主机上,我们向之前配置的目标服务器询问可用的块设备:

     root@ubuntu:~# iscsiadm -m discovery -t sendtargets -p 10.208.129.201 
     10.208.129.201:3260,1 iqn.2001-04.com.example:lxc 
     192.237.179.19:3260,1 iqn.2001-04.com.example:lxc 
     10.0.3.1:3260,1 iqn.2001-04.com.example:lxc 
     root@ubuntu:~#
    
    

    从上面的输出中,我们可以看到目标服务器提供了一个目标,其 iSCSI 合格名称IQN)为 iqn.2001-04.com.example:lxc。在这种情况下,目标服务器有三个 IP 地址,因此输出中有三行。

  4. 之前,我们配置了目标使用用户名和密码。我们需要配置启动主机,以便向目标主机提供相同的凭证来访问资源:

     root@ubuntu:~# iscsiadm -m node -T iqn.2001-04.com.example:lxc -p 
          10.208.129.201:3260 --op=update --name node.session.auth.authmethod 
          --value=CHAP 
     root@ubuntu:~# iscsiadm -m node -T iqn.2001-04.com.example:lxc -p   
          10.208.129.201:3260 --op=update --name node.session.auth.username 
          --value=lxc 
     root@ubuntu:~# iscsiadm -m node -T iqn.2001-04.com.example:lxc -p
          10.208.129.201:3260 --op=update --name node.session.auth.password 
          --value=secret 
     root@ubuntu:~#
    
    
  5. 更新启动器配置后,我们可以通过检查自动创建的配置文件来确认凭证是否已应用:

     root@ubuntu:~# cat 
          /etc/iscsi/nodes/iqn.2001-04.com.example\:lxc/10.208.129.201\,3260\,1
          /default | grep auth 
     node.session.auth.authmethod = CHAP 
     node.session.auth.username = lxc 
     node.session.auth.password = secret 
     node.conn[0].timeo.auth_timeout = 45 
     root@ubuntu:~#
    
    

使用提供的块设备登录到 iSCSI 目标,并将其作为 LXC 的 rootfs

配置好启动主机后,我们现在可以登录到 iSCSI 目标并使用提供的块设备:

  1. 要登录,从启动主机运行以下命令:

     root@ubuntu:~# iscsiadm -m node -T iqn.2001-04.com.example:lxc -p  
          10.208.129.201:3260 --login 
     Logging in to [iface: default, target: iqn.2001-04.com.example:lxc, 
          portal:10.208.129.201,3260] (multiple) 
     Login to [iface: default, target: iqn.2001-04.com.example:lxc, 
          portal:10.208.129.201,3260] successful. 
     root@ubuntu:~#
    
    
  2. 让我们验证启动主机现在是否包含 iSCSI 会话:

     root@ubuntu:~# iscsiadm -m session 
     tcp: [2] 10.208.129.201:3260,1 iqn.2001-04.com.example:lxc 
          (non-flash) 
     root@ubuntu:~#
    
    
  3. 启动主机现在应该有一个新的块设备可以使用:

     root@ubuntu:~# ls -la /dev/disk/by-path/ 
     total 0 
     drwxr-xr-x 2 root root 60 Nov 15 20:23 . 
     drwxr-xr-x 6 root root 120 Nov 15 20:23 .. 
     lrwxrwxrwx 1 root root 9 Nov 15 20:23 
          ip-10.208.129.201:3260-iscsi-iqn.2001-04.com.example:lxc-lun-0 
          -> ../../sda 
     root@ubuntu:~#
    
    
  4. 这个新的块设备可以作为常规存储设备使用。我们来在上面创建一个文件系统:

     root@ubuntu:~# mkfs.ext4 /dev/disk/by-path/ip-10.208.129.201\
          :3260-iscsi-iqn.2001-04.com.example\:lxc-lun-0 
     root@ubuntu:~#
    
    
  5. 在文件系统存在的情况下,我们将块设备作为默认位置,挂载到 /var/lib/lxc,以便用于 LXC 文件系统:

     root@ubuntu:~# mount /dev/disk/by-path/ip-10.208.129.201\
          :3260-iscsi-iqn.2001-04.com.example\:lxc-lun-0 /var/lib/lxc 
     root@ubuntu:~# df -h | grep lxc 
     /dev/sda 74G 52M 70G 1% /var/lib/lxc 
     root@ubuntu:~#
    
    

由于我们将 iSCSI 设备挂载到 LXC 容器根文件系统的默认位置,下次我们创建容器时,其根文件系统将位于 iSCSI 设备上。正如我们稍后将看到的那样,这在启动主机需要维护或由于某种原因变得不可用时非常有用,因为我们可以将相同的 iSCSI 目标挂载到新主机上,并且只需在 LXC 端没有配置更改的情况下启动相同的容器。

构建 iSCSI 容器

按照惯例创建一个新的 LXC 容器,启动它,并确保它正在运行:

root@ubuntu:~# lxc-create --name iscsi_container --template ubuntu 
root@ubuntu:~# lxc-start --name iscsi_container 
root@ubuntu:~# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
iscsi_container RUNNING 0 - 10.0.3.162 - 
root@ubuntu:~#

即使根文件系统现在位于新的 iSCSI 块设备上,从主机操作系统的角度来看,也没有任何变化:

root@ubuntu:~# ls -la /var/lib/lxc/iscsi_container/ 
total 16 
drwxrwx--- 3 root root 4096 Nov 15 21:01 . 
drwxr-xr-x 4 root root 4096 Nov 15 21:01 .. 
-rw-r--r-- 1 root root 716 Nov 15 21:01 config 
drwxr-xr-x 21 root root 4096 Nov 15 21:01 rootfs 
root@ubuntu:~#

由于容器的 rootfs 现在位于远程块设备上,取决于 LXC 启动主机与 iSCSI 目标主机之间的网络连接,可能会有一些延迟。在生产部署中,建议 iSCSI 目标和启动主机之间使用隔离的低延迟网络。

要将容器迁移到另一个主机,我们需要停止容器,卸载磁盘,然后从 iSCSI 目标注销块设备:

root@ubuntu:~# lxc-stop --name iscsi_container 
root@ubuntu:~# umount /var/lib/lxc 
root@ubuntu:~# iscsiadm -m node -T iqn.2001-04.com.example:lxc -p 10.208.129.201:3260 --logout 
Logging out of session [sid: 6, target: iqn.2001-04.com.example:lxc, portal: 10.208.129.201,3260] 
Logout of [sid: 6, target: iqn.2001-04.com.example:lxc, portal: 10.208.129.201,3260] successful. 
root@ubuntu:~#

恢复 iSCSI 容器

要恢复 iSCSI 容器,请按以下步骤操作:

  1. 在配置为启动主机的新主机上,我们可以登录到与之前相同的目标:

     root@ubuntu-backup:~# iscsiadm -m node -T iqn.2001-04.com.example:lxc
          -p 10.208.129.201:3260 --login 
     Logging in to [iface: default, target: iqn.2001-04.com.example:lxc, 
          portal: 10.208.129.201,3260] (multiple) 
     Login to [iface: default, target: iqn.2001-04.com.example:lxc, 
          portal: 10.208.129.201,3260] successful. 
     root@ubuntu-backup:~#
    
    
  2. 确保在登录到 iSCSI 目标后已提供新的块设备:

     root@ubuntu-backup:~# ls -la /dev/disk/by-path/ 
     total 0 
     drwxr-xr-x 2 root root 60 Nov 15 21:31 . 
     drwxr-xr-x 6 root root 120 Nov 15 21:31 .. 
     lrwxrwxrwx 1 root root 9 Nov 15 21:31 ip-10.208.129.201:
          3260-iscsi-iqn.2001-04.com.example:lxc-lun-0 -> ../../sda 
     root@ubuntu-backup:~#
    
    

    当在新主机上登录 iSCSI 目标时,请记住,呈现的块设备的名称可能与原始服务器上的不同。

  3. 接下来,将块设备挂载到 LXC 根文件系统的默认位置:

     root@ubuntu-backup:~# mount /dev/disk/by-path/ip-10.208.129.201\
          :3260-iscsi-iqn.2001-04.com.example\:lxc-lun-0 /var/lib/lxc 
     root@ubuntu-backup:~#
    
    
  4. 如果我们现在列出所有可用的容器,我们会看到之前在主机上创建的容器现在已经出现在新服务器上,仅通过挂载 iSCSI 目标即可实现:

     root@ubuntu-backup:~# lxc-ls -f 
     NAME STATE AUTOSTART GROUPS IPV4 IPV6 
     iscsi_container STOPPED 0 - - - 
     root@ubuntu-backup:~#
    
    
  5. 最后,我们可以像往常一样启动容器:

     root@ubuntu-backup:~# lxc-start --name iscsi_container 
     root@ubuntu-backup:~# lxc-ls -f 
     NAME STATE AUTOSTART GROUPS IPV4 IPV6 
     iscsi_container RUNNING 0 - 10.0.3.162 - 
     root@ubuntu-backup:~#
    
    

如果容器配置了静态 IP 地址,则新主机上也会有相同的 IP;然而,如果容器通过动态方式获取网络配置,则其 IP 地址可能会发生变化。如果你的容器迁移时在 DNS 中有关联的 A 记录,请记住这一点。

使用复制 GlusterFS 存储的 LXC 活跃备份

在上一节中,我们展示了如何通过 iSCSI 导出远程块设备,将其用作 LXC 的本地存储。我们将块设备格式化为不允许多个服务器共享访问的文件系统。如果你在多个服务器上登录目标设备,并且它们同时尝试写入,数据将会损坏。使用单节点上的 iSCSI 设备托管 LXC 容器提供了一种出色的冗余解决方案,当 LXC 服务器宕机时,我们只需在新发起服务器上登录相同的块设备并启动容器。我们可以将这种方式视为冷备份,因为在将 iSCSI 块设备迁移到新主机、登录并启动容器时会有停机时间。

还有另一种方式,我们可以使用一个可以同时被多个服务器附加和访问的共享文件系统,在多个主机上运行相同的 LXC 容器,并且这些容器有不同的 IP 地址。让我们探索这种使用可扩展网络文件系统 GlusterFS 作为远程共享文件系统的场景。

创建共享存储

GlusterFS 具有两个主要组件——一个服务器组件,运行 GlusterFS 守护进程,导出称为 砖块(bricks)的本地块设备;一个客户端组件,通过 TCP/IP 网络与服务器连接,使用自定义协议,创建聚合的虚拟卷,然后可以将其挂载并用作常规文件系统。

有三种类型的卷:

  • 分布式:这些是将文件分布到集群中的卷

  • 复制:这些是将数据复制到存储集群中两个或更多节点的卷

  • 条带化:这些条带文件分布在多个存储节点上

为了实现运行在这些共享卷上的 LXC 容器的高可用性,我们将使用两个 GlusterFS 服务器上的复制卷。在本例中,我们将在每个 GlusterFS 服务器上使用 LVM 上的块设备。

  1. 在第一个存储节点上,我们将在 /dev/xvdb 块设备上创建 PV、VG 和 LV:

     root@node1:~# pvcreate /dev/xvdb 
     Physical volume "/dev/xvdb" successfully created 
     root@node1:~# vgcreate vg_node /dev/xvdb 
     Volume group "vg_node" successfully created 
     root@node1:~# lvcreate -n node1 -L 10g vg_node 
     Logical volume "node1" created. 
     root@node1:~#
    
    
  2. 接下来,让我们在 LVM 设备上创建文件系统并挂载它。XFS 在 GlusterFS 中表现非常好:

     root@node1:~# mkfs.xfs -i size=512 /dev/vg_node/node1 
     meta-data=/dev/vg_node/node1   isize=512       agcount=4,  
          agsize=655360 blks 
     =                       sectsz=512      attr=2, 
          projid32bit=1 
     =                       crc=1           finobt=1, 
          sparse=0 
     data =                       bsize=4096      blocks=2621440, 
          imaxpct=25 
     =                       sunit=0         swidth=0 
          blks 
     naming =version 2              bsize=4096      ascii-ci=0 
          ftype=1 
     log    =internal log           bsize=4096      blocks=2560, 
          version=2 
     =                       sectsz=512      sunit=0 blks, 
          lazy-count=1 
     realtime =none                 extsz=4096      blocks=0, 
          rtextents=0 
     root@node1:~# mount /dev/vg_node/node1 /mnt/ 
     root@node1:~# mkdir /mnt/bricks
    
    
  3. 最后,让我们安装 GlusterFS 服务:

     root@node1:~# apt-get install -y glusterfs-server
    
    

    在第二个 GlusterFS 节点上重复上述步骤,必要时将node1替换为node2

  4. 一旦在两个节点上运行了 GlusterFS 守护进程,就可以从node1进行探测,看看是否能看到任何对等节点:

     root@node1:~# gluster peer status 
     Number of Peers: 0 
     root@node1:~# gluster peer probe node2 
     peer probe: success. 
     root@node1:~# gluster peer status 
     Number of Peers: 1 
     Hostname: node2 
     Uuid: 65a480ba-e841-41a2-8f28-9d8f58f344ce 
     State: Peer in Cluster (Connected) 
     root@node1:~#
    
    

    通过从node1进行探测,我们现在可以看到node1node2都属于同一个集群。

  5. 下一步是创建复制卷,指定之前创建和挂载的 LV 的挂载路径。由于我们在存储集群中使用了两个节点,复制因子将为2。以下命令将创建复制卷,该卷将包含来自node1node2的块设备,并列出创建的卷:

     root@node1:~# gluster volume create lxc_glusterfs replica 2 transport 
          tcp node1:/mnt/bricks/node1 node2:/mnt/bricks/node2 
     volume create: lxc_glusterfs: success: please start the volume to 
          access data 
     root@node1:~# gluster volume list 
     lxc_glusterfs 
     root@node1:~#
    
    
  6. 要启动新创建的复制卷,运行以下命令:

     root@node1:~# gluster volume start lxc_glusterfs 
     volume start: lxc_glusterfs: success
    
    

    要创建并启动卷,我们只需要从其中一个存储节点运行上述命令。

  7. 要获取新卷的信息,请在任何一个节点上运行此命令:

     root@node1:~# gluster volume info
     Volume Name: lxc_glusterfs
     Type: Replicate
     Volume ID: 9f11dc99-19d6-4644-87d2-fa6e983bcb83
     Status: Started
     Number of Bricks: 1 x 2 = 2
     Transport-type: tcp
     Bricks:
     Brick1: node1:/mnt/bricks/node1
     Brick2: node2:/mnt/bricks/node2
     Options Reconfigured:
     performance.readdir-ahead: on
     root@node1:~#
    
    

    从前面的输出中,注意到来自node1node2的两个砖块。我们也可以在两个节点上看到它们:

     root@node1:~# ls -la /mnt/bricks/node1/ 
     total 0 
     drwxr-xr-x 4 root root 41 Nov 16 21:33 . 
     drwxr-xr-x 3 root root 19 Nov 16 21:33 .. 
     drw------- 6 root root 141 Nov 16 21:34 .glusterfs 
     drwxr-xr-x 3 root root 25 Nov 16 21:33 .trashcan 
     root@node1:~#
    
    

    我们在node2上看到以下内容:

     root@node2:~# ls -la /mnt/bricks/node1/ 
     total 0 
     drwxr-xr-x 4 root root 41 Nov 16 21:33 . 
     drwxr-xr-x 3 root root 19 Nov 16 21:33 .. 
     drw------- 6 root root 141 Nov 16 21:34 .glusterfs 
     drwxr-xr-x 3 root root 25 Nov 16 21:33 .trashcan 
     root@node2:~#
    
    

我们应该看到相同的文件,因为我们使用的是复制卷。在一个挂载的卷上创建的文件应会出现在另一个上,尽管与之前的 iSCSI 设置一样,网络延迟很重要,因为数据需要通过网络传输。

构建 GlusterFS LXC 容器

在 GlusterFS 集群准备好之后,让我们使用第三台服务器从存储集群挂载复制卷,并将其用作 LXC 根文件系统和配置位置:

  1. 首先,让我们安装 GlusterFS 客户端:

     root@lxc1:~# apt-get install -y glusterfs-client attr
    
    
  2. 接下来,将存储集群中的复制卷挂载到 LXC 主机上:

     root@lxc1:~# mount -t glusterfs node2:/lxc_glusterfs /var/lib/lxc
    
    

    在前面的命令中,我们使用node2作为目标进行挂载;不过,我们也可以以完全相同的方式使用node1。在lxc_glusterfs挂载命令中指定的目标设备名称就是我们之前指定的复制卷名称。

  3. 现在,复制的 GlusterFS 卷已挂载到默认的 LXC 位置,让我们创建并启动一个容器:

     root@lxc1:~# lxc-create --name glusterfs_lxc --template ubuntu 
     root@lxc1:~# lxc-start --name glusterfs_lxc
    
    
  4. 附加到容器并安装 Nginx,以便稍后我们可以从多个服务器测试连接性:

     root@lxc1:~# lxc-attach --name glusterfs_lxc 
     root@glusterfs_lxc:~# apt-get -y install nginx 
     ... 
     root@glusterfs_lxc:~# echo "Nginx on GlusterFS LXC" > 
          /var/www/html/index.nginx-debian.html 
     root@glusterfs_lxc:~# exit 
     exit 
     root@lxc1:~#
    
    
  5. 获取容器的 IP 地址,并确保我们能从主机操作系统连接到 Nginx:

     root@lxc1:~# lxc-ls -f 
     NAME STATE AUTOSTART GROUPS IPV4 IPV6 
     glusterfs_lxc RUNNING 0 - 10.0.3.184 - 
     root@lxc1:~# curl 10.0.3.184 
     Nginx on GlusterFS LXC 
     root@lxc1:~#
    
    
  6. 容器创建后,根文件系统和配置文件将在两个存储节点上可见:

     root@node1:~# ls -la /mnt/bricks/node1/ 
     total 12 
     drwxr-xr-x 5 root root 62 Nov 16 21:53 . 
     drwxr-xr-x 3 root root 19 Nov 16 21:33 .. 
     drw------- 261 root root 8192 Nov 16 21:54 .glusterfs 
     drwxrwx--- 3 root root 34 Nov 16 21:53 glusterfs_lxc 
     drwxr-xr-x 3 root root 25 Nov 16 21:33 .trashcan 
     root@node1:~#
    
    
  7. 以下内容将在node2上可见:

     root@node2:~# ls -la /mnt/bricks/node2/ 
     total 12 
     drwxr-xr-x 5 root root 62 Nov 16 21:44 . 
     drwxr-xr-x 3 root root 19 Nov 16 21:33 .. 
     drw------- 261 root root 8192 Nov 16 21:44 .glusterfs 
     drwxrwx--- 3 root root 34 Nov 16 21:47 glusterfs_lxc 
     drwxr-xr-x 3 root root 25 Nov 16 21:33 .trashcan 
     root@node2:~#
    
    

根据您的网络带宽和延迟,复制数据到两个存储节点之间可能需要一些时间。这也会影响 LXC 容器的构建、启动和停止所需的时间。

恢复 GlusterFS 容器

让我们创建第二个 LXC 主机,安装 GlusterFS 客户端,并以与之前相同的方式挂载复制卷:

root@lxc2:~# apt-get install -y glusterfs-client attr 
root@lxc2:~# mount -t glusterfs node1:/lxc_glusterfs /var/lib/lxc 
root@lxc2:~# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
glusterfs_lxc STOPPED 0 - - - 
root@lxc2:~#

请注意,仅通过挂载 GlusterFS 卷,主机现在看到容器处于停止状态。这与 node1 上运行的容器完全相同——即相同的配置和根文件系统。由于我们使用的是共享文件系统,因此可以在多个主机上启动容器,而不必担心数据损坏,这与使用 iSCSI 的情况不同:

root@lxc2:~# lxc-start --name glusterfs_lxc 
root@lxc2:~# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
glusterfs_lxc RUNNING 0 - 10.0.3.184 - 
root@lxc2:~#

由于我们使用 DHCP 动态分配 IP 地址给容器,新的主机上的相同容器会获得一个新的 IP 地址。请注意,连接到容器中运行的 Nginx 仍然会返回相同的结果,因为容器在多个 LXC 节点之间共享其文件系统和配置文件:

root@lxc2:~# curl 10.0.3.184 
Nginx on GlusterFS LXC 
root@lxc2:~#

这种设置在某种程度上实现了热备份,在这种情况下,我们可以使用两个容器通过像 HAProxy 这样的代理服务,第二个节点仅在第一个节点宕机时才会使用,确保任何配置更改立即生效。作为替代方案,可以同时使用两个 LXC 容器,但请注意,它们会写入相同的文件系统,因此在这种情况下,Nginx 日志将由 lxc1lxc2 节点上的两个 LXC 容器写入。

监控和告警 LXC 指标

监控 LXC 容器与监控虚拟机或服务器没有太大区别——我们可以在容器内部运行监控客户端,或者在运行 LXC 的实际主机上运行。由于容器的根文件系统在主机上可见,且 LXC 使用 cgroups 和命名空间,我们可以直接从主机操作系统收集各种信息,如果我们不想在容器中运行监控代理的话。在我们查看两个 LXC 监控示例之前,先来看看如何收集我们可以监控和告警的各种指标。

收集容器指标

LXC 提供了一些简单的工具,用于监控容器的状态和资源利用率。正如你接下来将看到的那样,这些工具提供的信息并不冗长;但是,我们可以利用 cgroup 文件系统从中收集更多的信息。让我们来探索一下这些选项。

使用 lxc-monitor 跟踪容器状态

lxc-monitor 工具可用于跟踪容器的状态变化——例如它们何时启动或停止。

为了演示这一点,打开两个终端;在一个终端创建一个新容器,在另一个终端运行 lxc-monitor 命令。启动容器并观察第二个终端中的输出:

root@ubuntu:~# lxc-create --name monitor_lxc --template ubuntu 
root@ubuntu:~# lxc-start --name monitor_lxc 
root@ubuntu:~# lxc-monitor --name monitor_lxc 
'monitor_lxc' changed state to [STARTING] 
'monitor_lxc' changed state to [RUNNING] 

停止容器并注意状态变化:

root@ubuntu:~# lxc-stop --name monitor_lxc 
root@ubuntu:~# 
'monitor_lxc' changed state to [STARTING] 
'monitor_lxc' changed state to [RUNNING] 
'monitor_lxc' exited with status [0] 
'monitor_lxc' changed state to [STOPPING] 
'monitor_lxc' changed state to [STOPPED] 
root@ubuntu:~# lxc-start --name monitor_lxc

使用 lxc-top 获取 CPU 和内存利用率

lxc-top 工具类似于标准的 top Linux 命令,它显示 CPU、内存和 I/O 的使用情况。要启动它,请执行以下命令:

root@ubuntu:~# lxc-top --name monitor_lxc 
Container  CPU    CPU     CPU    BlkIO    Mem 
Name               Used     Sys    User   
Total    Used 
monitor_lxc        0.52   0.26    0.16    0.00 
12.71 MB 
TOTAL 1 of 1       0.52   0.26    0.16    0.00 
12.71 MB

使用 lxc-info 收集容器信息

我们可以使用 lxc-info 工具定期轮询信息,例如 CPU、内存、I/O 和网络利用率:

root@ubuntu:~# lxc-info --name monitor_lxc 
Name:        monitor_lxc 
State:       RUNNING 
PID:         19967 
IP:          10.0.3.88 
CPU use:     0.53 seconds 
BlkIO use:   0 bytes 
Memory use:  12.74 MiB 
KMem use:    0 bytes 
Link:        veth8OX0PW 
TX bytes:    1.34 KiB 
RX bytes:    1.35 KiB 
Total bytes: 2.69 KiB 
root@ubuntu:~#

利用 cgroups 收集内存指标

第一章,**Linux 容器介绍 中,我们详细探讨了 cgroups,并了解了 LXC 如何在 cgroup 虚拟文件系统中为每个启动的容器创建目录层级。要查找我们之前构建的容器的 cgroup 层级,请运行以下命令:

root@ubuntu:~# find /sys/fs/ -type d -name monitor_lxc 
/sys/fs/cgroup/freezer/lxc/monitor_lxc 
/sys/fs/cgroup/cpuset/lxc/monitor_lxc 
/sys/fs/cgroup/net_cls,net_prio/lxc/monitor_lxc 
/sys/fs/cgroup/devices/lxc/monitor_lxc 
/sys/fs/cgroup/perf_event/lxc/monitor_lxc 
/sys/fs/cgroup/hugetlb/lxc/monitor_lxc 
/sys/fs/cgroup/blkio/lxc/monitor_lxc 
/sys/fs/cgroup/pids/lxc/monitor_lxc 
/sys/fs/cgroup/memory/lxc/monitor_lxc 
/sys/fs/cgroup/cpu,cpuacct/lxc/monitor_lxc 
/sys/fs/cgroup/systemd/lxc/monitor_lxc 
root@ubuntu:~#

我们可以使用内存组中的文件来设置和获取可以监控并告警的指标。例如,要将容器的内存设置为 512 MB,运行以下命令:

root@ubuntu:~# lxc-cgroup --name monitor_lxc memory.limit_in_bytes 536870912 
root@ubuntu:~# 

要读取容器当前的内存利用率,请执行以下命令:

root@ubuntu:~# cat /sys/fs/cgroup/memory/lxc/monitor_lxc/memory.usage_in_bytes 
13361152 
root@ubuntu:~#

要收集有关容器内存的更多信息,请从以下文件读取:

root@ubuntu:~# cat /sys/fs/cgroup/memory/lxc/monitor_lxc/memory.stat 
cache 8794112 
rss 4833280 
rss_huge 0 
mapped_file 520192 
dirty 0 
... 
root@ubuntu:~#

使用 cgroups 收集 CPU 统计数据

要收集 CPU 使用情况,我们可以从 cpuacct cgroup 子系统中读取:

root@ubuntu:~# cat /sys/fs/cgroup/cpu,cpuacct/lxc/monitor_lxc/cpuacct.usage 
627936278 
root@ubuntu:~#

收集网络指标

每个容器在主机上创建一个虚拟接口;之前从 lxc-info 命令显示的接口是 veth8OX0PW。我们可以通过运行以下命令来收集发送和接收的包、错误率等信息:

root@ubuntu:~# ifconfig veth8OX0PW 
veth8OX0PW Link encap:Ethernet HWaddr fe:4d:bf:3f:17:8f 
 inet6 addr: fe80::fc4d:bfff:fe3f:178f/64 Scope:Link 
 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 
 RX packets:125645 errors:0 dropped:0 overruns:0 frame:0 
 TX packets:129909 errors:0 dropped:0 overruns:0 carrier:0 
 collisions:0 txqueuelen:1000 
 RX bytes:20548005 (20.5 MB) TX bytes:116477293 (116.4 MB) 
root@ubuntu:~#

另外,我们可以连接到容器的网络命名空间并通过这种方式获取信息。以下三个命令演示了如何在容器的网络命名空间中执行命令。注意 19967 的 PID;它可以从 lxc-info 命令中获取:

root@ubuntu:~# mkdir -p /var/run/netns 
root@ubuntu:~# ln -sf /proc/19967/ns/net /var/run/netns/monitor_lxc 
root@ubuntu:~# ip netns exec monitor_lxc ip a s 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1 
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 
 inet 127.0.0.1/8 scope host lo 
 valid_lft forever preferred_lft forever 
 inet6 ::1/128 scope host 
 valid_lft forever preferred_lft forever 
17: eth0@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 
 link/ether 00:16:3e:01:b7:8b brd ff:ff:ff:ff:ff:ff link-netnsid 0 
 inet 10.0.3.88/24 brd 10.0.3.255 scope global eth0 
 valid_lft forever preferred_lft forever 
 inet6 fe80::216:3eff:fe01:b78b/64 scope link 
 valid_lft forever preferred_lft forever 
root@ubuntu:~#

请注意,我们可以看到 LXC 容器内部的网络接口,尽管我们是在主机上运行这些命令。

使用 Monit 进行简单的容器监控和告警

现在我们已经演示了如何收集各种监控数据,接下来我们将实际设置一个监控系统并对其进行告警。在本节中,我们将安装一个简单的监控解决方案,利用 monit 守护进程。Monit 是一个易于配置的服务,使用可以基于特定监控事件和阈值自动执行的脚本。

接下来,让我们看一些例子:

  1. 要安装 monit,运行以下命令:

     root@ubuntu:~# apt-get install -y monit mailutils
    
    
  2. 接下来,创建一个最小配置文件。随安装包提供的配置文档写得相当清楚:

     root@ubuntu:~# cd /etc/monit/ 
     root@ubuntu:/etc/monit# cat monitrc | grep -vi "#" 
     set daemon 120 
     set logfile /var/log/monit.log 
     set idfile /var/lib/monit/id 
     set statefile /var/lib/monit/state 
     set eventqueue 
     basedir /var/lib/monit/events 
     slots 100 
     set httpd port 2812 and 
     allow admin:monit 
     include /etc/monit/conf.d/* 
     include /etc/monit/conf-enabled/* 
     root@ubuntu:/etc/monit#
    
    

    前面的配置启动了一个可以通过指定凭证在端口 2812 访问的 Web 界面。它还定义了两个可以读取配置文件的目录。

  3. 接下来,让我们创建一个监控配置,检查容器是否在运行。执行一个脚本,我们将写在下面,来执行实际检查:

     root@ubuntu:/etc/monit# cat conf.d/container_state.cfg 
     check program container_status with path 
          "/etc/monit/container_running.sh 
          monitor_lxc" 
     if status == 1 
     then exec "/bin/bash -c '/etc/monit/alert.sh'" 
     group lxc 
     root@ubuntu:/etc/monit#
    
    

    前面的配置指示 monit 定期运行 container_running.sh 脚本,如果退出状态为 1,则执行一个名为 alert.sh 的第二个脚本来发出警报。很简单。container_running.sh 脚本如下:

     root@ubuntu:/etc/monit# cat container_running.sh 
     #!/bin/bash 
     CONTAINER_NAME=$1 
     CONTAINER_STATE=$(lxc-info --state --name $CONTAINER_NAME 
          | awk '{print $2}') 
     if [ "$CONTAINER_STATE" != "RUNNING" ] 
     then 
     exit 1 
     else 
     exit 0 
     fi 
     root@ubuntu:/etc/monit#
    
    

    我们可以看到我们正在使用 lxc-info 命令来检查容器的状态。alert.sh 脚本更加简单:

     root@ubuntu:/etc/monit# cat alert.sh 
     #!/bin/bash 
     echo "LXC container down" | mail -s "LXC container Alert" 
          youremail@example.com 
     root@ubuntu:/etc/monit#
    
    
  4. 重新加载monit服务并检查我们在配置文件中之前命名的新的监控服务container_status的状态:

     root@ubuntu:/etc/monit# monit reload 
     Reinitializing monit daemon 
     The Monit daemon 5.16 uptime: 15h 43m 
     root@ubuntu:/etc/monit# monit status container_status 
     The Monit daemon 5.16 uptime: 15h 47m 
     Program 'container_status' 
     status Status ok 
     monitoring status Monitored 
     last started Fri, 18 Nov 2016 15:03:24 
     last exit value 0 
     data collected Fri, 18 Nov 2016 15:03:24 
     root@ubuntu:/etc/monit#
    
    

    我们还可以通过端口 2812 连接到网页界面,并查看新定义的监控目标:

    使用 Monit 进行简单容器监控和警报

  5. 让我们停止容器并检查monit的状态:

     root@ubuntu:/etc/monit# lxc-stop --name monitor_lxc 
     root@ubuntu:/etc/monit# monit status container_status 
     The Monit daemon 5.16 uptime: 15h 53m 
     Program 'container_status' 
     status Status failed 
     monitoring status Monitored 
     last started Fri, 18 Nov 2016 15:09:24 
     last exit value 1 
     data collected Fri, 18 Nov 2016 15:09:24 
     root@ubuntu:/etc/monit#
    
    

    使用 Monit 进行简单容器监控和警报

从命令输出和网页界面中注意到,container_status服务的状态现在是failed。由于我们设置了monit,当我们监控的服务失败时会发送电子邮件,检查邮件日志。你应该已经收到了来自monit的电子邮件,这封邮件很可能会被归入你的垃圾邮件文件夹:

root@ubuntu:/etc/monit# tail -5 /var/log/mail.log 
Nov 18 15:13:51 ubuntu postfix/pickup[26069]: 8AB30177CB3: uid=0 from=<root@ubuntu> 
Nov 18 15:13:51 ubuntu postfix/cleanup[31295]: 8AB30177CB3: message-id=<20161118151351.8AB30177CB3@ubuntu> 
Nov 18 15:13:51 ubuntu postfix/qmgr[5392]: 8AB30177CB3: from=<root@ubuntu>, size=340, nrcpt=1 (queue active) 
Nov 18 15:13:51 ubuntu postfix/smtp[31297]: 8AB30177CB3: to=< youremail@example.com >, relay=gmail-smtp-in.l.google.com[74.125.70.26]:25, delay=0.22, delays=0.01/0.01/0.08/0.13, dsn=2.0.0, status=sent (250 2.0.0 OK 1479482031 u74si2324555itu.40 - gsmtp) 
Nov 18 15:13:51 ubuntu postfix/qmgr[5392]: 8AB30177CB3: removed 
root@ubuntu:/etc/monit#

如需更多关于 Monit 的信息,请参见mmonit.com/monit/documentation/monit.html

Monit 是为每个服务器设置 LXC 容器监控的快速简便方法。它是无代理的,且由于 cgroup 层次结构中暴露的指标,可以轻松对各种数据点设置警报,而无需在容器中附加或运行任何额外的组件。

使用 Sensu 进行容器监控和警报触发

Monit 是一个非常适合在分散式环境中进行监控和警报的工具。然而,为了实现更强大且功能丰富的集中式监控部署,可以使用其他监控工具,如 Sensu。使用 Sensu 实现监控有两种主要方式——在每个容器中部署代理,或者在 LXC 主机上使用独立检查,类似于 Monit,从 cgroups 等数据源收集数据。

Sensu 使用客户端-服务器架构,服务器通过 RabbitMQ 提供的消息队列发布检查,客户端订阅该队列中的主题,并根据设定的阈值执行检查和警报。状态和历史数据存储在 Redis 服务器中。

让我们首先展示在 LXC 容器内部的 Sensu 部署,然后再进行无代理监控。

使用 Sensu 代理和服务器监控 LXC 容器

我们需要安装 Sensu 使用的必要服务,包括 Redis 和 RabbitMQ:

  1. 让我们先安装 Redis 服务器,安装完成后,确保它正在运行:

     root@ubuntu:~# apt-get -y install redis-server 
     root@ubuntu:~# redis-cli ping 
     PONG 
     root@ubuntu:~#
    
    
  2. 安装 RabbitMQ 也同样简单:

     root@ubuntu:~# apt-get install -y rabbitmq-server
    
    
  3. 安装完成后,我们需要创建代理将订阅并读取消息的虚拟主机:

     root@ubuntu:~# rabbitmqctl add_vhost /sensu 
     Creating vhost "/sensu" ... 
     root@ubuntu:~#
    
    
  4. 接下来,创建一个用户名和密码来连接到该主题:

     root@ubuntu:~# rabbitmqctl add_user sensu secret 
     Creating user "sensu" ... 
     root@ubuntu:~# rabbitmqctl set_permissions -p 
          /sensu sensu ".*" ".*" ".*" 
     Setting permissions for user "sensu" in vhost "/sensu" ... 
     root@ubuntu:~#
    
    
  5. 现在是安装 Sensu 服务器和客户端的时候了:

     root@ubuntu:~# wget -q 
          https://sensu.global.ssl.fastly.net/apt/pubkey.gpg -O- 
          | sudo apt-key add - 
     OK 
     root@ubuntu:~# echo 
          "deb https://sensu.global.ssl.fastly.net/apt sensu main" 
          | sudo tee /etc/apt/sources.list.d/sensu.list 
     deb https://sensu.global.ssl.fastly.net/apt sensu main 
     root@ubuntu:~# apt-get update 
     root@ubuntu:~# apt-get install -y sensu 
     root@ubuntu:~# cd /etc/sensu/conf.d/
    
    

至少,我们需要五个配置文件,一个是 Sensu API 端点的配置文件,两个指定我们使用的传输方式——在本例中是 RabbitMQ——还有 Sensu 的 Redis 配置文件,以及在同一服务器上运行的 Sensu 客户端的客户端配置文件。以下配置文件基本上不言自明——我们指定 RabbitMQ 和 Redis 服务器的 IP 地址和端口,以及 API 服务:

root@ubuntu:/etc/sensu/conf.d# cat api.json 
{ 
 "api": { 
 "host": "localhost", 
 "bind": "0.0.0.0", 
 "port": 4567 
 } 
} 
root@ubuntu:/etc/sensu/conf.d# 
root@ubuntu:/etc/sensu/conf.d# cat transport.json 
{ 
 "transport": { 
 "name": "rabbitmq", 
 "reconnect_on_error": true 
 } 
} 
root@ubuntu:/etc/sensu/conf.d# 
root@ubuntu:/etc/sensu/conf.d# cat rabbitmq.json 
{ 
 "rabbitmq": { 
 "host": "10.208.129.253", 
 "port": 5672, 
 "vhost": "/sensu", 
 "user": "sensu", 
 "password": "secret" 
 } 
} 
root@ubuntu:/etc/sensu/conf.d# 
root@ubuntu:/etc/sensu/conf.d# cat redis.json 
{ 
 "redis": { 
 "host": "localhost", 
 "port": 6379 
 } 
} 
root@ubuntu:/etc/sensu/conf.d# cat client.json 
{ 
 "client": { 
 "name": "ubuntu", 
 "address": "127.0.0.1", 
 "subscriptions": [ 
 "base" 
 ], 
 "socket": { 
 "bind": "127.0.0.1", 
 "port": 3030 
 } 
 } 
} 
root@ubuntu:/etc/sensu/conf.d#

有关 Sensu 的更多信息,请参阅sensuapp.org/docs/

在启动 Sensu 服务器之前,我们可以安装一个名为 Uchiwa 的基于 Web 的前端:

root@ubuntu:~# apt-get install -y uchiwa

在配置文件中,我们指定 Sensu API 服务的地址和端口——本地主机和端口4567——以及 Uchiwa 监听的端口——3000——在本例中:

root@ubuntu:/etc/sensu/conf.d# cat /etc/sensu/uchiwa.json 
{ 
 "sensu": [ 
 { 
 "name": "LXC Containers", 
 "host": "localhost", 
 "ssl": false, 
 "port": 4567, 
 "path": "", 
 "timeout": 5000 
 } 
 ], 
 "uchiwa": { 
 "port": 3000, 
 "stats": 10, 
 "refresh": 10000 
 } 
} 
root@ubuntu:/etc/sensu/conf.d#

配置完成后,我们来启动 Sensu 服务:

root@ubuntu:~# /etc/init.d/sensu-server start 
* Starting sensu-server [ OK ] 
root@ubuntu:~# /etc/init.d/sensu-api start 
* Starting sensu-api [ OK ] 
root@ubuntu:/etc/sensu/conf.d# /etc/init.d/sensu-client start 
* Starting sensu-client [ OK ] 
root@ubuntu:/etc/sensu/conf.d# 
root@ubuntu:~# /etc/init.d/uchiwa restart 
uchiwa stopped. 
uchiwa started. 
root@ubuntu:~#

现在 Sensu 服务器已经完全配置好,我们需要连接到主机上的一个容器并安装 Sensu 代理:

root@ubuntu:~# lxc-attach --name monitor_lxc 
root@monitor_lxc:~# apt-get install -y wget 
root@monitor_lxc:~# wget -q https://sensu.global.ssl.fastly.net/apt/pubkey.gpg -O- | sudo apt-key add - 
OK 
root@monitor_lxc:~# echo "deb https://sensu.global.ssl.fastly.net/apt sensu main" | sudo tee /etc/apt/sources.list.d/sensu.list 
deb https://sensu.global.ssl.fastly.net/apt sensu main 
root@monitor_lxc:~# apt-get update 
root@monitor_lxc:~# apt-get install -y sensu 
root@monitor_lxc:/# cd /etc/sensu/conf.d/

要配置 Sensu 代理,我们需要编辑客户端配置文件,在其中指定 IP 地址和容器的名称:

root@monitor_lxc:/etc/sensu/conf.d# cat client.json 
{ 
 "client": { 
 "name": "monitor_lxc", 
 "address": "10.0.3.2", 
 "subscriptions": ["base"] 
 } 
} 
root@monitor_lxc:/etc/sensu/conf.d#

我们通过提供之前创建的 IP 地址、端口和凭据,告诉代理如何连接到主机上的 RabbitMQ 服务器:

root@monitor_lxc:/etc/sensu/conf.d# cat rabbitmq.json 
{ 
 "rabbitmq": { 
 "host": "10.0.3.1", 
 "port": 5672, 
 "vhost": "/sensu", 
 "user": "sensu", 
 "password": "secret" 
 } 
} 
root@monitor_lxc:/etc/sensu/conf.d#

然后,指定传输机制:

root@monitor_lxc:/etc/sensu/conf.d# cat transport.json 
{ 
 "transport": { 
 "name": "rabbitmq", 
 "reconnect_on_error": true 
 } 
} 
root@monitor_lxc:/etc/sensu/conf.d#

在前面提到的三个文件配置好之后,我们来启动代理:

root@monitor_lxc:/etc/sensu/conf.d# /etc/init.d/sensu-client start 
* Starting sensu-client [ OK ] 
root@monitor_lxc:/etc/sensu/conf.d#

使用 Sensu 代理和服务器监控 LXC 容器

为了验证所有服务是否正常运行,并且 Sensu 代理能够连接到服务器,我们可以使用主机 IP 连接到 Uchiwa 界面,端口为 3000,如前所述。

仍然连接到 LXC 容器时,让我们安装一个 Sensu 检查。Sensu 检查可以作为 gem 使用,或者可以手动编写。我们来搜索gem库,看看是否有现成的内存检查,而不是自己编写:

root@monitor_lxc:/etc/sensu/conf.d# apt-get install -y rubygems 
root@monitor_lxc:/etc/sensu/conf.d# gem search sensu | grep plugins | grep memory 
sensu-plugins-memory (0.0.2) 
sensu-plugins-memory-checks (1.0.2) 
root@monitor_lxc:/etc/sensu/conf.d#

安装内存检查并重启代理:

root@monitor_lxc:/etc/sensu/conf.d# gem install sensu-plugins-memory-checks 
root@monitor_lxc:/etc/sensu/conf.d# /etc/init.d/sensu-client restart 
configuration is valid 
* Stopping sensu-client [ OK ] 
* Starting sensu-client [ OK ] 
root@monitor_lxc:/etc/sensu/conf.d#

在 Sensu 服务器主机上(而不是容器内),我们需要定义新的内存检查,以便 Sensu 服务器可以告诉代理执行它。我们通过创建一个新的检查文件来完成这一步:

root@ubuntu:/etc/sensu/conf.d# cat check_memory.json
{
  "checks": {
    "memory_check": {
    "command": "/usr/local/bin/check-memory-percent.rb -w 80 -c 90",
    "subscribers": ["base"],
    "handlers": ["default"],
    "interval": 300
   }
  }
}
root@ubuntu:/etc/sensu/conf.d#

我们指定需要从 Sensu 代理运行的检查的路径和名称,在本例中就是我们从gem安装到容器中的 Ruby 脚本。重启 Sensu 服务以使更改生效:

root@ubuntu:/etc/sensu/conf.d# /etc/init.d/sensu-server restart 
configuration is valid 
* Stopping sensu-server [ OK ] 
* Starting sensu-server [ OK ] 
root@ubuntu:/etc/sensu/conf.d# /etc/init.d/sensu-api restart 
configuration is valid 
* Stopping sensu-api [ OK ] 
* Starting sensu-api [ OK ] 
root@ubuntu:/etc/sensu/conf.d# /etc/init.d/uchiwa restart 
Killing uchiwa (pid 15299) with SIGTERM 
Waiting uchiwa (pid 15299) to die... 
Waiting uchiwa (pid 15299) to die... 
uchiwa stopped. 
uchiwa started. 
root@ubuntu:/etc/sensu/conf.d#

查看 Uchiwa 界面,我们可以看到内存检查现在已经激活:

使用 Sensu 代理和服务器监控 LXC 容器

我们可以在 LXC 容器内从 gem 安装多个 Sensu 检查脚本;像在普通服务器或虚拟机上一样,在 Sensu 服务器上定义它们,最终获得一个完整的监控解决方案。

在容器内运行代理时有一些注意事项:

  • 代理会消耗资源;如果我们尝试运行内存占用较小的容器,这可能不是最好的解决方案。

  • 在衡量 CPU 负载时,容器内的负载将反映宿主机本身的总体负载,因为容器并未通过虚拟化程序与宿主机隔离。衡量 CPU 利用率的最佳方法是从 cgroups 获取数据,或者使用宿主机上的lxc-info命令。

  • 如果使用共享根文件系统(如我们在上一章中看到的那样),监控容器内的磁盘空间可能会反映服务器上的总空间。

如果不希望在 LXC 容器内运行 Sensu 代理,我们可以改为在同一主机上的 Sensu 服务器执行独立检查。接下来,我们来探索这种设置。

使用独立 Sensu 检查监控 LXC 容器

在运行 LXC 容器的服务器上,我们可以像本章前面所做的那样,直接在宿主操作系统上安装 Sensu 代理,而不是在每个容器内安装。我们还可以利用 Sensu 的独立检查,它提供了一种去中心化的监控方式,这意味着 Sensu 代理定义并调度检查,而不是由 Sensu 服务器来完成。这为我们提供了一个好处,即无需在容器内安装代理和监控脚本,Sensu 代理会在每个服务器上运行这些检查。

让我们展示如何在我们一直使用的 LXC 主机上创建一个独立的检查:

root@ubuntu:/etc/sensu/conf.d# cat check_memory_no_agent.json 
{ 
 "checks": { 
 "check_memory_no_agent": { 
 "command": "check_memory_no_agent.sh -n monitor_lxc -w 943718400 
      -e 1048576000", 
 "standalone": true, 
 "subscribers": [ 
 "base" 
 ], 
 "interval": 300 
 } 
 } 
} 
root@ubuntu:/etc/sensu/conf.d#

定义独立检查的主要区别在于存在"standalone": true的字段,正如前面的输出所示。在检查配置的命令部分,我们指定执行什么脚本来执行实际检查,以及它应当在什么阈值时发出警报。脚本可以是任何东西,只要它在出现关键警报时以错误代码2退出,警告时以错误代码1退出,所有正常时以0退出。

这是一个非常简单的 bash 脚本,它使用memory.usage_in_bytes cgroup 文件来收集内存使用指标,并在达到指定阈值时发出警报:

root@ubuntu:/etc/sensu/conf.d# cd ../plugins/ 
root@ubuntu:/etc/sensu/plugins# cat check_memory_no_agent.sh 
#!/bin/bash 
# Checks memory usage for an LXC container from cgroups 
 usage() 
{ 
 echo "Usage: `basename $0` -n|--name monitor_lxc -w|--warning 
  5000 -e|--error 10000" 
 exit 2 
} 
sanity_check() 
{ 
 if [ "$CONTAINER_NAME" == "" ] || [ "$WARNING" == "" ] || [ "$ERROR" 
  == "" ] 
 then 
 usage 
 fi 
} 
report_result() 
{ 
 if [ "$MEMORY_USAGE" -ge "$ERROR" ] 
 then 
 echo "CRITICAL - Memory usage too high at $MEMORY_USAGE" 
 exit 2 
 elif [ "$MEMORY_USAGE" -ge "$WARNING" ] 
 then 
 echo "WARNING - Memory usage at $MEMORY_USAGE" 
 exit 1 
 else 
 echo "Memory Usage OK at $MEMROY_USAGE" 
 exit 0 
 fi 
} 
get_memory_usage() 
{ 
 declare -g -i MEMORY_USAGE=0 
 MEMORY_USAGE=$(cat 
  /sys/fs/cgroup/memory/lxc/$CONTAINER_NAME/memory.usage_in_bytes) 
} 
main() 
{ 
 sanity_check 
 get_memory_usage 
 report_result 
} 
while [[ $# > 1 ]] 
do 
 key=$1 
 case $key in 
 -n|--name) 
 CONTAINER_NAME=$2 
 shift 
 ;; 
 -w|--warning) 
 WARNING=$2 
 shift 
 ;; 
 -e|--error) 
 ERROR=$2 
 shift 
 ;; 
 *) 
 usage 
 ;; 
 esac 
 shift 
done 
main 
root@ubuntu:/etc/sensu/plugins#

上述脚本虽然简单,但应该是读者开始编写更多有用的 Sensu 检查的良好起点,这些检查能够操作各种 LXC 指标。通过查看脚本,我们可以看到它有三个基本功能。首先,它在sanitiy_check()函数中检查提供的容器名称,以及警告和错误阈值。然后,它在get_memory_usage()函数中从 cgroup 文件获取内存使用情况(以字节为单位),最后,它在report_result()函数中报告结果,通过返回适当的错误代码,正如之前所描述的那样。

更改脚本的权限和执行标志,重新加载 Sensu 服务,并确保检查在 Uchiwa 中显示:

root@ubuntu:/etc/sensu/plugins# chown sensu:sensu check_memory_no_agent.sh 
root@ubuntu:/etc/sensu/plugins# chmod u+x check_memory_no_agent.sh 
root@ubuntu:/etc/sensu/plugins# echo "sensu ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/sensu 
root@ubuntu:/etc/sensu/plugins# /etc/init.d/sensu-client restart

使用独立 Sensu 检查监控 LXC 容器

与 Monit 类似,Sensu 提供了处理程序,当触发警报时将被触发。这可以是发送电子邮件的自定义脚本,向 PagerDuty 等外部服务打电话等。所有这些都提供了处理 LXC 警报的自动化和主动的方式。

有关 Sensu 处理程序的更多信息,请参阅 sensuapp.org/docs/latest/reference/handlers.html 的文档。

LXC、Jenkins 和 Sensu 的简单自动扩展模式

在 第六章 中,我们探讨了如何通过在多个主机上为 LXC 和 HAProxy 配置更多容器来实现水平扩展服务。在本章中,我们探讨了监控 LXC 容器资源利用率并根据警报触发操作的不同方法。有了这些知识基础,我们现在可以实现一种常用的自动扩展模式,如下图所示:

LXC、Jenkins 和 Sensu 的简单自动扩展模式

此模式使用 Jenkins 作为构建系统,由 Sensu 警报处理程序控制。例如,当运行在 LXC 容器内的 Sensu 代理接收来自 Sensu 服务器的预定检查(例如内存检查)时,它执行脚本并根据配置的警报阈值返回 OKWarningCritical 状态。如果返回 Critical 状态,则配置的 Sensu 处理程序(可以简单到一个 curl 命令)将 API 调用发送到 Jenkins 服务器,后者依次执行预配置的作业。Jenkins 作业可以是选择从主机列表中的主机的脚本,根据一组标准来构建新容器,或者尝试增加提醒的 LXC 容器的可用内存。这是一种最简单的自动扩展设计模式之一,利用监控系统和 Jenkins 等 RESTful 构建服务。

在接下来的章节中,我们将探讨一个完整的 OpenStack 部署,该部署利用智能调度程序在可用内存或已运行的容器数量基础上选择计算主机以部署新的 LXC 容器。

我们已经看过如何在前面的图表中实现大多数组件和交互的示例。让我们快速触及 Jenkins 并设置一个简单的作业,通过远程 REST API 调用时创建新的 LXC 容器。其余部分留给读者自行尝试。

要在 Ubuntu 上安装 Jenkins,请运行以下命令:

root@ubuntu: ~# wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | sudo apt-key add - 
root@ubuntu:~# sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' 
root@ubuntu:~# apt-get update 
root@ubuntu:~# apt-get install jenkins

Jenkins 监听端口 8080。以下两条 iptables 规则将端口 80 转发到端口 8080,使连接更加便捷:

root@ubuntu:~# iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 
root@ubuntu:~# iptables -t nat -I OUTPUT -p tcp -d 127.0.0.1 --dport 80 -j REDIRECT --to-ports 8080

确保将 jenkins 用户添加到 sudoers 文件中:

root@ubuntu:~# cat /etc/sudoers | grep jenkins 
jenkins ALL=(ALL) NOPASSWD:ALL 
root@ubuntu:~#

安装完成后,Jenkins 将启动并可通过 HTTP 访问。打开 Jenkins 网页并粘贴如下文件的内容:

root@ubuntu:~# cat /var/lib/jenkins/secrets/initialAdminPassword 
32702ecf076e439c8d58c51f1247776d 
root@ubuntu:~#

安装推荐的插件后,执行以下操作:

  1. 创建一个名为LXC Provision的新作业。

  2. 构建触发器中,选择远程触发构建(例如,从脚本中),并输入一个随机字符串,例如somesupersecrettoken

  3. 构建部分,点击添加构建步骤并选择执行 shell

  4. 命令窗口中,添加以下简单的 bash 脚本:

          set +x
          echo "Building new LXC container"
          sudo lxc-create --template ubuntu --name $(cat /dev/urandom | 
          tr -cd 'a-z' | head -c 10)
    
  5. 最后,点击保存

Jenkins 作业应该类似于以下屏幕截图:

带有 LXC、Jenkins 和 Sensu 的简单自动扩展模式

要远程触发该作业,运行以下命令,替换用户名、密码和 IP 地址为你主机上配置的:

root@ubuntu:~# curl http://user:password@192.237.167.103:8080/job/LXC%20Provision/build?token=somesupersecrettoken 
root@ubuntu:~#

这将触发 Jenkins 作业,进而创建一个随机名称的新 LXC 容器:

root@ubuntu:~# lxc-ls -f 
NAME STATE AUTOSTART GROUPS IPV4 IPV6 
ifjvdlpvnv STOPPED 0 - - - 
root@ubuntu:~#

现在,只需为检查或一组检查创建一个 Sensu 处理程序(或monit触发脚本),然后可以执行类似前面curl命令的操作。 当然,这个部署只是为了让我们略微了解通过结合 Sensu、Jenkins 和 LXC 实现容器内自动扩展服务的可能性。

总结

在本章中,我们学习了如何使用 Linux 原生工具如tarrsync,以及 LXC 工具如lxc-copy来备份 LXC 容器。我们查看了如何使用 iSCSI 目标作为 LXC 根文件系统和配置文件存储,创建冷备份和热备份的示例。我们还学习了如何使用 GlusterFS 部署共享网络文件系统,以及在同一文件系统上运行多个容器,但在不同主机上的好处。

我们还简要讨论了如何使用 Monit 和 Sensu 等工具监控 LXC 容器的状态、健康状况和资源利用率,并如何触发操作,如运行脚本来响应这些警报。

最后,我们回顾了一种常见的自动扩展模式,通过结合多个工具,根据警报事件自动创建新容器。

在下一章中,我们将查看一个完整的 OpenStack 部署,这将使我们能够利用智能调度器创建 LXC 容器。

第八章:在 OpenStack 中使用 LXC

在前面的章节中,我们探讨了利用 Jenkins、自定义 REST API 和监控工具等工具帮助自动扩展 LXC 容器中运行的服务的常见设计模式示例。在本章中,我们将探讨一种通过使用 OpenStack 在一组服务器上完全自动化地配置 LXC 容器的方法。

OpenStack 是一个云操作系统,允许以集中但模块化和可扩展的方式配置虚拟机、LXC 容器、负载均衡器、数据库以及存储和网络资源。它非常适合管理一组计算资源(服务器),并根据 CPU 负载、内存使用率、虚拟机/容器密度等标准,选择最佳的目标来配置服务。

在本章中,我们将涵盖以下 OpenStack 组件和服务:

  • 部署 Keystone 身份服务,该服务将提供一个集中式的用户和服务目录,并提供一种使用令牌进行身份验证的简单方法

  • 安装 Nova 计算控制器,它将管理一池服务器并在其上配置 LXC 容器

  • 配置 Glance 镜像仓库,该仓库将存储 LXC 镜像

  • 配置 Neutron 网络服务,它将管理计算主机上的 DHCP、DNS 和网络桥接

  • 最后,我们将使用 libvirt OpenStack 驱动程序配置一个 LXC 容器

在 Ubuntu 上部署支持 LXC 的 OpenStack

OpenStack 部署可能由多个组件组成,这些组件通过暴露的 API 或像 RabbitMQ 这样的消息总线相互作用,如下图所示:

在 Ubuntu 上部署支持 LXC 的 OpenStack

在本章中,我们将部署这些组件的最小集——Keystone、Glance、Nova 和 Neutron——这些组件足以配置 LXC 容器,并且仍然能够利用 OpenStack 提供的调度逻辑和可扩展的网络。

在本教程中,我们将使用 Ubuntu Xenial,并且截至本写作时间,使用最新的 Newton OpenStack 版本。那个 OpenStack 版本的名称是什么?

准备主机

为了简化操作,我们将使用单个服务器托管所有服务,最小内存为 16 GB。在生产环境中,常见的做法是将每个服务分离到自己的服务器组中,以便于扩展和高可用性。按照本章中的步骤,您可以通过根据需要更换 IP 地址和主机名,轻松地在多个主机上进行部署。

提示

如果使用多个服务器,您需要确保所有主机的时间已同步,可以使用 ntpd 等服务。

让我们首先确保我们拥有最新的包,并安装包含 Newton OpenStack 版本的仓库:

root@controller:~# apt install -y software-properties-common 
root@controller:~# add-apt-repository cloud-archive:newton 
. . . 
Press [ENTER] to continue or ctrl-c to cancel adding it 
. . . 
OK 
root@controller:~# apt update && apt dist-upgrade -y 
root@controller:~# reboot 
root@controller:~# apt install -y python-openstackclient

确保将服务器名称(在此示例中为controller)添加到/etc/hosts

安装数据库服务

我们将要部署的服务使用 MariaDB 数据库作为其后端存储。通过运行以下命令来安装它:

root@controller:~# apt install -y mariadb-server python-pymysql

最小的配置文件应类似于以下内容:

root@controller:~# cat /etc/mysql/mariadb.conf.d/99-openstack.cnf 
[mysqld] 
bind-address = 10.208.130.36 
default-storage-engine = innodb 
innodb_file_per_table 
max_connections = 4096 
collation-server = utf8_general_ci 
character-set-server = utf8 
root@controller:~#

将服务绑定的 IP 地址替换为您服务器上的地址,然后启动服务并运行将保护安装的脚本:

root@controller:~# service mysql restart 
root@controller:~# mysql_secure_installation

上述命令将提示输入新 root 密码。为简便起见,我们将在本章其余部分使用 lxcpassword 作为所有服务的密码。

验证 MySQL 是否正确设置并能够连接:

root@controller:~# mysql -u root -h 10.208.130.36 -p 
Enter password: [lxcpassword] 
Welcome to the MariaDB monitor. Commands end with; or \g. 
Your MariaDB connection id is 47 
Server version: 10.0.28-MariaDB-0ubuntu0.16.04.1 Ubuntu 16.04 
Copyright (c) 2000, 2016, Oracle, MariaDB Corporation Ab and others. 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 
MariaDB [(none)]> [quit]

安装消息队列服务

OpenStack 支持以下消息队列——RabbitMQ、Qpid 和 ZeroMQ——它们促进了服务之间的进程间通信。我们将使用 RabbitMQ:

root@controller:~# apt install -y rabbitmq-server

添加新用户和密码:

root@controller:~# rabbitmqctl add_user openstack lxcpassword 
Creating user "openstack" ... 
root@controller:~# 

同时,授予该用户权限:

root@controller:~# rabbitmqctl set_permissions openstack ".*" ".*" ".*" 
Setting permissions for user "openstack" in vhost "/" ... 
root@controller:~#

安装缓存服务

Keystone 身份服务使用 Memcached 来缓存身份验证令牌。要安装它,请执行以下命令:

root@controller:~# apt install -y memcached python-memcache

将本地主机地址替换为您服务器的 IP 地址:

root@controller:~# sed -i 's/127.0.0.1/10.208.130.36/g' /etc/memcached.conf

配置文件应类似于以下内容:

root@controller:~# cat /etc/memcached.conf | grep -vi "#" | sed '/^$/d' 
-d 
logfile /var/log/memcached.log 
-m 64 
-p 11211 
-u memcache 
-l 10.208.130.36 
root@controller:~# service memcached restart

安装和配置身份服务

Keystone 身份服务为管理其余 OpenStack 组件的身份验证和授权提供了集中式的管理点。Keystone 还维护着一个服务目录,记录了各个服务及其提供的端点,用户可以通过查询该目录来定位这些服务。

要部署 Keystone,首先创建一个数据库并授予 keystone 用户权限:

root@controller:~# mysql -u root -plxcpassword 
MariaDB [(none)]> CREATE DATABASE keystone; 
Query OK, 1 row affected (0.01 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.00 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.01 sec) 
MariaDB [(none)]> exit 
Bye 
root@controller:~#

接下来,安装身份服务组件:

root@controller:~# apt install -y keystone

以下是 Keystone 的最小工作配置:

root@controller:~# cat /etc/keystone/keystone.conf 
[DEFAULT] 
log_dir = /var/log/keystone 
[assignment] 
[auth] 
[cache] 
[catalog] 
[cors] 
[cors.subdomain] 
[credential] 
[database] 
connection = mysql+pymysql://keystone:lxcpassword@controller/keystone 
[domain_config] 
[endpoint_filter] 
[endpoint_policy] 
[eventlet_server] 
[federation] 
[fernet_tokens] 
[identity] 
[identity_mapping] 
[kvs] 
[ldap] 
[matchmaker_redis] 
[memcache] 
[oauth1] 
[os_inherit] 
[oslo_messaging_amqp] 
[oslo_messaging_notifications] 
[oslo_messaging_rabbit] 
[oslo_messaging_zmq] 
[oslo_middleware] 
[oslo_policy] 
[paste_deploy] 
[policy] 
[profiler] 
[resource] 
[revoke] 
[role] 
[saml] 
[security_compliance] 
[shadow_users] 
[signing] 
[token] 
provider = fernet 
[tokenless_auth] 
[trust] 
[extra_headers] 
Distribution = Ubuntu 
root@controller:~#

如果您使用的是与本教程相同的主机名和密码,则无需更改。

接下来,通过运行以下命令填充 keystone 数据库:

root@controller:~# su -s /bin/sh -c "keystone-manage db_sync" keystone 
... 
root@controller:~#

Keystone 使用令牌来验证和授权用户及服务。提供了不同的令牌格式,例如 UUID、PKI 和 Fernet 令牌。在这个示例部署中,我们将使用 Fernet 令牌,它与其他类型不同,不需要在后端持久化。要初始化 Fernet 密钥库,请运行以下命令:

root@controller:~# keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone 
root@controller:~# keystone-manage credential_setup --keystone-user keystone --keystone-group keystone 
root@controller:~#

注意

欲了解有关可用身份令牌的更多信息,请参阅 docs.openstack.org/admin-guide/identity-tokens.html

执行以下命令进行基本的引导过程:

root@controller:~# keystone-manage bootstrap --bootstrap-password lxcpassword --bootstrap-admin-url http://controller:35357/v3/ --bootstrap-internal-url http://controller:35357/v3/ --bootstrap-public-url http://controller:5000/v3/ --bootstrap-region-id RegionOne 
root@controller:~#

我们将使用 Apache 和 WSGI 模块来驱动 Keystone。在 Apache 配置文件中添加以下配置段并重新启动:

root@controller:~# cat /etc/apache2/apache2.conf 
... 
ServerName controller 
... 
root@controller:~# service apache2 restart 

删除 Keystone 默认随附的 SQLite 数据库:

root@controller:~# rm -f /var/lib/keystone/keystone.db

让我们通过定义以下环境变量来创建管理员账户:

root@controller:~# export OS_USERNAME=admin 
root@controller:~# export OS_PASSWORD=lxcpassword 
root@controller:~# export OS_PROJECT_NAME=admin 
root@controller:~# export OS_USER_DOMAIN_NAME=default 
root@controller:~# export OS_PROJECT_DOMAIN_NAME=default 
root@controller:~# export OS_AUTH_URL=http://controller:35357/v3 
root@controller:~# export OS_IDENTITY_API_VERSION=3

现在是时候在 Keystone 中创建我们的第一个项目了。项目代表一个所有权单位,其中的所有资源都归项目所有。我们接下来要创建的 service 项目将用于我们在本章中将要部署的所有服务:

root@controller:~# openstack project create --domain default --description "LXC Project" service 
+-------------+----------------------------------+ 
| Field | Value | 
+-------------+----------------------------------+ 
| description | LXC Project | 
| domain_id | default | 
| enabled | True | 
| id | 9a1a863fe41b42b2955b313f2cca0ef0 | 
| is_domain | False | 
| name | service | 
| parent_id | default | 
+-------------+----------------------------------+ 
root@controller:~#

要列出可用的项目,请运行以下命令:

root@controller:~# openstack project list 
+----------------------------------+---------+ 
| ID | Name | 
+----------------------------------+---------+ 
| 06f4e2d7e384474781803395b24b3af2 | admin | 
| 9a1a863fe41b42b2955b313f2cca0ef0 | service | 
+----------------------------------+---------+ 
root@controller:~#

让我们创建一个普通的项目和用户,普通用户可以使用该项目,而不是 OpenStack 服务:

root@controller:~# openstack project create --domain default --description "LXC User Project" lxc 
+-------------+----------------------------------+ 
| Field | Value | 
+-------------+----------------------------------+ 
| description | LXC User Project | 
| domain_id | default | 
| enabled | True | 
| id | eb9cdc2c2b4e4f098f2d104752970d52 | 
| is_domain | False | 
| name | lxc | 
| parent_id | default | 
+-------------+----------------------------------+ 
root@controller:~# 
root@controller:~# openstack user create --domain default --password-prompt lxc 
User Password: 
Repeat User Password: 
+---------------------+----------------------------------+ 
| Field | Value | 
+---------------------+----------------------------------+ 
| domain_id | default | 
| enabled | True | 
| id | 1e83e0c8ca194f2e9d8161eb61d21030 | 
| name | lxc | 
| password_expires_at | None | 
+---------------------+----------------------------------+ 
root@controller:~#

接下来,创建一个 user 角色,并将其与我们在前两步中创建的 lxc 项目和用户关联:

root@controller:~# openstack role create user 
+-----------+----------------------------------+ 
| Field | Value | 
+-----------+----------------------------------+ 
| domain_id | None | 
| id | 331c0b61e9784112874627264f03a058 | 
| name | user | 
+-----------+----------------------------------+ 
root@controller:~# openstack role add --project lxc --user lxc user 
root@controller:~#

使用以下文件来配置 Keystone 的 Web 服务网关接口 (WSGI) 中间件管道:

root@controller:~# cat /etc/keystone/keystone-paste.ini 
# Keystone PasteDeploy configuration file. 
[filter:debug] 
use = egg:oslo.middleware#debug 
[filter:request_id] 
use = egg:oslo.middleware#request_id 
[filter:build_auth_context] 
use = egg:keystone#build_auth_context 
[filter:token_auth] 
use = egg:keystone#token_auth 
[filter:admin_token_auth] 
use = egg:keystone#admin_token_auth 
[filter:json_body] 
use = egg:keystone#json_body 
[filter:cors] 
use = egg:oslo.middleware#cors 
oslo_config_project = keystone 
[filter:http_proxy_to_wsgi] 
use = egg:oslo.middleware#http_proxy_to_wsgi 
[filter:ec2_extension] 
use = egg:keystone#ec2_extension 
[filter:ec2_extension_v3] 
use = egg:keystone#ec2_extension_v3 
[filter:s3_extension] 
use = egg:keystone#s3_extension 
[filter:url_normalize] 
use = egg:keystone#url_normalize 
[filter:sizelimit] 
use = egg:oslo.middleware#sizelimit 
[filter:osprofiler] 
use = egg:osprofiler#osprofiler 
[app:public_service] 
use = egg:keystone#public_service 
[app:service_v3] 
use = egg:keystone#service_v3 
[app:admin_service] 
use = egg:keystone#admin_service 
[pipeline:public_api] 
pipeline = cors sizelimit http_proxy_to_wsgi osprofiler url_normalize request_id build_auth_context token_auth json_body ec2_extension public_service 
[pipeline:admin_api] 
pipeline = cors sizelimit http_proxy_to_wsgi osprofiler url_normalize request_id build_auth_context token_auth json_body ec2_extension s3_extension admin_service 
[pipeline:api_v3] 
pipeline = cors sizelimit http_proxy_to_wsgi osprofiler url_normalize request_id build_auth_context token_auth json_body ec2_extension_v3 s3_extension service_v3 
[app:public_version_service] 
use = egg:keystone#public_version_service 
[app:admin_version_service] 
use = egg:keystone#admin_version_service 
[pipeline:public_version_api] 
pipeline = cors sizelimit osprofiler url_normalize public_version_service 
[pipeline:admin_version_api] 
pipeline = cors sizelimit osprofiler url_normalize admin_version_service 
[composite:main] 
use = egg:Paste#urlmap 
/v2.0 = public_api 
/v3 = api_v3 
/ = public_version_api 
[composite:admin] 
use = egg:Paste#urlmap 
/v2.0 = admin_api 
/v3 = api_v3 
/ = admin_version_api 
root@controller:~#

让我们测试到目前为止的配置,通过请求 admin 用户和 lxc 用户的令牌:

root@controller:~# openstack --os-auth-url http://controller:35357/v3 --os-project-domain-name default --os-user-domain-name default --os-project-name admin --os-username admin token issue 
Password: 
... 
root@controller:~# openstack --os-auth-url http://controller:5000/v3 --os-project-domain-name default --os-user-domain-name default --os-project-name lxc --os-username lxc token issue 
Password: 
... 
root@controller:~#

我们可以创建两个文件,分别包含我们之前配置的 adminuser 凭证:

root@controller:~# cat rc.admin 
export OS_PROJECT_DOMAIN_NAME=default 
export OS_USER_DOMAIN_NAME=default 
export OS_PROJECT_NAME=admin 
export OS_USERNAME=admin 
export OS_PASSWORD=lxcpassword 
export OS_AUTH_URL=http://controller:35357/v3 
export OS_IDENTITY_API_VERSION=3 
export OS_IMAGE_API_VERSION=2 
root@controller:~# 
root@controller:~# cat rc.lxc 
export OS_PROJECT_DOMAIN_NAME=default 
export OS_USER_DOMAIN_NAME=default 
export OS_PROJECT_NAME=lxc 
export OS_USERNAME=lxc 
export OS_PASSWORD=lxcpassword 
export OS_AUTH_URL=http://controller:5000/v3 
export OS_IDENTITY_API_VERSION=3 
export OS_IMAGE_API_VERSION=2 
root@controller:~#

例如,要使用 admin 用户,请按如下方式加载文件:

root@controller:~# . rc.admin

注意新的环境变量:

root@controller:~# env | grep ^OS 
OS_USER_DOMAIN_NAME=default 
OS_IMAGE_API_VERSION=2 
OS_PROJECT_NAME=admin 
OS_IDENTITY_API_VERSION=3 
OS_PASSWORD=lxcpassword 
OS_AUTH_URL=http://controller:35357/v3 
OS_USERNAME=admin 
OS_PROJECT_DOMAIN_NAME=default 
root@controller:~#

在加载了管理员凭证后,让我们请求一个认证令牌,之后可以与其他 OpenStack 服务一起使用:

root@controller:~# openstack token issue 
+------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 
| Field | Value | 
+------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 
| expires | 2016-12-02 19:49:07+00:00 | 
| id | gAAAAABYQcIj7eKEfCMTWY43EXZbqZ8UdeZ8CZIb2l2sqIFHFBV_bv6LHO4CFbFLdh7kUEw_Zk-MzQrl5mbq7g8RXPAZ31iBDpDie2-xIAMgRqsxkAh7PJ2kdhcHAxkBj-Uq65rHmjYmPZTUlUTONOP3_dId0_8DsdLkFWoardxG0FAotrlH-2s | 
| project_id | 06f4e2d7e384474781803395b24b3af2 | 
| user_id | 5c331f397597439faef5a1199cdf354f | 
+------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 
root@controller:~#

安装和配置镜像服务

镜像服务为用户提供一个 API,用于发现、注册和获取虚拟机镜像,或可作为 LXC 容器根文件系统的镜像。Glance 支持多种存储后端,但为了简便起见,我们将使用文件存储,它将 LXC 镜像直接保存在文件系统中。

要部署 Glance,首先创建一个数据库和一个用户,就像我们为 Keystone 所做的那样:

root@controller:~# mysql -u root -plxcpassword 
MariaDB [(none)]> CREATE DATABASE glance; 
Query OK, 1 row affected (0.00 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'localhost' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.00 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'%' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.00 sec) 
MariaDB [(none)]> exit 
Bye 
root@controller:~#

接下来,创建 glance 用户并将其添加到 admin 角色中:

root@controller:~# openstack user create --domain default --password-prompt glance 
User Password: 
Repeat User Password: 
+---------------------+----------------------------------+ 
| Field | Value | 
+---------------------+----------------------------------+ 
| domain_id | default | 
| enabled | True | 
| id | ce29b972845d4d77978b7e7803275d53 | 
| name | glance | 
| password_expires_at | None | 
+---------------------+----------------------------------+ 
root@controller:~# openstack role add --project service --user glance admin 
root@controller:~#

现在是创建 glance 服务记录的时候了:

root@controller:~# openstack service create --name glance --description "OpenStack Image" image 
+-------------+----------------------------------+ 
| Field | Value | 
+-------------+----------------------------------+ 
| description | OpenStack Image | 
| enabled | True | 
| id | 2aa82fc0a0224baab8d259e4f5279907 | 
| name | glance | 
| type | image | 
+-------------+----------------------------------+ 
root@controller:~#

在 Keystone 中创建 Glance API 端点:

root@controller:~# openstack endpoint create --region RegionOne image public http://controller:9292 
+--------------+----------------------------------+ 
| Field | Value | 
+--------------+----------------------------------+ 
| enabled | True | 
| id | aa26c33d456d421ca3555e6523c7814f | 
| interface | public | 
| region | RegionOne | 
| region_id | RegionOne | 
| service_id | 2aa82fc0a0224baab8d259e4f5279907 | 
| service_name | glance | 
| service_type | image | 
| url | http://controller:9292 | 
+--------------+----------------------------------+ 
root@controller:~#

OpenStack 支持多区域部署以实现高可用性;然而,为了简单起见,我们将所有服务部署在同一区域:

root@controller:~# openstack endpoint create --region RegionOne image internal http://controller:9292 
... 
root@controller:~# openstack endpoint create --region RegionOne image admin http://controller:9292 
... 
root@controller:~#

既然 Keystone 已经知道了 glance 服务,让我们安装它:

root@controller:~# apt install -y glance

使用以下两个最小配置文件,根据需要替换密码和主机名:

root@controller:~# cat /etc/glance/glance-api.conf 
[DEFAULT] 
[cors] 
[cors.subdomain] 
[database] 
connection = mysql+pymysql://glance:lxcpassword@controller/glance 
[glance_store] 
stores = file,http 
default_store = file 
filesystem_store_datadir = /var/lib/glance/images/ 
[image_format] 
disk_formats = ami,ari,aki,vhd,vhdx,vmdk,raw,qcow2,vdi,iso,root-tar 
[keystone_authtoken] 
auth_uri = http://controller:5000 
auth_url = http://controller:35357 
memcached_servers = controller:11211 
auth_type = password 
project_domain_name = default 
user_domain_name = default 
project_name = service 
username = glance 
password = lxcpassword 
[matchmaker_redis] 
[oslo_concurrency] 
[oslo_messaging_amqp] 
[oslo_messaging_notifications] 
[oslo_messaging_rabbit] 
[oslo_messaging_zmq] 
[oslo_middleware] 
[oslo_policy] 
[paste_deploy] 
flavor = keystone 
[profiler] 
[store_type_location_strategy] 
[task] 
[taskflow_executor] 
root@controller:~# 
root@controller:~# cat /etc/glance/glance-registry.conf 
[DEFAULT] 
[database] 
connection = mysql+pymysql://glance:GLANCE_DBPASS@controller/glance 
[keystone_authtoken] 
auth_uri = http://controller:5000 
auth_url = http://controller:35357 
memcached_servers = controller:11211 
auth_type = password 
project_domain_name = default 
user_domain_name = default 
project_name = service 
username = glance 
password = lxcpassword 
[matchmaker_redis] 
[oslo_messaging_amqp] 
[oslo_messaging_notifications] 
[oslo_messaging_rabbit] 
[oslo_messaging_zmq] 
[oslo_policy] 
[paste_deploy] 
flavor = keystone 
[profiler] 
root@controller:~#

通过运行以下命令填充 glance 数据库:

root@controller:~# su -s /bin/sh -c "glance-manage db_sync" glance 
... 
root@controller:~#

启动 Glance 服务:

root@controller:~# service glance-registry restart 
root@controller:~# service glance-api restart 
root@controller:~#

如我们在第二章 安装和运行 LXC 在 Linux 系统上 中看到的,我们可以手动为 LXC 容器构建镜像,或者从 Ubuntu 仓库下载预构建的镜像。让我们下载一个镜像并提取它:

root@controller:~# wget http://uec-images.ubuntu.com/releases/precise/release/ubuntu-12.04-server-cloudimg-amd64.tar.gz 
root@controller:~# tar zxfv ubuntu-12.04-server-cloudimg-amd64.tar.gz 
precise-server-cloudimg-amd64.img 
precise-server-cloudimg-amd64-vmlinuz-virtual 
precise-server-cloudimg-amd64-loader 
precise-server-cloudimg-amd64-floppy 
README.files 
root@controller:~#

包含根文件系统的文件具有 .img 扩展名。让我们将其添加到镜像服务中:

root@controller:~# openstack image create "lxc_ubuntu_12.04" --file precise-server-cloudimg-amd64.img --disk-format raw --container-format bare --public 
+------------------+------------------------------------------------------+ 
| Field | Value | 
+------------------+------------------------------------------------------+ 
| checksum | b5e5895e85127d9cebbd2de32d9b193c | 
| container_format | bare | 
| created_at | 2016-12-02T19:01:28Z | 
| disk_format | raw | 
| file | /v2/images/f344646d-d293-4638-bab4-86d461f38233/file | 
| id | f344646d-d293-4638-bab4-86d461f38233 | 
| min_disk | 0 | 
| min_ram | 0 | 
| name | lxc_ubuntu_12.04 | 
| owner | 06f4e2d7e384474781803395b24b3af2 | 
| protected | False | 
| schema | /v2/schemas/image | 
| size | 1476395008 | 
| status | active | 
| tags | | 
| updated_at | 2016-12-02T19:01:41Z | 
| virtual_size | None | 
| visibility | public | 
+------------------+------------------------------------------------------+ 
root@controller:~#

请注意,LXC 使用 raw 磁盘和 bare 容器格式。

镜像现在存储在 glance-api.conf 中定义的 filesystem_store_datadir 位置,如我们在前面的配置示例中看到的:

root@controller:~# ls -la /var/lib/glance/images/ 
total 1441804 
drwxr-xr-x 2 glance glance 4096 Dec 2 19:01 . 
drwxr-xr-x 4 glance glance 4096 Dec 2 18:53 .. 
-rw-r----- 1 glance glance 1476395008 Dec 2 19:01 f344646d-d293-4638-bab4-86d461f38233 
root@controller:~#

让我们列出 Glance 中可用的镜像:

root@controller:~# openstack image list 
+--------------------------------------+------------------+--------+ 
| ID | Name | Status | 
+--------------------------------------+------------------+--------+ 
| f344646d-d293-4638-bab4-86d461f38233 | lxc_ubuntu_12.04 | active | 
+--------------------------------------+------------------+--------+ 
root@controller:~#

安装和配置计算服务

OpenStack 计算服务管理一个计算资源池(服务器)和在这些资源上运行的各种虚拟机或容器。它提供一个调度服务,接收来自队列的创建新虚拟机或容器的请求,并决定在哪个计算主机上创建并启动它。

有关各种 Nova 服务的更多信息,请参考:docs.openstack.org/developer/nova/

让我们从创建nova数据库和用户开始:

root@controller:~# mysql -u root -plxcpassword 
MariaDB [(none)]> CREATE DATABASE nova_api; 
Query OK, 1 row affected (0.00 sec) 
MariaDB [(none)]> CREATE DATABASE nova; 
Query OK, 1 row affected (0.00 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON nova_api.* TO 'nova'@'localhost' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.03 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON nova_api.* TO 'nova'@'%' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.00 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'localhost' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.00 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'%' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.00 sec) 
MariaDB [(none)]> exit 
Bye 
root@controller:~#

一旦数据库创建并授权了用户权限,创建nova用户并将其添加到身份服务中的admin角色:

root@controller:~# openstack user create --domain default --password-prompt nova 
User Password: 
Repeat User Password: 
+---------------------+----------------------------------+ 
| Field | Value | 
+---------------------+----------------------------------+ 
| domain_id | default | 
| enabled | True | 
| id | a1305c903548431a80b608fadf78f287 | 
| name | nova | 
| password_expires_at | None | 
+---------------------+----------------------------------+ 
root@controller:~# openstack role add --project service --user nova admin 
root@controller:~#

接下来,创建nova服务和端点:

root@controller:~# openstack service create --name nova --description "OpenStack Compute" compute 
+-------------+----------------------------------+ 
| Field | Value | 
+-------------+----------------------------------+ 
| description | OpenStack Compute | 
| enabled | True | 
| id | 779d2feb591545cf9d2acc18765a0ca5 | 
| name | nova | 
| type | compute | 
+-------------+----------------------------------+ 
root@controller:~# openstack endpoint create --region RegionOne compute public http://controller:8774/v2.1/%\(tenant_id\)s 
+--------------+-------------------------------------------+ 
| Field | Value | 
+--------------+-------------------------------------------+ 
| enabled | True | 
| id | cccfd4d817a24f9ba58128901cbbb473 | 
| interface | public | 
| region | RegionOne | 
| region_id | RegionOne | 
| service_id | 779d2feb591545cf9d2acc18765a0ca5 | 
| service_name | nova | 
| service_type | compute | 
| url | http://controller:8774/v2.1/%(tenant_id)s | 
+--------------+-------------------------------------------+ 
root@controller:~# openstack endpoint create --region RegionOne compute internal http://controller:8774/v2.1/%\(tenant_id\)s 
... 
root@controller:~# openstack endpoint create --region RegionOne compute admin http://controller:8774/v2.1/%\(tenant_id\)s 
... 
root@controller:~#

现在是时候安装提供 API、调度器、控制台和调度服务的 Nova 软件包了:

root@controller:~# apt install -y nova-api nova-conductor nova-consoleauth nova-novncproxy nova-scheduler

我们刚刚安装的 Nova 软件包提供以下服务:

  • nova-api:该服务通过 RESTful API 接受并响应用户请求。我们使用它来创建、运行和停止实例等等。

  • nova-conductor:该服务位于我们之前创建的nova数据库与运行在计算节点上的nova-compute服务之间,后者负责创建虚拟机和容器。我们将在本章后续部分安装该服务。

  • nova-consoleauth:该服务为希望通过各种控制台连接到虚拟机或容器的用户授权令牌。

  • nova-novncproxy:该服务提供对运行 VNC 的实例的访问。

  • nova-scheduler:如前所述,该服务负责决定在哪里部署虚拟机或 LXC 容器。

以下是一个最小功能的 Nova 配置:

root@controller:~# cat /etc/nova/nova.conf 
[DEFAULT] 
dhcpbridge_flagfile=/etc/nova/nova.conf 
dhcpbridge=/usr/bin/nova-dhcpbridge 
log-dir=/var/log/nova 
state_path=/var/lib/nova 
force_dhcp_release=True 
verbose=True 
ec2_private_dns_show_ip=True 
enabled_apis=osapi_compute,metadata 
transport_url = rabbit://openstack:lxcpassword@controller 
auth_strategy = keystone 
my_ip = 10.208.132.45 
use_neutron = True 
firewall_driver = nova.virt.firewall.NoopFirewallDriver 
[database] 
connection = mysql+pymysql://nova:lxcpassword@controller/nova 
[api_database] 
connection = mysql+pymysql://nova:lxcpassword@controller/nova_api 
[oslo_concurrency] 
lock_path = /var/lib/nova/tmp 
[libvirt] 
use_virtio_for_bridges=True 
[wsgi] 
api_paste_config=/etc/nova/api-paste.ini 
[keystone_authtoken] 
auth_uri = http://controller:5000 
auth_url = http://controller:35357 
memcached_servers = controller:11211 
auth_type = password 
project_domain_name = default 
user_domain_name = default 
project_name = service 
username = nova 
password = lxcpassword 
[vnc] 
vncserver_listen = $my_ip 
vncserver_proxyclient_address = $my_ip 
[glance] 
api_servers = http://controller:9292 
root@controller:~#

配置文件就绪后,我们现在可以填充nova数据库:

root@controller:~# su -s /bin/sh -c "nova-manage api_db sync" nova 
... 
root@controller:~# su -s /bin/sh -c "nova-manage db sync" nova 
... 
root@controller:~#

最后,启动计算服务:

root@controller:~# service nova-api restart 
root@controller:~# service nova-consoleauth restart 
root@controller:~# service nova-scheduler restart 
root@controller:~# service nova-conductor restart 
root@controller:~# service nova-novncproxy restart 
root@controller:~#

由于我们将使用单节点进行此 OpenStack 部署,因此需要安装nova-compute服务。在生产环境中,我们通常有一个计算服务器池,只运行该服务。

root@controller:~# apt install -y nova-compute

使用以下最小配置文件,该文件允许在同一服务器上运行nova-compute及其余 Nova 服务:

root@controller:~# cat /etc/nova/nova.conf 
[DEFAULT] 
dhcpbridge_flagfile=/etc/nova/nova.conf 
dhcpbridge=/usr/bin/nova-dhcpbridge 
log-dir=/var/log/nova 
state_path=/var/lib/nova 
force_dhcp_release=True 
verbose=True 
ec2_private_dns_show_ip=True 
enabled_apis=osapi_compute,metadata 
transport_url = rabbit://openstack:lxcpassword@controller 
auth_strategy = keystone 
my_ip = 10.208.132.45 
use_neutron = True 
firewall_driver = nova.virt.firewall.NoopFirewallDriver 
compute_driver = libvirt.LibvirtDriver 
[database] 
connection = mysql+pymysql://nova:lxcpassword@controller/nova 
[api_database] 
connection = mysql+pymysql://nova:lxcpassword@controller/nova_api 
[oslo_concurrency] 
lock_path = /var/lib/nova/tmp 
[libvirt] 
use_virtio_for_bridges=True 
[wsgi] 
api_paste_config=/etc/nova/api-paste.ini 
[keystone_authtoken] 
auth_uri = http://controller:5000 
auth_url = http://controller:35357 
memcached_servers = controller:11211 
auth_type = password 
project_domain_name = default 
user_domain_name = default 
project_name = service 
username = nova 
password = lxcpassword 
[vnc] 
enabled = True 
vncserver_listen = $my_ip 
vncserver_proxyclient_address = $my_ip 
novncproxy_base_url = http://controller:6080/vnc_auto.html 
[glance] 
api_servers = http://controller:9292 
[libvirt] 
virt_type = lxc 
root@controller:~#

请注意,在libvirt部分下,我们如何指定 LXC 作为默认的虚拟化类型。要在 Nova 中启用 LXC 支持,安装以下软件包:

root@controller:~# apt install -y nova-compute-lxc

该软件包提供以下配置文件:

root@controller:~# cat /etc/nova/nova-compute.conf 
[DEFAULT] 
compute_driver=libvirt.LibvirtDriver 
[libvirt] 
virt_type=lxc 
root@controller:~#

重启nova-compute服务并列出所有可用的 Nova 服务:

root@controller:~# service nova-compute restart 
root@controller:~# openstack compute service list 
+----+------------------+------------+----------+---------+-------+----------------------------+ 
| ID | Binary | Host | Zone | Status | State | Updated At | 
+----+------------------+------------+----------+---------+-------+----------------------------+ 
| 4 | nova-consoleauth | controller | internal | enabled | up | 2016-12-02T20:01:19.000000 | 
| 5 | nova-scheduler | controller | internal | enabled | up | 2016-12-02T20:01:25.000000 | 
| 6 | nova-conductor | controller | internal | enabled | up | 2016-12-02T20:01:26.000000 | 
| 8 | nova-compute | controller | nova | enabled | up | 2016-12-02T20:01:22.000000 | 
+----+------------------+------------+----------+---------+-------+----------------------------+ 
root@controller:~#

确保验证所有四个服务都已启用并处于运行状态。配置并运行所有 Nova 服务后,现在是时候进入部署的网络部分了。

安装和配置网络服务

OpenStack 的网络组件,代号 Neutron,管理网络、IP 地址、软件桥接和路由。在前面的章节中,我们需要创建 Linux 桥接,向其添加端口,配置 DHCP 为容器分配 IP 地址等等。Neutron 通过方便的 API 和库暴露了所有这些功能,我们可以利用它们。

让我们从创建数据库、用户和权限开始:

root@controller:~# mysql -u root -plxcpassword 
MariaDB [(none)]> CREATE DATABASE neutron; 
Query OK, 1 row affected (0.00 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'localhost' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.00 sec) 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'%' IDENTIFIED BY 'lxcpassword'; 
Query OK, 0 rows affected (0.00 sec) 
MariaDB [(none)]> exit 
Bye 
root@controller:~#

接下来,创建neutron用户并将其添加到 Keystone 中的admin角色:

root@controller:~# openstack user create --domain default --password-prompt neutron 
User Password: 
Repeat User Password: 
+---------------------+----------------------------------+ 
| Field | Value | 
+---------------------+----------------------------------+ 
| domain_id | default | 
| enabled | True | 
| id | 68867f6864574592b1a29ec293defb5d | 
| name | neutron | 
| password_expires_at | None | 
+---------------------+----------------------------------+ 
root@controller:~# openstack role add --project service --user neutron admin 
root@controller:~#

创建neutron服务和端点:

root@controller:~# openstack service create --name neutron --description "OpenStack Networking" network 
+-------------+----------------------------------+ 
| Field | Value | 
+-------------+----------------------------------+ 
| description | OpenStack Networking | 
| enabled | True | 
| id | 8bd98d58bfb5410694cbf7b6163a71a5 | 
| name | neutron | 
| type | network | 
+-------------+----------------------------------+ 
root@controller:~# openstack endpoint create --region RegionOne network public http://controller:9696 
+--------------+----------------------------------+ 
| Field | Value | 
+--------------+----------------------------------+ 
| enabled | True | 
| id | 4e2c1a8689b146a7b3b4207c63a778da | 
| interface | public | 
| region | RegionOne | 
| region_id | RegionOne | 
| service_id | 8bd98d58bfb5410694cbf7b6163a71a5 | 
| service_name | neutron | 
| service_type | network | 
| url | http://controller:9696 | 
+--------------+----------------------------------+ 
root@controller:~# openstack endpoint create --region RegionOne network internal http://controller:9696 
... 
root@controller:~# openstack endpoint create --region RegionOne network admin http://controller:9696 
... 
root@controller:~#

在身份服务中定义了所有服务和端点后,安装以下软件包:

root@controller:~# apt install -y neutron-server neutron-plugin-ml2 neutron-linuxbridge-agent neutron-l3-agent neutron-dhcp-agent neutron-metadata-agent

我们之前安装的 Neutron 包提供以下服务:

  • neutron-server:该包提供 API,用于动态请求和配置虚拟网络

  • neutron-plugin-ml2:这是一个框架,允许使用各种网络技术,如 Linux 桥接、Open vSwitch、GRE 和 VXLAN,这些我们在前面的章节中已看到

  • neutron-linuxbridge-agent:该服务提供 Linux 桥接插件代理

  • neutron-l3-agent:通过创建虚拟路由器,该服务在软件定义的网络之间执行转发和 NAT 功能

  • neutron-dhcp-agent:该服务控制分配 IP 地址给运行在计算节点上的实例的 DHCP 服务

  • neutron-metadata-agent:这是一个将实例元数据传递给 Neutron 的服务

以下是 Neutron 的最小工作配置文件:

root@controller:~# cat /etc/neutron/neutron.conf 
[DEFAULT] 
core_plugin = ml2 
service_plugins = router 
allow_overlapping_ips = True 
transport_url = rabbit://openstack:lxcpassword@controller 
auth_strategy = keystone 
notify_nova_on_port_status_changes = True 
notify_nova_on_port_data_changes = True 
[agent] 
root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf 
[cors] 
[cors.subdomain] 
[database] 
connection = mysql+pymysql://neutron:lxcpassword@controller/neutron 
[keystone_authtoken] 
auth_uri = http://controller:5000 
auth_url = http://controller:35357 
memcached_servers = controller:11211 
auth_type = password 
project_domain_name = default 
user_domain_name = default 
project_name = service 
username = neutron 
password = lxcpassword 
[matchmaker_redis] 
[nova] 
auth_url = http://controller:35357 
auth_type = password 
project_domain_name = default 
user_domain_name = default 
region_name = RegionOne 
project_name = service 
username = nova 
password = lxcpassword 
[oslo_concurrency] 
[oslo_messaging_amqp] 
[oslo_messaging_notifications] 
[oslo_messaging_rabbit] 
[oslo_messaging_zmq] 
[oslo_policy] 
[qos] 
[quotas] 
[ssl] 
root@controller:~#

我们需要定义将要支持的网络扩展和网络类型。所有这些信息将在创建 LXC 容器及其配置文件时使用,稍后我们将看到:

root@controller:~# cat /etc/neutron/plugins/ml2/ml2_conf.ini 
[DEFAULT] 
[ml2] 
type_drivers = flat,vlan,vxlan 
tenant_network_types = vxlan 
mechanism_drivers = linuxbridge,l2population 
extension_drivers = port_security 
[ml2_type_flat] 
flat_networks = provider 
[ml2_type_geneve] 
[ml2_type_gre] 
[ml2_type_vlan] 
[ml2_type_vxlan] 
vni_ranges = 1:1000 
[securitygroup] 
enable_ipset = True 
root@controller:~#

定义将添加到软件桥接中的接口,以及该桥接将绑定的 IP 地址。在本例中,我们使用 eth1 接口及其 IP 地址:

root@controller:~# cat /etc/neutron/plugins/ml2/linuxbridge_agent.ini 
[DEFAULT] 
[agent] 
[linux_bridge] 
physical_interface_mappings = provider:eth1 
[securitygroup] 
enable_security_group = True 
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver 
[vxlan] 
enable_vxlan = True 
local_ip = 10.208.132.45 
l2_population = True 
root@controller:~#

我们为 L3 代理指定桥接驱动程序如下:

root@controller:~# cat /etc/neutron/l3_agent.ini 
[DEFAULT] 
interface_driver = neutron.agent.linux.interface.BridgeInterfaceDriver 
[AGENT] 
root@controller:~#

DHCP 代理的配置文件应类似于以下内容:

root@controller:~# cat /etc/neutron/dhcp_agent.ini 
[DEFAULT] 
interface_driver = neutron.agent.linux.interface.BridgeInterfaceDriver 
dhcp_driver = neutron.agent.linux.dhcp.Dnsmasq 
enable_isolated_metadata = True 
[AGENT] 
root@controller:~#

最后,元数据代理的配置如下:

root@controller:~# cat /etc/neutron/metadata_agent.ini 
[DEFAULT] 
nova_metadata_ip = controller 
metadata_proxy_shared_secret = lxcpassword 
[AGENT] 
[cache] 
root@controller:~#

我们需要更新 Nova 服务的配置文件。新的完整文件应如下所示;根据需要替换 IP 地址:

root@controller:~# cat /etc/nova/nova.conf 
[DEFAULT] 
dhcpbridge_flagfile=/etc/nova/nova.conf 
dhcpbridge=/usr/bin/nova-dhcpbridge 
log-dir=/var/log/nova 
state_path=/var/lib/nova 
force_dhcp_release=True 
verbose=True 
ec2_private_dns_show_ip=True 
enabled_apis=osapi_compute,metadata 
transport_url = rabbit://openstack:lxcpassword@controller 
auth_strategy = keystone 
my_ip = 10.208.132.45 
use_neutron = True 
firewall_driver = nova.virt.firewall.NoopFirewallDriver 
compute_driver = libvirt.LibvirtDriver 
scheduler_default_filters = RetryFilter, AvailabilityZoneFilter, RamFilter, ComputeFilter, ComputeCapabilitiesFilter, ImagePropertiesFilter, ServerGroupAntiAffinityFilter, ServerGroupAffinityFilter 
[database] 
connection = mysql+pymysql://nova:lxcpassword@controller/nova 
[api_database] 
connection = mysql+pymysql://nova:lxcpassword@controller/nova_api 
[oslo_concurrency] 
lock_path = /var/lib/nova/tmp 
[libvirt] 
use_virtio_for_bridges=True 
[wsgi] 
api_paste_config=/etc/nova/api-paste.ini 
[keystone_authtoken] 
auth_uri = http://controller:5000 
auth_url = http://controller:35357 
memcached_servers = controller:11211 
auth_type = password 
project_domain_name = default 
user_domain_name = default 
project_name = service 
username = nova 
password = lxcpassword 
[vnc] 
enabled = True 
vncserver_listen = $my_ip 
vncserver_proxyclient_address = $my_ip 
novncproxy_base_url = http://controller:6080/vnc_auto.html 
[glance] 
api_servers = http://controller:9292 
[libvirt] 
virt_type = lxc 
[neutron] 
url = http://controller:9696 
auth_url = http://controller:35357 
auth_type = password 
project_domain_name = default 
user_domain_name = default 
region_name = RegionOne 
project_name = service 
username = neutron 
password = lxcpassword 
service_metadata_proxy = True 
metadata_proxy_shared_secret = lxcpassword 
root@controller:~#

填充 neutron 数据库:

root@controller:~# su -s /bin/sh -c "neutron-db-manage --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/ml2_conf.ini upgrade head" neutron 
INFO [alembic.runtime.migration] Context impl MySQLImpl. 
INFO [alembic.runtime.migration] Will assume non-transactional DDL. 
Running upgrade for neutron ... 
INFO [alembic.runtime.migration] Context impl MySQLImpl. 
INFO [alembic.runtime.migration] Will assume non-transactional DDL. 
... 
OK 
root@controller:~#

最后,启动所有网络服务并重新启动 nova-compute

root@controller:~# service nova-api restart 
root@controller:~# service neutron-server restart 
root@controller:~# service neutron-linuxbridge-agent restart 
root@controller:~# service neutron-dhcp-agent restart 
root@controller:~# service neutron-metadata-agent restart 
root@controller:~# service neutron-l3-agent restart 
root@controller:~# service nova-compute restart

让我们验证 Neutron 服务是否正在运行:

root@controller:~# openstack network agent list 
+--------------------------------------+--------------------+------------+-------------------+-------+-------+---------------------------+ 
| ID | Agent Type | Host | Availability Zone | Alive | State | Binary | 
+--------------------------------------+--------------------+------------+-------------------+-------+-------+---------------------------+ 
| 2a715a0d-5593-4aba-966e-6ae3b2e02ba2 | L3 agent | controller | nova | True | UP | neutron-l3-agent | 
| 2ce176fb-dc2e-4416-bb47-1ae44e1f556f | Linux bridge agent | controller | None | True | UP | neutron-linuxbridge-agent | 
| 42067496-eaa3-42ef-bff9-bbbbcbf2e15a | DHCP agent | controller | nova | True | UP | neutron-dhcp-agent | 
| fad2b9bb-8ee7-468e-b69a-43129338cbaa | Metadata agent | controller | None | True | UP | neutron-metadata-agent | 
+--------------------------------------+--------------------+------------+-------------------+-------+-------+---------------------------+ 
root@controller:~#

定义 LXC 实例规格,生成密钥对,并创建安全组

在我们创建 LXC 实例之前,需要定义其规格 - CPU、内存和磁盘大小。以下命令创建一个名为 lxc.medium 的规格,包含一个虚拟 CPU、1 GB 内存和 5 GB 磁盘:

root@controller:~# openstack flavor create --id 0 --vcpus 1 --ram 1024 --disk 5 lxc.medium 
+----------------------------+------------+ 
| Field | Value | 
+----------------------------+------------+ 
| OS-FLV-DISABLED:disabled | False | 
| OS-FLV-EXT-DATA:ephemeral | 0 | 
| disk | 5 | 
| id | 0 | 
| name | lxc.medium | 
| os-flavor-access:is_public | True | 
| properties | | 
| ram | 1024 | 
| rxtx_factor | 1.0 | 
| swap | | 
| vcpus | 1 | 
+----------------------------+------------+ 
root@controller:~#

为了能够通过 SSH 访问 LXC 容器,我们可以在实例配置期间管理和安装 SSH 密钥,如果我们不希望它们被嵌入到实际镜像中。要生成 SSH 密钥对并将其添加到 OpenStack,请运行以下命令:

root@controller:~# ssh-keygen -q -N ""

输入要保存密钥的文件(/root/.ssh/id_rsa):

root@controller:~# openstack keypair create --public-key ~/.ssh/id_rsa.pub lxckey 
+-------------+-------------------------------------------------+ 
| Field | Value | 
+-------------+-------------------------------------------------+ 
| fingerprint | 84:36:93:cc:2f:f0:f7:ba:d5:73:54:ca:2e:f0:02:6d | 
| name | lxckey | 
| user_id | 3a04d141c07541478ede7ea34f3e5c36 | 
+-------------+-------------------------------------------------+ 
root@controller:~#

要列出我们刚刚添加的新密钥对,请执行以下命令:

root@controller:~# openstack keypair list 
+--------+-------------------------------------------------+ 
| Name | Fingerprint | 
+--------+-------------------------------------------------+ 
| lxckey | 84:36:93:cc:2f:f0:f7:ba:d5:73:54:ca:2e:f0:02:6d | 
+--------+-------------------------------------------------+ 
root@controller:~#

默认情况下,一旦新的 LXC 容器被配置,iptables 会禁止访问它。我们将创建两个安全组,允许 ICMP 和 SSH,以便我们可以测试连接并连接到实例:

root@controller:~# openstack security group rule create --proto icmp default 
+-------------------+--------------------------------------+ 
| Field | Value | 
+-------------------+--------------------------------------+ 
| created_at | 2016-12-02T20:30:14Z | 
| description | | 
| direction | ingress | 
| ethertype | IPv4 | 
| headers | | 
| id | 0e17e0ab-4495-440a-b8b9-0a612f9eccae | 
| port_range_max | None | 
| port_range_min | None | 
| project_id | 488aecf07dcb4ae6bc1ebad5b76fbc20 | 
| project_id | 488aecf07dcb4ae6bc1ebad5b76fbc20 | 
| protocol | icmp | 
| remote_group_id | None | 
| remote_ip_prefix | 0.0.0.0/0 | 
| revision_number | 1 | 
| security_group_id | f21f3d3c-27fe-4668-bca4-6fc842dcb690 | 
| updated_at | 2016-12-02T20:30:14Z | 
+-------------------+--------------------------------------+ 
root@controller:~# 
root@controller:~# openstack security group rule create --proto tcp --dst-port 22 default 
... 
root@controller:~#

创建网络

让我们首先在 Neutron 中创建一个名为nat的新网络:

root@controller:~# openstack network create nat 
+---------------------------+--------------------------------------+ 
| Field | Value | 
+---------------------------+--------------------------------------+ 
| admin_state_up | UP | 
| availability_zone_hints | | 
| availability_zones | | 
| created_at | 2016-12-02T20:32:53Z | 
| description | | 
| headers | | 
| id | 66037974-d24b-4615-8b93-b0de18a4561b | 
| ipv4_address_scope | None | 
| ipv6_address_scope | None | 
| mtu | 1450 | 
| name | nat | 
| port_security_enabled | True | 
| project_id | 488aecf07dcb4ae6bc1ebad5b76fbc20 | 
| project_id | 488aecf07dcb4ae6bc1ebad5b76fbc20 | 
| provider:network_type | vxlan | 
| provider:physical_network | None | 
| provider:segmentation_id | 53 | 
| revision_number | 3 | 
| router:external | Internal | 
| shared | False | 
| status | ACTIVE | 
| subnets | | 
| tags | [] | 
| updated_at | 2016-12-02T20:32:53Z | 
+---------------------------+--------------------------------------+ 
root@controller:~#

接下来,定义将分配给 LXC 容器的 DNS 服务器、默认网关和子网范围:

root@controller:~# openstack subnet create --network nat --dns-nameserver 8.8.8.8 --gateway 192.168.0.1 --subnet-range 192.168.0.0/24 nat 
+-------------------+--------------------------------------+ 
| Field | Value | 
+-------------------+--------------------------------------+ 
| allocation_pools | 192.168.0.2-192.168.0.254 | 
| cidr | 192.168.0.0/24 | 
| created_at | 2016-12-02T20:36:14Z | 
| description | | 
| dns_nameservers | 8.8.8.8 | 
| enable_dhcp | True | 
| gateway_ip | 192.168.0.1 | 
| headers | | 
| host_routes | | 
| id | 0e65fa94-be69-4690-b3fe-406ea321dfb3 | 
| ip_version | 4 | 
| ipv6_address_mode | None | 
| ipv6_ra_mode | None | 
| name | nat | 
| network_id | 66037974-d24b-4615-8b93-b0de18a4561b | 
| project_id | 488aecf07dcb4ae6bc1ebad5b76fbc20 | 
| project_id | 488aecf07dcb4ae6bc1ebad5b76fbc20 | 
| revision_number | 2 | 
| service_types | [] | 
| subnetpool_id | None | 
| updated_at | 2016-12-02T20:36:14Z | 
+-------------------+--------------------------------------+ 
root@controller:~#

更新 Neutron 中子网的信息:

root@controller:~# neutron net-update nat --router:external 
Updated network: nat 
root@controller:~# 

作为 lxc 用户,创建一个新的软件路由器:

root@controller:~# . rc.lxc 
root@controller:~# openstack router create router 
+-------------------------+--------------------------------------+ 
| Field | Value | 
+-------------------------+--------------------------------------+ 
| admin_state_up | UP | 
| availability_zone_hints | | 
| availability_zones | | 
| created_at | 2016-12-02T20:38:08Z | 
| description | | 
| external_gateway_info | null | 
| flavor_id | None | 
| headers | | 
| id | 45557fac-f158-40ef-aeec-496de913d5a5 | 
| name | router | 
| project_id | b0cc5ccc12eb4d6b98aadd784540f575 | 
| project_id | b0cc5ccc12eb4d6b98aadd784540f575 | 
| revision_number | 2 | 
| routes | | 
| status | ACTIVE | 
| updated_at | 2016-12-02T20:38:08Z | 
+-------------------------+--------------------------------------+ 
root@controller:~#

作为管理员用户,将我们之前创建的子网添加为路由器的接口:

root@controller:~# . rc.admin 
root@controller:~# neutron router-interface-add router nat 
Added interface be0f1e65-f086-41fd-b8d0-45ebb865bf0f to router router. 
root@controller:~#

让我们列出创建的网络命名空间:

root@controller:~# ip netns 
qrouter-45557fac-f158-40ef-aeec-496de913d5a5 (id: 1) 
qdhcp-66037974-d24b-4615-8b93-b0de18a4561b (id: 0) 
root@controller:~#

要显示软件路由器上的端口以及 LXC 容器的默认网关,请运行以下命令:

root@controller:~# neutron router-port-list router 
+--------------------------------------+------+-------------------+------------------------------------------------------------------------------------+ 
| id | name | mac_address | fixed_ips | 
+--------------------------------------+------+-------------------+------------------------------------------------------------------------------------+ 
| be0f1e65-f086-41fd-b8d0-45ebb865bf0f | | fa:16:3e:a6:36:7c | {"subnet_id": "0e65fa94-be69-4690-b3fe-406ea321dfb3", "ip_address": "192.168.0.1"} | 
+--------------------------------------+------+-------------------+------------------------------------------------------------------------------------+ 
root@controller:~#

使用 OpenStack 提供 LXC 容器

在启动我们的 LXC 容器之前,让我们再三检查一下是否满足所有要求。

首先列出可用的网络:

root@controller:~# openstack network list 
+--------------------------------------+------+--------------------------------------+ 
| ID | Name | Subnets | 
+--------------------------------------+------+--------------------------------------+ 
| 66037974-d24b-4615-8b93-b0de18a4561b | nat | 0e65fa94-be69-4690-b3fe-406ea321dfb3 | 
+--------------------------------------+------+--------------------------------------+ 
root@controller:~#

显示我们可以选择的计算实例规格:

root@controller:~# openstack flavor list 
+----+------------+------+------+-----------+-------+-----------+ 
| ID | Name | RAM | Disk | Ephemeral | VCPUs | Is Public | 
+----+------------+------+------+-----------+-------+-----------+ 
| 0 | lxc.medium | 1024 | 5 | 0 | 1 | True | 
+----+------------+------+------+-----------+-------+-----------+ 
root@controller:~#

接下来,列出可用的镜像:

root@controller:~# openstack image list 
+--------------------------------------+------------------+--------+ 
| ID | Name | Status | 
+--------------------------------------+------------------+--------+ 
| 417e72b5-7b85-4555-835d-ce442e21aa4f | lxc_ubuntu_12.04 | active | 
+--------------------------------------+------------------+--------+ 
root@controller:~#

同时,显示我们之前创建的默认安全组:

root@controller:~# openstack security group list 
+--------------------------------------+---------+------------------------+----------------------------------+ 
| ID | Name | Description | Project | 
+--------------------------------------+---------+------------------------+----------------------------------+ 
| f21f3d3c-27fe-4668-bca4-6fc842dcb690 | default | Default security group | 488aecf07dcb4ae6bc1ebad5b76fbc20 | 
+--------------------------------------+---------+------------------------+----------------------------------+ 
root@controller:~#

现在加载 网络块设备NBD)内核模块,因为 Nova 期望它:

root@controller:~# modprobe nbd

最后,要使用 OpenStack 提供 LXC 容器,请执行以下命令:

root@controller:~# openstack server create --flavor lxc.medium --image lxc_ubuntu_12.04 --nic net-id=66037974-d24b-4615-8b93-b0de18a4561b --security-group default --key-name lxckey lxc_instance 
... 
root@controller:~#

注意我们如何指定实例规格、镜像名称、网络 ID、安全组、密钥对名称和实例名称。

确保用系统返回的输出替换这些 ID。

要列出 LXC 容器、其状态以及分配的 IP 地址,请运行以下命令:

root@controller:~# openstack server list 
+--------------------------------------+--------------+--------+-----------------+------------------+ 
| ID | Name | Status | Networks | Image Name | 
+--------------------------------------+--------------+--------+-----------------+------------------+ 
| a86f8f56-80d7-4d36-86c3-827679f21ec5 | lxc_instance | ACTIVE | nat=192.168.0.3 | lxc_ubuntu_12.04 | 
+--------------------------------------+--------------+--------+-----------------+------------------+ 
root@controller:~#

正如我们在本章前面看到的,OpenStack 使用 libvirt 驱动程序来提供 LXC 容器。我们可以使用在第二章,安装和运行 LXC 在 Linux 系统上 中使用过的 virsh 命令,列出主机上的 LXC 容器:

root@controller:~# virsh --connect lxc:// list --all 
Id Name State 
---------------------------------------------------- 
16225 instance-00000002 running 
root@controller:~#

如果我们列出主机上的进程,可以看到 libvirt_lxc 父进程启动了容器的初始化进程:

root@controller:~# ps axfw 
... 
16225 ? S 0:00 /usr/lib/libvirt/libvirt_lxc --name instance-00000002 --console 23 --security=apparmor --handshake 26 --veth vnet0 
16227 ? Ss 0:00 \_ /sbin/init 
16591 ? S 0:00 \_ upstart-socket-bridge --daemon 
16744 ? Ss 0:00 \_ dhclient3 -e IF_METRIC=100 -pf /var/run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -1 eth0 
17692 ? S 0:00 \_ upstart-udev-bridge --daemon 
17696 ? Ss 0:00 \_ /sbin/udevd --daemon 
17819 ? S 0:00 | \_ /sbin/udevd --daemon 
18108 ? Ss 0:00 \_ /usr/sbin/sshd -D 
18116 ? Ss 0:00 \_ dbus-daemon --system --fork --activation=upstart 
18183 ? Ss 0:00 \_ cron 
18184 ? Ss 0:00 \_ atd 
18189 ? Ss 0:00 \_ /usr/sbin/irqbalance 
18193 ? Ss 0:00 \_ acpid -c /etc/acpi/events -s /var/run/acpid.socket 
18229 pts/0 Ss+ 0:00 \_ /sbin/getty -8 38400 tty1 
18230 ? Ssl 0:00 \_ whoopsie 
18317 ? Sl 0:00 \_ rsyslogd -c5 
19185 ? Ss 0:00 \_ /sbin/getty -8 38400 tty4 
19186 ? Ss 0:00 \_ /sbin/getty -8 38400 tty2 
19187 ? Ss 0:00 \_ /sbin/getty -8 38400 tty3 
root@controller:~#

容器的配置文件和磁盘的位置如下:

root@controller:~# cd /var/lib/nova/instances/ 
root@controller:/var/lib/nova/instances# ls -la a86f8f56-80d7-4d36-86c3-827679f21ec5/ 
total 12712 
drwxr-xr-x 3 nova nova 4096 Dec 2 20:52 . 
drwxr-xr-x 5 nova nova 4096 Dec 2 20:52 .. 
-rw-r--r-- 1 nova nova 0 Dec 2 20:52 console.log 
-rw-r--r-- 1 nova nova 13041664 Dec 2 20:57 disk 
-rw-r--r-- 1 nova nova 79 Dec 2 20:52 disk.info 
-rw-r--r-- 1 nova nova 1534 Dec 2 20:52 libvirt.xml 
drwxr-xr-x 2 nova nova 4096 Dec 2 20:52 rootfs 
root@controller:/var/lib/nova/instances#

让我们检查一下容器的配置文件:

root@controller:/var/lib/nova/instances# cat a86f8f56-80d7-4d36-86c3-827679f21ec5/libvirt.xml 
<domain type="lxc"> 
 <uuid>a86f8f56-80d7-4d36-86c3-827679f21ec5</uuid> 
 <name>instance-00000002</name> 
 <memory>1048576</memory> 
 <vcpu>1</vcpu> 
 <metadata> 
 <nova:instance 
      > 
 <nova:package version="14.0.1"/> 
 <nova:name>lxc_instance</nova:name> 
 <nova:creationTime>2016-12-02 20:52:52</nova:creationTime> 
 <nova:flavor name="lxc.medium"> 
 <nova:memory>1024</nova:memory> 
 <nova:disk>5</nova:disk> 
 <nova:swap>0</nova:swap> 
 <nova:ephemeral>0</nova:ephemeral> 
 <nova:vcpus>1</nova:vcpus> 
 </nova:flavor> 
 <nova:owner> 
 <nova:user uuid="3a04d141c07541478ede7ea34f3e5c36">
          admin
        </nova:user> 
 <nova:project uuid="488aecf07dcb4ae6bc1ebad5b76fbc20">
          admin
        </nova:project> 
 </nova:owner> 
 <nova:root type="image" uuid="417e72b5-7b85-4555-835d-ce442e21aa4f"/> 
 </nova:instance> 
 </metadata> 
 <os> 
 <type>exe</type> 
 <cmdline>console=tty0 console=ttyS0 console=ttyAMA0</cmdline> 
 <init>/sbin/init</init> 
 </os> 
 <cputune> 
 <shares>1024</shares> 
 </cputune> 
 <clock offset="utc"/> 
 <devices> 
 <filesystem type="mount"> 
 <source dir="/var/lib/nova/instances/
      a86f8f56-80d7-4d36-86c3-827679f21ec5/rootfs"/> 
 <target dir="/"/> 
 </filesystem> 
 <interface type="bridge"> 
 <mac address="fa:16:3e:4f:e5:b5"/> 
 <source bridge="brq66037974-d2"/> 
 <target dev="tapf2073410-64"/> 
 </interface> 
 <console type="pty"/> 
 </devices> 
</domain> 
root@controller:/var/lib/nova/instances#

我们已经看过之前章节中为 libvirt 容器构建的类似配置文件。

在 Neutron 管理的网络环境下,我们应该能够看到桥接和容器的接口作为端口被添加:

root@controller:/var/lib/nova/instances# brctl show 
bridge name      bridge id         STP enabled      interfaces 
brq66037974-d2   8000.02d65d01c617  no              tap4e3afc26-88 
 tapbe0f1e65-f0 
 tapf2073410-64 
 vxlan-53 
virbr0           8000.5254004e7712  yes             virbr0-nic 
root@controller:/var/lib/nova/instances#

让我们在桥接接口上配置一个 IP 地址,并允许容器使用 NAT 连接:

root@controller:/var/lib/nova/instances# ifconfig brq66037974-d2 192.168.0.1 
root@controller:~# iptables -A POSTROUTING -t nat -s 192.168.0.0/24 ! -d 192.168.0.0/24 -j MASQUERADE 
root@controller:~#

要使用 SSH 和我们之前生成的密钥对连接到 LXC 容器,请执行以下命令:

root@controller:/var/lib/nova/instances# ssh ubuntu@192.168.0.3
Welcome to Ubuntu 12.04.5 LTS (GNU/Linux 4.4.0-51-generic x86_64)
ubuntu@lxc-instance:~$ ifconfig
eth0 Link encap:Ethernet HWaddr fa:16:3e:4f:e5:b5
inet addr:192.168.0.3 Bcast:192.168.0.255 Mask:255.255.255.0
inet6 addr: fe80::f816:3eff:fe4f:e5b5/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1450 Metric:1
RX packets:290 errors:0 dropped:0 overruns:0 frame:0
TX packets:340 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:35038 (35.0 KB) TX bytes:36830 (36.8 KB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
ubuntu@lxc-instance:~$ exit
logout
Connection to 192.168.0.3 closed.
root@controller:/var/lib/nova/instances# cd
root@controller:~#

最后,要使用 OpenStack 删除 LXC 容器,请运行以下命令:

root@controller:~# openstack server delete lxc_instance 
root@controller:~# openstack server list 
root@controller:~#

摘要

在本章中,我们查看了一个基本的 OpenStack 部署示例,使用身份服务 Keystone 存储服务目录,以及进行身份验证和授权,使用 Nova 计算服务为 LXC 实例提供服务,使用 Glance 镜像服务存储 LXC 容器镜像,并通过 Neutron 网络服务创建桥接并为我们的容器分配 IP 地址。

完整的生产就绪部署将包括多个控制节点,这些节点运行前述服务,并配备一组计算服务器来提供容器。

使用 OpenStack 和 LXC 是创建和管理多租户云环境的好方法,可以以集中式和高度可扩展的方式运行各种软件应用。

附录 A. LXC 替代方案:Docker 和 OpenVZ

LXC 设计并且非常适合运行完整系统容器;这意味着 LXC 实例包含整个操作系统分发的文件系统,非常类似于虚拟机。尽管 LXC 可以运行单个进程,或者用自定义脚本替换 init 系统,但有其他更适合仅执行单个自包含程序的容器替代方案。在本附录中,我们将查看两种与 LXC 并存的容器实现替代方案:Docker 和 OpenVZ。

使用 OpenVZ 构建容器

OpenVZ 是最古老的操作系统级虚拟化技术之一,始于 2005 年。它类似于 LXC,专注于运行整个操作系统,而不是像 Docker 那样只运行单个程序。作为容器化技术,它与主机操作系统内核共享,没有 hypervisor 层。OpenVZ 使用的是经过补丁处理的 Red Hat 内核版本,与 Vanilla 内核分开维护。

让我们探索一些 OpenVZ 的特性,并看看它们与 LXC 的比较:

对于这个示例部署,我们将使用 Debian Wheezy:

root@ovz:~# lsb_release -rd
Description:      Debian GNU/Linux 7.8 (wheezy)
Release:    7.8
root@ovz:~#

首先添加 OpenVZ 仓库和密钥,然后更新软件包索引:

root@ovz:~# cat << EOF > /etc/apt/sources.list.d/openvz-rhel6.list
deb http://download.openvz.org/debian wheezy main
EOF
root@ovz:~#
root@ovz:~# wget ftp://ftp.openvz.org/debian/archive.key
root@ovz:~# apt-key add archive.key
root@ovz:~# apt-get update

接下来,安装 OpenVZ 内核:

root@ovz:~# apt-get install linux-image-openvz-amd64

如果使用 GRUB,使用 OpenVZ 内核更新启动菜单;在此示例中,内核被添加为菜单项 2:

root@ovz:~# cat /boot/grub/grub.cfg | grep menuentry
menuentry 'Debian GNU/Linux, with Linux 3.2.0-4-amd64' --class debian --class gnu-linux --class gnu --class os {
menuentry 'Debian GNU/Linux, with Linux 3.2.0-4-amd64 (recovery mode)' --class debian --class gnu-linux --class gnu --class os {
menuentry 'Debian GNU/Linux, with Linux 2.6.32-openvz-042stab120.11-amd64' --class debian --class gnu-linux --class gnu --class os {
menuentry 'Debian GNU/Linux, with Linux 2.6.32-openvz-042stab120.11-amd64 (recovery mode)' --class debian --class gnu-linux --class gnu --class os {
root@ovz:~#
root@ovz:~# vim /etc/default/grub
...
GRUB_DEFAULT=2
... 
root@ovz:~# update-grub
Generating grub.cfg ...
Found linux image: /boot/vmlinuz-3.2.0-4-amd64
Found initrd image: /boot/initrd.img-3.2.0-4-amd64
Found linux image: /boot/vmlinuz-2.6.32-openvz-042stab120.11-amd64
Found initrd image: /boot/initrd.img-2.6.32-openvz-042stab120.11-amd64
done
root@ovz:~#

我们需要在内核中启用路由并禁用代理 ARP:

root@ovz:~# cat /etc/sysctl.d/ovz.conf
net.ipv4.ip_forward = 1
net.ipv6.conf.default.forwarding = 1
net.ipv6.conf.all.forwarding = 1
net.ipv4.conf.default.proxy_arp = 0
net.ipv4.conf.all.rp_filter = 1
kernel.sysrq = 1
net.ipv4.conf.default.send_redirects = 1
net.ipv4.conf.all.send_redirects = 0
root@ovz2:~#
root@ovz:~# sysctl -p /etc/sysctl.d/ovz.conf
...
root@ovz:~#

现在是重新启动服务器的时候了,然后检查 OpenVZ 是否已加载:

root@ovz:~# reboot
root@ovz:~# uname -a
Linux ovz 2.6.32-openvz-042stab120.11-amd64 #1 SMP Wed Nov 16 12:07:16 MSK 2016 x86_64 GNU/Linux
root@ovz:~#

接下来,安装用户空间工具:

root@ovz:~# apt-get install vzctl vzquota ploop vzstats

OpenVZ 使用与 LXC 类似的模板。这些模板是归档的根文件系统,可以使用 debootstrap 等工具构建。让我们在 OpenVZ 默认期望它们的目录中下载一个 Ubuntu 模板:

root@ovz:~# cd /var/lib/vz/template/
root@ovz:/var/lib/vz/template# wget http://download.openvz.org/template/precreated/ubuntu-16.04-x86_64.tar.gz
root@ovz:/var/lib/vz/template#

在放置模板归档文件的位置后,让我们创建一个容器:

root@ovz:/var/lib/vz/template# vzctl create 1 --ostemplate ubuntu-16.04-x86_64 --layout simfs
Creating container private area (ubuntu-16.04-x86_64)
Performing postcreate actions
CT configuration saved to /etc/vz/conf/1.conf
Container private area was created
root@ovz:/var/lib/vz/template# 

我们指定 simfs 作为底层容器存储的类型,它将在主机操作系统上创建根文件系统,类似于 LXC 和默认目录类型。OpenVZ 还提供了其他选项,例如 Ploop,它会创建包含容器文件系统的镜像文件。

接下来,创建一个 Linux 桥接器:

root@ovz:/var/lib/vz/template# apt-get install bridge-utils
root@ovz:/var/lib/vz/template# brctl addbr br0

为了允许 OpenVZ 将其容器连接到主机桥接器,创建以下配置文件:

root@ovz:/var/lib/vz/template# cat /etc/vz/vznet.conf
#!/bin/bash
EXTERNAL_SCRIPT="/usr/sbin/vznetaddbr"
root@ovz:/var/lib/vz/template#

该文件指定了一个外部脚本,该脚本将把容器的虚拟接口添加到我们之前创建的桥接器中。

通过指定容器内外接口的名称和应连接到的桥接器,配置我们的容器网络接口:

root@ovz:/var/lib/vz/template# vzctl set 1 --save --netif_add eth0,,veth1.eth0,,br0
CT configuration saved to /etc/vz/conf/1.conf
root@ovz:/var/lib/vz/template#

通过执行以下命令列出主机上可用的容器:

root@ovz:/var/lib/vz/template# vzlist -a
CTID      NPROC STATUS    IP_ADDR      HOSTNAME
 1          - stopped      -               -
root@ovz:/var/lib/vz/template# cd

要启动我们的容器,请运行以下命令:

root@ovz:~# vzctl start 1
Starting container...
Container is mounted
Setting CPU units: 1000
Configure veth devices: veth1.eth0
Adding interface veth1.eth0 to bridge br0 on CT0 for CT1
Container start in progress...
root@ovz:~#

然后,要附加或进入容器,请执行以下命令:

root@ovz:~# vzctl enter 1
entered into CT 1
root@localhost:/# exit
logout
exited from CT 1
root@ovz:~#

可以像 LXC 一样,在不重新启动容器的情况下实时操作可用的容器资源。让我们将内存设置为 1 GB:

root@ovz:~# vzctl set 1 --ram 1G --save
UB limits were set successfully
CT configuration saved to /etc/vz/conf/1.conf
root@ovz:~#

每个 OpenVZ 容器都有一个配置文件,当传递 --save 选项给 vzctl 工具时,它会被更新。要查看它,请运行以下命令:

root@ovz:~# cat /etc/vz/conf/1.conf | grep -vi "#" | sed '/^$/d'
PHYSPAGES="0:262144"
SWAPPAGES="0:512M"
DISKSPACE="2G:2.2G"
DISKINODES="131072:144179"
QUOTATIME="0"
CPUUNITS="1000"
NETFILTER="stateless"
VE_ROOT="/var/lib/vz/root/$VEID"
VE_PRIVATE="/var/lib/vz/private/$VEID"
VE_LAYOUT="simfs"
OSTEMPLATE="ubuntu-16.04-x86_64"
ORIGIN_SAMPLE="vswap-256m"
NETIF="ifname=eth0,bridge=br0,mac=00:18:51:A1:C6:35,host_ifname=veth1.eth0,host_mac=00:18:51:BF:1D:AC"
root@ovz:~#

容器运行时,确保主机上的虚拟接口已添加到桥接中。请注意,桥接接口本身处于 DOWN 状态:

root@ovz:~# brctl show
bridge name bridge id          STP enabled    interfaces
br0         8000.001851bf1dac  no             veth1.eth0
root@ovz:~# ip a s
...
4: br0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN
link/ether 00:18:51:bf:1d:ac brd ff:ff:ff:ff:ff:ff
6: veth1.eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
 link/ether 00:18:51:bf:1d:ac brd ff:ff:ff:ff:ff:ff
 inet6 fe80::218:51ff:febf:1dac/64 scope link
 valid_lft forever preferred_lft forever
root@ovz:~#

我们可以在容器内执行命令,无需附加到它。让我们为容器的接口配置一个 IP 地址:

root@ovz:~# vzctl exec 1 "ifconfig eth0 192.168.0.5"
root@ovz:~#

启动主机上的桥接接口并配置一个 IP 地址,以便我们可以从主机访问容器:

root@ovz:~# ifconfig br0 up
root@ovz:~# ifconfig br0 192.168.0.1

让我们测试连通性:

root@ovz:~# ping -c3 192.168.0.5
PING 192.168.0.5 (192.168.0.5) 56(84) bytes of data.
64 bytes from 192.168.0.5: icmp_req=1 ttl=64 time=0.037 ms
64 bytes from 192.168.0.5: icmp_req=2 ttl=64 time=0.036 ms
64 bytes from 192.168.0.5: icmp_req=3 ttl=64 time=0.036 ms
--- 192.168.0.5 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.036/0.036/0.037/0.005 ms
root@ovz:~#

让我们进入容器并确保可用内存确实为 1 GB,正如我们之前设置的那样:

root@ovz:~# vzctl enter 1
entered into CT 1
root@localhost:/# free -g
total    used    free   shared  buff/cache   available
Mem:     1        0       0         0           0           0
Swap:    0        0       0
root@localhost:/# exit
logout
exited from CT 1
root@ovz:~#

注意 OpenVZ 容器如何使用 init 启动所有其他进程,就像虚拟机一样:

root@ovz:~# ps axfww
...
3303 ?        Ss     0:00 init -z
3365 ?        Ss     0:00  \_ /lib/systemd/systemd-journald
3367 ?        Ss     0:00  \_ /lib/systemd/systemd-udevd
3453 ?        Ss     0:00  \_ /sbin/rpcbind -f -w
3454 ?        Ssl    0:00  \_ /usr/sbin/rsyslogd -n
3457 ?        Ss     0:00  \_ /usr/sbin/cron -f
3526 ?        Ss     0:00  \_ /usr/sbin/xinetd -pidfile /run/xinetd.pid -stayalive -inetd_compat -inetd_ipv6
3536 ?        Ss     0:00  \_ /usr/sbin/saslauthd -a pam -c -m /var/run/saslauthd -n 2
3540 ?        S      0:00  |   \_ /usr/sbin/saslauthd -a pam -c -m /var/run/saslauthd -n 2
3542 ?        Ss     0:00  \_ /usr/sbin/apache2 -k start
3546 ?        Sl     0:00  |   \_ /usr/sbin/apache2 -k start
3688 ?        Ss     0:00  \_ /usr/lib/postfix/sbin/master
3689 ?        S      0:00  |   \_ pickup -l -t unix -u -c
3690 ?        S      0:00  |   \_ qmgr -l -t unix -u
3695 ?        Ss     0:00  \_ /usr/sbin/sshd -D
3705 tty1     Ss+    0:00  \_ /sbin/agetty --noclear --keep-baud console 115200 38400 9600 vt220
3706 tty2     Ss+    0:00  \_ /sbin/agetty --noclear tty2 linux
root@ovz:~#

我们现在知道,所有容器实现都使用 cgroups 来控制系统资源,OpenVZ 也不例外。让我们看看 cgroup 层次结构是如何挂载的:

root@ovz:~# mount | grep cgroup
beancounter on /proc/vz/beancounter type cgroup (rw,relatime,blkio,name=beancounter)
container on /proc/vz/container type cgroup (rw,relatime,freezer,devices,name=container)
fairsched on /proc/vz/fairsched type cgroup (rw,relatime,cpuacct,cpu,cpuset,name=fairsched)
tmpfs on /var/lib/vz/root/1/sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,size=131072k,nr_inodes=32768,mode=755)
cgroup on /var/lib/vz/root/1/sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /var/lib/vz/root/1/sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /var/lib/vz/root/1/sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio,name=beancounter)
root@ovz:~#

我们之前创建的容器的 ID 为 1,如前面的示例所示。我们可以通过运行以下命令获取容器内所有进程的 PID:

root@ovz:~# cat /proc/vz/fairsched/1/cgroup.procs
3303
3304
3305
3365
3367
3453
3454
3457
3526
3536
3540
3542
3546
3688
3689
3690
3695
3705
3706
root@ovz:~#

我们还可以获取分配给容器的 CPU 数量:

root@ovz:~# cat /proc/vz/fairsched/1/cpu.nr_cpus
0
root@ovz:~#

让我们为 ID 为 1 的容器分配两个核心:

root@ovz:~# vzctl set 1 --save --cpus 2
UB limits were set successfully
Setting CPUs: 2
CT configuration saved to /etc/vz/conf/1.conf
root@ovz:~#

然后确保更改在同一文件中可见:

root@ovz:~# cat /proc/vz/fairsched/1/cpu.nr_cpus
2
root@ovz:~#

容器的配置文件也应反映该更改:

root@ovz:~# cat /etc/vz/conf/1.conf | grep -i CPUS
CPUS="2"
root@ovz:~#

使用 ps 命令,或者通过读取前述系统文件,我们可以获取容器内 init 进程的 PID,在本示例中为 3303。知道了 PID 后,我们可以通过运行以下命令获取容器的 ID:

root@ovz:~# cat /proc/3303/status | grep envID
envID:      1
root@ovz:~#

由于容器的根文件系统存在于主机上,迁移 OpenVZ 实例与 LXC 类似——我们首先停止容器,然后归档根文件系统,将其复制到新服务器,并提取它。我们还需要容器的配置文件。让我们来看一个将 OpenVZ 容器迁移到新主机的示例:

root@ovz:~# vzctl stop 1
Stopping container ...
Container was stopped
Container is unmounted
root@ovz:~# 
root@ovz:~# tar -zcvf /tmp/ovz_container_1.tar.gz -C /var/lib/vz/private 1
root@ovz:~# scp  /tmp/ovz_container_1.tar.gz 10.3.20.31:/tmp/
root@ovz:~# scp /etc/vz/conf/1.conf 10.3.20.31:/etc/vz/conf/
root@ovz:~#

在第二台服务器上,我们提取根文件系统:

root@ovz2:~# tar zxfv /tmp/ovz_container_1.tar.gz --numeric-owner -C /var/lib/vz/private
root@ovz2:~#

配置文件和文件系统准备好后,我们可以通过运行以下命令来列出容器:

root@ovz2:~# vzlist -a
stat(/var/lib/vz/root/1): No such file or directory
CTID      NPROC STATUS    IP_ADDR         HOSTNAME
1          - stopped       -               -
root@ovz2:~# 

最后,要启动 OpenVZ 实例并确保它在新主机上运行,请执行以下命令:

root@ovz2:~# vzctl start 1
Starting container...
stat(/var/lib/vz/root/1): No such file or directory
stat(/var/lib/vz/root/1): No such file or directory
stat(/var/lib/vz/root/1): No such file or directory
Initializing quota ...
Container is mounted
Setting CPU units: 1000
Setting CPUs: 2
Configure veth devices: veth1.eth0
Container start in progress...
root@ovz2:~# vzlist -a
CTID      NPROC STATUS    IP_ADDR         HOSTNAME
1         47 running       -               -
root@ovz2:~#

OpenVZ 没有集中式控制守护进程,这使得与 upstartsystemd 等初始化系统的集成更加容易。需要注意的是,OpenVZ 是 Virtuozzo 公司提供的 Virtuozzo 虚拟化解决方案的基础,其最新版本将是一个完整操作系统的 ISO 镜像,而不是运行带有单独工具集的修补内核。

注意

要了解最新的 OpenVZ 和 Virtuozzo 版本,请访问 openvz.org

使用 Docker 构建容器

Docker 项目于 2013 年发布,并迅速获得了广泛的关注,超越了 OpenVZ 和 LXC。现在,大型生产部署都在运行 Docker,配合各种编排框架,如 Apache Mesos 和 Kubernetes,提供 Docker 集成。

与 LXC 和 OpenVZ 不同,Docker 更适合在最小化容器设置中运行单个应用程序。它使用 Docker Engine 守护进程,该进程控制 containerd 进程来管理容器的生命周期,从而使它更难与其他初始化系统(如 systemd)集成。

Docker 提供了一个方便的 API,供各种工具使用,并使得从预构建的镜像中快速配置容器变得容易,这些镜像可以托管在远程的公共或私有仓库/注册表中。

我们可以在同一主机上运行 LXC 和 Docker 容器而不会遇到任何问题,因为它们有清晰的隔离。在接下来的部分,我们将探索 Docker 的大部分功能,通过检查 Docker 容器的生命周期并了解它与 LXC 的比较。

让我们首先更新服务器并安装仓库和密钥:

root@docker:~# apt-get update && apt-get upgrade && reboot
... 
root@docker:~# apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
Executing: /tmp/tmp.Au9fc0rNu3/gpg.1.sh --keyserver
hkp://ha.pool.sks-keyservers.net:80
--recv-keys
58118E89F3A912897C070ADBF76221572C52609D
gpg: requesting key 2C52609D from hkp server ha.pool.sks-keyservers.net
gpg: key 2C52609D: public key "Docker Release Tool (releasedocker) <docker@docker.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1  (RSA: 1) 
root@docker:~# echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" | sudo tee /etc/apt/sources.list.d/docker.list
deb https://apt.dockerproject.org/repo ubuntu-xenial main
root@docker:~# apt-get update

列出当前可用的软件包版本并安装最新的候选版本:

root@docker:~# apt-cache policy docker-engine
docker-engine:
 Installed: (none)
 Candidate: 1.12.4-0~ubuntu-xenial
 Version table:
 1.12.4-0~ubuntu-xenial 500
 500 https://apt.dockerproject.org/repo ubuntu-xenial/main amd64 Packages
 1.12.3-0~xenial 500
 500 https://apt.dockerproject.org/repo ubuntu-xenial/main amd64 Packages
 1.12.2-0~xenial 500
 500 https://apt.dockerproject.org/repo ubuntu-xenial/main amd64 Packages
 1.12.1-0~xenial 500
 500 https://apt.dockerproject.org/repo ubuntu-xenial/main amd64 Packages
 1.12.0-0~xenial 500
 500 https://apt.dockerproject.org/repo ubuntu-xenial/main amd64 Packages
 1.11.2-0~xenial 500
 500 https://apt.dockerproject.org/repo ubuntu-xenial/main amd64 Packages
 1.11.1-0~xenial 500
 500 https://apt.dockerproject.org/repo ubuntu-xenial/main amd64 Packages
 1.11.0-0~xenial 500
 500 https://apt.dockerproject.org/repo ubuntu-xenial/main amd64 Packages 
root@docker:~# apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual
root@docker:~# apt-get install docker-engine

启动 Docker 服务并确保它们正在运行:

root@docker:~# service docker start
root@docker:~# pgrep -lfa docker
24585 /usr/bin/dockerd -H fd://
24594 docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --shim docker-containerd-shim --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --runtime docker-runc
root@docker:~#

Docker 守护进程运行后,让我们列出所有可用的容器,目前我们没有任何容器:

root@docker:~# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
root@docker:~#

让我们通过执行以下命令,从上游公共注册表中查找一个 Ubuntu 镜像:

root@docker:~# docker search ubuntu
NAME      DESCRIPTION        STARS     OFFICIAL  
AUTOMATED
ubuntu  Ubuntu is a Debian-based Linux operating s...   5200      [OK]
ubuntu-upstart  Upstart is an event-based replacement for ...   69        [OK]
...
root@docker:~#

我们为容器选择官方的 Ubuntu 镜像;要创建它,运行以下命令:

root@docker:~# docker create --tty --interactive ubuntu bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
af49a5ceb2a5: Pull complete
8f9757b472e7: Pull complete
e931b117db38: Pull complete
47b5e16c0811: Pull complete
9332eaf1a55b: Pull complete
Digest: sha256:3b64c309deae7ab0f7dbdd42b6b326261ccd6261da5d88396439353162703fb5
Status: Downloaded newer image for ubuntu:latest
ec66fcfb5960c0779d07243f2c1e4d4ac10b855e940d416514057a9b28d78d09
root@docker:~#

我们现在应该在系统中有一个缓存的 Ubuntu 镜像:

root@docker:~# docker images
REPOSITORY   TAG     IMAGE ID       CREATED      SIZE
ubuntu       latest  4ca3a192ff2a  2 weeks ago   128.2 MB
root@docker:~#

让我们再次列出主机上可用的容器:

root@docker:~# docker ps --all
CONTAINER ID    IMAGE      COMMAND     CREATED           STATUS         PORTS         NAMES
ec66fcfb5960    ubuntu     "bash"     29 seconds ago      Created                 docker_container_1
root@docker:~#

启动 Ubuntu Docker 容器同样简单:

root@docker:~# docker start docker_container_1
docker_container_1
root@docker:~# docker ps
CONTAINER ID    IMAGE   COMMAND     CREATED             STATUS          PORTS     NAMES
ec66fcfb5960    ubuntu  "bash"   About a minute ago   Up 17 seconds         docker_container_1
root@docker:~#

通过检查进程列表,注意到我们现在有一个单一的 bash 进程作为 dockerddocker-containerd 进程的子进程运行:

root@docker:~# ps axfww
...
24585 ?        Ssl    0:07 /usr/bin/dockerd -H fd://
24594 ?        Ssl    0:00  \_ docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --shim docker-containerd-shim --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --runtime docker-runc
26942 ?        Sl     0:00      \_ docker-containerd-shim ec66fcfb5960c0779d07243f2c1e4d4ac10b855e940d416514057a9b28d78d09 /var/run/docker/libcontainerd/ec66fcfb5960c0779d07243f2c1e4d4ac10b855e940d416514057a9b28d78d09 docker-runc
26979 pts/1    Ss+    0:00          \_ bash
root@docker:~#

通过附加到容器,我们可以看到它正在运行一个单独的 bash 进程,而不是像 LXC 或 OpenVZ 那样使用完整的初始化系统:

root@docker:~# docker attach docker_container_1
root@ec66fcfb5960:/# ps axfw
PID TTY    STAT   TIME   COMMAND
1   ?        Ss     0:00   bash
10   ?       R+     0:00   ps axfw 
root@ec66fcfb5960:/# exit
exit
root@docker:~#

注意,在退出容器后,它会被终止:

root@docker:~# docker attach docker_container_1
You cannot attach to a stopped container, start it first
root@docker:~# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
ec66fcfb5960        ubuntu              "bash"              
3 minutes ago       Exited (0) 26 seconds ago                   docker_container_1
root@docker:~#

让我们重新启动它;我们可以像使用 OpenVZ 或 libvirt LXC 一样,指定其名称或 ID:

root@docker:~# docker start ec66fcfb5960
ec66fcfb5960 
root@docker:~# docker ps
CONTAINER ID        IMAGE               COMMAND          CREATED             STATUS              PORTS               
NAMES
ec66fcfb5960        ubuntu              "bash"              
3 minutes ago       Up 2 seconds                            docker_container_1
root@docker:~#

要更新容器的内存,执行以下命令:

root@docker:~# docker update --memory 1G docker_container_1
docker_container_1
root@docker:~# 

检查容器的内存设置,确保内存已成功更新:

root@docker:~# docker inspect docker_container_1 | grep -i memory
"Memory": 1073741824,
"KernelMemory": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": -1,
root@docker:~#

与 LXC 和 OpenVZ 一样,相应的 cgroup 层次结构已被更新。我们应该能够在容器 ID 的 cgroup 文件中看到相同的内存量:

root@docker:~# cat
/sys/fs/cgroup/memory/docker/ec66fcfb5960c0779d07243f2c1e4d4ac10b855e940d416514057a9b28d78d09/memory.limit_in_bytes
1073741824
root@docker:~#

让我们更新 CPU 配额:

root@docker:~# docker update --cpu-shares 512 docker_container_1
docker_container_1
root@docker:~#

然后,检查 cgroup 文件中的设置,将容器 ID 替换为正在你主机上运行的 ID:

root@docker:~# cat /sys/fs/cgroup/cpu/docker/ec66fcfb5960c0779d07243f2c1e4d4ac10b855e940d416514057a9b28d78d09/cpu.shares
512
root@docker:~#

Docker 提供了很少的监控容器状态和资源利用率的方法,非常类似于 LXC:

root@docker:~# docker top docker_container_1
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                27812               27774               0                   17:41               pts/1               00:00:00            bash 
root@docker:~# docker stats docker_container_1
CONTAINER           CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
docker_container_1   0.00%               1.809 MiB / 1 GiB   0.18%               648 B / 648 B       0 B / 0 B           1
^C
root@docker:~#

我们也可以在不附加到容器的情况下,在容器的命名空间内运行命令:

root@docker:~# docker exec docker_container_1 ps ax
PID TTY    STAT   TIME COMMAND
1 ?        Ss+    0:00 bash
11 ?       Rs     0:00 ps ax
root@docker:~#

使用以下命令将文件从主机文件系统复制到容器中;我们在 LXC 和 OpenVZ 中看过类似的例子:

root@docker:~# touch /tmp/test_file
root@docker:~# docker cp /tmp/test_file docker_container_1:/tmp/
root@docker:~# docker exec docker_container_1 ls -la /tmp
total 8
drwxrwxrwt  2 root root 4096 Dec 14 19:39 .
drwxr-xr-x 36 root root 4096 Dec 14 19:39 ..
-rw-r--r--  1 root root    0 Dec 14 19:38 test_file
root@docker:~#

使用 Docker 在主机之间移动容器更加简单;无需手动归档根文件系统:

root@docker:~# docker export docker_container_1 > docker_container_1.tar
root@docker:~# docker import docker_container_1.tar
sha256:c86a93369be687f9ead4758c908fe61b344d5c84b1b70403ede6384603532aa9 
root@docker:~# docker images
REPOSITORY        TAG           IMAGE ID           CREATED             SIZE
<none>           <none>        c86a93369be6     6 seconds ago       110.7 MB
ubuntu           latest        4ca3a192ff2a      2 weeks ago         128.2 MB
root@docker:~#

要删除本地镜像,请运行以下命令:

root@docker:~# docker rmi c86a93369be6
Deleted:sha256:c86a93369be687f9ead4758c908fe61b344d5c84b1b70403ede6384603532aa9
Deleted:sha256:280a817cfb372d2d2dd78b7715336c89d2ac28fd63f7e9a0af22289660214d32
root@docker:~#

Docker API 暴露了一种定义软件网络的方法,类似于我们在 libvirt LXC 中看到的内容。让我们安装 Linux 桥接并查看 Docker 主机上的内容:

root@docker:~# apt-get install bridge-utils
root@docker:~# brctl show
bridge name    bridge id         STP   enabled interfaces
docker0       8000.0242d6dd444c  no      vethf5b871d
root@docker:~#

请注意由 Docker 服务创建的docker0桥接。让我们列出自动创建的网络:

root@docker:~# docker network ls
NETWORK ID          NAME           DRIVER          SCOPE
a243008cd6cd        bridge         bridge          local
24d4b310a2e1        host           host            local
1e8e35222e39        none           null            local
root@docker:~# 

要检查bridge网络,请运行以下命令:

root@docker:~# docker network inspect bridge
[
{
 "Name": "bridge",
 "Id":"a243008cd6cd01375ef389de58bc11e1e57c1f3
         e4965a53ea48866c0dcbd3665",
 "Scope": "local",
 "Driver": "bridge",
 "EnableIPv6": false,
 "IPAM": {
 "Driver": "default",
 "Options": null,
 "Config": [
 {
 "Subnet": "172.17.0.0/16"
 }
 ]
 },
 "Internal": false,
 "Containers": {
"ec66fcfb5960c0779d07243f2c1e4d4ac10b8
               55e940d416514057a9b28d78d09": {
 "Name": "docker_container_1",
 "EndpointID":"27c07e8f24562ea333cdeb5c
                    11015d13941c746b02b1fc18a766990b772b5935",
 "MacAddress": "02:42:ac:11:00:02",
 "IPv4Address": "172.17.0.2/16",
 "IPv6Address": ""
 }
 },
 "Options": {
 "com.docker.network.bridge.default_bridge": "true",
 "com.docker.network.bridge.enable_icc": "true",
 "com.docker.network.bridge.enable_ip_masquerade": 
                "true",
 "com.docker.network.bridge.host_binding_ipv4":  
                "0.0.0.0",
 "com.docker.network.bridge.name": "docker0",
 "com.docker.network.driver.mtu": "1500"
 },
 "Labels": {}
 }
]
root@docker:~#

最后,要停止并删除 Docker 容器,请执行以下命令:

root@docker:~# docker stop docker_container_1
docker_container_1
root@docker:~#  docker rm docker_container_1

运行非特权 LXC 容器

简要讨论一下 LXC 的安全性。从 LXC 1.0 版本开始,引入了对非特权容器的支持,允许非特权用户运行容器。以 root 身份运行 LXC 容器的主要安全问题是,容器内的 UID 0 与主机上的 UID 0 相同;因此,突破容器将使你在服务器上获得 root 权限。

在第一章,Linux 容器简介中,我们详细讨论了用户命名空间以及它如何使得用户命名空间内的进程拥有与默认命名空间不同的用户和组 ID。在 LXC 的上下文中,这使得进程可以在容器内以 root 身份运行,同时在主机上具有非特权 ID。为了利用这一点,我们可以为每个容器创建一个映射,使用一组定义的 UID 和 GID 在主机和 LXC 容器之间进行映射。

让我们看一个设置并运行 LXC 容器作为非特权用户的示例。

首先,更新您的 Ubuntu 系统并安装 LXC:

root@ubuntu:~# apt-get update&& apt-get upgrade
root@ubuntu:~# reboot
root@ubuntu:~# apt-get install lxc

接下来,创建一个新用户并为其分配密码:

root@ubuntu:~# useradd -s /bin/bash -c 'LXC user' -m lxc_user
root@ubuntu:~# passwd lxc_user
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
root@ubuntu:~#

记下我们创建的新用户在系统中的 UID 和 GID 范围:

root@ubuntu:~# cat /etc/sub{gid,uid} | grep lxc_user
lxc_user:231072:65536
lxc_user:231072:65536
root@ubuntu:~#

请记下创建的 Linux 桥接的名称:

root@ubuntu:~# brctl show
bridge name bridge id         STP enabled interfaces
lxcbr0            8000.000000000000 no
root@ubuntu:~#

指定为用户可以添加到桥接的虚拟接口数量,在此示例中为50

root@ubuntu:~# cat /etc/lxc/lxc-usernet
# USERNAME TYPE BRIDGE COUNT
lxc_user veth lxcbr0 50
root@ubuntu:~#

接下来,作为lxc_user,创建目录结构和配置文件,如下所示:

root@ubuntu:~# su - lxc_user
lxc_user@ubuntu:~$ pwd
/home/lxc_user
lxc_user@ubuntu:~$ mkdir -p .config/lxc
lxc_user@ubuntu:~$ cp /etc/lxc/default.conf .config/lxc/
lxc_user@ubuntu:~$ 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 231072 65536
lxc.id_map = g 0 231072 65536
lxc_user@ubuntu:~$

前面的id_map选项将映射lxc_user的虚拟 ID 范围。

我们现在可以像往常一样创建容器:

lxc_user@ubuntu:~$ lxc-create --name user_lxc --template download
...
Distribution: ubuntu
Release: xenial
Architecture: amd64
Downloading the image index
Downloading the rootfs
Downloading the metadata
The image cache is now ready
Unpacking the rootfs
...
lxc_user@ubuntu:~$

容器处于停止状态;要启动它,请运行以下命令:

lxc_user@ubuntu:~$ lxc-ls -f
NAME     STATE   AUTOSTART GROUPS IPV4 IPV6
user_lxc STOPPED 0         -      -    -
lxc_user@ubuntu:~$ lxc-start --name user_lxc
lxc_user@ubuntu:~$ lxc-ls -f
NAME     STATE   AUTOSTART GROUPS IPV4 IPV6
user_lxc RUNNING 0         -      -    -
lxc_user@ubuntu:~$

请注意,容器中的进程由lxc_user而不是 root 拥有:

lxc_user@ubuntu:~$ ps axfwwu
lxc_user  4291  0.0  0.0  24448  1584 ?        Ss   19:13   0:00 /usr/lib/x86_64-linux-gnu/lxc/lxc-monitord /home/lxc_user/.local/share/lxc 5
lxc_user  4293  0.0  0.0  32892  3148 ?        Ss   19:13   0:00 [lxc monitor] /home/lxc_user/.local/share/lxc user_lxc
231072    4304  0.5  0.0  37316  5356 ?        Ss   19:13   0:00  \_ /sbin/init
231072    4446  0.1  0.0  35276  4056 ?        Ss   19:13   0:00      \_ /lib/systemd/systemd-journald
231176    4500  0.0  0.0 182664  3084 ?        Ssl  19:13   0:00      \_ /usr/sbin/rsyslogd -n
231072    4502  0.0  0.0  28980  3044 ?        Ss   19:13   0:00      \_ /usr/sbin/cron -f
231072    4521  0.0  0.0   4508  1764 ?        S    19:13   0:00      \_ /bin/sh /etc/init.d/ondemand background
231072    4531  0.0  0.0   7288   816 ?        S    19:13   0:00      |   \_ sleep 60
231072    4568  0.0  0.0  15996   856 ?        Ss   19:13   0:00      \_ /sbin/dhclient -1 -v -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
231072    4591  0.0  0.0  15756  2228 pts/1    Ss+  19:13   0:00      \_ /sbin/agetty --noclear --keep-baud pts/1 115200 38400 9600 vt220
231072    4592  0.0  0.0  15756  2232 pts/2    Ss+  19:13   0:00      \_ /sbin/agetty --noclear --keep-baud pts/2 115200 38400 9600 vt220
231072    4593  0.0  0.0  15756  2224 pts/0    Ss+  19:13   0:00      \_ /sbin/agetty --noclear --keep-baud pts/0 115200 38400 9600 vt220
231072    4594  0.0  0.0  15756  2228 pts/2    Ss+  19:13   0:00      \_ /sbin/agetty --noclear --keep-baud console 115200 38400 9600 vt220
231072    4595  0.0  0.0  15756  2232 ?        Ss+  19:13   0:00      \_ /sbin/agetty --noclear --keep-baud pts/3 115200 38400 9600 vt220
lxc_user@ubuntu:~$

当以非 root 用户运行容器时,根文件系统和配置文件的位置是不同的:

lxc_user@ubuntu:~$ ls -la .local/share/lxc/user_lxc/
total 16
drwxrwx---  3   231072 lxc_user 4096 Dec 15 19:13 . 
drwxr-xr-x  3 lxc_user lxc_user 4096 Dec 15 19:13 ..
-rw-rw-r--  1 lxc_user lxc_user  845 Dec 15 19:13 config
drwxr-xr-x 21   231072   231072 4096 Dec 15 03:59 rootfs
-rw-rw-r--  1 lxc_user lxc_user    0 Dec 15 19:13 user_lxc.log
lxc_user@ubuntu:~$ cat .local/share/lxc/user_lxc/config | grep -vi "#" | sed '/^$/d'
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
lxc.include = /usr/share/lxc/config/ubuntu.userns.conf
lxc.arch = x86_64
lxc.id_map = u 0 231072 65536
lxc.id_map = g 0 231072 65536
lxc.rootfs = /home/lxc_user/.local/share/lxc/user_lxc/rootfs
lxc.rootfs.backend = dir
lxc.utsname = user_lxc
lxc.network.type = veth
lxc.network.link = lxcbr0
lxc.network.flags = up
lxc.network.hwaddr = 00:16:3e:a7:f2:97
lxc_user@ubuntu:~$

总结

在本章中,我们看到了如何使用 OpenVZ 和 Docker 等替代技术部署容器的示例。

OpenVZ 是最古老的容器解决方案之一,直到写作时,它正在重新命名为 Virtuozzo。LXC 和 OpenVZ 之间的主要区别在于 OpenVZ 运行的自定义 Linux 内核。它基于 Red Hat 内核,并且很快将以单一安装 ISO 的形式发布,而不像我们在之前的示例中使用的那样,依赖于打包的内核和用户空间工具。

Docker 是容器化领域的事实标准,也是采纳的领导者。作为较新的容器化技术之一,它的易用性和可用的 API 使其成为大规模运行微服务的理想解决方案。Docker 不需要修补内核即可工作,并且集中式注册表用于存储容器镜像,使其在许多场景中成为一个绝佳的选择。

我们通过展示如何运行非特权的 LXC 容器的示例结束了这一章。这个功能相对较新,它在容器安全性方面迈出了正确的一步。

posted @ 2025-07-08 12:23  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报