Docker容器与容器云
本文最后更新于 39 天前,其中的信息可能已经有所发展或是发生改变。

本文是对docker容器进行学习的随笔记录,主要记录docker容器的相关知识,如:原理、体系、环境支持、操作流程、命令行操作以及相关的技术生态系统。文章本身会根据作者对于知识的掌握深浅而进行筛选和记录,因此具有强烈的主观性。虽然是从各个专业书籍与博客搜集的记录,技术仍在进步,故此文章不具备时效性,请仔细分辨本文中的观点。若和现实生产与应用场景存在出入,作者不对因使用本笔记所产生的任何直接或间接损失承担责任

前言

关于docker容器在其他两篇文章中略有讨论过容器内部配置文件的修改,这只是docker容器的冰山一角。(见右边两篇博客)
Docker 容器的核心价值是:把“应用 + 运行环境依赖”封装成可移植、可复现、可隔离的交付单元,从而在开发、测试、上线、运维等环节减少环境差异与交付成本。例如:

  • 用 Dockerfile 固化语言运行时、依赖库、工具链版本(Node/Python/JDK/LLVM 等)。
  • 用 docker compose 一键拉起整套依赖:数据库、缓存、消息队列、对象存储等。
  • VS Code Dev Containers / JetBrains Gateway 让 IDE 直接连接容器开发。
  • 一个服务一个镜像;各自独立发布、扩缩容;
  • 通过服务发现、网关、sidecar(如 service mesh)统一治理。
  • docker compose 拉起临时环境跑集成测试;
  • 使用 Testcontainers(Java/Go/Node 等)在测试时动态启动 MySQL/Redis/Kafka;
  • 测完即销毁,保证每次环境干净一致。

还有很多便携之处就不一一举例了


安全方面

容器相对于传统的无容器部署,它可以更容易做到最小权限和实时监控。同时在滚动升级和漏洞修复方面也不逊色于传统的无容器部署。

网安的圈子里有一个攻击方式:容器逃逸。这就是黑客先进入docker容器创造的应用程序当中,再通过挂载危险路径/var/run/docker.sock或者通过privileged模式来操作/dev等模块,具体的细节在后面进行赘述。

第一章 Docker基础

1、Docker’s Orders

docker命令的执行一般都需要root权限,这是因为docker的命令行工具docker和Docker daemon是同一个二进制文件,同时后者负责接收并执行来自docker输入的命令,它的运行需要root权限。

在docker命令中有很多基础的命令需要重点记住:

子命令分类子命令
docker环境信息info、version
容器生命周期管理create、exec、kill、pause、restart、rm、run、start、stop、unpause
镜像仓库命令login、logout、pull、push、search
镜像管理build、images、import、load、rmi、save、tag、commit
容器运维操作attach、export、inspect、port、ps、rename、stats、top、wait、cp、diff、update
容器资源管理volumn、network
系统日志信息events、history、logs
docker info(权限需求的实例不在此演示)
docker images
docker ps 卡提可爱捏

1.1 docker run

docker run是Docker的核心命令之一,所有容器的启动都需要这一命令来操作,例如:

  • sudo docker run ubuntu echo “Hello World”
    Docker 以 ubuntu15.10 镜像创建一个新容器,然后在容器里执行 bin/echo “Hello world”,然后输出结果
  • pulling from library/ubuntu
    拉取ubuntu镜像
  • Hello World
    用ubuntu镜像输出Hello World

在启动ubuntu这个容器后,docker会给这个容器分配一个ID以方便识别,就是上图中的fd8cda969ed2

如果想要进入容器进行操作的话,可以先用docker run启动一个容器并分配一个伪终端来操作

sudo docker run -i -t --name mytest ubuntu:latest /bin/bash
  • -i 启动交互
  • -t 分配一个伪终端
    前两者可融合变为-it
  • –name 分配容器名字

除此之外,还有:

  • -c 分配所占的CPU权重
  • -m 分配容器内存
  • -v 挂载一个数据卷volume
  • -p 暴露端口

1.2 docker start/stop/restart

