题外话
最近对 Docker
和Kubernetes
进行了一番学习,前两天做了一次技术分享,回去听了一遍自己演讲的录音,发现单单 PPT 做好还是远远不够的,没有提前准备好逻辑严谨的讲稿,在讲的时候出现了卡壳、漏掉技术点、逻辑矛盾的问题。为了解决这个问题,我打算以后在做技术分享前,都按着 PPT 的内容先写成博客,理顺表达逻辑。另外,我觉得每次技术分享使用的 PPT 都应该尽可能的做好,因为你不知道未来会不会还要拿来再讲几遍。本文以 PPT+ 讲稿的方式编写,权当对自己这次技术分享做个记录,欢迎大家拍砖。
1. Docker 出现的背景
在平常的研发和项目场景中,以下情况普遍存在:
-
个人开发环境
为了做大数据相关项目,需要安装一套 CDH 集群,常见的做法是在自己电脑里搭建 3 台与 CDH 版本对应的虚拟机,把 CDH 集群装起来后,考虑到以后很有可能还要使用一个干净的 CDH 集群,为了避免以后重复安装环境,通常会对整套 CDH 集群做一个备份,这样电脑里就有 6 个虚拟机镜像了。另外,后面在学习其他技术时,比如学习 Ambari 大数据集群,那么为了不破坏已有的虚拟机环境,又要重新搭建 3 台虚拟机,本机磁盘很快被一大堆的虚拟机镜像占满。 -
公司内部开发环境
公司里往往会以小团队的方式来做项目,一般由运维部门从他们管理的服务器资源中分配出虚拟机供团队内部开发测试使用。比如做一个与机器学习相关的项目:- 小明在运维部门分配的虚拟机上搭建了一套 Ambari 集群,拿来跑大数据相关业务
- 小刚用 python3 写了一个机器学习算法,放到虚拟机上运行发现虚拟机里是 python2,算法不兼容,于是把虚拟机里的 python 版本升级了,算法跑通了,但 Ambari 用到 python 的部分功能可能就报错了
- 小李开发了应用,放到虚拟机上启动 tomcat,发现虚拟机里的是 OpenJDK,导致 tomcat 起不来,于是又安装了一个 JDK,这时候可能 Ambari 里的 Java 代码可能就报错了
- 小赵想利用服务器资源做性能测试,发现虚拟机严重削减了性能,最终还是要直接找物理机来跑测试,破坏了物理机原来的环境
- 做完项目后,这些虚拟机上安装的东西往往变得没用了,下个项目组来还是得新申请虚拟机重新部署软件
-
开发 / 测试 / 现场环境
研发人员在开发环境里写好了代码做好测试后,提交给测试部门,测试人员在测试环境跑起来发现有 BUG,研发人员说在开发环境没这个 BUG,和测试人员多次扯皮解决 BUG 后发布版本,发到现场在生产环境部署后,又发现有 BUG,这下轮到工程人员和测试人员扯皮。有时候为了兼容特殊的现场环境,还需要对代码进行定制化修改,拉出分支,这样导致了每次到现场升级都是一场噩梦 -
升级或迁移项目
在每次发版本要升级到现场时,如果现场起了多个 tomcat 应用,那么需要对每个 tomcat 都先停掉,替换 war 包,然后再起起来,轮流着做,不仅繁琐而且很容易出错,如果遇到升级后出现严重 BUG,还要手工做回退。另外,如果项目想上云,那么在云上部署后要重新进行一轮测试,如果后面考虑还云厂商,可能相同的测试还要再进行一次(比如更换了数据存储组件),费时费力。
总结以上列举的所有场景,他们存在的一个共同的问题是:没有一种既能够屏蔽操作系统差异,又能够以不降低性能的方式来运行应用的技术,来解决环境依赖的问题。Docker 应运而生。
2. Docker 是什么
Docker 是一种应用容器引擎。首先说一下何为容器,Linux 系统提供了 Namespace
和CGroup
技术实现环境隔离和资源控制,其中 Namespace 是 Linux 提供的一种内核级别环境隔离的方法,能使一个进程和该进程创建的子进程的运行空间都与 Linux 的超级父进程相隔离,注意 Namespace 只能实现运行空间的隔离,物理资源还是所有进程共用的,为了实现资源隔离,Linux 系统提供了 CGroup 技术来控制一个进程组群可使用的资源(如 CPU、内存、磁盘 IO 等),把这两种技术结合起来,就能构造一个用户空间独立且限定了资源的对象,这样的对象称为容器。Linux Container
是 Linux 系统提供的容器化技术,简称 LXC
,它结合 Namespace 和 CGroup 技术为用户提供了更易用的接口来实现容器化。LXC 仅为一种轻量级的容器化技术,它仅能对部分资源进行限制,无法做到诸如网络限制、磁盘空间占用限制等。dotCloud 公司结合 LXC 和 以下列出的技术
实现了 Docker 容器引擎,相比于 LXC,Docker 具备更加全面的资源控制能力,是一种应用级别的容器引擎。
- Chroot:该技术能在 container 里构造完整的 Linux 文件系统;
- Veth:该技术能够在主机上虚拟出一张网卡与 container 里的 eth0 网卡进行桥接,实现容器与主机、容器之间的网络通信;
- UnionFS:联合文件系统,Docker 利用该技术“Copy on Write”的特点实现容器的快速启动和极少的资源占用,后面会专门介绍该文件系统;
- Iptables/netfilter:通过这两个技术实现控制 container 网络访问策略;
- TC:该技术主要用来做流量隔离,限制带宽;
- Quota:该技术用来限制磁盘读写空间的大小;
- Setrlimit:该技术用来限制 container 中打开的进程数,限制打开的文件个数等
也正是因为 Docker 依赖 Linux 内核的这些技术,至少使用 3.8 或更高版本的内核才能运行 Docker 容器,官方建议使用 3.10 以上的内核版本。
3. 与传统虚拟化技术的区别
传统的虚拟化技术在虚拟机(VM)和硬件之间加了一个软件层 Hypervisor,或者叫做虚拟机管理程序。Hypervisor 的运行方式分为两类:
- 直接运行在物理硬件之上。如基于内核的 KVM 虚拟机,这种虚拟化需要 CPU 支持虚拟化技术;
- 运行在另一个操作系统。如 VMWare 和 VitrualBox 等虚拟机。
因为运行在虚拟机上的操作系统是通过 Hypervisor 来最终分享硬件,所以虚拟机 Guest OS 发出的指令都需要被 Hypervisor 捕获,然后翻译为物理硬件或宿主机操作系统能够识别的指令。VMWare 和 VirtualBox 等虚拟机在性能方面远不如裸机,但基于硬件虚拟机的 KVM 约能发挥裸机 80% 的性能。这种虚拟化的优点是不同虚拟机之间实现了完全隔离,安全性很高,并且能够在一台物理机上运行多种内核的操作系统(如 Linux 和 Window),但每个虚拟机都很笨重,占用资源多而且启动很慢。
Docker 引擎运行在操作系统上,是基于内核的 LXC、Chroot 等技术实现容器的环境隔离和资源控制,在容器启动后,容器里的进程直接与内核交互,无需经过 Docker 引擎中转,因此几乎没有性能损耗,能发挥出裸机的全部性能。但由于 Docker 是基于 Linux 内核技术实现容器化的,因此使得容器内运行的应用只能运行在 Linux 内核的操作系统上。目前在 Window 上安装的 docker 引擎其实是利用了 Window 自带的 Hyper- V 虚拟化工具自动创建了一个 Linux 系统,容器内的操作实际上是间接使用这个虚拟系统实现的。
4. Docker 基本概念
Docker 主要有如下几个概念:
- 引擎:创建和管理容器的工具,通过读取镜像来生成容器,并负责从仓库拉取镜像或提交镜像到仓库中;
- 镜像:类似于虚拟机镜像,一般由一个基本操作系统环境和多个应用程序打包而成,是创建容器的模板;
- 容器:可看作一个简易版的 Linxu 系统环境(包括 root 用户权限、进程空间、用户空间和网络空间等)以及运行在其中的应用程序打包而成的盒子;
- 仓库:集中存放镜像文件的场所,分为公共仓库和私有仓库,目前最大的公共仓库是官方提供的 Docker Hub,此外国内的阿里云、腾讯云等也提供了公共仓库;
- 宿主机:运行引擎的操作系统所在服务器。
5. Docker 与虚拟机、Git、JVM 的类比
为了让大家对 Docker 有更直观的认识,下面分别进行三组类比:
上图中 Docker 的镜像仓库类似于传统虚拟机的镜像仓库或存放镜像的本地文件系统,Docker 引擎启动容器来运行 Spark 集群(容器内包含基础的 Linux 操作系统环境),类比于虚拟机软件启动多个虚拟机,在虚拟机内分别运行 Spark 进程,两者区别在于 Docker 容器内的应用在使用物理资源时,直接与内核打交道,无需经过 Docker 引擎。
Docker 的仓库思想与 Git 是相同的。
Docker 的口号是“Build,Ship,and Run Any App,Anywhere”,也就是可以基于 Docker 构建、装载和运行应用程序,一次构建到处运行。Java 的口号是“Write Once,Run Anywhere”,即一次编写到处运行。Java 是基于 JVM 适配操作系统的特点来屏蔽系统的差异,Docker 则是利用内核版本兼容性的特点来实现一次构建导出运行,只要 Linux 系统的内核是 3.8 或更高的版本,就都能把容器跑起来。
当然,正如 Java 中如果应用代码使用了 JDK10 的新特性,基于 JDK8 就无法运行一样,如果容器内的应用使用了 4.18 版本的内核特性,那么在 CentOS7(内核版本为 3.10)启动容器时,虽然容器能够启动,但里面应用的功能是无法正常运行的,除非把宿主机的操作系统内核升级到 4.18 版本。
6. Docker 镜像文件系统
Docker 镜像采用分层存储格式,每个镜像可依赖其他镜像进行构建,每一层的镜像可被多个镜像引用,上图的镜像依赖关系,K8S 镜像其实是 CentOS+GCC+GO+K8S 这四个软件结合的镜像。这种分层结构能充分共享镜像层,能大大减少镜像仓库占用的空间,而对用户而言,他们所看到的容器,其实是 Docker 利用 UnionFS(联合文件系统)把相关镜像层的目录“联合”到同一个挂载点呈现出来的一个整体,这里需要简单介绍一个 UnionFS 是什么:
UnionFS 可以把多个物理位置独立的目录(也叫分支)内容联合挂载到同一个目录下,UnionFS 允许控制这些目录的读写权限,此外对于只读的文件和目录,它具有“Copy on Write(写实复制)”的特点,即如果对一个只读的文件进行修改,在修改前会先把文件复制一份到可写层(可能是磁盘里的一个目录),所有的修改操作其实都是对这个文件副本进行修改,原来的只读文件并不会变化。其中一个使用 UnionFS 的例子是:Knoppix,一个用于 Linux 演示、光盘教学和商业产品演示的 Linux 发行版,它就是把一个 CD/DVD 和一个存在在可读写设备(例如 U 盘)联合挂载,这样在演示过程中任何对 CD/DVD 上文件的改动都会在被应用在 U 盘上,不改变原来的 CD/DVD 上的内容。
UnionFS 有很多种,其中 Docker 中常用的是 AUFS,这是 UnionFS 的升级版,除此之外还有 DeviceMapper、Overlay2、ZFS 和 VFS 等。Docker 镜像的每一层默认存放在 /var/lib/docker/aufs/diff
目录中,当用户启动一个容器时,Docker 引擎首先在 /var/lib/docker/aufs/diff
中新建一个可读写层目录,然后使用 UnionFS 把该可读写层目录和指定镜像的各层目录联合挂载到 /var/lib/docker/aufs/mnt
里的一个目录中(其中指定镜像的各层目录都以只读方式挂载),通过 LXC 等技术进行环境隔离和资源控制,使容器里的应用仅依赖 mnt 目录中对应的挂载目录和文件运行起来。
利用 UnionFS 写实复制的特点,在启动一个容器时,Docker 引擎实际上只是增加了一个可写层和构造了一个 Linux 容器,这两者都几乎不消耗系统资源,因此 Docker 容器能够做到秒级启动,一台服务器上能够启动上千个 Docker 容器,而传统虚拟机在一台服务器上启动几十个就已经非常吃力了,而且虚拟机启动很慢,这是 Docker 相比于传统虚拟机的两个巨大的优势。
当应用只是直接调用了内核功能来运作的情况下,应用本身就能直接作为最底层的层来构建镜像,但因为容器本身会隔绝环境,因此容器内部是无法访问宿主机里文件的(除非指定了某些目录或文件映射到容器内),这种情况下应用代码就只能使用内核的功能。但是 Linux 内核仅提供了进程管理、内存管理、文件系统管理等一些基础且底层的管理功能,在实际的场景中,几乎所有软件都是基于操作系统来开发的,因此往往都需要依赖操作系统的软件和运行库等,如果这些应用的下一层直接是内核,那么应用将无法运行。所以实际上应用镜像往往底层都是基于一个操作系统镜像来补足运行依赖的。
Docker 中的操作系统镜像,与平常安装系统时用的 ISO 镜像不同。ISO 镜像里包含了操作系统内核及该发行版系统包含的所有目录和软件,而 Docker 中的操作系统镜像,不包含系统内核,仅包含系统必备的一些目录(如 /etc /proc 等)和常用的软件和运行库等,可把操作系统镜像看作内核之上的一个应用,一个封装了内核功能,并为用户编写的应用提供运行环境的工具。应用基于这样的镜像构建,就能够利用上相应操作系统的各种软件的功能和运行库,此外,由于应用是基于操作系统镜像来构建的,就算换到另外的服务器,只要操作系统镜像中被应用使用到的功能能适配宿主机的内核,应用就能正常运行,这就是一次构建到处运行的原因。
下图形象的表现出了镜像和容器的关系:
上图中 Apache 应用基于 emacs 镜像构建,emacs 基于 Debian 系统镜像构建,在启动为容器时,在 Apache 镜像层之上构造了一个可写层,对容器本身的修改操作都在可写层中进行。Debian 是该镜像的基础镜像(Base Image),它提供了内核 Kernel 的更高级的封装。同时其他的镜像也是基于同一个内核来构建的(以下的 BusyBox 是一个精简版的操作系统镜像):
这时候就会有一个问题,应用基于操作系统镜像来构建,那如果操作系统镜像本身就很占空间,岂不是镜像的分发不方便,而且镜像仓库占用的空间也会很大。有人已经考虑到这一点,针对不同的场景分别构造了不同的操作系统镜像,下面介绍几种最常用的系统镜像。
7. Docker 基础操作系统
以上系统镜像分别适用于不同的场景:
- BusyBox:一个极简版的 Linux 系统,集成了 100 多种常用 Linux 命令,大小不到 2MB,被称为“Linux 系统的瑞士军刀”,适用于简单测试场景;
- Alpine:一个面向安全的轻型 Linux 发行版系统,比 BusyBox 功能更完善,大小不到 5MB,是官网推荐的基础镜像,由于其包含了足够的基础功能和体积较小,在生产环境中最常用;
- Debian/Ubuntu: Debian 系列操作系统,功能完善,大小约 170MB,适合研发环境;
- CentOS/Fedora:都是基于 Redhat 的 Linux 发行版,企业级服务器常用操作系统,稳定性高,大小约 200MB,适合生产环境使用。
8. Docker 持久化存储
根据前面介绍的容器 UnionFS 写实复制的特点,可知在容器里增加、删除或修改文件,其实都是对可写层里的文件副本进行了操作。在容器关闭后,该可写层也会被删除,对容器的所有修改都会失效,因此需要解决容器内文件持久化的问题。Docker 提供了两种方案来实现:
- 把宿主机文件系统里的目录映射到容器内的目录,
如下图所示
。如此一来,容器内在该目录里创建的所有文件,都存储到宿主机的对应目录中,在关闭容器后,宿主机的目录依然存在,再次启动容器时还能读取到之前创建的文件,因此实现了容器的文件持久化。当然同时要明白,如果是对镜像自带文件进行了修改,由于镜像是只读的,该修改操作无法在关闭容器时保存下来,除非在修改了文件后构建一个新的镜像。
- 把多台宿主机的磁盘目录通过网络联合为共享存储,然后把共享存储中的特定目录映射给特定的容器,
如下图所示
。这样容器在重启时,还是能读取到关闭前创建的文件。生产环境中常用 NFS 作为共享存储方案。
9. Docker 镜像制作方法
镜像制作方法有两种:
- 通过正在运行的容器生成新镜像
当一个容器在运行时,在里面所有的修改都会体现在容器的可写层,Docker 提供了 commit 命令,可以把正在运行的容器,叠加上可写层的修改内容,生成一个新镜像。如上图所示,在容器里新安装 Spark 组件的,如果关闭容器,Spark 组件会随着可写层的消失而消失,如果在关闭容器之前使用 commit 命令生成新镜像,那么使用新镜像启动为容器时,容器里就会包含 Spark 组件。
这种方式比较简单,但无法直观的设置环境变量、监听端口等内容,适合在简单使用的场景运用。
- 通过 Dockerfile 文件来生成新镜像
Dockerfile 是一个定义了镜像创建步骤的文件,Docker 引擎通过 build 命令读取 Dockerfile,按定义的步骤来一步步构造镜像。在研发和实施环境中,通过 Dockerfile 创建容器是主流做法。下面是一个 Dockerfile 的例子:
FROM ubuntu/14.04 # 基础镜像
MAINTAINER guest # 制作者签名
RUN apt-get install openssh-server -y # 安装 ssh 服务
RUN mkdir /var/run/sshd # 创建目录
RUN useradd -s /bin/bash -m -d /home/guest guest # 创建用户
RUN echo‘guest:123456’| chpasswd # 修改用户密码
ENV RUNNABLE_USER_DIR /home/guest # 设置环境变量
EXPOSE 22 # 容器内默认开启的端口
CMD ["/usr/sbin/sshd -D"] # 启动容器时自动启动 ssh 服务
Docker 引擎可以根据以上 Dockerfile 定义的步骤,构造出一个带有 ssh 服务的 Ubuntu 镜像。
10. Docker 的使用场景
Docker 作为一种轻量级的虚拟化方案,应用场景十分丰富,下面收集了一些常见的场景:
-
作为轻量级虚拟机使用
可以使用 Ubuntu 等系统镜像创建容器,当作虚拟机来使用,相比于传统虚拟机,启动速度更快,资源占用更少,单机可以启动大量的操作系统容器,方便进行各种测试; -
作为云主机使用
结合 Kubernetes 这样的容器管理系统,可以在大量服务器上动态分配和管理容器,在公司内部,甚至可以取代 VMWare 这样的虚拟机管理平台,使用 Docker 容器作为云主机使用; -
应用服务打包
在 Web 应用服务开发场景,可以把 Java 运行环境、Tomcat 服务器打包为一个基础镜像,在修改了代码包后加入到基础镜像来构建一个新的镜像,能很方便的升级服务和控制版本; -
容器云平台 CaaS
Docker 的出现,使得很多云平台供应商开始提供容器云的服务,简称容器即服务 CaaS,以下对比一下 IaaS、PaaS 和 SaaS:- IaaS(基础设施即服务):提供虚拟机或者其他基础资源作为服务提供给用户。用户可以从供应商那里获得虚拟机或者存储等资源来装载相关的应用,同时这些基础设施的繁琐的管理工作将由 IaaS 供应商来处理。其主要的用户是企业的系统管理员和运维人员;
- PaaS(平台即服务):把开发平台作为服务提供给用户。用户可以在一个包括 SDK,文档和测试环境等在内的开发平台上非常方便地编写应用,而且不论是在部署,或者在运行的时候,用户都无需为服务器、操作系统、网络和存储等资源的管理操心,这些繁琐的工作都由 PaaS 供应商负责处理。其主要的用户是企业开发人员。
- SaaS(软件即服务):将应用作为服务提供给客户。用户只要接上网络,并通过浏览器,就能直接使用在云端上运行的应用,而不需要顾虑类似安装等琐事,并且免去初期高昂的软硬件投入。SaaS 主要面对的是普通的用户。
- CaaS(容器即服务):完成 IaaS 和 PaaS 两个层级的功能。相对于传统的 IaaS 和 PaaS 服务,CaaS 对底层的支持比 PaaS 更灵活,而对上层应用的操控又比 IaaS 更容易。同时因为 Docker 是比 VM 更细粒度的虚拟化服务,所以能够对计算资源做到更高效的利用。CaaS 可以部署在任何物理机,虚拟机或 IaaS 云之上。
-
持续集成和持续部署
互联网行业提倡敏捷开发,持续集成部署 CI/CD 便是最典型的开发模式。使用 Docker 容器云平台,就能实现从代码编写完成推送到 Git/SVN 后,自动触发后端 CaaS 平台将代码下载、编译并构建成测试 Docker 镜像,再替换测试环境容器服务,自动在 Jenkins 或者 Hudson 中运行单元 / 集成测试,测试通过后,马上就能自动将新版本镜像更新到线上,完成服务升级。整个过程全自动化,一气呵成,最大程度地简化了运维,而且保证线上、线下环境完全一致,而且线上服务版本与 Git/SVN 发布分支也实现统一。 -
解决微服务架构的实施难题
基于 Spring Cloud 这样的微服务框架,能够实现微服务的管理,但微服务本身还是需要运行在操作系统上。一个采用微服务架构开发的应用中,微服务的个数往往很多,这就导致了一台服务器上往往需要启动多个微服务来提高资源的利用率,而微服务本身可能就只能兼容部分操作系统,这就导致了就算有大量的服务器资源(操作系统可能不一样),但由于微服务本身与操作系统可能相关,就不能做到让微服务在任意服务器上运行,这就带来了资源的浪费和运维的困难。利用 Docker 容器的环境隔离能力,让微服务运行在容器内,就能够解决以上所说的问题。 -
执行临时任务
有时候用户只是想执行一次性的任务,但如果用传统虚拟机的方式就要搭建环境,执行完任务后还要释放资源,比较麻烦。使用 Docker 容器就可以构建临时的运行环境,执行完任务后关闭容器即可,方便快捷。 -
多租户环境
利用 Docker 的环境隔离能力,可以为不同的租户提供独占的容器,实现简单而且成本较低。
11. 总结
Docker 的技术并不神秘,只是整合了前人积累的各种成果实现的应用级的容器化技术,它利用各种 Linux 发行版中使用了版本兼容的内核容器化技术,来实现镜像一次构建到处运行的效果,并且利用了容器内的基础操作系统镜像层,屏蔽了实际运行环境的操作系统差异,使用户在开发应用程序时,只需确保在选定的操作系统和内核版本上能正确运行即可,几乎不需要关心实际的运行环境的系统差异,大大提高效率和兼容性。但随着容器运行得越来越多,容器管理将会称为另一个运维的难题,这时候就需要引入 Kubernetes、Mesos 或 Swarm 这些容器管理系统,后面有机会再介绍这些技术。