应用开发

MSP Serverless平台应用开发 #

一、基本概念梳理 #

本文主要介绍基于 msp serverless 平台的分布式开发概念和流程。通过以 wasm 为最小单元的开发,充分适用于当前的分布式开发环境。在展开介绍应用开发之前,我们需要熟悉其几个概念。

无论是采用传统的单体应用开发模式,还是微服务开发模式,亦或是 serverless,其解决的问题基本类似,只不过不同的模式下的叫法有差异而已。

一般而言,应用的开发都可以划分为以下几个部分:

  1. 纯业务逻辑。如:商品功能,会员功能等。面向业务开发人员。
  2. 中间件。如:数据库中间件,缓存中间件,流量控制等。面向中间件开发人员,或平台开发*人员。*
  3. 运行环境。根据不同的技术栈,有不同的运行环境,属于基础设施层。比如采用容器部署的,一般运行在k8s里,这部分面向平台开发人员。

在 msp serverless 平台中具有相似的概念,这里列举如下:

概念面向人员说明
actor业务开发编写业务逻辑的地方,如果涉及到非业务逻辑的公共能力(或中间件能力),通过引用interface的方式来获取能力。业务人员可使用rust或tinyGo来开发actor。
interface中间件或平台开发定义一些业务无关的公共能力接口,比如:keyvalue能力,但是不提供具体的实现。可以在 interface 中定义允许的操作(operations),以及各种结构体(structure)。使用 smithy IDL 进行 interface 的定义,使用 wash 工具来生成对应代码。
provider中间件或平台开发实现interface的能力,比如同样对于keyvalue功能,可以有redis实现,也可以有vault实现。并且可以用不同的技术栈来实现。可使用 rust 来进行 Provider 开发,官方未来会支持 Go。
link def业务开发或部署人员由于actor只是引用了具体的interface,并没有指定哪一个provider,所以在实际部署时,需要关联actor和provider。可以通过 shell(wash)命令行,或者平台进行操作。
wasm host中间件或平台开发作为整个 msp serverless 生态的运行基石,用于协调actor,provider之间的分布式通信,同时也提供分布式调度能力。采用 Elixir/OTP 技术栈实现。

整体的关系图梳理如下:

Untitled

msp serverless app 的开发采用基于契约(接口)的开发方式进行,其中 interface 就是契约的定义。

三、开发流程 #

如果我们需要实现一个全新的业务场景,需要进行以下几个步骤:

Untitled

接下来以 kvcounter 为例,进一步说明各个环节。

示例目标: #

开发一个 kvcounter 示例程序,当用户访问 http://localhost:8080 时,能够正常显示 counter,且每次访问 +1。

示例效果如下:

Untitled

1、创建(或使用)interface #

在本示例中,我们用到了 kv 功能,所以需要找到一个能够提供 keyvalue 功能的 interface。有两种方式,一是找到一个现成可用的interface,二是定义一个keyvalue 的 interface。

这里以定义一个新的 keyvalue 为例。

1.1 使用 wash 工具创建新的 interface #

wash new interface keyvalue

1.2 编写 interface,即:smithy 文件 #

首先确定该接口提供的操作,然后是每个操作(operations)涉及到的结构体(structure)

// key-value.smithy
// Definition of a key-value store and the 'wasmcloud:keyvalue' capability contract
//

// Tell the code generator how to reference symbols defined in this namespace
metadata package = [{
    namespace: "org.wasmcloud.interface.keyvalue",
    crate: "wasmcloud_interface_keyvalue",
    py_module: "wasmcloud_interface_keyvalue",
    doc: "Keyvalue: wasmcloud capability contract for key-value store",
}]

namespace org.wasmcloud.interface.keyvalue

use org.wasmcloud.model#wasmbus
use org.wasmcloud.model#rename
use org.wasmcloud.model#n
use org.wasmcloud.model#U32
use org.wasmcloud.model#I32

@wasmbus(
    contractId: "wasmcloud:keyvalue",
    providerReceive: true )
