历史发展

  2013年 PaaS项目广泛使用,那时主流用户的普遍用法,就是租一批 AWS或者 OpenStack 的虚拟机,然后像以前管理物理服务器那样,用脚本或者手工的方式在这些机器上部署应用。这势必会出现一个问题,就是云服务器的环境和本机环境不同,你要现在服务器里做各种配置,才能运行。用户把项目push到云端,云端选择可以运行这个程序的虚拟机,推送到机器的agent上启动。由于需要在一个虚拟机上启动很多个来自不同用户的应用,Cloud Foundry会调用操作系统的 Cgroups 和 Namespace 机制为每一个应用单独创建一个称作“沙盒”的隔离环境,然后在“沙盒”中启动这些应用进程。而当时PaaS里面沙盒的作用就是现在的容器。同年,docker开源,虽然docker和PaaS里面的容器并无太大区别,但微小的不同决定了改变。这个功能,就是 Docker 镜像。

  虽然PaaS解决了上传问题,但是维护问题却大了,要基于PaaS自己的打包机制去维护应用太麻烦了。Docker镜像解决的就是打包的问题。不止于PaaS打包了可执行程序和启停脚本。大多数 Docker 镜像是直接由一个完整操作系统的所有文件和目录构成的,所以这个压缩包里的内容跟你本地开发和测试环境用的操作系统是完全一样的。于是我们只要有镜像就能根据某种方法创建“沙盒”,开发测试完上线即可。所以我们只要下载各种需要的镜像、目录,然后一起build一个镜像,再起一个沙盒run起来这个镜像即可。当然在docker最开始诞生的时候,确实解决了应用打包的问题(甚至可以说是完美解决了这个问题),但并不能代替PaaS的大规模部署应用,直到后面的Docker Swarm、K8s等容器集群管理项目的诞生,docker可以完成集群调度功能,开始大放异彩。

  2014年,Fig项目,首次提出“容器编排”的概念。容器时代,“编排”显然就是对 Docker 容器的一系列定义、配置和创建动作的管理。只要在配置文件里写好先干什么后干什么,执行运行指令,那么容器便会按配置执行任务。Fig 项目被收购后改名为 Compose,它成了 Docker 公司到目前为止第二大受欢迎的项目,一直到今天也依然被很多人使用。Docker还收购了处理容器网络的SockerSocketPlane项目、Docker集群图形化管理的Tutum项目等等。2014 年注定是一个神奇的年份。就在这一年的 6 月,基础设施领域的翘楚 Google 公司突然发力,正式宣告了一个名叫 Kubernetes 项目的诞生。而这个项目,不仅挽救了当时的 CoreOS和 RedHat,还如同当年 Docker 项目的横空出世一样,再一次改变了整个容器市场的格局。所以这次,Google、RedHat 等开源基础设施领域玩家们,共同牵头发起了一个名为CNCF(Cloud Native Computing Foundation)的基金会。这个基金会的目的其实很容易理解:它希望,以 Kubernetes 项目为基础,建立一个由开源基础设施领域厂商主导的、按照独立基金会方式运营的平台级社区,来对抗以 Docker 公司为核心的容器商业生态。

  而为了打造出这样一个围绕 Kubernetes 项目的“护城河”,CNCF 社区就需要至少确保两件事情:1.Kubernetes 项目必须能够在容器编排领域取得足够大的竞争优势;2.CNCF 社区必须以 Kubernetes 项目为核心,覆盖足够多的场景。

  如果你看过 Kubernetes 项目早期的 GitHub Issue 和 Feature 的话,就会发现它们大多来自于Borg 和 Omega 系统的内部特性,这些特性落到 Kubernetes 项目上,就是 Pod、Sidecar 等功能和设计模式。这就解释了,为什么 Kubernetes 发布后,很多人“抱怨”其设计思想过于“超前”的原因。所以,RedHat 与 Google 联盟的成立,不仅保证了 RedHat 在 Kubernetes 项目上的影响力,也正式开启了容器编排领域“三国鼎立”的局面。这时,我们再重新审视容器生态的格局,就不难发现 Kubernetes 项目、Docker 公司和Mesos 社区这三大玩家的关系已经发生了微妙的变化。而Kubernetes 的应对策略则是反其道而行之,开始在整个社区推进“民主化”架构,即:从API 到容器运行时的每一层,Kubernetes 项目都为开发者暴露出了可以扩展的插件机制,鼓励用户通过代码的方式介入到 Kubernetes 项目的每一个阶段。Kubernetes 项目的这个变革的效果立竿见影,很快在整个容器社区中催生出了大量的、基于Kubernetes API 和扩展接口的二次创新工作,比如:目前热度极高的微服务治理项目 Istio;被广泛采用的有状态应用部署框架 Operator;还有像 Rook 这样的开源创业项目,它通过 Kubernetes 的可扩展接口,把 Ceph 这样的重量级产品封装成了简单易用的容器存储插件。

  于是最后docker无力回天,2017年在企业版中内置Kubernetes项目,Kubernetes成为社区中心。2018年docker CTO辞职。回顾历史有一个结论:容器本身没有价值,有价值的是“容器编排”。这也是为什么那么多大公司要参与到这场技术战争,最后Kubernetes夺得胜利的原因。

容器基础

  容器本身没有价值,有价值的是“容器编排”。

何为容器

  系统和应用封装到沙盒里,说起来简单,但到底该怎么做呢?

  一旦“程序”被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。像这样一个程序运起来后的计算机执行环境的总和,就是我们今天的主角:进程。所以,对于进程来说,它的静态表现就是程序,平常都安安静静地待在磁盘上;而一旦运行起来,它就变成了计算机里的数据和状态的总和,这就是它的动态表现。

  而容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而Namespace 技术则是用来修改进程视图的主要方法。

