梁晓勇 译 分布式实验室​对于Docker容器的使用方式,千万不要将错就错了!_Jav


本文是去年在都柏林举行的欧洲LinuxCon大会上我所做演讲的文字说明。由于Linux Foundation Events尚未提供演讲视频(他们所记录和发布的都是演讲稿),我无法给出视频链接,不过还是欢迎你阅读该演讲的幻灯片(点击阅读原文链接可查看PPT)。


​对于Docker容器的使用方式,千万不要将错就错了!_Jav_02


假设你是一名运维人员,在一次大规模伸缩和高度自动化部署中,你负责保持成百上千个容器启动与运行。开发人员将这些容器放置在一起,然后就简单地丢给你进行管理。你的任务只是保持它们存活、可用及安全;至于里面是什么则是开发人员的事。当然,你有用于构建容器的Git仓库,以及一个Docker registry,因此你可以随时检查容器里内容。不过,你做不了主。

进一步假设,你的所有容器都运行某种形式的web服务。这里为了方便讨论,我们假定它们全都运行着Apache,因为这是你的基准平台。开发人员可能会使用Python或Ruby或(最好的语言)PHP来编写应用程序,不过所有应用的共同点是你已经确定Apache作为基准平台。开发人员可以假定使用Apache,你作为运维人员,了解风险所在,并能为他们提供一个极其稳定的、优化的平台用于构建。

然后,Apache受到了某种令人不安的安全漏洞影响,你必须在极短时间内修复。比如,那些影响核心SSL库甚至是C库的东西。听起来很耳熟?不用怀疑。

非容器化世界里的修复

好了,在预期的***火力袭来之前,你现在必须在极短时间内修复所有系统上的OpenSSL或libc。在没有容器的世界里,你依赖于你信任的软件来源(通常是你的发行版提供商)提供针对受影响库的一个或多个修复包。然后,通过你喜欢的软件管理工具、系统自动化设施或无人值守升级方案来实施更新。

简而言之,在获得更新包之前你会有段紧张的时光,不过一旦他们可用了,就能在几分钟内完成修复。

但是现在呢?

随着容器部署的到来,打包、包管理或依赖跟踪的概念经常是个糟糕的主意。相反的,你会将所有需要的东西放入一个容器镜像,为每个服务部署一个容器,并且不必考虑运行在同一物理硬件上的不同服务的可能需求。

乍一看,这让事情变得简单了。开发人员需要以某种方式配置MySQL,而其他应用有不同要求?没关系,他们可以将所有东西旋转在自己单独的容器里,包括二进制文件、库文件及所有其他东西,问题解决。存储非常廉价,容器效率很高,额外开支很小。如果他们要做任何改变,比如从一个MySQL版本过渡到另一个版本,他们只需要重新构建容器,然后你用新的构建替换旧的,搞定。

不过现在要做改变不是开发人员,而是你,你想要部署一个关键性修复。

因此,你着手开始重新构建数百个容器,也可能是数千个,来修复这个问题。在一个完美的环境中,你有权访问每个构建链,清楚所负责区域的每个容器的每个版本,并能准确找出哪些受到漏洞影响,具有一个自动化工具链对其进行构建和部署,拥有完善的文档因而不需要与任何开发人员核实,因此就算有人在部署(目前可能受影响的)服务之后请了病假、度假或离职也没有关系。

当然,所有人都工作在这样一个完美的环境中。对吧?

那么现在,即便问题的修复方案已经出来后,你还是需要抓紧时间进行部署,其部署相比无容器的世界要复杂的


​对于Docker容器的使用方式,千万不要将错就错了!_Jav_03


当然不是。这个问题与你使用容器的事实或所使用的具体容器技术无关。问题在于所有人都在告诉你以某种方式使用容器,而从一个运维角度来说这种方式是错的。甚至算不上“有错,但还是比其他选项要好”,它完全是错的。我猜你会叫它Docker谬论。

这是个坏消息。好消息是有种更好、更健全也更简洁的方式,能让你的运维生涯舒适,同时对你的开发伙伴又不会太难。


​对于Docker容器的使用方式,千万不要将错就错了!_Jav_04


你可以以一种更简单、不那么花哨、不那么令人兴奋——总之,更好的方式来使用容器。

定义一个或多个核心平台

任何称职的组织都会选择几个发行版来构建产品和服务。可能甚至只有一个,不过假定你有多个,比如最新的Ubuntu LTS(在写作本文时,最新版的Ubuntu LTS是14.04,Trusty Tahr,这是基于Linux 3.13内核的。这个Ubuntu使用的kernel带有一个OverlayFS的预发布版本,早于3.14主线合并。我不推荐使用这个内核;相反的,你应使用LTS Enablement Stack中最新的内核来运行宿主机。在写作本文时,这是一个Linux 4.2内核,以generic-lts-wily包发行。)、最新的CentOS以及最新的Debian。你可以为每一个平台定义一个完全精简的包列表。我几乎可以向你保证没有开发人员会对关心列表中的任何一项。一个C库、一个shell、一个init系统、coreutils、NTP……很可能你会得到一个包含超过100个核心系统组件的清单,这些都需要你保持其安全性;同时开发人员会将其视为标配。

感谢软件包管理者和发行商多年来孜孜不倦的辛劳,你所能确信的是你将获得所有组件及时的安全更新。