service KeyValue {
  version: "0.1.1",
  operations: [
    Increment, Contains, Del, Get,
    ListAdd, ListClear, ListDel, ListRange,
    Set, , SetAdd, SetDel, SetIntersection, SetQuery, SetUnion, SetClear,
  ]
}

/// Gets a value for a specified key. If the key exists,
/// the return structure contains exists: true and the value,
/// otherwise the return structure contains exists == false.
@readonly
operation Get {
  input: String,
  output: GetResponse,
}

/// Response to get request
structure GetResponse {
    /// the value, if it existed
    @required
    @n(0)
    value: String,
    /// whether or not the value existed
    @required
    @n(1)
    exists: Boolean,
}

/// Sets the value of a key.
/// expires is an optional number of seconds before the value should be automatically deleted,
/// or 0 for no expiration.
operation Set {
  input: SetRequest,
}

structure SetRequest {
    /// the key name to change (or create)
    @required
    @n(0)
    key: String,

    /// the new value
    @required
    @n(1)
    value: String,

    /// expiration time in seconds 0 for no expiration
    @required
    @n(2)
    expires: U32,
}

/// Deletes a key, returning true if the key was deleted
@rename([{lang:"Python", name:"delete"}])
operation Del {
  input: String,
  output: Boolean,
}

/// Increments a numeric value, returning the new value
operation Increment {
  input: IncrementRequest,
  output: I32
}

structure IncrementRequest {
    /// name of value to increment
  @required
  @n(0)
  key: String,
  /// amount to add to value
  @required
  @n(1)
  value: I32,
}

/// list of strings
list StringList {
  member: String
}

/// Append a value onto the end of a list. Returns the new list size
operation ListAdd {
  input: ListAddRequest,
  output: U32
}

/// Parameter to ListAdd operation
structure ListAddRequest {
    /// name of the list to modify
    @required
    @n(0)
    listName: String,

    /// value to append to the list
    @required
    @n(1)
    value: String,
}

/// Deletes a value from a list. Returns true if the item was removed.
operation ListDel{
  input: ListDelRequest,
  output: Boolean
}

/// Removes an item from the list. If the item occurred more than once,
/// removes only the first item.
/// Returns true if the item was found.
structure ListDelRequest {
    /// name of list to modify
  @required
  @n(0)
  listName: String,
  @required
  @n(1)
  value: String
}

/// Deletes a list and its contents
/// input: list name
/// output: true if the list existed and was deleted
operation ListClear {
  input: String,
  output: Boolean
}

/// Retrieves a range of values from a list using 0-based indices.
/// Start and end values are inclusive, for example, (0,10) returns
/// 11 items if the list contains at least 11 items. If the stop value
/// is beyond the end of the list, it is treated as the end of the list.
operation ListRange {
    input: ListRangeRequest,
    output: StringList,
}

structure ListRangeRequest {

    /// name of list
    @required
    @n(0)
    listName: String,

    /// start index of the range, 0-based, inclusive.
    @required
    @n(1)
    start: I32,

    /// end index of the range, 0-based, inclusive.
    @required
    @n(2)
    stop: I32,
}

/// Add an item into a set. Returns number of items added (1 or 0)
operation SetAdd {
  input: SetAddRequest,
  output: U32,
}

structure SetAddRequest {
    /// name of the set
    @required
    @n(0)
    setName: String,
    /// value to add to the set
    @required
    @n(1)
    value: String,
}

/// Deletes an item from the set. Returns number of items removed from the set (1 or 0)
operation SetDel {
  input: SetDelRequest,
  output: U32,
}

structure SetDelRequest {
  @required
  @n(0)
  setName: String,

  @required
  @n(1)
  value: String,
}

/// perform union of sets and returns values from the union
/// input: list of sets for performing union (at least two)
/// output: union of values
operation SetUnion {
  input: StringList,
  output: StringList,
}

/// perform intersection of sets and returns values from the intersection.
/// input: list of sets for performing intersection (at least two)
/// output: values
operation SetIntersection {
  input: StringList,
  output: StringList,
}

/// Retrieves all items from a set
/// input: String
/// output: set members
operation SetQuery {
  input: String,
  output: StringList,
}

/// returns whether the store contains the key
@readonly
operation Contains {
  input: String,
  output: Boolean,
}

