Docker一览
“一次编译,到处运行”,这是Java最早提出的口号。但要实现这一目标并不容易,软件工程师以及计算机先驱们在实现这一目标上,做出了大量的努力,一个首要的问题就是怎么摆脱环境的束缚?因此虚拟化就出现了,Java有它自己的虚拟机实现,还出现各种各样的虚拟化技术包括指令集虚拟化、硬件抽象虚拟化、操作系统层虚拟化、语言层虚拟化等等。
有了虚拟化技术,紧接着就需要对计算机中的资源进行隔离,因此linux中有隔离文件的chroot,隔离访问的namespace,隔离资源的cgroups。
当上述所有条件诞生之后,容器就有了必备的所有条件。2008年Linux就推出了自己的容器虚拟化功能—-Linux容器,即LXC(LinuX Containers)。但后续却由于种种原因败给了2013开源的“后来者”Docker。(至于容器的编排,Docker又输给了Kubernetes,这是后话)
容器的主要目标是让软件分发部署过程从传统的发布安装包、靠人工部署转变为直接发布已经部署好的、包含整套运行环境的虚拟化镜像,同时容器也是云原生的基石。
启动Docker容器
对于Docker的安装,可以参照官方文档。安装好后,Docker的守护进程应该就启动了,当然你也可以手动的启动这个守护进程dockerd
。Docker是一个典型的C/S服务模型架构,这个dockerd守护进程就是Server,你可以用多种客户端来和守护进程通信,以及管理容器,创建镜像Images。对于镜像Images,可以从Docker的官方仓库中搜寻到想要的软件应用。

上面是一张官方文档的Docker架构图,我们能大致了解Docker运行模式。接下来我们利用官方的例子启动一个容器:
docker run -dp 80:80 docker/getting-started cb54a15522b4df82d65c85ee95d87a869ee8ae9c65939b2a32a20afbf1c927cd
启动之后,会返回这个容器的ID。
通过docker ps
我们能看到刚刚启动的docker容器。

这个官方例子容器内启动了一个启动了nginx服务,监听在80端口上,我们可以进入容器内部看下:
docker exec -it cb54a15522b4df82d65c85ee95d87a869ee8ae9c65939b2a32a20afbf1c927cd /bin/sh // 这个命令会attach容器的终端到我们目前的命令行工具中,然后用top就看到了进程 top

当然,这些进程最终在宿主机上也能体现,你可以手动操作一下,然后在宿主机上top看下,是不是也有nginx进程!
Docker存储篇
当我们启动一个容器应用时,大概率是需要存储数据的。不管是临时文件数据,日志数据,或者是数据库的持久化数据,都需要在硬盘上存储下来。如果你直接将数据写入容器内的目录上,这些数据将存储在容器的可写层上,当容器停止或者重启之后,这些数据将不复存在。这当然是容器隔离性的表现之一,正如上述一些我们需要持续保存的数据,那对于一个隔离的容器里该怎么存储这些数据呢?Docker为我们提供了3种不同的存储模式。这三种模式Docker文档用一张图说明的很清楚:

Volumes
Volume是一个逻辑概念,它是宿主机文件存储的一部分,容器启动时可以将Volume绑定到自己内部的一个目录下进行读写访问。Volume是由docker来管理的,你可以通过docker volume create
创建,docker volume rm
删除volume, 或者通过docker volume ls
来查看当前Docker下的volumes。
创建好的Volume在linux环境下默认在/var/lib/docker/volumes/
路径。因此对于其它应用来说不应该访问和操作这个目录。
通过上面的描述我们可以看到,这非常类似于宿主机的mount操作,但是它又将绑定关系操作与宿主机隔离开了。
// 通过-v就可以将一个名为myvol2的volume,挂在绑定到容器内的/app目录下 docker run -d \ --name devtest \ -v myvol2:/app \ nginx:latest
不管是重启容器,或者停止容器,volume存储的数据都不会丢失,下次启动又可再次直接使用,并且可以用于多个容器的数据共享。
当然Docker容器的volume驱动不仅限于将数据存储在当前宿主机,而且可以在远程主机或者云端。
Bind Mounts
相对于Volume,Bind Mounts就简单粗暴许多,它是直接将宿主机上的一个全路径目录或文件直接挂载绑定到容器内部。显而易见,这种性能肯定会比volume高,但是指定宿主机上的一个目录对隔离性可能会是一个破坏,因为对于Docker来说,它不便管理。
// 这样就将宿主机/ab/cd/target的目录,绑定到容器内的/app docker run -d \ -it \ --name devtest \ -v /ab/cd/target:/app \ nginx:latest
tmpfs mounts
这个选项仅仅在 Linux下生效。它和上述的volume以及bind mount的不同之处在于,这种模式的数据也是临时,但和直接写数据在容器内的不同是它不是在容器的可写层面,而是将数据写入宿主机的内存中。因此当容器停止后,数据将会被删除。
这种模式对于一些临时敏感数据的存储非常有用,因为它不是在容器的可写层面,并且会在容器退出时删除。
Docker网络篇
对于一个完整的系统来说,组成这个系统的多个容器服务之间必定是要相互通信沟通。但是Container与Container之间是相互隔离独立的,想要将他们连接在一起就需要依赖网络。Docker提供了多种网络形式来供我们选择。这也是Docker强大之处,你可以选择适合自己业务的网络连接方式,甚至也可以在异构的平台节点里连接自己的服务。下面介绍Docker的网络驱动类型:
桥接网络(Bridge Network)
Docker 默认就是这种网络形式。它是一种虚拟化的软件实现的Bridge网络,连接了相同Bridge网络的容器(Container)可以互相通信,没有连接的则相互隔离。这种是Docker桥接驱动自动在宿主机上安装网络规则,来达到这个目的。
桥接网络仅适用于多个容器间运行在相同的Docker守护进程主机内。Docker本身会创建一个默认的桥接网络bridge,容器Container启动的时候如果没有指定自定义的Bridge Network,则就是用默认的桥接网路。
你可以通过docker network ls
看到这个默认的桥接网络,它在你安装完Docker时就会创建。

