×
Community Blog WebAssembly + Dapr = Next-Generation Cloud-Native Runtime?

WebAssembly + Dapr = Next-Generation Cloud-Native Runtime?

This article discusses WebAssembly, Dapr, TypeScript, and AssemblyScript and how they work together to create the next-generation cloud-native runtime.

1

By Yi Li

Cloud computing has become the key infrastructure supporting the digital economy development. The cloud computing infrastructure has also been constantly evolving, from IaaS and Container as a Service (CaaS) to Serverless containers and function PaaS (fPaaS or FaaS). New computing forms are emerging every day. Cloud-native technologies, such as containers and serverless, are reshaping the entire application lifecycle.

2

According to a Gartner analysis report, the development path of the cloud computing infrastructure is also the process of increasing the cloud-native characteristics:

  • Higher Modularization - Finer-grained computing units, such as containers and Serverless functions, are more suitable for the application delivery of microservice architectures and can make full use of cloud capabilities to improve architecture agility.
  • Better Programmability - Automatic management and O&M can be achieved through declarative APIs and policies. The certainty of the distributed application O&M can be improved further through Immutable Infrastructure.
  • Higher Elasticity Efficiency - VM can be scaled out in minutes. Containers and Serverless containers can be scaled out in seconds. Functions can be scaled out in milliseconds through scheduling optimization.
  • Higher Toughness - Kubernetes provides powerful automatic orchestration capabilities to improve application self-healing. Serverless also places system-level complexity, such as stability, scalability, and security, on the infrastructure. Developers only need to focus on their business application logic, which releases productivity and improves system resiliency further.

Distributed cloud is another important trend in cloud computing development. Public cloud services can be extended to different physical locations, computing at a closer spot to customers. A distributed cloud meets customers' need of enjoying the convenience of cloud computing and the demand for real-time computing, security, and compliance. This has also driven changes in enterprise application architectures that require applications that can be deployed and migrated across different environments to provide services optimally.

Ubiquitous computing has become a reality with the emergence of new technologies, such as the mobile Internet, AI, and IoT. At the same time, this is giving rise to the diversity of computing power. The era of x86 architecture dominancy has passed. New chip forces, such as ARM and RISC-V, dominate the mobile communication and embedded device fields but also march into the edge computing and data center markets. This requires developers to modify their applications to support different CPU architectures. For example, an image recognition application needs to be deployed on edge devices or IoT devices in different environments with different architectures.

In new cloud computing scenarios, such as the distributed cloud, edge computing, and cloud-device integration, what are the characteristics of the next-generation cloud-native application runtime?

Next-Generation Cloud-Native Application Runtime

1. Ubiquitous Computing Leads to the Next-Generation Portable, High-Performance, and Lightweight Security Sandboxes

Container applications adopt the container image, a self-contained packaging method. It contains the application code and dependent system components to enable the application decoupling from the infrastructure. Thus, it allows the application to be deployed, operated, and maintained in different runtime environments, such as public clouds and private clouds, consistently. This simplifies elasticity and migration. In addition, the Docker image specification supports Multi-Arch images, which can simplify the construction and distribution of application images for different CPU architectures, such as x86 and ARM.

The function application only contains code packages for event response, which promotes the application delivery format from the native binary file to the high-level language level. This creates more room for application portability and can theoretically prevent differences in the CPU architecture of execution environments. This means Python and Node.js scripts or Java applications, which are independent of local code, can be run on different CPU architectures, such as x86 or ARM, without modification.

However, the reality is far from ideal. Portability and vendor locking are the obstacles to hinder the PaaS development.

  • Most script code still needs to call native code to process data and call middleware, such as database drivers. However, to compile native code, the build environment must be consistent with the target execution environment to ensure compatibility. For example, AWS Lambda and Alibaba Cloud Function Compute say the native binary code must depend on the specified kernel and libc version. Therefore, an increasing number of function PaaS services support container images as carriers to simplify function application packaging and dependency management.
  • Function applications usually rely on Backend as a Service (BaaS) for data access and computing. It is difficult to migrate function applications developed on AWS Lambda to Alibaba Cloud Function Compute because there's no standard for BaaS.

