Docker 架构和底层技术
架构
Docker 作用
- docker 提供了一个开发、打包和运行 application 的平台
- 把 application 和底层 infrastructure(底层的物理设备或者虚拟设备) 隔离开来
底层技术
- Namespace:隔离pid、net、ipc、mnt、uts
- Control Groups:对进程做资源限制(对于宿主机来说,一个容器就是一个进程)
Union File System:container 和 image 的分层
Docker 镜像 & 容器
Image 镜像
文件和 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 .
- 从镜像仓库中拉取,默认从 [Docker Hub](https://cloud.docker.com) 上拉取镜像
```bash
docker pull ubuntu || docker pull ubuntu:18.04 || docker pull gongyz/ubuntu:18.04
- 从已经创建的容器中更新镜像(不推荐)
# -m: 提交的描述信息
# -a: 指定镜像作者
# e218edb10161:容器ID
# gongyz/ubuntu:version: 指定要创建的目标镜像名和Tag(默认是latest)
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
<a name="e41b03c7"></a>
### Container 容器
![](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)
- 通过 Image 创建
- 在 Image layer(只读层) 之上建立一个 container layer(可读写层)
- Image 负责 application 的存储和分发,Container 负责运行 application
<a name="e71cb1a5-1"></a>
##### 基本命令
```bash
# 运行容器
docker run id/name || docker run -it id/name || docker run --name="***" id/name
# 后台运行容器
docker run -d id/name
# 容器的端口映射到宿主机端口
docker run -d -p 5000:5000 id/name
# 运行容器时指定环境变量,容器中可以通过 env 命令查看环境变量
docker run -e NAME=gongyz id/name
# 查看正在运行容器
docker container ls || docker ps
# 查看所有容器(包括已退出容器)
docker container ls -a || docker ps -a
# 启动/关闭容器
docker start id/name || docker stop id/name
# 进入某个容器并执行指定的命令
docker exec -it id/name /bin/bash
# 删除容器
docker rm id/name
# 删除所有容器
docker rm $(docker ps -aq)
# 删除已退出容器
docker rm $(docker ps -f "status=exited" -q)
# 查看容器详细信息
docker inspect id/name
# 查看容器运行日志
docker logs id/name
Dockerfile 语法
FROM
# 指定基础镜像,尽量使用官方的 image 作为 base image
FROM scratch # 指定一个空的基础镜像
FROM centos
FROM ubuntu:14.04
LABEL
# 定义 Image 的 Metadata,类似于代码注释
LABEL maintainer="gongyz 1018017334@qq.com"
LABEL version="1.0"
LABEL description="This is description"
RUN
# 执行指定的命令,每次执行 RUN 命令都会创建新的 Image Layer
# 为了避免无用分层,推荐合并多条命令成一行
# 为了可读性,复杂的 RUN 请用反斜杠换行
# RUN 经常用于安装软件包
RUN update && yum install -y vim \
python-dev # 反斜杠换行
WORKDIR
# 指定工作目录
# 推荐使用 WORKDIR, 不要使用 RUN cd
# 尽量使用绝对目录
WORKDIR /test # 如果没有会自动创建 test 目录
WORKDIR demo
RUN pwd # 输出结果是 /test/demo
ADD && COPY
# 将本地文件添加到镜像中指定目录
# 大部分情况下,COPY 优先于 ADD
# ADD还有解压的功能
# 添加远程文件/目录使用 curl 或者 wget
ADD hello /
ADD test.tar.gz / # 添加到根目录并解压
WORKDIR /root
ADD hello test/ # /root/test/hello
WORKDIR /root
COPY hello test/
ENV
# 设置常量
# 尽量使用 ENV 增加 Dockerfile 可维护性
ENV MYSQL_VERSION 5.6 # 设置常量
RUN apt install -y mysql-server="${MYSQL_VERSION}" # 引用常量
CMD
# 设置容器启动后默认执行的命令
# 如果 docker run 指定了其他命令,CMD命令将会被忽略
# 如果定义了多个CMD,只有最后一个会执行
# Dockerfile
FROM centos
ENV name Docker
CMD echo "hello $name"
# 启动容器
docker run image # 输出 hello Docker
docker run -it image /bin/bash # 没有输出
ENTERPOINT
# 让容器以应用程序或者服务的形式运行
# 不会被忽略
# 最佳实践:写一个 shell 脚本作为 enterpoint
# Dockerfile
COPY docker-enterpoint.sh /usr/bin/
ENTERPOINT ["docker-enterpoint.sh"]
EXPOSE 27017
CMD ["mongod"]
VOLUME
# 指定数据持久化的路径
VOLUME /var/lib/mysql
# 建议运行容器时指定 volume 的名称
docker run -v mysql:/var/lib/mysql
EXPOSE
# 容器向外暴露的端口
# Dockerfile
FROM python:2.7
LABEL maintainer="gongyz 1018017334@qq.com"
RUN pip install flask
COPY app.py /app/
WORKDIR /app
EXPOSE 5000
CMD ["python", "app.py"]
# app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "hello docker"
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)
Dockerfile 命令格式
# shell 格式 ( shell 格式底层会调用 /bin/sh -c <command> )
RUN apt-get install -y vim
CMD echo 'hello docker'
ENTERPOINT echo 'hello docker'
# Exec 格式
RUN ["apt-get", "install", "-y", "vim"]
CMD ["/bin/echo", "hello docker"]
ENTERPOINT ["/bin/echo", "hello docker"]
Docker 网络(重点)
单机
- Bridge Network
- Host Network
- Node Network
多机(了解)
借助于 linux 的 namespace 技术,docker 容器默认使用的是 Bridge 网络模式,该模式会为每一个容器分配、设置IP(每个容器都有自己独立的 network space),并将容器连接到一个 docker0 的虚拟网桥,通过 docker0 虚拟网桥实现容器以及容器和宿主机之间的通信。
容器虽然以及分配了IP地址,但是这个地址并不能直接访问 Internet,需要经过 NAT(网络地址转换)之后才能通过网卡连接到 Internet。
安装 Docker 时,它会自动创建三个网络,可以通过 docker network ls
命令列出这些网络:
docker network ls
NETWORK ID NAME DRIVER
7fca4eb8c647 bridge bridge
9f904ee27bf5 none null
cf03ee007fb4 host host
# 查看指定网络模式
docker network inspect name/id
测试:通过 busybox 创建两个容器,测试容器之间是否能通信
# 查看本机网络情况,可以看到有一个 docker0 的虚拟网桥,且状态为DOWN
ip a
# 创建两个容器
docker run -d --name test1 busybox /bin/sh -c "while true; do sleep 3600; done"
docker run -d --name test2 busybox /bin/sh -c "while true; do sleep 3600; done"
# 再次查看本机网络情况,可以看到 docker0 的状态为 up,且多了两对 veth pair,这两对 veth pair 一个连接在宿主机的network space上,另一个连接在容器的 network space上
ip a
# 查看 Bridge Network 模式下的容器,可以看到新创建的两个容器
docker network inspect bridge
# 测试能否 ping 通
# test1 ip 172.17.0.2
# test2 ip 172.17.0.3
# 宿主机 ip 192.168.240.119
docker exec -it test1 ping 127.0.0.3
docker exec -it test2 ping 127.0.0.2
docker exec -it test1 ping 192.168.240.119
docker exec -it test2 ping 192.168.240.119
容器之间的 link(很少用)
# 创建一个容器并连接到指定的容器
docker network --link id/name(指定的容器)id/name(镜像)
# 创建 test2 容器并 link 到 test1
docker run -d --name test2 --link test1 busybox /bin/sh -c "while true; do sleep 3600; done"
# ping test1
ping 172.17.0.2 # ip
ping test1 # name
注意:test2 连接到 test1 后,可以直接通过名称访问 test1,test1 不能却直接通过名称访问 test2。
自定义 network
# 创建一个 network
docker network create -d bridge mybridge # -d 参数指定 driver(网络模式)
# 查看 network 列表
docker network ls
# 创建 test3 容器并指定 network
docker run -d --name test3 --network mybridge busybox /bin/sh -c "while true; do sleep 3600; done"
# 自定义网络连接到已经创建的容器
docker network connect mybridge test2
注意:两个容器连接到同一个自定义 network,那么这两个容器默认是互相连接的,可以直接通过名称访问。
容器的端口映射
# 启动容器时通过 -p 参数将容器端口映射到宿主机端口
docker run -d --name web -p 80:80 nginx
Host/None Network
# none network 外部无法访问该容器中启动的服务,只能进入容器中访问
docker run -d --name test1 --network none busybox /bin/sh -c "while true; do sleep 3600; done"
# host network 与宿主机共享一个 network space,可能存在端口冲突的问题,比如两个容器使用相同端口
docker run -d --name test2 --network host busybox /bin/sh -c "while true; do sleep 3600; done"
多容器复杂应用的部署
场景: 部署一个包含 reids 数据库的 Python Web应用,web应用和 redis 在不同容器中启动
# app.py
from flask import Flask
from redis import Redis
import os
import socket
# REDIS_HOST 环境变量
app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)
@app.route('/')
def hello():
redis.incr('hits')
return 'Hello Container World! I have been seen %s times and my hostname is %s.\n' % (redis.get('hits'),socket.gethostname())
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
# Dockerfile
FROM python:2.7
LABEL maintaner="gongyz 1018017334@qq.com"
COPY . /app
WORKDIR /app
RUN pip install flask redis
EXPOSE 5000
CMD [ "python", "app.py" ]
# 创建镜像
docker image build -t gongyz/python-web .
# 启动 redis 容器
docker run -d --name redis redis
# 启动 python web 容器
docker run -d -p 5000:5000 --name python-web --link redis -e REDIS_HOST=redis gongyz/python-web
多机网络
前面涉及的内容都是单机网络,包括多容器应用也是部署在单机上,那不同主机之间的容器应该如何建立连接和通信呢?这就涉及到 docker 的另外一种网络模式:Overlay Network。
Docker 持久化
默认情况下,容器的数据是保存在容器的可读写层,当容器被删除时,可读层上的数据将会丢失。所以为了实现数据的持久性,需要选择一种持久化技术来保存数据。Docker 支持两种持久化方案:Data Volume 和 Bing 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
<a name="jeaYm"></a>
### Bind Mouting(绑定挂载)
```bash
# 将宿主机中已存在的目录或者文件 mount 到容器中
docker run -d -p 3000:3000 -v $(pwd)/html:/node-http/html gongyz/node-http