Docker容器实战(七) - Docker存储隔离原理?(上)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: Docker容器实战(七) - Docker存储隔离原理?(上)

容器为什么需要进行文件系统隔离呢?

  • 被其他容器篡改文件,导致安全问题
  • 文件的并发写入造成的不一致问题


Linux容器通过Namespace、Cgroups,进程就真的被“装”在了一个与世隔绝的房间里,而这些房间就是PaaS项目赖以生存的应用“沙盒”。墙内的它们是怎样的生活呢?

1 容器里的进程眼中的文件系统

也许你会认为这是一个Mount Namespace的问题

容器里的应用进程,理应看到一份完全独立的文件系统。这样,它就可以在自己的容器目录(比如/tmp)下操作,而完全不会受宿主机以及其他容器的影响。


那么,真实情况是这样吗?


“左耳朵耗子”叔在多年前写的一篇关于Docker基础知识的博客里,曾经介绍过一段小程序。

这段小程序的作用是,在创建子进程时开启指定的Namespace。


下面,我们不妨使用它来验证一下刚刚提到的问题。

#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>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
  "/bin/bash",
  NULL
};
int container_main(void* arg)
{  
  printf("Container - inside the container!\n");
  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}
int main()
{
  printf("Parent - start a container!\n");
  int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
  waitpid(container_pid, NULL, 0);
  printf("Parent - container stopped!\n");
  return 0;
}

在main函数里,通过clone()系统调用创建了一个新的子进程container_main,并且声明要为它启用Mount Namespace(即:CLONE_NEWNS标志)。


而这个子进程执行的,是一个“/bin/bash”程序,也就是一个shell。所以这个shell就运行在了Mount Namespace的隔离环境中。


我们来一起编译一下这个程序:


image.png

image.png

这样,我们就进入了这个“容器”当中。可是,如果在“容器”里执行一下ls指令的话,我们就会发现一个有趣的现象: /tmp目录下的内容跟宿主机的内容是一样的。

image.png

即使开启了Mount Namespace,容器进程看到的文件系统也跟宿主机完全一样。

这是怎么回事呢?


Mount Namespace修改的,是容器进程对文件系统“挂载点”的认知

但是,这也就意味着,只有在“挂载”这个操作发生之后,进程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。


这时,你可能已经想到了一个解决办法:创建新进程时,除了声明要启用Mount Namespace之外,我们还可以告诉容器进程,有哪些目录需要重新挂载,就比如这个/tmp目录。于是,我们在容器进程执行前可以添加一步重新挂载 /tmp目录的操作:

int container_main(void* arg)
{
  printf("Container - inside the container!\n");
  // 如果你的机器的根目录的挂载类型是shared,那必须先重新挂载根目录
  // mount("", "/", NULL, MS_PRIVATE, "");
  mount("none", "/tmp", "tmpfs", 0, "");
  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}

可以看到,在修改后的代码里,我在容器进程启动之前,加上了一句mount(“none”, “/tmp”, “tmpfs”, 0, “”)语句。就这样,我告诉了容器以tmpfs(内存盘)格式,重新挂载了/tmp目录。

这段修改后的代码,编译执行后的结果又如何呢?我们可以试验一下:

image.png

可以看到,这次/tmp变成了一个空目录,这意味着重新挂载生效了。我们可以用mount -l检查一下:

image.png

可以看到,容器里的/tmp目录是以tmpfs方式单独挂载的。


更重要的是,因为我们创建的新进程启用了Mount Namespace,所以这次重新挂载的操作,只在容器进程的Mount Namespace中有效。如果在宿主机上用mount -l来检查一下这个挂载,你会发现它是不存在的:


image.png

image.png

这就是Mount Namespace跟其他Namespace的使用略有不同的地方:

它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

可作为用户,希望每当创建一个新容器,容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。怎么才能做到这一点呢?


可以在容器进程启动之前重新挂载它的整个根目录“/”。

而由于Mount Namespace的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。


在Linux操作系统里,有一个名为


chroot(change root file system)

的命令, 改变进程的根目录到指定的位置


假设,我们现在有一个$HOME/test目录,想要把它作为一个/bin/bash进程的根目录。


  • 首先,创建一个test目录和几个lib文件夹:
$ mkdir -p $HOME/test
$ mkdir -p $HOME/test/{bin,lib64,lib}
$ cd $T

  • 然后,把bash命令拷贝到test目录对应的bin路径下:
$ cp -v /bin/{bash,ls} $HOME/test/bin

接下来,把bash命令需要的所有so文件,也拷贝到test目录对应的lib路径下。找到so文件可以用ldd 命令:

$ T=$HOME/test
$ list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
$ for i in $list; do cp -v "$i" "${T}${i}"; done

最后,执行chroot命令,告诉操作系统,我们将使用$HOME/test目录作为/bin/bash进程的根目录:

$ chroot $HOME/test /bin/bash


这时,你如果执行ls /,就会看到,它返回的都是$HOME/test目录下面的内容,而不是宿主机的内容。


更重要的是,对于被chroot的进程来说,它并不会感受到自己的根目录已经被“修改”成$HOME/test了。


这种视图被修改的原理,是不是跟我之前介绍的Linux Namespace很类似呢?

没错!实际上,Mount Namespace正是基于对chroot的不断改良才被发明出来的,它也是Linux操作系统里的第一个Namespace。


当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统, 比如Ubuntu16.04的ISO。这样,在容器启动之后,我们在容器里通过执行"ls /"查看根目录下的内容,就是Ubuntu 16.04的所有目录和文件。

而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。


所以,一个最常见的rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如/bin,/etc,/proc等等:


$ ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var

而你进入容器之后执行的/bin/bash,就是/bin目录下的可执行文件,与宿主机的/bin/bash完全不同。

对Docker项目来说,它最核心的原理实际上就是为待创建的用户进程:

  • 启用Linux Namespace配置
  • 设置指定的Cgroups参数
  • 切换进程的根目录(Change Root)


Docker项目在最后一步的切换上会优先使用pivot_root系统调用,如果系统不支持,才会使用chroot


这两个系统调用虽然功能类似,但是也有细微的区别


rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。

在Linux操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。

那么,对于容器来说,这个


操作系统的“灵魂”在哪

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

如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互

这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发动全身。


这也是容器相比于虚拟机的主要缺陷之一

毕竟后者不仅有模拟出来的硬件机器充当沙盒,而且每个沙盒里还运行着一个完整的Guest OS给应用随便折腾。


不过,正是由于rootfs的存在,容器才有了一个被反复宣传至今的重要特性:

一致性

什么是容器的“一致性”呢?


由于云端与本地服务器环境不同,应用的打包过程,一直是使用PaaS时最“痛苦”的一个步骤。

但有了容器镜像(即rootfs)之后,这个问题被非常优雅地解决了。

由于rootfs里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。


事实上,对于大多数开发者而言,他们对应用依赖的理解,一直局限在编程语言层面。比如Golang的Godeps.json。

但实际上,一个一直以来很容易被忽视的事实是,对一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。


有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:

无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。


这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。

不过,这时你可能已经发现了另一个非常棘手的问题:难道我每开发一个应用,或者升级一下现有的应用,都要重复制作一次rootfs吗?

比如,我现在用Ubuntu操作系统的ISO做了一个rootfs,然后又在里面安装了Java环境,用来部署应用。那么,我的另一个同事在发布他的Java应用时,显然希望能够直接使用我安装过Java环境的rootfs,而不是重复这个流程。


一种比较直观的解决办法是,我在制作rootfs的时候,每做一步“有意义”的操作,就保存一个rootfs出来,这样其他同事就可以按需求去用他需要的rootfs了。

但是,这个解决办法并不具备推广性。原因在于,一旦你的同事们修改了这个rootfs,新旧两个rootfs之间就没有任何关系了。这样做的结果就是极度的碎片化。

那么,既然这些修改都基于一个旧的rootfs,我们能不能以增量的方式去做这些修改呢?

这样做的好处是,所有人都只需要维护相对于base rootfs修改的增量内容,而不是每次修改都制造一个“fork”。

答案当然是肯定的。


这也正是为何,Docker公司在实现Docker镜像时并没有沿用以前制作rootfs的标准流程,而是做了一个小小的创新:

Docker在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量rootfs。

当然,这个想法不是凭空臆造出来的,而是用到


