WebAssembly + Dapr = 下一代云原生运行时?

1.jpg

做者 | 易立 来源 | 阿里巴巴云原生公众号git

云计算已经成为了支撑数字经济发展的关键基础设施。云计算基础设施也在持续进化,从 IaaS,到容器即服务(CaaS),再到 Serverless 容器和函数 PaaS (fPaaS 或者 FaaS),新的计算形态相继出现。以容器和 Serverless 为表明的云原生技术正在重塑整个应用生命周期。github

2.jpg

在 Gartner 分析报告中,云计算基础设施的发展路径,也是云原生特质逐渐加强的过程。其具体表如今:正则表达式

  • 模块化愈来愈高- 更加细粒度的计算单元,如容器和 Serverless 函数,更加适于微服务架构的应用交付,能够更加充分利用云的能力,提高架构敏捷性。redis

  • 可编程性愈来愈高- 能够经过声明式 API 和策略进行实现自动化管理与运维,能够经过 Immutable Infrastructure (不可变基础设施)进一步提高分布式应用运维的肯定性。docker

  • 弹性效率愈来愈高- VM 能够实现分钟级扩容;容器与 Serverless 容器能够实现秒级扩容;借助调度优化,函数能够作到毫秒级扩容。数据库

  • 韧性愈来愈高- Kubernetes 提供了强大自动化编排能力,提高应用系统自愈性。而 Serverless 进一步将稳定性、可伸缩性和安全等系统级别复杂性下沉到基础设施,开发者只需关注自身业务应用逻辑,进一步释放了生产力,提高系统的可恢复能力。编程

分布式云则是云计算发展的另一个重要趋势,公有云的服务能够拓展到不一样的物理位置,让计算进一步贴近客户。分布式云让客户享受云计算的便利的同时,也能够知足对计算实时性和安全合规的诉求。这也推进了企业应用架构的变化 - 应用要可以在不一样的环境进行部署、迁移,以最优化的方式提供服务。json

进一步随着移动互联网,AI 与 IoT 等新技术的涌现,无处不在的计算已经成为现实。与此同时,这也在催生算力的多样性,X86 架构一统天下的时代已通过去,ARM/RISC-V 等芯片新势力不但称雄移动通讯和嵌入式设备领域,也在向边缘计算和数据中心市场发起进攻。开发者甚至须要让应用支持不一样的 CPU 体系架构,好比咱们能够将一个图像识别应用部署在边缘或者 IoT 等不一样环境、不一样体系架构的设备之上运行。后端

在分布式云、边缘计算、云端一体等新的云计算场景下,下一代云原生应用运行时将具有什么样的特色?设计模式

下一代云原生应用运行时

1. 无处不在的计算催生下一代可移植、高性能、轻量化的安全沙箱

容器应用采用自包含的打包方式 -- 容器镜像,它包含了应用代码和依赖的系统组件,能够实现应用与基础设施解耦,让应用能够在公共云、专有云等不一样的运行环境以一致的方式进行部署、运维,简化了弹性和迁移。此外 Docker 镜像规范支持多架构(Multi-Arch)镜像,能够简化不一样 CPU 体系架构(如 x86, ARM 等)的应用镜像的构建与分发。

函数应用只包含用于事件响应的代码包,这将应用交付格式从原生二进制文件提高到了高级语言层面。这也给应用的可移植性带来了更大的想象空间,理论上甚至能够屏蔽执行环境 CPU 体系架构的差别。好比对于不依赖本地代码的 Python/NodeJS 等脚本或者 Java 应用,无需修改就能够在 x86 或者 ARM 等不一样 CPU 架构上运行。

然而理想很丰满,现实很骨感,可移植性和厂商锁定是函数 PaaS 发展的拦路虎。

  • 不少脚本代码依然须要经过调用原生代码来实现数据处理和调用中间件(如数据库驱动),可是编译原生代码须要构建环境与目标执行环境一致才能保障兼容性。好比 AWS Lambda / 阿里云函数计算都要求二进制原生代码依赖指定的内核和 libc 版本。所以,愈来愈多的函数 PaaS 服务支持容器镜像做为载体,来简化函数应用打包和依赖管理。

  • 函数应用一般依赖后端服务(BaaS, Backend as a Service)实现数据访问与计算处理等能力,因为 BaaS 不存在任何标准,这样很难将在 AWS Lambda 上开发的函数应用移植到阿里云的函数计算服务。