字面意思的命令
docker start可以使用-i来启动交互模式,docker stopdocker restart可以使用-t来设定停止前的等待时间

1.3 docker pull/push

pull是从Docker registry拉取imagerepository。也可以从Docker Hub拉取镜像资源
push是将本地的image或repository推送到Docker Hub的公共或者个人私有仓库

1.4 docker rmi/rm

docker rm用于删除容器
docker rmi用于删除镜像

第二章 Docker核心原理

1、Docker的内核

Docker容器本质是借助宿主机的进程来运行,说白了他也是一个需要利用宿主机内存的进程。Docker利用namespace实现了资源隔离,利用cgroups实现了资源限制,又通过写时复制机制(copy-on-write)实现了高效的文件操作。

1.1 namespace资源隔离

每一个容器都是独立存在的,但又没有完全独立。前面一句看似很矛盾,但以人来打个比方的话,就是:

每个人是独立存在的,但又和社会脱离不了干系

每个容器启动之后,会借用linux内核中的namespace隔离的系统调用来解决最基本的6项隔离:

namespace隔离内容
UTS主机名与域名
IPC信号量、消息队列和共享内存
PID进程编号
Netword网络设备、网络栈、端口等
Mount挂载点
User用户和用户组

1.1.1 进行namespace API操作的4种方式

a、clone()创建新进程的同时创建namespace

使用clone()来创建一个独立namespace的进程是最常见的做法,实际上clone()是linux系统调用fork()的一种通用的实现方式,可以用flags来控制使用多少功能。

b、查看/proc/[pid]/ns文件

在该路径文件下,可以查看指向不同namespace的文件,如果两个进程指向的namespace编号相同,就说明它们在同一个namespace下,否则便在不同namespace里面。

c、setns()加入一个已经存在的namespace

通过挂载的形式,可以在进程都结束的情况下把namespace保留下来,保留下来的目的是为了以后有进程加入做准备。在Docker中,使用docker exec命令在已经运行着的容器中执行一个新的命令,就需要用到该方法。

d、unshare()在原先进程上进行namespace隔离

这个和clone()很类似,不同的是,unshare()运行在原先的进程上,不需要启动一个新进程。
调用这个函数的主要作用就是不启动新进程的情况下,起到隔离的效果,相当于跳出原先的namespace进行操作。

*、附加:fork()系统调用

fork()的一个核心就是:这个函数被调用一次,却能够返回两次,而且可以根据返回值的不同就可以区分父进程和子进程:

  • 在父进程中,fork()返回新创建子进程的进程ID
  • 在子进程中,fork()返回0
  • 如果出现错误,fork()返回一个负值

fork()的一个简单示例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // 子进程
        printf("I am child process, PID: %d\n", getpid());
    } else if (pid > 0) {
        // 父进程
        printf("I am parent process, PID: %d, child PID: %d\n", getpid(), pid);
    } else {
        // fork 失败
        perror("fork failed");
        exit(1);
    }

    return 0;
}

输出:
I am parent process, PID: 1234, child PID: 1235
I am child process, PID: 1235

从输出就能看得出来fork()的一个作用就是生成父子进程

1.1.2 namespace

代码解释起来太过于痛苦,它的一个作用就是为了防止普通用户任意修改系统主机名导致set-user-ID相关的应用运行出错。

1.1.3 IPC namespace

1.1.4 PID namespace

⬇这里的东西推荐看看这本书 P33页开始⬇

Docker容器与容器云(第2版)

⬆文件下载在这⬆

1.2 cgroups资源限制

cgroups在容器当中可以起到下面四个作用

  • 资源限制
    对任务使用的资源总额进行限制
  • 优先级分配
    通过分配的CPU时间片数量以及磁盘IO宽带大小,直接控制了任务运行的优先级
  • 资源统计
    统计系统的资源使用量,适合计费(唉,资本)
  • 任务控制
    对任务执行挂起、回复等操作

1.2.1 子系统

