dcddc

西米大人的博客

0%

系统学习docker

概述

Docker 是基于 OS 层的虚拟化技术之上的容器引擎,实现对进程的封装隔离。开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上。

传统的虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需要的应用进程。实现资源隔离的方法是利用独立的 OS,并利用 Hypervisor 虚拟化 CPU、内存、IO 设备等实现的

docker 没有自己的内核,也没有硬件虚拟,是轻量级的虚拟化技术。docker 利用的是目前 Linux 内核本身支持的容器方式实现资源和环境隔离。简单的说,docker 利用 namespace 实现系统环境的隔离;利用 Cgroup 实现资源限制;利用镜像实现根目录环境的隔离。

Docker 基本概念

Docker Client:

  • Docker 提供给用户的客户端。Docker Client 提供给用户一个终端,用户输入 Docker 提供的命令来管理本地或者远程的服务器

Docker Daemon:

  • Docker 服务的守护进程。每台服务器(物理机或虚机)上只要安装了 Docker 的环境,基本上就跑了一个后台程序 Docker Daemon,它会接收 Docker Client 发过来的指令,并对服务器的进行具体操作

Docker file:

  • Dockerfile 是用来 build 镜像的,镜像可以用来传播,镜像运行后生成容器
    • (Dockerfile)build->(Image)ship->(Container/VM)run
  • docker daemon 在 docker build 阶段一行一行的执行 Dockerfile 中的指令,每一行指令执行完就 commit 一个镜像

Docker Images:

  • 俗称 Docker 的镜像,docker 镜像是基础加应用,是一个软件从最顶层一直到最底层系统库的完整依赖栈
  • Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变
  • Docker 镜像采用分层存储结构(AUFS)。每一层构建好后就不会在变,一层层构建,前一层是后一层的基础,由多层文件系统联合组成
  • bootFS(bootloader+kernel,linux 不同发行版,底层 bootFS 共用)&& rootFS(不同 OS 发行版的文件系统),区别只是 rootFS,且镜像用的是极简版的,所以操作系统 docker 镜像大小,例如 CentOS,远小于实际 CentOS 镜像大小

Docker Registry:

  • Docker Images 的仓库,就像 git 的仓库一样,用来管理 Docker 镜像的,提供了 Docker 镜像的上传、下载和浏览等功能
  • 每个仓库可以包含多个标签(Tag),每个标签对应一个镜像
    • 例如集团使用的应用自动化构建发布平台 aone,所有在 aone 构建的应用镜像都放在 aone 仓库里,标签用于区分不同镜像,一般命名为:appName_${环境名},例如线上是:appName_production

Docker Container:

  • 俗称 Docker 的容器,是真正跑项目程序、消耗机器资源、提供服务的地方。Docker Container 通过 Docker Images 启动,在 Docker Images 的基础上运行你需要的代码。你可以认为 Docker Container 提供了系统硬件环境,然后使用了 Docker Images 这些制作好的系统盘,再加上你的项目代码,跑起来就可以提供服务了

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等

docker 实践

安装:https://docs.docker.com/engine/install/centos/

  • 注意国内要设置阿里云镜像地址,加速 docker 下载过程

docker 命令:https://docs.docker.com/reference/

搜索 docker 镜像:https://hub.docker.com/

docker 网络

docker0 网关

docker 会在安装 docker 的宿主机上自动创建一个 docker0 网关(ip=x.x.0.1/16),桥接到宿主机的物理网卡

每次启动一个 docker 容器,docker 会使用 veth-pair 技术分配一对虚拟网络地址,一个给 docker 容器使用,一个给宿主机使用。给 docker 容器分配的虚拟网络是带 docker0 网关的子网 ip 的,给宿主机分配的虚拟网络只有 mac 地址

宿主机分配的虚拟网络相当于宿主机在 docker0 网关内的一个虚拟地址。当宿主机需要和 docker 容器通信时,先通过 veth-pair 生成的虚拟网络和 docker0 网关通信,网关再转发给 veth-pair 配对的 docker 容器子网 ip

  • veth-pair 技术用来实现虚拟网络间的通信,这个例子中就是宿主机和 docker 容器的通信

docker 容器间也可以通信,这很好理解,docker0 网关内部的子网之间当然可以通过 docker0 网关的转发实现通信(桥接模式,docker0 网关相当于一个网桥)

当 docker 容器删除后,veth-pair 分配的这一对虚拟网络也会删掉

自定义网络

创建 docker 容器默认使用 docker0 网关,但 docker0 网关有一个弊端,容器间不能通过容器名通信,只能通过 ip。这就导致容器一旦重启分配新的 ip,就 ping 不通了。为了实现基于容器名通信,可以创建自定义网络代替 docker0 网关:docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet

  • 网络类型也是网桥(网关),和 docker0 一样

启动容器时通过--net mynet指定容器加入自定义 docker 网络(默认 docker0)

网络连通

假设有 2 个 docker 网络,因为网关不同,分属不同网段,理论上这两个网络内的 docker 容器之间是不能通信的

不过如果要实现容器间跨 docker 网关通信,可以将容器 A 加入到另一个 docker 网络(网关),这样容器 A 就能和加入的 docker 网络内所有容器通信。原理很简单,容器加入另一个网络本质上就是给容器在另一个网络分配一个子网 ip