Namespace

  Namespace 技术是用来修改进程视图,让容器以为自己是独立的。

  本来,每当我们在宿主机上运行了一个 /bin/sh 程序,操作系统都会给它分配一个进程编号,比如 PID=100。这个编号是进程的唯一标识,就像员工的工牌一样。而现在,我们要通过 Docker 把这个 /bin/sh 程序运行在一个容器当中。这时候,Docker 就会在这个第 100 号员工入职时给他施一个“障眼法”,让他永远看不到前面的其他 99 个员工,更看不到比尔 · 盖茨。这样,他就会错误地以为自己就是公司里的第 1 号员工。这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如 PID=1。可实际上,他们在宿主机的操作系统里,还是原来的第 100 号进程。这种技术,就是 Linux 里面的 Namespace 机制。而 Namespace 的使用方式也非常有意思:它其实只是 Linux 创建新进程的一个可选参数。只要用Linux里的创建线程的系统调用clone命令的相关参数就可以实现。

  而除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用让被隔离进程看到当前 Namespace 里的网络设备和配置。这,就是 Linux 容器最基本的实现原理了。所以,Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。所以说,容器,其实是一种特殊的进程而已。

  在理解了 Namespace 的工作方式之后,你就会明白,跟真实存在的虚拟机不同,在使用Docker 的时候,并没有一个真正的“Docker 容器”运行在宿主机里面。Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的Namespace 参数。而虚拟机是要真正模拟一个操作系统所需各种硬件的。

  事实上docker扮演的角色,更多的是旁路式的辅助和管理工作。其实像 Docker 这样的角色甚至可以去掉。这样的架构也解释了为什么 Docker 项目比虚拟机更受欢迎的原因

  这是因为,使用虚拟化技术作为应用沙盒,就必须要由 Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完整的 Guest OS 才能执行用户的应用进程。这就不可避免地带来了额外的资源消耗和占用。根据实验,一个运行着 CentOS 的 KVM 虚拟机启动后,在不做优化的情况下,虚拟机自己就需要占用 100~200 MB 内存。此外,用户应用运行在虚拟机里面,它对宿主机操作系统的调用就不可避免地要经过虚拟化软件的拦截和处理,这本身又是一层性能损耗,尤其对计算资源、网络和磁盘 I/O 的损耗非常大。而相比之下,容器化后的用户应用,却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的;而另一方面,使用 Namespace 作为隔离手段的容器并不需要单独的 Guest OS,这就使得容器额外的资源占用几乎可以忽略不计。所以说,“敏捷”和“高性能”是容器相较于虚拟机最大的优势,也是它能够在 PaaS 这种更细粒度的资源管理平台上大行其道的重要原因。

  不过,有利就有弊,基于 Linux Namespace 的隔离机制相比于虚拟化技术也有很多不足之处,其中最主要的问题就是:隔离得不彻底。
  首先,既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。尽管你可以在容器里通过 Mount Namespace 单独挂载其他不同版本的操作系统文件,比如CentOS 或者 Ubuntu,但这并不能改变共享宿主机内核的事实。这意味着,如果你要在Windows 宿主机上运行 Linux 容器,或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器,都是行不通的。而相比之下,拥有硬件虚拟化技术和独立 Guest OS 的虚拟机就要方便得多了。
  其次,在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。这就意味着,如果你的容器中的程序使用 settimeofday(2) 系统调用修改了时间,整个宿主机的时间都会被随之修改,这显然不符合用户的预期。相比于在虚拟机里面可以随便折腾的自由度,在容器里部署应用的时候,“什么能做,什么不能做”,就是用户必须考虑的一个问题。

  此外,由于上述问题,尤其是共享宿主机内核的事实,容器给应用暴露出来的攻击面是相当大的,应用“越狱”的难度自然也比虚拟机低得多所以,在生产环境中,没有人敢把运行在物理机上的 Linux 容器直接暴露到公网上。因为一个容器出了问题就会泄露整个宿主机的信息,而很有可能泄露整个集群的信息。当然,我后续会讲到的基于虚拟化或者独立内核技术的容器实现,则可以比较好地在隔离与性能之间做出平衡。

Cgroups

  Cgroups 就是 Linux 内核中用来为进程设置资源限制,让容器之间不会相互竞争,每个容器用自己的资源。

  虽然容器内的第 1 号进程在“障眼法”的干扰下只能看到容器里的情况,但是宿主机上,它作为第 100 号进程与其他所有进程之间依然是平等的竞争关系。这就意味着,虽然第 100 号进程表面上被隔离了起来,但是它所能够使用到的资源(比如 CPU、内存),却是可以随时被宿主机上的其他进程(或者其他容器)占用的。当然,这个 100 号进程自己也可能把所有资源吃光。这些情况,显然都不是一个“沙盒”应该表现出来的合理行为。于是我们需要Cgroups 来为 Linux 内核中的进程设置资源限制。

  Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。在今天的分享中,我只和你重点探讨它与容器关系最紧密的“限制”能力。在用法上,我们通过CGroup的相关命令行命令,设置每一种资源分分配给每种进程的量,就能完成分配。除 CPU 子系统外,Cgroups 的每一项子系统都有其独有的资源限制能力,比如:blkio,为块设备设定 I/O 限制,一般用于磁盘等设备;cpuset,为进程分配单独的 CPU 核和对应的内存节点;memory,为进程设定内存使用的限制。

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

  这也是容器技术中一个非常重要的概念,即:容器是一个“单进程”模型。另外,跟 Namespace 的情况类似,Cgroups 对资源的限制能力也有很多不完善的地方,被提及最多的自然是 /proc 文件系统的问题。Linux 下的 /proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如 CPU 使用情况、内存占用率等,这些文件也是 top 指令查看系统信息的主要数据来源。但是,你如果在容器里执行 top 指令,就会发现,它显示的信息居然是宿主机的 CPU 和内存数据,而不是当前容器的数据。造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。在生产环境中,这个问题必须进行修正,否则应用程序在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据,这会给应用的运行带来非常大的困惑和风险。(如何修复容器中的 top 指令以及 /proc 文件系统中的信息呢?提示:lxcfs)

