Docker 架构和底层技术

架构

Docker 作用

  • docker 提供了一个开发、打包和运行 application 的平台
  • 把 application 和底层 infrastructure(底层的物理设备或者虚拟设备) 隔离开来


Docker 核心概念 - 图1

Docker Engine
Docker 核心概念 - 图2
整体架构
Docker 核心概念 - 图3

底层技术

  • Namespace:隔离pid、net、ipc、mnt、uts
  • Control Groups:对进程做资源限制(对于宿主机来说,一个容器就是一个进程)
  • Union File System:container 和 image 的分层

    Docker 镜像 & 容器

    Image 镜像

    Docker 核心概念 - 图4

  • 文件和 Metadata 的集合(root filesystem)

  • 镜像是分层的,并且每一层都可以添加、改变、删除文件,成为一个新的 Image
  • 不同的 Image 可以共享相同的分层(如 #4 和 #2 共享了相同的 Base Image)
  • Image 本身是只读的

bootfs:内核空间,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉
rootfs:用户空间的文件系统,包含我们熟悉的 /dev, /usr, /bin 等目录
对于 base 镜像来说,底层直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。

而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他 Linux 发行版,CentOS 的 rootfs 已经算臃肿的了(200MB左右),alpine 还不到 10MB。

获取 Image

  • 通过 Dockerfile 构建 ```bash

    Dockerfile

    -t :指定要创建的目标镜像名

    . :Dockerfile 文件所在目录,可以指定 Dockerfile 的绝对路径

    FROM scratch ADD hello / CMD [“/hello”]

构建命令

docker build -t gongyz/hello-world .

  1. - 从镜像仓库中拉取,默认从 [Docker Hub](https://cloud.docker.com) 上拉取镜像
  2. ```bash
  3. docker pull ubuntu || docker pull ubuntu:18.04 || docker pull gongyz/ubuntu:18.04
  • 从已经创建的容器中更新镜像(不推荐)
    1. # -m: 提交的描述信息
    2. # -a: 指定镜像作者
    3. # e218edb10161:容器ID
    4. # gongyz/ubuntu:version: 指定要创建的目标镜像名和Tag(默认是latest)
    5. docker commit -m="has update" -a="gongyz" e218edb10161 gongyz/ubuntu:version

    基本命令

    ```bash

    查看本地镜像

    docker image ls || docker images

查找镜像

docker search name

删除本地镜像

docker rmi name/id

删除未使用的镜像

docker rmi $(docker images —filter “dangling=true” -q —no-trunc)

查看镜像的 commit 记录

docker history name/id

  1. <a name="e41b03c7"></a>
  2. ### Container 容器
  3. ![](https://cdn.nlark.com/yuque/0/2021/png/387995/1616417051810-085b3723-405e-4d34-b953-00a6dba66fcb.png#crop=0&crop=0&crop=1&crop=1&height=571&id=RQ54O&originHeight=571&originWidth=748&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=none&title=&width=748)
  4. - 通过 Image 创建
  5. - 在 Image layer(只读层) 之上建立一个 container layer(可读写层)
  6. - Image 负责 application 的存储和分发,Container 负责运行 application
  7. <a name="e71cb1a5-1"></a>
  8. ##### 基本命令
  9. ```bash
  10. # 运行容器
  11. docker run id/name || docker run -it id/name || docker run --name="***" id/name
  12. # 后台运行容器
  13. docker run -d id/name
  14. # 容器的端口映射到宿主机端口
  15. docker run -d -p 5000:5000 id/name
  16. # 运行容器时指定环境变量,容器中可以通过 env 命令查看环境变量
  17. docker run -e NAME=gongyz id/name
  18. # 查看正在运行容器
  19. docker container ls || docker ps
  20. # 查看所有容器(包括已退出容器)
  21. docker container ls -a || docker ps -a
  22. # 启动/关闭容器
  23. docker start id/name || docker stop id/name
  24. # 进入某个容器并执行指定的命令
  25. docker exec -it id/name /bin/bash
  26. # 删除容器
  27. docker rm id/name
  28. # 删除所有容器
  29. docker rm $(docker ps -aq)
  30. # 删除已退出容器
  31. docker rm $(docker ps -f "status=exited" -q)
  32. # 查看容器详细信息
  33. docker inspect id/name
  34. # 查看容器运行日志
  35. docker logs id/name

Dockerfile 语法

FROM

  1. # 指定基础镜像,尽量使用官方的 image 作为 base image
  2. FROM scratch # 指定一个空的基础镜像
  3. FROM centos
  4. FROM ubuntu:14.04

LABEL

  1. # 定义 Image 的 Metadata,类似于代码注释
  2. LABEL maintainer="gongyz 1018017334@qq.com"
  3. LABEL version="1.0"
  4. LABEL description="This is description"

RUN

  1. # 执行指定的命令,每次执行 RUN 命令都会创建新的 Image Layer
  2. # 为了避免无用分层,推荐合并多条命令成一行
  3. # 为了可读性,复杂的 RUN 请用反斜杠换行
  4. # RUN 经常用于安装软件包
  5. RUN update && yum install -y vim \
  6. python-dev # 反斜杠换行

WORKDIR

  1. # 指定工作目录
  2. # 推荐使用 WORKDIR, 不要使用 RUN cd
  3. # 尽量使用绝对目录
  4. WORKDIR /test # 如果没有会自动创建 test 目录
  5. WORKDIR demo
  6. RUN pwd # 输出结果是 /test/demo

ADD && COPY

  1. # 将本地文件添加到镜像中指定目录
  2. # 大部分情况下,COPY 优先于 ADD
  3. # ADD还有解压的功能
  4. # 添加远程文件/目录使用 curl 或者 wget
  5. ADD hello /
  6. ADD test.tar.gz / # 添加到根目录并解压
  7. WORKDIR /root
  8. ADD hello test/ # /root/test/hello
  9. WORKDIR /root
  10. COPY hello test/

ENV

  1. # 设置常量
  2. # 尽量使用 ENV 增加 Dockerfile 可维护性
  3. ENV MYSQL_VERSION 5.6 # 设置常量
  4. RUN apt install -y mysql-server="${MYSQL_VERSION}" # 引用常量

CMD

  1. # 设置容器启动后默认执行的命令
  2. # 如果 docker run 指定了其他命令,CMD命令将会被忽略
  3. # 如果定义了多个CMD,只有最后一个会执行
  4. # Dockerfile
  5. FROM centos
  6. ENV name Docker
  7. CMD echo "hello $name"
  8. # 启动容器
  9. docker run image # 输出 hello Docker
  10. docker run -it image /bin/bash # 没有输出

ENTERPOINT

  1. # 让容器以应用程序或者服务的形式运行
  2. # 不会被忽略
  3. # 最佳实践:写一个 shell 脚本作为 enterpoint
  4. # Dockerfile
  5. COPY docker-enterpoint.sh /usr/bin/
  6. ENTERPOINT ["docker-enterpoint.sh"]
  7. EXPOSE 27017
  8. CMD ["mongod"]

VOLUME

  1. # 指定数据持久化的路径
  2. VOLUME /var/lib/mysql
  3. # 建议运行容器时指定 volume 的名称
  4. docker run -v mysql:/var/lib/mysql

EXPOSE

  1. # 容器向外暴露的端口
  2. # Dockerfile
  3. FROM python:2.7
  4. LABEL maintainer="gongyz 1018017334@qq.com"
  5. RUN pip install flask
  6. COPY app.py /app/
  7. WORKDIR /app
  8. EXPOSE 5000
  9. CMD ["python", "app.py"]
  10. # app.py
  11. from flask import Flask
  12. app = Flask(__name__)
  13. @app.route('/')
  14. def hello():
  15. return "hello docker"
  16. if __name__ == '__main__':
  17. app.run(host="0.0.0.0", port=5000)

Dockerfile 命令格式

  1. # shell 格式 ( shell 格式底层会调用 /bin/sh -c <command> )
  2. RUN apt-get install -y vim
  3. CMD echo 'hello docker'
  4. ENTERPOINT echo 'hello docker'
  5. # Exec 格式
  6. RUN ["apt-get", "install", "-y", "vim"]
  7. CMD ["/bin/echo", "hello docker"]
  8. ENTERPOINT ["/bin/echo", "hello docker"]

Docker 网络(重点)

单机

  • Bridge Network
  • Host Network
  • Node Network

多机(了解)

  • Overlay Network(基于VXLAN网络实现数据包的传递)

    Bridge Network

Docker 核心概念 - 图5
借助于 linux 的 namespace 技术,docker 容器默认使用的是 Bridge 网络模式,该模式会为每一个容器分配、设置IP(每个容器都有自己独立的 network space),并将容器连接到一个 docker0 的虚拟网桥,通过 docker0 虚拟网桥实现容器以及容器和宿主机之间的通信。
容器虽然以及分配了IP地址,但是这个地址并不能直接访问 Internet,需要经过 NAT(网络地址转换)之后才能通过网卡连接到 Internet。
安装 Docker 时,它会自动创建三个网络,可以通过 docker network ls 命令列出这些网络:

  1. docker network ls
  2. NETWORK ID NAME DRIVER
  3. 7fca4eb8c647 bridge bridge
  4. 9f904ee27bf5 none null
  5. cf03ee007fb4 host host
  6. # 查看指定网络模式
  7. docker network inspect name/id

测试:通过 busybox 创建两个容器,测试容器之间是否能通信

  1. # 查看本机网络情况,可以看到有一个 docker0 的虚拟网桥,且状态为DOWN
  2. ip a
  3. # 创建两个容器
  4. docker run -d --name test1 busybox /bin/sh -c "while true; do sleep 3600; done"
  5. docker run -d --name test2 busybox /bin/sh -c "while true; do sleep 3600; done"
  6. # 再次查看本机网络情况,可以看到 docker0 的状态为 up,且多了两对 veth pair,这两对 veth pair 一个连接在宿主机的network space上,另一个连接在容器的 network space上
  7. ip a
  8. # 查看 Bridge Network 模式下的容器,可以看到新创建的两个容器
  9. docker network inspect bridge
  10. # 测试能否 ping 通
  11. # test1 ip 172.17.0.2
  12. # test2 ip 172.17.0.3
  13. # 宿主机 ip 192.168.240.119
  14. docker exec -it test1 ping 127.0.0.3
  15. docker exec -it test2 ping 127.0.0.2
  16. docker exec -it test1 ping 192.168.240.119
  17. docker exec -it test2 ping 192.168.240.119

容器之间的 link(很少用)

  1. # 创建一个容器并连接到指定的容器
  2. docker network --link id/name(指定的容器)id/name(镜像)
  3. # 创建 test2 容器并 link 到 test1
  4. docker run -d --name test2 --link test1 busybox /bin/sh -c "while true; do sleep 3600; done"
  5. # ping test1
  6. ping 172.17.0.2 # ip
  7. ping test1 # name

注意:test2 连接到 test1 后,可以直接通过名称访问 test1,test1 不能却直接通过名称访问 test2。

自定义 network

  1. # 创建一个 network
  2. docker network create -d bridge mybridge # -d 参数指定 driver(网络模式)
  3. # 查看 network 列表
  4. docker network ls
  5. # 创建 test3 容器并指定 network
  6. docker run -d --name test3 --network mybridge busybox /bin/sh -c "while true; do sleep 3600; done"
  7. # 自定义网络连接到已经创建的容器
  8. docker network connect mybridge test2

注意:两个容器连接到同一个自定义 network,那么这两个容器默认是互相连接的,可以直接通过名称访问。

容器的端口映射

  1. # 启动容器时通过 -p 参数将容器端口映射到宿主机端口
  2. docker run -d --name web -p 80:80 nginx

Host/None Network

  1. # none network 外部无法访问该容器中启动的服务,只能进入容器中访问
  2. docker run -d --name test1 --network none busybox /bin/sh -c "while true; do sleep 3600; done"
  3. # host network 与宿主机共享一个 network space,可能存在端口冲突的问题,比如两个容器使用相同端口
  4. docker run -d --name test2 --network host busybox /bin/sh -c "while true; do sleep 3600; done"

多容器复杂应用的部署

场景: 部署一个包含 reids 数据库的 Python Web应用,web应用和 redis 在不同容器中启动

  1. # app.py
  2. from flask import Flask
  3. from redis import Redis
  4. import os
  5. import socket
  6. # REDIS_HOST 环境变量
  7. app = Flask(__name__)
  8. redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)
  9. @app.route('/')
  10. def hello():
  11. redis.incr('hits')
  12. return 'Hello Container World! I have been seen %s times and my hostname is %s.\n' % (redis.get('hits'),socket.gethostname())
  13. if __name__ == "__main__":
  14. app.run(host="0.0.0.0", port=5000, debug=True)
  15. # Dockerfile
  16. FROM python:2.7
  17. LABEL maintaner="gongyz 1018017334@qq.com"
  18. COPY . /app
  19. WORKDIR /app
  20. RUN pip install flask redis
  21. EXPOSE 5000
  22. CMD [ "python", "app.py" ]
  23. # 创建镜像
  24. docker image build -t gongyz/python-web .
  25. # 启动 redis 容器
  26. docker run -d --name redis redis
  27. # 启动 python web 容器
  28. docker run -d -p 5000:5000 --name python-web --link redis -e REDIS_HOST=redis gongyz/python-web

