为什么要写这个
简单回答是:容器技术非常热门,但门槛高。
容器技术是继大数据和云计算之后又一炙手可热的技术,而且未来相当一段时间内都会非常流行。
对 IT 行业来说,这是一项非常有价值的技术。而对 IT 从业者来说,掌握容器技术是市场的需要,也是提升自我价值的重要途径。
拿我自己的工作经历来说,毕业后的头几年是做 J2EE 应用开发。后来到一家大型IT公司,公司的产品从中间件到操作系统、从服务器到存储、从虚拟化到云计算都有涉及。
我所在的部门是专门做 IT 基础设施实施服务的,最开始是做传统的 IT 项目,包括服务器配置,双机 HA 等。随着虚拟化技术成熟,工作上也开始涉及各种虚拟化技术的规划和实施,包括 VMWare、KVM、PowerVM等。后来云计算兴起,在公司业务和个人兴趣的驱动下,开始学习和实践 OpenStack,在这个过程中写了《每天5分钟玩转OpenStack》教程并得到大家的认可。
现在以 Docker 为代表的容器技术来了,而且关注度越来越高,这一点可以从 Google Trend 中 Docker 的搜索上升趋势(蓝色曲线)中清楚看到。
每一轮新技术的兴起,无论对公司还是个人既是机会也是挑战。
我个人的看法是:如果某项新技术未来将成为主流,就应该及早尽快掌握。
因为:
- 新技术意味着新的市场和新的需求
初期掌握这种技术的人不会很多,而市场需求会越来越大,因而会形成供不应求的卖方市场,物以稀为贵,这对技术人员将是一个难得的价值提升机会。 - 学习新技术需要时间和精力,早起步早成材
机会讲过了,咱们再来看看挑战。
新技术往往意味着技术上的突破和创新,会有不少新的概念和方法。
而且从大数据,云计算和容器技术来看,这些新技术都是平台级别,覆盖的技术范围非常广,包括了计算、网络、存储、高可用、监控、安全等多个方面,要掌握这些新技术对 IT 老兵尚有不小难度,更别说新人了。
由于对技术一直保持着很高的热诚和执着,在掌握了 OpenStack 相关 IaaS 技术后,我便开始调研 PaaS 技术栈。正好这时 Docker 也越来越流行,自然而然便开始了容器相关技术的学习研究和实践。
学习容器技术的过程可以说是惊喜不断,经常惊叹于容器理念的先进和容器生态环境的完整和强大。很多传统软件开发和运维中的难题在容器世界里都能轻松解决,也渐渐理解了容器为何如此受到青睐。
不夸张地说,容器为我打开了一扇通往另一个软件世界的大门,让我沉浸其中,激动不已。高兴之余,我也迫不及待地想把我所看到所学到和所想到的有关容器的知识介绍给更多的人,让更多的IT工程师能够从容器技术中受益。
我希望这个教程也能为大家打开这扇门,降低学习的曲线,系统地学习和掌握容器技术。
写给谁看?
这套教程的目标读者包括:
软件开发人员
相信微服务架构(Microservice Architectur)会逐渐成为开发应用系统的主流。而容器则是这种架构的基石。市场将需要更多能够开发出基于容器的应用程序的软件开发人员。
IT 实施和运维工程师
容器为应用提供了更好的打包和部署方式。越来越多的应用将以容器的方式在开发、测试和生产环境中运行。掌握容器相关技术将成为实施和运维工程师的核心竞争力。
我自己
我坚信最好的学习方法是分享。编写这个教程同时也是对自己学习和实践容器技术的总结。对于知识,只有把它写出来并能够让其他人理解,才能说明真正掌握了这项知识。
包含哪些内容?
本系列教程包括以下三大块内容:
下面分别介绍各部分包含的内容。
启程
“启程”会介绍容器的生态系统,让大家先从整体上了解容器都包含哪些技术,各种技术之间的相互关系是什么,然后再来看我们的教程都会涉及生态中的哪些部分。
为了让大家尽快对容器有个感性认识,我们会搭建实验环境并运行第一个容器,为之后的学习热身。
容器技术
这是教程的主要内容,包含“容器核心知识”和“容器进阶知识”两部分。
核心知识主要回答有关容器 what, why 和 how 三方面的问题。 其中以 how 为重,将展开讨论架构、镜像、容器、网络和存储。
进阶知识包括将容器真正用于生产所必需的技术,包括多主机管理、跨主机网络、监控、数据管理、日志管理和安全管理。
容器平台技术
如下图所示,“容器平台技术”包括容器编排引擎、容器管理平台和基于容器的 PaaS。容器平台技术在生态环境中占据着举足轻重的位置,对于容器是否能够落地,是否能应用于生产至关重要。
怎样的编写方式?
我会继续采用《每天5分钟玩转OpenStack》的方式,通过大量的实验由浅入深地探讨和实践容器技术,力求达到如下目标:
- 快速上手:以最直接、最有效的方式让大家把容器用起来。
- 循序渐进:由易到难,从浅入深,详细分析容器的各种功能和配置使用方法。
- 理解架构:从设计原理和架构分析入手,深入探讨容器的架构和运行机理。
- 注重实践:以大量实际操作案例为基础,让大家能够掌握真正的实施技能。
在内容的发布上还是通过微信公众号(cloudman6)每周 1、3、5 定期分享。欢迎大家通过公众号提出问题和建议,进行技术交流。
为什么叫《每天5分钟玩转Docker容器技术》?
为了降低学习的难度并且考虑到移动端碎片化阅读的特点,每次推送的内容大家只需要花5分钟就能看完(注意这里说的是看完,有时候要完全理解可能需要更多时间哈),每篇内容包含1-3个知识点,这就是我把教程命名为《每天5分钟玩转Docker容器技术》的原因。虽然是碎片化推送,但整个教程是系统、连贯和完整的,只是化整为零了。
好了,今天这5分钟算是开了个头,下次我们正式开始玩转容器技术。
容器生态系统
对于像容器这类平台级别的技术,通常涉及的知识范围会很广,相关的软件,解决方案也会很多,初学者往往容易迷失。
那怎么办呢?
我们可以从生活经验中寻找答案。
当我们去陌生城市旅游想了解一下这个城市一般我们会怎么做?
我想大部分人应该会打开手机看一下这个城市的地图:
- 城市大概的位置和地理形状是什么?
- 都由哪几个区或县组成?
- 主要的交通干道是哪几条?
同样的道理,学习容器技术我们可以先从天上鸟瞰一下:
- 容器生态系统包含哪些不同层次的技术?
- 不同技术之间是什么关系?
- 哪些是核心技术哪些是辅助技术?
首先得对容器技术有个整体认识,之后我们的学习才能够有的放矢,才能够分清轻重缓急,做到心中有数,这样就不容易迷失了。
接下来我会根据自己的经验帮大家规划一条学习路线,一起探索容器生态系统。
学习新技术得到及时反馈是非常重要的,所以我们马上会搭建实验环境,并运行第一个容器,感受什么是容器。
千里之行始于足下,让我们从了解生态系统开始吧。
鸟瞰容器生态系统
容器生态系统
一谈到容器,大家都会想到 Docker。
Docker 现在几乎是容器的代名词。确实,是 Docker 将容器技术发扬光大。同时,大家也需要知道围绕 Docker 还有一个生态系统。Docker 是这个生态系统的基石,但完善的生态系统才是保障 Docker 以及容器技术能够真正健康发展的决定因素。
大致来看,容器生态系统包含核心技术、平台技术和支持技术。
下面分别介绍。
容器核心技术
容器核心技术是指能够让 container 在 host 上运行起来的那些技术。
这些技术包括容器规范、容器 runtime、容器管理工具、容器定义工具、Registry 以及 容器 OS,下面分别介绍。
容器规范
容器不光是 Docker,还有其他容器,比如 CoreOS 的 rkt。为了保证容器生态的健康发展,保证不同容器之间能够兼容,包含 Docker、CoreOS、Google在内的若干公司共同成立了一个叫 Open Container Initiative(OCI) 的组织,其目是制定开放的容器规范。
目前 OCI 发布了两个规范:runtime spec 和 image format spec。
有了这两个规范,不同组织和厂商开发的容器能够在不同的 runtime 上运行。这样就保证了容器的可移植性和互操作性。
容器 runtime
runtime 是容器真正运行的地方。runtime 需要跟操作系统 kernel 紧密协作,为容器提供运行环境。
如果大家用过 Java,可以这样来理解 runtime 与容器的关系:
Java 程序就好比是容器,JVM 则好比是 runtime。JVM 为 Java 程序提供运行环境。同样的道理,容器只有在 runtime 中才能运行。
lxc、runc 和 rkt 是目前主流的三种容器 runtime。
lxc 是 Linux 上老牌的容器 runtime。Docker 最初也是用 lxc 作为 runtime。
runc 是 Docker 自己开发的容器 runtime,符合 oci 规范,也是现在 Docker 的默认 runtime。
rkt 是 CoreOS 开发的容器 runtime,符合 oci 规范,因而能够运行 Docker 的容器。
容器管理工具
光有 runtime 还不够,用户得有工具来管理容器啊。容器管理工具对内与 runtime 交互,对外为用户提供 interface,比如 CLI。这就好比除了 JVM,还得提供 java
命令让用户能够启停应用不是。
lxd 是 lxc 对应的管理工具。
runc 的管理工具是 docker engine。docker engine 包含后台 deamon 和 cli 两个部分。我们通常提到 Docker,一般就是指的 docker engine。
rkt 的管理工具是 rkt cli。
容器定义工具
容器定义工具允许用户定义容器的内容和属性,这样容器就能够被保存,共享和重建。
docker image 是 docker 容器的模板,runtime 依据 docker image 创建容器。
dockerfile 是包含若干命令的文本文件,可以通过这些命令创建出 docker image。
ACI (App Container Image) 与 docker image 类似,只不过它是由 CoreOS 开发的 rkt 容器的 image 格式。
Registry
容器是通过 image 创建的,需要有一个仓库来统一存放 image,这个仓库就叫做 Registry。
企业可以用 Docker Registry 构建私有的 Registry。
Docker Hub(https://hub.docker.com) 是 Docker 为公众提供的托管 Registry,上面有很多现成的 image,为 Docker 用户提供了极大的便利。
http://Quay.io(https://quay.io/)是另一个公共托管 Registry,提供与 Docker Hub 类似的服务。
容器 OS
由于有容器 runtime,几乎所有的 Linux、MAC OS 和 Windows 都可以运行容器。但这不并没有妨碍容器 OS 的问世。
容器 OS 是专门运行容器的操作系统。与常规 OS 相比,容器 OS 通常体积更小,启动更快。因为是为容器定制的 OS,通常它们运行容器的效率会更高。
目前已经存在不少容器 OS,CoreOS、atomic 和 ubuntu core 是其中的杰出代表。
容器平台技术
容器核心技术使得容器能够在单个 host 上运行。而容器平台技术能够让容器作为集群在分布式环境中运行。
容器平台技术包括容器编排引擎、容器管理平台和基于容器的 PaaS。
容器编排引擎
基于容器的应用一般会采用微服务架构。在这种架构下,应用被划分为不同的组件,并以服务的形式运行在各自的容器中,通过 API 对外提供服务。为了保证应用的高可用,每个组件都可能会运行多个相同的容器。这些容器会组成集群,集群中的容器会根据业务需要被动态地创建、迁移和销毁。
大家可以看到,这样一个基于微服务架构的应用系统实际上是一个动态的可伸缩的系统。这对我们的部署环境提出了新的要求,我们需要有一种高效的方法来管理容器集群。而这,就是容器编排引擎要干的工作。
所谓编排(orchestration),通常包括容器管理、调度、集群定义和服务发现等。通过容器编排引擎,容器被有机的组合成微服务应用,实现业务需求。
docker swarm 是 Docker 开发的容器编排引擎。
kubernetes 是 Google 领导开发的开源容器编排引擎,同时支持 Docker 和 CoreOS 容器。
mesos 是一个通用的集群资源调度平台,mesos 与 marathon 一起提供容器编排引擎功能。
以上三者是当前主流的容器编排引擎。
容器管理平台
容器管理平台是架构在容器编排引擎之上的一个更为通用的平台。通常容器管理平台能够支持多种编排引擎,抽象了编排引擎的底层实现细节,为用户提供更方便的功能,比如 application catalog 和一键应用部署等。
Rancher 和 ContainerShip 是容器管理平台的典型代表。
基于容器的 PaaS
基于容器的 PaaS 为微服务应用开发人员和公司提供了开发、部署和管理应用的平台,使用户不必关心底层基础设施而专注于应用的开发。
Deis、Flynn 和 Dokku 都是开源容器 PaaS 的代表。
容器支持技术
下面这些技术被用于支持基于容器的基础设施。
容器网络
容器的出现使网络拓扑变得更加动态和复杂。用户需要专门的解决方案来管理容器与容器,容器与其他实体之间的连通性和隔离性。
docker network 是 Docker 原生的网络解决方案。除此之外,我们还可以采用第三方开源解决方案,例如 flannel、weave 和 calico。不同方案的设计和实现方式不同,各有优势和特点,需要根据实际需要来选型。
服务发现
动态变化是微服务应用的一大特点。当负载增加时,集群会自动创建新的容器;负载减小,多余的容器会被销毁。容器也会根据 host 的资源使用情况在不同 host 中迁移,容器的 IP 和端口也会随之发生变化。
在这种动态的环境下,必须要有一种机制让 client 能够知道如何访问容器提供的服务。这就是服务发现技术要完成的工作。
服务发现会保存容器集群中所有微服务最新的信息,比如 IP 和端口,并对外提供 API,提供服务查询功能。
etcd、consul 和 zookeeper 是服务发现的典型解决方案。
监控
监控对于基础架构非常重要,而容器的动态特征对监控提出更多挑战。针对容器环境,已经涌现出很多监控工具和方案。
docker ps/top/stats 是 Docker 原生的命令行监控工具。除了命令行,Docker 也提供了 stats API,用户可以通过 HTTP 请求获取容器的状态信息。
sysdig、cAdvisor/Heapster 和 Weave Scope 是其他开源的容器监控方案。
数据管理
容器经常会在不同的 host 之间迁移,如何保证持久化数据也能够动态迁移,是 Rex-Ray 这类数据管理工具提供的能力。
日志管理
日志为问题排查和事件管理提供了重要依据。
docker logs 是 Docker 原生的日志工具。而 logspout 对日志提供了路由功能,它可以收集不同容器的日志并转发给其他工具进行后处理。
安全性
对于年轻的容器,安全性一直是业界争论的焦点。
OpenSCAP 能够对容器镜像进行扫描,发现潜在的漏洞。
本教程覆盖的知识范围
前面我们已经鸟瞰了整个容器生态系统,对容器所涉及的技术体系有了全面的认识。那我们的系列教程会讨论其中的哪些内容呢?
会覆盖容器生态系统 91.6% 的技术!
运行第一个容器
为了让大家对容器有个感性认识,我们将尽快让一个容器运行起来。首先我们需要搭建实验环境。
环境选择
容器需要管理工具、runtime 和操作系统,我们的选择如下:
- 管理工具 - Docker Engine
因为 Docker 最流行使用最广泛。 - runtime - runc
Docker 的默认 runtime - 操作系统 - Ubuntu
虽然存在诸如 CoreOS 的容器 OS,因考虑到我们目前处于初学阶段,选择大家熟悉的操作系统更为合适。等具备了扎实的容器基础知识后再使用容器 OS 会更有利。
安装 Docker
本节我们将在 ubuntu 16.04 虚拟机中安装 Docker。因为安装过程需要访问 internet, 所以虚拟机必须能够上网。
Docker 支持几乎所有的 Linux 发行版,也支持 Mac 和 Windows。各操作系统的安装方法可以访问:https://docs.docker.com/engine/installation/
Docker 分为开源免费的 CE(Community Edition)版本和收费的 EE(Enterprise Edition)版本。下面我们将按照文档,通过以下步骤在 Ubuntu 16.04 上安装 Docker CE 版本。
配置 Docker 的 apt 源
1.安装包,允许 apt
命令 HTTPS 访问 Docker 源。
2.添加 Docker 官方的 GPG key
3.将 Docker 的源添加到 /etc/apt/sources.list
安装 Docker
运行第一个容器
环境就绪,马上运行第一个容器,执行命令:
其过程可以简单的描述为:
- 从 Docker Hub 下载 httpd 镜像。镜像中已经安装好了 Apache HTTP Server。
- 启动 httpd 容器,并将容器的 80 端口映射到 host 的 80 端口。
下面我们可以通过浏览器验证容器是否正常工作。在浏览器中输入 http://[your ubuntu host IP]
可以访问容器的 http 服务了,第一个容器运行成功!我们轻轻松松就拥有了一个 WEB 服务器。随着学习的深入,会看到容器技术带给我们更多的价值。
小结
我们已经完成了教程的第一部分。
我们认识了容器生态系统,后面会陆续学习生态系统中的大部分技术。我们在 Ubuntu 16.04 上配置好了实验环境,并成功运行了第一个容器 httpd。
容器大门已经打开,让我们去探秘吧。
What - 什么是容器?
容器是一种轻量级、可移植、自包含的软件打包技术,使应用程序可以在几乎任何地方以相同的方式运行。开发人员在自己笔记本上创建并测试好的容器,无需任何修改就能够在生产系统的虚拟机、物理服务器或公有云主机上运行。
容器与虚拟机
谈到容器,就不得不将它与虚拟机进行对比,因为两者都是为应用提供封装和隔离。
容器由两部分组成:
- 应用程序本身
- 依赖:比如应用程序需要的库或其他软件
容器在 Host 操作系统的用户空间中运行,与操作系统的其他进程隔离。这一点显著区别于的虚拟机。
传统的虚拟化技术,比如 VMWare, KVM, Xen,目标是创建完整的虚拟机。为了运行应用,除了部署应用本身及其依赖(通常几十 MB),还得安装整个操作系统(几十 GB)。
下图展示了二者的区别。
如图所示,由于所有的容器共享同一个 Host OS,这使得容器在体积上要比虚拟机小很多。另外,启动容器不需要启动整个操作系统,所以容器部署和启动速度更快,开销更小,也更容易迁移。
Why - 为什么需要容器?
为什么需要容器?容器到底解决的是什么问题?
简要的答案是:容器使软件具备了超强的可移植能力。
容器解决的问题
我们来看看今天的软件开发面临着怎样的挑战?
如今的系统在架构上较十年前已经变得非常复杂了。以前几乎所有的应用都采用三层架构(Presentation/Application/Data),系统部署到有限的几台物理服务器上(Web Server/Application Server/Database Server)。
而今天,开发人员通常使用多种服务(比如 MQ,Cache,DB)构建和组装应用,而且应用很可能会部署到不同的环境,比如虚拟服务器,私有云和公有云。
一方面应用包含多种服务,这些服务有自己所依赖的库和软件包;另一方面存在多种部署环境,服务在运行时可能需要动态迁移到不同的环境中。这就产生了一个问题:
如何让每种服务能够在所有的部署环境中顺利运行?
于是我们得到了下面这个矩阵:
各种服务和环境通过排列组合产生了一个大矩阵。开发人员在编写代码时需要考虑不同的运行环境,运维人员则需要为不同的服务和平台配置环境。对他们双方来说,这都是一项困难而艰巨的任务。
如何解决这个问题呢?
聪明的技术人员从传统的运输行业找到了答案。
几十年前,运输业面临着类似的问题。
每一次运输,货主与承运方都会担心因货物类型的不同而导致损失,比如几个铁桶错误地压在了一堆香蕉上。另一方面,运输过程中需要使用不同的交通工具也让整个过程痛苦不堪:货物先装上车运到码头,卸货,然后装上船,到岸后又卸下船,再装上火车,到达目的地,最后卸货。一半以上的时间花费在装、卸货上,而且搬上搬下还容易损坏货物。
这同样也是一个 NxM 的矩阵。
幸运的是,集装箱的发明解决这个难题。
任何货物,无论钢琴还是保时捷,都被放到各自的集装箱中。集装箱在整个运输过程中都是密封的,只有到达最终目的地才被打开。标准集装箱可以被高效地装卸、重叠和长途运输。现代化的起重机可以自动在卡车、轮船和火车之间移动集装箱。集装箱被誉为运输业与世界贸易最重要的发明。
Docker 将集装箱思想运用到软件打包上,为代码提供了一个基于容器的标准化运输系统。Docker 可以将任何应用及其依赖打包成一个轻量级、可移植、自包含的容器。容器可以运行在几乎所有的操作系统上。
其实,“集装箱” 和 “容器” 对应的英文单词都是 “Container”。
“容器” 是国内约定俗成的叫法,可能是因为容器比集装箱更抽象,更适合软件领域的原故吧。
我个人认为:在老外的思维中,“Container” 只用到了集装箱这一个意思,Docker 的 Logo 不就是一堆集装箱吗?
Docker 的特性
我们可以看看集装箱思想是如何与 Docker 各种特性相对应的。
容器的优势
对于开发人员 - Build Once, Run Anywhere
容器意味着环境隔离和可重复性。开发人员只需为应用创建一次运行环境,然后打包成容器便可在其他机器上运行。另外,容器环境与所在的 Host 环境是隔离的,就像虚拟机一样,但更快更简单。
对于运维人员 - Configure Once, Run Anything
只需要配置好标准的 runtime 环境,服务器就可以运行任何容器。这使得运维人员的工作变得更高效,一致和可重复。容器消除了开发、测试、生产环境的不一致性。
How - 容器是如何工作的?
接下来学习容器核心知识的最主要部分。
我们首先会介绍 Docker 的架构,然后分章节详细讨论 Docker 的镜像、容器、网络和存储。
Docker 架构详解
Docker 的核心组件包括:
- Docker 客户端 - Client
- Docker 服务器 - Docker daemon
- Docker 镜像 - Image
- Registry
- Docker 容器 - Container
Docker 架构如下图所示:
Docker 采用的是 Client/Server 架构。客户端向服务器发送请求,服务器负责构建、运行和分发容器。客户端和服务器可以运行在同一个 Host 上,客户端也可以通过 socket 或 REST API 与远程的服务器通信。
Docker 客户端
最常用的 Docker 客户端是 docker
命令。通过 docker
我们可以方便地在 Host 上构建和运行容器。
docker
支持很多操作(子命令),后面会逐步用到。
除了 docker
命令行工具,用户也可以通过 REST API 与服务器通信。
Docker 服务器
Docker daemon 是服务器组件,以 Linux 后台服务的方式运行。
Docker daemon 运行在 Docker host 上,负责创建、运行、监控容器,构建、存储镜像。
默认配置下,Docker daemon 只能响应来自本地 Host 的客户端请求。如果要允许远程客户端请求,需要在配置文件中打开 TCP 监听,步骤如下:
1.编辑配置文件 /etc/systemd/system/multi-user.target.wants/docker.service,在环境变量 ExecStart
后面添加 -H tcp://0.0.0.0
,允许来自任意 IP 的客户端连接。
如果使用的是其他操作系统,配置文件的位置可能会不一样。
2.重启 Docker daemon。
3.服务器 IP 为 192.168.56.102,客户端在命令行里加上 -H 参数,即可与远程服务器通信。
info
子命令用于查看 Docker 服务器的信息。
Docker 镜像
可将 Docker 镜像看成只读模板,通过它可以创建 Docker 容器。
例如某个镜像可能包含一个 Ubuntu 操作系统、一个 Apache HTTP Server 以及用户开发的 Web 应用。
镜像有多种生成方法:
- 可以从无到有开始创建镜像
- 也可以下载并使用别人创建好的现成的镜像
- 还可以在现有镜像上创建新的镜像
我们可以将镜像的内容和创建步骤描述在一个文本文件中,这个文件被称作 Dockerfile,通过执行 docker build <docker-file>
命令可以构建出 Docker 镜像,后面我们会讨论。
Docker 容器
Docker 容器就是 Docker 镜像的运行实例。
用户可以通过 CLI(docker)或是 API 启动、停止、移动或删除容器。可以这么认为,对于应用软件,镜像是软件生命周期的构建和打包阶段,而容器则是启动和运行阶段。
Registry
Registry 是存放 Docker 镜像的仓库,Registry 分私有和公有两种。
Docker Hub(https://hub.docker.com/) 是默认的 Registry,由 Docker 公司维护,上面有数以万计的镜像,用户可以自由下载和使用。
出于对速度或安全的考虑,用户也可以创建自己的私有 Registry。后面我们会学习如何搭建私有 Registry。
docker pull
命令可以从 Registry 下载镜像。 docker run
命令则是先下载镜像(如果本地没有),然后再启动容器。
Docker 组件如何协作?
一个完整的例子
还记得我们运行的第一个容器吗?现在通过它来体会一下 Docker 各个组件是如何协作的。
容器启动过程如下:
- Docker 客户端执行
docker run
命令。 - Docker daemon 发现本地没有 httpd 镜像。
- daemon 从 Docker Hub 下载镜像。
- 下载完成,镜像 httpd 被保存到本地。
- Docker daemon 启动容器。
docker images
可以查看到 httpd 已经下载到本地。
docker ps
或者 docker container ls
显示容器正在运行。
小结
Docker 借鉴了集装箱的概念。标准集装箱将货物运往世界各地,Docker 将这个模型运用到自己的设计哲学中,唯一不同的是:集装箱运输货物,而 Docker 运输软件。
每个容器都有一个软件镜像,相当于集装箱中的货物。容器可以被创建、启动、关闭和销毁。和集装箱一样,Docker 在执行这些操作时,并不关心容器里到底装的什么,它不管里面是 Web Server,还是 Database。
用户不需要关心容器最终会在哪里运行,因为哪里都可以运行。
开发人员可以在笔记本上构建镜像并上传到 Registry,然后 QA 人员将镜像下载到物理或虚拟机做测试,最终容器会部署到生产环境。
使用 Docker 以及容器技术,我们可以快速构建一个应用服务器、一个消息中间件、一个数据库、一个持续集成环境。因为 Docker Hub 上有我们能想到的几乎所有的镜像。
不知大家是否意识到,潘多拉盒子已经被打开。容器不但降低了我们学习新技术的门槛,更提高了效率。
如果你是一个运维人员,想研究负载均衡软件 HAProxy,只需要执行 docker run haproxy
,无需繁琐的手工安装和配置既可以直接进入实战。
如果你是一个开发人员,想学习怎么用 django 开发 Python Web 应用,执行 docker run django
,在容器里随便折腾吧,不用担心会搞乱 Host 的环境。
不夸张的说:容器大大提升了 IT 人员的幸福指数。
最小的镜像
镜像是 Docker 容器的基石,容器是镜像的运行实例,有了镜像才能启动容器。
本章内容安排如下:
- 首先通过研究几个典型的镜像,分析镜像的内部结构。
- 然后学习如何构建自己的镜像。
- 最后介绍怎样管理和分发镜像。
镜像的内部结构
为什么我们要讨论镜像的内部结构?
如果只是使用镜像,当然不需要了解,直接通过 docker
命令下载和运行就可以了。
但如果我们想创建自己的镜像,或者想理解 Docker 为什么是轻量级的,就非常有必要学习这部分知识了。
我们从一个最小的镜像开始吧。
hello-world - 最小的镜像
hello-world 是 Docker 官方提供的一个镜像,通常用来验证 Docker 是否安装成功。
我们先通过 docker pull
从 Docker Hub 下载它。
用 docker images
命令查看镜像的信息。
hello-world 镜像竟然还不到 2KB!
通过 docker run
运行。
其实我们更关心 hello-world 镜像包含哪些内容。
Dockerfile 是镜像的描述文件,定义了如何构建 Docker 镜像。Dockerfile 的语法简洁且可读性强,后面我们会专门讨论如何编写 Dockerfile。
hello-world 的 Dockerfile 内容如下:
只有短短三条指令。
- FROM scratch
此镜像是从白手起家,从 0 开始构建。 - COPY hello /
将文件“hello”复制到镜像的根目录。 - CMD ["/hello"]
容器启动时,执行 /hello
镜像 hello-world 中就只有一个可执行文件 “hello”,其功能就是打印出 “Hello from Docker ......” 等信息。
/hello 就是文件系统的全部内容,连最基本的 /bin,/usr, /lib, /dev 都没有。
hello-world 虽然是一个完整的镜像,但它并没有什么实际用途。通常来说,我们希望镜像能提供一个基本的操作系统环境,用户可以根据需要安装和配置软件。
这样的镜像我们称作 base 镜像。
base 镜像
上一节我们介绍了最小的 Docker 镜像,本节讨论 base 镜像。
base 镜像有两层含义:
- 不依赖其他镜像,从 scratch 构建。
- 其他镜像可以之为基础进行扩展。
所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。
我们以 CentOS 为例考察 base 镜像包含哪些内容。
下载镜像:
docker pull centos
查看镜像信息:
镜像大小不到 200MB。
等一下!
一个 CentOS 才 200MB ?
平时我们安装一个 CentOS 至少都有几个 GB,怎么可能才 200MB !
相信这是几乎所有 Docker 初学者都会有的疑问,包括我自己。下面我们来解释这个问题。
Linux 操作系统由内核空间和用户空间组成。如下图所示:
rootfs
内核空间是 kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉。
用户空间的文件系统是 rootfs,包含我们熟悉的 /dev, /proc, /bin 等目录。
对于 base 镜像来说,底层直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。
而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他 Linux 发行版,CentOS 的 rootfs 已经算臃肿的了,alpine 还不到 10MB。
我们平时安装的 CentOS 除了 rootfs 还会选装很多软件、服务、图形桌面等,需要好几个 GB 就不足为奇了。
base 镜像提供的是最小安装的 Linux 发行版。
下面是 CentOS 镜像的 Dockerfile 的内容:
第二行 ADD 指令添加到镜像的 tar 包就是 CentOS 7 的 rootfs。在制作镜像时,这个 tar 包会自动解压到 / 目录下,生成 /dev, /proc, /bin 等目录。
注:可在 Docker Hub 的镜像描述页面中查看 Dockerfile 。
支持运行多种 Linux OS
不同 Linux 发行版的区别主要就是 rootfs。
比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。
所以 Docker 可以同时支持多种 Linux 镜像,模拟出多种操作系统环境。
上图 Debian 和 BusyBox(一种嵌入式 Linux)上层提供各自的 rootfs,底层共用 Docker Host 的 kernel。
这里需要说明的是:
1. base 镜像只是在用户空间与发行版一致,kernel 版本与发行版是不同的。
例如 CentOS 7 使用 3.x.x 的 kernel,如果 Docker Host 是 Ubuntu 16.04(比如我们的实验环境),那么在 CentOS 容器中使用的实际是是 Host 4.x.x 的 kernel。
① Host kernel 为 4.4.0-31
② 启动并进入 CentOS 容器
③ 验证容器是 CentOS 7
④ 容器的 kernel 版本与 Host 一致
2. 容器只能使用 Host 的 kernel,并且不能修改。
所有容器都共用 host 的 kernel,在容器中没办法对 kernel 升级。如果容器对 kernel 版本有要求(比如应用只能在某个 kernel 版本下运行),则不建议用容器,这种场景虚拟机可能更合适。
镜像的分层结构
Docker 支持通过扩展现有镜像,创建新的镜像。
实际上,Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件构建出来的。比如我们现在构建一个新的镜像,Dockerfile 如下:
① 新镜像不再是从 scratch 开始,而是直接在 Debian base 镜像上构建。
② 安装 emacs 编辑器。
③ 安装 apache2。
④ 容器启动时运行 bash。
构建过程如下图所示:
可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。
问什么 Docker 镜像要采用这种分层结构呢?
最大的一个好处就是 - 共享资源。
比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。
这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改?
答案是不会!
修改会被限制在单个容器内。
这就是我们接下来要学习的容器 Copy-on-Write 特性。
可写的容器层
当容器启动时,一个新的可写层被加载到镜像的顶部。
这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。
只有容器层是可写的,容器层下面的所有镜像层都是只读的。
下面我们深入讨论容器层的细节。
镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。
- 添加文件 在容器中创建文件时,新文件被添加到容器层中。
- 读取文件 在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后打开并读入内存。
- 修改文件 在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
- 删除文件 在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。
这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。
构建镜像
对于 Docker 用户来说,最好的情况是不需要自己创建镜像。几乎所有常用的数据库、中间件、应用软件等都有现成的 Docker 官方镜像或其他人和组织创建的镜像,我们只需要稍作配置就可以直接使用。
使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。特别是使用那些官方镜像,因为 Docker 的工程师知道如何更好的在容器中运行软件。
当然,某些情况下我们也不得不自己构建镜像,比如:
- 找不到现成的镜像,比如自己开发的应用程序。
- 需要在镜像中加入特定的功能,比如官方镜像几乎都不提供 ssh。
所以本节我们将介绍构建镜像的方法。同时分析构建的过程也能够加深我们对前面镜像分层结构的理解。
Docker 提供了两种构建镜像的方法:
- docker commit 命令
- Dockerfile 构建文件
docker commit
docker commit 命令是创建新镜像最直观的方法,其过程包含三个步骤:
- 运行容器
- 修改容器
- 将容器保存为新的镜像
举个例子:在 ubuntu base 镜像中安装 vi 并保存为新镜像。
- 第一步, 运行容器
-it
参数的作用是以交互模式进入容器,并打开终端。412b30588f4a
是容器的内部 ID。
- 安装 vi
确认 vi 没有安装。
安装 vi。
- 保存为新镜像
在新窗口中查看当前运行的容器。
silly_goldberg
是 Docker 为我们的容器随机分配的名字。
执行 docker commit 命令将容器保存为镜像。
新镜像命名为 ubuntu-with-vi
。
查看新镜像的属性。
从 size 上看到镜像因为安装了软件而变大了。
从新镜像启动容器,验证 vi 已经可以使用。
以上演示了如何用 docker commit 创建新镜像。然而,Docker 并不建议用户通过这种方式构建镜像。原因如下:
- 这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在 debian base 镜像中也加入 vi,还得重复前面的所有步骤。
- 更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。
既然 docker commit 不是推荐的方法,我们干嘛还要花时间学习呢?
原因是:即便是用 Dockerfile(推荐方法)构建镜像,底层也 docker commit 一层一层构建新镜像的。学习 docker commit 能够帮助我们更加深入地理解构建过程和镜像的分层结构。
Dockerfile 构建镜像
Dockerfile 是一个文本文件,记录了镜像构建的所有步骤。
第一个 Dockerfile
用 Dockerfile 创建上节的 ubuntu-with-vi,其内容则为:
下面我们运行 docker build 命令构建镜像并详细分析每个细节。
① 当前目录为 /root。
② Dockerfile 准备就绪。
③ 运行 docker build 命令,-t
将新镜像命名为 ubuntu-with-vi-dockerfile
,命令末尾的 .
指明 build context 为当前目录。Docker 默认会从 build context 中查找 Dockerfile 文件,我们也可以通过 -f
参数指定 Dockerfile 的位置。
④ 从这步开始就是镜像真正的构建过程。 首先 Docker 将 build context 中的所有文件发送给 Docker daemon。build context 为镜像构建提供所需要的文件或目录。
Dockerfile 中的 ADD、COPY 等命令可以将 build context 中的文件添加到镜像。此例中,build context 为当前目录 /root
,该目录下的所有文件和子目录都会被发送给 Docker daemon。
所以,使用 build context 就得小心了,不要将多余文件放到 build context,特别不要把 /
、/usr
作为 build context,否则构建过程会相当缓慢甚至失败。
⑤ Step 1:执行 FROM
,将 ubuntu 作为 base 镜像。
ubuntu 镜像 ID 为 f753707788c5。
⑥ Step 2:执行 RUN
,安装 vim,具体步骤为 ⑦、⑧、⑨。
⑦ 启动 ID 为 9f4d4166f7e3 的临时容器,在容器中通过 apt-get 安装 vim。
⑧ 安装成功后,将容器保存为镜像,其 ID 为 35ca89798937。
这一步底层使用的是类似 docker commit 的命令。
⑨ 删除临时容器 9f4d4166f7e3。
⑩ 镜像构建成功。
通过 docker images 查看镜像信息。
镜像 ID 为 35ca89798937,与构建时的输出一致。
在上面的构建过程中,我们要特别注意指令 RUN 的执行过程 ⑦、⑧、⑨。Docker 会在启动的临时容器中执行操作,并通过 commit 保存为新的镜像。
查看镜像分层结构
ubuntu-with-vi-dockerfile 是通过在 base 镜像的顶部添加一个新的镜像层而得到的。
这个新镜像层的内容由 RUN apt-get update && apt-get install -y vim
生成。这一点我们可以通过 docker history
命令验证。
docker history
会显示镜像的构建历史,也就是 Dockerfile 的执行过程。
ubuntu-with-vi-dockerfile 与 ubuntu 镜像相比,确实只是多了顶部的一层 35ca89798937,由 apt-get 命令创建,大小为 97.07MB。docker history 也向我们展示了镜像的分层结构,每一层由上至下排列。
注: 表示无法获取 IMAGE ID,通常从 Docker Hub 下载的镜像会有这个问题。