docker

入门

虚拟化技术

主要目的:可以在一个物理设备上做到环境隔离

主机级别虚拟化

两种类型

  • 在裸(物理)机上装hostOs,之后在hostOs上hypervisor(eg:vmware),之后在hypervisor上装各种系统
  • 在裸(物理)机上直接装hypervisor,之后在hypervisor上装各种系统

img

假设需要运行一个web服务,为此我们必须先安装hypervisor,再安装系统,之后在系统上安装nginx或者tomcat;

如果是第一种类型,使用物理资源还需要经过两个内核的调度,这样开销是比较大的。

容器级别虚拟化

linux分为内核跟用户空间,linux内核有两个功能:

  • namespace
  • cgroup

通过这两个功能,linux可以做到对用户空间的隔离。这些隔离的用户空间就是容器。

每一个虚拟机都有自己的ip,域名,文件系统等等,一个主机有的它基本都有。

容器也需要有独立的:

  • 主机名:域名
  • 文件系统
  • 进程通信ipc
  • 进程id(pid)
  • 用户组
  • net(ip地址,端口,等)

这个就是通过命名空间(namespace)实现的;自己理解可以类比写代码时的namespace(相同的类名在不同的namespace下是隔离的)

然而不同namespace之间物理资源(更多是硬件资源,cpu,内存等)还是相互竞争的。仍然需要管理每个container可以使用的资源。cgroups 实现了对资源的配额和度量,通过它我们可以实现资源的隔离分配。

docker

容器级别虚拟化相对主机级别虚拟化

  • 开销小,因为直接运行在宿主机上
  • 隔离性上差点意思

这两种虚拟化技术都有一个问题:迁移性。

docker利用镜像跟仓库很好的解决了这个问题。

有了docker之后我们可以一次编写到处运行,只要安装了docker,这一点跟jvm挺像的。

最大的优点:分发部署

Docker基本用法

docker架构图

img

c/s架构,使用远程api的方式管理跟创建Docker容器。

  1. dockerClient

    类似postman,可以发送get,post,put等等各种请求给host,通过api的方式增删改查image跟container

  2. dockerHost

    • dockerDeamon:用来监听套接字,一般在宿主主机的后台运行,等待接受客户端的请求(docker命令),类比服务端程序。
    • image: 用来生成容器的
      • 只读的
      • 分层构建的(可以通过dockerFile理解),可以根据自己的需求在已有得镜像的基础上随意扩展
      • 静态的
    • container:隔离的用户空间,一个独立于主机的隔离的进程,由image创建
  3. Registry

    主要功能是作为repository存放image,docker官方提供了dockerhub,但是服务器在国外,访问比较慢,在国内也提供了,但是也不快。可以使用阿里或者科大或者清华的镜像仓库。个人配置的是阿里的,体验挺好的(需要注册一个阿里账户,之后会分配一个地址)。

    阿里镜像加速:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

image跟container的关系:类似程序跟进程的关系,类似class跟实体的关系

流程

在终端输入 docker container create –name n3 nginx,

  1. host的deamon收到请求(http的方式)
  2. 在本地检查是否有nginx镜像
  3. 如果有,那么根据镜像创建n3容器
  4. 如果么有,那么发请求到(https)registry中拉去nginx镜像到本地,之后在本地创建n3容器

常用命令

docker
  • docker verison
  • docker info (镜像加速配置完之后可以通过它来查看是否配置成功了)
  • docker search 镜像名 :dockerhub上查找镜像
  • docker inspect 容器名或者镜像名 : 查看容器或者镜像的详细信息(可以查看容器的ip地址,挂载信息等等)
  • docker exec -it 容器名 /bin/bash : 进入容器终端
  • docker logs 容器名
  • docker cp 容器名:容器文件或文件夹 宿主机文件或文件夹 :将容器中的文件或文件夹复制出来
  • docker tag 给镜像打标签
  • docker commit 通过容器创建镜像