在 Serverless 计算中,现有的主流技术是利用沙箱容器技术,如 AWS Firecraker 或者阿里云沙箱容器,来实现强隔离的安全执行环境,可是也带来更大的资源消耗。虽然如今阿里云沙箱容器通过优化能够实现 300ms 的冷启动速度,接近 Docker 这样的 OS 容器启动速度,可是还没法知足函数 PaaS 毫秒级的启动要求,目前须要经过的调度策略,预留必定的 standby 实例才能够知足,可是这样也引入了更多的资源消耗。

WebAssembly(WASM) 是一个新的 W3C 规范,是一个通用、开放、高效、安全的底层虚拟机抽象。它的设计初衷是为了解决JavaScript的性能问题,使得 Web 应用有接近本机原生应用的性能。能够将现有编程语言应用,如 C/C++, Rust 等,编译成为 WASM 的字节码,运行在浏览器中的一个沙箱环境中。

WASM 让应用开发技术与运行时环境解耦,极大促进了代码复用。Mozilla 更在 2019 年推出了 WebAssembly System Interface(WASI),它提供相似 POSIX 这样的标准 API 来标准化 WebAssembly 与系统资源的交互抽象,好比文件系统访问,内存管理等。WASI 的出现拓展了 WASM 的应用场景,可让其做为一个虚拟机运行各类类型的服务端应用。WASM/WASI 为应用的可移植性带来全新的但愿,为了进一步推进 WebAssembly 生态发展,Mozilla、Fastly、英特尔和红帽公司携手成立了字节码联盟(Bytecode Alliance),共同领导 WASI 标准、 WebAssembly 运行时、工具等工做。

WebAssembly 所具有的的安全、可移植、高效率,轻量化的特色,为应用沙箱的发展带来了全新的思路。WASM 能够轻松实现毫秒级冷启动时间和极低的资源消耗。同时 WASM 字节码比原生机器码有更高的安全级别。此外,WASI 实现了细粒度基于能力的安全模型,遵循最小权限原则。在执行过程当中,WASI 应用只能访问由依赖注入指明的确切资源集,这种方式与传统粗粒度的操做系统级隔离相比,进一步收敛了安全攻击面。

正因如此,WASM/WASI 获得了 Serverless、IoT/边缘计算等社区的普遍关注。Fastly、Cloudflare 等厂商相继发布了基于 WebAssembly 技术实现了更加轻量化的 Serverless 服务。

然而 WebAssembly 在服务器端的应用之路依然布满荆棘。首先 WASI 的能力还在很是早期的状态,一些关键能力依然缺失,首当其冲的就是缺少标准化的网络访问能力:https://github.com/WebAssembly/WASI/issues/315

目前 WASI 应用仅能作一些计算类任务,基本没法实现分布式应用,也没法调用多样性的后端服务和 Redis、MySQL、Kafka 等应用中间件。这大大限制了 WASI 的应用场景。

当理想撞上现实,头破血流仍是绝处逢生?

2. 下一代可移植应用运行时加速编程界面上移,应用基础设施能力下沉

Dapr 是微软开源的面向云原生应用的分布式应用运行时,目标使全部开发人员可以使用任何语言和任何框架轻松地构建弹性的、事件驱动的、可移植的微服务应用。

3.jpg

Dapr 实现了一系列构建高性能、可伸缩、高可用的分布式应用的设计模式,好比提供了服务发现和服务调用能力,也实现了一个简单、一致的编程模型来支持事件驱动应用架构。

