Docker 安装

五十岚2022年7月3日
大约 11 分钟

Docker 的基础概念和安装说明

Docker

让开发者打包他的应用、及依赖包,到一个轻量级、可移植的容器中,可发布到任何流行的 Linux 机器上,也能实现虚拟化,完全使用沙箱机制隔离),相互之间不会有任何接口,且 开销极低

1. Docker概述

旧金山 dotCloud 基于 Linux 容器技术 LXC 封装的内部工具,13 年诞生,15 年逐步投入生产,后面开源出来改个名字叫 Docker ,目的为了节省资源(硬件、虚拟机

1.1 教程

1.2 Linux 容器

Docker1.8 版本之前,全部是封装 LinuxLXCopen in new window,一个 用户态 使用容器化特性的 接口调用 Kernel),但不具备跨平台能力

随后为了实现跨平台,抽出了 libcontaineropen in new window 项目,把 namespacecgroup 的操作封装在该项目里,支持不同的平台类型

1.3 容器与虚拟机对比

虚拟机

VMwarePVEESXiWorkstation 等,多台虚拟机都虚拟出了一套 不同虚拟机器硬件资源Kernel内核 )和 Lib 库,然后在上层运行各自的 APP,像是物理机的系统中的子系统一样,从物理虚拟层面进行隔离,占用资源极高

  • Hypervisor: 一种运行在基础物理服务器和操作系统之间的中间软件层,可允许多个操作系统和应用共享硬件
  • 共享硬件资源,但每起一个虚拟机都需要额外的安装操作系统,从而带来重复的操作系统开销
容器

则是多个容器 共同使用 一套物理机 硬件资源Kernel 然后从运行所需的 Lib 库 层面 进行隔离,因此极大的压榨了物理资源,使物理机物尽其用

  • Container Runtime: 通过 Linux 内核虚拟化能力管理多个容器,多个容器共享一套操作系统内核,因此 摘掉了内核占用的空间 及运行所需要的耗时,使得容器极其轻量与快速

Docker 解决如下需求

  • 环境、依赖不一致
  • 物理硬件资源不够
  • 快速交付介质,直接交付打包后的 Docker 镜像,各平台部署
  • 跨平台,方便装任何系统,屏蔽平台间差异
  • 物理资源相互隔离(也可以做到内存、CPU 等资源分配与隔离,但安全性不如虚拟机
  • Docker 启动多容器生命周期管理

1.4 Docker 架构

20156 月,Docker 成立 OCIopen in new windowOpen Container Initiative 开放容器计划 )组织,建立通用标准并由该组织维护 libcontainer 项目,后续由从仅包含 Kernel 的库加入了 CLI 工具且改名为 runCopen in new window运行容器的轻量级工具

Docker 随后做出了架构调整

将容器运行时相关的程序从 docker daemon 剥离出来,形成了 containerd

  • runC: 是一个 Linux CLI命令行 )工具, runC + containerd-shim 通过 gRPC 去调用 containerd 来创建和运行容器
  • containerd: 一个 守护程序它管理容器的生命周期,屏蔽了 docker deamon 底层细节(同时解耦升级后的不兼容 ),抽象出了一套 gRPC 接口,提供了在节点上执行容器和管理镜像的最小功能集

因此,Docker 演变为了 CS 架构的产品

  • Client:docker CLI 通过 REST API 调用 Server 端服务
  • Server:docker daemon 守护进程

2. 安装

推荐参考官方文档:安装 Docker 引擎(英文)open in new window

3. 实现原理

虚拟化核心需要解决的问题:资源隔离资源限制

  • 虚拟机硬件虚拟化技术,通过一个 hypervisor 层实现对资源的彻底隔离
  • 容器时操作系统级别的虚拟化,利用的是内核的 CgroupNamespace 特性,此功能完全通过软件实现

3.1 Namespace 资源隔离

命名空间是全局资源的一种抽象,将资源放到不同的命名空间中,各个 命名空间中的资源是相互隔离的

Docker 容器 对操作系统来说是个进程, 实现如下

// Linux里,用clone() 实现进程创建的系统调用
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
  • child_func: 传入子进程运行的程序主函数
  • child_stack: 子进程使用的栈空间
  • flags: 表示使用那些 CLONE_* 标志位
  • args: 用于传入用户参数
