Docker总架构图
客户端-服务器架构以及请求的发送,解析等原理不再赘述,这不是我们学习docker的重点。我们知道,Docker提供给了我们一个在隔离环境中运行的进程,那么我其实想深入探究的是
- 容器的网络是怎么在这个隔离的环境中与其他容器,与宿主机,与外界进行数据交换的。
- 镜像是一种什么样的结构,使得一个静态的镜像就生成多个动态运行的容器。
- 容器内的进程是怎么样去创建,运行的,这个进程和普通的宿主机的进程有哪些不同。
Docker通过一个叫做 libcontainer 的库实现了底层与容器创建运行相关的命令和顶层调用的隔离。在Docker0.9之前,Docker的底层命令依赖的是LXC,而在这之后,作为Docker的第一个子项目,libcontainer替代了LXC的功能,并且完全用Go语言实现。(实际还是用Go去执行具体的Linux命令)
下面就我上面提到的三点进行重点概述
Docker网络
在一台没有安装Docker的宿主机上,网络环境可能平淡无奇。但是,当用户开始启动并安装docker,一切都发生了变化。Docker Daemon有可能会在宿主机上新建一个网桥设备,以支持我们熟知的桥接模式
Docker Daemon首先要完成的任务是创建网桥,网桥可以由用户指定,也可以使用默认的docker0 。创建网桥的步骤如下:
- 确定网桥的IP地址
- 调用系统调用,创建网桥
- 将IP地址赋予网桥
- 启动网桥设备
第一步是确定IP地址,先判断用户启动参数中是否制定了IP,没有的话从docker预留的网段中选择IP。
第二步首先判断内核版本是否支持,然后调用系统调用创建网桥
第三步是将IP地址绑定到这个网桥上,最后一步为启动网桥。值得一提的是,这几步中涉及到的底层调用都是通过netlink进行的
这里我在总结的时候其实有个疑问,为什么可以为一个操作系统设置网桥,并且给他分配IP呢?Linux网桥的作用是什么?难道不应该是一个主机一个IP吗?
其实,一开始创建的docker0网桥,它还有另一个更为人熟知的名字,没错就是网卡。也就是说,docker是新建了一块虚拟网卡。这就不奇怪了,因为我们知道,一台机器可以有几个IP地址取决于它有几块网卡。但是又有一个疑问,docker0的IP地址并不为外界所熟知,那么外界如何和我们通信呢?答案就在Linux的一个内核参数,叫net.ipv4.ip_forward。这个参数指定了Linux系统当前对路由转发功能的支持情况;其值为0时表示禁止进行IP转发;如果是1,则说明IP转发功能已经打开。只要开启了这个,那么从外部访问容器内部时只需要访问宿主机的地址和对应的容器映射的端口,访问的数据包到宿主机上后经过ip包解析后通过目的port和iptables的规则会将数据包由eth0网卡转发至docker0网桥上进行下一步路由。
那我还有一个问题,为什么要额外建立一个虚拟的网桥,然后将数据包转发给它呢?直接都在eth0上面不好吗?其实,这样做的目的是为了让更好的抽象容器的概念。我们就是为了,让容器认为它独占了整台机器,所以就必须给容器一个虚拟的ip地址,让他自认为他是用这个ip与外界通信的。看上面的Docker桥接模式示意图,每个Container上有一个小的eth0,这就代表了每个容器都有自己的虚拟网卡,都有自己的ip,都有自己的从0-65535的端口。这些容器既可以通过docker0与外界通信,也可以通过docker0与其他容器通信。不管和谁通信,他都觉得自己和对方都是一台独立的机器,有自己的ip。这就是容器网络的本质。
Docker镜像
镜像是一种文件存储形式。提到Docker镜像,很容易联想到虚拟机镜像。镜像是一种文件存储形式,文件管理员可以通过技术手段将很多文件制成一个镜像。虚拟机的镜像存储有操作系统,文件系统,设备文件等,而Docker镜像与之相似,只不过不含操作系统内容,同时Docker镜像由多个镜像组合而成。它在容器视角而看是一个只读的layer
rootfs
rootfs是一个Docker容器在启动时其内部进程可见的文件系统视角
Docker利用Linux的Union Mount技术,将两层文件系统联合挂载,形成一种COW的文件系统
Images
Docker镜像采用了类似于roofts但是更为精妙的方式。为了让容器的迁移,构建的粒度更细,更为轻量化,我们可以将一个rootfs文件系统的内容拆分为多个镜像
如此一来,在不同的容器,不同的rootfs中的images可以服用
镜像的构建:docker build
镜像是由Dockerfile中的内容构建而成的。Docker Daemon会根据Dockerfile中的内容,首先生成一个buildFile结构体,然后以解析出的内容为原材料,开始构建镜像
在逐行解析Dockerfile命令的过程中,对Dockerfile中的每一条除了FROM的指令,都会构建出一个新的image。因此,build的流程就是构建一个个image的流程。
Dockerfile的命令可以分为两类:
第一类命令修改上一层image的文件系统内容,比如,命令RUN在基于上一层image的容器中运行一条命令,这条命令很有可能修改上一层image的内容。命令ADD/COPY将Dockerfile所在宿主机目录的内容复制到上一层image
第二类命令仅仅修改景象的config信息。比如ENV会指定环境变量
这里我们重点分析一下RUN命令的执行流程
首先第一步是检查缓存,如果有缓存的话,直接返回即可,不必再重复构建。什么是缓存呢?我们在构建镜像时,buildFile的image属性存储的是我们的基础镜像,而config属性存储的是在基础镜像之上的一些配置信息
type Config struct {
Hostname string
Domainname string
User string
Memory int64 // Memory limit (in bytes)
MemorySwap int64 // Total memory usage (memory + swap); set `-1' to disable swap
CpuShares int64 // CPU shares (relative weight vs. other containers)
Cpuset string // Cpuset 0-2, 0,1
AttachStdin bool
AttachStdout bool
AttachStderr bool
PortSpecs []string // Deprecated - Can be in the format of 8080/tcp
ExposedPorts map[nat.Port]struct{}
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
OpenStdin bool // Open stdin
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
Env []string
Cmd []string
Image string // Name of the image as it was passed by the operator (eg. could be symbolic)
Volumes map[string]struct{}
WorkingDir string
Entrypoint []string
NetworkDisabled bool
OnBuild []string
}
因此,我们在构建之前,只需要遍历本地镜像,只要存在一个镜像,此镜像的父镜像ID与当前buildFiled的image值相等,同时此镜像的所有config内容也与buildFile.config相同,则完全可以认为执行RUN命令产生的结果与此镜像的效果一致,直接使用该镜像即可。
如果找不到缓存,则只能手动创建镜像。怎么创建镜像呢?其实是先根据基础镜像创建一个容器,然后将容器挂载到文件系统上,然后运行这个容器,在容器中运行Dockerfile中的某行命令,然后将容器再提交为镜像,继续运行下一条命令。
Docker 容器
容器经常被用来与虚拟机比较,比较的维度也很多:容器运行时与宿主机共享同一个操作系统,而虚拟机是独立的操作系统、容器更加节省物理资源、容器到启动更加快,甚至达到秒级、容器的IO性能明显高于虚拟机…下面来具体看看容器是如何运行的,Docker是如何将 “死” 的镜像变为 “活” 的容器的。
在Docker中,容器的运行,即docker run其实分为两个步骤,一个是容器的创建,另一个是容器的启动。容器的创建源码是Create函数,它创建了一个container对象
创建主要是将一个“死”的镜像,变为一个“死”的容器,我们重点来看容器的启动,它将一个“死”的容器变为“活”的容器。
启动函数Start包含以下11个步骤:
- setupContainerDns
为了让容器与外界建立通信,自然离不开DNS服务,用于确保域名的正常工作。如果网络模式是host模式,则和宿主机共享网络环境,不需要配置DNS。如若不然,则需要为容器内部配置DNS服务。DNS主要配置DNS服务器,如果用户在启动过程中或者配置文件中没有明确指定,则使用宿主机的配置。如果宿主机的不可用,则默认使用8.8.8.8和8.8.4.4,这两个为谷歌为我们提供的免费的DNS服务器IP地址。 - Mount
容器的运行离不开文件系统的支持,Mount的主要任务是找到容器的根目录 - initializeNetworking
初始化用户指定的网络模式(bridge、host等),进行相应的网络配置和初始化 - verifyDaemonSetting
检查系统内核是否支持cgroup内存限制,以及是否支持网络接口间ipv4数据包的转发。 - prepareVolumesForContainer
volume的存在使得Docker容器的存储可以实现持久话。该步为容器配置Volume结构体的信息
- setupLinkedContainers
容器间的link操作允许容器通过环境变量的形式发现另外一个容器,并在这两个容器之间传输信息。 该函数完成容器link环境变量的配置,建立多个容器之间的link关系。 - setupWorkingDirectory
创建或切换至配置的容器工作目录 - createDaemonEnvironment
完成容器环境变量的加载 - populateCommand
填充Command对象,之前的所有配置信息都会体现在Command对象实例中
- waitForStart
启动容器进程,通过指定的namespace和配置信息fork进程。