子系统是cgroups的资源控制系统,同时每个子系统独立地控制一种资源。目前docker使用的子系统有9种,分别如下:

  • blkio:为块设备设定输入/输出限制
  • cpu:使用调度程序控制任务对CPU的使用
  • cpuacct:自动生成cgroups中任务对CPU资源使用情况的报告
  • cpuset:为cgroups中的人物分配独立的CPU和内存
  • devices:开启或关闭cgroups中 任务对设备的访问
  • freezer:挂起或回复cgroups中的任务
  • memory:限定cgroups中任务的内存使用量,并且自动生成相应的资源使用报告
  • perf_event:可以让cgroups中的任务进行统一的性能测试
  • net_cls:通过等级识别符(classid)让linux流量控制程序识别cgroups中生成的数据包

Docker对cgroups本身没有做过增强,或者说cgroups现阶段可以满足Docker的需求。用户也不需要直接操作cgroups,所以对于cgroups只需要认识到底层运作当中有这么个东西就好了

2、Docker架构

Docker 的整体架构是一个典型的客户端-服务器(C/S)模式,主要由以下几大部分组成:

  • Docker Client(客户端)
  • Docker Daemon(守护进程 / dockerd)
  • Docker 对象(Image、Container、Network、Volume 等)
  • 底层依赖(Linux 内核特性 + 各种驱动/库)
2.1 Docker Client
  • docker CLI 命令行工具
  • Docker Desktop 的图形界面——Windows下的docker操控界面居多
  • IDE 插件(VS Code、IntelliJ 等)
  • CI/CD 工具中的 docker 命令
  • 远程 API 调用(REST API)

作用:把我们输入的命令(如 docker run、docker build、docker pull 等)转换成 HTTP/REST 请求,发给 Docker Daemon。

2.2 核心:Docker Daemon

Docker 的真正“大脑”,一个常驻后台的进程。它负责相应来自Docker client的请求,然后翻译成docker能“听懂”的语言来调用完成容器管理操作

主要职责:

  • 监听 API 请求(默认 unix socket:/var/run/docker.sock,也支持 tcp)
  • 管理 Docker 的全生命周期:build、pull、push、run、stop、rm 等
  • 协调镜像、容器、网络、存储等各种子模块
2.3 底层:Linux 内核能力

Docker 本身不包含虚拟化能力,所有隔离都依赖 Linux 内核提供的原生特性:

内核特性作用对应隔离维度
Namespaces进程、网络、用户、挂载、IPC、UTS 等隔离看起来像独立系统
cgroups资源限制(CPU、内存、IO、设备等)防止容器吃光宿主机
Union FS / Overlay FS分层镜像、写时复制(CoW)镜像层高效叠加
Netfilter / iptables网络 NAT、端口映射容器间、内外通信
Seccomp / AppArmor / SELinux安全加固(可选)限制系统调用
用户 → Docker Client
↓ (REST API)
Docker Daemon (dockerd)
├─ API Server
├─ Image 管理(distribution / layer / image / registry / reference)
├─ graphdriver(存储驱动:overlay2 / aufs / devicemapper …)
├─ execdriver → containerd + runc(执行驱动)
├─ network → libnetwork / containerd network
└─ volumedriver(卷驱动:local / 插件 …)
↓
底层:Linux Kernel
├─ namespaces
├─ cgroups
└─ Union FS / Overlay FS / Device Mapper / …

以上就是平常能够接触到的、比较浅层的架构。


以下开始就是Docker daemon文件当中会用到的几个驱动(driver),由这些driver来调用docker容器内部。它们分别是——execdriver、volumedriver、graphdriver

execdriver

execdriver是对Linux操作系统的namespaces、cgroups、apparmor、SELinux等容器运行所
需的系统操作进行的一层二次封装,其本质作用类似于LXC,但是功能要更全面。这也
就是为什么LXC会作为execdriver的一种实现而存在。当然,execdriver最主要的实现,也
是现在的默认实现,是Docker官方编写的libcontainer库。
说白了execdriver要负责管理进程管理包括启动、停止、重启等操作,还为容器提供了一个独立地执行环境,监控容器内的进程

