Docker的简介
Docker是一个开放平台,使开发人员和管理员可以在称为容器的松散、隔离的环境中构建镜像、交付和运行分布式应用程序,以便在开发、qa和生产环境之间进行高效的应用程序生命周期管理。
容器是一个应用层抽象,用于将代码和依赖资源打包在一起。多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行。与虚拟机相比,容器占用的空间较少(容器镜像大小通常只有几十兆),瞬间就能完成启动。
虚拟机则是一个物理硬件层抽象,用于将一台服务器变成多台服务器。管理程序允许多个vm在一台机器上运行。每个vm都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此占用大量空间,启动也十分缓慢。
容器与虚拟机对比如下图所示:
Docker的构成
-
Docker仓库:https://hub.docker.com
-
Docker自身组件
Docker Client:Docker的客户端
Docker Server:Docker daemon的主要组成部分,接受用户通过Docker Client发出的请求,并按照相应的路由规则实现路由分发
Docker镜像:Docker镜像运行之后变成容器(docker run)
Docker的基本组成
- 镜像
Docker 镜像(Image)就是一个只读的模板(一个特殊的文件系统)。镜像可以用来创建 Docker 容器,一个镜像可以创建很多容器。
操作系统分为内核和用户空间。对于Linux而言,内核启动后,会挂载root文件西戎为其提供用户空间支持。Docker镜像就相当于一个root文件系统。
Docker镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的配置参数(如匿名卷、环境变量、用户等)。
镜像不包含任何动态数据,其内容在构建之后也不会被改变。可以使用“docker image ls”来列出本机的镜像。
在设计时,充分利用UnionFS的技术,将Docker设计为分层存储的架构。镜像实际是由多层文件系统联合组成的。
镜像构建时会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。
比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。
在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。
因此,在构建镜像的时候需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。
分层存储的特征还使得镜像的复用、定制变得更为简单,甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容来构建新的镜像。 - 容器
Docker 利用容器(Container)独立运行的一个或一组应用,应用程序或服务运行在容器里面,容器就类似于一个虚拟化的运行环境,容器是用镜像创建的运行实例。就像是Java中的类和实例对象一样,镜像是静态的定义,容器是镜像运行时的实体。容器为镜像提供了一个标准的和隔离的运行环境,它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。 - 仓库
仓库(Repository)是集中存放镜像文件的场所。仓库分为公开仓库(Public)和私有仓库(Private)两种形式。
最大的公开仓库是 Docker Hub(https://hub.docker.com/),存放了数量庞大的镜像供用户下载。国内的公开仓库包括阿里云 、网易云等。
通常,一个仓库会包含一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。可以通过“<仓库名>:<标签>”的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,就以latest作为默认标签。
docker镜像文件类似于Java的类模板,而docker容器实例类似于java中new出来的实例对象。
镜像的特性
当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。
Docker的主要应用场景
- 简化和标准化代码流水线,助力敏捷开发和DevOps实践
Docker镜像一次构建,即可到处运行,无须处理复杂的环境以来关系,可以极大地简化珍哥哥代码流水线,并且可以标准化整个CI(持续集成)、CD(持续部署)流程,因此可以大大加速软件的开发进程,提高团队的敏捷性。
一个简单、可供参考的基于Docker的代码流水线如下图所示:
其他的应用场景,大家可以自行参考资料。
Docker网络管理
无论是在Windows还是Linux中,当Docker安装完成后,都会在宿主机中创建一个虚拟网桥,通过该网桥实现容器之间以及容器与外部网络的连接。
网桥工作在OSI七层模型中的数据链路层。网桥是早期的两端口二层网络设备,用来连接不同的网段。网段就像一个中继器,中继器从一根网络电缆中接收信号,放大它们,再将其送入下一根电缆。网桥将网络中的多个网段在数据链路层连接起来。
在Docker中,各个容器是通过一个名为docker0的虚拟网桥实现互联的。该虚拟网桥可以设置ip地址,相当于一个隐藏的虚拟网卡。
Docker守护进程在一个容器中启动时,实际上它要创建网络连接的两端。一端在容器中的网络设备,而另一端是在运行Docker守护进程的主机上打开一个名为veth*的一个接口,用来实现docker0这个网桥与容器的网络通信。
网络模式
目前,docker支持4种网络模式,网络模式可以在创建容器时使用–network选项来指定。
- host模式
docker使用了Linux的命名空间来进行资源隔离,如PID命名空间隔离进程、Network命名空间隔离网络、Mount命名空间隔离文件系统等。一个Network命名空间提供一份独立的网络环境,包括网卡、路由、iptable规则等都与其他的network命名空间隔离。一个Docker容器一般会分配一个独立的Network命名空间。但是,如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的network命名空间,而是和宿主机共用一个Network命名空间。容器将不再虚拟出自己的网卡、配置自己的IP地址等,而是使用宿主机的ip和端口。
- container模式
指定新创建的容器与已经存在的容器共享一个Network命名空间,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的IP地址,而是和一个指定的容器共享IP地址,端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程等还是隔离的。两个容器的进程可以通过共享的网卡设备通信。 - none模式
docker容器拥有自己的Network命名空间,但是并不为Docker容器进行任何网络设置。也就是说,这个Docker容器没有网卡、IP地址、路由等信息,需要自己为docker容器添加网卡、配置IP等。 - bridge模式
docker默认的网络设置,此模式会为每个容器分配Network命名空间、设置IP地址等,并将一个主机上的docker容器连接到一个虚拟网桥上。
Docker容器的互联
由于同一个主机中所有的容器都连接在同一个虚拟网桥docker0上,因此在默认情况下,同一个主机中的容器之间是可以互联的。
我们启动两个centos容器,然后互相ping ip,验证网络是否能通。
Docker数据管理
为了能够保存(持久化)数据以及共享容器之间的数据,Docker提出了Volume的概念。简单来说,Volume就是目录或文件,它可以绕过默认的联合文件系统,而以正常的文件或目录的形式存在于宿主机中。
数据卷
Docker镜像由多个文件系统(只读层)叠加而成,当启动一个容器时,Docker会加载只读层镜像并在其上(镜像栈顶部)添加一个读写层。如果已经运行的容器修改了现有的文件,那么会从读写层下面的只读层复制到读写层,该文件只读层依然存在,只是已经被读写层中该文件的复制副本所隐藏。当删除docker容器或重新启动时,之前的修改将丢失。在dockers中,只读层以及在顶部的读写层组合被称为联合文件系统。
数据卷是为一个或多个容器专门指定的目录,这些目录绕过联合文件系统为持续性或共享数据提供功能。
-
创建数据卷
-
不同类型的数据卷
在创建数据卷的时候,可以在创建容器时将主机本地的路径挂载到容器内作为数据卷。也可以在创建后挂载数据卷到容器使用,可以在运行容器时通过指定-v或–mount参数来使用该volume,并且可以依据数据卷类型不同挂载不同格式的数据卷。- Volume:普通数据卷,映射到特定的位置(/var/lib/docker/volumes)。
- Bind:绑定数据卷,映射到主机的任意位置。
- tmpfs:临时数据卷,只存在于内存中。
使用默认的Volume数据卷,运行一个Nginx容器,source指定数据卷,destination指定source映射在容器中的目录:
docker run -d --name=nginx_demo_docker --mount source=nginx_demo_volume,destination=/usr/share/nginx/html
nginx:latest
也可以通过-v来指定数据卷信息:
docker run -d --name=nginx_demo_docker -v nginx_demo_volume /usr/share/nginx/html
nginx:latest
使用bind指定Volume数据卷类型,运行一个Nginx容器,source指定数据卷,destination指定source映射在容器中的目录:
docker run -d --name=nginx_demo_docker_bind --mount type=bind,source=C:\Users\dell\web,destination=/usr/sh
are/nginx/html nginx:latest
也可以通过-v来指定数据卷信息:
docker run -d --name=nginx_demo_docker_bind -v C:\Users\dell\web:/usr/sh
are/nginx/html nginx:latest
挂载成功之后,本机中C:\Users\dell\web目录一旦改变,docker中的nginx/html目录也会改变。
如果使用Linux运行docker,那么避免写入数据到容器存储层还有一个方案:tmpfs mount。
tmpfs mount只是将数据保存在宿主机的内存中,一旦容器停止运行,tmpfs mount会被移除,从而造成数据丢失。
docker run -d -it --name=tmptest --mount type=tmpfs,destination=/app nginx:latest
数据卷容器
如果用户需要在多个容器之间共享一些持续更新的数据,最简单的方式是使用数据卷容器。数据卷容器也是一个容器,但是它的目的是专门提供数据卷给其他容器挂载。
-
新建数据卷容器
新建一个数据卷容器db_data,并查看该容器的目录:
-
共享数据卷容器
新建其他的容器使用db_data的数据:
注意:db_data这个数据卷容器不能随便关闭,如果关闭了,其他挂载了db_data里面的数据卷的容器就用不了。
Docker基础命令
systemctl start docker | 启动docker |
systemctl stop docker | 停止docker |
systemctl restart docker | 重启docker |
systemctl status docker | 查看docker状态 |
systemctl enable docker | 开机启动 |
docker info | 守护进程的系统资源设置 |
docker search 镜像名称 | docker仓库的查询 |
docker pull 镜像名称 | 下载docker仓库 |
docker images | docker镜像的查询 |
docker rmi 镜像名称id | docker镜像的删除 |
docker ps | 容器的查询 |
docker run 镜像名称+版本号 | (docker run mysql:8.0.29)容器的创建启动 |
docker start/stop | 容器启动停止 |
docker tag source_image[:tag] target_image[:tag] | docker tag centos local/centos 设置镜像的标签,事实上centos和local/centos共用一个存储空间,只是标签不同而已 |
systemctl 是管理 Linux 的 systemd 服务的工具,systemctl 只能用于linux系统。
run命令扩展
docker run命令不仅可以创建容器,而且在创建之后会立即启动该容器。docker run与docker create命令的语法大同小异,只是有两个选项需要特别注意:
- -t,该选项的功能是:为当前的容器分配一个命令行虚拟终端,以便用户与容器交互,以该选项创建的容器可以称为交互式容器。
- -d,以该选项创建的容器称为后台型容器,新的容器保持在后台运行。
单一容器管理
每个容器被创建后,都会分配一个CONTAINER ID作为容器的唯一标示,后续对容器的启动、停止、修改、删除等所有操作,都是通过CONTAINER ID来完成偏向于数据库概念中的主键。
docker ps --no-trunc | 查看 |
docker start/stop CONTAINER ID | 启动/停止 |
docker start/stop wordPress | 通过容器别名启动/停止 |
docker kill 容器id或容器名 | 强制停止容器 |
docker rm 容器id | 删除已停止的容器 |
docker inspect wordPress | 查看容器所有基本信息 |
docker logs wordPress | 查看容器日志 |
docker stats wordPress | 查看容器所占用的系统资源 |
docker exec 容器名 容器内执行的命令 | 容器执行命令 |
docker exec -it 容器名 /bin/bash | 登入容器的bash(我们可以使用该命令来创建mysql数据库,如下所示) |
docker system prune | 批量清除本地中未使用的镜像 |
docker system df | 查看磁盘占用情况 |
docker push wordPress | 将镜像推送到远程仓库 |
docker pull wordPress | 拉取远程仓库的镜像 |
docker image history wordPress | 查看镜像历史 |
docker image build [path] | 镜像构建,构建需要Dockerfile |
docker login [options] [server] | 登录远程仓库 docker login --username xxx --password xxxx |
Docker日志
默认情况下,docker地日志会发送到容器地标准输出设备(STDOUT)和标准错误设备(STDERR),STDOUT和STDERR实际上就是容器地控制台终端。
docker logs命令地语法格式如下:
docker logs [options] container
这时看到地日志是静态的,如果想要持续看到新打印出地日志信息可以加上-f参数:docker logs -f container。
- logging driver
docker日志会发送到STDOUT和STDERR。实际上,docker还提供了其他地一些机制,允许我们从运行地容器中提取日志,这些机制统称为logging driver。
对docker而言,其默认地logging driver是json-file,如果在启动docker时没有特别指定,这个就是默认地logging driver。
- ELK
ELK是Elastic公司提供地一套完整地日志收集以及前端展示地解决方案。ELK可以收集、过滤、传输、存储以及实现多系统的组件日志进行集中管理和准时搜索、分析;还可以帮助运维人员进行线上业务的准时监控等。
ELK系统的安装和配置,参考下面这篇博客:Docker中安装ELK
Docker持续开发工作流
一般情况下的Docker应用程序内部循环的持续开发工作流只关注在开发人员的计算机上进行的开发工作,不包括设置环境等初始步骤,因为这些步骤只需进行一次。应用程序一般由开发人员自己的代码和附加库组成。
相关步骤说明如下:
-
开发。根据需求开发应用程序和传统开发没有什么变化,不限编程语言。
-
编写Dockerfile。Dockerfile是由一系列命令和参考构成的脚本,用来构建镜像。
Dockerfile就是定义这些镜像以及构建自定义镜像的脚本文件。虽然我们可以通过docker commit命令来收到创建镜像,但是通过dockerfile文件可以自动创建镜像,并且能够自定义创建过程。因此,在一般情况下,推荐使用dockerfile来创建镜像。本质上,dockerfile就是由一系列指令和参数构成的脚本,这些命令应用于基础镜像并最终创建一个新的镜像。相关的dockerfile内容可以参考下面这篇博客:https://blog.csdn.net/qq_36828822/article/details/132813617
-
创建自定义镜像。基于“docker build”命令构建自定义镜像。
-
定义docker-compose。docker compose是一个用于定义和运行多个docker应用程序的工具,非常适合进行开发和测试,尤其适用于微服务架构。
-
启动docker应用。可以用docker run命令来启动,也可以基于“docker-compose up”。
-
测试。测试人员基于容器环境进行测试,无须开发人员介入,随时部署或销毁。
-
部署或继续开发。基于容器实现持续交付和部署。
docker-compose编排
compose是docker公司推出的一个工具软件,可以管理多个Docker容器组成一个应用。需要定义一个yaml格式的配置文件docker-compose.yaml,写好多个容器之间的调用关系。然后,只要一个命令,就能同时启动/关闭这些容器。
docker compose适用于所有环境:生产、开发、测试环境以及CI工作流程。
compse的核心概念
- 一个文件:docker-compose.yaml
- 两个要素:
- 服务:一个个应用容器实例,比如订单微服务,redis容器,mysql容器。
- 工程:由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yaml文件中定义。
三个步骤
- 编写Dockerfile定义各个微服务应用并构建出对应的镜像文件
- 使用docker-compose.yml定义一个完整的业务单元,按照先后顺序安排好整体应用中的各个容器服务
- 最后,执行docker-compose up命令来启动并运行整个应用程序,完成一键部署上线
了解YAML语言
yaml时一种简洁的非标记语言。yaml以数据为中心,使用空白、缩进、分行组织数据,从而使得表示更加简洁易读。
基本规则:
- 大小写敏感
- 使用缩进表示层级关系
- 禁止使用tab缩进,只能使用空格键
- 缩进长度没有限制,只要元素对齐就表示这些元素属于一个层级
- 使用#表示注释
- 字符串可以不用引号标注
- docker-compose文件配置项,参考这个网站:https://docs.docker.com/samples/
- yaml中允许表示三种格式:常量值、对象和数组
compse常用命令
docker-compse -h | 查看帮助 |
docker-compse up | 启动所有docker-compose服务 |
docker-compse up -d | 启动所有docker-compose服务并后台运行 |
docker-compse down | 停止并删除容器、网络、卷、镜像 |
docker-compse start | 启动服务 |
docker-compse pause | 暂停服务 |
docker-compse unpause | 恢复暂停 |
docker-compse rm | 删除容器 |
docker-compse stop | 停止服务 |
docker-compse restart | 重启服务 |
docker-compse config | 检查配置 |
docker-compse config -q | 检查配置,有问题才有输出 |
docker-compse logs yml里面的服务id | 查看容器输出日志 |
docker-compse top | 展示当前docker-compose编排过的容器进程 |
docker-compse ps | 展示当前docker-compose编排过的运行的所有容器 |
docker-compse exec yml里面的服务id /bin/bash | 进入容器实例内部 |
docker-compose port | 打印端口绑定的公共端口 |
docker-compose ls | 列出正在运行的撰写项目 |
docker-compose kill | 强制停止服务容器 |
docker-compose images | 列出创建的容器使用的镜像 |
docker-compose events | 从容器接收实时事件 |
docker-compose create | 为服务创建容器 |
docker-compose cp | 在服务容器和本地文件系统之间复制文件/文件夹 |
docker-compose build | 生成或重建服务 |
如何编写docker-compose
官网:https://docs.docker.com/compose/compose-file/03-compose-file/。自然就是参考官网了,如果忘记了相关的语法看官网就行了,这个东西没必要深究,会使用就可以了。而且,如果使用了GoLand编译器安装了Docker插件之后编写docker-compose的时候都会有提示的。
- 案例如下:
version: '3.0'
services:
mysql8:
image: mysql:8.0.29
command: --default-authentication-plugin=mysql_native_password #解决外部无法访问
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
# 设置初始化脚本
- ./script/mysql/:/docker-entrypoint-initdb.d/
ports:
# - 映射为13316端口
- "13316:3306"
redis:
image: 'bitnami/redis:latest'
environment:
- ALLOW_EMPTY_PASSWORD=yes
ports:
- '6379:6379'
案例——使用Go推送钉钉消息
- main.go
package main
import (
"fmt"
"os"
)
// 环境变量
var envList = []string{
//钉钉机器人地址
"WEBHOOK",
//@的手机号码
"AT_MOBILES",
//@所有人
"IS_AT_ALL",
//消息内容
"MESSAGE",
//消息类型(仅支持文本和markdown)
"MSG_TYPE",
}
func main() {
//获取环境变量
envs := make(map[string]string)
for _, envName := range envList {
envs[envName] = os.Getenv(envName)
//参数检查
if envs[envName] == "" && envName != "AT_MOBILES" && envName != "IS_AT_ALL" {
fmt.Println("envionment variable " + envName + " is required")
os.Exit(1)
}
}
if envs["AT_MOBILES"] == "" && envs["IS_AT_ALL"] == "" {
fmt.Println("必须设置参数 AT_MOBILES 和 IS_AT_ALL 两者之一!")
os.Exit(1)
}
builder, err := NewBuilder(envs)
if err != nil {
fmt.Println("BUILDER FAILED: ", err)
os.Exit(1)
}
if err := builder.run(); err != nil {
fmt.Println("BUILD FAILED", err)
os.Exit(1)
} else {
fmt.Println("BUILD SUCCEED")
}
}
- types.go
package main
// 文本消息
type TextWebhook struct {
Msgtype string `json:"msgtype"`
Text Text `json:"text"`
At At `json:"at"`
}
type Text struct {
Content string `json:"content"`
}
// @,支持@手机号码和所有人
type At struct {
AtMobiles []string `json:"atMobiles"`
IsAtAll bool `json:"isAtAll"`
}
// markdown消息
type MarkdownWebHook struct {
Msgtype string `json:"msgtype"`
Markdown Markdown `json:"markdown"`
At At `json:"at"`
}
type Markdown struct {
Title string `json:"title"`
Text string `json:"text"`
}
- builder.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
// 结构
type Builder struct {
Webhook string
AtMobiles []string
IsAtAll bool
Message string
payload interface{}
}
// 组装消息格式
func NewBuilder(envs map[string]string) (*Builder, error) {
b := &Builder{}
b.Webhook = envs["WEBHOOK"]
if strings.ToLower(envs["IS_AT_ALL"]) == "true" {
b.IsAtAll = true
}
b.AtMobiles = strings.Split(envs["AT_MOBILES"], ",")
at := At{
AtMobiles: b.AtMobiles,
IsAtAll: b.IsAtAll,
}
if envs["MESSAGE"] != "" {
b.Message = envs["MESSAGE"]
switch envs["MSG_TYPE"] {
case "text":
text := Text{
Content: b.Message,
}
info := TextWebhook{
Msgtype: "text",
Text: text,
At: at,
}
b.payload = info
return b, nil
case "markdown":
md := Markdown{
Title: "钉钉通知",
Text: b.Message,
}
b.payload = MarkdownWebHook{
Msgtype: "markdown",
Markdown: md,
At: at,
}
return b, nil
default:
return nil, fmt.Errorf("不支持的消息类型!")
}
}
return nil, fmt.Errorf("尚不支持其他格式!")
}
func (b *Builder) run() error {
if err := b.callWebhook(); err != nil {
return err
}
return nil
}
// 调用钉钉webhook
func (b *Builder) callWebhook() error {
payload, _ := json.Marshal(b.payload)
fmt.Printf("sending webhook info: %s\n", string(payload))
body := bytes.NewBuffer(payload)
res, err := http.Post(b.Webhook, "application/json;charset=utf-8", body)
if err != nil {
return err
}
result, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return err
}
var resultJSON interface{}
if err = json.Unmarshal(result, &resultJSON); err != nil {
return err
}
fmt.Println(resultJSON)
fmt.Println("Send webhook succeed.")
return nil
}
- Dockerfile
FROM golang:alpine AS builder
#选择alpine版本,镜像会比较小
WORKDIR /go/src/component-dingding
COPY ./ /go/src/component-dingding
RUN set -ex && \
go build -v -o /go/bin/component-dingding \
-gcflags '-N -l' \
./*.go
FROM alpine
RUN apk update && apk add ca-certificates
COPY --from=builder /go/bin/component-dingding /usr/bin/
CMD ["component-dingding"]
# 注意不要单独使用 MAINTAINER 指令,MAINTAINER已被Label标签代替
MAINTAINER "hzulwy"
# LABEL指令用于将元数据添加到镜像,支持键值对和JSON,我们可以使用 docker inspect 命令来查看
LABEL DingtalkComponent='{\
"description": "使用钉钉发送通知消息.",\
"input": [\
{"name": "WEBHOOK", "desc": "必填, 钉钉机器人Webhook地址"},\
{"name": "AT_MOBILES", "desc": "非必填,被@人的手机号"},\
{"name": "IS_AT_ALL", "desc": "非必填,@所有人时:true, 否则为:false"},\
{"name": "MESSAGE", "desc": "必填,自定义发送的消息内容"},\
{"name": "MSG_TYPE", "desc": "必填,自定义发送的消息类型,目前仅支持text和markdown"}\
]\
}'
- 打开命令行终端,执行下面命令
docker build --rm -f 'Dockerfile' -t go-dingtalk:latest .
docker run --rm -e 'WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=bd6b79172572d79070bd6b1f8cfcb1277ca93b9b461b725a4168de04c5afe2e5' \
-e "MESSAGE=*hello,监控报警.*" \
-e "IS_AT_ALL=true" \
-e "MSG_TYPE=markdown" \
-d go-dngtalk
--rm用于自动清理。
工具推荐
- 使用vscode安装docker插件