相关实践学习
通过容器镜像仓库与容器服务快速部署spring-hello应用
本教程主要讲述如何将本地Java代码程序上传并在云端以容器化的构建、传输和运行。
Kubernetes极速入门
Kubernetes(K8S)是Google在2014年发布的一个开源项目,用于自动化容器化应用程序的部署、扩展和管理。Kubernetes通常结合docker容器工作,并且整合多个运行着docker容器的主机集群。 本课程从Kubernetes的简介、功能、架构,集群的概念、工具及部署等各个方面进行了详细的讲解及展示,通过对本课程的学习,可以对Kubernetes有一个较为全面的认识,并初步掌握Kubernetes相关的安装部署及使用技巧。本课程由黑马程序员提供。 &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
目录
相关文章
|
21天前
|
数据库 Docker 容器
docker容器为啥会开机自启动
通过配置适当的重启策略,Docker容器可以在主机系统重启后自动启动。这对于保持关键服务的高可用性和自动恢复能力非常有用。选择适合的重启策略(如 `always`或 `unless-stopped`),可以确保应用程序在各种情况下保持运行。理解并配置这些策略是确保Docker容器化应用可靠性的关键。
169 93
|
20天前
|
Java Linux C语言
《docker基础篇:2.Docker安装》包括前提说明、Docker的基本组成、Docker平台架构图解(架构版)、安装步骤、阿里云镜像加速、永远的HelloWorld、底层原理
《docker基础篇:2.Docker安装》包括前提说明、Docker的基本组成、Docker平台架构图解(架构版)、安装步骤、阿里云镜像加速、永远的HelloWorld、底层原理
268 89
|
24天前
|
Ubuntu NoSQL 开发工具
《docker基础篇:4.Docker镜像》包括是什么、分层的镜像、UnionFS(联合文件系统)、docker镜像的加载原理、为什么docker镜像要采用这种分层结构呢、docker镜像commit
《docker基础篇:4.Docker镜像》包括是什么、分层的镜像、UnionFS(联合文件系统)、docker镜像的加载原理、为什么docker镜像要采用这种分层结构呢、docker镜像commit
160 70
|
23天前
|
Ubuntu NoSQL Linux
《docker基础篇:3.Docker常用命令》包括帮助启动类命令、镜像命令、有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubuntu镜像演示)、容器命令、小总结
《docker基础篇:3.Docker常用命令》包括帮助启动类命令、镜像命令、有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubuntu镜像演示)、容器命令、小总结
108 6
《docker基础篇:3.Docker常用命令》包括帮助启动类命令、镜像命令、有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubuntu镜像演示)、容器命令、小总结
|
2月前
|
搜索推荐 安全 数据安全/隐私保护
7 个最能提高生产力的 Docker 容器
7 个最能提高生产力的 Docker 容器
126 35
|
22天前
|
数据库 Docker 容器
docker容器为啥会开机自启动
通过配置适当的重启策略,Docker容器可以在主机系统重启后自动启动。这对于保持关键服务的高可用性和自动恢复能力非常有用。选择适合的重启策略(如 `always`或 `unless-stopped`),可以确保应用程序在各种情况下保持运行。理解并配置这些策略是确保Docker容器化应用可靠性的关键。
44 17
|
2月前
|
Ubuntu Linux 开发工具
docker 是什么?docker初认识之如何部署docker-优雅草后续将会把产品发布部署至docker容器中-因此会出相关系列文章-优雅草央千澈
Docker 是一个开源的容器化平台,允许开发者将应用程序及其依赖项打包成标准化单元(容器),确保在任何支持 Docker 的操作系统上一致运行。容器共享主机内核,提供轻量级、高效的执行环境。本文介绍如何在 Ubuntu 上安装 Docker,并通过简单步骤验证安装成功。后续文章将探讨使用 Docker 部署开源项目。优雅草央千澈 源、安装 Docker 包、验证安装 - 适用场景:开发、测试、生产环境 通过以上步骤,您可以在 Ubuntu 系统上成功安装并运行 Docker,为后续的应用部署打下基础。
docker 是什么?docker初认识之如何部署docker-优雅草后续将会把产品发布部署至docker容器中-因此会出相关系列文章-优雅草央千澈
|
22天前
|
运维 Java 虚拟化
《docker基础篇:1.Docker简介》,包括Docker是什么、容器与虚拟机比较、能干嘛、去哪下
《docker基础篇:1.Docker简介》,包括Docker是什么、容器与虚拟机比较、能干嘛、去哪下
91 12
|
2月前
|
存储 Kubernetes 开发者
容器化时代的领航者:Docker 和 Kubernetes 云原生时代的黄金搭档
Docker 是一种开源的应用容器引擎,允许开发者将应用程序及其依赖打包成可移植的镜像,并在任何支持 Docker 的平台上运行。其核心概念包括镜像、容器和仓库。镜像是只读的文件系统,容器是镜像的运行实例,仓库用于存储和分发镜像。Kubernetes(k8s)则是容器集群管理系统,提供自动化部署、扩展和维护等功能,支持服务发现、负载均衡、自动伸缩等特性。两者结合使用,可以实现高效的容器化应用管理和运维。Docker 主要用于单主机上的容器管理,而 Kubernetes 则专注于跨多主机的容器编排与调度。尽管 k8s 逐渐减少了对 Docker 作为容器运行时的支持,但 Doc
154 5
容器化时代的领航者:Docker 和 Kubernetes 云原生时代的黄金搭档
|
23天前
|
Kubernetes Linux 虚拟化
入门级容器技术解析:Docker和K8s的区别与关系
本文介绍了容器技术的发展历程及其重要组成部分Docker和Kubernetes。从传统物理机到虚拟机,再到容器化,每一步都旨在更高效地利用服务器资源并简化应用部署。容器技术通过隔离环境、减少依赖冲突和提高可移植性,解决了传统部署方式中的诸多问题。Docker作为容器化平台,专注于创建和管理容器;而Kubernetes则是一个强大的容器编排系统,用于自动化部署、扩展和管理容器化应用。两者相辅相成,共同推动了现代云原生应用的快速发展。
97 11