分布式机器学习的故事:Docker 改变世界Docker 最近很火。Docker 实现了“集装箱”一种介于“软件包”和“虚拟机”之间的概念并被寄予厚望,以期革新 Internet 服务以及其他大数据处理系统的开发、测试、和部署流程。 为了使用 Docker,需要了解不少工具及其设计思路;而这些工具的文档分布在不同的网站。为了方便大家学习,本文以开发一个极简的搜索引擎为例,展示 Docker 带来的革新。 说是革新,其实是 Google 已经用了十年的方式,只是最近才因为 Docker 开源项目而广为人知。Eric Brewer(Google VP of Infrastructure)在 Dockercon14 活动上的演讲回顾了这段历程。目前,Google 每周会执行 20 亿个集装箱。可以说,最近十年是各互联网公司和高校都在奋力模仿 Google 的计算技术的十年。了解这一模仿的过程,可以帮助我们深入理解分布式系统(包括现在常说的“大数据系统”)中若干重要问题。为此,本文以技术教程为主线,穿插了一些关于 Hadoop 和 Mesos 等“模仿”项目的介绍,简要追溯它们勇敢而艰难的“邯郸学步”的历程。最后,本文会介绍 Google 最近公布的“正确答案”KubernetesGoogle 核心技术 Borg 的开源版本。 DockerDocker 是一个软件系统,实现了一种称为“集装箱”的概念。集装箱类似 Google 机群管理系统 Borg 中的包(package)。 通常我们说的“包”是软件包比如 Ubuntu/Debian Linux 里常见的.deb 文件安装的时候,安装程序会把被依赖的包也装上。可是执行的时候呢?得根据具体情况配置,然后依次启动互相依赖的多个程序。比如,启动一个 Web 服务之前,要启动 Apache 和 MySQL;而且他们仨都得有合理的配置,确保它们能一起工作,来实现这个 Web 服务。 但是 Docker 集装箱以及 Borg 中的包更像虚拟机。虚拟机里包括程序和配置,所以可以被执行也就是执行其中的程序。因为程序是配置好的,所以虚拟机可以被扔到各种环境上去执行包括开发机、做演示用的笔记本电脑、用 VirtualBox 虚拟的机群、测试机群、预发布环境和产品环境。近几年随着“云计算”概念的普及,虚拟机被广泛使用,作为分布式计算的基础调度单元。 Docker 作为一个软件系统,可以用来创建“集装箱镜像”(container image)和执行这些镜像。就像 VirtualBox 是一个软件系统,可以用来创建和执行虚拟机。但是集装箱比虚拟机“轻”一个虚拟机包括一组虚拟硬件、操作系统,用来执行用户程序;而集装箱里没有虚拟的硬件,也没有操作系统,它用主机(host)的硬件和操作系统来执行程序。 那么在集装箱里跑程序和直接在主机上跑有什么区别呢?一个区别是,集装箱有一套网络端口空间(port space)。一个集装箱里的进程可以各自开端口,也可以连接对方的端口进行通信。但是这些端口是集装箱之外的进程看不到的。我们也可以让集装箱把某些内部端口号展示给外部,比如把集装箱内的端口 5000 映射到外部的 8080。这样,当我们用主机上的程序(比如浏览器)访问本机(主机)的 8080 端口时,实际上访问的是集装箱里的 5000 端口。这项对外公开集装箱内部端口的技术,称为端口转发(port forwarding),和虚拟机的端口转发概念一样。另一个区别在于,集装箱里有虚拟的文件系统。这样我们可以把要执行的程序拷贝进集装箱。也可以把主机上的某些目录映射成集装箱虚拟文件系统的某些目录。 集装箱这个想法已经在深刻地改变传统分布式系统的开发、测试和部署的流程了。传统的做法是,开发者写一个 Makefile(或者其他描述,比如 CMakeList、POM 等)来说明如何把源码编译成二进制文件。随后,开发人员会在开发机上配置并且执行二进制文件,来作测试。测试人员会在测试机群上配置和执行,来作验证。而运维人员会在数据中心里的预发布环境和产品环境上配置和执行,这就是部署。因为开发机、测试机群、和产品环境里机器的数量和质量都不同,所以配置往往很不同。加上每个新版本的软件系统,配置方式难免有所差异,所以经常造成意外错误。以至于绝大部分团队都选择趁夜深人静、用户不活跃的时候,上线新版本,苦不堪言。 而利用集装箱概念的开发流程里,开发者除了写 Makefile,还要写一个 Dockerfile,来描述如何把二进制文件安装进一个集装箱镜像(container image),并且做好配置。而一个镜像就像一台配置好的虚拟机,可以在机群上启动多个实例(instance),而每个实例通常称为一个集装箱(container)。在自测的时候,开发者在开发机上执行一个或者多个集装箱;在验证时,测试人员在测试机群上执行集装箱;在部署时,运维人员在产品环境执行集装箱。因为执行的都是同样地集装箱,所以不容易出错。 这种流程更合理的划分了开发者和其他角色的工作边界,也大大简化了测试和部署工作。 boot2docker上节提到,Docker 虚拟了网络地址空间和文件系统。实际上,它还虚拟了进程 ID 空间(pid space)等系统数据结构。这些功能是一个叫 dockerd 的 daemon 程序借助 Linux 内核中的 control groups(又叫 cgroups)功能实现的。 dockerd 负责执行集装箱;就像 VirtualBox 负责执行虚拟机一样。而 cgroup 是 Google 的两个工程师 Paul Menage 和 Rohit Seth 贡献给 Linux 社区的。cgroups 的使用始于 2006 年。但是从他们的工作记录看,主要工作持续到 2008 和 2009 年。据说,Google 开发它就是为了方便在自己的机群上部署各种 Internet 应用和离线处理系统。具体一点儿的故事,请看这篇 Information Week 上的帖子。。 因为 cgroups 功能只有 Linux 内核有,所以 Docker 目前只能运行在 Linux 上。可是,现在很多开发者都在用 Mac。为了能让这些开发者方便的测试自己创作的集装箱镜像,Docker 的开发者写了 boot2docker利用 VirtualBox 虚拟一个 Linux 主机,并且在上面安装 dockerd。而命令行控制程序 docker 执行在 Mac 主机上,被配置成和虚拟 Linux 主机上的 dockerd 协作。 boot2docker 的安装方式很简单:照着这个流程,下载并执行一个安装包即可。因为 boot2docker 利用了 VirtualBox,所以安装它之前需要先装 VirtualBox。Homebrew 也提供了安装 boot2docker 的选项,但是可能因为 bug 导致 dockerd 和 docker 版本不同,没法协同工作。 在利用 boot2docker 在 Mac 上开始工作之前,还有几个注意事项。当我们在 Linux 主机上启动一个集装箱的时候,我们可以让 Docker 把主机的某些目录映射成集装箱内的目录。这样集装箱里的程序和主机上的程序共享数据,是一种方便的调试方式。但是在用 boot2docker 的时候,“主机”不是 Mac,而是虚拟 Linux 主机。此时如果想把 Mac 上的目录映射到集装箱,先得将其通过 VirtualBox 映射到 Linux 主机。 另一个注意事项和端口转发有关。当我们把集装箱内的某个端口映射为主机的某个端口时,只是映射到了虚拟 Linux 主机;如果想让 Mac 上的程序能访问,还得把虚拟机端口通过 VirtualBox 映射成 Mac 上的端口。这些注意事项,在下文中会有详细解释。 CoreOS实际开发中的测试机群和产品环境通常都是用的 Linux 服务器。要在上面执行集装箱,也需要安装 Docker。因为 Docker 的开发者提供各种 Linux 软件包,所以通常输入一个命令,即可安装 Docker。比如在 Ubuntu/Debian Linux 里,这个命令是:
但是目前最常用的用来执行 Docker 集装箱的 Linux 发行版本既不是 Ubuntu、Debian 也不是 RedHat、Fedora,而是 CoreOS。这个发行版本根本没有软件包管理程序,所以也不能通过输入某个命令来安装软件。但是 CoreOS 预装了 Docker,所以可以制作集装箱镜像,或者下载别人发布的集装箱镜像来执行。目前,Amazon AWS 和 Google Compute Engine 这两大云计算平台都提供预装了 CoreOS 的虚拟机。 实际上,Google 数据中心里运行的 Linux 系统和 CoreOS 有很多相似之处。我记得 2010 年我刚离开 Google 加入腾讯的时候,一位腾讯的同事好奇地问:“Google 的机群里用的 Linux 用什么软件包管理程序?是 apt-get 吗?还是 yum?”我回答:“其实服务器上运行的 Linux 是不需要包管理的,只有桌面 Linux 系统才需要”。这位同事很难相信。其实,要不是因为“见了一回猪跑”,我也想不到会是这样。 CoreOS 和其他 Linux 发行版本相比,执行效率高、内存耗费省;此外,利用双磁盘分区技术,即便是更新 Linux 内核也不需要重启。CoreOS 还有很多独特之处,使得它在问世后很短的时间里就被 Amazon 和 Google 采用。如果想进一步了解这些特性,请看这个对 Docker 作者的访谈。 Go 语言接下来,我们看看如何在 Mac 上用 Go 语言写一个极简化的搜索引擎,并且封装成集装箱镜像。 我们选择 Go 语言为例,而不是更常见的 Java、Python、Perl、Ruby、Scala 等,有很现实的原因后面这些语言写的程序,在执行时都需要某些运行环境的支持。比如,Java 程序依赖 Java 虚拟机,Python 程序需要 Python 解释器,这些加上预装的程序库需要占用几百 MB 的集装箱空间。而用 Go 写的程序默认是全静态编译的,执行时不需要任何环境支持,不需要预装库,甚至连 Linux 系统动态库都不依赖。鉴于一家公司的系统往往由成千上万的集装箱构成;每个集装箱少几百 MB,能为公司省出很大一笔开销。那些每月要向 Amazon 或者 Goolgle 付账的公司,对此必然印象深刻。这是 Go 语言在很多创业公司拓展迅猛的一个原因。 交叉编译如果我们用 C 或者 C 开发,也可以生成全静态链接的二进制程序文件。但是在 Web 时代,C/C 的开发效率不如 Go。Google 里倒是普遍使用 C ,但是 Google 里有一套精心设计、积攒多年的 C 库,这是外界没有的。外界普遍得使用第三方库,并往往因此挠头。比如,不同的第三方库(Thrift 和 boost)各有各的线程池机制,很难统一管理多线程。C 11 倒是有了标准线程管理,但是把很多库统一到 C 11 是一项开销极大的工作。Go 语言是专门为分布式系统开发设计的,根本就没有线程的概念,在语法上用 goroutine 代替了,线程池实现在 Go runtime 里,被编译进每个二进制程序。 因为集装箱用主机的操作系统和硬件来执行程序,而 Docker 只支持 Linux,所以 Go 程序必须被编译成 Linux 二进制文件,才能通过 Docker 运行。而我们在 Mac 上开发,需要利用交叉编译技术来生成 Linux 二进制文件。 为了得到一个支持交叉编译的 Go 语言编译器,我们需要从源码安装 Go,并且需要做一些额外的安装工作。具体过程如下:
这里,我们用到了 Dave Cheney 写的一个 Bash 脚本程序。这个程序支持生成以下平台上的 Go 语言标准库:
并行计算最常用的目标平台是 linux/amd6464bit 的 Linux 系统,也是 CoreOS 的平台格式。下文中我们会演示如何在 Mac 下用这个编译器生成 Linux 平台的二进制代码文件。 极简版搜索引擎在这篇帖子里,作者 Adriaan de Jonge 用一个最简单的 http server 作为例子,说明如何在 Mac 下用 Docker 运行一个程序。 这篇帖子对我很有帮助。只是这个例子程序太过简单了通常一个互联网产品包含不只一个程序现代互联网产品几乎都采用 micro service架构,一个 http server 和多个 RPC server 协同工作。之外,还会有一些 daemon 程序,不时向 RPC server 提供不断更新的数据。比如在搜索引擎里,一个 indexer 程序会不断将 cralwer 程序爬下来的网页内容加以整理,并且发送给搜索引擎服务。 本节里我们介绍的极简版的搜索引擎就包括两个程序search engine server 和向它提供索引内容的 indexer daemon。search engine server 首先是一个 http server,可以通过浏览器访问对每个输入的 query,返回相应的结果。同时,它还是一个 RPC server,接受从 indexer daemon 发来的更新后的索引内容。这两个程序的源码在这里。 为了下载和构建这个例子程序,请输入如下命令:
此时,在 /tmp/learn-docker/bin 目录里应该有两个二进制程序文件 indexer 和 searchengine。这两个文件都是 Darwin/AMD64 格式的。我们可以在 Mac 主机上运行它俩: |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|