容器文件系统

  Namespace 让应用进程只能看到指定的区域; Cgroups 限制了进程能用的资源;那么容器里的进程看到的文件系统又是什么样子的呢?
  我们设想的这一定是一个关于 Mount Namespace 的问题:容器里的应用进程,理应看到一份完全独立的文件系统。这样,它就可以在自己的容器目录(比如 /tmp)下进行操作,而完全不会受宿主机以及其他容器的影响。而实际是即使开启了 Mount Namespace,如果在“容器”里执行一下 ls 指令的话, /tmp 目录下的内容跟宿主机的内容是一样的。即容器进程看到的文件系统也跟宿主机完全一样。

  仔细思考一下,你会发现这其实并不难理解:Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知。但是,这也就意味着,只有在“挂载”这个操作发生之后,进程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。这时,你可能已经想到了一个解决办法:创建新进程时,除了声明要启用 Mount Namespace之外,我们还可以告诉容器进程,有哪些目录需要重新挂载,就比如这个 /tmp 目录。于是,我们在容器进程执行前可以添加一步重新挂载 /tmp 目录的操作。这样的结果就是:容器内的 /tmp 变成了一个空目录,这意味着重新挂载生效了。并且 因为我们创建的新进程启用了 Mount Namespace,所以这次重新挂载的操作,只在容器进程的 Mount Namespace 中有效。如果在宿主机上用 mount -l 来检查一下这个挂载,你会发现它是不存在的。

  为了避免每次创建容器时手动挂载,你可以使用chroot进行一系列设置。而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。

容器镜像的实现过程

  现在,你应该可以理解,对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
  1. 启用 Linux Namespace 配置;
  2. 设置指定的 Cgroups 参数;
  3. 切换进程的根目录(Change Root)。

  这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用pivot_root 系统调用,如果系统不支持,才会使用 chroot。这两个系统调用虽然功能类似,但是也有细微的区别。另外,需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

  这也是容器相比于虚拟机的主要缺陷之一:毕竟后者不仅有模拟出来的硬件机器充当沙盒,而且每个沙盒里还运行着一个完整的 Guest OS 给应用随便折腾。不过,正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。但有了容器之后,更准确地说,有了容器镜像(即 rootfs)之后,这个问题被非常优雅地解决了。由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。事实上,对于大多数开发者而言,他们对应用依赖的理解,一直局限在编程语言层面。比如Golang 的 Godeps.json。但实际上,一个一直以来很容易被忽视的事实是,对一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。

  最后为了便于对镜像进行增量修改,Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。当然,这个想法不是凭空臆造出来的,而是用到了一种叫作联合文件系统(Union FileSystem)的能力。Union File System 也叫 UnionFS,最主要的功能是将多个不同位置的目录联合挂载(unionmount)到同一个目录下。现在,我们启动一个容器,比如:$ docker run -d ubuntu:latest sleep 3600 这时候,Docker 就会从 Docker Hub 上拉取一个 Ubuntu 镜像到本地。这个所谓的“镜像”,实际上就是一个 Ubuntu 操作系统的 rootfs,它的内容是 Ubuntu 操作系统的所有文件和目录。不过,与之前我们讲述的 rootfs 稍微不同的是,Docker 镜像使用的rootfs,往往由多个“层”组成。可以看到,这个 Ubuntu 镜像,实际上由五个层组成。这五个层就是五个增量 rootfs,每一层都是 Ubuntu 操作系统文件与目录的一部分;而在使用镜像时,Docker 会把这些增量联合挂载在一个统一的挂载点上(等价于前面例子里的“/C”目录)。不出意外的,这个目录里面正是一个完整的 Ubuntu 操作系统。

  Linux 容器文件系统的实现方式。而这种机制,正是我们经常提到的容器镜像,也叫作:rootfs。它只是一个操作系统的所有文件和目录,并不包含内核,最多也就几百兆。而相比之下,传统虚拟机的镜像大多是一个磁盘的“快照”,磁盘有多大,镜像就至少有多大。通过结合使用 Mount Namespace 和 rootfs,容器就能够为进程构建出一个完善的文件系统隔离环境。当然,这个功能的实现还必须感谢 chroot 和 pivot_root 这两个系统调用切换进程根目录的能力。而在 rootfs 的基础上,Docker 公司创新性地提出了使用多个增量 rootfs 联合挂载一个完整rootfs 的方案,这就是容器镜像中“层”的概念。

  通过“分层镜像”的设计,以 Docker 镜像为核心,来自不同公司、不同团队的技术人员被紧密地联系在了一起。而且,由于容器镜像的操作是增量式的,这样每次镜像拉取、推送的内容,比原本多个完整的操作系统的大小要小得多;而共享层的存在,可以使得所有这些容器镜像需要的总空间,也比每个镜像的总和要小。这样就使得基于容器镜像的团队协作,要比基于动则几个GB 的虚拟机磁盘镜像的协作要敏捷得多。更重要的是,一旦这个镜像被发布,那么你在全世界的任何一个地方下载这个镜像,得到的内容都完全一致,可以完全复现这个镜像制作者当初的完整环境。这,就是容器技术“强一致性”的重要体现。容器镜像的发明,不仅打通了“开发 - 测试 - 部署”流程的每一个环节,更重要的是:容器镜像将会成为未来软件的主流发布方式。

