Docker生命周期:Build,Ship and Run

摘要

本文部分内容来源于网络,大部分内容原创

“Build,Ship and Run” 是docker最早的一句宣传语,讲的是docker的整个生命周期。

Build:指的是如何创建镜像
Ship:指的是镜像如何流转
Run:指的是镜像如何运行(进入运行态)

Docker生命周期:Build,Ship and Run

“Build,Ship and Run” 是docker最早的一句宣传语,讲的是docker的整个生命周期。

  • Build:指的是如何创建镜像
  • Ship:指的是镜像如何流转
  • Run:指的是镜像如何运行(进入运行态)
    docker的整个生命周期实际上就是镜像的流转和状态的改变。

众所周知docker最核心的是容器技术,那么在docker中容器技术的是什么?镜像,而container仅仅只是容器(镜像的运行态),镜像才是docker容器技术的核心。

  • Docker生命周期的流转基本都是通过镜像来流转的
  • 容器是基于镜像启动的,没有镜像也就没有了容器
  • 镜像采用分层技术
  • 通过CoW技术让容器可以共享镜像的文件系统,确保底层数据不丢失

容器技术

  • 又称为容器虚拟化
  • 是一种操作系统虚拟化技术(与其他虚拟化差异比较大)
  • 轻量级虚拟化技术
  • linux内核支持

什么是容器技术(普遍认可的说法)

  • 首先要有一个相对独立的运行环境
  • 最小化其对外界的影响
  • namespace(命名空间):访问隔离
  • cgroup(控制组):资源控制

容器与持久化数据

为了让Docker生命周期更灵活的run起来,容器的运行层似乎应该是可被丢弃的,那么就要求容器不要在本地生成持久化数据,又或者如何将原本要写在本地的持久化数据转义到可靠的存储位置。

容器的持久化数据

容器技术发展历史

Docker为什么脱颖而出

  • 开源
  • 定义了镜像的概念

创建镜像:Build

build一个image

镜像基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看镜像
docker image ls [-a]
# 搜索镜像、下载镜像
docker search|pull image-name
# 创建镜像
docker build|commit
# 导出、导入镜像
docker save|load
# 导出容器、导入生成镜像
docker export|import
# 删除镜像
docker rmi
# 深入镜像
docker inspect image-name
# 查看镜像构建历史
docker history imageid
# 查看镜像开放端口
docker port image-name
# 创建新的镜像标签
docker tag old-image new-image

容器是怎么变成镜像的

docker在发布初期就提供了将容器变成镜像的方法:

  • 通过基础镜像创建容器
  • 然后登陆容器像普通的操作系统一样手动部署环境
  • 然后将容器打包生成镜像
1
2
3
docker commit -p container-id image-name
docker save -o /data/image-name.tar image-name
docker load -i image-name.tar

但是这种方式生成的镜像,所有操作都在一个镜像层中,不利于后期的维护、管理。

Dockerfile

原理同样是将容器变成镜像,通过配置文件实现了镜像的自动创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 一个简单的Dockerfile
#################
# Version: 0.0.1
FROM ubuntu:16.04
RUN apt update
#RUN apt installedl nginx -y
RUN ["apt", "install", "-y", "nginx"]
RUN echo "Hi, I am in your container" \
> /usr/share/nginx/html/index.html
EXPOSE 80
ENTRYPOINT nginx
#################
# 构建镜像
docker build -t "base/nginx:custom" .

# 使用新镜像启动容器
docker run -d -p 8080:80 --name myweb base/nginx:custom -g "daemon off;"

CMD

1
2
3
4
5
构建镜像后,情动容器默认执行的命令,如初始化环境,可以在run的时候被覆盖
CMD只能存在一个,如果多个,会运行最后一个
CMD ["/bin/ps"]

docker run -it ooppwwqq0/sa:v2 /bin/bash

ENTRYPOINT

1
2
3
4
5
6
与CMD命令有些像,但是默认不能被覆盖一般与CMD配合使用,可以配合docker run 传递参数
ENTRYPOINT ["/usr/sbin/nginx"]

CMD ["-h"]

如果想覆盖可以使用--entrypoint来覆盖

WORKDIR

1
2
3
4
5
6
7
8
9
可以用来切换目录,限定一些指令在目录下执行,也可以为最终的镜像设置工作目录

WORKDIR /opt/webapp/db
RUN touch filename
WORKDIR /opt/webapp
ENTRYPOINT ["pwd"]

可以使用-w选项覆盖
docker run -it -w /data/logs ubuntu pwd

ENV

