互联网企业容器技术实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3 Docker镜像及镜像仓库

➤➤1.3.1 什么是Docker镜像

Docker 镜像是由一层层的文件系统组成的特殊文件系统,为容器提供了运行时所需要的程序、配置、环境等。镜像属于只读文件系统,不能包含任何的动态数据,在构建完成后就不会被改变。

Docker在运行容器前需要在本地存在对应的容器镜像,如果本地不存在该镜像,则Docker会先从镜像仓库中将对应的镜像拉取到本地,然后通过该镜像启动容器。

拉取完Docker镜像就可以查看本地是否已经存在对应的镜像,要查找本地拥有了哪些镜像可以通过docker image ls命令,列出本地存在的镜像,如下所示。

如果想要删除某个镜像,可以通过 docke rmi[镜像:标签]命令实现。如下所示为删除 test镜像:

这样就删除了test:latest镜像。其中的latest标签比较特殊,对于Docker镜像来说,如果不显式地指定标签,则默认会选择latest标签,表示最新版本的镜像。所以删除test:latest镜像也可以直接使用docker rmi test,同样的拉取test:latest镜像也可以直接使用docker pull test,省略latest标签。

➤➤1.3.2 构建Docker镜像

构建Docker镜像的方式有两种,一种是通过docker commit方式,另一种是通过Dockerfile文件构建的。相比之下,推荐使用Dockerfile的方式构建,因为Dockerfile方式简化了镜像的构建过程,并且可以更好地进行版本控制。首先简单讲解通过docker commit的方式构建镜像,然后会详细讲解如何通过Dockerfile的方式构建镜像。

1. 使用docker commit构建镜像

下面通过一个简单的示例讲解如何通过docker commit的方式构建镜像。

通过如上命令创建了一个基于ubuntu:16.04镜像的容器,并在容器中安装了apache2。

可以看到生成的容器ID为2a87d7bbbbdf,接着可以通过docker commit命令创建一个新的镜像。

成功地创建了一个新的镜像licheng17/apache2,接着可以通过docker push命令把创建的镜像提交到镜像仓库中。在提交前需要先登录自己的docker hub 账户,如果没有docker hub账户,则需要先注册。当然也可以创建自己的私有镜像仓库,登录私有仓库,将镜像上传。

镜像上传成功后可以登录自己的 docker hub,查看是否增加了刚上传的 Docker 镜像licheng17/apache2,如图1-3所示。

图1-4 Docker网络的整体架构

从图1-4中可以看出Docker daemon 通过Libnetwork提供的API接口实现对网络的创建和管理功能。而Libnetwork通过CNM来实现这些网络功能,CNM模型有三个组件:Sandbox (沙盒)、端点(Endpoint)、网络(Network),具体如下。

➢ Sandbox(沙盒):每个沙盒包含一个容器网络栈(Network stack)的配置,配置包括容器的网口、路由表和DNS设置等。

➢ Endpoint(端点):通过Endpoint,Sandbox可以被加入一个Network里。

➢ Network(网络):一组能相互直接通信的Endpoints。

上面的定义还是比较抽象的,理解起来比较困难,所以可以参考CNM模型在Linux上的实现技术来理解。比如,沙盒的实现可以是一个Linux Network Namespace,Endpoint可以是一对VETH,Network则可以用Linux Bridge或Vxlan实现。

Libnetwork提供了Bridge、Host、Overlay、Remote和Null五种网络模式。

1)Bridge驱动:此驱动为Docker的默认驱动,使用该驱动时,Libnetwork将创建出来的Docker容器连接到Docker网桥上,能够满足容器的基本使用性需求。

2)Host驱动:在该驱动模式下,Docker容器直接使用宿主机的网络,与宿主机享有完全相同的网络环境。

3)Overlay驱动:此驱动采用VXLAN中的SDN controller模式。在使用过程中,需要一个额外的配置存储服务,例如Consul、etcd和Zookeeper等,并在启动Docker daemon的时候通过参数指定所使用的配置存储服务地址。

4)Remote驱动:该驱动实际上并没有实现Docker自己的网络服务,而是通过调用外部的网络驱动插件的形式实现网络服务。用户可以根据自己的需求选择或开发合适的网络插件。

5)Null驱动:在这种网络模式下,Docker仅仅为容器创建自己的Network Namespace,提供网络隔离的功能,但并不会为容器创建任何网络配置,需要用户根据自己的需求为容器添加网络,并配置网络环境。

➤➤1.4.2 Docker网络原理

这部分主要介绍Docker的网络原理,通过Linux系统模拟实现一个Docker默认网络的方法。

首先对Docker网络的实现原理进行整体简略的介绍,当Docker服务安装并启动后,在主机上输入 ifconfig 命令,可以发现主机上多了一个 docker0 的虚拟网桥,这个网桥为 Docker的网络通信提供支持。如图1-5所示,在默认网络情况下,当启动一个Docker容器时,Docker会为容器分配一个IP地址,并通过一对veth pair将容器的eth0绑定到主机的docker0网桥中。

图1-5 Docker网络的实现原理

1. 模拟实现Docker网络

下面通过Linux系统模拟Docker网络模型,实现上面介绍的网络结构。

第一步:创建Network Namespace

此时Namespace test创建成功了,在/var/run/netns目录中可以看到一个test文件。当启动一个容器时,Docker会为容器创建一个Network Namespace,可以看到在/var/run/docker/netns目录下生成了一个对应的文件。

第二步:添加网口到Namespace

先创建veth:

在当前Namespace中可以看到veth0和veth1:

将veth1加到namespace“test”:

通过ip link list命令发现,当前Namespapce只能看到veth0,而veth1已经找不到了。

通过如下命令可以查看test namespace的网口,发现刚刚不见了的veth1。说明veth1已经加入test namespace中。

配置Network Namespace的网口。可以通过ip netns exec进行配置。

通过ip netns exec可以配置查看veth1网口的IP地址正是172.16.0.2。

这样一个隔离的容器网络就完成了,又该如何实现容器网络与外部的通信呢?这就需要用到Bridge了。

第三步:创建网桥

在默认的Network Namespace下创建test0网桥:

test0网桥创建成功,其中docker0是Docker服务器自己的网桥。下一步是给test0分配IP地址并生效,充当Gateway:

将veth0“插到”test0这个Bridge上:

查看test网络生成了一条直连路由表,现在test网络可以ping通test0了,但由于没有默认路由,Docker还不能与其他网络相通,通过ping docker0测试发现确实如此。

为test网络添加一条默认路由表,测试ping docker0发现成功。

现在的test网络可以与主机相通了,但还不能访问外部网络,因为icmp包回来时找不到目的地,也就找不到172.17.0.2了,可以通过iptables来解决。

可以看出添加完 iptables规则后已经可以正常访问外网了。这样一个类Docker 的网络就完成了,其实Docker的网络相对会比这复杂很多,但基本实现原理方法是一样的。

2.Docker容器端口映射原理

上一节已经通过模拟实现Docker网络的方式讲解了Docker的网络原理。但还有一个重要的功能没有讲,就是容器端口映射的功能,即将容器的服务端口a绑定到宿主机的端口b上,最终达到一种效果:外部程序通过宿主机的端口b访问,就像直接访问Docker容器网络内部容器提供的服务一样。

如下通过Docker启动了一个Nginx容器服务,并将容器内部的80端口映射到本地主机的9091端口。然后可以通过本地的9091端口访问容器的80端口,即Nginx服务。

容器是怎样实现端口映射功能的呢?在默认情况下,在1.12.1版本以前Docker引擎采用docker-proxy进程来实现,在之后的版本中,虽然增加了iptables的方式实现,但在默认情况下通过配置-userland-proxy=true为每个expose端口的容器启动一个proxy实例来实现端口流量转发。docker-proxy实际上是实现了在默认命名空间和容器命名空间之间的流量转发功能而已。

但是因为每增加一个端口映射,宿主机就会多出一个docker-proxy进程,一旦需要过多的端口映射,就需要增加过多的docker-proxy进程,这样将会消耗大量的资源。因此,在1.7及更高版本中,Docker提供了一种完全由iptables DNAT实现的端口映射,但docker-proxy的方式依旧是默认方式,通过配置-userland-proxy=false来选择iptables DNAT模式。

从上述的iptables规则可以看出,iptables将本地的9091端口通过DNAT映射到172.12.0.2的80端口,其中172.12.0.2就是Nginx的容器IP。