iAfoot(4780) 关于容器技术

218 阅读24分钟

容器技术是Docker的一项基础技术

容器技术的前世今生

关于容器技术

容器技术,又称为容器虚拟化,字面上看它首先是一种虚拟化技术。在如今的技术浪潮下,虚拟化技术层出不穷,包括硬件虚拟化、半虚拟化、操作系统虚拟化等。要知道容器虚拟化是一种操作系统虚拟化,是属于轻量级的虚拟化技术即可。

容器技术之所以受欢迎,一个重要的原因是它已经集成到了Linux内核中,已经被当作Linux内核原生提供的特性。当然在其他平台上也有相应的容器技术,但本书讨论的以及Docker涉及的都是指Linux平台的容器技术。

对于容器,目前并没有一个严格的定义,但普遍认可的说法是,它首先必须是一个相对独立的运行环境,在这一点上,有点类似虚拟机的概念,但又没有虚拟机那样彻底。另外,在一个容器环境内,应该最小化其对外界的影响,比如不能在容器中把host上的资源全部消耗掉,这就是资源控制

一般来说,容器技术主要包括Namespace和Cgroup这两个内核特性。

  • Namespace又称为命名空间(也可翻译为名字空间),它主要做访问隔离。其原理是针对一类资源进行抽象,并将其封装在一起提供给一个容器使用,对于这类资源,因为每个容器都有自己的抽象,而它们彼此之间是不可见的,所以就可以做到访问隔离。
  • Cgroup是control group的简称,又称为控制组它主要是做资源控制。其原理是将一组进程放在一个控制组里,通过给这个控制组分配指定的可用资源,达到控制这一组进程可用资源的目的。

实际上,Namespace和Cgroup并不是强相关的两种技术,用户可以根据需要单独使用它们,比如单独使用Cgroup做资源控制,就是一种比较常见的做法。而如果把它们应用到一起,在一个Namespace中的进程恰好又在一个Cgroup中,那么这些进程就既有访问隔离,又有资源控制,符合容器的特性,这样就创建了一个容器。

容器技术的历史

上文提到容器技术属于一种操作系统虚拟化,事实上,其最早的原型可以简化为对目录结构的简单抽象,如图2-1所示。

图2-1所示为在普通的目录结构中创建一个完整的子目录结构。这种抽象化目录结构的出现最早源于1982年,那时通过chroot技术把用户的文件系统根目录切换到某个指定的目录下,实现了简单的文件系统视图上的抽象或虚拟化。但是这种技术只是提供了有限的文件系统隔离,并没有任何其他隔离手段,而且人们后来发现这种技术并不安全,用户可以逃离设定的根目录从而访问host上的文件。

针对上面提到的安全性问题,在2000年,内核版本2.3.41引入了pivot_root技术,它可以有效地避免chroot带来的安全性问题。今日的容器技术,比如LXC、Docker等,也都使用了pivot_root来做根文件系统的切换。然而pivot_root也仅仅是在文件系统的隔离上做了一些增强,并没有在其他隔离性上有所提高。

同样在2000年左右,市场上出现了一些商业的容器技术,比如Linux-VServer和SWsoft(现在的Odin)开发的Virtuozzo,虽然这些技术相对当时的XEN和KVN,有明显的性能提升,但是因为各种原因,并未在当时引起市场太多的关注。

注意

这里只讨论Linux系统上的容器技术,同时期还有很多有名的非Linux平台的容器技术,比如FreeBSD的jail、Solaris上的Zone等。

到了2005年,同样是Odin公司,在Virtuozzo的基础上发布了OpenVZ技术,同时开始推动OpenVZ中的核心容器技术进入Linux内核主线,而此时IBM等公司也在推动类似的技术,最后在社区的合作下,形成了目前大家看到的Cgroup和Namespace,这时,容器技术才开始逐渐进入大众的视野。

对于Namespace,其各个子系统进入内核的版本号及贡献公司如表2-1所示。

说明:

User Namespace在3.8版本重新实现。

对于Cgroup,其各个子系统进入内核的版本号及贡献公司如表2-2所示。

注意:

以上只列举了早期主要的子系统,较新的子系统如net cls、hugetlb等并未列出。

整个容器的发展历史可以通过图2-2来展示。