用Dockerfile制作镜像

  相较于之前介绍的制作 rootfs 的过程,Docker 为你提供了一种更便捷的方式,叫作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"]

  通过这个文件的内容,你可以看到Dockerfile 的设计思想,是使用一些标准的原语(即大写高亮的词语),描述我们所要构建的 Docker 镜像。并且这些原语,都是按顺序处理的。比如 FROM 原语,指定了“python:2.7-slim”这个官方维护的基础镜像,从而免去了安装Python 等语言环境的操作。需要注意的是,Dockerfile 中的每个原语执行后,都会生成一个对应的镜像层。即使原语本身并没有明显地修改文件的操作(比如,ENV 原语),它对应的层也会存在。只不过在外界看来,这个层是空的。

  镜像制作完了,如果需要上传DockerHub 上分享给更多的人。首先需要注册一个 Docker Hub 账号,然后使用 docker login 命令登录。接下来,要用 docker tag 命令给容器镜像起一个完整的名字,包括镜像仓库名、镜像名、版本号,然后就可以通过push命令把镜像上传到Docker Hub中了。此外,还可以使用 docker commit 指令,把一个正在运行的容器,直接提交为一个镜像。一般来说,需要这么操作原因是:这个容器运行起来后,我又在里面做了一些操作,并且要把操作结果保存到镜像里。当然你可以在企业内部自己搭建一个类似Docker Hub的镜像上传系统。这个统一存放镜像的系统,就叫作 Docker Registry。感兴趣的话,你可以查看Docker 的官方文档,以及VMware 的 Harbor 项目。

  一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。这个操作所依赖的,乃是一个名叫 setns() 的 Linux 系统调用。此外,Docker 还专门提供了一个参数,可以让你启动一个容器并“加入”到另一个容器的Network Namespace 里,这个参数就是 -net。所以其实 docker exec 这个操作,是调用Linux的系统调用。这种通过操作系统进程相关的知识,逐步剖析 Docker 容器的方法,是理解容器的一个关键思路,一定要掌握。

Docker分层镜像

Docker能帮助解决服务运行环境可迁移问题的关键,就在于Docker镜像的使用上,实际在使用Docker镜像的时候往往并不是把业务代码、依赖的软件环境以及操作系统本身直接都打包成一个镜像,而是利用Docker镜像的分层机制,在每一层通过编写Dockerfile文件来逐层打包镜像。这是因为虽然不同的微服务依赖的软件环境不同,但是还是存在大大小小的相同之处,因此在打包Docker镜像的时候,可以分层设计、逐层复用,这样的话可以减少每一层镜像文件的大小。

以微博的业务Docker镜像为例,来看看实际生产环境中如何使用Docker镜像。正如下面这张图所描述的那样,微博的Docker镜像大致分为四层。

基础环境层。这一层定义操作系统运行的版本、时区、语言、yum源、TERM等。

运行时环境层。这一层定义了业务代码的运行时环境,比如Java代码的运行时环境JDK的版本。

Web容器层。这一层定义了业务代码运行的容器的配置,比如Tomcat容器的JVM参数。

业务代码层。这一层定义了实际的业务代码的版本,比如是V4业务还是blossom业务。

docker分层

这样的话,每一层的镜像都是在上一层镜像的基础上添加新的内容组成的,以微博V4镜像为例,V4业务的Dockerfile文件内容如下

FROM registry.intra.weibo.com/weibo_rd_content/tomcat_feed:jdk8.0.40_tomcat7.0.81_g1_dns
ADD confs /data1/confs/
ADD node_pool /data1/node_pool/
ADD authconfs /data1/authconfs/
ADD authkey.properties /data1/
ADD watchman.properties /data1/
ADD 200.sh /data1/weibo/bin/200.sh
ADD 503.sh /data1/weibo/bin/503.sh
ADD catalina.sh /data1/weibo/bin/catalina.sh
ADD server.xml /data1/weibo/conf/server.xml
ADD logging.properties /data1/weibo/conf/logging.properties
ADD ROOT /data1/weibo/webapps/ROOT/
RUN chmod +x /data1/weibo/bin/200.sh /data1/weibo/bin/503.sh /data1/weibo/bin/catalina.sh
WORKDIR /data1/weibo/bin

FROM代表了上一层镜像文件是“tomcat_feed:jdk8.0.40_tomcat7.0.81_g1_dns”,从名字可以看出上一层镜像里包含了Java运行时环境JDK和Web容器Tomcat,以及Tomcat的版本和JVM参数等;ADD就是要在这层镜像里添加的文件, 这里主要包含了业务的代码和配置等;RUN代表这一层镜像启动时需要执行的命令;WORKDIR代表了这一层镜像启动后的工作目录。这样的话就可以通过Dockerfile文件在上一层镜像的基础上完成这一层镜像的制作。

Docker Volume(数据卷)

  如前文所述,容器技术使用了 rootfs 机制和 Mount Namespace,构建出了一个同宿主机完全隔离开的文件系统环境。这时候,我们就需要考虑这样两个问题:
  1. 容器里进程新建的文件,怎么才能让宿主机获取到?
  2. 宿主机上的文件和目录,怎么才能让容器里的进程访问到?

  这正是 Docker Volume 要解决的问题:Volume 机制,允许你将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作。

  在 Docker 项目里,它支持两种 Volume 声明方式,可以把宿主机目录挂载进容器的目录当中

$ docker run -v /test ...
$ docker run -v /home:/test ...

