大厂都在玩的容器技术到底是什么?

1,428 阅读12分钟

引言

著名杂志《经济学人》曾经评价“没有集装箱,就没有全球化”,可以说集装箱的出现重塑了现代货运体系,实现了交通运输行业的标准化,有效降低物流运输成本,极大提升了货物转运效率。而在云原生领域,容器就相当于集装箱,它使得软件发布以及软件运行隔离实现标准化,引领了云原生基础设施的跨越式发展。从某种意义上来说,容器技术重塑了整个软件供应链。今天就和大家聊聊各个大厂都在玩的容器技术到底是什么。

为什么需要容器技术

在正式介绍容器技术之前,我们先来看下软件领域为什么需要容器技术。一项新技术的出现必定是为了解决当下遇到的的某项具体问题或者说更加提高现有软件运行效率。那我们就来分析下在容器技术出现之前,软件领域到底面临什么样的问题。

在很早很早之前,我们部署服务的时候都是直接部署在硬件服务器上。如果想对服务进行扩容就必须要购买服务器,然后再进行应用部署以及各种繁琐的环境配置以及服务配置,由于都是人工操作所以还特别容易出错,不仅浪费时间还很费程序猿,因此服务部署以及迁移效率都极其低下。在互联网早期的时候,用户数以及业务体量还不是很大,人工操作还能够应付得过来。但是随着业务规模不断发展以及用户数的爆炸式增长,这样的软件服务生产方式已经无法满足业务高速发展的需求。将应用服务直接部署在服务器上主要有以下三方面的问题。

image.png

服务互干扰

一台服务器一般不会只部署一个服务应用,都是部署多个服务应用。但是由于这些服务都是公用服务器中的CPU、内存、硬盘以及网络IO等服务器资源,那么必定就会存在资源互相争用、服务互相影响的情况。

资源利用低

业务是存在高峰期和低谷期的,对于电商平台来说,一般深夜的属于业务低谷期,这个时候的服务器的资源利用率相比业务高峰期的时候要低很多。因此在业务低谷期,实际服务器的资源利用率比较低,不能物尽其用。

迁移扩展难

原有的服务器数量不足以应对高速发展的业务时,就需要不断的进行服务器实例扩充,但是由于服务直接部署在服务器中,在进行服务迁移扩展的时候,需要各种依赖库、环境配置以及网络配置等,步骤复杂,扩展困难。

正是软件领域面临这么多问题,因此大神们才会发挥他们的聪明才智不断推进技术发展。因此大神们就设想如果有一种部署方式可以实现差别的服务构建,那就可以解决服务部署的各种配置问题。如果有一种技术可实现真正的资源隔离,进程之间互相不影响,这样就可以解决互相影响的问题,那将是多么美好的一件事情。这些美好的技术设想实际就是容器技术发展的原动力。当然技术的发展并不是一蹴而就的,总是随着时间的推移不断进行完善。

容器技术的思想最早可以追溯到1979年,这一年Unix版本V7发布,在这个版本中作者发明了chroot系统调用,通过它可以实现改变一个进程及其子进程的根目录到另外一个目录下,为进程指定一个单独的、新的文件系统上下文环境,可见在很早的时候Unix的大神们已经有了进行进程隔离的意识和思想了。

那么到底什么叫进程隔离呢?举个栗子大家一看就明白,相信很多同学都使用过tomcat这个web容器,我们可以在tomcat中部署war服务。假设我们有3 个服务都部署在了1个tomcat实例中,假如我们需要重启其中的某个服务,我们就需要重启整个tomcat,那么tomact中的3个服务都会被重启。重启一个服务影响其他2个服务,服务操作存在高度的耦合。但是如果我们把三个服务部署到三个不同的tocmat容器实例中,那么重启任何一个服务都不会影响到其他两个服务,实现了服务的独立管理。

image.png