此外 Dapr 经过基础设施屏蔽了应用访问后端服务的技术细节,如资源绑定、安全管理,可观测性等等。这个对 Serverless 应用很是重要,一方面将开发和部署进行了解耦,让开发者和运维团队能够经过关注点分离简化系统复杂性;一方面,能够将短生命周期、无状态的 Serverless 应用逻辑,与数据库链接池管理这样的长期运行,有状态的中间件访问能力进行解耦,提高了 Serverless 应用的可伸缩性和运行效率。

“Any language, any framework, anywhere” 是 Dapr 的重要设计目标。Dapr 经过在应用和后端服务之间,经过 Sidecar 方式提供一个抽象层,并经过标准化的 HTTP/gRPC API 实现了应用的可移植性,和后端服务的可替换性。

走向诗和远方

4.jpg

咱们能够将 WebAssembly 和 Dapr 相结合,来实现可移植、强隔离、轻量化的微服务应用架构。Dapr sidecar 与 WASM 虚拟机部署在一块儿。WASI 应用经过 HTTP/gRPC 访问本地的 Dapr 服务端点,由 Dapr 代理链接各类后端服务或者实现服务间通讯。

这样的架构设计让 WASI 应用的安全边界很是清晰,符合 WASI 安全模型,WASI 应用只能经过 Dapr sidecar 实现外部服务访问。同时在这个架构中,只有 WASM 虚拟机和 Dapr 做为可信的环境依赖以原生机器码运行。而应用是可移植的 WASM 字节码,大大提高了架构的可移植性和安全性。

来自微软 Deis Labs 的 Radu Matei,最近提供了一个实验性项目能够为 WASI 添加 HTTP 支持。详见:_https://deislabs.io/posts/wasi-experimental-http/ _

在此基础上,咱们来构建一个最小原型,验证 WebAssembly 与 Dapr 相结合的技术可行性。

1. Dapr 环境准备

咱们首先按照 https://docs.dapr.io/getting-started/ 的流程:

$ dapr init
⌛  Making the jump to hyperspace...
✅  Downloading binaries and setting up components...
✅  Downloaded binaries and completed components set up.
ℹ️  daprd binary has been installed to /Users/yili/.dapr/bin.
ℹ️  dapr_placement container is running.
ℹ️  dapr_redis container is running.
ℹ️  dapr_zipkin container is running.
ℹ️  Use `docker ps` to check running containers.
✅  Success! Dapr is up and running. To get started, go here: https://aka.ms/dapr-getting-started


$ dapr run --app-id myapp --dapr-http-port 3500
WARNING: no application command found.
ℹ️  Starting Dapr with id myapp. HTTP Port: 3500. gRPC Port: 63734
ℹ️  Checking if Dapr sidecar is listening on HTTP port 3500
...
ℹ️  Checking if Dapr sidecar is listening on GRPC port 63734
ℹ️  Dapr sidecar is up and running.
✅  You're up and running! Dapr logs will appear here.

2. 利用 Redis 做为 WASI 应用的状态存储

咱们下面利用 Dapr 的 Get Started 的例子,利用 Redis 做为 WASI 应用的状态存储。具体逻辑以下图。

5.png

注:下面的应用须要 Rust 和 AssemblyScript 环境配置,请你们自行完成。

咱们在 Radu 项目的基础上 fork 了一个版本,首先来下载代码,并进行构建。

$ git clone https://github.com/denverdino/wasi-experimental-http
$ cd wasi-experimental-http
$ cargo build
...
    Finished dev [unoptimized + debuginfo] target(s) in 3m 02s

咱们利用 AssemblyScript 来实现了这个测试应用,测试代码以下:

$ cat tests/dapr/index.ts
// @ts-ignore
import { Console } from "as-wasi";
import { DaprClient, StateItem } from "./dapr";
import { JSON } from "assemblyscript-json";


Console.log("Testing Dapr API ....")

let dapr = new DaprClient()
dapr.saveState("statestore", "weapon", JSON.Value.String("Death Star"))

let o = JSON.Value.Object()
o.set("name", "Tatooine")
o.set("test", 123)
let item = new StateItem("planets", o)
let items: StateItem[] = [item]
dapr.saveBulkState("statestore", items)

