1.3 虚拟环境∗
由于Spark程序开发的生态环境相对比较复杂和多样,对于初学者会很困难。本教程利用虚拟化技术,选择Docker作为虚拟化实验平台,实现开发环境的快速统一部署。Docker是一个开放源代码软件项目,让应用程序部署在软件容器的工作模式下,可以自动化进行,借此在Linux操作系统上,提供一个额外的软件抽象层,以及操作系统层虚拟化的自动管理机制。Docker利用Linux核心中的资源分脱机制,如cgroup,以及Linux核心名字空间,创建独立的软件容器(Container)。这可以在单一Linux实体下运作,避免引导一个虚拟机造成的额外负担。Linux核心对名字空间的支持完全隔离了工作环境中应用程序的视野,包括进程树、网络、用户ID与挂载文件系统,而核心的cgroup提供资源隔离,包括CPU、内存、block I/O与网络。从0.9版本起,Docker使用的抽象虚拟是在LXC与systemd-nspawn提供接口的基础上,开始包括libcontainer函数库作为以独立的方式开始直接使用由Linux核心提供的虚拟化组件。
依据行业分析公司451研究,“Docker是有能力打包应用程序及其虚拟容器,可以在任何Linux服务器上运行的依赖性工具,这有助于实现灵活性和便携性,应用程序在任何地方(如公有云、私有云、单机等)都可以运行”。
1.3.1 发展历史
Docker最初是dotCloud公司创始人Solomon Hykes在法国发起的一个公司内部项目,它是基于dotCloud公司多年云服务技术的一次革新,并于2013年3月以Apache 2.0授权协议开源,主要项目代码在GitHub上进行维护。Docker项目后来还加入了Linux基金会,并成立推动开放容器联盟。
Docker自开源后受到广泛的关注和讨论,至今其GitHub项目已经超过36 000个星标和10 000多个Fork。甚至由于Docker项目的火爆,在2013年年底,dotCloud公司决定改名为Docker。Docker最初是在Ubuntu 12.04上开发实现的;Red Hat则从RHEL 6.5开始对Docker进行支持;谷歌也在其PaaS产品中广泛应用Docker。
Docker使用谷歌公司推出的Go语言进行开发实现,基于Linux内核的cgroup、namespace以及AUFS类的Union FS等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其他的隔离的进程,因此也称其为容器。最初实现是基于LXC,从0.7以后开始去除LXC,转而使用自行开发的libcontainer,从1.11开始,则进一步演进为使用runC和containerd。
Docker在容器的基础上进行了进一步的封装,从文件系统、网络互联到进程隔离等,极大地简化了容器的创建和维护,使得Docker技术比虚拟机技术更轻便、快捷。
下面的图片比较了Docker和传统虚拟化方式。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需的应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟,因此容器要比传统虚拟机更轻量化(见图1-5)。
图1-5 传统虚拟化技术与Docker虚拟化技术的区别
虚拟机和容器都是在硬件和操作系统上的,虚拟机有Hypervisor层,Hypervisor是整个虚拟机的核心。它为虚拟机提供了虚拟的运行平台,管理虚拟机的操作系统运行。每个虚拟机都有自己的系统和系统库以及应用。容器没有Hypervisor这一层,并且每个容器都和宿主机共享硬件资源及操作系统,那么由Hypervisor带来性能的损耗,在Linux容器这边是不存在的。但是,虚拟机技术也有其优势,能为应用提供一个更加隔离的环境,不会因为应用程序的漏洞给宿主机造成任何威胁,同时还支持跨操作系统的虚拟化,例如可以在Linux操作系统下运行Windows虚拟机。从虚拟化层面看,传统虚拟化技术是对硬件资源的虚拟,容器技术则是对进程的虚拟,从而可提供更轻量级的虚拟化,实现进程和资源的隔离。从架构看,Docker比虚拟化少了两层,取消了Hypervisor层和Guest OS层,使用Docker Engine进行调度和隔离,所有应用共用主机操作系统,因此在体量上,Docker较虚拟机更轻量级,在性能上优于虚拟化,接近裸机性能。从应用场景看,Docker和虚拟化有各自擅长的领域,在软件开发、测试场景和生产运维场景中各有优劣势。
1.3.2 技术特征
作为一种新兴的虚拟化方式,Docker与传统的虚拟化方式相比具有众多的优势。由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,因此Docker对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。传统的虚拟机技术启动应用服务往往需要数分钟,而Docker容器应用,由于直接运行于宿主内核,无须启动完整的操作系统,因此可以做到秒级,甚至毫秒级的启动时间,大大节约了开发、测试、部署的时间。开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些bug并未在开发过程中被发现。而Docker的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性。
对开发和运维人员最希望的是一次创建或配置,可以在任意地方正常运行。使用Docker可以通过定制应用镜像实现持续集成、持续交付、部署。开发人员可以通过Dockerfile进行镜像构建,并结合持续集成系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合持续部署系统进行自动部署。而且使用Dockerfile使镜像构建透明化,不仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好地在生产环境中部署该镜像。由于Docker确保了执行环境的一致性,使得应用的迁移更加容易。Docker可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此,用户可以很轻易地将在一个平台上运行的应用迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。
Docker使用的分层存储以及镜像的技术,使得应用重复部分的复用更容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,Docker团队同各个开源项目团队一起维护了一大批高质量的官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大降低了应用服务的镜像制作成本。两种虚拟化技术的对比见表1-2。
表1-2 两种虚拟化技术的对比
1.3.3 技术架构
Docker使用客户端和服务器架构,客户端与守护进程进行对话,该守护进程完成了构建、运行和分发容器的繁重工作。客户端和守护程序可以在同一系统上运行,或者可以将客户端连接到远程守护程序。客户端和守护程序在UNIX套接字或网络接口上使用REST API进行通信。守护程序侦听Docker API请求并管理Docker对象,如图像、容器、网络和卷。守护程序还可以与其他守护程序通信以管理Docker服务。客户端是许多用户与Docker交互的主要方式,当使用Docker命令时,客户端会将这些命令发送到守护程序,以执行这些命令,Docker客户端可以与多个守护程序通信。注册中心存储Docker镜像。Docker Hub是一个由Docker公司负责维护的公共注册中心,包含可用来下载和构建容器的镜像,并且还提供认证、工作组结构、工作流工具、构建触发器以及私有工具(如私有仓库可用于存储并不想公开分享的镜像)。Docker Hub是任何人都可以使用的公共注册中心,并且Docker配置为默认在Docker Hub上查找映像,也可以运行自己的私人注册中心。使用docker pull或docker run命令时,所需的镜像将从配置的注册中心中提取,使用docker push命令时,会将镜像推送到配置的注册中心。使用Docker时,可以创建和使用镜像、容器、网络、数据卷、插件和其他对象。下面对其中一些技术架构中的对象和组件进行简单介绍。Docker技术框架如图1-6所示。
图1-6 Docker技术框架
1.3.3.1 镜像
镜像是用于创建容器的只读指令模板,是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了为运行时准备的一些配置参数(如匿名卷、环境变量、用户等),不包含任何动态数据,其内容在构建之后也不会被改变。因为镜像包含操作系统完整的根文件系统,其体积往往是庞大的,因此在Docker设计时,就充分利用Union FS的技术,将其设计为分层存储的架构。所以,严格来说,镜像并非像一个ISO的打包文件,它只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。例如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西都应该在该层构建结束前清理掉。分层存储的特征还使得镜像的复用、定制变得更容易,甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。
可以创建自己的镜像,也可以仅使用其他人创建并在注册中心中发布的镜像。要构建自己的镜像,可使用简单的语法创建一个Dockerfile文件,以定义创建镜像并运行它所需的步骤。Dockerfile中的每条指令都会在镜像中创建一个层,更改Dockerfile并重建镜像时,仅重建那些已更改的层。与其他虚拟化技术相比,这是使镜像如此轻巧、小型和快速的部分原因。
1.3.3.2 容器
容器是镜像的可运行实例。可以使用Docker API或CLI创建、启动、停止、移动或删除容器,可以将容器连接到一个或多个网络,将存储附加到该网络,甚至根据其当前状态创建新镜像。默认情况下,容器与其他容器及其主机之间的隔离程度相对较高,可以控制容器的网络、存储或其他基础子系统与其他容器或与主机的隔离程度。容器的实质是进程,但与直接在主机上执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此,容器可以拥有独立的根文件系统、网络配置、进程空间,甚至用户空间。容器内的进程运行在一个隔离的环境里,使用起来好像是在一个独立于主机系统下操作一样。这种特性使得容器封装的应用比直接在主机运行更加安全,也因为这种隔离的特性,很多人初学Docker时常常会混淆容器和虚拟机。
前面讲过镜像使用的是分层存储,容器也是如此。每个容器运行时都以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层。容器存储层的生存周期和容器一样,容器消亡时容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。按照Docker最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用数据卷或者绑定主机目录,在这些位置的读写会跳过容器存储层,直接对主机(或网络存储)发生读写,其性能和稳定性更高。数据卷的生存周期独立于容器,容器消亡数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据不会丢失。容器由其镜像以及在创建或启动时为其提供的任何配置选项定义。删除容器后,未存储在持久性存储中的状态更改将消失。下面通过示例docker run命令讲解镜像和容器的关系。以下命令运行一个ubuntu容器,运行/bin/bash以交互方式附加到本地命令行会话。
当运行此命令时,假设使用的是默认注册中心的配置,则会发生以下情况。
(1)如果在本地没有ubuntu镜像,则Docker会将其从已配置的注册中心拉出,就像手动执行了docker pull ubuntu命令。
(2)Docker会创建一个新容器,就像手动执行了docker container create命令一样。
(3)Docker将一个读写文件系统分配给容器作为其最后一层。这允许运行中的容器在其本地文件系统中创建或修改文件和目录。
(4)Docker创建了一个网络接口将容器连接到默认网络,包括为容器分配IP地址。默认情况下,容器可以使用主机的网络连接到外部网络。
(5)Docker启动容器并执行/bin/bash,因为容器是交互式运行并且已附加到终端(由于-i和-t标志),所以可以在输出记录到终端时使用键盘提供输入。
(6)键入exit以终止/bin/bash命令时,容器将停止但不会被删除,可以重新启动或删除。
1.3.3.3 注册中心
镜像构建完成后,可以很容易地在当前宿主机上运行,但是,如果需要在其他服务器上使用这个镜像,就需要一个集中的存储、分发镜像的服务,Docker注册中心就是这样的服务。注册中心中可以包含多个仓库;每个仓库可以包含多个标签;每个标签对应一个镜像。通常,一个仓库会包含同一个软件不同版本的镜像,而标签常用于对应该软件的各个版本,可以通过<仓库名>:<标签>的格式指定具体是这个软件哪个版本的镜像。如果不给出标签,将以latest作为默认标签。以Ubuntu镜像为例,ubuntu是仓库的名字,其内包含不同的版本标签,如16.04、18.04等。可以通过ubuntu:16.04或者ubuntu:18.04具体指定所需哪个版本的镜像,如果忽略了标签,如ubuntu,那么将视为ubuntu:latest。仓库名经常以两段式路径形式出现,如leeivan/spark-lab-env,前者通常是注册中心多用户环境下的用户名,后者通常是对应的软件名,但这并非绝对,取决于使用的具体注册中心的软件或服务。
注册中心公开服务是开放给用户使用、允许用户管理镜像的服务。一般地,这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。最常使用的注册中心公开服务是官方的Docker Hub,这也是默认的注册中心,并拥有大量高质量的官方镜像,除此以外,还有CoreOS的Quay.io,CoreOS相关的镜像存储在这里。谷歌的Google Container Registry和Kubernetes的镜像使用的就是这个服务。
由于某些原因,在国内访问这些服务可能比较慢。国内的一些云服务商提供了针对Docker Hub的镜像服务,这些镜像服务被称为加速器。常见的有阿里云加速器、DaoCloud加速器等。使用加速器会直接从国内的地址下载Docker Hub的镜像,比直接从Docker Hub下载速度会提高很多。国内也有一些云服务商提供类似Docker Hub的公开服务,如时速云镜像仓库、网易云镜像服务、DaoCloud镜像市场、阿里云镜像库等。
1.3.4 管理命令
本教程的实验环节需要使用Docker部署Spark开发环境,需要了解Docker操作的基本命令,以及虚拟环境的创建、部署等。下面介绍Docker命令在大部分情境下的使用方法以及应用,可以在进入Spark的实验环节之前进行练习参考。在各个阶段运行的Docker命令如图1-7所示。
图1-7 在各个阶段运行的Docker命令
生命周期管理
操作运维
根文件系统命令
镜像仓库
本地镜像管理
其他命令
下面列出docker命令的示例。
(1)列出机器上的镜像,其中可以根据REPOSITORY判断这个镜像来自哪个服务器,如果没有“/”,则表示官方镜像;如果类似username/repos_name,则表示Docker Hub的个人公共库;如果类似regsistory.example.com:5000/repos_name,则表示的是私服。IMAGE ID列其实是缩写,若显示完整,则须加上--no-trunc选项。
命令1-1
(2)在Docker Hub中搜索镜像,搜索的范围是官方镜像和所有个人公共镜像,在NAME列中/后面是仓库的名字。
命令1-2
(3)从公共注册中心下拉镜像。
命令1-3
上面的命令需要注意,从1.3版本开始只会下载标签为latest的镜像,也可以明确指定具体的镜像。
命令1-4
当然,也可以从个人的公共仓库(包括自己是私人仓库)拉取,格式为docker pull username/repository<:tag_name>。
命令1-5
(4)推送镜像或仓库到注册中心,与上面的pull对应,可以推送到公共注册中心。
命令1-6
在仓库不存在的情况下,推送上去镜像会创建为私有库,然后通过浏览器创建默认公共库。
(5)从镜像启动一个容器,在容器上执行命令。docker run命令首先会从特定的镜像上创建一层可写的容器,然后通过docker start命令启动它。停止的容器可以重新启动并保留原来的修改。run命令启动参数有很多,以下是一些常规使用说明。当利用run创建容器时,首先检查本地是否存在指定的镜像,若不存在,就从公有仓库下载,利用镜像创建并启动一个容器,然后分配一个文件系统,并在只读的镜像层外面挂载一层可读写层,从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中,从地址池配置一个IP地址给容器,执行用户指定的应用程序,执行完毕后容器被终止,使用镜像创建容器并执行相应命令。
命令1-7
这是最简单的方式,与在本地直接执行echo'hello world'几乎感觉不出任何区别,而实际上它会从本地ubuntu:latest镜像启动到一个容器,并执行打印命令后退出,可以通过命令查看创建的容器。
需要注意的是,默认有一个--rm=true参数,即完成操作后停止容器并从文件系统移除。因为Docker的容器实在太轻量级了,很多时候用户都是随时删除和创建容器。容器启动后会自动随机生成一个CONTAINER ID,这个ID在commit命令后可以变为IMAGE ID。使用镜像创建容器并进入交互模式。
命令1-8
这个命令会启动一个伪终端,上面的--name参数可以指定启动后的容器名字,如果不指定,则会自动生成一个名字。通过ps或top命令只能看到一两个进程,因为容器的核心是执行的应用程序,需要的资源都是应用程序运行必需的,除此之外,并没有其他的资源,可见Docker对资源的利用率极高。此时使用exit或组合键Ctrl+D退出后,这个容器也就消失了。使用下面的命令在后台运行一个容器:
命令1-9
它将直接把启动的容器挂起放在后台运行,并且会输出一个CONTAINER ID,通过docker ps命令可以看到这个容器的信息,通过“docker logs 0d6e40aa8791”可在容器外面查看它的输出,也可以通过“docker attach 0d6e40aa8791”连接到这个正在运行的终端,此时如果使用组合键Ctrl+C退出,容器就消失了。
docker exec是另一个常用到的指令,主要的作用是可以进入容器中执行某项命令,但要注意,这个指令要正常运行,容器必须在活着的状态才行。例如,假设容器处于退出状态,则执行docker exec时会出现错误信息。在下面的范例中,eede6d35b47d是容器ID(也可以换成容器名称)。
docker exec可用来进入容器内,然后在容器中执行指令,例如:
使用Docker指令时,有些动作是可以用run或exec完成的,那么两者有什么不同呢?其实,run命令可以在没有容器的情况下使用,可以先建立一个容器,然后启动;docker exec就是针对已存在的容器进行操作,如果容器没活着,那么在执行指令时会出现错误。
(6)映射主机到容器的端口。
docker容器在启动的时候如果不指定端口映射参数,在容器外部是无法通过网络访问容器内的网络应用和服务的,也可使用Dockerfile文件中的EXPOSE指令配置。端口映射可使用-p、-P实现。当使用-P标记时,Docker会随机映射一个49000~49900的端口到内部容器开放的网络端口;当使用-p标记时,则可以指定要映射的端口,并且在一个指定端口上只可以绑定一个容器,支持如下三种格式。
需要注意的是:宿主机的一个端口只能映射到容器内部的某一个端口上,如8080:80之后,就不能8080:81;容器内部的某个端口可以被宿主机的多个端口映射,如8080:80、8090:80和8099:80。端口的映射有以下7种方法。
①将容器暴露的所有端口都随机映射到宿主机上(不推荐使用),例如:
②将容器指定端口随机映射到宿主机的一个端口上,例如:
以上指令会将容器的80端口随机映射到宿主机的一个端口上。
③将容器指定端口映射到宿主机的一个指定端口上,例如:
以上指令会将容器的80端口映射到宿主机的8000端口上。
④绑定外部的IP和随机端口到容器的指定端口(宿主机IP是10.168.2.141),例如:
以上指令会将宿主机的IP10.168.2.141和随机指定端口32768映射到容器的80端口。
⑤绑定外部的IP和指定端口到容器的指定端口(宿主机IP是10.168.2.141),例如:
以上指令会将宿主机的IP10.168.2.141和指定端口8000映射到容器的80端口。
⑥查看容器绑定和映射的端口及IP地址,例如:
⑦目录映射其实是绑定挂载主机的路径到容器的目录,这对于内外传送文件比较方便。为了避免容器停止以后保存的镜像不被删除,使用-v<host_path:container_path>就把提交的镜像保存到挂载的主机目录下,绑定多个目录时再加多个-v,例如:
通过-v参数,冒号前为宿主机目录,必须为绝对路径,冒号后为镜像内挂载的路径。现在镜像内就可以共享宿主机里的文件了。默认挂载的路径权限为读写。如果指定为只读,可以用ro。
docker还提供了一种高级的用法,叫数据卷。数据卷其实就是一个正常的容器,专门用来提供数据卷供其他容器挂载的,感觉像是由一个容器定义的一个数据挂载信息,其他容器启动可以直接挂载数据卷容器中定义的挂载信息,例如:
创建一个普通的容器。用--name给它指定一个名字(若不指定,则会生成一个随机的名字)。再创建一个新的容器,来使用这个数据卷。
--volumes-from用来指定从哪个数据卷挂载数据。
(7)开启/停止/重启(start/stop/restart)容器。
可以通过run新建一个容器,也可以重新启动已经停止的容器,不能再指定容器启动时运行的指令,因为Docker只能有一个前台进程。容器停止(或Ctrl+D)时,会在保存当前容器的状态之后退出,下次启动时保有上次关闭时更改。
docker start:启动一个或多个已经被停止的容器。
docker stop:停止一个运行中的容器。
docker restart:重启容器。
(8)进入运行中的容器。
如果运行docker run时,使用-d参数,容器启动后会进入后台执行,某些时候需要进入容器进行操作,有很多种方法可以完成这样的操作,包括使用docker attach或docker exec命令等。docker exec是内建的命令,下面示范如何使用该命令。
docker attach也是内建的命令,下面示范如何使用该命令。
先按组合键Ctrl+P,然后按组合键Ctrl+Q从当前容器离开,而容器继续在后台执行。但是,使用attach命令有时并不方便,当多个窗口同时进入同一容器的时候,所有窗口都会同步显示。当某个窗口因命令阻塞时,其他窗口也就无法执行操作了。
(9)查看容器的信息。
docker ps命令可以查看容器的CONTAINER ID、NAME、IMAGE NAME、端口开启及绑定、容器启动后执行的命令,经常通过ps找到CONTAINER ID。
docker ps:默认显示当前正在运行中的容器。
docker ps-a:查看包括已经停止的所有容器。
docker ps-l:显示最新启动的一个容器(包括已停止的)。