In Serverless computing, mainstream technologies use sandbox containers, such as AWS Firecracker or Alibaba Cloud sandbox containers, to implement secure execution environments with strong isolation. However, this method consumes more resources. Although Alibaba Cloud sandbox containers are optimized to achieve a cold start speed of 300ms, which is close to the startup speed of OS containers like Docker, they cannot meet the millisecond-level startup requirements of function PaaS. Currently, the preceding scheduling policy is a must to reserve certain standby instances. However, this also increases resource consumption.

WebAssembly (WASM) is a new W3C specification. It is a universal, open, efficient, and secure abstraction for underlying virtual machines. It is designed to solve the performance problem of JavaScript and enable Web applications to have performance close to native applications. Existing programming languages, such as C, C++, and Rust, can be compiled into WASM bytecode and run in a sandbox environment in the browser.

WASM decouples application development technologies from the runtime environment, which improves code reusability substantially. The WebAssembly System Interface (WASI) launched by Mozilla in 2019 provides a standard API similar to POSIX to standardize the interaction abstraction between WebAssembly and system resources, such as file system access and memory management. The emergence of WASI expands the application scenarios of WASM and allows it to act as a virtual machine to run various types of server applications. WASM and WASI bring new possibilities for the portability of applications. Mozilla, Fastly, Intel, and Red Hat have established the Bytecode Alliance jointly to promote the WebAssembly ecology. Together, they are leading the development of the WASI standard, WebAssembly runtime, and tools.

The security, portability, high efficiency, and lightweight features of WebAssembly bring a new idea to the development of application sandboxes. WASM can easily implement cold start in milliseconds with low resource consumption. At the same time, WASM bytecode has a higher security level than native machine code. In addition, WASI implements a fine-grained capability-based security model that follows the principle of the least privilege. During execution, the WASI application can only access the exact resource set specified by dependency injection. Compared with the traditional coarse-grained OS-level isolation, this method converges the security attack surface.

For this reason, WASM and WASI have attracted extensive attention from the Serverless, IoT, and edge computing communities. Fastly, Cloudflare, and other vendors have successively released more lightweight Serverless services based on the WebAssembly technology.

However, difficulties still exist when applying to WebAssembly on the server side. The WASI development is still in a very early stage, with some key capabilities still missing. One of the major problems is the lack of standardized network access capabilities. For more information, please see this link.

Currently, the WASI application can only perform some computing tasks. It cannot implement distributed applications in general, nor can it call various backend services and application middleware, such as Redis, MySQL, and Kafka. This effectively limits the application scenarios of WASI.

When the ideal clashes with reality, will we come to an end with a broken head or weather the storm with a way out of nowhere?

2. The Next-Generation Portable Application Runtime Accelerates the Programming Interfaces Moving Upward and Application Infrastructure Capabilities Moving Downward

Dapr is an open-source distributed application runtime developed by Microsoft for cloud-native applications. It aims to enable all developers to build elastic, event-driven, and portable microservice applications easily in any language or on any framework.

3

Dapr provides a series of design patterns to build scalable and highly available distributed applications with high performance. It provides service discovery and calling capabilities and implements a simple and consistent programming model to support the event-driven application architecture.

In addition, Dapr shields the technical details of application access to backend services through the infrastructure, such as resource binding, security management, and observability. This is very important for Serverless applications. On the one hand, it decouples development from deployment, which allows developers and O&M teams to reduce system complexity through the separation of concerns. On the other hand, it decouples the stateless Serverless application logic with a short lifecycle from stateful middleware access capabilities in long-term operations like database connection pool management. By doing so, it improves the scalability and operational efficiency of Serverless applications.

"Any language, any framework, anywhere" is an important design goal of Dapr. Dapr provides an abstraction layer between an application and backend services using Sidecar and uses standard HTTP and gRPC APIs. Thus, it implements the portability of applications and makes backend services replaceable.

Towards Ideal Implementation

4

