记得之前曾经粗略的写过一篇Docker的基础及ASP.NET Core部署Docker示例的入门文章,但那个时候刚刚学习Docker对Docker的认知还比较浅,现在重新来温故知新一下。此外,本篇已加入《.NET Core on K8S学习实践系列文章索引》,可以点击查看更多容器化技术相关系列文章。
一、容器的用途
首先,我们来温习一下Docker的几个用途,亦或者说Docker到底帮我们解决什么问题?
1、标准化打包
记得在容器技术出来之前,我们开发者进行打包一般都依赖于各自开发语言平*有的打包机制,比如.NET和Java平台下都会依赖于各自不同的发布部署技术,但在容器技术出来之后,不管是.NET还是Java都会将其发布为容器镜像推送到镜像仓库中来进行复用。
2、隔离
每个容器在运行时都会认为自己是独自占有了一台机器,即一个独立的环境互不干扰。其实,容器的本质是一个进程,进程与进程之间相互隔离造就了容器与容器互不影响的特性。在启动一个容器(即创建一个进程时),通过 Namespace 技术实现容器的隔离、通过 Cgroups 来实现容器的资源控制。
关于Namespace 和 Cgroups 可以继续浏览本文3.3小节。
3、标准化部署
在容器技术出来之前,和打包机制一样,我们都依赖于具体开发语言平台的部署机制,比如IIS、Tomcat等。但是,容器技术出来之后,即使我们使用不同的开发语言都可以使用同样的部署技术,例如Mesos或Kubernetes。至此,之前的运维人员也不在需要学习多套部署技术,只需要了解如K8s一类的标准化容器编排平台即可。
二、容器与集装箱的关系
提到容器要解决的问题,就不得不提一下运输业以及集装箱。几十年前,运输业面临着因货物类型不同而导致损失,又或者在运输过程中使用不同的交通工具也会让整个过程痛苦不堪。幸运的是,集装箱的发明帮助运输业解决了这个问题:
(1)任何货物,无论是钢琴还是玛莎拉蒂,都被放到各自的集装箱中。
(2)集装箱在整个运输过程中都是密封的,只有到达最终目的地才被打开。
(3)集装箱可以被高效地装卸、重叠和长途运输。例如:现代化的起重机可以自动在卡车、轮船和火车之间移动集装箱。
容器的核心思想其实也就是将集装箱的思想应用到了软件的打包和部署上,为各类不同的代码提供了一个基于容器的标准化运输系统。换句话说,容器可以将任何应用及其依赖环境打包为一个轻量级、可移植、自包含的独立运行环境,容器可以运行在几乎所有的操作系统之上。
不得不说,Docker的Logo就是一堆集装箱放在海豚上,作为海豚的docker就是一个标准化的运输系统:
三、容器核心技术揭秘
1、Linux操作系统内核一窥
为了进一步理解Docker,我们先来看看Linux操作系统及其内核,如下图所示:
从上图可知,最底层为硬件层,包含了内存、磁盘、CPU、网卡等;往上一层是内核空间,Kernel就是操作系统内核,负责管理硬件层中的各种资源 以及 调度进程 等工作;顶层是用户空间,用户程序就在此空间内运行,并调用内核空间提供的服务;
2、虚拟机和容器的差别
大概了解了操作系统的内核之后,我们再来看看老生常谈的容器和虚拟机的差异,如下图所示:
虚拟机:主要是由硬件虚拟化+内核虚拟化技术来实现,它在宿主机操作系统或硬件层的基础之上引入一层Hypervisor来虚拟出磁盘、CPU等资源,然后在虚拟出来的资源的基础之上运行Guest OS进而实现最终的虚拟机。
容器:直接在宿主机操作系统之上构建一个Docker Engine,共享宿主机操作系统内核,在此基础之上只引入了少量的Guest OS来实现。
对比:
(1)虚拟机的隔离性比容器好,因为虚拟机是一种强隔离机制;
(2)虚拟机比较重量级,启动时速度比较慢,消耗资源也比较多;
(3)容器的隔离性不如虚拟机,它是一种软件隔离机制,但它比较轻量级,引入的东西较少,所以速度快消耗资源少;因此,在同一个物理机上能够启动的容器的数量远远多于虚拟机的数量;
3、容器的核心技术
了解了操作系统的内核以及和虚拟机的差异,现在我们可以正式了解一下基于Linux内核的Docker容器核心技术到底有哪些(当然,本文只是粗略的介绍一下,更详细的部分请浏览本文的参考资料文章),如下图所示:
(1)CGroups:
容器进程创建好后,若不进行其他处理,该进程运行时所消耗及占用的资源(如 CPU、内存)等,是可以被其他宿主机进程或其他容器进程享用的。为了解决这个问题,Linux 容器设计中引入了 Cgroups 的概念。
Linux CGroups 的全称是 Linux Control Group,它的主要作用就是限制一个进程(这里也可以指容器)能够使用的资源上限(如 Cpu、内存、网络等等)。关于Docker的资源限制,可以阅读我这一篇《Docker资源限制学习与验证》文章。
(2)Namespaces:
刚刚提到,容器的本质是一个进程,进程与进程之间相互隔离造就了容器与容器互不影响的特性。在启动一个容器(即创建一个进程时),通过 Namespaces 技术实现容器的隔离。
容器进程的创建通过 Linux 平台下的 Clone 方法创建,在调用该方法创建进程时,通过指定额外的 Namespace 参数,使得刚创建的进程属于一个独立的空间。
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL)
指定额外参数 CLONE_NEWPID 创建的新进程,有一个自己的 独立进程空间,在这个空间里,它的进程 ID 为 1。它既看不到其在宿主机的真正进程、也看不到其他容器的进程。
(3)Networking
容器的创建还需要网络的支持,Networking这一块主要是虚拟网卡、网桥及iptables为容器提供组网支持;
(4)Storage
最后,容器的创建还需要存储的支持,Storage这一块提供了容器支持的一些文件系统,如Device Mapper、Btrfs 及 Aufs 等等;
4、容器的镜像
刚刚提到,容器镜像为标准化打包提供了基础。容器镜像采用的是分层的方式来组织的,如下图所示:
可以看到,底层的是基础镜像,称为Base Image,例如Ubuntu、CentOS等,它可以和宿主机的OS是不一样的,但是它会共享宿主机操作系统的内核;在基础镜像之上,可以有多层镜像,例如Java JDK的依赖,.NET Core Runtime依赖等;依赖层之上呢,可以是具体的应用程序的Release。
综上所述,容器镜像采用分层的方式,可以很方便地实现镜像层的复用。如果两个容器所依赖的底层镜像层是相同的,可以共同应用同一个Hash值的底层镜像,进而也可以节省传输和网络的开销。例如,图中Image1和Image2的就实现了基础镜像层的复用。
四、容器的架构一览
有了之前的基础知识,最后我们再来看看Docker的架构,如下图所示:
从上图可以看出,一个典型的Docker架构包含了三块内容:
(1)Docker Registry:镜像仓库,主要负责存储镜像,官方的仓库是Docker Hub,你也可以基于开源项目Harbor或者使用阿里云等云服务厂商提供的镜像仓库服务来搭建私有镜像仓库,如果有兴趣可以参考我的这一篇《Docker常用流行镜像仓库搭建》。
(2)Docker Host:Docker宿主,首先它会运行一个Docker daemon,会接收Docker Client发送的指令来执行拉取镜像、缓存、启动等操作;其次,Docker daemon执行完Docker Client发送过来的指令后,所有的容器都会在Docker Host上运行;
(3)Docker Client:客户端操作,主要负责通过docker命令行对容器进行基本操作,如拉取镜像,构建镜像,运行容器等等;
更多关于Docker架构的内容请参考:https://docs.docker.com/get-started/overview/
五、关于Docker Compose
Docker主要用来运行单容器应用,而Docker Compose则是一个用来定义和应用多容器应用的工具,如下图所示:
使用Docker Compose,我们可以将多容器的定义和部署方式定义在一个yml文件中,这种方式特别是微服务这种架构风格,可以将多个微服务的定义及部署都规范在一个yml文件中,然后一键部署、启动或销毁整个微服务应用。所有的一切操作,只需要下面的一句话:
$docker-compose up
很多人建议在测试环境,使用Docker Compose来快速的部署和测试微服务应用,在生产环境则建议使用Kubernetes这种生产级的容器云平台。
如果对Docker Compose感兴趣,我之前也有写一篇使用Docker Compose来编排Spring Cloud微服务的示例文章,有兴趣可以看看。
六、小结
本文从Docker容器要解决的几个问题入手,介绍了容器与集装箱的关联、容器的核心实现技术、容器的架构,最后简单介绍了一个Docker Compose这个多容器应用工具,相信能够从背景知识上帮你了解容器到底要帮助我们解决的问题。
参考资料
杨波,《Spring Boot与Kubernetes云原生应用实践》(强力推荐订阅学习)
EdisonZhou,《ASP.NET Core on Docker入门》
EdisonZhou,《Docker资源限制学习与验证》
godruoyi,《容器的工作原理和隔离机制》
CloudMan,《每天5分钟玩转Docker容器技术》