volumedriver

volumedriver是volume数据卷存储操作的最终执行者,负责volume的增删改查,屏蔽不同
驱动实现的区别,为上层调用者提供一个统一的接口。Docker中作为默认实现的
volumedriver是local,默认将文件存储于Docker根目录下的volume文件夹里。其他的
volumedriver均是通过外部插件实现的。

graphdriver

graphdriver是所有与容器镜像相关操作的最终执行者。graphdriver会在Docker工作目录下
维护一组与镜像层对应的目录,并记下镜像层之间的关系以及与具体的graphdriver实现相
关的元数据。这样,用户对镜像的操作最终会被映射成对这些目录文件以及元数据的增
删改查,从而屏蔽掉不同文件存储实现对于上层调用者的影响。在Linux环境下,目前
Docker已经支持的graphdriver包括aufs、btrfs、zfs、devicemapper、overlay和vfs。

3、client和daemon

3.1 client模式

docker命令对应的源文件是docker/docker.go,它的使用方式如下

docker [OPTIONS] COMMAND [arg……]

'''
举例说明:平常启动容器后会经常调用一个命令 docker ps (查看镜像)
其中的ps就是flag
'''

其中OPTIONS参数成为flag,任何时候执行一个docker命令,Docker都需要先解析这些flag,然后按照用户声明的COMMAND向指定的子命令执行对应的操作

如果子命令为daemon,Docker会创建一个运行在宿主机的daemon进程,即执行daemon模式。其余子命令都会执行client模式。而这些处于client模式的docker命令工作流程包含以下步骤:

  1. 解析flag信息
    解析一些重点信息如:Debug、LogLevel、Hosts、protoAddrparts
  2. 创建client实例
    client的创建就是在已有配置参数信息的基础上,调用api/client/cli.go#NweDockerCli
  3. 执行具体的命令
    Docker client对象创建成功后,剩下的执行具体命令的过程就交给cli/cli.go来处理了。

3.2 daemon模式

3.1说到只有一个进程会进入daemon模式,其余会进入client模式。那么这唯一一个进入了daemon模式的进程中,Docker daemon会通过一个server模块接收来自client的请求,然后根据请求类型,交由具体的方法去执行。因此daemon首先就需要启动并初始化这个server。同时,启动server后,Docker进程还需要初始化一个daemon对象(daemon/daemon.go)来负责处理server接收到的请求

关于Docker daemon工作路径

这里的马赛克有点乱来了

一般而言,每次安装docker之后都会在/var/lib路径下创建一个docker的文件夹,这里面就是Docker daemon的工作路径。其中里面containers就是创建的容器文件。在后面输入容器的ID就可以查看里面相对应的配置文件。注意:如果想要查看docker里面的文件是需要提权的,在没有做安全的防护下,会在日志当中留有记录,谨记请勿随便查看或修改内部的文件

Docker根目录的结构:

/var/lib/docker/
├── aufs # aufs驱动工作的目录
│ ├── diff # aufs文件系统的所有层的存储目录(新下载的镜像内
 容就逐层保存在这里)
│ ├── layers # 存储上述所有aufs层之间的关系等元数据
│ └── mnt # aufs文件系统的挂载点,如果容器中写了一个新文
 件,会出现在这里 
├── containers # 容器的配置文件目录
│
├── image/aufs # 存储镜像和镜像层信息,注意这里只是元数据,真 
 的镜像层内容保存在aufs/diff下
│ ├── imagedb # 存储所有镜像的元数据
│ ├── layerdb # 存储所有镜像层和容器层的元数据
│ ├── repositories.json # 记录镜像仓库中所有镜像的repository和tag名
│ └── distribution
│
└── volumes # volumes的工作目录,存放所有volume数据和元数据

3.2.1 daemon对象