目录挂载

  那么,Docker 又是如何做到把一个宿主机上的目录或者文件,挂载到容器里面去呢?

  当容器进程被创建之后,尽管开启了 Mount Namespace,但是在它执行 chroot(或者 pivot_root)之前,容器进程一直可以看到宿主机上的整个文件系统。而宿主机上的文件系统,也自然包括了我们要使用的容器镜像。所以,我们只需要在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。更重要的是,由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 MountNamespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。

  这里要使用到的挂载技术,就是 Linux 的绑定挂载(Bind Mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响。其实,如果你了解 Linux 内核的话,就会明白,绑定挂载实际上是一个 inode 替换的过程。在Linux 操作系统中,inode 可以理解为存放文件内容的“对象”,而 dentry,也叫目录项,就是访问这个 inode 所使用的“指针”。所以,在一个正确的时机,进行一次绑定挂载,Docker 可以成功地将一个宿主机上的目录或文件,不动声色地挂载到容器中。

容器的“单进程模型”

  强调一下:容器的“单进程模型”,并不是指容器里只能运行“一个”进程,而是指容器没有管理多个进程的能力。这是因为容器里 PID=1 的进程就是应用本身,其他的进程都是这个PID=1 进程的子进程。可是,用户编写的应用,并不能够像正常操作系统里的 init 进程或者systemd 那样拥有进程管理的功能。比如,你的应用是一个 Java Web 程序(PID=1),然后你执行 docker exec 在后台启动了一个 Nginx 进程(PID=3)。可是,当这个 Nginx 进程异常退出的时候,你该怎么知道呢?这个进程退出后的垃圾收集工作,又应该由谁去做呢?

  是docker里面不能同时跑java web和ngnix,但是同时跑了以后就没有容器的特性,也无法进行良好的管理了

Kubernetes出现

  如上文所述,一个“容器”,实际上是一个由 Linux Namespace、Linux Cgroups 和rootfs 三种技术构建出来的进程的隔离环境。从这个结构中我们不难看出,一个正在运行的 Linux 容器,其实可以被“一分为二”地看待:

  1. 一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs,这一部分我们称为“容器镜像”(Container Image),是容器的静态视图;

  2. 一个由 Namespace+Cgroups 构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime),是容器的动态视图。

  更进一步地说,作为一名开发者,我并不关心容器运行时的差异。因为,在整个“开发 - 测试 - 发布”的流程中,真正承载着容器信息进行传递的,是容器镜像,而不是容器运行时。这个重要假设,正是容器技术圈在 Docker 项目成功后不久,就迅速走向了“容器编排”这个“上层建筑”的主要原因:我只要能够将用户提交的 Docker镜像以容器的方式运行起来,整条路径上的各个服务节点,比如 CI/CD、监控、安全、网络、存储等等,都有我可以发挥和盈利的余地。这样,容器就从一个开发者手里的小工具,一跃成为了云计算领域的绝对主角;而能够定义容器组织和管理规范的“容器编排”技术,则当仁不让地坐上了容器技术领域的“头把交椅”。

  为了更好地理解 Kubernetes 的“先进性”与“完备性”,我们不妨从 Kubernetes 的顶层设计说起。首先,Kubernetes 项目要解决的问题是什么?

  编排?调度?容器云?还是集群管理?实际上,这个问题到目前为止都没有固定的答案。因为在不同的发展阶段,Kubernetes 需要着重解决的问题是不同的。但是,对于大多数用户来说,他们希望 Kubernetes 项目带来的体验是确定的:现在我有了应用的容器镜像,请帮我在一个给定的集群上把这个应用运行起来。更进一步地说,我还希望 Kubernetes 能给我提供路由网关、水平扩展、监控、备份、灾难恢复等一系列运维能力。所以说,如果 Kubernetes 项目只是停留在拉取用户镜像、运行容器,以及提供常见的运维功能的话,那么别说跟“原生”的 Docker Swarm 项目竞争了,哪怕跟经典的 PaaS 项目相比也难有什么优势可言。而实际上,在定义核心功能的过程中,Kubernetes 项目正是依托着 Borg 项目的理论优势,才在短短几个月内迅速站稳了脚跟,进而确定了一个如下图所示的全局架构:

Kubernetes 项目架构

  我们可以看到,Kubernetes 项目的架构,跟它的原型项目 Borg 非常类似,都由 Master 和 Node两种节点组成,而这两种角色分别对应着控制节点和计算节点。

  其中,控制节点,即 Master 节点,由三个紧密协作的独立组件组合而成,它们分别是负责 API 服务的 kube-apiserver、负责调度的 kube-scheduler,以及负责容器编排的 kube-controller-manager。整个集群的持久化数据,则由 kube-apiserver 处理后保存在 Ectd 中。

计算节点

  而计算节点上最核心的部分,则是一个叫作 kubelet 的组件。在 Kubernetes 项目中,kubelet 主要负责同容器运行时(比如 Docker 项目)打交道。而这个交互所依赖的,是一个称作 CRI(Container Runtime Interface)的远程调用接口,这个接口定义了容器运行时的各项核心操作,比如:启动一个容器需要的所有参数。这也是为何,Kubernetes 项目并不关心你部署的是什么容器运行时、使用的什么技术实现,只要你的这个容器运行时能够运行标准的容器镜像,它就可以通过实现 CRI 接入到 Kubernetes 项目当中。

  而具体的容器运行时,比如 Docker 项目,则一般通过 OCI 这个容器运行时规范同底层的 Linux 操作系统进行交互,即:把 CRI 请求翻译成对 Linux 操作系统的调用(操作 Linux Namespace 和Cgroups 等)。此外,kubelet 还通过 gRPC 协议同一个叫作 Device Plugin 的插件进行交互。这个插件,是Kubernetes 项目用来管理 GPU 等宿主机物理设备的主要组件,也是基于 Kubernetes 项目进行机器学习训练、高性能作业支持等工作必须关注的功能。而kubelet 的另一个重要功能,则是调用网络插件和存储插件为容器配置网络和持久化存储。这两个插件与 kubelet 进行交互的接口,分别是 CNI(Container Networking Interface)和CSI(Container Storage Interface)。

