容器的发展史
容器是一个通用性的技术,不指代某一个特定的软件产品。
由于Docker的诞生,带动了容器技术的大火,以至于后面很多时候说的容器都指向Docker,其实除了Docker,还有许多其他的容器技术,比如RKT,LXC,Podman等。
其实,在Docker诞生之前,容器技术就已经存在了。
可以通过下面这张图来直观的感受容器的发展历史。
时间线如下:
1979 年,Unix v7 系统支持 chroot,为应用构建一个独立的虚拟文件系统视图。
1999 年,FreeBSD 4.0 支持 jail,第一个商用化的 OS 虚拟化技术。
2004 年,Solaris 10 支持 Solaris Zone,第二个商用化的 OS 虚拟化技术。
2005 年,OpenVZ 发布,非常重要的 Linux OS 虚拟化技术先行者。
2004 年 ~ 2007 年,Google 内部大规模使用 Cgroups 等的 OS 虚拟化技术。
2006 年,Google 开源内部使用的 process container 技术,后续更名为 cgroup。
2008 年,Cgroups 进入了 Linux 内核主线。
2008 年,LXC(Linux Container)项目具备了 Linux 容器的雏型。
2011 年,CloudFoundry 开发 Warden 系统,一个完整的容器管理系统雏型。
2013 年,Google 通过 Let Me Contain That For You (LMCTFY) 开源内部容器系统。
2013 年,Docker 项目正式发布,让 Linux 容器技术逐步席卷天下。
2014 年,Kubernetes 项目正式发布,容器技术开始和编排系统齐头并进。
2015 年,由 Google,Redhat、Microsoft 及一些大型云厂商共同创立了 CNCF,云原生浪潮启动。
2016 年 - 2017 年,容器生态开始模块化、规范化。CNCF 接受 Containerd、rkt项目,OCI 发布 1.0,CRI/CNI 得到广泛支持。
2017 年 - 2018 年,容器服务商业化。AWS ECS,Google EKS,Alibaba ACK/ASK/ECI,华为 CCI,Oracle Container Engine for Kubernetes;VMware,Redhat 和 Rancher 开始提供基于 Kubernetes 的商业服务产品。
2017 年 - 2019 年,容器引擎技术飞速发展,新技术不断涌现。2017 年底 Kata Containers 社区成立,2018 年 5 月 Google 开源 gVisor 代码,2018 年 11 月 AWS 开源 firecracker,阿里云发布安全沙箱 1.0。
2020 年 - 202x 年,容器引擎技术升级,Kata Containers 开始 2.0 架构,,阿里云发布安全沙箱 2.0。
整理容器技术近 20 年的发展历史,大致可以将其分为四个历史阶段,如下:
技术萌芽期
在1979年的时候Unix V7实现Chroot功能,开启了容器技术的先河,再到后来谷歌推出Cgroup技术以及Linux推出的LXC容器技术,可以看到这段时间其实是在不断丰富和发展容器的时代。
容器本质上是在解决资源隔离的问题。所以在技术萌芽期
,主要是在解决隔离哪些资源以及如何隔离的问题。
从技术发展来看,可以将资源隔离大致分为五种。
- 语言运行时隔离:像Java、Nodejs等需要运行时环境,为了保证应用干净,可以做语言运行时隔离,但是目前在实现上还有不少阻力,当前还是采用容器或VM来实现隔离。
- 进程隔离:OS以进程作为Task运行过程抽象,进程拥有独立的地址空间和执行上下文,本质上OS对进程进行了CPU和内存虚拟化。但是进程之间还共享了文件系统、网络协议栈、IPC通信空间等多种资源,进程之间会因为争抢这些资源而导致严重的干扰。这个层级的隔离适合在不同的主机上运行单个用户的不同程序,由用户通过系统管理手段来保证资源分配以及安全防护等问题。
- OS虚拟化:OS隔离就是操作系统虚拟化,是进程隔离的升级版。OS隔离是为每一组进程分配一个独立的OS环境,以进一步虚拟文件系统、IPC通信空间、网络协议栈、进程ID等资源。
OS隔离需要解决三个核心问题:独立视图、访问控制、安全防护
。Chroot、Linux namespace 机制为进程组实现独立视图,cgroup 对进程组进行访问控制,而 Capabilities、Apparmor、seccomp 等机制则实现安全防护。当然,OS 是一个非常复杂、动态变化的系统,OS 分身术虽然让进程组感觉有了独立的 OS,但是真实实现还是一个 OS 实例,所以整体防护能力还是堪忧。 - 硬件虚拟化:OS 虚拟化是实现 OS 内核的分身术,而硬件虚拟化则是实现硬件设备的分身术。硬件虚拟化技术的出现,让同一个物理服务器上能够同时运行多个操作系统,每个操作系统都认为自己在管理一台完整的服务器。不同操作系统之间是严格隔离的,Guest 操作系统对硬件的访问都是受 VMM 或 CPU 的严格监管的。硬件虚拟化既有很好的安全性,也有很好的隔离性,缺点就是引入的硬件虚拟化层导致了额外的性能开销。
- 硬件分区:这个是传统小型机体系采用的资源分隔技术,就是从硬件或固件层彻底把一台大型服务器分隔为多个硬件单元,从而获得最高等级的安全性和隔离性。但是小型机作为一个逐步没落的技术路线,其不足之处还是显而易见的:资源分隔粒度不灵活、系统成本偏高、系统可扩展性受限。
总体看,在 2013 年 docker 被发明以前,Linux 操作系统已经大体上解决了容器核心技术之一的运行环境隔离技术,或者说 Linux OS 虚拟化技术已经基本上成型了。虽然容器运行环境隔离技术已经基本就位,我们仍需等待另外一项关键技术才能迎来容器技术的腾飞时刻。
技术迸发期
2013年之前,云计算行业一直在为云原生的正确打开姿势而操心,PAAS看起来是个不错的方向。PAAS确实推动了云计算的发展,但是并没有形成行业趋势。直到Docker的出现,大家才发现并不是方向不对,而是应用分发和交付的手段不行
。
Docker 真正核心的创新是容器镜像
(docker image),一种新型的应用打包、分发和运行机制。容器镜像将应用运行环境,包括代码、依赖库、工具、资源文件和元信息等,打包成一种操作系统发行版无关的不可变更
软件包。
- 容器镜像打包了整个容器运行依赖的环境,以避免依赖运行容器的服务器的操作系统,从而实现 “build once,run anywhere”。
- 容器镜像一旦构建完成,就变成 read only,成为不可变基础设施的一份子。
- 操作系统发行版无关,核心解决的是容器进程对操作系统包含的库、工具、配置的依赖,但是容器镜像无法解决容器进程对内核特性的特殊依赖。这个在实际使用容器的过程中也经常跳进这个大坑:
Docker的诞生,解决了如何发布软件以及如何运行软件的问题。但是其仅仅是作为一个单机软件打包、发布、运行系统 。但是业界希望基于Docker构建一个云化的集群系统,来对业务容器进行编排和管理。
在13年Docker席卷全球过后,14年谷歌开源了Kubernetes(一个基于Borg的容器编排、调度工具),用于解决容器部署、运行、管理等问题,并且很快就成为了行业标准。
但是Docker为了打造自己的容器版图,不甘示弱,也发布了自己的容器编排工具Docker Swarm、Docker Machine、Docker Compose等,力图与Kubernetes正面碰撞。
由容器所带来的竞争越演越烈,为了滚固自己的地位,2015年6月,Docker带头成立OCI,旨在“指定并维护容器镜像格式和容器运行时规范”。其核心输出是OCI Runtime Spec(容器运行时规范)、OCI Image Spec(镜像格式规范)和OCI Destribution Spec(容器分发规范)。可以看到,OCI主要解决的是容器构建、分发和运行问题
。
而后,谷歌带头成立CNCF(Cloud Native Computing Foundation),旨在“构建云原生计算——一种围绕着微服务、容器和应用动态调度的、以基础设置为中心的架构,并促进其广泛使用。”(格局一下就打开了)。所以,CNCF解决的是应用管理和容器编排的问题
。
OCI和CNCF对云原生的发展发挥了非常重要的作用,共同制定了行业标准,给云原生注入了无限活力。
虽然Docker推出的容器编排工具(Docker Swarm、Docker Machine、Docker Compose)以失败落幕,但是整个容器技术却是百花齐放的时代。
商用探索期
经过5年的技术发展,容器技术基本成熟,云原生体系也逐具雏型。
从2017年开始,各大云厂商开始试水容器和云原生服务。从目前的商业形态来看,容器相关的云服务大致可分为三类:
- 通用容器编排服务:在容器编排系统三国杀结果出来以前,基于多方下注策略构建的容器编排服务系统。其中 AWS 是自研的编排系统,Azure 的 ACS 同时支持 Docker Swarm、DC/OS 和 Kubernetes,阿里云 ACS 则是支持 Docker swarm 和 Kubernetes。Google 和华为则是坚定支持 Kubernetes 而未推出支持其它容器编排系统的容器服务。随着 Kubernetes 一统容器编排江湖,这条路线的容器服务日渐式微,Azure 更是在今年初直接终止了 ACS 服务。
- Kubernetes容器编排服务:Google 是理所当然最早试水 Kubernetes 容器编排服务的大厂,也较早开展了 K8s 容器编排服务。随着 2017 年各大厂在 CNCF 这张谈判桌上达成了 Kubernetes 兼容性认证流程,Kubernetes 编排服务市场迎来一轮大爆发,到 2018 年各大云厂商的 K8s 容器编排服务就完整就位了。
- Serverless容器实例服务:从 2017 年开始,行业开始试水 Serverless 容器实例服务,把用户从维护容器基础设施的繁重任务中解放出来从而聚焦业务本身。Google Cloud Run 核心目标是支持 Knative,所以其使用形态上附加了不少约束条件。
从上图可以看出,从 2014 年开始探索公共云容器服务,特别是经过 2017 - 2018 年这两年的抢跑期,容器服务的基本商业形态已经比较明晰了。发展态势可以概括为:
- 行业对容器化的接受程度已经很高,容器化普及率也是逐年提升。
- 容器编排系统已经一战定江山,K8s 成为事实上的容器编排之王。
- Serverless 容器实例服务受到市场的欢迎,客户群体日益扩大。
- 长期看托管容器编排服务和 Serverless 容器实例服务将长期共存,协同满足客户对服务成本和弹性能力的需求。
商用模式探索期间,核心目标是快速试错引导和确认客户需求,构建适用的产品形态。这个期间的产品技术架构的构建思路是利用现有成熟技术快速搭建商用形态,在试错过程中不断前行。
商用拓展期
到 2019 年,容器服务的商业形态以及市场趋势已经很明显了,行业整体进入了商业拓展阶段,对外宣传吸引更多的客户群体,对内苦练内功提升产品技术竞争力,行业正在经历从“有”到“优”的技术升级。到现在为止,我们大体上可以把容器引擎技术划分为两代:
- Container on VM。也就是按照分层设计思路,通过 IaaS + PaaS 的架构构建容器服务,这个是商用探索阶段的典型架构。基于各大云厂商成熟的 IaaS 基础设施生产虚拟机,在虚拟机里面部署容器服务组件。这种架构采用的是 lift and shift 策略,把容器服务的运维责任从用户转移到云厂商。采用和用户相同的软件组件,只是转移运维责任,有利于引导客户逐步上云、接受云原生思维。但是这个时期云厂商提供的服务是单纯的运维托管,相对用户自建容器服务并没有太明显的技术优势,甚至受多租户隔离的限制部分使用体验还不如用户自建容器服务。
- Container with hardware virtualization。如果沿用 Container on VM 的分层设计架构,云厂商很难构建独有的技术优势。对于 Serverless 容器实例服务,服务交付平面已经从 IaaS 的硬件接口上移到 OS Syscall,所以不要遵循 VM + 容器的分层设计思路。我们需要从需求本源出发,容器服务需要高性能、强隔离、够安全和低成本的容器引擎。
总结来看,容器服务生态大概经历了四个阶段,分别解决或试图解决不同的问题:
- 技术萌芽期:解决了容器运行环境的隔离问题
- 技术迸发期:解决了软件分发及容器编排问题
- 商用探索期:确认了容器的商用服务形态
- 商用拓展期:扩大适用场景和部署规模,通过技术创新提升产品竞争力
Docker
在上面已经大量的提到Docker,而且在很长的一段时间里,容器就是Docker,Docker就是容器
。
其实,Docker只是基于容器技术实现的一个软件。
Docker是基于Linux内核的CGroup、Namespace以及UnionFS等技术,对进程进行封装隔离,属于操作系统层面的封装隔离,由于隔离的进程独立于宿主机和其他进程,因此也称其为容器。
其实Docker最初是基于LXC(Linux Container)实现的,从0.7版本之后去除LXC,转而使用自己研发的Libcontainer,从1.11版本开始,则进一步演进为使用RunC和Containerd。
Docker的诞生,极大的简化了容器的创建和维护,使得Docker技术比虚拟机技术更轻便、快捷。
为什么要用Docker
在虚拟机时代,传统的部署方式如下:
- 开发将编译好的制品上传到共享仓库
- 运维从共享仓库下载制品,上传到运行服务
- 运维启停服务,比如启停Tomcat
- 如果需要新服务,还需要创建虚拟机,安装基础环境,比如JDK,Tomcat等
整个过程是不是流转很复杂?有了Docker过后,运维不需要再创建虚拟机了,只需要在Linux操作系统上安装好Docker软件,然后就可以起应用了,具体过程如下:
- 开发通过Dockerfile将应用打包成镜像,上传到镜像仓库
- 运维从镜像仓库下载镜像,通过Docker run启动服务
这只是单机Docker,如果使用Kubernetes进行容器编排,在管理应用上就更方便。
总的来说,Docker的优势如下:
- 更高效的利用系统资源
- 更快的启动时间
- 一致的运行环境
- 更轻松的迁移、维护和扩展
纸上谈来终觉浅,下面来通过对比Docker和虚拟机,来直观感受一下Docker的优势。
首先从实现逻辑上,如下图:
从上图可以直观感受到Docker更轻便。VM需要基于Host OS进行虚拟化,再在虚拟机上安装Guest OS,再部署应用。而Docker则可以直接在Host OS安装Docker Engine,接着就可以部署应用了,极大的节约了资源。
其次,再从性能上对比,如下:
特性 | 容器 | 虚拟机 |
启动 | 秒级 | 分钟级 |
硬盘使用 | 一般为MB | 一般为GB |
性能 | 接近原生 | 弱于原生 |
系统支持量 | 单机支持上千容器 | 一般几十个 |
从上面的对比来看,Docker是不是吊打虚拟机?
Docker的架构
Docker在1.11之前主要是通过docker daemon来处理client的请求,容器的相关操作都是通过docker daemon来完成。从1.11之后,并不是简简单单的通过docker daemon来处理了,它集成了Containerd、RunC等多个组件。这些组件之间相互协作来完成客户端请求和容器管理。
现在的架构图如下:
- Dockerd:面向用户提供容器操作,是对容器相关API的上层封装。
- Containerd:对外提供gRPC形式的API,API的定义中不再包含于集群、编排等相关功能,但是它也不是简单的将Docker API照搬过来,而是进行了更细粒度的抽象,并且还实现了监控管理、多租户的接口,方便外部应用利用这套API来实现高效和定制容器管理功能。
- RunC:是 Docker 按照开放容器格式标准(OCF, Open Container Format)制定的一种具体实现。runC 是从 Docker 的 libcontainer 中迁移而来的,实现了容器启停、资源隔离等功能。Docker 默认提供了 docker-runc 实现,事实上,通过 containerd 的封装,可以在 Docker Daemon 启动的时候指定 runc 的实现。
- Containerd-shim:在默认情况下,Docker守护进程在停止容器的时候会发送SIGTERM信号,而容器进程有可能错误的忽略该信号,为了能够正确的处理系统信号等相关特性,通过Containerd-shim来保证能够正确处理各种信号,所以每个容器都会对应一个Containerd-shim实例。它对外的接口是ttRPC。
除此之外,还有主机和镜像仓库:
- 主机:用于承载Docker运行的底座
- 镜像仓库:存放镜像制品的地方
Docker的技术底座
Docker除了容器镜像
是革命性的创新,其他的算是新瓶装旧酒。
Docker是基于Linux内核的CGroups、Namespace以及UnionFS等技术来实现的,这些也是它的技术底座。
- Namespace:命名空间,容器隔离的基础,保证A容器看不到B容器
- CGroups:容器资源统计和隔离,主要用到CGroups的子系统,比如CPU,Memory等
- UnionFS:联合文件系统,分层镜像实现的基础
Namespace
在Linux系统中,Namespace是内核级别以一种抽象的形式来封装系统资源,通过将不同的系统资源存放在不同的Namespace以实现资源的隔离。
命名空间(namespaces)是 Linux 为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。在日常使用 Linux 或者 macOS 时,我们并没有运行多个完全分离的服务器的需要,但是如果我们在服务器上启动了多个服务,这些服务其实会相互影响的,每一个服务都能看到其他服务的进程,也可以访问宿主机器上的任意文件,这是很多时候我们都不愿意看到的,我们更希望运行在同一台机器上的不同服务能做到完全隔离,就像运行在多台不同的机器上一样。
Linux 的命名空间机制提供了以下七种不同的命名空间,包括 :
- CLONE_NEWCGROUP
- CLONE_NEWIPC
- CLONE_NEWNET
- CLONE_NEWNS
- CLONE_NEWPID
- CLONE_NEWUSER
- CLONE_NEWUTS
通过这七个选项, 我们能在创建新的进程时, 设置新进程应该在哪些资源上与宿主机器进行隔离。具体如下:
Namespace | Flag | Page | Isolates |
Cgroup | CLONE_NEWCGROUP | cgroup_namespaces | Cgroup root directory |
IPC | CLONE_NEWIPC | ipc_namespaces | System V IPC,POSIX message queues 隔离进程间通信 |
Network | CLONE_NEWNET | network_namespaces | Network devices,stacks, ports, etc. 隔离网络资源 |
Mount | CLONE_NEWNS | mount_namespaces | Mount points 隔离文件系统挂载点 |
PID | CLONE_NEWPID | pid_namespaces | Process IDs 隔离进程的ID |
Time | CLONE_NEWTIME | time_namespaces | Boot and monotonic clocks |
User | CLONE_NEWUSER | user_namespaces | User and group IDs 隔离用户和用户组的ID |
UTS | CLONE_NEWUTS | uts_namespaces | Hostname and NIS domain name 隔离主机名和域名信息 |
当我们在一台主机上使用Docker run启动两个容器后,在这两个容器中都可以看到一个PID=1的进程,众所周知,PID=1是进程的祖宗,是唯一的,那容器是如何实现在一台主机上启动的两个容器内的进程PID都为1呢?
这其实就用到了Linux Namespace技术,Docker在创建容器的时候会调用Linux内核中创建进程的clone()方法。
int clone(int (*fn) (void *),void *child stack, int flags, void *arg, . . . /* pid_ t *ptid, void *newtls, pid_ t *ctid */ ) ;
通过调用这个方法,就会创建一个独立的进程空间,它的Pid=1。
当然,Docker使用到的Namespace不仅仅只有Pid,还有其他的Namespace以提供不同层面的隔离。
- The pid namespace: 管理 PID 命名空间 (PID: Process ID).
- The net namespace: 管理网络命名空间(NET: Networking).
- The ipc namespace: 管理进程间通信命名空间(IPC: InterProcess Communication).
- The mnt namespace: 管理文件系统挂载点命名空间 (MNT: Mount).
- The uts namespace: Unix 时间系统隔离. (UTS: Unix Timesharing System).
- The user namespace: 管理系统用户.
PID Namespace
进程是Linux系统中非常重要的概念,它表示一个正在执行的程序。
在Linux操作系统上,可以通过ps -ef
命令查看当前系统中正常执行的进程,如下在CentOS 7系统上查看到的进程信息。
# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 Mar05 ? 04:50:01 /usr/lib/systemd/systemd --switched-root --system --deserialize 22 root 2 0 0 Mar05 ? 00:00:02 [kthreadd] root 4 2 0 Mar05 ? 00:00:00 [kworker/0:0H] root 6 2 0 Mar05 ? 00:13:29 [ksoftirqd/0] root 7 2 0 Mar05 ? 00:02:39 [migration/0] root 8 2 0 Mar05 ? 00:00:00 [rcu_bh] root 9 2 0 Mar05 ? 01:08:08 [rcu_sched] root 10 2 0 Mar05 ? 00:00:00 [lru-add-drain] root 11 2 0 Mar05 ? 00:00:19 [watchdog/0] root 12 2 0 Mar05 ? 00:00:15 [watchdog/1] root 13 2 0 Mar05 ? 00:02:38 [migration/1] root 14 2 0 Mar05 ? 00:11:02 [ksoftirqd/1] root 16 2 0 Mar05 ? 00:00:00 [kworker/1:0H] root 18 2 0 Mar05 ? 00:00:00 [kdevtmpfs] root 19 2 0 Mar05 ? 00:00:00 [netns] ......
其中Pid=1和Pid=2的两个进程比较特殊,前者负责系统启动和配置管理的 ,后者负责管理和调度其他进程的。
PS:在其他操作系统上,1号进程可能叫init,在CentOS 7之前也叫init,但是在CentOS 7之后就改成systemd,主要是为了解决init进程启动时间长并且脚本复杂等问题。
如果我们在当前操作系统上创建一个容器,进容器查看进程,如下:
# docker run -d --name busybox busybox sleep 3000 b776c8105860a5d46046365ca03b71ec13dac9e1dbe4b8d392e468d49a42020f # docker exec busybox ps -ef PID USER TIME COMMAND 1 root 0:00 sleep 3000 7 root 0:00 ps -ef
我们可以看到容器里的进程非常干净,并没有外部主机的那些进程。
这就是Pid Namespace
的作用。
- 不同的用户进程就是通过
Pid Namespace
进行隔离的,且不同的Namespace中可以有相同的Pid。 - 有了
Pid Namespace
,每个Namespace中的Pid能够相互隔离。
所以在Docker容器中,我们可以看到存在Pid=1
的进程。但是在主机上,我们查找这个容器,可以发现其进程只是一个普通进程,如下:
# ps -ef | grep b776c8105860 root 2862574 3699 0 16:20 ? 00:00:00 containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/b776c8105860a5d46046365ca03b71ec13dac9e1dbe4b8d392e468d49a42020f -address /var/run/docker/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc root 2866529 2861401 0 16:21 pts/0 00:00:00 grep --color=auto b776c8105860
如果我们查看所有进程数,可以看到如下关系(删除了很多不必要的信息):
systemd─┬ ├─dockerd─┬─containerd─┬ │ │ ├─containerd-shim─┬─sleep │ │ │ └─10*[{containerd-shim}]
整体的管理逻辑如下:
NET Namespace
Docker通过Pid Namespace
完成了进程隔离,是每个容器都有单独的进程。
但是,光有进程隔离还不够。每个容器里运行的都是真实的业务应用,业务应用之间需要实现互相访问,就需要每个容器拥有独立的网络,比如独立的网卡、IP地址,路由等。
为了实现上面的需求,Docker就用到了Net Namespace
,用它来完成网络隔离。
但是光隔离还不够,最终的目的是要完成通信,即容器和宿主机通信、容器和其他主机通信以及容器和互联网通信等。
为了完成最终的目标,Docker为我们提供了四种不同的网络模式以满足不同的客户需求:
- Host:主机模式,使用主机网络
- Container:容器模式,和某个容器共享网络
- None:拥有自己的网络空间,但是不配置任何网络信息
- Bridge:桥接模式,通过veth桥接到主机上
这里只介绍Bridge模式,其他模式现在很少使用,也很少有企业单独使用Docker。
当安装完Docker并启动后,就会在主机上看到一个docker0
网桥,它有独立的不同于主机的IP地址,如下:
# ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 52:54:00:e3:77:24 brd ff:ff:ff:ff:ff:ff inet 10.0.4.9/22 brd 10.0.7.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::5054:ff:fee3:7724/64 scope link valid_lft forever preferred_lft forever 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:6f:d4:33:49 brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:6fff:fed4:3349/64 scope link valid_lft forever preferred_lft forever
每当在该主机上启动一个容器,会创建一对虚拟网卡veth设备,其中一端连接到docker0,另一端连接到容器中的网卡(例如eth0)上,并且网关地址是docker0的地址。
虚拟网桥的工作方式和物理交换机的工作方式类似,我们可以把主机当作是一个物理交换机,这样所有容器都通过交换机连接在了一个二层网络。
现在每个容器都有自己的IP地址,并且也和主机进行桥接了。我们可以试着在主机上ping容器的IP地址,看是否正常。
# ping 172.17.0.3 PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data. 64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.056 ms 64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.056 ms 64 bytes from 172.17.0.3: icmp_seq=3 ttl=64 time=0.041 ms
可以看到正常通信,那具体是怎么通信的呢?
首先docker0和容器网卡eth0是veth对,两个属于直连,是可以直接通信的,那主要问题就在docker0怎么和宿主机通信的。
其实docker0是通过iptables和主机通信的,所有符合条件的的请求都会通过iptables转发到docker0并由网桥分发给对应的容器。
# iptables -t nat -L Chain PREROUTING (policy ACCEPT) target prot opt source destination DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL Chain OUTPUT (policy ACCEPT) target prot opt source destination DOCKER all -- anywhere !loopback/8 ADDRTYPE match dst-type LOCAL Chain DOCKER (1 references) target prot opt source destination RETURN all -- anywhere anywhere
Docker 通过 Linux 的Net命名空间实现了网络的隔离,又通过 iptables 进行数据包转发,让 Docker 容器能够优雅地为宿主机器或者其他容器提供服务。