随着容器技术在内核主线中的不断成熟和完善,2013年诞生的Docker真正让容器技术得到了全世界技术公司和开发人员的关注,相信容器技术的未来一定会比它的前世和今生更加精彩。

一分钟理解容器

容器的组成

上文已多次提及,容器的核心技术是Cgroup+Namespace,但光有这两个抽象的技术概念是无法组成一个完整的容器的。所有的这一切,都需要有工具来驱动,需要有一个工具来提供用户可操作的接口,来创建一个容器。对于Linux容器的最小组成,可以由以下公式来表示: 容器=cgroup+namespace+rootfs+容器引擎(用户态工具)

其中各项的功能分别为:

  • Cgroup:资源控制。
  • Namespace:访问隔离。
  • rootfs:文件系统隔离。
  • 容器引擎:生命周期控制。

目前市场上所有Linux容器项目都包含以上组件。

容器的创建原理

本节将通过简单的代码抽象,清晰地展示容器的创建原理。

  • 对于代码一,通过clone系统调用,并传入各个Namespace对应的clone flag,创建了一个新的子进程,该进程拥有自己的Namespace。根据以上代码可知,该进程拥有自己的pid、mount、user、net、ipc、uts namespace。
  • 对于代码二,将代码一中产生的进程pid写入各个Cgroup子系统中,这样该进程就可以受到相应Cgroup子系统的控制。
  • 对于代码三,该fun函数由上面生成的新进程执行,在fun函数中,通过pivot_root系统调用,使进程进入一个新的rootfs,之后通过exec系统调用,在新的Namespace、Cgroup、rootfs中执行“/bin/bash”程序。 通过以上操作,成功地在一个“容器”中运行了一个bash程序。

Cgroup介绍

Cgroup是什么

Cgroup是Control group的简写,属于Linux内核提供的一个特性,用于限制和隔离一组进程对系统资源的使用,也就是做资源QoS,这些资源主要包括CPU、内存、block I/O和网络带宽Cgroup从2.6.24开始进入内核主线,目前各大发型版都默认打开了Cgroup特性

实现的角度来看,Cgroup实现一个通用的进程分组的框架,而不同资源的具体管理则是由各个Cgroup子系统实现的。截止到内核4.11版本,Cgroup中实现的子系统及其作用如下:

  • Devices:设备权限控制。
  • cpuset:分配指定的CPU和内存节点。
  • cpu:控制CPU占用率。
  • cpuacct:统计CPU使用情况。
  • memory:限制内存的使用上限。
  • freezer:冻结(暂停)Cgroup中的进程。
  • net_cls:配合ts(traffic controller)限制网络带宽。
  • net_prio:设置进程的网络流量优先级。
  • hugo_tlb:限制HugeTLB的使用。
  • perf_event:允许Perf工具基于Cgroup分组做性能监测。

在Cgroup出现之前,只能对一个进程做一些资源控制,例如通过sched_setaffinity系统调用限定一个进程的CPU亲和性,或者用ulimit限制一个进程的打开文件上限、栈大小等等。另外,使用ulimit可以对少数资源基于用户做资源控制,例如限制一个用户能创建的进程数。而Cgroup可以对进程进行任意的分组,如何分组是用户自定义的,例如安卓的应用分为前台应用和后台应用,前台应用是直接跟用户交互的,需要响应速度快,因此前台应用对资源的申请需要得到更多的保证。为此安卓将前台应用和后台应用划分到不同的Cgroup中,并且对放置前台应用的Cgroup配置了较高的系统资源限额。

提示

从1.6版本开始,Docker也支持ulimit,读者可以查阅相关Docker文档及Linux用户手册。

Cgroup的接口和使用

Cgroup的原生接口通过cgroupfs提供,类似于procfs和sysfs,是一种虚拟文件系统。以下用一个实例演示如何使用Cgroup。

(1)挂载cgroupfs

命令如下:

首先必须将cgroupfs挂载起来,这个动作一般已经在启动时由Linux发行版做好了。可以把cgroupfs挂载在任意一个目录上,不过标准的挂载点是/sys/fs/cgroup

注意:

实际上sysfs里面只有/sys/fs/cgroup目录,并且sysfs是不允许用户创建目录的,这里可以将tmpfs挂载上去,然后在tmpfs上创建目录。

(2)查看cgroupfs

