【容器的前世今生-1】虚拟化技术与文件隔离

Demon.Lee 2021年07月22日 897次浏览

本文是对周志明老师《凤凰架构》一书中关于容器崛起小章节的学习笔记。

本文实践环境:
Operating System: CentOS Linux release 7.9.2009 (Core)
Kernel: Linux 3.10.0
Architecture: x86-64
Docker Client/Server Version: 20.10.7

在有了以 docker 为代表的容器技术的今天,我们在一台 Linux 机器上运行某个程序通常只需要两步(当然,前提是已经安装好了 docker 程序。不过还好,这个程序只需要安装一次就够了):
第一步:通过 docker pull 命令下载对应的镜像;
第二步:通过 docker run 命令运行镜像。
程序相关的配置,依赖的第三方库等,都在镜像中打包好了,这大大简化了运维(系统)管理员的工作。可在没有容器技术的时代,软件的部署过程就没有这么爽了。

让我们把时间拨回到 2013 年,此时容器技术还没火,大厂们还在 PaaS 领域摸索前行。在这时,如果要部署一套软件,运维(系统)管理员要做哪些事情呢?Ta 需要在准确的操作系统上,按照软件部署说明文档:安装第三方依赖库,修改好配置文件,配置对应的资源权限等等,然后启动可执行程序,将服务跑起来。一次性就能安装好那真是不错,很多时候,都会在某个步骤中冒出几个错误来,让人大跌眼镜。而最痛苦的事,莫过于对一个大程序进行版本升级,修复补丁啥的,出错的概率就更高了。那为啥会出错呢,这就不得不从软件的兼容性说起。

软件兼容性

一个软件要能正确的运行,需要满足 3 个方面的兼容性:

  • ISA(Instruction Set Architecture) 兼容:指的是目标机器指令集兼容性,比如在 ARM 架构的机器上编译的程序是无法在 X86 架构的机器上运行的。

    维基百科:指令集架构(英语:Instruction Set Architecture,缩写为ISA),又称指令集或指令集体系,是计算机体系结构中与程序设计有关的部分,包含了基本数据类型,指令集,寄存器,寻址模式,存储体系,中断,异常处理以及外部I/O。指令集架构包含一系列的 opcode 即操作码(机器语言),以及由特定处理器执行的基本命令。不同的处理器“家族” —— 例如Intel IA-32和x86-64、IBM/Freescale Power和ARM处理器家族——有不同的指令集架构。

  • ABI 兼容:目标系统或依赖库的二进制兼容性,比如在 Linux 系统上编译的二进制程序是无法在 Windows 系统上运行的。

    维基百科:在软件开发中,应用二进制接口(英语:application binary interface,缩写为ABI)是指两程序模块间的接口;通常其中一个程序模块会是库或操作系统所提供的服务,而另一边的模块则是用户所运行的程序。 一个ABI定义了机器代码如何访问数据结构与运算程序,此处所定义的界面相当低端并且相依于硬件。而类似概念的API则在源代码定义这些,较为高端,并不直接相依于硬件,通常会是人类可阅读的代码。ABI不同于应用程序接口(API),API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译,然而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。

  • 环境兼容:程序所依赖环境的兼容性,比如配置文件、环境变量、数据库地址、文件存储等,只要其中一个环境因素没有配置正确,程序就无法正常运行。

那么有哪些虚拟化技术可以实现以上 3 个方面的兼容性,各类技术之间又有哪些优缺点呢?

虚拟化技术分类

这里依据抽象目标与兼容性高低的不同,对虚拟化技术类型整理如下表所示:

类型说明代表技术优势劣势备注
ISA Level Virtualization指令集虚拟化,即仿真。通过软件来模拟 ISA 架构的处理过程,该软件会将虚拟机发出的指令转换为本机 ISA 的指令QEMU,Bochs几乎不受局限的兼容性每条指令都要由软件来转换和模拟,性能损失大运行完整的 OS,ABI 兼容性和环境兼容性真实存在,ISA 兼容性则通过模拟实现
Hardware Abstraction Level Virtualization硬件抽象层虚拟化,即以软件或硬件来模拟计算机架构(cpu、memory、显卡、磁盘控制器和芯片组等)的工作过程。VMware ESXi,Hyper-V同上占用宿主机资源大同上。另外,默认情况下,大家说的“虚拟机”指的就是这一类
OS Level Virtualization操作系统层虚拟化,即容器化。各个进程共享内核,但拥有独立的系统资源Docker, containerd轻量级虚拟化,启动速度快,性能好,占用资源少牺牲一定的隔离性与兼容性,默认情况下要求内核相同只能提供操作系统内核以上的部分 ABI 兼容性与完整的环境兼容性
Library Level Virtualization运行库虚拟化,用软件翻译的方法来模拟系统,通过一个独立进程来代替操作系统内核WINE,WSL占用宿主机资源不高ABI 兼容性的高低取决于软件的实现,不稳定Wine官方说明:Wine 不是像虚拟机或者模拟器一样模仿内部的 Windows 逻辑,而是將 Windows API 调用翻译成为动态的 POSIX 调用,免除了性能和其他一些行为的内存占用,让你能够干净地集合 Windows 应用到你的桌面。
Programming Language Level Virtualization语言层虚拟化,即由虚拟机将高级语言生成的中间代码,转换为目标机器可以直接执行的指令Java 的 JVM,.NET 的 CLR--不解决 ABI 兼容性和环境兼容性问题,而是将不同环境的差异抽象封装为统一的编程接口,供程序员使用。

不同的虚拟化技术,让我们看到同一个软件想在不同平台上实现一致性的探索。回到当下,容器化技术已经打败了其他的虚拟化方案,成为 PaaS 领域事实标准。那容器化技术的起点是啥呢?

容器崛起之路的起点:文件隔离 chroot

起初,软件开发人员使用容器技术的目的主要有两个:1)隔离各类资源,防止误操作风险;2)充当蜜罐,吸引黑客攻击,监视黑客的行为。容器的起点可以追溯到 1979 年 Unix 第 7 版中的 chroot 命令,下面将相关发展历程简单总结:

  • 1979 年 Unix 第 7 版发布 chroot 命令(change root),其作用是当某个进程使用 chroot 操作后,该进程的根目录就会锁定在命令参数所指定的位置,后续该进程及其子进程就无法访问和操作该目录之外的其他文件。
  • 1991 年世界上第一个监控黑客行动的蜜罐程序由 chroot 实现。
  • 2000 年 FreeBSD 4.0 系统重新实现了 chroot 命令,将其作为进程沙箱隔离的基础。
  • 2000 年 Linux Kernel 2.3.41 版引入了 pivot_root 技术来实现文件隔离,pivot_root 直接切换了根文件系统,避免了 chroot 命令可能出现的安全性漏洞。
  • 2007 年发布的 iOS 又以 FreeBSD 为基础,此时黑客们把绕过 iOS 沙箱机制称为越狱,即以 root 权限安装任意程序。

目前使用的主要容器技术,如 LXC,Docker 等,都是优先使用 pivot_root 来实现根文件系统切换的。但不管是 chroot ,还是 pivot_root, 都不能提供完美的隔离性。究其原因,不管是底层的硬件资源(如磁盘,网络,内存,处理器等),还是经操作系统封装的高层次资源(如 unix 分时,进程 ID,用户 ID,进程间通信等)都存在着大量非文件形式暴露的入口,所以无法通过切换根文件系统来实现彻底的隔离。

下面,笔者就以 chroot 命令为例,简要实践一下其功能。运行 man 1 chroot 可以得到如下内容:

NAME
       chroot - run command or interactive shell with special root directory

SYNOPSIS
       chroot [OPTION] NEWROOT [COMMAND [ARG]...]
       chroot OPTION

DESCRIPTION
       Run COMMAND with root directory set to NEWROOT.

       --userspec=USER:GROUP
              specify user and group (ID or name) to use

       --groups=G_LIST
              specify supplementary groups as g1,g2,..,gN

       --help display this help and exit

       --version
              output version information and exit

       If no command is given, run '${SHELL} -i' (default: '/bin/sh -i').

       GNU coreutils online help: <http://www.gnu.org/software/coreutils/> Report chroot translation bugs to <http://translationproject.org/team/>

在当前某个目录下新建一个 chroot-rootfs 文件夹,然后使用该文件夹作为新的根文件系统:

[demonlee@localhost blog-dev]$ mkdir -p  chroot-rootfs
[demonlee@localhost blog-dev]$ ls -l
total 0
drwxr-xr-x. 12 demonlee demonlee 137 Jul 22 11:16 chroot-rootfs
[demonlee@localhost blog-dev]$ sudo chroot chroot-rootfs ls /
chroot: failed to run command ‘ls’: No such file or directory
[demonlee@localhost blog-dev]$ 

此时,我们发现 ls 命令不存在,那是因为当前的根文件系统 chroot-rootfs 没有对应的可执行文件。为此,我们可以使用 docker 镜像 busybox 中的根文件系统:

[demonlee@localhost blog-dev]$ docker pull busybox:stable
stable: Pulling from library/busybox
b71f96345d44: Pull complete 
Digest: sha256:0f354ec1728d9ff32edcd7d1b8bbdfc798277ad36120dc3dc683be44524c8b60
Status: Downloaded newer image for busybox:stable
docker.io/library/busybox:stable
[demonlee@localhost blog-dev]$ docker export $(docker create busybox) | tar -C chroot-rootfs  -xvf -)
[demonlee@localhost blog-dev]$ ls ./chroot-rootfs 
bin  dev  etc  home  proc  root  sys  tmp  usr  var
[demonlee@localhost blog-dev]$ du -sh *
1.3M	chroot-rootfs
[demonlee@localhost blog-dev]$ 

通过 docker 命令,将镜像中的文件导出到当前的 chroot-rootfs 目录中(可以看到该 rootfs 非常轻量),此时再运行相关命令,并与宿主机上的命令进行对比:

[demonlee@localhost blog-dev]$ ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
[demonlee@localhost blog-dev]$ sudo chroot chroot-rootfs ls /
[sudo] password for demonlee: 
bin   dev   etc   home  proc  root  sys   tmp   usr   var
[demonlee@localhost blog-dev]$ pwd
/home/demonlee/ispace/blog-dev
[demonlee@localhost blog-dev]$ sudo chroot chroot-rootfs pwd
/
[demonlee@localhost blog-dev]$ 

可以看到,通过 chroot 命令我们成功实现了 rootfs 的切换。

隔离访问与隔离资源:Namespaces and Cgroups

关于 Namespaces and Cgroups 的相关内容,笔者在容器的基本操作和实现原理这篇文章中已梳理过,这里就不再赘述。

总结

从虚拟化技术需要实现的技术难点,到每种虚拟化技术的产生,通过这种方式,逐步梳理容器技术的前世今生。只有挖到根,我们才能真正的理解为何现在的容器技术是这个样子。

参考文献