在Docker daemon进程在经过多层设置以及创建对象之后,最终创造出用户经常能见到的daemon对象实例:

  • ID:根据传入的证书生成的容器ID,若没有传入则自动使用ECDSA加密算法生成。
  • repository:部署所有Docker容器的路径。
  • containers:用于存储具体Docker容器信息的对象。
  • execCommands:Docker容器所执行的命令。
  • referenceStore:存储Docker镜像仓库名和镜像ID的映射。
  • distributionMetadataStore:v2版registry相关的元数据存储。
  • trustKey:可信任证书。
  • idIndex:用于通过简短有效的字符串前缀定位唯一的镜像。
  • sysInfo:Docker所在宿主机的系统信息。
  • configStore:Docker所需要的配置信息。
  • execDriver:Docker容器执行驱动,默认为native类型。
  • statsCollector:收集容器网络及cgroup状态信息。
  • defaultLogConfig:提供日志的默认配置信息。
  • registryService:镜像存储服务相关信息。
  • EventsService:事件服务相关信息。
  • volumes:volume所使用的驱动,默认为local类型。
  • root:Docker运行的工作根目录。
  • uidMaps:uid的对应图。
  • gidMaps:gid的对应图。
  • seccompEnabled:是否使用seccompute。
  • nameIndex:记录键和其名字的对应关系。
  • linkIndex:容器的link目录,记录容器的link关系

3.2.2 恢复已有的Docker容器

在Docker daemon启动时,会查看daemon.repository,也就是/var/lib/docker/containers中的内容。如果存在容器,则会将相应信息收集并进行维护,同时重启restart repolicy为always的容器

/var/lib/docker/containers

为保证安全已做隐私化处理
同时重启restart repolicy为always的容器

restart policy = always 是 Docker 中最“强硬”、也最常用于生产环境的自动重启策略。它的核心设计目标是:让容器尽可能一直处于运行状态

有自动重启就会有手动重启,对于always来说,就相当于在Windows下的一些流氓软件设置的开机自启动,除非自己设置了禁止开机自启动(取消always),否则他会一直在你启动电脑后立刻在后台占用你的内存;而一般软件不会设置开机自启动,只有启动了软件才会呈现在你面前。这就是一般的默认设置。下面一份表格可以比对出默认和设置了always的区别

情况–restart=always–restart=unless-stopped推荐场景
容器 crash / 异常退出立刻重启立刻重启
Docker daemon / 主机重启自动启动(不管之前什么状态)自动启动(除非之前是手动停的)
执行 docker stop 后保持停止,但 daemon 重启后又起来保持停止,daemon 重启后仍然保持停止运维更友好
适合场景极致高可用、不能容忍任何宕机生产环境最常用(推荐)大多数真实生产环境
always vs unless-stopped 对比

3.3 从client到daemon

client和daeon单独启动并初始化的流程已经了解了大概,那么一个已经在运行中的daemon该如何处理来自client的请求?

1、docker run

Client

  • docker run之后,用户端进入client模式,并开始前面所描述的初始化流程。
  • 新建出client并通过反射机制找到CmdRun,后者开始解析用户提供的容器参数后,发出两个强求:
    • 创建容器
    • 启动容器

Daemon

  • daemon利用API Server来响应上述要求,新建一个container即可。注意,这其中container是能够知道管理它的daemon进程信息的

2、启动容器

daemon会着手处理后续client发来的start请求操作,前面提到的创建namespace,配置cgroup,挂载rootfs,以及container所需的各项参数,都在创建容器过程中就设置好了,所以daemon会直接在start.go中执行daemon.ContainStart,这样就可以在宿主机上创建对应容器了。

3、Execdriver.Run

daemon做好了所有的准备工作,那创建的细节又会如何处理?ExecDriver.Run这时候就排上了用场(具体是哪种Driver由container来决定)

execdriver作为daemon的重要组成部分之一,它封装了对底层namespace、cgroup等所有对OS资源进行操作的所有方法。而Docker当中,execdriver的默认实现(native)就是libcontainer

具体操作方式也是Docker daemon向execdriver提供三个参数,等待返回结果即可

  • commandv:该容器需要的所有配置信息集合(container的属性之一);
  • pipes:用于将容器stdin、 stdout、stderr重定向到daemon;
  • startCallback():回调方法。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