你可以docker network create
或者docker network rm
来创建删除自己的桥接网络。
Docker的桥接网络是利用Linux的虚拟化交换机来实现的,它允许Container有自己的IP地址,通过桥接网络构建的容器网络大概如下图所示:

上图的veth就是虚拟的网络接口,能够用来和虚拟Bridge进行配合工作,我们可以通过在宿主机上执行ifconfig
看到它的存在。

默认桥接网络和自定义桥接网路区别
- 自定义桥接网络可以自动做DNS解析。因此容器间可以使用名字来进行互相访问。
- 自定义桥接网络有很好的隔离性。
- 自定义桥接网络可以动态的attach和detach到容器。如果是默认桥接网络的话就需要先停止容器,然后再重启它。
- 自定义桥接网络可以有各自的配置,比如MTU, IPtables。默认的则是公用一套配置。
- 自定义桥接网络下的多个容器间的端口亦是互相暴露的。
Overlay网络
Overlay网络驱动会在多个Docker守护进程主机间创建一个分布式的网络,以便它们能正常通信,甚至跨多个节点,数据中心。这个网络是在宿主机特定的网络之上(这也是Overlay的含义)。这是一种VXLAN技术,将容器间需要传输的数据利用UDP协议封装出去,因此互联网号码分配局(Internet Assigned Numbers Authority,IANA)还特意为它分配了UDP-4789的端口号,它将二层的以太帧数据包装在四层的UDP数据报中。
Overlay可以让服务与服务,容器与容器之间自由通信,但缺点显而易见,多出来的封包解包操作将带来不小的性能影响,因此效率不高。
宿主机网络(host networking)
这种网络形式只在Linux环境下生效,直接利用宿主机的网络,此时容器就不能有它自己的Ip地址。因此,--publish
参数就会失效,但是这样的性能表现比较好,因为它不需要网络转换,缺点就是如果容器已经占用了80端口,那宿主机上就不能再使用80端口。
Macvlan
一张网卡设备对应一个Mac地址,但是利用Macvlan就可以使得一张物理网卡具备多个Mac地址,甚至多个IP地址。它虚拟出多个子网卡接口,每个子接口具备一个网卡地址,因此也就可以有自己的IP地址,此时每个虚拟出来的子网卡接口与真正的物理网卡作用一样,它能直接通过DHCP获取到属于自己的IP地址,亦可以直接同外部网络通信,此时的物理网卡有点像扮演交换机的角色。
每个虚拟网卡设备都有一个 MAC 地址,新增虚拟网卡设的操作本质上相当于在系统内核中注册了一个收发特定数据包的回调函数,每个回调函数都能对一个 MAC 地址的数据包进行响应,当物理设备收到数据包时,会先根据 MAC 地址进行一次判断,确定交给哪个虚拟网卡设备来处理。

图片来源:ipwithease
这种模式性能较好,但是却很容易将网络中的IP耗尽,导致网络混乱;并且网络接口一般对Mac地址数量是有限制的,一旦超过则会影响性能,另外一些无线网络中是不支持Macvlan子接口。综合上述一些原因,Docker官方说:
If your application can work using a bridge (on a single Docker host) or overlay (to communicate across multiple Docker hosts), these solutions may be better in the long term.
意思就是为了长远考虑,建议还是使用Bridge或者Overlay.
IPvlan
了解了Macvlan,对于IPvlan就比较好理解了。最大的区别就是,虚拟出来的子网卡接口的Mac地址是相同的。

图片来源:ipwithease
IPVlan需要在Linux内核v4.2版本以上才支持的比较好,Docker对之前的版本支持有一些BUG。IPvlan又分为L2和L3模式,
L2模式,它和macvlan是非常相似的,父接口转发子接口的数据,这是IPVlan的默认模式。
L3模式,此时就类似于路由器模式,IPvlan在各个虚拟网络和主机网络之间进行不同网络报文的路由转发工作,只要父接口相同,即使虚拟机/容器不在同一个网络,也可以互相 ping 通对方,因为 ipvlan 会在中间做报文的转发工作。如果需要搭建复杂的网络拓扑,这种模式可能比较适合。
none 无网络访问
有些时候我们需要禁用网络访问,因此我们启动容器时,可以使用--network none
。此时只有容器内的环回地址被启用。
// 官方例子: docker run --rm -dit \ --network none \ --name no-net-alpine \ alpine:latest \ ash