可以看到这里有很多控制文件,其中以cpuset开头的控制文件都是cpuset子系统产生的,其他文件则由Cgroup产生。这里面的tasks文件记录了这个Cgroup的所有进程(包括线程),在系统启动后,默认没有对Cgroup做任何配置的情况下,cgroupfs只有一个根目录,并且系统所有的进程都在这个根目录下,即进程pid都在根目录的tasks文件中

注意:

实际上现在大多数Linux发行版都是采用的systemd,而systemd使用了Cgroup,所以在这些发行版上,当系统启动后看到的cgroupfs是有一些子目录的。

(3)创建Cgroup

通过mkdir创建一个新的目录,也就创建了一个新的Cgroup

(4)配置Cgroup

接下来配置这个Cgroup的资源配额,通过上面的命令,就可以限制这个Cgroup的进程只能在0号CPU上运行,并且只会从0号内存节点分配内存

(5)使能Cgroup

最后,通过将进程id写入tasks文件,就可以把进程移动到这个Cgroup中。并且,这个进程产生的所有子进程也都会自动放在这个Cgroup里。这时,Cgroup才真正起作用了。

提示:

?表示当前进程。另外,也可以把pid写入cgroup.procs中,两者的区别是写入tasks只会把指定的进程加到child中,写入cgroup.procs则会把这个进程所属的整个线程组都加到child中

Cgroup子系统介绍

对实际资源的分配和管理是由各个Cgroup子系统完成的,下面介绍几个主要的子系统。

1. cpuset子系统

cpuset可以为一组进程分配指定的CUP和内存节点

cpuset的主要接口如下。

  • cupset.cups:允许进程使用的CPU列表(例如0~4,9)。
  • cpuset.mems:允许进程使用的内存节点列表(例如0~1)。

2. cpu子系统

cpu子系统用于限制进程的CPU占用率。实际上它有三个功能,分别通过不同的接口来提供。

  • CPU比重分配。这个特性使用的接口是cpu.shares。假设在cgroupfs的根目录下创建了两个Cgroup(C1和C2),并且将cpu.shares分别配置为512和1024,那么当C1和C2争用CPU时,C2将会比C1得到多一倍的CPU占用率。要注意的是,只有当它们争用CPU的cpu share才会起作用,如果C2是空闲的,那么C1可以得到全部的CPU资源。
  • CPU带宽限制。这个特性使用的接口是cpu.cfs_period_us和cpu.cfs_quota_us,这两个接口的单位是微妙。可以将period设置为1秒,将quota设置为0.5秒,那么Cgroup中的进程在1秒内最多只能运行0.5秒,然后就会被强制睡眠,直到进入下一个1秒才会继续运行
  • 实时进程的CPU带宽限制。以上两个特性都只能限制普通进程,若要限制实时进程,就要使用cpu.rt_period_us和cup.rt_runtimes_us这两个接口了。使用方法和上面类似。

3. cupacct子系统

cpuacct子系统用来统计各个Cgroup的CPU使用情况,有如下接口。

  • cpuacct.stat:报告这个Cgroup分别在用户态和内核态消耗的CPU时间,单位是USER_HZ。USER_HZ在x86上一般是100,即1 USER_HZ等于0.01秒。
  • cpuacct.usage:报告这个Cgroup消耗的总CPU时间,单位是纳秒
  • cpuacct.usage_percup:报告这个Cgroup在各个CPU上消耗的CPU时间,总和就是cpuacct.usage的值

4. memory子系统

memory子系统用来限制Cgroup所能使用的内存上限,有如下接口。

  • memory.limit_in_bytes:设定内存上限,单位是字节,也可以使用k/K、m/M或者g/G表示要设置数值的单位,例如

默认情况下,如果Cgroup使用的内存超过上限,Linux内核会尝试回收内存,如果仍然无法将内存使用量控制在上限之内,系统将会触发OOM,选择并“杀死”该Cgroup中的某个进程

  • memory.memsw.limit_in_bytes:设定内存加上交换分区的使用总量。通过设置这个值,可以防止进程把交换分区用光。
  • memory.oom_control:如果设置为0,那么在内存使用量超过上限时,系统不会“杀死”进行,而是阻塞进程直到有内存被释放可供使用时;另一方面,系统会向用户态发送事件通知,用户态的监控程序可以根据该事件来做相应的处理,例如提高内存上限等。
  • memory.stat:汇报内存使用信息

5. blkio子系统

blkio子系统用来限制Cgroup的block I/O带宽,有如下接口。

  • blkio.weight:设置权重值,范围在100到1000之间。这跟cpu.shares类似,是比重分配,而不是绝对带宽的限制,因此只有当不同的Cgroup在争用同一块设备的带宽时才会起作用。
  • blkio.weight_device:对具体的设备设置权重值,这个值会覆盖上述的blkio.weight。例如将Cgroup对/dev/sda的权重设为最小值:
  • blkio.throttle.read_bps_device:对具体的设备,设置每秒读磁盘的带宽上限。例如对/dev/sda的读带宽限制在1MB/秒:
  • blkio.throttle.write_bps_device:设置每秒写磁盘的带宽上限。同样需要指定设备。
  • blkio.throttle.read_iops_device:设置每秒读磁盘的IOPS上限。同样需要指定设备。
  • blkio.throttle.write_iops_device:设置每秒写磁盘的IOPS上限。同样需要指定设备。

注意:

blkio子系统有两个重大的缺陷。一是对于比重分配,只支持CFQ调度器。另一个缺陷是,不管是比重分配还是上限限制,都只支持Direct-IO,不支持Buffered-IO,这使得blkio cgroup的使用场景很有限,好消息是Linux内核社区正在解决这个问题。

6. devices子系统

devices子系统用来控制Cgroup的进程对那些设备有访问权限,有如下接口。

  • devices.list。只读文件,显示目前允许被访问的设备列表。每个条目都有3个域,分别为:
  • 类型:可以是a、c或b,分别表示所有设备、字符设备和块设备
  • 设备号:格式为major:minor的设备号。
  • 权限:r、w和m的组合,分别表示可读、可写、可创建设备结点(mknod)。

例如“a*: *rmw”,表示所有设备都可以被访问,而“c 1: 3r”,表示对1:3这个字符设备(即/dev/null)只有读权限。

  • devices.allow。只写文件,以上面描述的格式写入该文件,就可以允许相应的设备访问权限。
  • devices.deny。只写文件,以上面描述的格式写入该文件,就可以禁止相应的设备访问权限。

Namespace介绍

Namespace是什么

Namespace是将内核的全局资源做封装,使得每个Namespace都有一份独立的资源,因此不同的进程在各自的Namespace内对同一种资源的使用不会互相干扰

这样的解释可能不清楚,举个例子,执行sethostname这个系统调用时,可以改变系统的主机名,这个主机名就是一个内核的全局资源。内核通过实现UTS Namespace,可以将不同的进程分割在不同的UTS Namespace中,在某个Namespace修改主机名时,另一个Namespace的主机名还是保持不变。

目前Linux内核总共实现了6种Namespace:

  • IPC:隔离System VIPC和POSIX消息队列。
  • Network:隔离网络资源。
  • Mount:隔离文件系统挂载点。
  • PID:隔离进程ID。
  • UTS:隔离主机名和域名。
  • User:隔离用户ID和组ID。

Namespace的接口和使用

对Namespace的操作,主要是通过clone、setns和unshare这3个系统调用来完成的

clone可以用来创建新的Namespace。它接受一个叫flags的参数,这些flag包括CLONE_NEWNS、CLONE_NEWIPC、CLONE_NEWUTS、CLONE_NEWNET、CLONE_NEWPID和CLONE_NEWUSER,我们通过传入这些CLONE_NEW*来创建新的Namespace。这些flage对应的Namespace都可以从字面上看出来,除了CLONE_NEWNS,这是用来创建Mount Namespace的。指定了这些flag后,由clone创建出来的新进程,就位于全新的Namespace里了,并且很自然地这个新进程以后创建出来的进程,也都在这个Namespace中。

提示:

Mount Namespace是第一个实现的Namespace,当初实现时并不是为了实现Linux容器,因此也没有预料到会有新的Namespace出现,因此用了CLONE_NEWNS而不是CLONE_NEWMNT之类的名字。

那么,能不能为已有的进程创建新的Namespace呢?答案是可以,unshare就是用来达到这个目的的。调用这个系统调用的进程,会被放进新创建的Namespace里,要创建什么Namespace由flags参数指定,可以使用flag也就是上面提到的那些。