命令:docker network connect mynet mycontainer

docker 的优势

Docker 在运维上的优势可以类比 Java 语言:

  • Java 可以一次编译到处运行,Docker 实现了一次构建(镜像),到处运行(整个应用及其依赖)
  • Java 的 JVM 屏蔽了不同 OS 的差异,Docker 的 Daemon 屏蔽了不同 OS 发行版,不同机器的环境差异
  • Java 的字节码只要有 JDK 环境就能跑起来,且行为一致,Docker 的镜像只要有 Daemon 环境就能跑起来,且行为一致。两者内部都有清晰定义的格式,且自成规范
  • Java 使得开发者不需要针对不同的 OS 编程和编译,只要在任意一个 JDK 环境中编程和编译,即可交付可运行于任意 JDK 环境的 jar 包;Docker 使得开发者不需要针对不同的 OS 打不同的发行包,只要在任意一个 Daemon 环境中打包调试好,就可以交付可运行于任意 Daemon 环境的镜像

类比 Java 微服务架构,一个应用一个 jar 包,连 tomcat 都打包在 jar 中。只要有 jdk 环境就能一键跑起来。而不像之前除了 jdk 之外还要有 web 容器;开发者从交付一个 war 包,到交付一个包含 web 容器的 jar,打包了 jvm 之上的所有依赖栈。这显然是在 Java 的世界里借鉴了 Docker 的思想,微服务的 jar 包就是 jvm 之上的镜像。Docker 镜像更进一步是打包了内核之上的所有依赖栈,开发者从交付一个代码包到交付一个包括所有依赖栈的镜像。可见从部分交付到整体交付是一种趋势

Dockerfile 中常用指令

  • FROM:指定基础镜像,必须是第一个指令
  • ENV:指定运行时的系统环境变量
  • COPY:可以从 context 中拷贝文件或目录到镜像中
    • context 即 dockerFile 所在路径下,一般存放一些应用的启动、自检脚本和环境配置
  • RUN:执行任意 shell 指令
  • ENTRYPOINT:容器启动时自动执行的脚本,我们一般会将应用启动脚本放在这里,相当于系统自启应用
  • VOLUME:指定容器内的目录挂载到宿主机上面,为了能够保存(持久化)数据以及共享容器间的数据,为了实现数据共享,例如日志文件共享到宿主机或容器间共享数据
  • USER:用指定用户执行这条指令后面的所有指令

docker build 的执行过程

docker build 用于构建镜像:

  • 读取 Dockerfile 文件发送到 docker daemon
    • docker 是 C/S 架构,docker 客户端发送 Dockerfile 到服务端 docker daemon
  • 读取当前目录的所有文件(docker build 的 context),打成 tar 包,发送到 docker daemon
    • 因此不要将 Dockerfile 放到你项目根目录
  • 对 Dockerfile 进行解析,处理成命令加上对应参数的结构
  • 按照顺序循环遍历所有的命令,对每个命令调用对应的处理函数进行处理
    • 一定包含一个 COPY 命令,把构建出的应用主包(压缩的 tgz 包)复制到镜像指定目录下,所以在 docker build 前,应用需要先编译构建完成
  • 每个命令(除了 FROM)都会在一个容器执行,执行的结果会生成一个新的镜像,为最后生成的镜像打上标签

加速构建镜像的技巧

  • 把不常变化的部分打成一个基础镜像上传到仓库,然后其它的 Dockerfile FROM 这个基础镜像
    • 构建机会从 docker 仓库 pull 基础镜像,当构建机上的基础镜像已是最新时,不会重拉基础镜像(类似 git pull)。这样每次打包就只执行变化的部分,不需要把整个镜像都 build 一遍
    • 一般不变的部分是 OS 基础镜像,应用依赖的 rpm 包,应用容器配置,web 服务器配置等,这些往往写在基础镜像的 Dockerfile 里
    • 变化的部分一般是应用主包这些,放在日常、预发、线上环境有各自的 Dockerfile,它们都 FROM 共同的基础镜像,这样能加速各个环境下的镜像构建过程
    • 其实各个环境下的 dockerFile 即使不 From 基础镜像,在 build 时会通过命令的 md5 从缓存取镜像(镜像是分层存储的,每个命令执行完都对应一层镜像),也避免了重复构建
      • 不过一旦从 md5 取不到缓存,后面的命令都用不到 cache,因此需要保证将不变的镜像构建指令放到前面,把每次都变化的部分(例如应用主包复制)放到后面
  • dockerFile 目录下增加.dockerignore 文件
    • 格式和.gitignore 文件一样,排除不需要加入 docker build context 中的文件来加快 build 速度
  • 避免安装不需要的包到镜像中
    • 有很多基线中都写了不需要的包,应用开发需要确定一下哪些包是确实需要的,不要把安装不需要的包
  • 减少镜像层
    • 可以把多个指令合并成一个指令来减少镜像层

容器生命周期中用户的可定制点

  • 修改 dockerFile
  • 指定应用启动、自检、停止时的执行脚本
    • 脚本放在 dockerFile 上下文路径下
    • dockerFile 里重定向上下文路径里的启动、自检、停止脚本到 docker 镜像指定目录