Docker
介绍
DevOps = 文化 + 过程 + 工具
虚拟化:为了跨平台、资源(cpu、存储、带宽等)管理和隔离。
虚拟机:运行的程序通常会经过 Hypervisor 的监视来保证与硬件的兼容,在现实出于性能的考虑不会全部都经过 Hypervisor。
容器:
- 推动容器化的原因:虚拟化不能复用操作系统,耗费资源;开发和运维需要一个统一的沟通模式。另外在容器上运行的程序比在其他虚拟技术下运行的要更快(其中的原因是没有指令转换,也没有 Hypervisor 的监视)。
- 对软件及其依赖的标准化打包、应用间相互隔离、共享同一个OS内核、可以运行在不同的操作系统上。app层面的隔离。
开发环境搭建
安装虚拟机和Centos7
安装 vagrant
cd workspace
vagrant init centos/7
vagrant up --provider virtualbox
下载兼容的 virtualbox 版本
到 http://cloud.centos.org/centos/7/vagrant/x86_64/images/ 下载 centos7
vagrant box add centos/7 Documents/tools/CentOS-7-x86_64-Vagrant-2001_01.VirtualBox.box
vagrant up
vagrant ssh // 登陆
vagrant status // 查看虚拟机状态
vagrant halt // 停止虚拟机
vagrant destroy // 删除虚拟机
安装 Docker CE
https://docs.docker.com/install/linux/docker-ce/centos/
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo // 这里可以更换其他源
sudo yum install docker-ce
// 阿里、网易、腾讯等都有
sudo mkdir /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://xxx.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl enable docker // 开机自启动
sudo systemctl start docker // 在我们通过软件包的形式安装 Docker Engine 时,安装包已经为我们在 Linux 系统中注册了一个 Docker 服务,所以不需要启动dockerd,直接启动即可。
sudo docker run -d --name=demo hello-world // -it 交互式运行,例如 docker run centos /bin/bash。name也是唯一的,可以当作containerid用,如docker start demo
如果run后不能退出,可以新开一个terminal,登陆虚拟机,把不能退出的 container stop 掉
便利性提升
// 运行 docker 命令时省去 sudo
sudo groupadd docker
sudo gpasswd -a vagrant docker
sudo service docker restart
exit
vagrant ssh
// 安装 vim
sudo yum install -y vim
理论和基本命令
底层基础
- 命名空间:命名隔离
- CGroups控制组:资源隔离和分配
- 联合文件系统:挂载不同实际文件或文件夹到同一目录。(解决虚拟环境对文件系统占用过量,实现虚拟环境快速启停等问题。)
应用层基础
-
镜像:只读文件包,对镜像的修改只会形成新的镜像。镜像层,基于 UnionFS 文件系统的一组镜像层依次挂载而得,每一个镜像层单独拿出来,与它之下的镜像层都可以组成一个镜像。对容器运行环境进行持久化存储的结果。
Docker 的镜像我们必须通过 Docker 来打包,也必须通过 Docker 下载或导入后使用,固定的格式意味着我们可以很轻松的在不同的服务器间传递 Docker 镜像。
镜像层实现磁盘存储1+1<2的效果。
-
容器:隔离出的虚拟环境,由镜像、程序运行环境、指令集。
-
网络:docker能提供新环境的网络适配。
-
数据卷:方便文件系统目录的挂载、持久存储、数据共享
Docker Engine:本质是多个独立软件组成的软件包,核心包括:
- daemon:容器管理、应用编排、镜像分发等功能,或者成为 docker 服务。
- CLI:方便在控制台使用 docker 的 RESTful API,跟 daemon 就是 c 和 s 的关系。
docker版本:分为stable和edge。版本名由「年份.月份.bugfix」组成。目前 docker 主要基于 Linux kernel 3.10 以上版本,具体而言看书。
desktop版(更多介绍看书):
容器生命周期
- Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态。
- Running:容器正在运行,也就是容器中的应用正在运行。
- Paused:容器已暂停,表示容器中的所有程序都处于暂停 ( 不是停止 ) 状态。
- Stopped:容器处于停止状态,占用的资源和沙盒环境都依然存在,只是容器中的应用程序均已停止。
- Deleted:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都已释放和移除。
容器的启动,本质上是PID 为 1 这个进程的启动,停止也一样。
容器在创建和启动的过程中,不需要进行任何的文件系统复制操作,也不需要为容器单独开辟大量的硬盘空间,只有在容器中发生对文件的修改时,修改才会体现到镜像沙盒环境上。(启动的时候只需要指向镜像文件,如果有修改才会复制镜像文件中)
常用命令
# 查看当前连接的 docker daemon 中存放和管理了哪些镜像。Docker 只显示了镜像 ID 的64个的前12个字符,大部分情况下,它们已经能够让我们在单一主机中识别出不同的镜像了。
docker images
# 部分字段说明:镜像的命名由username(识别上传镜像的不同用户)、repository(镜像的表意描述) 和 tag(版本等细节)组成。
docker pull ${repository/repository:tag}
docker search ${keyword}
# 更详细的镜像信息
docker inspect ${repository/repository:tag} 或者 ${containerId}
# 删除镜像,共享的底层不会删
docker rmi ${repository/repository:tag}
docker rmi `docker images | awk '/^<none>/ { print $3 }'`
docker create --name ${custom_name} ${repository/repository:tag} # 处于 Created 状态
docker start ${custom_name}
# 可以将上面两步合并,但默认前台运行,所以习惯加 -d
docker run -d
# 罗列当前的容器,-a把不在running的也列出
docker ps -a
docker stop ${custom_name} # 维持的文件系统沙盒环境还是存在的,内部被修改的内容也都会保留
# 完全删除。当我们短时间内不需要使用容器时,最佳的做法是删除它而不是仅仅停止它。对于想保留数据,用数据卷更好。
docker rm ${custom_name}
# 在正在运行的容器中运行指定命令
docker exec ${custom_name} more /etc/hostname
# 进入容器的 bash,没有才用 sh。其中 -i ( --interactive ) 表示保持我们的输入流,只有使用它才能保证控制台程序能够正确识别我们的命令。而 -t ( --tty ) 表示启用一个伪终端,形成我们与 bash 的交互,如果没有它,我们无法看到 bash 内部的执行结果。
docker exec -it ${custom_name} bash
# 查看日志
docker logs ${custom_name}
k8s.gcr.io/defaultbackend-amd64:1.5
k8s.gct.io/defaultbackend-amd64
Docker 网络
容器的网络模型由下面三个重要概念组成。
- 沙盒提供了容器的虚拟网络栈,包括端口套接字、IP 路由表、防火墙等的内容。其实现隔离了容器网络与宿主机网络,形成了完全独立的容器网络环境。
- 网络可以理解为 Docker 内部的虚拟子网,网络内的参与者相互可见并能够进行通讯。Docker 的这种虚拟网络也是于宿主机网络存在隔离关系的,其目的主要是形成容器间的安全通讯环境。
- 端点是位于容器或网络隔离墙之上的洞,其主要目的是形成一个可以控制的突破封闭的网络环境的出入口。当容器的端点与网络的端点形成配对后,就如同在这两者之间搭建了桥梁,便能够进行数据传输了。
容器网络模型为容器引擎提供了一套标准的网络对接范式,而在 Docker 中,实现这套范式的是 Docker 所封装的 libnetwork 模块,该模块也提供统一接口。目前官方提供五种网络驱动:Bridge Driver、Host Driver、Overlay Driver、MacLan Driver、None Driver。
docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes mysql
# 容器连接,这样容器间的网络已经打通。只需要将容器的网络命名填入到连接地址中,就可以访问需要连接的容器。例如代码为 String url = "jdbc:mysql://mysql:3306/webapp",Docker 会将其指向 MySQL 容器的 IP 地址
docker run -d --name webapp --link mysql webapp:latest
# 只有容器自身允许的端口,才能被其他容器所访问。docker ps 中的 PORTS 就是暴露的端口。运行时可以通过 --expose 指定。
# 也可以用别名连接,database 作为别名,代码中写了 database,就会指向 mysql
docker run -d --name webapp --link mysql:database webapp:latest
当多个主机在同一子网里时,才能互相看到并进行网络数据交换。上面的操作之所以能够连通,是因为启动 Docker 服务时,它会为我们创建一个默认的 bridge 网络,而我们创建的容器在不专门指定网络的情况下都会连接到这个网络上。网络可以通过 docker inspect container
查看。
# 创建网络,-d指定网络驱动类型,individual是网络名称
docker network create -d bridge individual
docker network ls
# 在运行时指定网络。不同网络的容器无法连通。
docker run -d --name webapp --link mysql --network individual webapp:latest
# 指定端口映射 -p <ip>:<host-port>:<container-port>,ip默认时监听所有网卡,即0.0.0.0
docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12
管理和存储数据
容器的文件系统弊端:伴随容器生命周期,没有持久化;外部很难绕开容器访问数据。
解决方法:将宿主操作系统中,文件系统里的文件或目录挂载到容器中,实现容器内外共享这个文件。
挂载方式:
- Bind Mount 能够直接将宿主操作系统中的目录和文件挂载到容器内的文件系统中,通过指定容器外的路径和容器内的路径,就可以形成挂载映射关系,在容器内外对文件的读写,都是相互可见的。
- Volume 也是从宿主操作系统中挂载目录到容器内,只不过这个挂载的目录由 Docker 进行管理,我们只需要指定容器内的目录,不需要关心具体挂载到了宿主操作系统中的哪里。
- Tmpfs Mount 支持挂载系统内存中的一部分到容器的文件系统里,不过由于内存和容器的特征,它的存储并不是持久的,其中的内容会随着容器的停止而消失。
挂载文件到容器Bind Mount:
# -v <host-path>:<container-path>,必须使用绝对路径。指定目录进行挂载,也能够指定具体的文件来挂载。当挂载了目录的容器启动后,我们可以看到我们在宿主操作系统中的文件已经出现在容器中了。这里:ro是只读的意思
docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12
这种方式在权限允许的情况下能够挂载任何目录或文件,所以要注意外部目录的选择。在保证安全的前提下,这种方式适合:
- 从宿主操作系统共享配置,比如 /etc/timezone 这个时区配置;
- 方便开发,虽然在 Docker 中,推崇直接将代码和配置打包进镜像,但这种方式能够在外部直接修改代码。
挂载临时文件目录
--tmpfs,只需指定容器内目录。
方式场景:
- 应用中使用到,但不需要进行持久保存的敏感数据,可以借助内存的非持久性和程序隔离性进行一定的安全保障
- 读写速度要求较高,数据变化量大,但不需要持久保存的数据。
挂载数据卷
目录存放在 Docker 内部,接受 Docker 的管理。同样是 -v,但不指定宿主目录。可以通过-v <name>:<container-path>
指定卷的名字。如果数据卷不存在,Docker 会为我们自动创建和分配宿主操作系统的目录,而如果同名数据卷已经存在,则会直接引用。适合场景:
- 当希望将数据在多个容器间共享时,利用数据卷可以在保证数据持久性和完整性的前提下,完成更多自动化操作。
- 当我们希望对容器中挂载的内容进行管理时,可以直接利用数据卷自身的管理方法实现。
- 当使用远程服务器或云服务作为存储介质的时候,数据卷能够隐藏更多的细节,让整个过程变得更加简单。
# 让多个容器挂载同一个数据卷
docker run -d --name webapp -v html:/webapp/html webapp:latest
docker run -d --name nginx -v html:/usr/share/nginx/html:ro nginx:1.12
# 直接创建、查看、删除
docker volume create appdata
docker volume ls
docker volume rm appdata
docker rm $(docker container ls -f "status=exited" -q) // 删除掉所有已经退出的container
# 删除容器同时删除关联的卷
docker rm -v webapp
# 删除没有被引用的卷
docker volume prune
数据卷容器:一个没有具体指定的应用,甚至不需要运行的容器,我们使用它的目的,是为了定义一个或多个数据卷并持有它们的引用。主要是更轻松的实现容器的迁移,因为不在需要指定挂载的目录。
# 由于不需要容器本身运行,因而我们找个简单的系统镜像都可以完成创建。
docker create --name appdata -v /webapp/storage ubuntu
# 数据卷容器就可以算是容器间的文件系统桥梁。我们可以像加入网络一样引用数据卷容器,只需要在创建新容器时使用专门的 --volumes-from 选项即可。
docker run -d --name webapp --volumes-from appdata webapp:latest
备份和迁移数据卷:数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程。
# 先建立一个临时的容器,将用于备份的目录和要备份的数据卷都挂载到这个容器上。--rm 容器在停止后自动删除。后续就是主程序启动命令。
docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar cvf /backup/backup.tar /webapp/storage
# 恢复数据卷中的数据
docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip
另一种挂载选项
docker run -d --name webapp webapp:latest --mount 'type=volume,src=appdata,dst=/webapp/storage,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>' webapp:latest
保存和共享镜像
保存:Docker 镜像的本质是多个基于 UnionFS 的镜像层依次挂载的结果,而容器的文件系统则是在以只读方式挂载镜像后增加的一个可读可写的沙盒环境(沙盒的修改不是真的对镜像的修改,而是创建新的文件盖住镜像里的文件)。Docker 提供了讲这个沙盒环境持久化为镜像层的方法。
# 会先暂停容器,后面一个是镜像 tag 命名。但或许先创建一个新 dockerfile,从原 image 上进行修改再 commit 更好。直接 commit 的话,不知道这个 commit 在原 image 的基础上发生了什么。
docker commit -m "Configured" webapp webapp:2.0
# 镜像命名。对镜像进行命名后,虽然能够在镜像列表里同时看到新老两个镜像,实质是它们其实引用着相同的镜像层。
docker tag 0bc42f7ff218 webapp:1.0
迁移:
docker save -o ./webapp-1.0.tar webapp:1.0
docker load -i webapp-1.0.tar
# (可忽略)将镜像输出。默认定义下,docker save 命令会将镜像内容放入输出流中,这就需要我们使用管道进行接收 ( 也就是命令中的 > 符号 )
docker save webapp:1.0 > webapp-1.0.tar
docker load < webapp-1.0.tar
# commit 和 save 的结合
docker export -o ./webapp.tar webapp
docker import ./webapp.tar webapp:1.0
Dockerfile
-
FROM:当 FROM 第二次或者之后出现时,表示在此刻构建时,要将当前指出镜像的内容合并到此刻构建镜像的内容里。
FROM <image> [AS <name>] FROM <image>[:<tag>] [AS <name>] FROM <image>[@<digest>] [AS <name>]
-
RUN:执行命令并创建新的Image Layer。通过 && 和 反斜杠 合并为一行。不合并的话,每个 RUN 都会产生一个镜像层。但应该将易变和不易变的过程拆分,且不变的放前面,从而提高构建时使用 cache 的效率。缓存的判断逻辑是:1.所基于的镜像层是否一样;2.用于生成镜像层的指令的内容是否一样。
-
ENTRYPOINT 和 CMD(都不是 Dockerfile 必须的):ENTRYPOINT 指令主要用于对容器进行一些初始化,通常使用脚本文件来作为 ENTRYPOINT 的内容(结尾是
exec "$@"
,表示执行所有传入脚本的参数,即CMD),而 CMD 指令则用于真正定义容器中主程序的启动命令。启动容器中进程号为 1 的进程。当 ENTRYPOINT 与 CMD 同时给出时,CMD 中的内容会作为 ENTRYPOINT 定义命令的参数,最终执行容器启动的还是 ENTRYPOINT 中给出的命令。如果docker run时加上其他命令,则CMD会被忽略。一个Dockerfile里只有最后一个CMD生效。具体的比较例子看后面的截图。 -
EXPOSE:
<port> [<port>/<protocol>...]
-
VOLUME
-
COPY 或 ADD :能够帮助我们直接从宿主机的文件系统里拷贝内容到镜像里的文件系统中。COPY 与 ADD,两者的区别主要在于 ADD 能够支持使用网络端的 URL 地址作为 src 源,并且在源文件被识别为压缩包时,自动进行解压,而 COPY 没有这两个能力。虽然看上去 COPY 能力稍弱,但对于那些不希望源文件被解压或没有网络请求的场景,COPY 指令是个不错的选择。
ADD/COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
-
WORKDIR:改变当前路径,用绝对目录,不要用 run cd
-
ARG:参数变量,后续命令中可以引用。如
ARG C, RUN $C
。在构建时docker build --build-arg C=8 --build-arg B=8.0.53 -t tomcat:8.0 ./tomcat
-
ENV:环境变量,例如
ENV MYSQL_VERSION 5, RUN $MYSQL_VERSION
。影响范围比 ARG 要大,在运行的容器里,一样拥有这些变量。除了在文件中定义值外,也可以在执行构建命令时修改,如docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
ENV 指令所定义的变量,永远会覆盖 ARG 所定义的变量
例子
# Dockerfile
FROM python:2.7
LABEL maintainer="ly<xxxx@gmail.com>"
RUN pip install flask
COPY app.py /app/ # 先把源码上传到相应目录。不要漏掉第二个“/”,否则app.py就重命名为app
WORKDIR /app
EXPOSE 5000
CMD ["python", "app.py"]
# Dockerfile
FROM ubuntu
RUN apt-get update && apt-get install -y stress
ENTRYPOINT ["/usr/bin/stress"]
CMD []
// 上面这个 build 好后,docker run it xxx/xx -args,这样 args 就可以传到 CMD 中
构建
# build指定的是目录,从这个目录下寻找名为 Dockerfile 的文件
docker build ./webapp
# -t 命名,-f 指定 dockerfile 文件。--no-cache禁用缓存。
docker build -t webapp:latest -f ./webapp/a.Dockerfile --no-cache ./webapp
Docker仓库
Alpine:Linux 是一个相当精简的操作系统,对软件镜像进行改造,并基于其构建新的镜像,则还是用完整的 Linux 版本。
对容器进行配置:在镜像的详情里,一般会描述启动镜像的参数,例如mysql,通过 docker run --name mysql -e MYSQL_DATABASE=webapp -e MYSQL_USER=www -e MYSQL_PASSWORD=my-secret-pw -d mysql:5.7
就能完成启动、数据库建立、用户创建等工作。
Docker Compose(本地测试)
在单机上实现多个容器的统一部署。compose 不属于 Engine,需要另外下载,其实就是一个 python 程序。desktop 默认已经集成。
使用
先编写 compose.yml
docker-compose up -d
docker-compose down
# 指定 compose 文件
docker-compose -f ./compose/docker-compose.yml -p myapp up -d
# 查看日志。这里 nginx 是 yml 中定义的服务名,并非 container name,所以用 docker logs nginx 可能看不到。
docker-compose logs nginx
docker-compose create/start/stop
整体服务例子
目录设计
└─ project
├─ app #用于存放程序工程,即代码、编译结果以及相关的库、工具等;
├─ compose
│ └─ docker-compose.yml
├─ mysql
│ └─ my.cnf
├─ redis
│ └─ redis.conf
└─ tomcat
├─ server.xml
└─ web.xml
准备配置文件
这有三种方法:
-
借助配置文档直接编写
-
下载程序源代码中的配置样例
-
通过容器中的默认配置获得(下面以 tomcat 为例)
# 先创建临时的 tomcat 容器 docker run --rm -d --name temp-tomcat tomcat:8.5 # 对于 tomcat,经常改动的配置主要是 server.xml 和 web.xml docker cp temp-tomcat:/usr/local/tomcat/conf/server.xml ./server.xml docker cp temp-tomcat:/usr/local/tomcat/conf/web.xml ./web.xml # 清理临时容器,由于上面使用了 --rm,下面只需要 stop 就能自动删除 docker stop temp-tomcat
编写 docker-compose.yml
version: "3" # compose的版本
services:
redis:
image: redis:3.2
volumes:
- ../redis/redis.conf:/etc/redis/redis.conf:ro
- ../redis/data:/data
command:
- redis-server
- /etc/redis/redis.conf
ports:
- "6379:6379"
mysql:
image: mysql:5.7
volumes:
- ../mysql/my.cnf:/etc/mysql/my.cnf:ro
- ../mysql/data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: my-secret-pw
ports:
- "3306:3306"
tomcat:
image: tomcat:8.5
volumes:
- ../app:/usr/local/tomcat/webapps/ROOT
ports:
- "80:8080" # 把 Tomcat 默认的 8080 端口映射到了宿主机的 80 端口上,这样便于我们直接通过地址访问网站,不需要经常人工补充端口号了
volumes:
mysql-data: # 仅需要提供数据卷的名称
external: true # 如果要引入 compose 外的数据卷,从 Docker Engine 中已有的数据卷里寻找并直接采用
- conf 都是通过挂载的方式引入,这样是为了能从外部引入配置的变动。
- 还挂载了 data、log 相关的目录,这样能够在外部永久保存。
- 把代码或者编译后的程序挂载到容器中
但上述对线上部署都不适用。
微服务例子(等到k8s再解决)
需要在本地搭建起自己所开发服务的运行环境,再与其他开发者搭建的环境互联即可。通过 Overlay Network 实现跨物理主机的限制。
参考: