整理 | 丁广辉 责编 | 张红月
出品 | CSDN(ID:CSDNnews)
如果从微服务的功能定义开始,很可能把这种模式定义为通过网络进行通信的REST服务。但当谈到编写微服务的程序时,有一个严峻的现实摆在面前,创建微服务的第一步需要涉及大量的代码。虽然已经不在考虑这个问题,仅仅是将这些代码的存在作为微服务是一个“框架”的理由,却也因此忽略了它对开销、运行时间、要求、安全性、成本和可维护性的影响。
Kubernetes并不能明显的减少微服务所需开销
当开始编写一个新微服务时,第一步就是导入大量的代码,将其分布在数百个不同的包中。安装一个极简的Node.js微服务框架时,ExpressJS会获取到100个依赖项。在能用该框架写出“Hello World”之前,这些依赖项就是100个上游库。这些依赖关系,在开始编码之前,代码的最低起点是54,000行,这还是对于最小化的框架而言。如果在向其中加入类似Locomotive这样的MVC层,起始代码的数量就会跃迁至近22万行。这种情况并非是当你使用Javascript或Node.js时才会发生,类似的情况在大多数编程语言中比比皆是。
在此基础上,在将“业务逻辑”分层在框架之上。与起始阶段的几万甚至几十万行代码相比,业务逻辑那仅仅几千行的代码,通常只是依赖关系上的一层皮毛。
为了运行一个微服务,需要从一个HTTP服务器开始。这个服务器上将只运行一个微服务,并且需要一直运行。因此再生产中,需要扩展到多个实例,以便能够有效的处理故障情况和负载高峰。
而代码行数的多少部分决定了需要什么样的计算资源。代码库越大,内存和存储就会越多,最终导致使用的CPU也越多。在现如今的云计算世界里,这就直接转化为需要的计算单元的大小。然后,考虑到每个计算实例的基本需求,在将其乘以需要运行的副本数量。最后在考虑到一般应用的微服务的平均流量,可以得出要支付的成本是以复制的方式保持一个完整的应用一直在运行,即使它在大部分时间都处于空闲状态。
这些都是传统微服务需要支付的开销。虽然Kubernetes帮助解决了运行时存在的问题。它的存在让不需要在每个虚拟机上运行一个微服务,而是运行一些庞大的虚拟机,让Kubernetes把负载分配给它们。但是需要支付的开销依然并没有减少。Kubernetes也从未表明它的核心价值是降低云计算成本。现在的情况是,就算使用Kubernetes运行微服务,依旧要拥有足够的计算能力,来保持这些微服务一直运行,无论它们是否收到请求。
如果对经济性感兴趣,如果对在生产中运行时省钱感兴趣,即使只是对消耗更少的CPU和内存感兴趣,那么现在也是时候挑战每个微服务都以数十万行的导入代码开始的想法了。
但在理解这意味着什么之前,可以从安全的角度来谈论它,尽管也可以从开发者压力的角度来谈论它。
安全可以更容易
在微服务开始时有50至100个依赖关系,其中许多依赖关系都是跨领域的。这意味着并不直接使用它们,它们是其他依赖关系的依赖。在你写下第一行代码开始,就需要做好承担安全责任的准备。因为每当这些库中的一个漏洞被暴露出来,或者发生了零日漏洞在上游库被修复的情况时,开发者这些在依赖关系上编写应用程序的人就是那个修复或采取防御措施的人。
在传统的微服务架构中,作为开发者要对安全操作的方面负责。而现在,只要从其他来源导入代码,安全操作这个问题就会存在。虽然通过使用其他库,会获得很多好处。但在某种意义上,必须接受这样一个事实:操作性问题的依赖关系安全性,将永远成为开发者的关注点,因为开发者是必须解决这个问题的人。
但可以在堆栈的最关键层减少风险。在目前的微服务架构中。代码包括HTTP服务器、TLS实现、主机运行时代码、系统接口和其他低层次的问题。除此之外,由于开发者的代码是作为主机操作系统上的一个进程运行的,所以开发人员也就要承担起进程安全的责任。还是以一种非常现实的方式,毕竟当安全问题出现时,进程是整个模型中最敏感的部分。
换句话说,突破进程、劫持SSL或访问系统文件往往会出现最严重的安全缺陷。而在目前的微服务模型中,开发人员一般依靠外部库来处理这些问题,并将这些库复制到自己的代码库中,然后开发者负责在出现漏洞时做出反应。一般开发者对堆栈中最关进的部分都会采取这种做法。但开发者需要对堆栈这部分负责吗?目前还不清楚开发者是否真的需要负责。
做好微服务的更好方法
微服务已经做的很不错了。把复杂的工作流程分解成更容易管理的小块,也正确地考虑了关注点的分离。准确来说,开发人员把任何特定单元的认识开销保持在相对较低的水平。反过来,开发者也得到了很好的服务。
相比之下,上面看到的问题是低层次的问题,它们的出现与开发人员做出的技术选择相关,而且开发人员一般在很大程度上忽略了这些问题。不过,可以从亚马逊的Lambda的功能即服务(FaaS)模型中找到一些替代方案来避免这些问题。FaaS承诺,开发人员可以不用编写出一整个服务器,而是只写一个处理函数就够。当收到输入指令时,该函数将被执行,并且只运行到它能产生输出就会被送回。这种模式就很令人舒服,因为几乎没有“低级”代码需要管理。
当然,FaaS并不是万能的,它也存在问题,首先,FaaS的实现是被平台约束的。亚马逊的FaaS和Azure的工作方式不同,就导致你使用了FaaS就无法使用Azure。第二,因为平台施加了限制,所以FaaS能做的事情是有限的。不一定需要深入研究这些细节,仅从表面上看,由于FaaS的这些限制,导致它与微服务相比,使用限制相当明显。
但如果把FaaS的强项移植到微服务模型上会怎样?能够为微服务架构建立一个更好的基础?如果看一下现在流行的网络框架,会发现顶层模式是相当标准的将路由映射到处理函数中。在大多数情况下,底层代码没有什么是真正需要开发者关注的。可能需要你去配置一些东西,比如主机名或端口号什么的。但很少有人会从一个框架开始就花时间去修补HTTP或TLS的实现。因为从这开始,就会为程序增加人外的代码数量。因此,下一代微服务平台的逻辑位置就是消除基本的依赖关系,让其工作时仅运行HTTP服务器。TLS、流程管理和基本路由。
编写一个新的应用程序将从映射路由到处理函数开始,而不是通过创建一个Web服务器。理想情况下,这就是应用程序的安全边界运行的地方。开发人员将对代码和任何新依赖关系的安全负责。但该平台将保持较低的水平,使其升级成为一个操作性问题,而不是开发者的问题。最重要的是,这意味着一个SSL补丁(例如)将不需要你重建、重新测试和重新发布你的应用程序。相反,你只需要更新你的主机运行环境,这通常可以在没有任何开发人员参与的情况下完成。
像FaaS模型一样,以这种方式编写的代码可以 "扩展为零"。这也就意味着,当你的代码没有处理请求时,它就不会运行。底层平台将一直在那里等待新的HTTP请求。但由于请求处理方法的安全边界存在,导致多个应用程序可以使用同一个HTTP监听器,反过来看,意味着可以实现更高的密度。而在这种情况下,这直接转化为计算需求。
不过,也需要做一些架构上的思考。因为在这种模式里,应用程序的启动速度成了一个关键。需要的启动速度要低至微妙甚至是毫秒的程度,而基于容器的服务启动速度一般都倾向于15-120秒。虽然很多人都对“无状态微服务”口诛笔伐,但现在它是一个真正需要达到的要求。
我们有WebAssembly!
容器生态系统以及之前的虚拟机生态系统的只要好处是隔离。虚拟机提供了一个隔离工作负载的有效方式。而容器的出现再次完善了它。上面谈论的从本质上消除Web服务器来减少微服务的代码数量是一个非常好的想法。
但又出现了一个新问题,还能实现所期待的隔离水平吗?对于这个问题可以毫不犹豫的回答:当然可以,因为我们有WebAssembly。
WebAssembly最初被认为是一个浏览器执行环境,是超越Javascript的一步,是做Java Applets和ActiveX曾经试图做的事情的更好方法。但事实证明,在目前的情况下,浏览器的沙盒模型正是需要的沙盒模型。虽然它在运行时不信任(或沙盒)二进制文件,但通过能力模型,它确实会有选择地给沙盒中的代码提供功能。而其中启动速度是最重要的,毕竟用户不喜欢缓慢的网站。另外,主机的架构和操作系统不必与开发者的主机和操作系统相同,这也是云计算的一个优势。此外,WebAssembly格式被设计为许多编程语言的编译目标。
WebAssembly所具备的能力正是微服务的新迭代所需要的。开发人员可以用任何想要的语言,例如Rust、C、Javascript、Python等等,去编写HTTP处理程序,然后将其编译为WebAssembly。这样当主机运行时就会提供必要的功能,如文件系统(高度沙盒化的)、出站HTTP、键/值存储等。可以把微服务集中在处理离散的问题上。而底层平台可以用来管理所有的HTTP、SSL/TLS等等。
Fermyon就正在研究一个这样的平台,它与一组可插拔的组件相结合,将安全地提供微服务所以需要的能力。而在这个过程中他们还发现了这种工作方式的一些额外的优势。
工具标准化
当网络服务器被外部化,或将组件作为能力注入到运行当中时,开发人员对代码进行检测就会获得一些好处。这些好处表现为对运行代码的调试、诊断和监控方面的改进。
例如,可以通过提供标准化的指标和入职作为开始。将其植入到平台中去。因为每个请求的计时都是随时可以使用的,所以不需要浪费时间开发,还有正常运行时间、可用性和故障报告也是一样的道理。所有的这些都是平台自带的功能,因此不需要开发。
还有更令人开心的事,当你开始在应用程序中记录日志时,平台可以将日志信息转化为日志分析器喜欢的结构化格式。而这些信息与更广泛的平台日志紧密的联系在一起。这也就意味着,开发者过去需要做的无聊的杂事之一,现在可以交给平台来处理了。而且可以更轻松的做日志分析并获得丰富的指标。
当然还可以让做到眼不见,心不烦,平台可以将指标和日志组件带入WebAssembly二进制文件中。在这种情况下,开发人员能够轻松的利用模块中的函数调用来插入指标数据,并将这些信息直接发送到平台的指标工具中。这不能让这项工作更简单,仅仅是让其变得不可见了而已。
实现细节和数据服务的外部化
有时开人员关心自己的键/值是否Redis,有时却又漠不关心。Fermyon平台确保了对键/值存储、对象存储、文件系统、环境变量等功能,只提供一个实现。本地文件只能使用平面文件。服务器版本可以使用Redis、Minio和NFS。云版本可以使用托管版本。但是对于开发者来说,API是一样的。调用一个函数来指定一个键和值,调用另一个函数来传递键和值。其实只要核心功能按照预期工作,实现细节就无关紧要。
WebAssembly组件模型使数据服务外部化成为一种可能,而且不需要特殊的中间件层或外部服务。相反,在启动时,WebAssembly运行环境会自动选择正确的组件来满足开发者指定的界面要求。面向对象的开发者可能会从接口/类的角度考虑这个问题,但有一个关键区别,选择实现一个接口是运行时一个操作,而不是开发者代码的一部分。
这个由WebAssembly带来的可能性有助于对微服务进一步的理解,帮助开发人员重新思考对分布式计算的理解。
将分布式计算推进一步
曾经的分布式计算在实践时显得非常混乱。单片机应用在这方面展现出笨拙的一面。但现在这一切都发生了变化。容器成为了一种新兴事物,Go语言等在很大程度上借鉴了CSP等分布式计算概念。更重要的是,微服务架构的出现挑战了分布式计算很难,以及对它应该如何完成这两个假设。
时代发展,旧的教条退去了。开发人员开始把分布式计算看做事一个新问题,而关于这个问题可以通过小型和狭义的网络服务、REST-ful应用程序以及对无状态计算的投入组合来解决。Kubernetes为部署微服务架构提供了一个强调无状态方式的协调框架。
分布式计算似乎并不是一个很难实现的目标。但开发人员需要做出的架构决策却被推到了风口浪尖上。因此,我觉得需要认真的思考下微服务应该如何实现结构化,以实现正确的网络拓扑结构。有意思的是,当Go将CSP融入自身时,Kubernetes也将CSP融入了基础设施中。而开发者不得不在两个不同的抽象层次上思考同一个模式。
而WebAssembly是为开发人员提供的另一种实现分布式计算的方法,它可以减少开发分布式应用的认知开销。而组件模型正是实现这一目标的关键。在上面,我们把组件看成是将外部服务嫁接到微服务的一种方式。但并没有真正的谈及组件是什么这个问题。因为其中很重要的一点是,接口的定义使得实际的实现细节和开发者无关。而且其中一些实现实际上是底层主机运行时带来的功能。这些功能在运行时被挂到了WebAssembly二进制文件中。
但是一些其他的实现却不是主机运行时的一部分。它们只是其他的WebAssembly模块,在启动时被链接到了一起。这样一来,它们对开发者来说就像是一个库。但在平台上却是作为一个独立的WebAssembly模块被执行。它们有自己的内存空间,并与开发者的代码相互隔离。当开发者与这个外部组件交互时,感觉起来像是调用了一个函数,但行为上看起来更像一个RPC。
这也正是分布式计算有趣的地方,在运行时,不是开发者在处理计算的分布。就开发者而言,它只一个对库的调用。运行时可以选择在哪里运行该组件,且与开发者的代码相邻。开发者对它唯一关心的就是功能是否被正确的、安全的、可靠的执行,仅此而已。分布式计算不再是第一代微服务的复杂拓扑结构,它是编写微服务新方法中的一个事实细节。
结论
微服务架构一直是以云为中心开发的主要推动力,这一点是毋庸置疑的。而虚拟机开启了微服务架构的可能性。在微服务的第二波浪潮中,容器生态系统将分布式计算与微服务编撰为云计算的“新常态”。而在这一过程中,遇到了一些困难。这使得我们暂停下来并重新思考一些假设,关于安全、关于设计、关于与外部服务合作的假设。
当我们思考这些假设时,看到了WebAssembly带来的可能性。我们可以以此来减少开发人员在关键层面安全上花费的时间。而且还减少了微服务开发者必须撑到的代码数量。与此同时,还可以改善日志和指标,同时简化管路外部服务的过程。而提供这些优势机制——WebAssembly组件模型,为我们提供了一个光明的分布式计算未来版本。
参考链接:https://www.fermyon.com/blog/rethinking-microservices
—END—
《新程序员001-004》全面上市,对话世界级大师,报道中国IT行业创新创造
— 推荐阅读 —
☞危机四起!俄罗斯遭 Oracle、苹果、Google 等断供背后的启示
☞微软再损一将!继Nat Friedman后,另一Xamarin联合创始人也已离职
☞火热的 Web 3,究竟离我们有多远?