控制节点

  虽然在 Master 节点的实现细节上 Borg 项目与 Kubernetes 项目不尽相同,但它们的出发点却高度一致,即:如何编排、管理、调度用户提交的作业?

  所以,Borg 项目完全可以把 Docker 镜像看做是一种新的应用打包方式。这样,Borg 团队过去在大规模作业管理与编排上的经验就可以直接“套”在 Kubernetes 项目上了。这些经验最主要的表现就是,从一开始,Kubernetes 项目就没有像同时期的各种“容器云”项目那样,把 Docker 作为整个架构的核心,而仅仅把它作为最底层的一个容器运行时实现。而 Kubernetes 项目要着重解决的问题,则来自于 Borg 的研究人员在论文中提到的一个非常重要的观点:运行在大规模集群中的各种任务之间,实际上存在着各种各样的关系。这些关系的处理,才是作业编排和管理系统最困难的地方。事实也正是如此。

  而在容器技术普及之前,传统虚拟机环境对这种关系的处理方法都是比较“粗粒度”的。你会经常发现很多功能并不相关的应用被一股脑儿地部署在同一台虚拟机中,只是因为它们之间偶尔会互相发起几个 HTTP 请求。更常见的情况则是,一个应用被部署在虚拟机里之后,你还得手动维护很多跟它协作的守护进程(Daemon),用来处理它的日志搜集、灾难恢复、数据备份等辅助工作。但容器技术出现以后,你就不难发现,在“功能单位”的划分上,容器有着独一无二的“细粒度”优势:毕竟容器的本质,只是一个进程而已。也就是说,只要你愿意,那些原先拥挤在同一个虚拟机里的各个应用、组件、守护进程,都可以被分别做成镜像,然后运行在一个个专属的容器中。它们之间互不干涉,拥有各自的资源配额,可以被调度在整个集群里的任何一台机器上。而这,正是一个 PaaS 系统最理想的工作状态,也是所谓“微服务”思想得以落地的先决条件。

  当然,如果只做到“封装微服务、调度单容器”这一层次,Docker Swarm 项目就已经绰绰有余了。如果再加上 Compose 项目,你甚至还具备了处理一些简单依赖关系的能力,比如:一个“Web 容器”和它要访问的数据库“DB 容器”。可是,如果我们现在的需求是,要求这个项目能够处理前面提到的所有类型的关系,甚至还要能够支持未来可能出现的更多种类的关系呢?一旦要追求项目的普适性,那就一定要从顶层开始做好设计。所以,Kubernetes 项目最主要的设计思想是,从更宏观的角度,以统一的方式来定义任务之间的各种关系,并且为将来支持更多种类的关系留有余地。

Pod

  在常规环境下,这些应用往往会被直接部署在同一台机器上,通过 Localhost 通信,通过本地磁盘目录交换文件。而在 Kubernetes 项目中,这些容器则会被划分为一个“Pod”,Pod 里的容器共享同一个 Network Namespace、同一组数据卷,从而达到高效率交换信息的目的。而对于另外一种更为常见的需求,比如 Web 应用与数据库之间的访问关系,Kubernetes 项目则提供了一种叫作“Service”的服务。所以,Kubernetes 项目的做法是给 Pod 绑定一个 Service 服务,而 Service 服务声明的 IP 地址等信息是“终生不变”的。这个Service 服务的主要作用,就是作为 Pod 的代理入口(Portal),从而代替 Pod 对外暴露一个固定的网络地址。这样,对于 Web 应用的 Pod 来说,它需要关心的就是数据库 Pod 的 Service 信息。不难想象,Service 后端真正代理的 Pod 的 IP 地址、端口等信息的自动更新、维护,则是 Kubernetes 项目的职责。像这样,围绕着容器和 Pod 不断向真实的技术场景扩展,我们就能够摸索出一幅如下所示的Kubernetes 项目核心功能的“全景图”。

K8s技术概念

  按照这幅图的线索,我们从容器这个最基础的概念出发,首先遇到了容器间“紧密协作”关系的难题,于是就扩展到了 Pod;有了 Pod 之后,我们希望能一次启动多个应用的实例,这样就需要Deployment 这个 Pod 的多实例管理器;而有了这样一组相同的 Pod 后,我们又需要通过一个固定的 IP 地址和端口以负载均衡的方式访问它,于是就有了 Service。可是,如果现在两个不同 Pod 之间不仅有“访问关系”,还要求在发起时加上授权信息。最典型的例子就是 Web 应用对数据库访问时需要 Credential(数据库的用户名和密码)信息。那么,在Kubernetes 中这样的关系又何处理呢?Kubernetes 项目提供了一种叫作 Secret 的对象,它其实是一个保存在 Etcd 里的键值对数据。这样,你把 Credential 信息以 Secret 的方式存在 Etcd 里,Kubernetes 就会在你指定的 Pod(比如,Web 应用的 Pod)启动时,自动把 Secret 里的数据以 Volume 的方式挂载到容器里。这样,这个 Web 应用就可以访问数据库了。

应用运行的形态

  除了应用与应用之间的关系外,应用运行的形态是影响“如何容器化这个应用”的第二个重要因素

  为此,Kubernetes 定义了新的、基于 Pod 改进后的对象。比如 Job,用来描述一次性运行的Pod(比如,大数据任务);再比如 DaemonSet,用来描述每个宿主机上必须且只能运行一个副本的守护进程服务;又比如 CronJob,则用于描述定时任务等等。如此种种,正是 Kubernetes 项目定义容器间关系和形态的主要方法。可以看到,Kubernetes 项目并没有像其他项目那样,为每一个管理功能创建一个指令,然后在项目中实现其中的逻辑。这种做法,的确可以解决当前的问题,但是在更多的问题来临之后,往往会力不从心。相比之下,在 Kubernetes 项目中,我们所推崇的使用方法是:

  首先,通过一个“编排对象”,比如 Pod、Job、CronJob 等,来描述你试图管理的应用;
  然后,再为它定义一些“服务对象”,比如 Service、Secret、Horizontal Pod Autoscaler(自动水平扩展器)等。这些对象,会负责具体的平台级功能。

  这种使用方法,就是所谓的“声明式 API”。这种 API 对应的“编排对象”和“服务对象”,都是Kubernetes 项目中的 API 对象(API Object)。这就是 Kubernetes 最核心的设计理念.

Kubernetes 项目如何启动一个容器化任务呢?