let testObj = dapr.getState("statestore", "planets")
let testStr = dapr.getState("statestore", "weapon")

if (testStr.toString() == "Death Star" && testObj.isObj && (<JSON.Integer>(<JSON.Obj>testObj).getInteger("test")).valueOf() == 123) {
    Console.log("Test successfully!")
} else {
    Console.log("Test failed!")
}

代码逻辑很是简单,就是建立一个 Dapr 客户端,而后经过 REST API,进行 Dapr 的状态管理。咱们能够快速验证一下。

$  cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.19s
     Running `target/debug/wasi-experimental-http-wasmtime-sample`
Testing Dapr API ....
POST http://127.0.0.1:3500/v1.0/state/statestore with [{"key":"weapon","value":"Death Star"}]
POST http://127.0.0.1:3500/v1.0/state/statestore with [{"key":"planets","value":{"name":"Tatooine","test":123}}]
GET http://127.0.0.1:3500/v1.0/state/statestore/planets
GET http://127.0.0.1:3500/v1.0/state/statestore/weapon
Test successfully!
module instantiation time: 333.16637ms

3. 关键要点分析

wasi-experimental-http 项目在 Wasmtime (来自 Bytecode Alliance 的一个 WASM 实现)虚拟机上实现了扩展,支持在 WASI 应用中,访问 HTTP 服务。它还提供了一个 AssemblyScript 的 HTTP Client 实现。

wasi-experimental-http 项目:https://github.com/deislabs/wasi-experimental-http/

在此之上,咱们为 AssemblyScript 提供一个 Dapr 的封装,能够参见:https://github.com/denverdino/wasi-experimental-http/blob/main/tests/dapr/dapr.ts

// @ts-ignore
import { Console } from "as-wasi";
import { Method, RequestBuilder, Response } from "../../crates/as";

import { JSONEncoder, JSON } from "assemblyscript-json";

export class StateItem {
  key: string
  value: JSON.Value
  etag: string | null
  metadata: Map<string, string> | null

  constructor(key: string, value: JSON.Value) {
    this.key = key
    this.value = value
    this.etag = null
    this.metadata = null
  }
}

...

export class DaprClient {
  port: i32
  address: string

  constructor() {
    this.address = "127.0.0.1"
    this.port = 3500
  }

  stateURL(storeName: string): string {
    return "http://" + this.address + ":" + this.port.toString() + "/v1.0/state/" + storeName
  }

  saveState(storeName: string, key: string, value: JSON.Value): boolean {
    let item = new StateItem(key, value)
    let items: StateItem[] = [item]
    return this.saveBulkState(storeName, items)
  }

  saveBulkState(storeName: string, items: StateItem[]): boolean {
    // Handle field
    let encoder = new JSONEncoder();

    // Construct necessary object
    encoder.pushArray(null);
    for (let i = 0, len = items.length; i < len; i++) {
      let item = items[i]
      encoder.pushObject(null);
      encoder.setString("key", item.key)
      encodeValue(encoder, "value", item.value)
      if (item.etag != null) {
        encoder.setString("etag", <string>item.etag)
      }
      encoder.popObject()
    };
    encoder.popArray();
    // Or get serialized data as string
    let jsonString = encoder.toString();
    let url = this.stateURL(storeName);
    Console.log("POST " + url + " with " + jsonString);
    let res = new RequestBuilder(url)
      .method(Method.POST)
      .header("Content-Type", "application/json")
      .body(String.UTF8.encode(jsonString))
      .send();
    let ok = res.status.toString() == "200"
    res.close();
    return ok
  }

  getState(storeName: string, key: string): JSON.Value {
    let url = this.stateURL(storeName) + "/" + key;
    Console.log("GET " + url);
    let res = new RequestBuilder(url)
      .method(Method.GET)
      .send();
    let ok = res.status.toString() == "200"
    let result = <JSON.Value> new JSON.Null()
    if (ok) {
      let body = res.bodyReadAll();
      result = <JSON.Value>JSON.parse(body)
    }
    res.close();
    return result
  }
};

