前言
云原生是未来IT技术的一个重要的发展方向。作为云计算的从业者,我们还是要搞清楚云原生到底代表着什么,以免在跟用户交流的时候,给出不准确的解释显得不够专业。这段时间以来,我也积累了一些对云原生的知识,希望能够通过这篇文章做一个沉淀。
要理解云原生,首先需要知道其定义。下面是CNCF(云原生计算基金会)对于云原生的定义:
云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。
这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。
解读上面这段话之后,我发现这里其实说明了两个事情。第一个是云原生包含哪些技术,第二个是云原生的目标是什么。
云原生的技术
下面先来讨论第一个话题,云原生包含哪些技术。就第一段话而言,其实它在描述给我们一个当前的IT环境的变化。在没有云计算之前,我们知道,所有公司的IT构建都是需要自己购买服务器,然后在上面自行安装应用。或者有些公司实力更强,会在上面做一些简单的虚拟化。不同的服务器,虽然运行环境有差异,但是因为都是公司自建,相对可控。进入云计算时代之后,我们会发现,由于出现了公有云,而且提供公有云的各供应商也有自己不同的规格和操作系统,底层运行环境开始变得复杂起来。
如果说是这些差别还只是停留在IaaS层面,那么更复杂的差异应该是应用层。随着开源软件的发展,我们在社区可以找到各种各样解决不同场景不同问题的中间件和数据库。由于引入了这些组件,必要地,我们也会将相应的驱动加入到应用软件当中。在组件升级或者应用升级的过程中,这些额外引入的组件都可能存在版本兼容性的问题,为系统添加了一些不确定风险性。
最后一个复杂度来自于研发团队自身。以前公司会用的编程语言比较少,基本上Java/PHP占了绝大多部分的比例。也可以看到有很多公司就一套技术栈,也能解决各种问题。但是现在不一样了,随着互联网规模的不断扩大,日常对于系统的要求也千奇百怪。有要求高并发的,有要求延时低的,有要求编写简易的,有要求数据处理性能强的。对于不同的场景,选择不同的编程语言有时候成为一种必须。而且这些编程语言所打造的系统,互相之间还需要能够交互,被对方调用。这其实就需要一套屏蔽底层编程语言差别的有效的通信协议或者方案来完成。
说了那么多,都是我们当前IT业务会遇到的问题,那应该怎么解决呢?CNCF给我们规范了一些工具来解决这些问题,它们就是容器,服务网格,微服务,不可变基础设施和声明式API。在这里,我想先逐个简单说一下这些东西都是什么。
容器技术是以Docker为代表,是一种标准化的软件单元。它将应用及其所有依赖项打包在一起,能够快速部署到任何能够运行该容器技术的环境中。换句话中,以前我们部署应用的时候是提前将应用的运行环境先安装好,然后将应用部署上去。利用容器,我们只需要一个容器的运行环境,不管是跑在CentOS还是Ubuntu上面,直接把应用以及其依赖部署上去,这样应用就可以运行起来。而且不同应用之间,不会出现因为依赖版本问题导致冲突。这对于运维效率来说是一个重大的提升。所以简单来说说容器是解决了部署和隔离的问题。加上现在流行的K8S,容器运行的容错和调度能力又上了一个台阶。
服务网格和微服务,这两个词经常被大家混在一起谈,甚至有人认为它们是同一个概念。我认为,两者的视角是不一样的。先谈微服务,它是应用从单体应用到分布式应用过渡之后,对于分布式应用的一种统称。微服务的视角,更多是从应用的角度,如何就应用域进行拆分来展开的。应用域关心的是业务之间的边界和关系。基于这些边界如何拆分应用,变成一个个的微服务。基于这些关系,微服务之间如何进行调用。它解决的是一个单体应用过于臃肿,难以快速迭代和快速排查定位问题的难题。而服务网格,视角则更多是研发和运维之间展开。这是因为微服务发展到一定的阶段,更多的编程语言和组件的引入,原来一些分布式应用中的通用能力需要被下沉到基础设施中去。这些通用能力中,最具代表性的就是网络通信能力。把网络能力下沉下去之后,整个系统的流量情况,系统架构就会变得可被观察(Observable),这样发生问题的时候就更容易排查。另外,由于通信被底层接管,也就一并屏蔽了应用层的编程语言差异。
不可变基础设施和声明式API,这两个技术比较少为人所谈及,但是其实所描述的场景也是非常重要。不可变基础设施是指在当你更新一个应用的时候,不会在当前的应用运行环境中执行。取而代之的是,在一个新的运行环境,将应用部署进去,替代原有的应用。这种操作方式在云计算之前使用的是比较少的。因为每个公司内部,即使是已经有了虚拟化技术,但是空闲出来的资源肯定也是相对紧张的。在公共云上,由于理论上算力是无穷大的,所以用户可以在每次部署的时候完全新建一个虚拟机,然后将应用和环境准备好,再去导入流量,再释放上一个版本。这是云计算带来的好处。声明式API,有别于命令式API,是通过对目标最终状态的定义来发出指令的方式。声明式的指令,其实更符合人对于事情的理解。打个比分,我想要部署某个版本的应用,并且该应用应该运行5个实例。这样的描述方式很清晰,很好理解。放到执行环境中,也很容易将其解析成一个个的执行动作。而且这样的指令还有一个好处,就是幂等性能够得到保证,不管我执行多少次,结果状态都会是一致的。
云原生的目标
上面简单介绍了各种云原生的技术,下面来谈谈云原生的目标。其实第二段话对于云原生的目的描述的也比较清晰,就是容错性好,易于管理,便于观察和松耦合。
为什么我们要强调上述几个目标?以我的经验,一些高速发展中的互联网公司,由于业务快速迭代的需求,系统数量会成爆炸式发展。中型规模的公司,内部管理着成百上千小系统是很正常的事情。这些系统之间的关系纷繁复杂,给架构师和运维人员带来不少的压力。从老板的层面,一般对技术就要求一句话“不要影响业务”。这简单的一句话,翻译成技术同学的工作可能就没那么简单了。一般他们也会遵循一个原则,单点问题不影响整体业务,业务故障能够尽快恢复。虽然具体到指标可能每个公司会不一样,但是离不开都是围绕这样的原则打造IT体系。
在云原生的体系下面,其实这个问题会变得很好解决。
我们来假想一下一个完美的云原生系统是怎么打造起来的。首先,所有应用都通过容器技术来进行打包。无论是本地研发,云上测试还是生产环境,运行时环境都可以保持一样。这样就大大降低了测试和生产可能存在的环境差异导致的影响降低“不知缘由”问题的发生概率。
其次,容器在云上一般都用K8S来调度,而K8S正是使用了声明式的API。上文已经说过,声明式的API有一个很好的地方是其描述最终的状态和幂等性。这在实际情况中,方便了我们对于系统部署的定义和发生错误时候自动恢复机制的介入。如上文所述,假如我需要部署某应用5个版本A1的实例,我只需要向K8S发送这么一段描述性指令就好。这个指令不需要关心当前系统的状态如何(是已经运行着5个实例还是超过5个实例),只需要发送者确认所需要的结果状态即可。因此系统的最终状态是预测的,也是可控的。对比一下,如果是命令式指令的话,每次发送指令之前,我们都需要先观察当前系统状态,并且计算与目标状态的差异,再将差异转化成指令发送到系统。这中间人为介入得越多,可能产生的风险的概率就会越大。声明式还有一个优势,就是让K8S一直监测着系统当前的运行状态。假如一段时间之后,某一个实例因为内部原因崩溃了导致无法继续服务,K8S会重新拉起这个实例,将故障实例替换掉。这中间甚至不需要任何人工介入。
不可变的基础设施,其实在有虚拟机的时候已经可以做到了。但是真正发扬起来应该是容器技术。原因有两点,第一,容器本身就是一个打包的技术,它把应用运行相关的一切内容都打包在一起。这样在部署的时候会将所有依赖,包括运行环境也好,组件也好,所有东西都一起部署。要么一起成功,要么一起失败,不会存在中间状态。这样就使得部署的最终状态是可预测的。第二,从整体来看,因为容器的资源利用率更高,这样我们可以在同样的资源情况下面,就有更富余的资源容量去做新部署。这样一来我们可以对新部署做检验而不影响原有业务的负载能力,同时我们也不会担心新部署对原有部署产生不可预测的影响。
如果说上述那些都是在执行操作层面,对可能存在的风险与故障防范于未然。那么服务网格就是一种让故障发生之后,可知可控可排查的手段。如上文提到,服务网格中,很重要的一种下沉能力是网络监控能力。其实换句话讲,应该是网络接管能力。通过接管整个分布式系统的网络服务,我们可以轻松地勾画出系统的架构图(阿里云上的ACK安装了Istio之后,就有这个能力)。这可不是简单的服务之间网络流向,而是可以实时地看到服务与服务之间的请求流量,业务压力情况,服务健康情况,异常概率等。这对于实时发现系统问题和排查系统瓶颈相当重要。特别在大型分布式环境下,如果没有这样一个全局视图的话,发生问题就会好些盲人摸象一般,无法看清问题。
总结
云原生还是一个相对新鲜的事物,而这篇文章也只是我这段时间以来,有限地接触到云原生一些概念和场景之后做的一点点总结。希望后续的工作中会有更多相关的场景,也能继续积累更多的感悟。