一、什么是容器

容器:是一种轻量级、可移植、自包含的软件打包技术,使应用程序可以在几乎任何地方以相同的方式运行。开发人员在自己笔记本上创建并测试好的容器,无需任何修改就能够在生产系统的虚拟机、物理服务器或公有云主机上运行。

组成:1、应用程序本身,2、依赖:比如应用程序需要的库或其他软件

二、为什么使用容器

容器和虚机都是为应用提供封装和隔离,但是容器使软件具备了超强的可移植能力。虚机:每一个虚机都需要安装一个操作系统,每个操作系统都有kernel,每一个kernel都依赖硬件,而docker仅依赖于库(目前kernel几乎都是X86指令架构),所以虚机内核和host内核是一样的(uname -r)

三、怎么学习和使用docker

1、容器核心架构-组件:Docker 采用的是 Client/Server 架构

a、Docker 客户端 - Client

b、Docker 服务器 - Docker daemon

c、Docker 镜像 - Image

d、Registry

e、Docker 容器 - Container

2、镜像

一个image由manifestimage index (可选)、filesystem layersconfiguration四部分组成。

先来看看构成image的四部分的关系图:

Filesystem Layer包含了文件系统的信息,即该image包含了哪些文件/目录,以及它们的属性和数据。

image config就是一个json文件,这个json文件包含了对这个image的描述,包括CPU的架构、OS、config即运行容器的时候的默认参数、rootfs:指定了image所包含的filesystem layers,type的值必须是layers,diff_ids包含了layer的列表

manifest也是一个json文件,这个文件包含了对前面filesystem layers和image config的描述,包括镜像的层级即依赖的镜像和本次镜像-----都有可能多个层级

image index也是个json文件,是为了能够让镜像支持多个多个平台和多个tag

-----------docker pull的时候会去先下载image的manifests,根据manifests文件中config的sha256码,得到image config文件,遍历manifests里面的所有layer,根据其sha256码在本地找,如果找到对应的layer,则跳过,否则从服务器取相应layer的压缩包,然后拼出完整的image

3、上面是对镜像结构的描述,那怎创建镜像呢

A、通过docker commit创建镜像:

命令:docker commit {container_id} {image_name}

首先,创建基础镜像的容器,然后安装自己的库或者执行自己的命令(本次镜像层),最后通过命令创建镜像

B、通过Dockerfile构建文件创建镜像:

命令:docker build -t {image_name} .

通过 Dockerfile 构建镜像的过程:

1)、从 base 镜像运行一个容器。

2)、执行一条指令,对容器做修改。

3)、执行类似 docker commit 的操作,生成一个新的镜像层。

4)、Docker 再基于刚刚提交的镜像运行一个新容器。

5)、重复 2-4 步,直到 Dockerfile 中的所有指令执行完毕。

docker commit与Dockerfile 对比:docker commit无法通过docker history查看镜像是怎么来的,只能看到镜像大小增加了,等到镜像使用有问题后,无法追溯问题在哪?

4、Dockerfile文件:

Dockerfile是由一系列命令和参数构成的脚本,这些命令应用于基础镜像并最终创建一个新的镜像。

1)FROM(指定基础image)

构建指令,必须指定且需要在Dockerfile其他指令的前面。后续的指令都依赖于该指令指定的image。FROM指令指定的基础image可以是官方远程仓库中的,也可以位于本地仓库。

2)MAINTAINER(用来指定镜像创建者信息)

构建指令,用于将image的制作者相关的信息写入到image中。当我们对该image执行docker inspect命令时,输出中有相应的字段记录该信息。

3)COPY

将文件从 build context 复制到镜像。

COPY 支持两种形式:

1、COPY src dest

2、COPY ["src", "dest"]

4)ADD

与 COPY 类似,从 build context 复制文件到镜像。不同的是,如果 src 是归档文件(tar, zip, tgz, xz 等),文件会被自动解压到 dest。镜像。

5)ENV

设置环境变量,环境变量可被后面的指令使用。

6)EXPOSE

设置指令,该指令会将容器中的端口映射成宿主机器中的某个端口。当你需要访问容器的时候,可不是用容器的IP地址而是使用宿主机器的IP地址和映射后的端口

