初识 Docker 容器技术和相关概念
01Docker是什么
Docker是一个开源的软件,它允许开发人员将应用程序和程序所依赖的库等运行环境一起打包,构建出容器镜像后开发人员可以放心的将整个镜像包移植到任意服务器上运行。由于Docker是连同依赖环境一同打包的,这种打包机制保证本地和云端环境的高度一致性,同时也避免了之前由于环境不一致导致的各种差异性问题。Docker的出现也是给PaaS世界带来了“降维打击”,它的杀手锏之一就是解决了应用打包这个根本问题,更贴近开发者。而Docker通过这种Build once Run anywhere的能力,对于开发人员可以帮助他们更专注编写代码不用担心运行的系统环境问题,而对于运维人员通过容器的隔离机制和灵活性来完成业务的弹性扩缩和降低资源消耗。
如果一个新技术得到了广泛的推广,那么技术的推进过程往往是它解决了上一个时代所无法解决的痛点问题,补足了之前的不足。我们来看一下在容器部署时代之前的时代对比就一目了然了。
可以看到容器部署时代主要解决的问题一个是怎么样可以频繁构建,另一个是跨环境的一致性问题。这个与DevOps的理念不谋而合,DevOps所解决的核心问题也是如何使得企业在构建、测试、发布应用或软件能够更加快捷、频繁、可靠,所以很多时候Docker已经成为DevOps推动的重要工具之一。
Docker像虚拟机但不是虚拟机
刚开始接触Docker的同学往往把它比作虚拟机,不可否认它和虚拟机确实很像,同样可以隔离资源,同样可以有单独的“操作系统”,我们来看一下虚拟机和Docker的对比图,如下所示:
可以看到右图中画出了虚拟机的基本结构图,它通过Hypervisoer这个组件完成虚拟化工作,在它之上模拟出一个个虚拟的操作系统,每个虚拟的操作系统也有各自的虚拟硬件设备,如CPU、内存、I/O设备。而再来看左边Docker容器的结构图,Docker不会真正的给你模拟出一个虚拟的操作系统,它是通过Docker引擎这个“黑魔法”来实现上述的功能。服务器硬件还是需要一个Host Operating System操作系统,而Docker起得是辅助作用,在应用启动的过程中添加一些隔离和限制的方法。也正是由于虚拟机和Docker的本质区别,使得Docker比虚拟化技术占用的资源更小,启动的更快。
那么Docker应该怎么理解呢?上文我们提到了应用在启动过程中Docker给应用添加了一些“黑魔法”,这些“黑魔法”指的就是Namespace和Cgroups,它们分别起到隔离和限制的作用,这些特性也很早就被引入到Linux内核中。
咱们先来看Namespace,它的主要实现是让进程有独立的运行空间,并且对外界是无感知的,通过每个进程有独立的系统环境达到隔离的效果。在Linux内核中提供了六种Namespace隔离的系统调用,如下所示:
以其中的PID为例来验证一下,首先我们来运行一个Docker容器,可以看到在容器中看到的PID是1,但是在操作系统中真实的PID是5366,通过Namespace这种隔离术让容器在内部有了一套独立的运行环境,包括自己的进程、主机名、网络栈、文件系统等。
我们来做一个实验,首先创建一个test.c文件,文件内容如下:
在代码中当我们调用clone()中我们指定了CLONE_NEWPID参数,这样新创建的容器进程就会有一个全新的PID,在它的Namespace中,PID的值是1,我们可以运行这段代码验证一下。
在每个Namespace中的应用进程,都会认为自己的PID值是1,同时它也看不到物理服务器真正的进程空间,每个进程通过这种方式相互隔离开来。
接下来我们看一下Docker的另一个特性Cgroup,它的主要作用是将应用进程使用的资源进行限制。Cgroups是control groups的缩写,它是linux内核的特性,主要的作用是限制、计算和隔离进程的资源使用,包括cpu、内存、磁盘io、网络等方面。
Cgroups提供虚拟文件系统作为进行分组管理和各个子系统设置的用户接口,首先先了解一些cgourp的概念。
Task任务,在Cgroups中task任务代表一个系统进程
Control group控制族群,control group控制组主要是通过标准对进程的划分,达到对进程的限定。
Hierarchy层级,control group可以通过hierarchy组成一个层级树,下层cgourp继承父节点的属性。
Subsystem子系统,指定资源,比如cpu、内存等的资源调度控制器,负责cgourp下的资源控制功能。
如下图所示,cpu和内存两个子系统都有各自的层级结构,同时又通过task任务调用取得相互的联系。
具体我们来看一下Docker是如何使用Cgroup进行资源限制的,先运行一个容器同时限制它的内存为100M,执行下面这个命令:
我们运行了一个nginx的容器,并且使用-memory参数限制了应用的内存为100M,通过容器的ID反向在/sys/fs/cgroup/中查到系统中cgroup的资源限制。在这个目录中可以看到所有应用的CPU、内存、网络I/O等限制情况。
我们已经讲解了Docker的两个重要武器Namespace和Cgroups,Docker技术使用它们实现应用进程的分隔,以便各自进程独立运行。同时Docker在打包上使用分层镜像的模式,使得它能够轻松跨多种环境,解决在部署过程中的程序依赖。为多环境自动部署应用提供最佳实践方案。
02
Docker的主要特性
环境一致性
Docker可以有很好的移植性,由于在容器打包成镜像的过程中已经解决环境依赖问题,所以可以保证在多种环境下运行的一致性。你的应用可以比作成货物,由Docker进行打包封装到集装箱里,迁移到任何环境货物的本质是不会改变的。无论是本地环境还是多种云端环境,Docker都可以融入其中并有很好的可一致性。现在微软也越来越拥抱Docker,无论是Azure的支持还是对dotnet应用编译打包等。
基于镜像的版本管理
在每个Docker镜像文件中都包含多个层,这些镜像层组合在一起构成整个容器镜像,在构建B镜像时如果发现B镜像已经包含A镜像,那么B镜像在构建时可以直接在A镜像之上进行叠加。我们可以使用公有的容器镜像仓库或者私有化部署的镜像仓库比如harbor,这样就可以像使用代码仓库已经管理我们的容器。
快速迭代与发布
Docker在开发和发布的生命周期中,不但能保证各个环境的一致性,而且可以基于镜像仓库进行管理,在与CI/CD工具进行集成后可以方便我们进行容器镜像的发布和回滚。这样你的应用整体部署时间可以缩短至几秒,你可以轻松高效的创建和销毁应用容器。
快速扩容缩容
在企业运行场景中,比如一家电商平台在促销时间段业务访问量突增的情况下,通过Docker容器的快速启动特性,并且配合容器的编排平台的感知调度,让业务应用可以弹性扩充满足高并发下的访问压力。
03
学会编写Dockerfile
在学习Dockerfile语法前,先需要一个Docker的运行环境,可以通过如下方法安装Docker:
curl -fsSL "https://get.docker.com/" | sh && systemctl enable --now docker
Dockerfile可以看做是一个Docker镜像的表述文件,它里面包含一条条的指令,每一条指令将构建出一个Docker镜像层,指令的叠加也是镜像层的叠加最终形成一个完整的Docker镜像。一个标准的Dockerfile格式如下:
话不多说,我们先写一段代码用golang写一个web服务端,命名为main.go
根据上面的代码我们来编写一个Dockerfile将代码封装到容器镜像里。具体的Dockerfile可以是如下内容:
我们来看这段Dockerfile,第一行使用FROM来声明这个容器镜像使用的基础镜像,比如我们在这里引用了centos7这个基础镜像。第二行中MAINTAINER是来声明我们这个容器镜像的作者信息。而后面的RUN命令是在这个基础镜像中执行的linux命令,每RUN一行都会在当前镜像中创建一个新的镜像层并依次叠加。最后的CMD命令是声明在容器启动的默认命令。可以看到Dockerfile文件其实就是申明了一个应用在运行时需要的所有依赖环境和信息。你可以把这个Dockerfile比作一个你想要的汽车的装配清单,你把这个清单放在任何一个汽车工厂像Docker引擎上都可以装配出你期望的那个汽车来。我们可以根据这个Dockerfile来编译出我们想要的容器应用镜像,如下:
docker build -t golang-example:1.1 .
可以看到我们通过上述的Dockerfile编译出golang-example这个容器镜像,在编译过程中正如我们之前所说的每个命令都会在容器镜像中新建一个层级,最终的镜像ID是378f604a2012
05
来,运行一个的容器
在上一个章节中我们创建了一个名称为golang-example版本1.1的容器镜像,接下来我们尝试在宿主机上运行这个容器镜像,命令如下:
docker run -it --rm -p 8080:8080 golang-example:1.1
这里的-p参数指的是映射端口,让你的容器里应用运行端口和宿主机上端口做映射,使得用户可以通过宿主机的8080端口进行访问容器。我们可以通过curl命令测试一下应用是否正常启动。
我们可以非常便捷的启动一个容器,同时可以使用docker run -help去查询里面的运行参数,让我们制定容器启动的限制和网络环境等。
06
在工作中我们使用容器的姿势
在我们实际工作中,怎么设计一个Dockerfile呢?下面有一些建议以供大家参考:
1)一个容器一个应用,虽然你可以将多个应用封装到一个容器里,但是不建议这么操作,这样的话会导致应用镜像的体积越来越大,构建时间也会变长,应用进程管理和僵尸进程等问题。
2)将多条RUN命令合并,前面有讲到每条RUN命令都会新建一层,如果一个Dockerfile中包含过多的RUN命令,可以通过&&将多个RUN合并成一条,这样会大大减小容器镜像的体积。
3)使用alpine作为基础镜像,随着容器技术的火热,alpine这个linux操作系统也映入大家的视线,它比centos、Ubuntu等容器镜像要小很多,使用alpine作为基础镜像编译出来的容器镜像要小很多。
4)使用exec模式避免使用shell模式,在CMD和ENTRYPOINT中推荐使用exec模式(CMD [“command”,“param1”,“param2”])避免使用shell模式(CMD command param1 param2)。shell模式会调用shell进程去执行命令,它会成为/bin/sh -c的子命令。而exec模式不需要shell进程,可以接收Unix信号,比如执行docker stop时可以接收SIGTERM信号,可以让进程安全退出。
5)使用多段FROM进行构建,在Docker版本17以后可以使用多段FROM进行构建,这样就可以在运行环境中不掺杂编译环境,从而减小容器镜像的体积,如下:
闭环。
本章节我们就介绍到这里,下一章节我们将带来《kubernetes in 5 mins》精彩内容,欢迎继续关注!