We can combine WebAssembly with Dapr to implement a microservice application architecture that is portable, highly isolated, and lightweight. Dapr sidecar can be deployed with WASM virtual machines. The WASI application accesses the local Dapr service endpoint through HTTP and gRPC, and the Dapr proxy connects various backend services or implements inter-service communication.

This architecture sets the security boundary of the WASI application clear. It complies with the WASI security model, only allowing the access of the WASI application to external services through Dapr sidecar. In this architecture, only the WASM virtual machines and Dapr run as trusted environments dependent on native machine code. However, applications are portable WASM bytecode, which improves the portability and security of the architecture substantially.

Radu Matei from Microsoft Deis Labs recently provided an experimental project to add HTTP support to WASI. Please visit this link.

On this basis, let's build a minimal prototype to verify the technical feasibility of the combination of WebAssembly and Dapr.

1. Dapr Environment Preparation

Follow the process on → 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. Use Redis as the State Store Component of the WASI Application

Next, we take Dapr's Get Started as an example to use Redis as the state store component of WASI applications. Here's the logic:

5

Note: The following applications require certain Rust and AssemblyScript environment configuration.

We fork a version based on the Radu project. First, we need to download the code and build it:

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

We use AssemblyScript to implement this test application. The test code is listed below:

$ 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!")
}

The logic of this code is simple. It creates a Dapr client and then manages Dapr status through the REST API. We can verify it quickly:

$  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. Key Points Analysis

The wasi-experimental-http project implements extensions on Wasmtime virtual machines. Wasmtime is a WASM implementation provided by Bytecode Alliance to support access to HTTP services in the WASI application. It also provides an HTTP Client implementation of AssemblyScript.

About the wasi-experimental-http project: https://github.com/deislabs/wasi-experimental-http/

On this basis, we provide a Dapr package for AssemblyScript. Please visit this link for more information.

// @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
  }
};

The main function of the test application creates a Wasmtime runtime environment, adds it as HTTP extension, and loads the WASM bytecode that executes the test application: 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)
}

The Road with Obstacles Goes Far and Long, but Keep Going! We Shall Overcome!

WASM and WASI lay a solid foundation for a lightweight and portable application runtime with default security. They have been widely used in blockchain and other fields. However, for universal server applications, there's still a lot of room for WASM/WASI improvement. Due to the lack of standardized network programming interfaces, such as berkeley socket, the only way to complete this is to expand the WASM virtual machine. In addition, the multithreading capability of WASM has not been standardized. Current HTTP calls are blocking synchronous calls, which cannot realize efficient and stable network communication.

Another shortcoming of WASM/WASI lies in development efficiency and ecological construction. Currently, many programming languages are providing support for WebAssembly, but script languages (such as AssemblyScript) are more appropriate for common developers. AssemblyScript lowers the learning curve compared with Rust and C++ and provides a good experience with IDE tools, such as VS Code, by reusing the syntax of TypeScript. However, unlike the execution of TypeScript, which is translated into JavaScript, AssemblyScript applications are compiled into WASM bytecode for execution. AssemblyScript is essentially a static compiled language that is very different from dynamic interpreted languages, such as JavaScript and TypeScript. They are also different in syntax. For example, AssemblyScript currently lacks support for common functions, such as closure and Regex, which makes it difficult to develop WASM applications.

In addition, compared with the powerful ecology of NPM, the AssemblyScript community is still very young. Many functions need to be built from scratch, such as serialization and deserialization of JSON. Here's what we have chosen. However, its ease of use and performance still need to be improved compared with the mature JSON class library. We have also witnessed the rapid growth of AssemblyScript and how more developers are contributing to AssemblyScript code libraries, such as regex support.

The emergence of Dapr brings WASM/WASI another possibility to develop distributed applications for general purposes, especially for portable and Serverless applications. However, Dapr is not perfect enough. API standardization improves portability for backend services but also hinders support for differentiated capabilities. The Sidecar architecture increases flexibility as well as deployment and management complexity.

Every technology has its adolescence. We hope the joint effort of the community can make the idea of ubiquitous computing and imminent innovation a reality.

0 0 0
Share on

Alibaba Developer

207 posts | 33 followers

You may also like

Comments

Alibaba Developer

207 posts | 33 followers

Related Products