/// clears all values from the set and removes it
/// input: set name
/// output: true if the set existed and was deleted
operation SetClear {
    input: String
    output: Boolean
}

我们看到 smithy 文件只定义了相关的接口,并不涉及到具体的实现。这也是 msp serverless app开发中比较重要的概念。

actor只依赖于具体的interface,而一个interface可以由多个provider来实现。

1.3 生成代码,发布到对应的仓,方便actor开发人员使用 #

我们可以直接执行 make 指令,生成对应的代码。之后可以把 interface 发布到 crate.io,或者使用本地路径的方式作为包依赖引入(Go语言同理,进行发包即可)。这样在实际开发 actor 时,就可以将 interface 作为项目的依赖包直接引入使用。

2、开发 provider #

provider的作用如下:启动后,通过 stdio 和 host 进行交互,其主要作用包括:健康检查、通过 RPC 和 actor 交互等。

Untitled

2.1 使用 wash 新建 provider #

通过 wash 指令,可以生成 provider 的模板代码

wash new provider kvredis

2.2 编写 provider #

重点是基于 redis 实现 interface 中定义的各种 operations,完整代码见链接,核心代码框架摘录如下:


/// Handle KeyValue methods that interact with redis
#[async_trait]
impl KeyValue for KvRedisProvider {
		/// Increments a numeric value, returning the new value
    #[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, key = %arg.key))]
    **async fn increment**(&self, ctx: &Context, arg: &IncrementRequest) -> RpcResult<i32> {
        let mut cmd = redis::Cmd::incr(&arg.key, &arg.value);
        let val: i32 = self.exec(ctx, &mut cmd).await?;
        Ok(val)
    }

    /// Deletes a key, returning true if the key was deleted
    #[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, key = %arg.to_string()))]
    **async fn del**<TS: ToString + ?Sized + Sync>(&self, ctx: &Context, arg: &TS) -> RpcResult<bool> {
        let mut cmd = redis::Cmd::del(arg.to_string());
        let val: i32 = self.exec(ctx, &mut cmd).await?;
        Ok(val > 0)
    }

    /// Gets a value for a specified key. If the key exists,
    /// the return structure contains exists: true and the value,
    /// otherwise the return structure contains exists == false.
    #[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, key = %arg.to_string()))]
    **async fn get**<TS: ToString + ?Sized + Sync>(
        &self,
        ctx: &Context,
        arg: &TS,
    ) -> RpcResult<GetResponse> {
        let mut cmd = redis::Cmd::get(arg.to_string());
        let val: Option<String> = self.exec(ctx, &mut cmd).await?;
        let resp = match val {
            Some(s) => GetResponse {
                exists: true,
                value: s,
            },
            None => GetResponse {
                exists: false,
                ..Default::default()
            },
        };
        Ok(resp)
    }

    /// Sets the value of a key.
    /// expires is an optional number of seconds before the value should be automatically deleted,
    /// or 0 for no expiration.
    #[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, key = %arg.key))]
    **async fn set**(&self, ctx: &Context, arg: &SetRequest) -> RpcResult<()> {
        let mut cmd = match arg.expires {
            0 => redis::Cmd::set(&arg.key, &arg.value),
            _ => redis::Cmd::set_ex(&arg.key, &arg.value, arg.expires as usize),
        };
        let _value: Option<String> = self.exec(ctx, &mut cmd).await?;
        Ok(())
    }

2.3 生成 provider 产物 #

简单的使用 make 指令即可完成部署产物,该产物可以推送至 OCI 仓库以方便使用。

2.4 推送到仓库 #

可以通过 make push 指令将打包后的代码推送到镜像仓库。你需要修改 Makefile 中的 REG_URL 配置为自己需要推送的镜像仓库。如果你是测试的仓库,通过 http 协议访问,那么就可以正常推送了。如果你是正式的仓库,需要通过https访问和用户鉴权,那么你还要修改 Makefile 中的PUSH_REG_CMD 选项为wash reg push $(REG_URL) ,并且通过 WASH_REG_USER 和 WASH_REG_PASSWORD 两个环境变量分别定义用户名和密码。

3、开发 actor #

actor 是 msp serverless 技术生态中最小的部署单元,它同时也是 wasm 模块。它用来处理 msp serverless host 传递给他的消息,并且可以调用 provider 提供的函数。

3.1 新建 actor #

使用 wash 指令可以很方便的生成便于快速开发的模板代码:

 wash new actor kvcounter

3.2 开发 actor #

声明契约依赖

kvcounter 依赖了两个 interface,用来提供两个能力:httpserver能力和 kv 存储能力。

我们需要声明该依赖,这样打包完成 kvcounter 后,可以 inspect 出对应的信息。

Untitled

所以我们需要修改 makefile 的 CLAIMS 信息:

# examples/actor/kvcounter

PROJECT  = kvcounter
VERSION  = $(shell cargo metadata --no-deps --format-version 1 | jq -r '.packages[] .version' | head -1)
REVISION = 0
# list of all contract claims for actor signing (space-separated)
**CLAIMS   = wasmcloud:httpserver wasmcloud:keyvalue**

使用 interface

根据不同语言,我们可以引入不同的 interface package。以 rust 为例,我们可在 cargo.toml 中加入 kvcounter 所依赖的两个包:

[package]
name = "kvcounter"

[dependencies]
**wasmcloud-interface-keyvalue = "0.7.0"**
**wasmcloud-interface-httpserver = "0.6.0"**

在 actor 中调用:

#[async_trait]
impl HttpServer for KvCounterActor {
    async fn handle_request(&self, ctx: &Context, req: &HttpRequest) -> RpcResult<HttpResponse> {
        // increment the value in kv and send response in json
        let (body, status_code) = match **increment_counter**(ctx, key, amount).await {
            Ok(v) => (json!({ "counter": v }).to_string(), 200),
            // if we caught an error, return it to client
            Err(e) => (json!({ "error": e.to_string() }).to_string(), 500),
        };
        let resp = HttpResponse {
            body: body.as_bytes().to_vec(),
            status_code,
            ..Default::default()
        };
        Ok(resp)
    }
}

/// increment the counter by the amount, returning the new value
async fn increment_counter(ctx: &Context, key: String, value: i32) -> RpcResult<i32> {
    let new_val = KeyValueSender::new()
        **.increment(ctx, &IncrementRequest { key, value })**
        .await?;
    Ok(new_val)
}

3.4 打包 #

我们可以使用 make 指令,将代码打包为 .wasm 文件,进而推送到 OCI 仓库。

3.5 推仓 #

可以通过 make push 指令将打包后的代码推送到镜像仓库。你需要修改Makefile中的 REG_URL 配置为自己需要推送的镜像仓库。如果你是测试的仓库,通过http协议访问,那么就可以正常推送了。如果你是正式的仓库,需要通过https访问和用户鉴权,那么你还要修改Makefile中的 PUSH_REG_CMD 选项为 wash reg push $(REG_URL) ,并且通过WASH_REG_USER和WASH_REG_PASSWORD两个环境变量分别定义用户名和密码。

当然你可以通过wash命令而不是make命令直接推送:wash reg push yourharbor/name:version build/wasmfile

4、部署 #

我们可以使用 wasm shell(即 wash) 或者 web dashboard 进行部署。主要进行如下操作:

  1. start actors。将 actor 的 wasm 文件上传到 serverless runtime,或者提供 registry 地址供其下载。
  2. start providers。将 provider 的包上传到 serverless runtime,或提供 registry 地址供其下载。
  3. 建立 link 关系。

在平台的操作截图效果如下:

Untitled

这里需要注意的是,kvcounter 的 actor 需要分别与 httpserver 以及 kvredis 的 provider 进行 link,也就是有两条 link 记录。

5、访问 #

至此,已能正常通过 http 进行访问。

  ~ curl localhost:8080
{"counter":17}%
  ~ curl localhost:8080
{"counter":18}%

总结 #

本文主要是梳理了 msp serverless 应用开发的概况,包括其主要涉及的概念、工具链的使用、主要的开发流程。在使用过程中,如您遇到问题,可联系 msp 平台团队获取更多支持。