比如,我现在已经制作好了一个 Nginx 容器镜像,希望让平台帮我启动这个镜像。并且,我要求平台帮我运行两个完全相同的 Nginx 副本,以负载均衡的方式共同对外提供服务。如果是自己 DIY 的话,可能需要启动两台虚拟机,分别安装两个 Nginx,然后使用 keepalived为这两个虚拟机做一个虚拟 IP。而如果使用 Kubernetes 项目呢?你需要做的则是编写如下这样一个 YAML 文件(比如名叫nginx-deployment.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
   matchLabels:
    app: nginx
  template:
   metadata:
    labels:
     app: nginx
  spec:
   containers:
   - name: nginx
    image: nginx:1.7.9
    ports:
    - containerPort: 80

在上面这个 YAML 文件中,我们定义了一个 Deployment 对象,它的主体部分(spec.template部分)是一个使用 Nginx 镜像的 Pod,而这个 Pod 的副本数是 2(replicas=2)。然后执行:

$ kubectl create -f nginx-deployment.yaml

这样,两个完全相同的 Nginx 容器副本就被启动了。不过,这么看来,做同样一件事情,Kubernetes 用户要做的工作也不少嘛。别急,在后续的讲解中,我会陆续介绍 Kubernetes 项目这种“声明式 API”的种种好处,以及基于它实现的强大的编排能力。

容器的常见误区

  容器是为了跨环境迁移的,第一种迁移的场景是开发,测试,生产环境之间的迁移。如果不需要迁移,或者迁移不频繁,虚拟机镜像也行,但是总是要迁移,带着几百 G 的虚拟机镜像,太大了。
  第二种迁移的场景是跨云迁移,跨公有云,跨 Region,跨两个 OpenStack 的虚拟机迁移都是非常麻烦,甚至不可能的,因为公有云不提供虚拟机镜像的下载和上传功能,而且虚拟机镜像太大了,一传传一天。
  所以跨云场景下,混合云场景下,容器也是很好的使用场景。这也同时解决了仅仅私有云资源不足,扛不住流量的问题。
  第三部分:容器的正确使用场景

  根据以上的分析,我们发现容器推荐使用在下面的场景下。
  1.部署无状态服务,同虚拟机互补使用,实现隔离性
  2.如果要部署有状态服务,需要对里面的应用十分的了解
  3.作为持续集成的重要工具,可以顺利在开发,测试,生产之间迁移
  4.适合部署跨云,跨 Region,跨数据中心,混合云场景下的应用部署和弹性伸缩
  5.以容器作为应用的交付物,保持环境一致性,树立不可变更基础设施的理念
  6.运行进程基本的任务类型的程序
  7.用于管理变更,变更频繁的应用使用容器镜像和版本号,轻量级方便的多
  8.使用容器一定要管理好应用,进行 Health Check 和容错的设计

K8s与Docker Swarm区别?

  服务架构来讲,多个独立功能内聚的服务带来了整体的灵活性,但是同时也带来了部署运维的复杂度提升,这时Docker配合Devops带来了不少的便利(轻量、隔离、一致性、CI、CD等)解决了不少问题,再配合compose,看起来一切都很美了,为什么还需要K8s?可以试着这样理解么?把微服务理解为人,那么服务治理其实就是人之间的沟通而已,人太多了就需要生存空间和沟通方式的优化,这就需要集群和编排了。Docker Compose,swarm,可以解决少数人之间的关系,比如把手机号给你,你就可以方便的找到我,但是如果手机号变更的时候就会麻烦,人多了也会麻烦。而k8s是站在上帝视角俯视芸芸众生后的高度抽象,他看到了大概有哪些类人(组织)以及不同组织有什么样的特点(Job、CornJob、Autoscaler、StatefulSet、DaemonSet...),不同组织之间交流可能需要什么(ConfigMap,Secret...),这样比价紧密的人们在相同pod中,通过Service-不会变更的手机号,来和不同的组织进行沟通,Deployment、RC则可以帮组人们快速构建组织。Dokcer 后出的swarm mode,有类似的视角抽象(比如Service),不过相对来说并不完善。

Service Mesh

  Service Mesh 是最近引入的一个新名词。它的官方定义是这样的:Service Mesh 是一个基础设施层,负责服务间通讯,主要目标是保证请求的正确传递。它在部署时表现为轻量级的网络代理,通常是跟应用一起部署,但是它对应用程序是透明的。这种微服务治理方案给微服务架构的具体实现指出了一个清晰的方向,围绕这一概念逐步开始形成新的技术生态,在业界造成不少震动。这种震动对于企业 IT 转型工作带来的影响,甚至比容器化的影响更加深远。

  它与现在的Spring Cloud,Dubbo等侵入式框架相比:下移,沉淀,形成一个通讯层。充分利用底层基础设施。不仅仅在于将服务间通讯从应用程序中剥离出来,更在于一路下沉到基础设施层并充分利用底层基础设施的能力。

Service Mesh 特点:

1.撑全局架构的一些逻辑应该往下沉,编程基础设施,尽可能与业务系统解耦,这样才能使我们的推进交付的速度更快
2.整个基础设施的发展趋势也是不断下沉和标准化的过程,从 Linux 开始到 K8S,现在再到 Service Mesh 或者是 Serverless。
3.对传统IT系统技术改造的过程中,Service Mesh 是一个很好的解决方案。
4.Java系统中对.net、C++、python体系的融合中,Service Mesh 有很大的作用。

K8S 和 Service Mesh 两种技术之间的关系:

  首先,先从复杂度层面来说,我觉得 Service Mesh 由于有 Sidecar 的引入,肯定会带来运维上的复杂度。K8S 的 Pod 作为一种基础设施,里面可以天然地支持多个 Container,来支持这种 Sidecar 技术,对运维是非常大的帮助。然后再加上它对资源细粒度的控制,比如说 CPU、内存资源的控制,以及像权限控制 RBAC 方面的一些功能,我们觉得这些都对简化复杂度有很大的帮助。

  第二点就比较通用一点,原来的应用程序如果没有跑在 K8S 的话,它的元数据信息对整个外部系统是不可见的。如果把这个程序放到 Pod 里面之后,因为 Pod 是在 K8S 上运行,本身是带有元数据的,比如说 label,如果我们把这些元数据对外部可见,让它开放了,这样不仅对于 Service Mesh,而是面向整个上层的 K8S 生态开放了,应用本身能拿到更多的红利,这是我的两点看法。

  上 Service Mesh 之前,是不是一定要先上 K8S?这个话题当时有过讨论,大家的一致想法是:从技术上讲是不限制的,因为 Service Mesh 从理论上讲,底下可以不是 K8S。但是从长期的发展上说,K8S 可以给 Service Mesh 提供非常大的支持。所以,从远景上说,K8S 跟 Service Mesh 未来长期的发展路线是必然结合着使用的。

K8S怎么落地?

1.我们原有的这些运维体系,如何比较平滑地迁移到这种容器化的体系上来。
2.要真正把 K8S 在蚂蚁落地,还会面临各种各样的具体落地上面的一些问题。比如像集群部署、网络或者是存储方面的问题。
3.最后提一下存储,K8S 的存储这一块相对于 K8S 的其它模块进化是相对比较慢的,所以我们在落地存储方案时会经历一些方案上的变化,或者是痛点。
4.大家认为 K8S 复杂,但他认为其实 K8S 的 API 本来就不是面向最终用户的,应该是面向开发者的。所以在 K8S 之上,我们应该再分装一个 PaaS 平台出来,让这个 PaaS 平台去面向最终用户

K8S的发展?

  未来的操作系统。
1.首先短期来看,我们还是要解决性能和稳定性这方面的问题,还有落地的问题。如果长远来看,就是生态这个点。
2.然后再往上面看,在内核这一层,现阶段的 Mesh 技术,在包拦截、包过滤、协议处理上面,都还有提升的空间。

Service Mesh 的收益,学习路径?

  1.第一个就是多语言,确实以前是统一的,但是到现在,其实就很难有这样一个大的把控。
  2.第二个,我们内部可能还好一些,但是在外部,确实有不少遗留系统,那我们不期望大家一听到很好的概念,就推翻重来,还是期望能保留已有的资产,复用原有的基础设施,那就可以用 Service Mesh 把这些系统给接进来。
  3.第三个就是我刚才提到的,可能像蚂蚁这样的公司会比较看中,就是整个基础设施迭代演化的速度。

  当前服务网格实现的不成熟和快速变化使得部署他们的成本和风险很高,随着应用程序复杂性的增加,服务网格将成为实现服务到服务的能力的现实选择。当然目前K8s和其他编排平台提供的丰富功能足够我们解决当前问题。在未来,中间件作为基础设施未来和云平台融合是一个不可阻挡地趋势,除了 Service Mesh,未来还可能会出现 Message Mesh,DB Mesh 等等产品。

Serviceless

  Serverless被翻译为“无服务器架构”,这个概念在2012年时便已经存在,比微服务和Service Mesh的概念出现都要早,但是直到微服务概念大红大紫之后,Serverless才重新又被人们所关注。

  Serverless(无服务器架构)并不意味着没有任何服务器去运行代码,Serverless是无需管理服务器,只需要关注代码,而提供者将处理其余部分工作。“无服务器架构”也可以指部分服务器端逻辑依然由应用程序开发者来编写的应用程序,但与传统架构的不同之处在于,这些逻辑运行在完全由第三方管理,由事件触发的无状态(Stateless)暂存于计算容器内。

Serverless的主要优点

1.开发者只需要专注于业务逻辑就可以了,开发效率更高。开发一个典型的服务器端项目,需要花很多时间处理依赖、线程、日志、发布和使用服务、部署及维护等相关的工作,基于Serverless架构则不需要操心这些工作。
2.不需要关注云主机、操作系统、网络等基础资源,Serverless框架还可以根据负载量自动水平扩展。
3.不需要架构。Serverless架构本身就是高可用、高扩展的架构,基本上不需要架构师的参与。
4.按需付费, 成本相对比较低。用户购买的ECS使用时间一般不到5成,但是为另外5成闲置时间付费了。Lambda按照运行的时间收费,成本会低很多。

Serverless的主要缺点

1.不适合处理复杂的业务逻辑,它更适合调用云上的其他服务,粘合关键的产品。以AWS Lambda例,我们通常使用Lambda写一段逻辑将文件上传到S3,将请求的数据写入到DynamoDB或RDS数据库等一些相对简单的功能。
2.排查问题困难,因为逻辑散落在各处。
3.Serverless无法用于高并发应用,为每个请求启动一个进程开销太高,流量瞬间爆发容易导致超时。例如双十一支付宝高峰期每秒处理的交易数为8.59万笔,如果使用Serverless架构,意味着我们的系统内每秒有8.59万个进程被创建又被销毁,这是难以负担的开销。
4.Serverless调用之间不能共享状态让编写复杂程序变得极度困难。无状态是互连网应用追求的目标,例如满足“12要素”的应用。但Serverless将无状态进行的更加彻底,在不同的调用之间无法共享内存状态,例如使用hashmap。我们的AI应用中统计已处理图片总数的全局计数器在传统架构中只是一个全局变量,但在Serverless架构中它变成存储在内存数据库(Redis)中的一条记录,更新成本、保证原子性等因素让我们的编码变得数倍复杂。对于大多云原生的互联网应用来说,这种彻底的无状态架构是一个巨大的挑战,而对于动辄有几十万、上百万行代码的、充满了状态的企业应用来说,Serverless的无状态改造几乎是一个无法完成的任务。
5.业务拆分问题。如何将业务拆分成成百上千个运行在独立进程、运行时间受限的函数是巨大的挑战。而是否需要如此细粒度的拆分是需要回答的第一个问题。有些问题或许变成无解难题又或成本极高,例如分布式数据库事务。
6.厂商锁定。云计算是赢者通吃的行业,大而全的云厂商优势巨大,Serverless加剧了这种趋势。以前用户还需要自己写很多服务器端的逻辑,迁移的时候,把服务器端代码重新部署一下。采用Serverless架构之后,代码都是各个平台的Lambda代码片段,没法迁移。从客户的角度来看,是不希望自己被某家云厂商所绑架的。所以云计算行业需要做很多标准化的工作,方便用户无缝在各种云之间迁移。
7.对已存在的项目迁移困难。将现有的复杂的单体式应用进行如此细粒度的拆分需要极高的成本。

相关原理

Proxy

CAP

BASE