一、docker简介
1.1 docker的前世今生
Docker是基于Go语言实现的开源容器项目,诞生于2013年年初,最初的发起者是dotCloud公司,Docker自开源后受到广泛的关注和讨论,目前已有多个相关项目(包括Docker三剑客、Kubernets等),逐渐形成了围绕Docker容器的生态体系。由于Docker在业界造成的影响力实在太大,dotCloud后来直接改名Docker Inc,并专注于Docker相关技术和产品的开发。
Docker项目已加入Liunx基金会,并遵循Apache2.0协议,全部开源代码均在https://github.com/docker/docker上进行维护。现在主流的Linux操作系统都已经支持Docker。
Docker的构想是要实现**“Build,Skip and Run Any APP,Anywhere**”,即通过对应用的封装(Packaging)、分发(Distribution)、部署(Deployment)、运行(Runtime)生命周期进行管理,达到应用组件“一次封装,到处运行”的目的。这里的应用组件,既可以是一个WEB应用、一个编译环境,也可以是一套数据库平台服务,甚至可以是一个操作系统或集群。
跟大部分新兴技术的诞生一样,Docker也并非“从石头缝里蹦出来的”,而是站在前人的肩膀上,其中最重要的是Linux容器(Linux Containers,LXC)技术。LXC是linux原生支持的容器技术,可以提供轻量级的虚拟化。LXC使用了两个主要的技术:
(1)namespace
Linux Namespaces机制提供一种资源隔离方案。
namespace | 隔离内容 | 说明 |
---|---|---|
UTS(unix timesharing system) | 主机名和域名 | 每个容器拥有独立的主机名和域名,在网络上可被识别为独立的节点 |
IPC(Inter-Process Communication) | 信号量、消息队列和共享内存 | 隔离进程间的通信 |
PID(Process Identification) | 进程编号 | 隔离进程ID |
Net(Network) | 网络设备、网络栈、端口等 | 隔离网络 |
MNT(mount) | 挂载点(文件系统) | 隔离文件系统和提供硬盘挂载点 |
User(user) | 用户和用户组 | 隔离用户和用户组 |
(2)cgroup
Cgroups 的全称是control groups,cgroups为每种可以控制的资源定义了一个子系统,用于限制、统计和隔离容器的资源使用,如CPU、内存、磁盘、I/O等
主要用到的cgroups子系统:cpu(限制进程的cpu使用率),blkio(可以限制进程的块设备IO),device(可以控制进程能够访问某些设备),freezer(可以挂起或恢复cgroups中的进程),memory(可以限制进程的memory使用量)
cgroups实现了对容器的资源分配和限制,比如给容器1分配10core 30G 内存,那这个容器最多用这么大的资源;如果内存超过30G ,会启动swap,效率降低,也可能会被调度系统给kill掉。
LXC 的工作原理基于 Linux 内核的命名空间和控制组(cgroups)技术,通过以下步骤实现容器的隔离和管理:
创建命名空间:为每个容器分配独立的命名空间,确保容器内的进程在自己的隔离环境中运行。
设置控制组:为容器分配资源限制,如 CPU、内存、磁盘 I/O 等,防止容器过度消耗主机资源。
挂载文件系统:为每个容器挂载独立的文件系统,确保容器的文件系统与宿主机隔离。
启动容器进程:在容器的命名空间和控制组内启动进程,容器内部的进程可以像在本地运行一样执行LXC 工具:包括 lxc-create、lxc-start、lxc-stop、lxc-destroy 等,用于容器的创建、启动、停止和销毁。
LXC 配置文件:定义容器的配置信息,如网络、存储、资源限制等。
LXC 配置文件:以 config 文件形式存储,定义容器的各项配置,如内核参数、网络配置、存储配置等。
在Docker的早期版本中,它使用LXC作为底层容器引擎来实现容器隔离和虚拟化,Docker提供了各种容器管理工具(如分发、版本、移植等)让用户无需关注底层的操作,可以更简单管理和使用容器。
从技术的层面上看,LXC已经趟过了绝大部分的“坑”,完成了容器技术实用化的大半历程。但是随着时间的推移,Docker逐渐摆脱了对LXC的依赖,并自己实现了一个名为libcontainer的容器引擎,以取代LXC。Libcontainer是一个开源的Linux容器管理库,它是由Docker团队开发的,用于支持Docker容器引擎的底层。Libcontainer提供了一个接口,使得应用程序可以直接访问Linux内核中的容器相关功能,例如命名空间、控制组、文件系统等。
runC是一个基于Libcontainer的容器运行时工具,它是Docker容器引擎的一个组件。runC利用Libcontainer提供的接口,将容器镜像转换为容器实例,并在隔离环境中启动容器进程。runC还提供了对容器的生命周期管理,包括启动、停止、重启等。因此,可以说runC是Libcontainer的一个应用程序,用于管理和运行容器。同时,runC也是一个独立的开源项目,可以用于在各种容器环境中运行容器。
Docker Engine、runC和Libcontainer之间存在如下关系:
1、Docker Engine利用runC作为其默认的容器运行时工具,通过runC来管理和运行容器。
2、runC本身则是基于Libcontainer库开发的,它利用Libcontainer提供的接口来创建和管理容器。
3、Libcontainer是一个独立的开源库,它提供了一套API,使得应用程序可以直接访问Linux内核中的容器相关功能,例如命名空间、控制组、文件系统等。
基于Linux平台上的多项开源技术,Docker提供了高效、敏捷和轻量级的容器方案,并支持部署到本地环境和多种主流平台。可以说,Docker首次为应用的开发、运行和部署提供了“一站式”的实用解决方案。
1.2 docker和虚拟机的区别
docker的优势:
快速部署:短时间内可以部署成百上千个应用,更快速交付到线上。
高效虚拟化:不需要额外的hypervisor支持,直接基于linux 实现应用虚拟化,相比虚拟机大幅提高性能和效率。
节省开支:提高服务器利用率,降低IT支出。
简化配置:将运行环境打包保存至容器,使用时直接启动即可。
快速迁移和扩展:可跨平台运行在物理机、虚拟机、公有云等环境,良好的兼容性可以方便将应用从A宿主机迁移到B宿主机,甚至是A平台迁移到B平台。
docker的缺点:
隔离性:各应用之间的隔离不如虚拟机彻底。
1.3 docker的oci规范
为了推进容器生态的健康发展,在linux基金会的主导下,docker、微软、红帽、谷歌和IBM等公司在2015年6月共同成立了一个open container initiative的组织,其目的就是制定开放的标准的容器规范。
OCI的规范分为包含runtime spec和image spec两部分,不同的容器公司开发的容器只要兼容这两个规范,就可以保证容器的可移植性和相互可操作性。
(1)runtime spec
runtime运行时规范定义了如何配置和执行容器、容器的生命周期管理(包括容器的创建、启动、停止、删除等操作)以及容器的资源限制、命名空间隔离等。
runtime主要定义了以下规范,并以json格式保存在/run/docker/runtime-runc/moby/容器ID/state.json文件,此文件会根据容器的状态实时更新以下内容:
- 版本信息:oci规范的版本信息。
- 容器ID:通常是一个哈希值,可以在所有state.json文件中提取出容器ID对容器进行批量操作(关闭、删除等),该文件在容器关闭后会被删除,容器启动后会自动生成。
- status:容器的运行状态,包含以下几种:
- creating:正在被创建
- created:容器进程未退出,而用户的应用进程还未执行的状态
- running:容器进程已经退出,而且用户的应用进程已经开始正常运行
- stopped:容器进程已经退出
- PID:在容器中运行的首个进程在宿主机上的进程号,即将宿主机的那个进程设置为容器的守护进程。
- 容器文件目录:存放容器rootfs及相应配置的目录,外部程序只需读取state.json就可以定位到宿主机上的容器文件目录。
- 容器创建:创建包括文件系统、namespace、cgroups、用户权限在内的各项内容。
- 容器进程的启动:容器运行时通过一个json格式的配置文件来启动容器,该文件在/run/containerd/io.containerd.runtime.v1.linux/moby/容器ID/config.json。
- 容器生命周期:容器进程可以被外部程序关停,runtime规范定义了对容器操作信号的捕获,并做相应的资源回收的处理,避免僵尸进程的出现。
目前主流的三种runtime:
lxc:linux上早期的runtime,docker早期就是采用lxc做为runtime。
runc:目前docker默认的runtime,runc遵守OCI规范。
rkt:Coreos开发的容器runtime,也符合OCI规范。
(2)image spec
镜像规范定义了容器镜像的格式和内容,以确保容器镜像在不同的容器运行时和工具之间具有互操作性和兼容性,镜像规范主要包含以下内容:
镜像层(Layers):镜像由多个只读层组成,每个层包含了一组文件和上层之间的差异(增量)。这些层按照顺序堆叠,形成一个完整的文件系统。每个层通常是一个tar格式的文件。
镜像配置(Image Configuration):镜像配置包含镜像的元数据,包括、镜像的创建时间、作者、根文件系统(rootfs)、历史。
清单(Manifest):清单是镜像的描述文件,定义了镜像包含的所有层及其顺序、tag标签以及config文件名称
索引(Index):指向不同平台的manifest文件,这个文件能保证一个镜像可以跨平台使用。
媒体类型(Media Types):OCI镜像规范定义了不同类型对象的媒体类型。
内容地址(Content Addressing):OCI镜像规范使用内容寻址的方式来唯一标识镜像的每个部分(层、配置、清单等)。每个对象通过其内容的哈希值来标识,这确保了镜像的完整性和一致性。常用的哈希算法是SHA-256。
分层文件系统(Layered Filesystem):OCI镜像采用分层文件系统(Layered Filesystem)模型,每个镜像由多个层组成,层与层之间是增量关系。镜像的最终文件系统是通过将这些层叠加在一起实现的。这种方式使得镜像的存储和传输更加高效,因为不同镜像可以共享相同的基础层。
分发(Distribution):OCI 镜像规范还定义了如何通过 HTTP API 分发和拉取镜像,确保镜像在不同的镜像仓库和运行时之间的互操作性。
特别说明:
我们常讲的
容器运行时
是一个 混合概念,包含低级别容器运行时,和高级别容器运行时,
低级容器运行时
,比如runc、lxc、gVisor、Kata
容器等,它们主要负责真正与内核进行交互,创建和管理容器运行时的内核级别资源,如cgroups、namespaces
等,但无法直接管理镜像。
高级容器运行时
,比如Docker
引擎、Podman
、containerd
等,它们基于低级容器运行时提供了更高层次的管理能力,可以管理容器生命周期并直接调用低级运行时,同时也内置镜像管理能力,能够拉取、推送镜像等。两者关系是,高级运行时对低级运行时进行抽象,向上提供完整的容器管理接口。高级运行时在内部会调用对应的低级运行时来管理内核级资源和运行容器。
1.4 docker的组成
Docker组成:https://docs.docker.com/guides/docker-overview/
Docker主机(Host):一个物理机或虚拟机,用于运行Docker服务进程和容器。
Docker服务端(Server):Docker守护进程,运行docker容器。
Docker客户端(Client):客户端使用docker命令或其他工具调用dockerAPl。Docker 仓库(Registry):保存镜像的仓库,类似于git或svn这样的版本控制系统。官方仓库:https://hub.docker.com/
Docker镜像(lmages):镜像可以理解为创建实例使用的模板。
Docker容器(Container):容器是从镜像生成对外提供服务的一个或一组服务。
1.4.1docker镜像
docker镜像是一个特殊的文件系统,除了提供容器应用运行时所需的程序、库、资源、配置等文件外,还包含了一些应用运行时的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
docker镜像构建时, 利用 Union FS (联合文件系统)的技术,Union FS支持将不同目录mount 到同一个目录中从而形成一个虚拟文件系统,同时Union FS支持对文件系统的修改作为一次提交来一层层叠加。docker 通过这些文件再加上宿主机的内核提供了一个 linux 的虚拟环境。
联合文件系统可以对每一层文件系统设置三种权限:只读(readonly)、读写(readwrite)和写出(whiteout-able),但是docker镜像中每一层文件系统都是只读的。构建镜像的时候从一个最基本的操作系统开始,每个构建的操作都相当于增加并修改了一层文件系统,一层层往上叠加,上层的修改会覆盖底层该位置的可见性。当使用镜像的时候我们只会看到一个完全的整体,不知道里面有几层也不需要知道里面有几层。**在Docker 中,底下的只读层就是 image,可写层就是 Container。**结构如下:
docker使用unionFS(统一文件系统)技术将不同的层整合成一个文件系统,为这些层提供一个统一的视角。Docker镜像实际上是由多个不同的联合文件系统组成:
- 最底层:bootfs,包含bootloader(引导加载程序)和 kernel(内核),使用宿主机的bootfs。
- 第二层:root文件系统rootfs,包含的就是典型 Linux 系统中的/dev,/proc,/bin,/etc等标准目录和文件,称为base image。
- 然后再往上叠加其他的文件系统
不同的linux发行版,bootfs基本一样,而rootfs不同,如ubuntu,centos等。由于docker镜像使用宿主机的bootfs层,这使得镜像本身只需要包含rootfs层所需的文件和工具即可。因此镜像占用的存储空间比较少,有部分精简镜像只有几MB。两个不同的docker镜像在同一个宿主机内核上实现不同的rootfs如下所示:
分层存储的特征使得docker镜像的复用、 定制变的更为容易。 甚至可以用之前构建好的镜像作为基础层, 然后进一步添加新的层, 以定制自己所需的内容, 构建新的镜像 。
目前docker的默认存储引擎为overlay2,不同的存储引擎需要相应的系统支持。
存储驱动类型有如下几种:
1、AUFS (AnotherUnionFS)是一种 Union FS,是文件级的存储驱动。联合文件系统支持将不同目录挂载到同一个目录,这种文件系统可以一层一层地叠加修改文件。无论底下有多少层都是只读的,只有最上层的文件系统是可写的。当需要修改一个文件时,AUFS 创建该文件的一个副本,使用CoW 将文件从只读层复制到可写层进行修改,结果也保存在可写层。是Docker 18.06 及更早版本的首选存储驱动程序。
2、Overlay:一种 Union FS 文件系统,Linux 内核 3.18 后支持。也是一种Union FS,和AUFS的多层不同的是Overlay只有两层:一个upper文件系统和一个lower文件系统,分别代表Docker的镜像层和容器层。当需要修改一个文件时,使用CoW将文件从只读的lower复制到可写的upper进行修改,结果也保存在upper层。
3、overlay2:Overlay 的升级版,到目前为止,所有Linux 发行版推荐使用的存储类型。
4、devicemapper:是 CentOS 和 RHEL 的推荐存储驱动程序,使用块存储而非文件级的存储。因为之前的内核版本不支持 overlay2,但是当前较新版本的 CentOS 和 RHEL 现在已经支持overlay2,因此推荐使用 overlay2。
5、ZFS(Sun-2005)/btrfs(Oracle-2007):一种具有快照、复制等特性的文件系统,适用于高容量存储。目前没有广泛使用。
6、vfs:用于测试环境,适用于无法使用 copy-on-write 文件系统的情况。此存储驱动程序的性能很差,通常不建议用于生产。
1.4.2 docker容器
容器:镜像运行时的实体,Docker利用容器来运行应用。
镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等 。
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、 自己的网络配置、 自己的进程空间, 甚至自己的用户 ID 空间。 容器内的进程是运行在一个隔离的环境里, 使用起来就好像是在一个独立于宿主的系统下操作一样。 这种特性使得容器封装的应用比直接在宿主运行更加安全。 也因为这种隔离的特性, 很多人初学 Docker 时常常会混淆容器和虚拟机。
镜像使用的是分层存储, 容器也是如此。 每一个容器运行时, 是以镜像为基础层,在其上创建一个当前容器的存储层, 我们可以称这个为了容器运行时读写而准备的存储层为容器存储层。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据 ,容器存储层要保持无状态化。所有的文件写入操作,都应该使用数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此, 使用数据卷后,容器可以随意删除、重新 run,数据却不会丢失。
1.4.3 docker仓库
仓库:集中存放镜像文件的地方
镜像构建完成后,可以很容易的在当前宿主上运行,但是, 如果需要在其他服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。
一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。
所以说,镜像仓库是 Docker 用来集中存放镜像文件的地方,类似于我们之前常用的代码仓库。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本 。
我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以latest 作为默认标签。
以 Ubuntu 镜像 为例, ubuntu 是仓库的名字, 其内包含有不同的版本标签, 如, 14.04 ,16.04 。 我们可以通过 ubuntu:14.04 , 或者 ubuntu:16.04 来具体指定所需哪个版本的镜像。 如果忽略了标签, 比如 ubuntu , 那将视为 ubuntu:latest 。仓库名经常以两段式路径形式出现, 比如 jwilder/nginx-proxy , 前者往往意味着 Docker Registry 多用户环境下的用户名, 后者则往往是对应的软件名。 但这并非绝对, 取决于所使用的具体 Docker Registry 的软件或服务。
仓库分为公开仓库( Public)和私有仓库( Private) 两种形式,即Docker Registry 公开服务和私有 Docker Registry 。
- Docker Registry 公开服务
Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。
一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。
最常使用的 Registry 公开服务是官方的 Docker Hub ,这也是默认的 Registry,并拥有大量的高质量的官方镜像,网址为:hub.docker.com 。
在国内访问 Docker Hub 可能会比较慢,国内的一些云服务商提供了针对 Docker Hub 的镜像服务( Registry Mirror ), 这些镜像服务被称为加速器。 常见的有阿里云加速器、 DaoCloud 加速器等。 使用加速器会直接从国内的地址下载 Docker Hub 的镜像, 比直接从 Docker Hub 下载速度会提高很多。
- 私有docker registry
除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry 。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。
开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 Docker 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。在官方的商业化版本 Docker Trusted Registry 中, 提供了这些高级功能。
除了官方的 Docker Registry 外, 还有第三方软件实现了 Docker Registry API, 甚至提供了用户界面以及一些高级功能。 比如, VMWare Harbor 和 Sonatype Nexus。