image
  • docker image pull 镜像名:tag : 拉取镜像,tag可以缺省
  • docker image ls : 查看所有的镜像
  • docker image rm 镜像名 :删除镜像
container
  • docker container ls -a : 查看所有容器

  • docker container ls -s : 可以查看容器的size

  • docker container create –name n1 nginx :创建nginx容器n1

  • docker container start n1 :启动n1

  • docker container stop xxx :停止容器

  • docker container rm xxx :删除stop的容器

  • docker container run -it –name n2 nginx /bin/bash :创建并启动nginx容器n2,并进入容器终端

  • doker container run -it –name xxx –rm -v xxx1:xxx2 镜像名 /bin/bash

    • it:可以交互的终端
    • rm:运行完自动删除
    • d: 运行在后台
    • name xxx: 容器名
    • v : 文件挂载 主机的xxx1挂载到容器的xxx2
tip
  • Netstat -tnl 查看监听的端口

Docker 镜像管理基础

镜像就是把业务代码,可运行环境进行整体的打包。

镜像包含了启动容器需要的文件系统跟其他内容,因此,镜像用来创建并启动容器。

参考:https://zhuanlan.zhihu.com/p/70424048

思考题

  • 我们基于同一个镜像(ubuntu 18.4)启动了两个容器,会占用两倍磁盘空间吗?
  • 我们在容器内修改或者新建了某个文件,要修改原镜像吗?
  • 我们基于某镜像(ubuntu 18.04)新建一个镜像(myubuntu),需要将原镜像文件全部拷贝到新镜像中吗?

首先,让我们尝试思考下,如果我们去做,该如何高效的解决这些问题?

  • 问题 1,只要将同一个镜像文件加载到内存不同位置就行了,没必要在磁盘上存储多份,可以节省大量存储空间。
  • 问题 2,我们可以参考 Linux 内核管理内存的 Copy-On-Write 策略,也即读时大家共用一份文件,如果需要修改再复制一份进行修改,而大部分文件是其实不会修改的,这样可以最大限度节省空间,提升性能。
  • 问题 3,我们可以将镜像文件分为多个独立的层,然后新镜像文件只要引用基础镜像文件就可以了,这样可以节省大量空间。至于修改基础镜像文件的情况,参考问题 2

镜像分层

img

  1. 镜像分层,除了最上层的容器层是可读写的,其他层都是只读的。如果容器层需要读写其他层的数据,需要用到copy-on-write技术
  2. 只读层是可以共享的
  3. 每个容器都有自己的读写层,容器被删之后读写层也会被删除。如果想要持久化数据,可以使用docker挂载(volume):比如log日志
  4. 执行docker ps -s(或者docker container ls -s)可以看到容器最后一列有个size(2B (virtual 127MB)),这里的2B就是读写层的大小,127M是读写层加只读层的大小。
UnionFS

docker可以实现分层依赖的就是unionFs,所谓 UnionFS 就是把不同物理位置的目录合并到同一个目录中。对应docker,就是把下层的文件“合并”到上层,如果在上层想要修改下层的文件,需要先把下层文件copy到上层,之后再修改,但是下层的文件是不变的。

14年的时候linux内核中合并了overlayFs,也是现在docker使用的存储方案,执行docker info可以看到。

https://zhuanlan.zhihu.com/p/70424048 这篇文章中使用mount命令在ubuntu中模拟了docker的分层。

镜像构建分发

构建
  1. 通过容器构建

    1
    docker commit -p m1 kj/mysql:v1
    • -p 暂停容器
    • m1 容器名
    • kj/mysql 镜像仓库名
    • v1 镜像版本
  2. 通过dockerfile构建

    • 可以跟github,docker仓库(阿里云或者dockerhub)联动
  • 上传dockerFile到github,docker仓库检测到dockerfile的变化,会去github上拉代码然后build好镜像
    • 底层也是基于容器构建的,只不过是docker帮我们新建了一个容器,dockerfile实际是在这个容器里面执行的
分发
  1. 上传到仓库分发

    1
    2
    docker login 
    docker push
    • Dockerhub镜像的仓库名必须跟hub上的一致
    • 阿里云
  2. 本地打包分发

    1
    2
    docker save
    docker load

容器虚拟网络

基本概念

Linux内核支持二层以及三层设备的虚拟化。

  1. 虚拟网卡

    • linux内核支持虚拟网卡的创建(ip命令)

    • 每一个虚拟网卡是成对出现的,可以模拟成一根网线(下图中的eth0跟veth就是虚拟网卡的两端)

    作用:连接虚拟网桥跟容器

  2. 虚拟网桥(交换机)

    • Docker0

    • ip,mac映射

    • linux内核也支持虚拟交换机的创建(brctl 命令)

    • 通过交换机,同一个网段不同的容器可以通信

    作用:将主机以及所有的容器放在同一个局域网内

  3. nat

    网络地址转化

    作用

    • 将容器的私有地址转化为物理网卡的地址(snat)

    • 将物理网卡的地址转化为容器的地址(dnat)

docker的四种网络模式

bridge

当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。

从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到docker0网桥中。可以通过brctl show命令查看。

bridge模式是docker的默认网络模式,不写–net参数,就是bridge模式。使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。可以使用iptables -t nat -vnL查看

img

host

如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。

container

这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。

none

使用none模式,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。

bridge模型验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# ifconfig

docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536

veth4c07805: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

veth718f748: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

vethce1977e: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

vethf87afa7: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500

docker0就是虚拟网桥

vethxxx是虚拟网卡的一端,都是连接在docker0上的,使用brctl验证如下:

1
2
3
4
5
6
7
8
[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# yum -y install bridge-utils

[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242e24e01a3 no veth4c07805
veth718f748
vethce1977e
vethf87afa7

验证网卡连接:

1
2
3
4
5
6
7
8
[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT
5: vethce1977e@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0
9: vethf87afa7@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0
17: veth718f748@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master
19: veth4c07805@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master

5,9,17,19是4个网卡,以“vethce1977e@if4”为例,vethce1977e是连接在docker0上的,if4是放在容器上的。

进入容器查看网卡:

1
2
3
4
5
6
7
8
9
10
wangzeqi@wangzeqideMacBook-Pro ~ % docker run -it --name b1 busybox /bin/sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:03
inet addr:172.17.0.3 Bcast:172.17.255.255 Mask:255.255.0.0

lo Link encap:Local Loopback

/ # ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1): 56 data bytes
64 bytes from 172.17.0.1: seq=0 ttl=64 time=2.169 ms

eth0就是虚拟网卡的另一端,ping 虚拟网桥docker0可以ping通。

nat:查看iptables规则

1
[root@iZ2ze4qtfwa4w5rqbr2e83Z ~]# iptables -t nat -vnL

所有收到的源地址为172.18.0.0/16(除了docker0)目标地址为任意地址的请求,都做snat(局域网内部私有地址转化为主机物理网卡的地址) tip: MASQUERADE的翻译是化妆,非常形象

主机端口通过dnat转发到容器端口

参考:

https://www.cnblogs.com/yy-cxd/p/6553624.html

https://blog.csdn.net/nia305/article/details/82775384

tip

Q:Docker For Mac没有docker0网桥

在使用Docker时,要注意平台之间实现的差异性,如Docker For Mac的实现和标准Docker规范有区别,Docker For Mac的Docker Daemon是运行于虚拟机(xhyve)中的, 而不是像Linux上那样作为进程运行于宿主机,因此Docker For Mac没有docker0网桥,不能实现host网络模式,host模式会使Container复用Daemon的网络栈(在xhyve虚拟机中),而不是与Host主机网络栈,这样虽然其它容器仍然可通过xhyve网络栈进行交互,但却不是用的Host上的端口(在Host上无法访问)。bridge网络模式 -p 参数不受此影响,它能正常打开Host上的端口并映射到Container的对应Port。

Docker存储卷

写时复制

  • 镜像是分层的,docker容器启动的时候会去加载镜像的只读层,然后在最上层添加一个读写层。
  • 容器中的io都是针对读写层
  • 如果要对只读层的内容进行修改或者删除,需要将只读层的内容copy到读写层,之后在读写层操作(tip:删除的话只是在读写层标记为删除)

概念

考虑我们做一个mysql的容器

  1. 数据读写都在容器中的读写层,存储效率低。
  2. 数据会随着容器的删除而丢失

此时就需要存储卷

  1. “宿主机上”有个存储目录A,容器有个存储目录B,把这两个目录关联起来,A跟B的内容始终是同步的。那么A就是存储卷。
  2. 在容器删除的时候数据不会丢失
  3. 两个容器可以通过这种方式通信
  4. 1中“宿主机”并不准确,这个目录也可以是共享目录,比如nfs服务器上的目录,这样容器就不用局限在一个宿主机上。

两种类型

  1. 绑定挂载卷:宿主机的目录跟容器的目录都是run的时候指定的
  2. docker管理卷:容器中的目录是run的时候指定的,宿主机目录由docker deamon自动指定

演示

  1. 启动容器c1:关联宿主机目录/Users/wangzeqi/container/data 跟 容器目录/data
  2. 在/data目录下新建一个文件
  3. 删除容器c1,查看/Users/wangzeqi/container/data目录
  4. 启动容器c2,关联/Users/wangzeqi/container/data跟/data/temp,
  5. 查看/data/temp目录
  6. 查看容器信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#1
docker container run --name c1 --rm -it -v /Users/wangzeqi/container/data:/data busybox
#2
cd /data
echo "hello" >> test.txt
#3
exit
cd /Users/wangzeqi/container/data
cat test.txt
#4
docker container run --name c2 --rm -it -v /Users/wangzeqi/container/data:/data/temp busybox
#5
cd data/temp/
cat test.txt
#6
docker inspect c2

#可以看到如下信息
"Mounts": [
{
"Type": "bind", #存储卷类型
"Source": "/Users/wangzeqi/container/data", # 宿主机目录
"Destination": "/data/temp", # 容器目录
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],

#6 或者
docker inspect -f {{.Mounts}} c2
#打印如下
[{bind /Users/wangzeqi/container/data /data/temp true rprivate}]

tip

  1. -v的目录如果不存在docker会自动创建

  2. 两个容器可以共享一个存储卷

    • –volumes-from c1 复制容器c1的挂载关联规则
  3. 两个容器可以共享一个网络空间(docker网络模式中的container模式)

    • –network container:c1 加入c1的网络空间
  4. 为了不用每次都指定容器的网络配置或者挂载配置,假如需要搭建nmt (nginx ,tomcat,mysql )架构或者更加复杂的,可以

    • 创建一个基础架构容器,新建其他容器的时候从基础架构容器复制配置信息
    • 或者使用容器编排工具(docker-compose),这个也支持创建基础架构容器
    • 或者使用k8s编排

Dockerfile

构建镜像的源码。镜像是分层的,dockerfile中每一条命令对应一层。

工作逻辑

  1. 选定一个工作目录

  2. 工作目录下放Dockerfile文件

  3. 如果构建过程需要访问某个文件,那个这个文件必须在工作目录下

  4. 构建的时候工作目录下的文件都会打包到镜像。

  5. .dockerignore文件中声明的文件不会打包到镜像

指令

先看一个实例,看注释即可(以下是在node容器中构建ant design vue项目的dockerfile)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 定义基础镜像
FROM node:13.11.0
# install yarn
RUN npm install -y yarn
# 指定工作目录
WORKDIR /app/lszy_admin_web
# 安装依赖
COPY package.json yarn.lock ./
RUN yarn install
# 将工程copy到工作目录
COPY ./ ./
# 声明build的arg,测试环境:build:test;正式环境:build
ARG env
# build
RUN yarn ${env}
# 将dist复制到存储卷
CMD cp -rf dist dist_copy
FROM
  1. 定义基础镜像
  2. 默认情况下build命令会先在本地查看是否有该基础镜像,没有的话会去dockerHub拉取(如果设置了阿里的镜像加速,那么会从加速地址去拉取)或者也可以指定拉取的地址(registry)
  3. 格式:镜像仓库名:tag 或者 镜像仓库名@digest , 第二种更加安全
MAINTAINER

废弃了,设置作者信息

LABLE
  1. 替代maintainer
  2. key-value形式
  3. 用来设置说明信息,类似注释
COPY && ADD
  1. 从文件或者文件夹复制到镜像的指定目录
  2. ADD可以复制url,COPY只能复制宿主机的内容
  3. ADD复制tar文件时,会自动解压,copy不会自动解压
  4. 一条命令中源文件可以有多个,目标文件只能一个
  5. 复制文件夹的时候是复制文件夹下所有内容,文件夹本身不会复制过去
  6. 目标文件必须“/”结尾
WORKDIR
  1. 指定容器启动之后的工作目录
  2. 容器启动之后默认会进入该目录
  3. DockerFile中WORKDIR声明工作路径之后,下面所有命令中涉及到镜像中的路径时都可以使用相对该工作目录的相对路径
ARG && ENV
1
2
3
4
5
6
7
8
9
10
11
12
13
14
wangzeqi@wangzeqideMacBook-Pro workspace % docker build -t "test" .
Sending build context to Docker daemon 5.12kB
Step 1/3 : FROM busybox
---> 6d5fcfe5ff17
Step 2/3 : WORKDIR /
---> Running in 2630e64e0b23
Removing intermediate container 2630e64e0b23
---> 70c2a25a7355
Step 3/3 : RUN mkdir test && cd test && echo "hello" >> test.txt
---> Running in 91496fb114ae
Removing intermediate container 91496fb114ae
---> ddd723329232
Successfully built ddd723329232
Successfully tagged test:latest
  1. 使用dockerfile构建镜像的过程如上,(注意每一层执行之后都有一个removing操作)每一层都会新建一个临时容器,在容器上运行该层的命令,运行完成之后删除容器

  2. ARG用于定义镜像构建时传递的参数:在构建的过程中如果我们需要动态的传递参数,那么可以使用ARG命令指定形参,在build的时候传递实参

    1
    2
    3
    4
    5
    # Dockerfile
    ARG arg1

    # 执行构建
    docker build --build-arg arg1=build:test . -t node_lszy_admin_web
  3. ENV则是定义容器运行时传递的参数

    1
    2
    3
    4
    5
    # Dockerfile
    ENV env1

    # 运行容器
    docker run --name test --rm --env env1=build test
CMD && RUN
1. CMD 是在容器启动之后默认执行的命令,如果我们在docker run 之后加了命令,那么dockerfile中的的cmd将会被替换掉
 2. RUN 是在构建镜像是执行的命令
VOLOMN
  1. 定义容器中的存储卷
  2. run的时候不需要使用-v指定存储卷的位置
  3. 对应的宿主机上的挂载卷是docker自己指定的(对应存储卷的第二种类型)
EXPOSE
  1. 定义对外(宿主机)暴露的端口
  2. run的时候直接-P(注意是大写P),docker会自动分配宿主机上的一个端口映射到容器暴露的端口
  3. 如果需要自己指定端口映射,运行时使用-p 3306:3306 (小写p)即可

tip:linux环境变量(arg跟env命令可能会用到)

1
2
3
4
5
wangzeqi@wangzeqideMacBook-Pro dist % echo ${name:-wzq}
wzq
wangzeqi@wangzeqideMacBook-Pro dist % name=zpy
wangzeqi@wangzeqideMacBook-Pro dist % echo $name
zpy
  • “-wzq”如果name为空,那么name=wzq
  • “+wzq”如果name不为空,那么name=wzq

其他

  1. 一文教您如何通过 Docker 快速搭建各种测试环境(Mysql, Redis, Elasticsearch, MongoDB) https://juejin.im/post/5ce531b65188252d215ed8b7