EXPOSE、-expose、-p对比

EXPOSE(dockerfile)和参数(-expose):暴露端口,不依赖宿主机------目前发现只是写了下配置文件,并没什么用,应该只是告诉镜像使用者,应该映射哪个端口

-p:发布端口,docker run -p ip::host_port:docker_port  其中,ip和host_port都可以省略,Docker会帮助选择一个宿主机端口

可以通过docker inspect docker_name查询,字段:"ExposedPorts",只使用EXPOSE(dockerfile)和参数(-expose),无法通过宿主机地址+端口进行访问的

7)RUN

在容器中运行指定的命令。

8)CMD

容器启动时运行指定的命令。
Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效。CMD 可以被 docker run 之后的参数替换。

9)ENTRYPOINT

设置容器启动时运行的命令。
Dockerfile 中可以有多个 ENTRYPOINT 指令,但只有最后一个生效。CMD 或 docker run 之后的参数会被当做参数传递给 ENTRYPOINT。

run、cmd与ENTRYPOINT的区别:

1、RUN 执行命令并创建新的镜像层,RUN 经常用于安装软件包。

2、CMD 设置容器启动后默认执行的命令及其参数,但 CMD 能够被 docker run 后面跟的命令行参数替换。

3、ENTRYPOINT 配置容器启动时运行的命令。

 

5、上面讲述了镜像的创建,接下来看下容器的创建:

容器创建需要:1、内核  2、runtime  3、管理工具

runtime 是容器真正运行的地方。runtime 需要跟操作系统 kernel 紧密协作,为容器提供运行环境,lxc、runc 和 rkt 是目前主流的三种容器 runtime。MANO使用的是runc 通过docker info可以查看。

runc 的管理工具是 docker engine。docker engine 包含后台 deamon 和 cli 两个部分。我们通常提到Docker,一般就是指的 docker engine。

创建容器命令:

docker run -it --name {container_name} {image_name}

注意:正常启动容器时,容器会正常退出,是因为缺少死循环,因此需要在命令最后加上</bin/bash -c "while true;do sleep 1; done">,或者在run.sh(正常启动容器都不会把所有命令写在Dockerfile文件里,会通过ENTRYPOINT命令执行run.sh的)中

容器启动后,需要通过一些命令查看容器图:

6、docker内存限额:

docker run -it -m 200M --memory-swap=300M progrium/stress --vm 1 --vm-bytes 280M

1、-m 或 --memory:设置内存的使用限额,例如 100M, 2G。

2、--memory-swap:设置 内存+swap 的使用限额。

3、默认情况下,上面两组参数为 -1,即对容器内存和 swap 的使用没有限制

4、--vm 1:启动 1 个内存工作线程

5、--vm-bytes 280M:每个线程分配 280M 内存。

progrium/stress为镜像名称,如果--memory-swap不写,默认是memory的两倍

7、CPU限制:

Docker 可以通过 -c 或 --cpu-shares 设置容器使用 CPU 的权重。如果不指定,默认值为 1024。

与内存限额不同,通过 -c 设置的 cpu share 并不是 CPU 资源的绝对数量,而是一个相对的权重值。某个容器最终能分配到的 CPU 资源取决于它的 cpu share 占所有容器 cpu share 总和的比例。

8、block IO 权重:

Block IO 是另一种可以限制容器使用的资源。Block IO 指的是磁盘的读写,docker 可通过设置权重、限制 bps 和 iops 的方式控制容器读写磁盘的带宽

默认情况下,所有容器能平等地读写磁盘,可以通过设置 --blkio-weight 参数来改变容器 block IO 的优先级。

--blkio-weight 与 --cpu-shares 类似,设置的是相对权重值,默认为 500。在下面的例子中,container_A 读写磁盘的带宽是 container_B 的两倍。

docker run -it --name container_A --blkio-weight 600 ubuntu   

docker run -it --device-write-bps /dev/mapper/vg_data-lv_home:200MB docker1

限制 bps 和 iops

bps 是 byte per second,每秒读写的数据量。

iops 是 io per second,每秒 IO 的次数。

可通过以下参数控制容器的 bps 和 iops:

--device-read-bps,限制读某个设备的 bps。

--device-write-bps,限制写某个设备的 bps。

--device-read-iops,限制读某个设备的 iops。

--device-write-iops,限制写某个设备的 iops。

 

6、7和8总结:

cgroup 和 namespace 是最重要的两种技术。cgroup 实现资源限额, namespace 实现资源隔离。

cgroup 全称 Control Group。Linux 操作系统通过 cgroup 可以设置进程使用 CPU、内存 和 IO 资源的限额。相信你已经猜到了:前面我们看到的--cpu-shares-m--device-write-bps 实际上就是在配置 cgroup。

对应的资源配置在/sys/fs/cgroup/{res_name}/docker/{dockerid}下

9、docker网络:

网络应该是docker中比较重要和关键的资源,接下来分享些docker原生的网络、如何自定义网络和容器网络通信(包括容器之间和容器与外界)

1)、容器提供的原生的网络:

Docker 安装时会自动在 host 上创建三个网络:bridge、host和none。

可以通过docker network ls 查看容器目前提供的网络

A、none

none 网络就是什么都没有的网络。挂在这个网络下的容器除了 lo,没有其他任何网卡。容器创建时,可以通过 --network=none 指定使用 none 网络。

docker run -it --network=none hdi

B、host 网络

连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 完全一样。可以通过 --network=host 指定使用 host 网络。

直接使用 Docker host 的网络最大的好处就是性能,如果容器对网络传输效率有较高要求,则可以选择 host 网络。当然不便之处就是牺牲一些灵活性,比如要考虑端口冲突问题,Docker host 上已经使用的端口就不能再用了。

docker run -it --network=host hdi

C、bridge

Docker 安装时会创建一个 命名为 docker0 的 linux bridge。如果不指定--network,创建的容器默认都会挂到 docker0 上。

可以通过brctl show显示出来

一个新的网络接口 vethd7b76f5 被挂到了 docker0 上,vethd7b76f5就是新创建容器的虚拟网卡。

可以进入容器看下网络配置:命令:ip a

容器有一个网卡 eth0@if52。大家可能会问了,为什么不是vethd7b76f5 呢?

实际上 eth0@if52 和 vethd7b76f5 是一对 veth pair。veth pair 是一种成对出现的特殊网络设备,可以把它们想象成由一根虚拟网线连接起来的一对网卡,网卡的一头(eth0@if52)在容器中,另一头(vethd7b76f5)挂在网桥 docker0 上,其效果就是将eth0@if52 也挂在了 docker0 上。

由命名可见该虚拟网卡序号为51关联52,而52网卡在哪呢?其实就是vethd7b76f5,可以在host下看到:命令:ip a

而网卡序号正式挂在bridge0上的vethd7b76f5。

我们还看到 eth0@if52 已经配置了 IP 172.17.0.6,为什么是这个网段呢?让我们通过 docker network inspect bridge 看一下 bridge 网络的配置信息:

原来 bridge 网络配置的 subnet 就是 172.17.0.0/16,并且网关是 172.17.0.1。这个网关在哪儿呢?大概你已经猜出来了,就是 docker0。

D、自定义网络:

除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络

Docker 提供三种 user-defined 网络驱动:bridge, overlay 和 macvlan。overlay 和 macvlan 用于创建跨主机的网络.

docker network create --driver bridge my_net

上述命令可以类似于前面docker0(默认bridge)的网络,可以通过subnet和gateway参数为网络提供子网和网关

docker network create --driver bridge --subnet 172.19.0.0/16 --gateway 172.19.0.1 my_net2

可以通过docker network inspect my_net2查看具体信息:

可以为容器指定静态的网络:只有使用 --subnet 创建的网络才能指定静态 IP
docker run -it --network=my_net2 --ip 172.19.0.16 image_name

docker network connect my_net container_id

创建两个自定义网络my_net与my_net2,启动两个容器,Docker1的网卡eth0与eth1分别挂在在docker0和my_net上,Docker2的网卡eth0与eth1分别挂在在my_net和my_net2上,很明显docker1能够与docker2通讯,因为两个容器在同一个网络上

不同网络下的docker不能够直接通讯,ping不通