通过部署多个实例,我们实现了服务之间的进程隔离,而进程拥有独立的地址空间以及执行上下文。但是这种形式的独立管理并不是真正意义上的独立管理,为什么这么说呢?因为实际上他们还是共用服务器的CPU、内存以及IO等服务器资源。假如Tomcat1占用的服务器内存高了,那么剩余给Tomcat2以及Tomcat2的内存分配就相对来说会变少。因此实际上这三个Tomcat虽然是独立的进程但还是会相互影响。有没有办法实现真正的独立,不互相影响呢?

实际上实现资源隔离的方式大概有硬件虚拟化、OS虚拟化以及硬件分区等几种常见的实现方式。但是综合各方面的表现,OS虚拟化成为后期容器技术发展的主流技术路线。

容器技术的解决的核心问题就是实现软件运行时的环境隔离,通过容器构建一个标准的、无差别的服务运行环境,这样就不会因为环境、配置以及依赖等原因造成的在这台服务器上好好的,在另外的一台服务器上又不行的尴尬问题。2008年的时候,通过将Cgroups的资源管理能力以及Namespace的视图隔离能力糅合在一起,Linux Container被合入linux主线,Linux Container是Linux系统提供的容器技术,能提供轻量级的虚拟化能力,能够进行隔离进程和资源。通过这种OS层面的虚拟化技术,实际上也就是解决了容器的核心问题即为如何实现服务运行时的隔离。因此可以说Linux Container是后期实现Docker技术的基础。

在2013年,Docker正式发布。Docker是基于Linux Container技术发展而来的,它的口号是:“Build,Ship and Run Any App,Anywhere”。Docker创新构建了一种全新的软件打包、软件分发以及软件运行的机制,它通过容器镜像,将应用服务本身以及运行服务所需要的环境、配置、资源文件以及依赖库等都打包成一个唯一版本的软件镜像包。往后在任何地方运行的服务都是基于这个软件镜像包来进行构建和运行的,真正解决了如何高效发布软件以及如何高效运行软件的两大核心问题。关于Docker,后面会有专门的文章进行介绍。

image.png

类似下图这种虚拟机与容器的对比图相信大家都看过,左边的部分就是虚拟机的大致原理,实际上是通过Hypervisor实现服务器硬件资源的虚拟化,从而在服务器中模拟出来具备CPU、内存、硬盘等完整计算机硬件基础设施同时还有Guest OS,简单理解就是在服务器中派生出了新的虚拟服务器。用户的应用服务进程都是运行在这些虚拟出来的计算机资源当中的。同一台服务器可以同时运行多个操作系统,各个操作系统之间是相互隔离的,虽然安全性隔离性都很完备,但是大家应该能看得出来,硬件虚拟化需要额外的性能开销,因此它是一种非常重的资源隔离技术。

image.png (图片来自于网络) 

容器技术原理

前面和大家简要介绍了容器技术的发展,我们都知道了容器最核心的是实现了应用服务资源隔离。那么到底容器是如何实现资源隔离的呢?实际上它依赖了Namespace(命名空间)、Cgroups(控制组)底层Linux内核技术。

image.png

Namespace

我们先来看下wiki中关于Linux Namespace的描述:

Namespaces are a feature of the Linux kernel that partitions kernel resources such that one set of processes sees one set of resources while another set of processes sees a different set of resources. The feature works by having the same namespace for a set of resources and processes, but those namespaces refer to distinct resources.

这描述看上去就很绕,总结一下就是Linux Namespace是Linux kernel提供的一种进行资源隔离的底层能力。通过Namespace实现对服务器全局资源的封装隔离,使得不同Namespace中的进程互相独立,彼此透明。

如下图所示,在一台宿主服务器当中,Linux的Namespace实际就是Linux内核中资源隔离的实现方式。玩过Docker的同学都知道,到我们run了一个docker镜像之后,在服务器中就会产生一个docker容器,当我们进入到容器里面去之后,使用ps命令查看,我们会惊奇的发现容器中运行的服务pid=1。相当于这个服务是容器内的第一号进程。而如果我们在服务器中运行这个服务,操作系统会给这个服务进程分配一个全局唯一的进程号,假设是34134。同样是这个程序在服务器中运行pid是34134,但是在Docker容器中的pid却是1。这是怎么回事呢?