以上两个系统调用都是用来创建新的Namespace的,而setns则可以将进程放到已有的Namespace里,问题是如何指定已有的Namespace?答案在procfs里。每个进程在procfs下都有一个目录,在哪里就有Namespace相关的信息,如下。

这里每个虚拟文件都对应了这个进程所处的Namespace。因此,如果另一个进程要进入这个进程的Namespace,可以通过open系统调用打开这里面的虚拟文件并得到一个文件描述符,然后把文件描述符传给setns,调用返回成功的话,就进入这个进程的Namespace了。

提示:

docker exec命令的实现原理就是setns。

以下是一个简单的程序,在Linux终端调用这个程序就会进入新的Namespace,同时也可以打开另一个终端,这个终端是在host的Namespace里,这样就可以对比两个Namespace的区别了。

这个程序创建了UTS Namespace,可以通过修改flag,创建其他Namespace,也可以创建几个Namespace的组合。这个程序将会用来为下面的内容做演示。

各个Namespace介绍

1. UTS Namespace

UTS Namespace用于对主机名和域名进行隔离,也就是uname系统调用使用的结构体struct utsname里的nodename和domainname这两个字段,UTS这个名字也是由此而来的。

那么,为什么要使用UTS Namespace做隔离?这是因为主机名可以用来代替IP地址,因此,也就可以使用主机名在网络上访问某台机器了,如果不隔离,这个机制在容器里就会出问题。

调用之前的程序后,在Namespace终端执行以下命令:

这里已经改变了主机名,现在通过host终端来看看host的主机名:

可以看到,host的主机名并没有变化,这就是Namespace所起的作用。

2. IPC Namespace

IPC是Inter-Process Communication的简写,也就是进程间通信。Linux提供了很多种进程间通信的机制,IPC Namespace针对的是SystemV IPC和Posix消息队列。这些IPC机制都会用到标识符,例如用标识符来区别不同的消息队列,然后两个进程通过标识符找到对应的消息队列进行通信等。

IPC Namespace能做到的事情是,使相同的标识符在两个Namespace中代表不同的消息队列,这样也就使得两个Namespace中的进程不能通过IPC进程通信了。

举个例子,在namespace终端创建一个消息队列:

3. PID Namespace

PID Namespace用于隔离进程PID号,这样一来,不同的Namespace里的进程PID号就可以是一样的了。

当创建一个PID Namespace时,第一个进程的PID号是1,也就是init进程。init进程有一些特殊之处,例如init进程需要负责回收所有孤儿进程的资源。另外,发送给init进程的任何信号都会被屏蔽,即使发送的是SIGKILL信号,也就是说,在容器内无法“杀死”init进程。

但是当用ps命令查看系统的进程时,会发现竟然可以看到host的所有进程:

这是因为ps命令是从procfs读取信息的,而procfs并没有得到隔离。虽然能看到这些进程,但由于它们其实是在另一个PID Namespace中,因此无法向这些进程发送信号:

4. Mount Namespace

Mount Namespace用来隔离文件系统挂载点,每个进程能看到的文件系统都记录在/proc/?/mounts里。在创建一个新的Mount Namespace后,进程系统对文件系统挂载/卸载的动作就不会影响到其他Namespace。

之前看到的,创建PID Namespace后,由于procfs没有改变,因此通过ps命令看到的仍然是host的进程树,其实可以通过在这个PID Namespace里挂载procfs来解决这个问题,如下:

但此时由于文件系统挂载点没有隔离,因此host看到的procfs也会是这个新的procfs,这样在host上就会出问题:

可如果同时使用Mount Namespace和PID Namespace,新的Namespace里的进程和host上的进程将会看的各自的procfs,故而也就不存在上面的问题了。

5. Network Namespace

这个Namespace会对网络相关的系统资源进行隔离,每个Network Namespace都有自己的网络设备、IP地址、路由表、/proc/net目录、端口号等。网络隔离的必要性是很明显的,举一个例子,在没有隔离的情况下,如果两个不同的容器都想运行同一个Web应用,而这个应用又需要使用80端口,那就会有冲突了。

新创建的Network Namespace会有一个loopback设备,除此之外不会有任何其他网络设备,因此用户需要在这里面做自己的网络配置。IP工具已经支持Network Namespace,可以通过它来为新的Network Namespace配置网络功能。首先创建Network Namespace:

使用“ip netns exec”命令可以对特定的Namespace执行网络管理:

看到确实只有loopback这个网络接口,并且它还因处于DOWN状态而不可用:

通过以下命令可以启用loopback网络接口:

最后可以这样删除Namespace:

容器的网络配置是一个很大的话题,后面有专门的章节讲解,因此这里暂不展开。

6. User Namespace

User Namespace用来隔离用户和组ID,也就是说一个进程在Namespace里的用户和组ID与它在host里的ID可以不一样,这样说可能读者还不理解有什么实际的用处。User Namespace最有用的地方在于,host的普通用户进程在容器里可以是0号用户,也就是root用户。这样,进程在容器内可以做各种特权操作,但是它的特权被限定在容器内,离开了这个容器它就只有普通用户的权限了。

注意:

容器内的这里root用户,实际上还有很多特权操作不能执行,基本上如果这个特权操作会影响到其他容器或者host,就不会被允许。

在host上,可以看到我们是lizf用户。

现在创建新的User Namespace,看看又是什么情况?

可以看到,用户名和组名都变了,编程65534,不再是原来的1000和100. 接下来的问题是,怎么设定Namespace和host的UID的映射关系?方法是在创建新的Namespace后,设置这个Namespace里进程的/proc//uid_map。在Namespace终端看到的是这样的:

可以看到uid_map是空的,也就是还没有UID的映射。这可以在host终端上通过root用户设置,如下。

上面命令表示要将[1000, 65536]的UID在Namespace里映射成[0,65536]。再切回到Namespace终端看看:

可以看到,我们成功地将lizf用户映射成容器里的root用户了。对于gid,也可以做类似的操作。

至此,关于Namespace和Cgroup的知识就讲完了,可以看出,Namespace和Cgroup的使用是很灵活的,同时这里面又有不少需要注意的地方,因此直接操作Namespace和Cgroup并不是很容易。正是因为这些原因,Docker通过Libcontainer来处理这些底层的事情。这样一来,Docker只需要简单地调用Libcontainer的API,就能将完整的容器搭建起来。而作为Docker的用户,就更不用操心这些事情了,而只需要学习Docker的使用手册,就能通过一两条简单的Docker命令启动容器。

容器造就Docker

关于容器是否是Docker的核心技术,业界一直存在着争议。有人认为Docker的核心技术是对分层镜像的创新使用,有人认为其核心是统一了应用的打包分发和部署方式,为服务器级别的“应用商店”提供了可能,而这将会是颠覆传统行业的举措。事实上,这一系列创新并不是依赖于容器技术的,基于传统的hypervisor也可以做到,业界也由此诞生了一些开源项目,比如Hyper、Clear Linux等。另外,Docker官方对Docker核心功能的描述“Build,Ship and Run”中也确实没有体现与容器强相关的内容。

尽管如此,笔者依然认为容器是Docker的核心技术之一。

首先从Docker的诞生历史上,它主要是为了完善当时不温不火的容器项目LXC,使用户可以更方便地使用容器,让容器可以更好地应用到项目开发和部署的各个流程中。从一开始LXC就是Docker上的唯一容器引擎也可以看出这一点。所以可以说,Docker是为容器而生的。

另外,更重要的一点,跟Docker一起发展和被大家熟知的,还有叫做“微服务”(micro server)的设计哲学,而这会把容器的优势发挥得更加淋漓尽致。容器作为Linux平台的轻量级虚拟化,其核心优势是跟内核的无缝融合,其在运行效率上的优势和极小的系统开销,与需要将各个组件单独部署的微服务应用完美融合。而且微服务在隔离性问题上更加可控,这也避免了容器相对传统虚拟化在隔离性上的短板。所以,未来在微服务的设计哲学下,容器必将跟Docker一起得到更加广泛的应用和发展。

在理解了容器,理解了容器的核心技术Cgroup和Namespace,理解了容器技术如何巧妙且轻量地实现“容器”本身的资源控制和访问隔离之后,可以看到Docker和容器是一种完美的融合和相辅相成的关系,它们不是唯一的搭配,但一定是最完美的组合。与其说是容器造就了Docker,不如说是它们造就了彼此,容器技术让Docker得到更多的应用和推广,Docker也使得容器技术被更多人熟知。在可预见的未来,它们也一定会彼此促进,共同发展,在全新的解决方案和生态系统中扮演着重要的角色。