Platform As A Runtime(PaaR)——超越平台工程
原文:Platform as a Runtime (PaaR) - Beyond Platform Engineering
作者:Aviran Mordo
我个人对平台工程非常有兴趣。Wix已经实施了十几年的平台工程——当然,在“平台工程”这个名词诞生之前就开始了。本文将介绍 Wix 工程团队在过去几年中部署和实施的一些项目,这些项目将平台工程提升到了一个新的水平。接下来我们将深入探讨一些新的平台项目,为我们的未来愿景提供一些见解。
提供一些背景信息,Wix 是一个领先的网站构建平台,拥有来自 190 个国家/地区的超过 2.4 亿注册用户(网站构建者)。我们的平台部署在全球 3 个地区的 20 个服务点。Wix 有 5,000 多名员工,其中约一半从事研发工作。因此,可以肯定地说,(软件)工程是我们工作的核心,也是我们业务价值的核心驱动力。
The need for speed
成功的软件企业,能够快速交付高质量代码。随着规模的不断扩大,挑战也接踵而来。随着公司的发展壮大,因为依赖性、存量代码和复杂性的增加,让软件的交付过程日趋缓慢。
计算机科学和(软件)工程诞生以来,就一直在涌现各种方法,用于对工作进行组织,从而实现更高的速度和质量。而持续交付、DevOps 和 Serverless 是目前的最新趋势。
随着方法论趋势的演进,我们的工具也随之变化。这些技术的目的,当然是为了让公司能够更快速地交付软件——初创科技公司的数量及其增长推动了这样的演进。为了支持这种增长,我们看到云托管/计算提供商的出现,它们可以快速访问庞大数据中心的服务器,成功地降低了运维开销。
随后出现了微服务理念,使公司能够更快地扩展规模并“更轻松”地维护其软件。最后,我们有了 Serverless,它消除了更多的开销和对服务器进行维护的需求。顾名思义,使用 Serverless,您就不再需要自行维护服务器。服务器扩容、服务机配置、Kubernetes 集群管理等问题不复存在,运维开销进一步被缩减。
微服务如何影响开发速度
如果从单体服务开始,那么事情相当简单。可以选用任何框架,利用任何工具。一切都能快速推进。
但是随着规模的扩张,服务数量开始膨胀,事情就开始变得复杂。系统和服务之间需要进行集成——RPC/REST、数据库、消息服务等开始出现在你的组件列表里。运维、测试和管理的工作越来越多。这时就需要框架了。
少量的微服务没什么问题。然而随着你的不断成长,微服务会越来越多。下图是 Wix 的微服务地图——每个矩形是一个微服务集群,这些连线代表了微服务之间的通信。
如你所见,越多的服务就需要越多的团队。微服务之间需要协调工作,因此需要考虑他们之间的共性——他们需要使用相同的“语言”,也就是使用相同的协议和接口。例如如何处理 Cookie 和安全性?怎么完成 RPC 调用?Http Header 和 日志如何处理?
下图中的条目,列出了一个需要关注的问题的列表,这些内容重要,但不完整。
微服务架构的冰山一角
让我们看一个示例服务的分层。虚拟机是基础层。应用在容器内运行,其之上是微服务/应用程序框架——例如 JVM 世界中的 Spring、NodeJS 的 Express。然后,在此之上,构建可信环境框架,该层使所有底层服务能够以相同的方式进行通信。通过相同的协议,使用相同的 HTTP Header,使用相同的加密/解密算法,等等。这样,它们就可以被网络上的所有其他服务使用和信任。
软件工程师会在这些层的基础上工作。业务服务处于金字塔的顶端,这些服务代表的是公司实际销售给客户的业务价值。
对于我们工程师来说,这只是冰山一角。在我们处理业务逻辑代码之前,首先要处理它的整个底部部分。不幸的是,开销并没有到此为止。除了需要开发的实际产品功能之外,开发人员还需要考虑法规、业务和法律问题,例如 GDPR 合规性。这虽然不是实际销售的价值,但却是每个服务的必要内容。
这些介绍有些冗长,但它解释了开发大型系统的重要性。现在让我们喘口气,谈谈……
平台即运行时(PaaR)
通常会使用内部的框架或库来构建微服务,gRPC、Kafka 客户端、连接池、A/B 测试等内容都来自这些基础代码。但是,通过这种方法,最终得到的是一个分布式通用框架。在这种情况下,就需要频繁地更新所有微服务,以便让所有服务的版本保持兼容。
如何应对这种依赖开销呢?
一种方案是把构建时依赖修改为运行时依赖。但在 Wix,我们更进一步。除了我们之前提到的通信协议和合约之外,构建内部框架时还需要考虑其他问题。这些其他问题包括常见的业务流程、常见的法律问题(GDPR、PII 等)、常见的租户模型、权限、身份管理等。
因此,我们将所有这些业务问题和流程添加为运行时依赖项,最终得到平台即运行时 (PaaR).
添加到 PaaR 中的众多内容意味着什么?每个在 PaaR 中运行的微服务都会自动处理这些问题,而无需在每个服务中进行开发。例如在 GDPR 场景里,所有服务都会自动使用 GDPR 方式获取个人数据并在合适的时间“忘记”数据,从而节省每个服务宝贵的开发时间。
我们是怎样做到的?
首先从我们的 Serverless 平台开始。平台核心是 NodeJS,支持了整个应用程序框架。选择 Node.js 的原因——它轻量级,支持动态代码加载,并且简单易学。
第一步:我们将整个框架与 GRPC/REST/Kafka(包括发现服务)的集成层一起编码到 Node.js 服务器中,并使其成为“运行时服务器”。
这样一来,我们就拥有了一组具有相同功能的“运行时”服务器。
第二步:我们为数据服务添加了另一层,让开发人员可以轻松快速地连接数据库(运行时处理所有连接字符串、连接池、JDBC 等)。
我们最终得到的是一个“智能”容器,它可以处理入口流量,并将所有常见问题嵌入其中,但没有太多业务逻辑。
这两个步骤后,开发人员可以用运行时依赖的方式来构建业务逻辑,依赖项的代码不会和业务代码耦合在一起编译打包。
开发人员在本地完成开发之后,就可以准备进行部署了。在 Serverless 生态中,部署应用无需关注 Node.js 服务或者容器的细节,只需要部署 TypeScript 文件或包,平台将更新代码自动加载到平台的运行时。
运行时自带了注册发现能力,因此在进行服务间调用时,只需要声明调用目标,平台会自动提供匹配的客户端来完成对目标服务的调用。
这些交互都是通过运行时完成的,也就是说,真正的集成工作是在运行时完成的,因此集成测试的工作开销也减少了。
这种做法的好处是,只需部署业务代码,而不必将其与公共库和 Node.js 运行时捆绑在一起,这使得可部署的文件非常小(大小通常小于 100Kb)。
分离项目结构和部署拓扑
上文中描述的内容,和 Lambda 地行为是颇有些相似的。差异在于,Lambda 的框架和也是和可部署程序捆绑在一起的。但是如果我要加入新函数要怎么办呢?在 Lambda 中,需要新建一个实例,或者把新的函数添加到现有的实例中;而在我们的可信环境中,我们可以把多个函数运行在同一个进程里。简单说,我们可以用微服务或者函数的粒度进行开发,但是所有这些功能又可以用单体进程的方式来运行。当然,我们也可以把这些功能分离到平台中不同的主机上——像微服务一样。
不仅如此,我们还能用微服务或者单体的方式开发软件,并把软件的不同部分,部署到不同的平台主机上。
举个例子,假设我们有一个服务,它有两个服务端点:一个用来响应用户请求的 RPC 端点;还有一个用来监听 Kafka 主题。我们可以在同一个项目中开发这两个函数,沿着这个思路,开发者会认为这两个端点是是一体的,这些功能是同一个逻辑服务,但其实他们会被部署到不同主机上。
这样一来,这里的两个端点就有了不同的伸缩策略。在没有增加开发者心智负担和工作量的情况下,面向顾客的 RPC 函数和面向 Kafka 的两部分内容能够各自伸缩,却又完全无需进行重构和拆分。
在 Wix,函数的发布是轻松又高效的。开发者将函数推送到 Github 仓库后,新增代码只是一个小小的函数,只要个把分钟,新代码就能部署和启动运行了。
平台工程思维
那么我们收获了什么呢?
- 集成非常容易:开发人员能够简单的声明要调用的函数和微服务,就能够自动获得所需的客户端。
- 测试代码少:胶水代码无需进行集成测试
- 开发人员专注于业务逻辑的实现,开发和部署粒度非常小。
- 部署速度快:代码量小,无需框架公共库的打包工作
- 无需脚手架:集成工作都已经预先配置,并且开发人员用生命方式就可以引用。
未来愿景
接下来,我们应该如何利用现有资源并在此基础上进行构建呢?例如我们可以让不同的团队管理各自不同的运行时。只需要克隆环境就能完成这样的需求。这样一来,ecom 和 blog 团队都可以有各自不同的运行时集群,这种情况下,不同团队的代码就不会被推送到同一运行时。但是不同运行时之间的函数还是可以互相调用的。
在这种情况下会产生一个需求:根据运行时来优化函数之间的亲和性。假设我们有两个运行时,每个运行时都包含多个函数:
上图看到,函数 2 和 5 之间存在交互。他们在不同的运行时中运行,网络延迟会降低他们之间的通信效率。如果我们能让系统自动地把函数 5 部署到第一个运行时,函数 2 部署到第二个运行时,调用关系就会变成这样:
这样我们就跨过构建,直接在运行时完成函数的组织和优化,以此类推,我们能够形成一个跨网调用最少的高度优化的运行时环境。而现在我们只能把领域设计、开发环境跟运行时拓扑进行耦合(才能达成这样的效果)。
单一运行时才是未来
将运行时扩展到其他编程语言是个有吸引力的想法。依我看,要完成这一目的,不需要构建多语言系统,也不应该为特定技术堆栈多次构建相同的框架。我们需要的是能够支持任何语言的单一运行时
要实现这一目标,我们设想的方法是将整体框架一分为二:“Host”和“Guest”。Host 中包含运行时框架、服务集成和数据服务层;Guest 中包含的则是业务代码以及业务代码的集成能力。这样一来,只需要开发一次应用程序框架,而不是不断尝试在不同编程语言框架之中努力实现对等功能。这种方法的明显优势就是只要在 Host 上进行框架更新,无需跨语言的重复劳动。
缺点也是存在的——Guest 之间的调用是通过 Host 的跨进程通信来完成的。我们还在使用 GraalVM,试图在 Host 进程之间运行多个不同语言实现的 Guest。现在我们是通过两个不同的进程,简化了开发工作,得到了一个可用的系统。
当下进展
我们的开发人员很喜欢目前的工作,这标志着该方法的成功。方法的核心就是平台工程思维,在这种环境中,很多复杂问题变得简单。Wix 开发人员能在几小时内开发出从前需要开发几天甚至几个星期的东西。
从本质上来说,我们的平台工程超越了简单的开发人员门户,取而代之的是成熟的 PaaR,为开发运维工作降低了心智负担。
要了解更多信息,请观看:Beyond Serverless and DevOps。