1
2
3
4
可以在镜像构建过程中设置环境变量,可以延续到以此镜像为基础的容器中,也可以用-e选项来传递环境变量

ENV WEB_PATH /home/web
WORKDIR $WEB_PATH

USER

1
2
3
4
指定启动的镜像使用的用户,默认是root
USER user:group
USER uid:gid
USER user:gid

VOLUME

1
2
3
4
5
指定容器启动的时候添加卷,一个卷可以存在一个或多个容器内的特定目录里面,可以实现将数据或者代码添加到镜像中而不是提交到镜像中,并且允许多个容器共享

VOLUME ["/opt/project", "/data"]
前面的宿主机地址挂载到后面容器的地址,后面不写会自动创建和前面一样的路径并挂载
-v选项在run的时候可以自己指定

ADD

1
2
3
4
5
6
用来将构建上下文中的文件或目录复制到镜像中

ADD conf.php /opt/app/conf/conf.php
源文件的路径可以是一个url,不能对构建上下文外的文件进行操作.
Docker通过目标地址的结尾字符判断文件源是文件还是目录,“/”结尾则源是目录,否则认为是文件。
如果源文件在本地是压缩包,如gzip、bzip2、xz,ADD可以解压到指定目录

COPY

1
2
3
4
COPY与ADD比较像,但是COPY不会做解压工作,只是单纯的拷贝。原路径要在构建上下文中,目标路径必须是一个绝对路径,如果目标目录不存在,会自动创建mkdir -p

COPY conf.d/ /etc/apache/
如果原路径是目录,那个整个目录都会拷贝到目的目录中。

ONBUILD

1
2
3
4
5
ONBUILD指令能为镜像添加触发器,当一个镜像被其他镜像用作基础镜像时,该镜像的触发器会被执行(如你的镜像需要从未准备好的位置添加代码,或者你需要执行特定于构建镜像的环境的构建脚本)
触发器会在构建过程插入新指令,我们可以认为这些指令是紧跟在FROM之后的,触发器可以是任意指令。

ONBUILD ADD . /app/src
ONBUILD RUN cd /app/src && make

镜像有哪些普遍存在的问题?

  • 镜像层级(127层)
    • 层级过多和过少都不利于维护和管理
  • 镜像大小
    • 镜像太大会给大量传输带来压力
  • 镜像传输
    • 如果镜像不可避免的过大,难免会对仓库和传输速度带来压力
  • 镜像构建效率
    • 构建镜像的操作负责将会影响镜像的构建效率

应该创建什么样的镜像

由于Dockerfile在创建镜像时每一条命令都会生成一层镜像,即使内容相同也会生成不同的镜像,下面的两个Dockerfile虽然内容一样,但是会生成镜像的镜像层也是不一样的。

1
2
3
4
FROM centos:lastest

RUN echo "hello" > /hello.txt
RUN echo "test" > /test.txt
1
2
3
4
FROM centos:lastest

RUN echo "test" > /test.txt
RUN echo "hello" > /hello.txt
  • 在基础镜像选择上尽量选择合理尺寸的镜像
  • 尽量减少镜像的层级
  • 尽可能删除不需要的组件和不需要的文件以减少镜像的尺寸
  • From最好不要用lastest标签,避免不同镜像的顶层是不同,从而无法复用
  • 不变或者变化很少的体积较大的依赖库和经常修改的自有代码分开;尽量放在上层
  • 尽量将公用部分做成基础镜像
  • 过多的层级、过细的分层不利于维护和管理
  • 尽量避免单层过大
  • 因为cache缓存在运行Docker build命令的本地机器上,建议固定使用某台机器来进行Docker build,以便利用cache。

多阶段构建

将一个Dockerfile用于开发(其中包含构建应用程序所需的所有内容)以及一个用于生产的瘦客户端,它只包含你的应用程序以及运行它所需的内容。这被称为“建造者模式”。

Docker 17.05.0-ce版本以后支持多阶段构建。使用多阶段构建,你可以在Dockerfile中使用多个FROM语句,每条FROM指令可以使用不同的基础镜像,这样您可以选择性地将服务组件从一个阶段COPY到另一个阶段,在最终镜像中只保留需要的内容。

1
2
3
4
5
6
7
8
9
10
11
FROM alpine AS builder
WORKDIR /go/
COPY . .
WORKDIR /go/
RUN echo "xxxxx" > a.txt
RUN mv a.txt /root

FROM scratch
WORKDIR /root/
COPY --from=builder /root .
EXPOSE 8080

一些好用的基础镜像

  • alpine: 小巧的基础镜像(基础服务运行的利器)
  • busybox:只包含busybox工具的镜像
  • scrtch:离线空镜像(运行二进制bin文件的利器)