多机网络

前面涉及的内容都是单机网络,包括多容器应用也是部署在单机上,那不同主机之间的容器应该如何建立连接和通信呢?这就涉及到 docker 的另外一种网络模式:Overlay Network。
Docker 核心概念 - 图6

Docker 持久化

默认情况下,容器的数据是保存在容器的可读写层,当容器被删除时,可读层上的数据将会丢失。所以为了实现数据的持久性,需要选择一种持久化技术来保存数据。Docker 支持两种持久化方案:Data VolumeBing Mouting

Data Volume(数据卷)

  • 基于本地文件系统的 Volume,可以在执行 docker create 或者 docker run 时,通过 -v 参数将主机的目录作为容器的数据卷。
  • 基于 plugin 的 Volume ,支持第三方的存储方案,比如NAS、AWS。 ```bash

    Docker中所有的 volume 都在宿主机上的指定目录下 /var/lib/docker/volumes

    Data Volume 需要在 Dockerfile 中通过 VOLUME 参数指定数据卷

Dockerfile

VOLUME /var/lib/mysql

启动一个 mysql 容器(建议运行容器时指定 volume 的名称)

docker run -d -v mysql:/var/lib/mysql —name mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

  1. <a name="jeaYm"></a>
  2. ### Bind Mouting(绑定挂载)
  3. ```bash
  4. # 将宿主机中已存在的目录或者文件 mount 到容器中
  5. docker run -d -p 3000:3000 -v $(pwd)/html:/node-http/html gongyz/node-http