赞
踩
本文首发在个人博客上:万字总结!Docker简介及底层关键技术剖析
Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app)。几乎没有性能开销,可以很容易地在机器和数据中心中运行。最重要的是,他们不依赖于任何语言、框架或包装系统。
Docker 基于 Linux 内核的 cgroup,namespace,以及 UnionFS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 版本开始,则进一步演进为使用 runC 和 containerd。
runc 是一个 Linux 命令行工具,用于根据 OCI容器运行时规范 创建和运行容器。
containerd 是一个守护程序,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。
LXC的全名为Linux Container,它是一种轻量级的Linux内核容器虚拟化技术,允许在同一主机上运行多个相互隔离的Linux Container,每个容器都有自己的完整的文件系统、网络、进程和资源隔离环境。
LXC与传统的虚拟机技术不同,LXC不需要运行完整的操作系统镜像。LXC使用Linux内核提供的cgroups和命名空间(Namespaces)功能来实现容器隔离。它有效地将由单个操作系统管理的资源划分到孤立的组中,以更好地在孤立的组之间平衡有冲突的资源使用需求。
docker 出现之初,便是采用了 lxc 技术作为 docker 底层,对容器虚拟化的控制。后来随着 docker 的发展,它自己封装了 libcontainer (golang 的库)来实现 Cgroup 和 Namespace 控制,从而消除了对 lxc 的依赖。
现在Docker相较于LXC已经有了十足的发展,其生态也更加完善。主要有以下几个方面的区别。
下面的图片比较了 Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
其区别可以从以下几点展开。
对比项 | Docker 容器 | 虚拟机 |
---|---|---|
隔离性 | 较弱的隔离,属于进程之间的隔离,各个容器共享宿主机的内核 | 强隔离,属于系统级别的隔离,会模拟出一整个操作系统和硬件,各个虚拟机之间完全隔离 |
启动速度 | 秒级 | 分钟级 |
镜像大小 | 一般为 MB | 一般为 GB |
托管主体 | Docker Engine 在操作系统和 Docker 容器之间进行协调。 | 虚拟机监控器在计算机的物理硬件和虚拟机之间进行协调。 |
运行性能 | 接近原生(损耗小于 2%) | 损耗小于 15% |
镜像可移植性 | 平台无关 | 平台相关 |
占用资源量 | Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,单机上可支持上千个容器 | 虚拟机是一个完整的操作系统,需要占用大量的磁盘、内存和 CPU 资源,一般一个主机上只能支持几十个虚拟机 |
安全性 | 1. 容器内的用户从普通用户权限提升为 root 权限,就直接具备了宿主机的 root 权限。 2. 容器中没有硬件隔离,使得容器容易受到攻击。 | 1. 虚拟机租户 root 权限和主机的 root 虚拟机权限是分离的。 2. 硬件隔离技术:防止虚拟机突破和宿主机交互。 |
高可用性 | docker对业务的⾼可⽤⽀持是通过快速重新部署实现的。 | 虚拟化具备负载均衡,⾼可⽤,容错,迁移和数据保护等经过⽣产实践检验的成熟保障机制,VMware可承诺虚拟机99.999%⾼可⽤,保证业务连续性。 |
资源共享 | 按需共享,依据cgroups进行控制。 | 按固定数量共享,在虚拟机映像的配置要求中设置。 |
总的来说,Docker或者说容器技术和虚拟机并非简单的取舍关系,如果你希望一个完全隔离的和资源有保障的环境,那么虚拟机是你的不二选择;如果你只希望进程之间相互隔离,同时拥有轻量化的属性,那么linux容器技术或者Docker,才是更好的选择。
当我们使用docker容器时,首先需要首先先下载一个对应的镜像,镜像相比于虚拟机的进行更加轻量,原因在于它实际上主要只是一个rootfs(当然还包括一些额外的配置文件)。对于一个精简的 OS ,rootfs 可以很小,只需要包括最基本的命令、工具和程序库即可,因为底层直接共用 Host 主机的 kernel。
镜像中的rootfs采取的是分层存储的方式,即每个镜像都是由多个只读层叠加而成。这种分层存储的特性使得镜像的复用、定制、共享变的更为容易。同时,每一层都可以被容器读取,容器的文件系统就是这些层的叠加。镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。
从上图中我们可以看到,当由 ubuntu:14.04 镜像启动容器时,ubuntu:14.04 镜像的镜像层内容将作为容器的 rootfs;而 ubuntu:14.04 镜像的 json 文件,会由 docker daemon 解析,并提取出其中的容器执行入口 CMD 信息,以及容器进程的环境变量 ENV 信息,最终初始化容器进程。当然,容器进程的执行入口来源于镜像提供的 rootfs。
rootfs代表了一个系统中的根文件系统,在传统的 Linux 操作系统内核启动时,首先挂载一个只读的 rootfs,当系统检测其完整性之后,再将其切换为读写模式。
在docker容器中,rootfs是docker容器在启动时内部进程可见的文件系统,即docker容器的根目录。rootfs通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类 Unix 操作系统中的目录系统,如 /dev、/proc、/bin、/etc、/lib、/usr、/tmp 及运行docker容器所需的配置文件、工具等。
docker实现rootfs依靠的实联合挂载技术。
最新的docker采用的联合挂载技术为overlay2。对于overlay2,它的主要作用就是将一堆目录下的内容联合挂载到一个目录下,它包含以下几个目录:
在合并的目录中进行操作时,各个目录之间有上下顺序,上层目录的同名文件会遮盖住下层的文件。如果对LowerDir中的文件进行了修改,那么实际的文件是不会改变的,而是会在UpperDir中对其进行拷贝,然后在UpperDir中进行修改,这也称为写时复制技术。如果对LowerDir中的文件进行删除,那么实际的文件也不会被删除,而是在UpperDir中创建一个同名文件,并将其标记为删除状态。
一个容器完整的层应由三个部分组成:
容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间、文件系统、网络配置等。
如上所述,容器在运行时会添加一层可读写的容器层,其生命周期与docker一致。按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者 绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
为了便于镜像的上传和下载,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
仓库名经常以 两段式路径 形式出现,比如 jwilder/nginx-proxy,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。
最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry,并拥有大量的高质量的官方镜像。除此以外,还有 Red Hat 的 Quay.io;Google 的 Google Container Registry,Kubernetes 的镜像使用的就是这个服务;代码托管平台 GitHub 推出的 ghcr.io。
Docker 是一个客户端-服务器(C/S)架构程序。Docker 客户端只需要向 Docker 服务器或者守护进程发出请求,服务器或者守护进程将完成所有工作并返回结果。Docker 提供了一个命令行工具以及一整套 RESTful API。你可以在同一台宿主机上运行 Docker 守护进程和客户端,也可以从本地的 Docker 客户端连接到运行在另一台宿主机上的远程 Docker 守护进程。
Docker 服务端是 Docker 所有后台服务的统称。其中 dockerd 是一个非常重要的后台管理进程,它负责响应和处理来自 Docker 客户端的请求,然后将客户端的请求转化为 Docker 的具体操作。例如镜像、容器、网络和挂载卷等具体对象的操作和管理。
Docker 从诞生到现在,服务端经历了多次架构重构。起初,服务端的组件是全部集成在 docker 二进制里。但是从 1.11 版本开始, dockerd 已经成了独立的二进制,此时的容器也不是直接由 dockerd 来启动了,而是集成了 containerd、runC 等多个组件。
Linux 命名空间、控制组和 UnionFS 三大技术支撑了目前 Docker 的实现,也是 Docker 能够出现的最重要原因,此外Docker网络、Docker存储也尤为重要。UnionFS前面已经介绍过了,不再赘述。
命名空间是 是 Linux 提供的一种内核级别环境隔离的方法,本质就是对全局系统资源的一种封装隔离。每个容器都有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统中运行一样。命名空间保证了容器之间彼此隔离,互不影响。
目前linux内核支持以下6种命名空间:
控制组是linux内核的一个特性,主要用来对共享资源进行隔离、限制,审计等。避免多个容器同时运行时的系统资源竞争,它最早是由 Google 的程序员 2006 年起提出,Linux 内核自 2.6.24 开始支持。
Cgroup 可以限制一个进程组的资源使用,包括 CPU、内存、磁盘IO、网络带宽等。在 Docker 中,Cgroup 主要用来限制容器的相关资源使用。
Cgroups 分 v1 和 v2 两个版本:
Cgroups 主要包括下面几部分:
*** cgroups 本身:** cgroup 是对进程分组管理的一种机制,一个 cgroup 包含一组进程。
个人理解:
比如上图表示两个 hierarchiy,每一个 hierarchiy 中是一颗树形结构,树的每一个节点是一个 cgroup (比如 cpu_cgrp, memory_cgrp)。
在每一个 hierarchiy 中,每一个节点(cgroup)可以设置对资源不同的限制权重(即自定义配置)。比如上图中 cgrp1 组中的进程可以使用 60%的 cpu 时间片,而 cgrp2 组中的进程可以使用 20%的 cpu 时间片。
Docker的网络模式主要有以下几种:
模式 | 描述 |
---|---|
bridge | 为每一个容器分配、设置 IP 等,并将容器连接到一个 docker0 虚拟网桥,默认为该模式。 |
host | 容器将不会虚拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的 IP 和端口。 |
none | 容器有独立的 Network namespace,但并没有对其进行任何网络设置,如分配 veth pair 和网桥连接,IP 等。 |
container | 新创建的容器不会创建自己的网卡和配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。 |
bridge 模式是 Docker 默认的网络模式。在Docker服务启动后,会在主机上创建一个名为 docker0 的虚拟网桥,当我们创建一个bridge网络模式的容器时,首先容器会新建一个网络命名空间,然后它会连接到这个虚拟网桥上。连接的方法是创建一对虚拟网卡veth pair设备,其中一个端口连接到容器内部,命名为eth0,另一个端口连接到docker0上,命名为vethxxx模式的名字。这种虚拟网卡利用内存从来进行数据包的收发,当一端收到数据包后会自动转发给另一端,并对外表现为一个独立的网络设备。同时也会从docker0子网中分配一个 IP 给容器使用,并设置 docker0 的 IP 地址为容器的默认网关。
当容器需要对外通信时,数据包会先被容器内的 veth 网卡接收,然后通过 veth pair 传递给 docker0,再由 docker0 转发给宿主机的物理网卡,然后会对数据包进行Net包装,表现的就像是主机自己发出的数据包一样,最终到达目的地。
如果需要同一个主机下的容器之间通信,那么需要设置–link参数来允许容器之间的通信,注意默认情况下,容器之间是无法通信的。而随着Docker 1.9版本的发布,Docker官方推荐使用用户自定义的网络来代替–link参数。
这种模式下的一个大缺点就在于容器都没有一个公有IP,即和宿主机不处于同一个网段。导致的结果是宿主机以外的世界不能直接和容器进行通信。
host模式相当于Vmware中的NAT模式,与宿主机在同一个网络中,但没有独立IP地址。启动容器使用host模式,容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。
使用host模式的容器可以直接使用宿主机的IP地址与外界通信,容器内部的服务端口也可以使用宿主机的端口,不需要进行NAT,host最大的优势就是网络性能比较好,docker host上已经使用的端口就不能再用了,网络的隔离性不好。
使用none模式,Docker 容器拥有自己的 Network Namespace,但是,并不为Docker 容器进行任何网络配置。也就是说,这个 Docker 容器没有网卡、IP、路由等信息。需要我们自己为 Docker 容器添加网卡、配置 IP 等。 None模式示意图:
这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备进行高效通信。 Container模式示意图:
根据前面的UnionFS的介绍我们可知,在默认情况下在Docker容器内创建和修改的所有文件都在可写层,只能在容器内部使用,难以被其他服务共享。且容器删除后,数据也跟着删除。同时写入容器的可写层需要Docker存储驱动管理文件系统。存储驱动使用Linux内核提供的联合文件系统,其性能不如直接写入主机文件系统的Docker卷。所以docker也提供了一些存储持久化的功能,主要可以分为:volume、bind mount和tmpfs。
Volumes 是Docker推荐的挂载方式,它在主机中创建一个数据卷,并对应到容器中的一个目录。与把数据存储在容器的可写层相比,使用Volume可以避免增加容器的容量大小,还可以使存储的数据与容器的生命周期独立。Volumes存储在主机文件系统中由Docker管理的位置,在Linux主机上该位置默认就是/var/lib/docker/volumes目录,其他非docker进程不能修改该路径下的文件,完全由docker引擎来管理。Volumes支持使用Volumes驱动,可以让用户将数据存储在远程主机或云提供商处等。可以以命名方式或匿名方式挂载卷:
卷由Docker创建并管理,卷适合以下应用场景。
Bind mount 模式下可以存储在宿主机器任何一个地方,但是会依赖宿主机器的目录结构,不能通过docker CLI 去直接管理,并且非docker进程和docker进程都可以修改该路径下的文件。
它的特点:
绑定挂载适合以下应用场景。
tmpfs挂载仅限于运行Linux操作系统的Docker主机使用,它只存储在主机的内存中,不会被写到主机的文件系统中,因此不能持久保存容器的应用数据。
在不需要将数据持久保存到主机或容器中时,tmpfs挂载最合适。
如果容器产生了非持久化数据,那么可以考虑使用tmpfs挂载避免将数据永久存储到任何位置,并且通过避免写入容器的可写层来提高容器的性能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。