不同业务场景的镜像解决方案

业务场景 镜像选择 构建方式 部署方式
二进制bin文件 构建:alpine
运行:scrtch
多级构建 镜像部署
有环境依赖;无系统依赖;环境无系统依赖 alpine 先构建基础镜像
然后构建环境镜像
最后构建业务镜像
镜像部署 or 原地升级
有环境依赖;有系统依赖 选择需要依赖的系统镜像 同上 同上
有系统依赖;无环境依赖 同上 先构建基础镜像
然后构建业务镜像
同上

镜像流转:Ship

如何让镜像ship起来

  • register
  • harbor
  • harbor集群

镜像的导入导出

image可以通过docker save|export等方式导出一个镜像tar包,通过网络传输的方式将tar包传输到需要的位置,然后通过docker load|import将tar包中的镜像导入。

Registry

镜像中心是docker官方提供的镜像存储仓库,它在docker技术发展初期给了image能够更加便捷的ship起来,但是随着docker生态的不断发展,用户对镜像管理、使用的需求,以及周边设施的完善,Registry能够提供的支持就显得有些不够了。而在这个时候docker又一次受到了来自友商的重击:Harbor

Register搭建

Harbor

Habor是由VMWare公司开发、开源的容器镜像仓库,它是在Docker Registry的基础上进行了相应的企业级扩展,从而获得了更加广泛的应用,这些新的企业级特性包括:

  • 简单的搭建方式
  • 友好的用户管理界面
  • 基于角色的访问控制
  • 提供了更好的性能和漏洞扫描等安全功能
  • 提升用户使用Registry构建和运行环境传输镜像的效率
  • 日志审计
  • 多个Register之间的镜像同步功能
  • 提供分层传输机制,优化网络传输
  • 支持水平扩展,多种集群化方案,分担仓库压力

Harbor镜像仓库搭建

镜像同步

为什么要做镜像同步

  • 容器规模庞大,需要多个镜像仓库分担压力
  • 系统稳定性要求,多个仓库的集群提高系统健壮性
  • 镜像仓库分级规则,上下级仓库依赖
  • 镜像在不同环境的自动流转(如:开发环境上传镜像->流转到测试仓库->流转到线上仓库)

Harbor集群

主从同步

基于Harbor提供的镜像复制功能可以搭建多级别的主从复制集群,可以用来提高系统健壮性、分担仓库压力等。但是只配置主从集群还是解决不了仓库单点的问题。

双主复制

基于Harbor的镜像同步功能在两个节点间做双向同步(多节点同步待测试)来确保数据一致性,这样不需要考虑后端服务的维护成本,通过负载器对仓库节点做负载分发。

但是如果其中一个节点A挂掉,在此期间节点B产生了新的数据,节点A恢复后可能不会去同步数据(需要测试),而且双向同步势必会有一定的延迟,当需要同步的数据量过大时,延迟也一定会被放大。

  • 优点:不增加后端服务维护成本
  • 缺点:可能会出现数据不一致的问题、数据同步可能延迟

负载集群

负载集群需要多个仓库节点共享后端数据服务,如:

  • 存储
  • 数据库
  • redis

然后通过负载器对多个仓库节点做负载分发。但是基础服务维护成本过大。

  • 优点:可靠的负载集群
  • 缺点:维护成本大

容器运行:Run

让容器run起来

容器基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看容器
docker ps [-a]
# 删除容器
docker rm
# 启动一个容器
docker run [-it|-d] --name xxx img_name [cmd]
# 在运行的容器内运行一个新的任务
docker exec -it con_name [cmd]
# 启动、停止容器
docker start|stop con_name
# 查看容器日志
docker logs [-ft] con_name
# 查看容器性能
docker top|stats con_name
# 深入容器
docker inspect con_name
# 从容器生成镜像
docker commit
# 导出容器
docker export
...

Run

通常来说我们在生产环境一般不会运行一个没有任何限制的容器(特殊情况除外),容器运行的时候我们会将一些基础配置和资源限制加到启动参数里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker run -d --name web \
--network host \
--cpu-shares 512 \
--env Name=web01 \
--health-cmd /home/work/health.sh \
--health-interval 1s \
--health-retries 3 \
--health-start-period 1s \
--health-timeout 1s \
--label env=prod \
--label name=web01 \
--memory 4096M \
--publish 8080:80 \
--restart on-failure:3 \
--user work \
--volume data:/data \
nginx:alpine

这么一大堆参数放到命令行来执行不好维护的同时又不够优雅

docker-compose