测试应用的 main 函数,会建立一个 Wasmtime 运行时环境,并为其添加为 HTTP 扩展,并加载执行测试应用的 WASM 字节码:https://github.com/denverdino/wasi-experimental-http/blob/main/src/main.rs

fn main() {
    let allowed_domains = Some(vec![
        "http://127.0.0.1:3500".to_string(),
    ]);
    let module = "tests/dapr/build/optimized.wasm";
    create_instance(module.to_string(), allowed_domains.clone()).unwrap();
}

/// Create a Wasmtime::Instance from a compiled module and
/// link the WASI imports.
fn create_instance(
    filename: String,
    allowed_domains: Option<Vec<String>>,
) -> Result<Instance, Error> {
    let start = Instant::now();
    let store = Store::default();
    let mut linker = Linker::new(&store);

    let ctx = WasiCtxBuilder::new()
        .inherit_stdin()
        .inherit_stdout()
        .inherit_stderr()
        .build()?;

    let wasi = Wasi::new(&store, ctx);
    wasi.add_to_linker(&mut linker)?;
    // Link `wasi_experimental_http`
    let http = HttpCtx::new(allowed_domains, None)?;
    http.add_to_linker(&mut linker)?;

    let module = wasmtime::Module::from_file(store.engine(), filename)?;

    let instance = linker.instantiate(&module)?;
    let duration = start.elapsed();
    println!("module instantiation time: {:#?}", duration);
    Ok(instance)
}

道阻且长,行则将至

WASM/WASI 为轻量化、可移植、缺省安全的应用运行时提供了良好的基础,在区块链等领域 WebAssembly 已经获得了普遍的应用。然而,对于通用性的服务器端应用,WASM/WASI 的差距还很是明显。因为 berkeley socket 这样标准化的网络编程接口的缺失,只能经过扩展 WASM 虚拟机的方式来进行补齐。此外 WASM 的多线程能力尚未被标准化,目前的 HTTP 调用采用阻塞式同步调用,还没法实现高效和稳定的网络通讯。

此外,另外 WASM/WASI 的一个短板就是开发效率和生态建设。目前而言,虽然众多的编程语言已经逐渐开始提供 WebAssembly 的支持,可是对于普通开发者而言,AssemblyScript 这样的脚本语言是更加合适的选择。AssemblyScript 复用了 TypeScript 的语法,与 Rust/C++ 相比,大大下降了学习曲线,也提供了很是好的 IDE 工具体验,如 VS Code 等。可是与 TypeScripty 经过翻译成为 JavaScript 执行不一样,AssemblyScript 应用会被编译成 WASM 字节码执行。AssemblyScript 本质上是一个静态类型的编译型语言,本质上与 JS/TS 这样的动态类型的解释型语言很是不一样。两者在语法上也有一些不一样,好比目前 AssemblyScript 缺乏对闭包 (closure) 和正则表达式 (Regex) 等经常使用功能支持,这让开发 WASM 应用仍是有必定的技术门槛。

另外与 NPM 强大的生态相比,AssemblyScript 社区也很年轻。不少功能都须要从头构建,好比对 JSON 的序列化与反序列化,咱们选择了 https://github.com/nearprotocol/assemblyscript-json ,可是其易用性和性能与成熟的 JSON 类库还有必定差距。固然咱们也看到 AssemblyScript 的快速成长,以及愈来愈多的开发者开始贡献 AssemblyScript 代码库,好比 regex 支持等等。

Dapr 的出现为 WASM/WASI 开发通用的分布式应用,尤为是为可移植的、Serverless 化的应用带来另一缕曙光。然而 Dapr 也并不是完美:API 标准化在提高对后端服务可移植性的同时也阻碍了对差别化能力的支持。Sidecar 架构在提高灵活性的同时增长了部署和管理复杂性。

做为一个理性乐观派,任何技术都有其青涩的时代,期待社区的共同努力让计算无处不在、创新触手可及的理想成为现实。

6.png

相关文章
相关标签/搜索