iptables-save命令可以看到如下:

-A DOCKER-ISOLATION -i br-5d863e9f78b6 -o docker0 -j DROP

-A DOCKER-ISOLATION -i docker0 -o br-5d863e9f78b6 -j DROP

原因:iptables DROP 掉了网桥 docker0 与 br-5d863e9f78b6 之间双向的流量

可以将上述容器httpd添加my_net2网络:docker network connect my_net2 docker_id

可以通过删除路由规则让两个容器之间可以通讯

2)网络通信的三种方式: IP,Docker DNS Server 或 joined 

A、IP:

两个容器要能通信,必须要有属于同一个网络的网卡。

B、Docker DNS Server

使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge 网络是无法使用 DNS 的

C、joined 

joined 容器非常特别,它可以使两个或多个容器共享一个网络栈,共享网卡和配置信息,joined 容器之间可以通过 127.0.0.1 直接通信

3)网络交互

A、容访问外部网络:

如果host是能够访问外部网络的,那么容器默认就是能够访问外网的。

可以在容器内PING外网:

可以查看docker0网卡查看数据是怎么转发的:

是通过docker上eth0-172.17.0.3发往10.43.35.96的,那么问题来了,内部的网络172.17.0.3怎么能够和10.43.35.96通讯的?

其实,当数据到达docker0时,docker0会将数据转交给NAT,将地址转换成eth0的地址

Iptables -t nat -S

可以显示NAT表:-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

其含义是:来自 172.17.0.0/16 网段的包,目标地址是外网(! -o docker0),就把它交给 MASQUERADE 处理。而 MASQUERADE 的处理方式是将包的源地址替换成 host 的地址发送出去,即做了一次网络地址转换(NAT)

 

B、外网访问容器:

外部网络如何访问到容器? 

其实,是通过端口映射

启动容器是通过 -p 参数设置host与docker的映射关系:docker run --name hdd -p 22324:2324 image_name

这样host的 22324端口就和容器hdd的2324端口有了映射关系,只要是发往22324的端口的数据,就会转发给hdd

每一个映射的端口,host 都会启动一个 docker-proxy 进程来处理访问容器的流量

针对网络与外部的交互,整理了下面这幅图:

10、存储:

Docker 为容器提供了两种存放数据的资源:storage driver 和 data volume

1)由 storage driver 管理的镜像层和容器层 (无状态-容器删除,数据丢失)

storage driver:容器的copy-on-write就是storage driver实现的,删除容器时,销毁

Docker 支持多种 storage driver,有 AUFS、Device Mapper、Btrfs、OverlayFS、VFS 和 ZFS,优先使用 Linux 发行版默认的 storage driver

可以通过 docker info查看

Storage Driver: overlay2

Docker Root Dir: /home/docker

2)Data Volume (有状态-容器删除数据保存)

data volume:保存永久性数据,包括bind mount 和 docker managed volume

A、bind mount

bind mount是将 host 上已存在的目录或文件 mount 到容器

docker run -d --name hdd -v /home/zp:/home/ngomm hdi     -----目录

docker run -d --name hdd -v /home/zp/hello_world_uwsgi/Dockerfile:/home/test1/df hdi             ------文件

B、docker managed volume

与 bind mount 在使用上的最大区别是不需要指定 mount 源,指明 mount point 就行了,可以通过docker inspect hdd 查看mount源地址

docker run -d --name hdd -v /home/test1/ngomm hdi

容器共享数据:

1.bind mount几个容器共享同一个目录

2.volume container 先创建一个容器:docker create --name vcdata -v /home/dir1:/home/dir1 -v /home/dir2 hdi,再创建其他容器使用该vc:docker run -itd --name hdd --volumes-from vcdata hdi

还可以将volume写到镜像dockerfile中

可以通过docker inspect vcdata 查看:

很明显指定host目录的volume使用指定目录,没有分配的会默认创建一个/home/docker/volumes/82442f84beb2824b908413912dd8266bd52c43ba615e245b07c655f4c2ea7e06/_data

相关命令:

1、docker images {image-id}

查询镜像信息

2、docker inspect {image-id}

查看镜像详情信息

3、docker history {image-id}

查看镜像的构建过程

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