探索Docker:原理、安装与基础应用

进程:

一旦“程序”被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。像这样一个程序运行起来后的计算机执行环境的总和称为进程

静态表现:存储在磁盘中的代码

动态表现:运行时计算机数据和状态的总和

容器的核心技术:通过约束和修改进程的动态表现,从而创造一个边界

涉及到的系统调用:

  • clone() – 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。
  • unshare() – 使某进程脱离某个namespace
  • setns() – 把某进程加入到某个namespace

一 修改进程视图:namespace

namespace类型:

  • PID Namespace
  • Mount Namespace
  • UTS Namespace
  • IPC Namespace
  • Network Namespace
  • User Namespace

原理:在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。

二 约束:Cgroup

Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作

操作

#比如控制cpu
cd /sys/fs/cgroup/cpu
#创建一个控制组,也就是一个目录,会自动生成可以控制的配置文件
mkdir container
#比如可以用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。
echo 20000 > /sys/fs/cgroup/cpu/container/cfs_quota_us
#20000 代表20ms,cpu.cfs_period_us一般是100ms,表示100ms内,被该控制组限制的进程只能使用 20 ms 的 CPU 时间
echo 被控制进程的pid > /sys/fs/cgroup/cpu/container/tasks

Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。

注意: Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

三 rootfs 根文件系统

切换进程的根目录(Change Root)。

  • 拷贝必要的文件
  • pivot_root 系统调用,如果系统不支持,使用 chroot。

需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。

同一台机器上的所有容器,都共享宿主机操作系统的内核。

docker安装

  1. 更新软件包索引:首先,使用以下命令更新系统的软件包索引:
    sudo yum check-update
  2. 安装需要的软件:安装一些必要的软件包,以便使用 Docker 的软件源:
    sudo yum install -y yum-utils device-mapper-persistent-data lvm2
  3. 设置 Docker 软件源:使用以下命令设置 Docker CE 软件源:
    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
  4. 安装 Docker CE:现在,可以使用以下命令安装 Docker CE:
    sudo yum install docker-ce
  5. 启动 Docker 服务:安装完成后,可以启动 Docker 服务并设置开机自启:
    sudo systemctl start docker
    sudo systemctl enable docker
  6. 验证 Docker 安装:最后,您可以运行以下命令来验证 Docker 是否成功安装:
    sudo docker run hello-world

    如果 Docker 安装正确,将会输出 “Hello from Docker!” 的信息。

  7. 配置用户权限(可选):如果您希望无需使用 sudo 来运行 Docker 命令,可以将当前用户添加到 Docker 用户组中。首先,执行以下命令将当前用户添加到 Docker 用户组:
    sudo usermod -aG docker $USER

docker镜像的结构

  • 只读层:最基础的文件
  • init层:可动态配置读写,但是不能commit的层
  • 读写层:增量修改可以commit和push的层

Dockerfile

# 使用官方提供的Python开发镜像作为基础镜像
FROM python:2.7-slim

# 将工作目录切换为/app
WORKDIR /app

# 将当前目录下的所有内容复制到/app下
ADD . /app

# 使用pip命令安装这个应用所需要的依赖
RUN pip install --trusted-host pypi.python.org -r requirements.txt

# 允许外界访问容器的80端口
EXPOSE 80

# 设置环境变量
ENV NAME World

# 设置容器进程为:python app.py,即:这个Python应用的启动命令
CMD ["python", "app.py"]

默认情况下,Docker 会提供一个隐含的 ENTRYPOINT,即:/bin/sh -c。所以,在不指定 ENTRYPOINT 时,比如这个例子里,实际上运行在容器里的完整进程是:/bin/sh -c "python app.py",即 CMD 的内容就是 ENTRYPOINT 的参数

注意:Dockerfile 中的每个原语执行后,都会生成一个对应的镜像层。即使原语本身并没有明显地修改文件的操作(比如,ENV 原语),它对应的层也会存在。只不过在外界看来,这个层是空的。

可以使用 docker commit 指令,把一个正在运行的容器,直接提交为一个镜像

docker常用命令:


#镜像相关命令:
docker images:列出本地主机上的镜像列表。
docker pull <image_name>:从 Docker 镜像仓库下载镜像。
docker build -t <image_name> <dir_path>:根据 Dockerfile 构建镜像。
docker push <image_name>:将本地镜像推送到镜像仓库。
#容器相关命令:
docker ps:列出正在运行的容器。
docker ps -a:列出所有容器,包括已停止的容器。
docker run <image_name>:根据镜像创建并启动一个容器。
docker stop <container_id>:停止一个运行中的容器。
docker rm <container_id>:删除一个已停止的容器。
#日志和进入容器命令:
docker logs <container_id>:查看容器的日志。
docker exec -it <container_id> <command>:在运行中的容器中执行命令,并进入容器的 shell 终端。
#网络相关命令:
docker network ls:列出 Docker 网络。
docker network create <network_name>:创建一个自定义网络。
docker network connect <network_name> <container_name>:将容器连接到指定网络。
#数据卷相关命令:
docker volume ls:列出 Docker 数据卷。
docker volume create <volume_name>:创建一个数据卷。
docker volume inspect <volume_name>:查看数据卷的详细信息。
docker volume rm <volume_name>:删除一个数据卷。
#查看docker容器运行的进程号
docker inspect --format '{{ .State.Pid }}'  4ddf4638572d
25686

docker exec 是怎么做到进入容器里的?

每个进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。

setns() 的 Linux 系统调用

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)

int main(int argc, char *argv[]) {
    int fd;
    
    fd = open(argv[1], O_RDONLY);
    if (setns(fd, 0) == -1) {
        errExit("setns");
    }
    execvp(argv[2], &argv[2]); 
    errExit("execvp");
}
  • argv[1],即当前进程要加入的 Namespace 文件的路径/proc/25686/ns/net
  • argv[2]则是你要在这个 Namespace 里运行的进程 /bin/bash。

open() 系统调用打开了指定的 Namespace 文件,并把这个文件的描述符 fd 交给 setns() 使用。在 setns() 执行后,当前进程就加入了这个文件对应的 Linux Namespace 当中了。

数据卷Volumn

$ docker run -v /test ... 
$ docker run -v /home:/test ...
  • 命令1:由于你并没有显示声明宿主机目录,那么 Docker 就会默认在宿主机上创建一个临时目录 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的 /test 目录上。
  • 命令2,Docker 就直接把宿主机的 /home 目录挂载到容器的 /test 目录上。

原理:在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。

由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。

注意:这里提到的"容器进程",是 Docker 创建的一个容器初始化进程 (dockerinit),而不是应用进程 (ENTRYPOINT + CMD)。dockerinit 会负责完成根目录的准备、挂载设备和目录、配置 hostname 等一系列需要在容器内进行的初始化操作。最后,它通过 execv() 系统调用,让应用进程取代自己,成为容器里的 PID=1 的进程。

另外,由于容器的镜像操作,比如 docker commit,都是发生在宿主机空间的。而由于 Mount Namespace 的隔离作用,宿主机并不知道这个绑定挂载的存在。所以,在宿主机看来,容器中可读写层的 /test 目录(/var/lib/docker/aufs/mnt/[可读写层 ID]/test),始终是空的。所以虽然这个 /test 目录里的内容,挂载在容器 rootfs 的可读写层,但是不会被 docker commit 提交

Linux 的绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响。

容器和虚拟机的对比

  • 虚拟机实现了更深层次的资源隔离,而docker还是依赖宿主机操作系统的隔离性,多个容器共享的是同一个宿主机的操作系统内核。不能跨操作系统运行docker容器。另外还有很多对象是不能被namespace化的,比如时间
  • 虚拟机必须要创建一个完整的操作系统,带来额外的资源消耗与占用,docker更加轻量级(敏捷,高性能)

扩展博客:Docker基础技术:Linux Namespace(上) | 酷 壳 - CoolShell

遗留问题:

1 容器和应用共生死,生命周期

2 /proc文件系统,不了解cgroup的限制,在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据

相关推荐

  1. Docker in Docker (DinD): 深入探索实际应用

    2024-03-13 03:56:03       39 阅读
  2. Docker容器基础Docker安装基本使用

    2024-03-13 03:56:03       30 阅读
  3. 深入探索MySQL的虚拟列:发展、原理应用

    2024-03-13 03:56:03       50 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-13 03:56:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-13 03:56:03       100 阅读
  3. 在Django里面运行非项目文件

    2024-03-13 03:56:03       82 阅读
  4. Python语言-面向对象

    2024-03-13 03:56:03       91 阅读

热门阅读

  1. `PF_NETLINK` 是用于与内核通信的Socket族之一

    2024-03-13 03:56:03       41 阅读
  2. effective c++ 笔记 条款49-52

    2024-03-13 03:56:03       36 阅读
  3. 【笔记】道路不平度的功率谱密度计算时的问题

    2024-03-13 03:56:03       42 阅读
  4. MogDB/openGauss关于PL/SQL匿名块调用测试

    2024-03-13 03:56:03       38 阅读
  5. 从菜鸟到大师细看程序员的五种层次

    2024-03-13 03:56:03       39 阅读
  6. 抓包是什么?我们为什么要抓包?

    2024-03-13 03:56:03       39 阅读
  7. Vue中怎么使用router进行页面传参

    2024-03-13 03:56:03       44 阅读
  8. 用游戏面试应聘者的方法

    2024-03-13 03:56:03       45 阅读