根据需要部署核心平台

在你的物理硬件上部署这些基准平台。部署数量以满足各平台上预计要运行的全部容器的需求为准。以自动化方式来完成这些操作,这样就不需要手工登录到这些系统中。

为容器使用OverlayFS

OverlayFS(https://en.wikipedia.org/wiki/OverlayFS)是一个作为主线内核一部分发行的联合挂载文件系统。通过OverlayFS,你可以完成几件巧妙的事:

  • 使用一个只读的基础文件系统以及一个可写的叠加层来创建一个读/写联合挂载。

  • 写入联合挂载时,只触及叠加层,基础文件系统保持不变。

  • 通过使用不透明目录(opaque directories),对联合挂载隐藏基础文件系统中所选择的内容。

  • 使用一个基础文件系统和多个叠加层来创建任意数量的独立的读/写联合挂载。

  • 通过简单地重新挂载,立即更新所有联合挂载知晓的基础文件系统。

这让OverlayFS在与LXC一起使用时显得非常强大。你可以定义一堆的叠加层目录——每个容器一个——并且全都共享一个基础文件系统:你的宿主机根文件系统。(默认情况下,LXC容器会运用某些容器专用目录,比如/proc/dev以及/sys。可以通过在容器叠加层中创建不透明目录来隐藏其他宿主机文件系统内容;通常会对/root/home/tmp等目录这么操作。)

然后,联合挂载就变成了LXC容器的根目录。它自动具有宿主机上所有东西的读权限(除非特意隐藏),同时所写入的内容将进入叠加层中。当你丢弃一个容器,只是删除其叠加层。

以下是一个这类容器的最小示例配置:

# 更多配置选项请查看 lxc.container.conf(5)
# 通用配置
lxc.include = /usr/share/lxc/config/ubuntu.common.conf
# 容器专用配置
lxc.arch = amd64
# 网络配置
lxc.network.type = veth
lxc.network.link = lxcbr0
lxc.network.flags = up
lxc.network.hwaddr = 00:16:3e:76:59:10
# 自动挂载
lxc.mount.auto = proc sys cgroup

lxc.rootfs = overlayfs:/var/lib/lxc/host/rootfs:/var/lib/lxc/mytestcontainer/delta0
lxc.utsname = mytestcontainer

需要注意的是LXC用户区(userland)目前要求OverlayFS基础目录要存放在/var/lib/lxc子目录下。你可以像上述示例那样,将/绑定挂载到/var/lib/lxc/host/rootfs来满足这一要求。

它所创造的东西之一是关注点明确的分隔:叠加层里的东西完全由开发人员决定。他们可以从PyPI、Ruby Gems、NPM及其他地方拉取包。而宿主机根目录下的东西则由你负责。

自动化,自动化,自动化

这点很明显,而且不言而喻,不过再次重申也无妨:你想让一切自动化。当然,你可以选择自己的工具来完成,不过Ansible具有非常好的LXC容器支持,可以轻松实现这一点。

以下是一个简单的Ansible playbook示例用于创建100个容器,全部基于宿主机根目录。(请注意,没有该Ansible示例所显示的那么简单。你可能需要一些额外的微调,比如添加挂载或不透明目录。为了展示概念,我对示例进行了简化。)

- hosts: localhost
  tasks:
    - name: Create a local bind mount for the host root filesystem
      mount:
        name: /var/lib/lxc/host/rootfs
        src: /
        opts: bind
        fstype: none
        state: mounted
    - name: Create a template container using the host root
      lxc_container:
        name: host
        state: stopped
        directory: /var/lib/lxc/host/rootfs
        config: /var/lib/lxc/host/config
        container_config:
          - "lxc.mount.auto = proc sys cgroup"
          - "lxc.include = /usr/share/lxc/config/ubuntu.common.conf"
    - name: Create 100 OverlayFS based containers
      lxc_container:
        name: host
        backing_store: overlayfs
        clone_snapshot: true
        clone_name: "mytestcontainer{{ item }}"
        state: started
      with_sequence: count=100

当然,这也意味着你需要让开发人员将他们的容器配置定义在Ansible中。不过,这基本上是一件好事,因为它意味着开发和运维人员将使用同一个语言进行读写。同时,如果开发人员会编写Dockerfile,他们对Ansible YAML也不会太陌生。


​对于Docker容器的使用方式,千万不要将错就错了!_Jav_05


想一下,使用这个方法,要更新同一个主机上运行着的数百个容器里的libc,你现在所需要做的事情。

  • 更新宿主机的libc。

  • 重启容器。

就这么简单。这真的就是一瞬间更新数百个容器你所需要做的。LXC将在容器重启时重新挂载你的OverlayFS,因此所有针对宿主机的变化都将立即反映到容器的叠加层文件系统中。

在一个Ubuntu平台上,你甚至可以结合无人值守来自动化这一步:

# /etc/apt/apt.conf.d/50unattended-upgrades
// Automatically upgrade packages from these (origin:archive) pairs
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
# /etc/apt/apt.conf.d/05lxc
DPkg::Post-Invoke      { "/sbin/service lxc restart"; };

就这么多。在几分钟内更新大量容器。无须重新构建、无须重新部署,什么都不需要。不管销售人员怎么向你推销,打包确实是可行的、具有优势的。