容器化是实现云原生的前置条件,没有容器化技术,云原生下的弹性能力、高资源利用率将不具备任何优势。本节会从虚拟化与容器化的区别展开,介绍容器化的核心原理及Docker的优点。
最早接触虚拟机是在学生时代,通过给装有Windows系统的PC装一个Ubuntu系统来体验不一样的操作系统,后来为了玩游戏,给Mac电脑安装了Windows系统的虚拟机。依稀记得那时通过VirtualBox等软件找各种操作系统镜像,笨重地拉起一个新窗口,指定一些硬盘、内存大小,里面就是一个全新的隔离世界了。那时候不理解虚拟机的原理,只记得硬盘、内存的大小是有限制的,不能设置得太大,否则主机就会变得运行缓慢。
后来才慢慢理解,虚拟机技术是通过Hypervisor层抽象底层的基础设施资源,隔离出一个个独立虚拟机空间。虚拟机与物理机的区别如图1-7所示。
在虚拟化环境下,物理服务器的CPU、内存、硬盘和网卡等硬件资源被虚拟化并接受Hypervisor 的调度,多个操作系统在Hypervisor的协调下可以共享这些虚拟化后的硬件资源,同时每个操作系统又可以保持彼此的独立性。根据所处的层次不同,Hypervisor又被分为Bare-metal(裸机)虚拟化方式和Host OS虚拟化方式。裸机虚拟化方式的Hypervisor不需要完整的主机操作系统,可以直接将Hypervisor部署在裸机上并将硬件资源虚拟机化,常见的这类Hypervisor有支持MacOS的HyperKit,支持Windows的Hyper-V、Xen以及KVM(Kernel-based Virtual Machine)。Host OS虚拟化方式是将Hypervisor安装在操作系统中,虚拟化软件以应用程序的形式运行在Windows或Linux操作系统中,常见的这类Hypervisor有VirtualBox和VMWare Workstation。两者的区别如图1-8所示。
图1-7 虚拟机与物理机的区别
图1-8 两种虚拟化方式对比
容器化可以像虚拟机一样虚拟化基础计算机,而无须虚拟化操作系统,如图1-9所示。容器位于物理服务器及其主机操作系统的顶部。每个容器共享主机操作系统内核,相比于虚拟机,容器非常轻量。
容器化其实有很多技术,比如LXC、BSD Jails、Solaris Zones。Docker的出现使容器更加易用,更多的技术人员把Docker作为容器化的代表。Docker的主要原理是把Linux的控制组(Control Groups,简称Cgroups)、命名空间(Namespace)、联合文件系统(Union File System,UFS)等容器底层技术进行抽象和封装,包含了镜像、容器、仓库三大组件,并为用户提供创建和管理容器的界面。Docker的主要特点是一次构建,多处运行,很好地诠释了容器化的跨平台和强一致性。我们通过表1-2来对比虚拟机与容器的差异。
图1-9 传统虚拟化对比容器虚拟化
表1-2 虚拟机与容器差异表
无论LXC还是Docker,底层主要的核心技术是Cgroups、Namespace。Cgroups是Linux内核提供的一种用来限定进程资源使用的技术,可以限制和隔离进程所使用的物理资源,比如CPU、内存、磁盘和网络I/O。相对于物理资源隔离,Namespace则是用来隔离进程ID、网络等系统资源的,类似Java中的类加载器(classloader)。即使是同样的PID、同样的IP,不同的Namespace之间也是相互独立的,毫无影响。比如父容器通过调用clone()函数创建两个子进程,ID分别为100、101,这两个子进程拥有自己的Namespace,映射到子进程后,分别对应PID为1的init进程,虽然在两个Namespace里PID都为1,但是有了Namespace的隔离,两者互不影响,如图1-10所示。
图1-10 Namespace隔离图
有了隔离,子容器之间可以相对独立、互不打扰地工作。每个容器都是为了处理特定工作的,比如有的容器负责提供数据库服务,有的容器负责提供缓存服务,有的容器负责应用系统的运行。如何决定容器创建后做什么工作呢?答案是通过Dockerfile。
我把Dockerfile比作人体的DNA,它记录了容器运行的子进程,进而决定了容器的核心功能。通过Dockerfile我们可以构建镜像,随时拉起多个容器,实现应用的高速扩展。业务应用的镜像本质上都很相似,假设应用A的Dockerfile为DockerfileA,应用B的Dockerfile为DockerfileB,它们都依赖于操作系统、JDK、Tomcat、日志采集器等,只有应用的War包不一样。如果每个镜像都重复维护多个共性的部分,带来的资源损耗和维护成本都是巨大的。
Docker采用分层技术来解决这个问题,每个容器都有自己独立的容器层,不同的容器共享一个镜像层,这样容器之间就可以共享基础资源。我们保存一个基础镜像(通常称作base镜像)到磁盘后,它就可以被其他镜像共享了,如图1-11所示。
图1-11 镜像共享图
Dockerfile底层用的核心文件共享技术就是UFS。UFS是一种轻量级、高性能、分层的文件系统。UFS把文件系统的每次修改作为一个个层进行叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。如果一次同时加载多个文件系统,UFS会把各层文件叠加起来,最终文件系统会包含所有底层文件和目录,从外部视角来看,用户看到的是一个文件系统。镜像就是利用UFS的特性,通过分层来进行继承、叠加,通常我们会先制作一个基础镜像,通过基础镜像衍生出各种具体的应用镜像。UFS是Docker镜像的基础。
Docker的普及,得益于它的众多优势,本节介绍与研发过程比较紧密的几个优势。
以前一台32核128GB的物理机最多可以创建20台4核8GB的虚拟机,每台虚拟机部署一个应用,机器使用率常年在10%以下,导致极大的资源浪费。换成Docker的方式启动应用后,由于共享操作系统网络、CPU、内存,一台物理机平均能运行超过60个容器,对应超过60个应用。在实际生产环境中,我所在公司1台32核128GB的物理机可以稳定运行110个应用,效率提升3~5倍,CPU使用率为35%~40%,大大提升了资源使用率,如图1-12所示。
图1-12 新旧物理机对比
之前申请一套数据库或者Redis,我们都会经过开发需求工单申请→运维机器调拨→系统环境初始化→数据库参数配置→数据库部署→数据库数据备份和恢复→开发业务库申请→交付使用,整个流程需要3~5天。换成Docker后,通过一套标准的基线Dockerfile直接调度,整个流程降到分钟级别。
在传统的KVM(Kernel-based Virtual Machines,基于内核的虚拟机)形态下,总是存在应用架构升级或版本升级的情况,有升级就有过渡期,新老版本并存,老版本被遗留很久,或者测试环境与生产环境配置不一致等问题,有了Dockerfile的标准化,这些问题都将不复存在。
传统的虚拟机部署流程一般是编码→编译→打成War包或Jar包→提交发布单→选择目标虚拟机IP并发布→验证,过程中如果遇到问题需要回滚,就非常麻烦了,往往需要运维人员登录具体的虚拟机,找到历史War包,覆盖最新的War包并回退。容器化之后的流程变为编码(Dockerfile初始化后很少修改)→编译→打Fat-Jar包→提交发布单→自动构建为Docker镜像→拉起容器,过程中若发现问题,直接拉取上一个最新版本的镜像,一键回滚。整个过程与环境的交互程度、人工参与度会大大降低,可流程化、重复化的步骤全都变为平台化操作,进而大大提升交付效率。
微服务主张将独立的功能抽象为独立的进程,原来的单体应用有可能一下变为10个微服务应用,对服务的维护、迭代都是很大的挑战。容器化后,业界比较认同的最佳实践是一个容器一个微服务,这样对微服务的运维管理会非常简洁轻量。
容器化后,由于根据Dockerfile可以在各种机房、不同环境随意调度,弹性能力大大增强,更符合云原生环境下对无状态的要求,因此能够更快捷、更原生地支持企业上云或多数据中心建设。