image.png

这种隔离技术就是Namspace机制,通过Namespace构建了一个全新的运行环境,与其他运行环境互相透明,让服务在这个小空间里面自封为王。实际上是Linux kernel内核提供的系统调用函数clone()。通过clone函数创建的新进程会在一个全新的进程空间当中。因此实际上容器的本质还是进程,只不过是一种特殊的进程,在创建它的时候指定了一些参数,使得容器只能访问到当前Namespace内的文件、IO等资源。

int pid = clone(main_function, stack_size, SIGCHLD, NULL);

Namespace除了PID还实现了其他不同资源级别的隔离。

名称        宏定义             隔离内容\
IPC         CLONE_NEWIPC      System V IPC, POSIX message queues (since Linux 2.6.19)\
Network     CLONE_NEWNET      Network devices, stacks, ports, etc. (since Linux 2.6.24)\
Mount       CLONE_NEWNS       Mount points (since Linux 2.4.19)\
PID         CLONE_NEWPID      Process IDs (since Linux 2.6.24)\
User        CLONE_NEWUSER     User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)\
UTS         CLONE_NEWUTS      Hostname and NIS domain name (since Linux 2.6.19)

Cgroups

Namespace机制帮助我们解决了资源隔离的问题,那么仅仅只是隔离对于容器运行来说就够了吗?虽然在容器内部应用服务可能是个王者,资源都是他独享的,但是映射到服务器内核当中的真实的进程后它就是个各个普普通通的青铜,需要和其他青铜共享计算机各类资源。显然这不是我们想要的容器的效果。而Linux Cgroups技术就是帮助我们设置资源限制的功能。Linux Cgroups即Linux Control Group就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

在Linux操作系统中,Cgroups的能力通过内核的文件系统操作接口暴露出来的,它是以文件和目录的方式组织在操作系统的/sys/fs/cgroup路径下。在Linux服务器中输入mount -t cgroup,可以看到如下的目录文件结构。

图片

从上图中我们可以看到,在/sys/fs/cgroup目录下有很多关于资源的子目录或者说子系统,如cpucet、cpu以及memory等。这些目录都是当前服务器可以被Cgroups进行限制的资源类别。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。我们可以查看memory下面的配置文件,ls /sys/fs/cgroup/memory。

图片

我们再看下Docker的源码:

// New creates and initializes a new containerd server
func New(ctx context.Context, config *Config) (*Server, error) {
    //...
    if err := apply(ctx, config); err != nil {
        return nil, err
    }
    //...
}

// apply sets config settings on the server process
func apply(ctx context.Context, config *Config) error {
    if config.OOMScore != 0 {
        log.G(ctx).Debugf("changing OOM score to %d", config.OOMScore)
        if err := sys.SetOOMScore(os.Getpid(), config.OOMScore); err != nil {
            log.G(ctx).WithError(err).Errorf("failed to change OOM score to %d", config.OOMScore)
        }
    }
    if config.Cgroup.Path != "" {
        cg, err := cgroups.Load(cgroups.V1, cgroups.StaticPath(config.Cgroup.Path))
        if err != nil {
            if err != cgroups.ErrCgroupDeleted {
                return err
            }
            if cg, err = cgroups.New(cgroups.V1, cgroups.StaticPath(config.Cgroup.Path), &specs.LinuxResources{}); err != nil {
                return err
            }
        }
        if err := cg.Add(cgroups.Process{
            Pid: os.Getpid(),
        }); err != nil {
            return err
        }
    }
    return nil
}

通过代码我么可以看得出来在创建一个Docker容器的时候,调用了apply函数,在这个apply函数中会进行cgourp的加载,而后通过cg.Add将创建的容器id添加到控制组的task文件中。

总结

本文主要对容器技术的发展进行了简单回顾,从很早之前服务部署以及应用方面存在的不足出发,阐述了容器技术的出现到底解决了什么问题,同时和大家分享了容器技术的本质以及原理,相信通过本文大家可以对容器技术有一个基本的感受,后续再和大家继续分享云原生技术体系。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