镜像结构
容器运行是依赖于镜像文件的,每次运行的时候都需要通过镜像来创建一个容器实例运行。一般一个镜像都是多个层次文件系统组成的(Union FS
),镜像里面包含了各种程序所需依赖的环境,将其打包成一个完整的镜像,组织结构如下:
从基本的看起一个典型的Linux
文件系统由bootfs
和 rootfs
两部分组成,bootfs(boot file system)
主要包含bootloader
和 kernel
,bootloader
主要用于引导加载 kernel
,当 kernel
被加载到内存中后bootfs
会被umount
掉,rootfs (root file system)
包含的就是典型 Linux
系统中的/dev
、/proc
、/bin
、/etc
等标准目录和文件。
传统Linux
启动流程是先加载bootfs
时会把rootfs
设置为read-only
,然后在系统自检之后将 rootfs
从 read-only
改为 read-write
,然后我们就可以在 rootfs
上进行读写操作了。
但Docker
在bootfs
自检完毕之后并不会把 rootfs
的read-only
改为read-write
,而是利用union mount
(UnionFS
的一种挂载机制)将image
中的其他的layer
加载到之前的 read-only
的rootfs
层之上,每一层layer
都是rootfs
的结构,并且是read-only
的,这样就能达到无法修改某个layer
作用了,容器和镜像关系如下图:
镜像构建
通常我们写的程序需要打包成镜像,环境信息也要配置到镜像里面,Docker
官方就提供一个叫Dockerfile
的文件,该文件为一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明,构建一个镜像流程如下:
首写Docker
会根据我们编写的Dockerfile
文件配置信息,然后通过docker build -t xxx:latest .
生成对应镜像,然后我们就可以根据镜像运行程序了。
下面是我编写一个Java
的SpringBoot
程序代码如下:
package me.ibyte.java.webservice.controller;
import me.ibyte.java.webservice.model.User;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
/**
* @Author: LeonDing <br/>
* @Date:2022/20-6:47 PM <br/>
* <p>
* 首页控制器。
* </p>
*/
@RestController
@RequestMapping("/")
public class IndexController {
private final User[] userRepository;
public IndexController() {
this.userRepository = new User[]{
new User(1001, "Leon Ding", 22),
new User(1002, "Tom Sun", 21),
new User(1003, "John Li", 33),
new User(1004, "Jay Wang", 18),
};
}
@ResponseBody
@GetMapping("/user/{id}")
public Message UserController(@PathVariable Integer id, HttpServletResponse rsp) {
for (int i = 0; i < this.userRepository.length; i++) {
if (id.equals(this.userRepository[i].getID())) {
return new Message(200, this.userRepository[i]);
}
}
rsp.setStatus(500);
return new Message(500, null);
}
public class Message {
public Integer code;
public User data;
public Message(Integer code, User data) {
this.code = code;
this.data = data;
}
}
}
然后使用maven
打包成一个jar
包,然后我们编写运行这个程序所需要的运行环境的Dockerfile
:
# 基础镜像
FROM openjdk:11
# 设置环境变量
ENV VERSION=0.0.1
# 映射端口
EXPOSE 8080
# 在构建时的临时变量
ARG JAR_FILE=./webservice-0.0.1-SNAPSHOT.jar
# 将文件移动到镜像文件夹下面
ADD ${JAR_FILE} webservice.jar
# 运行启动Jar
CMD java -jar ./webservice.jar
上面信息就是运行一个基础的Java
程序环境配置,docker
会根据我们编写环境配置清单来配置运行环境。
上图为根据Dockerfile
构建镜像,下图为构建镜像过程的细节:
运行镜像访问我们编写的访问截图如下:
配置参数说明:
参数 | 作用 |
---|---|
FROM | 基础镜像 |
ENV | 设置容器内的环境变量 |
ADD | 会将目标文件应到对应目录下,如果是压缩包还能自动解压 |
CMD | 用来执行shell 命令,但是在RUN 时执行 |
RUN | 在构建镜像的时候执行的 |
COPY | 从上下文目录中复制文件或者目录到容器里指定路径 |
ENTRYPOINT | 类似于CMD ,在docker run 的时候如果有参数,那么这些参数将会被当作参数送给ENTRYPOINT 指令指定的程序 |
ARG | 在构建镜像时候的环境变量 |
VOLUME | 自动挂载数据卷到容器映射 |
EXPOSE | 映射端口 |
WORKDIR | 设置工作目录 |
还有一些其他不常用的参数配置,可以自行查阅官方文档。
容器编排
当我们项目变大的时候,我们的程序可能就需要不同模块来构建我们的系统,例如我们一个系统有数据库有缓存有消息队列。如果是微服务项目并且项目之间都有依赖关系,启动都有关系的,平时一两个项目模块还好,如果项目模块太多了,那么我们手动构建软件镜像再启动容器就很麻烦,为此Docker
官方也提供一个了工具叫Compose
就是来做容器管理的,Compose
是用于定义和运行多容器 Docker
应用程序的工具。通过 Compose
,您可以使用 YM
L 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML
文件配置中创建并启动所有服务。
通常我们一个服务程序环境如上,有业务代码程序,还有缓存,还有数据库,分别对应不同镜像。
version: "3"
services:
login-service:
image: registry.gitlab.com/auula/login:latest
restart: always
ports:
- "8080:8080"
depends_on:
- database
- cache-redis
cache-redis:
image: "redis:alpine"
ports:
- "6379"
depends_on:
- database
database: # 名称,它也是 network 中 DNS 名称
image: mysql:5.7 # 镜像,如果像自定义镜像可以不指定这个参数,而用 build
volumes: # 定义数据卷,类似 -v
- db_data:/var/lib/mysql
restart: always # 类似 --restart
# 'no' 默认,不自动重启,以为 no 是 yaml 关键字所以加引号
# always 总是自动重启
# on-failure 当失败时自动重启,也就是 exit code 不为 0 时
# unless-stopped 除非手动停止,否者一直重启
environment: # 定义环境变量,类似 -e
MYSQL_ROOT_PASSWORD: xxx
MYSQL_DATABASE: xxx
MYSQL_USER: xxx
MYSQL_PASSWORD: xxx
其实docker-compose
所做的事情就是把我们平时敲的命令组织成一个yaml
文件来管理,并且还可以配置启动顺序,来帮助管理容器启动时候的参数和服务之间关系,来构建部署项目。