摘要:当前云原生应用的开发模式在 FaaS 环境下存在挑战,这里提出一种开发模式构想:“单体式编程,编译时拆分,分布式执行”,旨在简化云应用开发,提升开发效率和应用性能。思路是通过编译器自动拆分单体应用代码,实现云基础设施上的分布式运行。
云原生应用通常形象地解释为应用架构出生或生长在云基础设施上的应用程序。此类基于云基础设施构建的应用程序能够有效利用云提供的自动扩缩、高可靠性等特性,这对于个人开发者和中小型企业来说,不需要关心基础设施的复杂性,又能获得强大的支撑能力,显然是一件很香的事情。
那么,现在是怎么开发云应用程序的?提到云原生应用,大家通常会想到容器、微服务,这两项技术也在 CNCF 对云原生的定义中被提到。一个典型的开发流程可能是:开发者开发微服务粒度的应用代码,封装成容器,最终托管到 PaaS 平台上。
但随着云的发展,函数计算( Function as a Service, FaaS )已经成为云的重要组成部分。相较于 PaaS ,FaaS 与云的各项能力结合的更紧密,在扩缩容、冷启动等方面,FaaS 的性能也更加强悍,例如,AWS Lambda 的冷启动时延已经达到百毫秒级。
那么,微服务、容器的开发模式是否仍适用于基于 FaaS 的云原生应用?分析来看:
整体来说,直接基于 FaaS 的开发模式体验较差,如何有效管理和协调这些函数成为了一个重要问题。显然,我们不希望开发一个应用程序是这种体验,那么,我们该如何开发云原生应用呢?在这里提出一个构想:单体式编程,编译时拆分,分布式执行。
看到这个构想,你可能会觉得这与并行编程框架 OpenMP 的思路比较相近,的确如此。OpenMP 利用编译器在代码并行区域添加多线程代码实现并行执行,而在这个构想中同样是利用编译器识别并提取可独立计算的代码区域,但具体的分布式执行则交由云基础设施负责。
大数据计算的 MapReduce 、Spark 可能也有些相似,以及近来比较火热的 Ray ,同为云计算领域,同样都是面向大规模分布式计算。这里,与以上各类框架显著不同的一个点是,底层运行时实现方式的不同。以上各类框架都是各自构建了一套底层运行时环境来支撑特定种类任务的分布式执行,而这里的构想则是希望基于云基础设施提供的 FaaS 作为统一的底层环境,来实现通用计算,支撑各类云原生应用。两种方式相较而言,前者针对特定负载可能有调度、性能优势,而后者能与云基础设施提供的各项能力结合的更紧密,同时也能让各种场景的负载有结合的可能。
在开发体验上,的确是相近的。
因为我们在开发一个单体应用程序时体验是非常好的,我们的上下文都在一个工程项目中,变量间的依赖关系、函数间的调用关系能够在执行前由 Lint 、Format 工具,IDE 插件等各类工具进行检查。
没有任何编程约束的代码文件的确难以进行拆分,但是可以通过定义关键字、特殊类、特殊函数等方式来指导编译器对代码进行拆分。
例如下面这份代码,可以将 Function 看作使特殊类,而它在构造函数中传递的函数定义就可以被分析提取成一个独立的计算模块。当然,在实际实现时肯定需要考虑更多的情况。
class Function {
constructor(fn: (...args: any[]) => any) { /* ... */ }
}
const fn = new Function((a: number, b: number ) => { return a + b; });
async main() {
const c = await fn.invoke(1, 1);
console.log("1 + 1 = ", c);
}
main();
特别的是,在拆分出一个个计算模块后,整个代码文件相应去除各个计算模块的代码,剩下的部分也应该成为一个计算模块。而这个计算模块就是整个应用程序的入口,类似单体应用中的 main 函数,而这个 main 函数就可以完成整个应用程序的逻辑编排。
这样,我们就获得了单体式编程的开发体验,并通过编译时拆分的方式获得了基于云的分布式执行的能力。这种开发模式的产物是直接长在云基础设施上的,属于云原生应用。
来看一个基于这种构想的示例程序。这个程序是利用蒙特卡洛计算 Pi ,整个代码逻辑很简单:创建 10 个 Worker ,每个 Worker 各自执行 100 万次采样,最终汇总采样结果。
const calculatePi = new Function((iterations: number): number => {
let insideCircle = 0;
for (let i = 0; i < iterations; i++) {
const x = Math.random();
const y = Math.random();
if (x * x + y * y <= 1) {
insideCircle++;
}
}
const piEstimate = (insideCircle / iterations) * 4;
return piEstimate;
});
async main() {
const workerCount = 10;
const iterationsPerWorker = 1000000;
let piPromises: Promise<number>[] = [];
for (let i = 0; i < workerCount; i++) {
piPromises.push(calculatePi.invoke(iterationsPerWorker));
}
const piResults = await Promise.all(piPromises);
const piSum = piResults.reduce((sum, current) => sum + current, 0);
const pi = piSum / numWorkers;
console.log(`Estimated value of π: ${pi}`);
}
main();
这份代码执行起来的预期效果应该像上面这张图展示的:
calculatePi
,另一个对应整体代码去除 calculatePi
后的剩余代码( main 代码)。更复杂的示例可以从这里了解:
这个想法属个人观点,有问题欢迎指出与讨论。2024 年 Pluto 也将基于这个想法继续进行尝试,在具体实现时,会利用静态分析、IaC 等技术来支撑,感兴趣的 V 友也欢迎交流,一起来玩。
1
mightybruce 329 天前 1
云原生应用开发模式 != FaaS
单体式编程,编译时拆分,分布式执行 和 FaaS 也没有直接关系 去看看谷歌的 service weaver 吧。 至于你提到的 ray 那完全是另一回事,ray 需要对针对计算做资源和任务编排比如 task actor ,是相应 AI 人员做的事情,k8s 是针对服务部署以及服务的,而不关心计算之间的事情。 |
2
Jianzs OP @mightybruce 这里提的开发模式的确不等同于 FaaS ,而是针对 FaaS 函数难以管理和协调问题 的一个解决思路,能降低 FaaS 使用的复杂性。
我理解你的意思应该是:K8s 、云只负责分配资源与暴露服务,具体负载的执行由特定的运行时( Ray 、Service Weaver 等)来负责。 个人观点,针对负载类型构建运行时,各司其职,能带来更高的性能优势。但是对集群整体而言,资源利用效率可能下降,因为 K8s 不知道上层应用内部的情况,不能很好地混部与调度。同时,各类负载共享同一种运行时,还能促进不同类型负载进行结合,也能使不同类型的负载共享基础设施提供的 BaaS 能力。 题外话:这篇文章限于篇幅与主题,只讲述了有关计算的研发模式。其实,除了 FaaS ,云原生应用还依赖于云基础设施提供的丰富的 BaaS 能力,我们也会尝试通过编译的手段分析出应用程序对 BaaS 组件的依赖,进而自动创建基础设施环境。整体上,我们解决的问题主要是:IaC 、云背景(各种权限等)等对于个人开发者与中小团队具有上手成本,云的使用(包括 FaaS 、BaaS )仍具有较高的门槛。 |
3
mightybruce 329 天前 1
就说 ray 计算吧,kuberay 就结合 kubernetes 和 ray 两者,它是以 operator 方式来部署的。
k8s 当然知道资源的情况,但是 k8s 不需要去了解你的业务内部和一些特定的业务需求。 |
4
ZSeptember 329 天前 1
想法是不错的,对于基于 FAAS 的项目大量提高了开发体验。
不过现阶段不是很看好 FAAS ,成本太高了,以前公司项目用过 GCP 的 cloud functions ,开发体验挺差的,基本弃用了。 话说是不是别叫 lang ,我还以为是重新搞一套语言了。 |
5
Jianzs OP @ZSeptember 确实会带来疑惑,起初是有计划直接搞一套语言来着,但是感觉生态、体验就不如直接基于现有语言。现在倒也是基于编程语言工具去搞事情,AssemblyScript 不也是基于 TS 做的语言嘛,哈哈哈哈哈
能具体说说你们当时体验的感受么?体验差是差在哪里?成本高是因为请求量高了后,不如虚拟机部署? |
6
Blackn 329 天前
支持一下 OP ,感觉思路挺好的
|