分类系统调用参数相关内核版本
Mount namespacesCLONE_NEWNSLinux 2.4.19
UTS namespacesCLONE_NEWUTSLinux 2.6.19
IPC namespacesCLONE_NEWIPCLinux 2.6.19
PID namespacesCLONE_NEWPIDLinux 2.6.24
Network namespacesCLONE_NEWNETLinux 2.6.24 ~ 29
User namespacesCLONE_NEWUSERLinux 2.6.23 ~ 3.8
  • pid: 用于进程隔离(PID:进程 ID
  • net: 管理网络接口(NET:网络
  • ipc: 管理对 IPC 资源的访问(IPC:进程间通信,信号量,消息队列和共享内存
  • mnt: 管理文件系统挂载点(MNT: 挂载
  • uts: 隔离主机名和域名
  • user: 隔离用户、用户组

实现容器独立的主机名和进程空间

#define _GNU_SOURCE
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    "/bin/bash",
    NULL
};

int container_main(void* arg)
{
    printf("容器进程[%5d] ----进入容器!\n",getpid());
    sethostname("container", 10); // 设置 hostname
    /**执行/bin/bash */
    execv(container_args[0], container_args);
    printf("Something's Error!\n");
    return 1;
}

int main()
{
    printf("宿主机进程[%5d] - 开始一个容器!\n",getpid());
    /* 调用clone函数 新的进程、挂载、空间*/
    int container_pid = clone(container_main, container_stack+STACK_SIZE,  CLONE_NEWPID | CLONE_NEWUTS | SIGCHLD, NULL);
    /* 等待子进程结束 */
    waitpid(container_pid, NULL, 0);
    printf("宿主机 - 容器结束!\n");
    return 0;
}

执行编译并测试

vim container.c
gcc container.c -o  container
./container
## 宿主机进程[11660] - 开始一个容器!
## 容器进程[    1] ----进入容器!

# 查看hostname,发现是 container
hostname

# 查看当前进程号,发现是 1号进程
echo $$

通过 proc 对比

# 查看子进程(打印出来的)
pstree -p 11660
## container2(11660)───bash(11661)

ll /proc/11661/ns/
ll /proc/11660/ns/
# Ctrl + d 退出

发现和父进程不同,故piduts 具有不同的命名空间

因此 Docker 在启动一个容器的时候,会调用 Linux Kernel Namespace 的接口,创建一块虚拟空间,user 通常相同用一样的,不会新建

3.2 CGroup 资源限制

Namespace: 可以保证容器间的隔离,但无法限制占用资源,若容器中执行 CPU 密集型任务,或内存泄漏,此时无法控制,因此需要 Control Groups

CGroup: 可以隔离宿主机上的物理资源:CPU、内存、磁盘 I/O、网络带宽,每一个 CGroup 都是一组被相同标准的参数限制的进程,我们只需把容器和进程加入到中指定的 CGroup

3.3 UnionFS 联合文件系统

每台机器若运行上百容器,若都去全量 copy 文件系统,那么再轻量也会占用大量存储空间,导致

  • 运行容器速度慢
  • 占用大量磁盘物理空间

因此 Docker 用如下手段解决这个问题

  • 镜像分层存储
  • UnionFS

每个镜像是有一系列的层组成,一层代表 Dockerfile 中的一条指令,如下文件,就包含了 4 条指令

FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py

每一行就创建一层,Dockerfile 构建出来的镜像运行的容器结构如下

镜像就是如上一层层堆叠起来的,而且都是只读的,运行时才会在基础层上添加新的可写层(容器层),对于运行中的容器所做的所有更改(CUD操作)都将写入容器层

如何写入

当写入时

  • 容器层用了写时复制 CoW 技术(copy-on-write),故所有数据都从 image 里读,让容器共享 image 的文件系统
  • 写时才去进行复制到自己文件系统上的副本操作,也不会影响到 image 的源文件,提高磁盘利用率
如何合并层到一起

UnionFS 是为了 Linux 系统设计的,用来把多个文件系统联合到同一个挂载点的文件系统服务。能够将不同文件夹中的层 联合(Union) 到 同一个文件夹 中,整个联合的过程成为联合挂载 Union Mount

说明

上述即 AUFSDocker存储驱动)的一种实现

此外还支持不同驱动 devicemapperoverlay2zfsBtrfs 等... 新版已经使用 overlay2 取代了AUFS,但在没有 overlay2 驱动的机器上,依然使用 AUFS

上次编辑于: 2023/4/27 03:31:13