云原生的核心技术之一就是容器,很多人会以为Docker等于容器,其实Docker并不等于容器。其实容器可以理解为:cgroups(资源控制)+namespaces(访问隔离)+rootfs(文件系统)+engine(容器生命周期管理)。
容器与虚拟机的区别
系统虚拟化是将一台物理计算机虚拟成一台或多台虚拟计算机系统,每个计算机系统都有自己的虚拟硬件,其上的操作系统认为自己运行在一台独立的主机上,计算机软件在一个虚拟平台上,而不是真实的硬件平台上运行。
容器和虚拟机都是虚拟化技术。容器是在Linux上本机运行,并与其他容器共享主机的内核,无须模拟操作系统指令,它是运行在宿主机上的一个独立的进程。在操作系统(Operating System,OS)的基础上进行虚拟化以及进程资源隔离,占用的CPU/内存资源不比其他任何可执行文件多,非常轻量。
虚拟机运行的是一个完整的访客操作系统,每个虚拟机中都有一个独立的操作系统内核,通过软件模拟宿主机的操作系统指令,虚拟出多个OS,然后在OS的基础上构建相对独立的程序运行环境,因此隔离效果要比容器好一些。
Docker的构成
Docker是C/S架构的程序,Docker客户端向Docker守护进程(Dockerdaemon)发起请求,守护进程负责构建、运行和分发Docker容器,处理完成后返回结果。
Docker客户端和守护进程既可以在同一个系统上运行,也可以将Docker客户端连接到远程Docker守护进程,CLI使用REST API通过脚本或直接通过CLI命令来控制Docker守护进程或与之交互。守护进程创建并管理Docker对象,如镜像、容器、网络和数据卷。
Docker三大组件
要在Docker宿主机上拉起并运行Docker容器,与3个组件密切相关,分别是Docker镜像、Docker镜像仓库、Docker容器。
- Docker镜像(image):镜像是容器的基石,容器基于镜像启动,镜像就像是容器的源代码,保存了用于容器启动的各种条件(应用代码,二方库、环境变量和配置文件等)。
Docker运行(run)一个容器前,在本地需要存在对应的镜像。如果本地不存在,则从默认的镜像仓库下载对应的镜像(默认的镜像仓库是Docker Hub公共服务器中的仓库,也可以改为国内或者公司自己搭建的镜像仓库)。如果没有在仓库名称后指定具体的镜像标签(tag),则Docker会默认拉取标签为latest的镜像。
Dockerfile是一个文本格式的配置文件,利用给定的指令描述基于某一个父镜像(from image×××)创建新镜像的过程。编辑好Dockerfile文件后,可以通过docker build命令创建本地镜像。在使用docker build命令通过Dockerfile创建镜像时,会产生一个build上下文(context)。
- Docker镜像仓库(registry):Docker镜像仓库用于保存用户创建的镜像,仓库分为公有和私有两种。Docker公司自己提供了仓库Docker Hub,可以在Docker Hub上创建账户,保存并分享自己创建的镜像,当然也可以架设私有镜像仓库。
由于网络原因,从Docker Hub(该仓库的服务器在国外)上下载镜像的速度太慢,或者Docker镜像拉取不下来,需要配置镜像加速器(要求Docker版本1.10.0以上)。我们一般会选择国内的某一家云服务商提供的镜像仓库服务。
- Docker容器(container):容器是Docker的执行单元(运行时),通过镜像启动,容器中可以运行客户端的多个进程。如果说镜像是Docker生命周期的构建和打包阶段,那么容器则是启动和执行阶段。
容器是镜像的一个运行实例,不同的是它带有额外的可写层。是独立运行的一个或一组应用,以及它们所运行的必需环境。一个容器实例就是宿主机上的一个独立进程。其拥有独立的文件系统、网络和进程树。
新建容器的方式有两个:一个是使用dockercontainer run命令,另一个是使用docker container create命令。这两个命令的不同之处在于,create命令新建的容器处于停止状态,还需要使用dockercontainer start命令来启动它。
Docker内存控制内存异常(Out Of Memory Exception,OOME)在Linux系统上,如果内核探测到当前宿主机已经没有可用内存,那么会抛出一个OOME,并且会开启killing去终止一些进程。
容器最多能使用的CPU时间有两种限制方式:一是有多个CPU密集型的容器竞争CPU时,设置各个容器能使用CPU时间的相对比例;二是以绝对的方式设置容器在每个调度周期内最多能使用的CPU时间。
Docker 提供的内存限制功能包括:容器能使用的内存和交换分区大小、容器的核心内存大小、容器虚拟内存的交换行为、容器内存的软性限制、是否终止占用过多内存的容器、容器被终止的优先级。
Docker还提供了来满足服务访问的基本需求:一个是允许映射容器内应用的服务端口到本地宿主机;另一个是通过互联机制实现多个容器间通过容器名来快速访问。
Docker处理流程
假如我们要启动一个新的Docker应用“app1”,整个工作的处理流程如下:
Docker的优势
Docker支持将应用打包进一个可移植的容器中,重新定义了应用开发、测试、部署上线的过程,核心理念是“一次构建,到处运行”,其典型应用场景是在开发和运维上提供持续集成和持续部署的服务。
- 标准化和版本控制:Docker是软件工程领域的“标准化”交付组件。还可以像Git仓库一样,可以让你提交变更到Docker镜像中,并通过不同的版本来管理它们。
- 一次构建,多次交付:Docker 具有可移植性。基于Docker容器镜像能够很容易地移植到其他云厂商的平台上,应用不用做任何改动。
- 应用隔离:Docker能够确保每个容器都拥有自己的资源,并且和其他容器是隔离的。你可以用不同的容器来运行使用不同堆栈的应用程序。
Docker分层设计
为了实现“一次构建,到处运行”的目标,我们需要把这些依赖全部打包到一起,以屏蔽环境的差异性。解决部署包过大使分发下载慢的问题,Docker引入了分层的概念。把一个应用分为任意多个层,比如操作系统是第一层,依赖的库和第三方软件是第二层,应用的软件包和配置文件是第三层。如果两个应用有相同的底层,就可以共享这些层。
为了避免冲突问题,Docker参考了Java的子类继承机制,设计了有优先级的层次,上层和下层有相同的文件和配置时,上层覆盖下层,以上层的数据为准。
在Docker的官方仓库里,只需有完整的文件系统和程序包,没有动态生成新文件的需求。当把它下载到宿主机上运行以对外提供服务时,有可能修改文件(比如应用启动参数、日志输出),需要有空白层用于写时拷贝。Docker把这两种不同的状态做了区分,分别叫作镜像(image)和容器(container)。
镜像是指分层的、可被LXC/Libcontainer 理解的文件存储格式。仓库中的应用都是以镜像的形式存在的,把镜像从Docker镜像仓库中下载到本地宿主机,以这个镜像为模板启动应用,就叫作容器。镜像是只读的,容器是可读写的。
Docker中的镜像分为基础镜像(base image)和扩展镜像。基础镜像为上层应用提供操作系统内核,通常是各种Linux发行版的Docker镜像,比如 Ubuntu、Debian、CentOS 等。Linux系统包含内核空间kernel和用户空间rootfs两部分,容器只使用各自的rootfs,但共用宿主机的kernel。一台宿主机上的所有容器都共用宿主机的kernel,在容器中无法对kernel升级。
若多个镜像从相同的基础镜像构建而来,那么Docker宿主机只需在磁盘上保存一份基础镜像,同时内存中也只需加载一份基础镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享。创建镜像时,分层可以让Docker只保存我们添加和修改的部分内容。
当一个容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。容器层可以读写,容器所有文件变更都发生在这一层,而镜像层只允许读取。
Docker数据管理
Docker中的数据主要分为两类:非持久化数据和持久化数据。
非持久化数据是不需要保存的运行过程临时数据,每个Docker容器都有自己的非持久化存储。非持久化存储自动创建,从属于容器,生命周期与容器一致,这意味着删除容器也会删除全部的非持久化数据。
Docker提供了多种存储驱动(storage-driver)来实现不同方式的数据存储,下面是常用的几种存储驱动。
- AUFS:AUFS代表AnotherUnionFS,是一种联合文件系统(UnionFilesystem,Union FS),是文件级的存储驱动。AUFS能透明覆盖一个或多个现有文件系统的层状文件系统,把多层合并成文件系统的单层表示。简单来说,就是支持将不同目录挂载到同一个虚拟文件系统下的文件系统。
- OverlayFS:Overlay是Linux内核3.18后支持的,也是一种Union FS。和AUFS的多层不同的是,Overlay FS只有两层——一个upper文件系统和一个lower文件系统,分别代表Docker的镜像层和容器层。
- Device mapper:是Linux内核2.6.9后支持的,提供了一种从逻辑设备到物理设备的映射框架机制。在该机制下,用户可以很方便地根据自己的需要制定实现存储资源的管理策略。AUFS和OverlayFS都是文件级存储,而Device mapper是块级存储。
- Btrfs:Btrfs称为下一代写时拷贝文件系统,并入Linux内核,也是文件级存储,但可以像Device mapper一样直接操作底层设备。Btrfs把文件系统的一部分配置为一个完整的子文件系统,称为subvolume。
- ZFS:ZFS文件系统是一个革命性的、全新的文件系统,它从根本上改变了文件系统的管理方式。ZFS创建在虚拟的、称为“zpool”的存储池上。每个存储池由若干虚拟设备(virtual devices、vdevs)组成。这些虚拟设备既可能是原始磁盘,也可能是一个RAID1镜像设备,或是非标准独立冗余磁盘阵列(Redundant Arrays of Independent Disks,RAID)等级的多磁盘组。zpool上的文件系统可以使用这些虚拟设备的总存储容量。
默认容器的数据保存在容器的可读写层,当容器被删除时,其上的数据将会丢失。为了实现数据的持久性,需要选择一种数据持久化技术来保存数据,当前有以下几种方式。无论选择哪种挂载类型,从容器内部看都没有区别,它们都是目录或者文件。数据都寄存在宿主机上,只不过具体位置有所区别。
- 数据卷(volume):也叫Docker容器管理数据卷(Docker managedvolume),在Docker启动时用-v或--volume参数跟宿主机目录做绑定。如果是Docker 17.06或更高的版本,推荐使用--mount(同绑定挂载)。
- 绑定挂载(bind mount):将宿主机中的文件、目录挂载到容器上,在Docker启动时用mount参数与宿主机目录做绑定。此方式与Linux系统的挂载方式很相似。
- tmpfs挂载:tmpfs挂载类型文件与普通文件的区别是只存在于宿主机内存中,不会持久化。
- 数据卷容器:如果用户需要在多个容器之间共享一些持续更新的数据,最简单的方式是使用数据卷容器。数据卷容器也是一个容器,但是它是专门用来提供数据卷供其他容器挂载的。
Docker能够集成外部存储系统,使集群的多个节点间共享外部存储数据变得可行。例如网络文件系统(Network File System,NFS)或Amazon S3可以共享应用到多个Docker宿主机,因此无论容器或服务副本运行在哪个节点上,都可以共享该存储。
Docker网络
为了支持网络协议栈的多个实例,Linux在网络协议栈中引入了网络命名空间。这些独立的协议栈被隔离到不同的命名空间中。Docker正是利用了网络的命名空间特性,实现不同容器之间的网络隔离。
Docker的本地网络实现其实利用了Linux上的网络命名空间和虚拟网络设备。Linux的网络虚拟化是LXC项目中的一个子项目,LXC包括文件系统虚拟化、进程空间虚拟化、用户虚拟化、网络虚拟化等,Docker就是使用LXC的网络虚拟化来模拟多个网络环境。
Linux网络虚拟化的类型如下:
- 桥接:创建一个虚拟桥设备(网桥),网桥可以理解为一个软件交换机,负责挂载其上的接口之间进行包转发。
- 隔离:仅将需要互相通信的虚拟机的后半段网卡添加到同一个虚拟的桥设备上,即可完成虚拟机之间的通信,且与外网仍旧是物理机隔离。
- 路由:将虚拟机关联至虚拟桥设备上,再给桥设备配置一个与虚拟机同段的IP地址(内网地址)作为虚拟机的网关(物理网卡是连接外网的,所以应该与内网IP地址不是一个段),最后打开物理主机的核心转达功能,即可让虚拟机ping外部主机,但是外部主机无法发送相应包,因为外部主机没有到达虚拟机的路由。
- NAT:在路由模型的基础上,为其配置源地址转换(Source NAT,SNAT)规则,即可完成真正的虚拟机与外网通信,且自己使用的是内网地址。
Docker中的网络接口默认是虚拟接口,虚拟接口的最大优势是转发效率高。这是因为Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将直接复制到接收接口的接收缓存中,无须通过外部物理网络设备进行交换。
Linux虚拟化网络都是基于网络命名空间netns实现的,netns可以创建一个完全隔离的新网络环境,这个环境包括一个独立的网卡空间、路由表、ARP表、IP地址表、iptables等。总之,与网络有关的组件都是独立的。
Docker网络架构由3个主要部分构成:CNMM、Libnetwork 和驱动。Docker守护进程通过调用Libnetwork对外提供的API完成网络的创建和管理等功能;Libnetwork中则使用了CNMM来完成网络功能的提供;而CN中主要有沙盒(sandbox)、接入点(endpoint)和网络(network)3种组件。
CN模型包括3种基本组件。
- 沙盒:代表一个容器所在独立的网络栈(准确地说,是其网络命名空间),包括以太网接口、端口、路由表以及DNS配置。
- 接入点:代表网络上可以挂载容器的虚拟接口,会分配IP地址。就像普通网络接入点一样,接入点主要负责创建连接;在CNMM模型中,接入点负责将沙盒连接到网络。
- 网络:可以连通多个接入点的一个虚拟子网,是IEEE 802.1d网桥(虚拟交换机)的软件实现。
目前CNMM支持4种网络驱动类型:null、bridge、overlay、remote。不同网络驱动的特性简单说明如下:
- null:不提供网络服务,容器启动后无网络连接。
- bridge:即Docker传统上默认用Linux网桥和iptables实现的单机网络。
- overlay:即用虚拟扩展局域网(Virtual eXtensible Local Area Network,VXLAN)隧道技术实现的跨宿主机容器网络。
- remote:扩展类型,预留给其他外部实现的方案,由第三方编写网络驱动,如Calico、Contiv、Kuryr、Weave等。
覆盖网络是理想的容器间通信方式,具备良好的网络伸缩性。Docker为覆盖网络提供了本地驱动,其背后是基于 Libnetwork 以及相应的overlay驱动来构建的,使得创建覆盖网络非常简单,只需在docker networkcreate 命令中添加 --d overlay 参数。
overlay驱动默认采用VXLAN协议,在IP地址可以互相访问的多个宿主机之间搭建隧道,让容器可以互相访问。Docker 使用 VXLAN 隧道技术创建了虚拟二层覆盖网络。在 VXLAN 的设计中,允许用户基于已经存在的三层网络架构创建虚拟的二层网络。
Docker容器的网络访问控制主要通过Linux上的iptables防火墙软件来进行管理和实现。iptables是Linux系统流行的防火墙软件,大部分发行版中自带iptables。
Docker的forward规则默认允许所有的外部IP访问容器时,可以通过在filter的Docker链上添加规则对外部的IP访问做出限制。不仅是与外部通信,Docker容器之间互相通信也受到iptables规则限制。
Docker三剑客
Docker Machine、Docker Compose和Docker Swarm是Docker原生提供的三大编排工具,用来部署管理多个宿主机的Docker容器集群,号称“Docker三剑客”。随着Docker对K8S的支持,以及K8S的普及,这里的三剑客重在了解。
Docker Machine是Docker官方提供的一个命令行工具,它可以帮助我们在远程的机器上安装Docker,或在虚拟机host上直接安装虚拟机并在虚拟机中安装Docker。用于配置和管理Docker化的主机(带有Docker引擎的主机),运维人员可以使用一台Docker Machine主机在一个或多个虚拟机上安装Docker引擎。
Docker Machine是一个框架,比较开放。对于任何提供虚拟机服务的平台,只要在这个框架下开发针对该平台的驱动,Docker Machine 就可以集成到该平台,在该平台上执行创建、删除、启动、停止Docker等行为。
Docker 的最佳实践是一个容器只运行一个进程,因此运行多个微服务就要运行多个容器。多个容器协同工作需要一个有效的工具来管理它们,定义这些容器如何相互关联,这就需要容器编排工具。
作为Docker官方的编排工具,Docker Compose的重要性不言而喻,它可以让用户编写一个简单的模板文件。模板文件是Docker Compose的核心,涉及的指令关键字比较多,但是大部分的指令与docker run相关参数的含义是类似的,默认的模板名是docker-compose.yml(YAML文件格式)。利用模板文件,用户可以快速地创建和管理基于Docker容器的应用集群,并定义多容器之间的关系。一个docker-compose up 命令就可以运行完整的应用。
Docker Compose 是在单个服务器或主机上创建多个容器的工具,而 DockerSwarm 可以在多个服务器或主机上创建容器集群服务,将一群Docker宿主机抽象成一个单一的虚拟主机。
DockerSwarm的优势之一是原生支持Docker API,给用户的使用带来极大的便利。Swarm使用标准的Docker API作为其前端的访问入口,因此各种形式的基于标准API的Docker客户端工具(Docker Compose、Docker SDK、各种管理软件等)均可以直接与Swarm通信,甚至Docker本身都可以很容易地与Swarm集成,这大大方便了用户将原本基于单节点的系统移植到Swarm上。
Swarm的具体工作流程:Docker客户端发送请求给Swarm,Swarm守护进程是一个调度器(scheduler)加路由器(router);Swarm处理请求并根据调度策略发送至相应的Docker节点;Docker节点执行相应的操作并返回响应。
节点是Swarm集群的最小资源单位,每个节点实际上都是一台Docker主机(物理机或虚拟机)。Swarm集群中的节点分为两种。
- 管理节点(manager nodes):负责响应外部对集群的操作请求,并维护集群中的资源,监控集群状态,分发任务给工作节点。一般推荐每个集群设置5~7个管理节点。
- 工作节点(worker nodes):负责执行管理节点安排的具体任务,为了提高资源利用率,默认情况下,管理节点自身也是工作节点。每个工作节点上运行代理(agent)来汇报任务完成情况。
Swarm集群是典型的主从(master-slave)架构,通过发现服务来选举中心管理节点,各个节点上运行代理接受中心管理节点的统一管理,集群会自动通过Raft协议分布式选举出中心管理节点,无须额外的发现服务支持,避免了单点的瓶颈问题,同时也内置了DNS的负载均衡和对外部负载均衡机制的集成支持。
Swarm的配置和状态信息保存在一套位于所有管理节点上的分布式etcd数据库中。该数据库运行于内存中,并保持数据的最新状态,并且它几乎不需要任何配置,只作为Swarm的一部分被安装,无须管理。
任务是Swarm集群中最小的调度单位,即一个指定的应用容器。当用户通过创建或更新服务声明一个期望状态的服务时,调度器通过调度任务来实现期望的状态。例如,指定一个服务始终保持运行3个HTTP实例,调度器就创建3个任务,每个任务运行一个容器。容器是任务的实例化。如果一个 HTTP 容器之后出现故障停止,此任务被标志为失败,调度器就会创建一个新的任务来生成一个新容器。任务是一个单向机制,单向地执行一个系统状态,如assigned、prepared、running等。如果一个任务失败了,调度器会删除这个任务和它的容器,然后创建一个新的任务来替换它。
Docker常用命令
Docker基础命令
docker info 检查当前容器的安装情况(包括镜像数、容器数、多少个物理机节点等) docker version 查看当前安装的Docker版本Docker生命周期管理命令
docker run -d apache -p 8080:80 <docker镜像ID/镜像名称> 用某一个镜像在后台运行一个容器,run命令加上-d参数可以在后台运行,-name是指定容器名字。-p将宿主机的8080端口映射到容器里的80端口。 docker create -name mynginx nginx:latest 创建一个新的容器,但不启动它。 docker start/stop/restart <docker容器ID> 启动/停止/重启某个容器。 docker kill <docker容器ID> 终止一个运行中的容器,kill不管容器是否同意,直接执行kill -9 强行终止。 docker rm -vf <docker容器ID> 删除一个或多个(空格分隔)容器。 docker exec -it <docker容器ID> bash 在运行的容器中执行命令,进入某一个容器。Docker容器操作命令
docker ps -a |grep xxx 显示某一个组件XXXX的容器列表。 docker inspect <docker容器ID> |grep -i host 查看容器所在宿主机IP地址。 docker top mynginx 查看容器中运行的进程信息,支持ps命令参数。 docker stats <docker容器ID> 实时显示容器自由(cpu、内存)使用统计,在容器里用free、cat/proc/meminfo等指令看到的是物理机内存,并非容器的。 docker events -sine="14673202400" 从服务器获取实时事件。 docker logs <docker容器ID> 查看容器内的标准日志输出。 docker port mynginx 列出指定的容器的端口映射。 docker cp ./aa.txt 4c9328e:/tmp/ 用于容器与宿主机之间的数据复制。 docker diff 11dfd1f54c1b 从创建容器以来,列出容器文件系统中已更改的文件和目录。 docker update -memmory=16g -memory-swap=20g{cid} 修改运行中的容器配置,即时生效,无须重启。Docker镜像管理命令
docker images 列出本地宿主机上的镜像。 docker history runoob/ubuntu:v3 查看指定镜像的分层结构以及创建历史。 docker image inspect a1235938 获取镜像的元数据信息(如镜像分层信息)。 docker rmi -f <image镜像ID> 删除本地一个或多个镜像(空格分开)。 docker tag ubuntu:15:10 runoob/ubuntu:v3 标记本地镜像,将ubuntu:15:10标记为runoob/ubuntu:v3。 docker build -t repos_local/centos-jdk7-tomcat7 使用Dockefile文件构建Docker镜像,-t是设置tag名称。 docker export -o my.tar a123457 将一个容器导出为文件,用于以后用import命令将容器导入为一个新的镜像。 docker import my.tar runoob/ubuntu:v3 从归档文件中创建镜像。 docker save -o my.tar runoob/ubuntu:v3 将指定镜像保存成tar归档文件。 docker load -input my.tar 加载使用docker save命令导出的镜像 docker login -u <用户名> -p <密码> 登录一个Docker镜像仓库,如果未指定镜像仓库地址,则默认为官方仓库Docker Hub。 docker logout 退出一个Docker镜像仓库,如果未指定镜像仓库地址,则默认为官方仓库Docker Hub。 docker pull registry.XXX.com/apache-php5:latest 从镜像仓库中拉取或者更新指定镜像到本地 docker push nginx:v1 将本地镜像的上传到镜像仓库,执行该命令前要先登录镜像仓库 docker search nginx 从镜像仓库查找镜像