Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature: load multi wasm instance #176

Closed
zhenjunMa opened this issue Aug 12, 2021 · 15 comments · Fixed by #182
Closed

New feature: load multi wasm instance #176

zhenjunMa opened this issue Aug 12, 2021 · 15 comments · Fixed by #182
Labels
help wanted Extra attention is needed kind/enhancement New feature or request

Comments

@zhenjunMa
Copy link
Contributor

1. What would you like to be added

Now Layotto only supports loading a single *.wasm file. We need to make it support adding multiple different *.wasm files at the same time. This feature should also consider when Layotto receives the request, which wasm instance should be forwarded to.

2. Why is this needed

This is a problem that Layotto must solve as a solution to FaaS. The expected FaaS deployment model in the future is shown in the following figure:

image

3. Design draft

A. wasm config

Need to support the configuration to load multiple wasm files.

config path: config/wasm/config.json

"stream_filters": [
  {
    "type": "Layotto",
    "config": {
      "name": "wasm_demo",
      "instance_num": 1,
      "vm_config": {
        "engine": "wasmer",
        "path": "demo/wasm/code/golang/wasm.wasm"
      }
    }
  }
]

The config field in the above configuration needs to be adjusted to an array form, as follows:

"config":[
  {config1},
  {config2}
]

B、wasm code template

In order for Layotto to better track all wasm instances loaded by itself, we need to formulate some specifications for these wasm instances. The first step is to make these wasm instances expose a function named ID, as follows:

func ID() string {
    return current instance ID, the same ID will be grouped together
}

This function will be called after the instance loaded by Layotto and used for subsequent routing.

C、wasm route

In a multi-wasm instance scenario, Layotto may load multiple wasm instances. These instances may or may not be a group. Therefore, Layotto needs to store a map-type data structure internally for routing. The details are as follows:

ID1: {
     wasm instance1,
     wasm instance2
},
ID2: {
     wasm instance1
}

After Layotto receives the request, taking Http as an example, it needs to take out the target instance ID from the header, and then select a wasm instance for processing through the LB strategy. The LB strategy can first support only random algorithms.

D、demo

After the this feature are implemented, the current example needs to be modified to demonstrate the above functions. In the example, at least three wasm instances need to be started and divided into two groups. We can initiate the call with the following command:

curl -H 'id:id' -H 'name:Layotto' localhost:2045

The results returned by multiple calls are expected to be as follows:

id1-instance1
id1-instance2
id1-instance1
id1-instance2
id2-instance1
id2-instance1

E、wasm hot reloading

Thanks to @zu1k for adding the feature of hot-loading wasm instances to Layotto this week. For details, see #165. Therefore, how to hot-load multiple wasm instances should be considered in this feature.

Note: If you think the hot reloading is too complicated, you can split this feature into multiple wasm instances to support and support hot-loading for pr submissions.

中文

一、需要实现什么功能

现在Layotto只支持加载单个*.wasm文件,我们需要让它能支持同时加个多个不同的*.wasm文件,该功能还会涉及到当Layotto收到请求以后,该转发给哪个wasm实例。

二、做这个功能的价值

这是Layotto作为FaaS的一种解决方案所必须解决的问题,未来我们预期的FaaS部署模式如下图所示:

image

Layotto会按需加载多个wasm实例,对于这些wasm实例而言,它们可以属于同一个组,也可以是完全不相关的,类似于当前微服务架构下服务跟IP的关系。

三、方案概述

A、wasm配置

需要支持配置加载多个wasm文件。
配置文件所在路径: config/wasm/config.json

"stream_filters": [
  {
    "type": "Layotto",
    "config": {
      "name": "wasm_demo",
      "instance_num": 1,
      "vm_config": {
        "engine": "wasmer",
        "path": "demo/wasm/code/golang/wasm.wasm"
      }
    }
  }
]

上述配置中的config字段需要调整为数组形式,如下:

"config":[
  {config1},
  {config2}
]

B、wasm模板

为了让Layotto更好的跟踪自己加载的所有wasm实例,我们需要为这些wasm实例制定一些的规范,第一步可以让这些wasm实例必须对外暴露一个名为ID的函数,如下:

func ID() string {
    return 当前实例对应的ID,相同的ID会归为一组
}

该函数会在Layotto加载完以后进行调用并用于后续的路由。

C、wasm路由

在多wasm实例场景中,Layotto可能会加载多个wasm实例,这些实例可能是一组,也可能不是,因此Layotto在内部需要保存一个map类型的数据结构用于路由,内如大致如下:

ID1: {
     wasm instance1,
     wasm instance2
},
ID2: {
     wasm instance1
}

Layotto收到请求以后,以Http为例,需要从header中取出目标实例ID,然后通过LB策略选择一个wasm instance进行处理,LB策略可以先只支持随机算法。

D、示例演示

上述功能实现以后,需要修改现在的示例来演示上述功能,在示例中,至少需要启动三个wasm实例,并分为两组,我们可以用如下命令发起调用:

curl -H 'id:id' -H 'name:Layotto' localhost:2045

多次调用返回结果预期如下:

id1-instance1
id1-instance2
id1-instance1
id1-instance2
id2-instance1
id2-instance1

E、wasm热加载

感谢 @zu1k 本周为Layotto增加了热加载wasm实例的feature,具体详情见:#165 因此在该需求中需要考虑如何热加载多个wasm实例。

注:如果觉得这块过于复杂,可以拆分成多wasm实例支持跟支持热加载两个pr提交。

@zhenjunMa zhenjunMa added kind/enhancement New feature or request help wanted Extra attention is needed labels Aug 12, 2021
@zhenjunMa zhenjunMa changed the title New feature: multi wasm instance New feature: load multi wasm instance Aug 12, 2021
@zu1k
Copy link
Member

zu1k commented Aug 13, 2021

有个疑问,对于如下配置文件,其中前两个wasm文件返回相同ID,被划分为一组。

此ID对应的组中共有3个wasm实例,其中1个实例属于第一个wasm文件,2个实例属于第二个wasm文件,在进行wasm路由的时候是平等对待吗?

或者说我的疑问是同一个ID对应多个wasm文件,如果不同wasm文件实现的功能不同,该如何进行路由

"stream_filters": [
 {
    "type": "Layotto",
    "config": [
      {
        "name": "id_1",
        "instance_num": 1,
        "vm_config": {
          "engine": "wasmer",
          "path": "demo/wasm/code/golang/wasm_1_1.wasm"
        }
      },
      {
        "name": "id_1",
        "instance_num": 2,
        "vm_config": {
          "engine": "wasmer",
          "path": "demo/wasm/code/golang/wasm_1_2.wasm"
        }
      },
      {
        "name": "id_2",
        "instance_num": 1,
        "vm_config": {
          "engine": "wasmer",
          "path": "demo/wasm/code/golang/wasm_2.wasm"
        }
      }
    ]
  }
]

@zhenjunMa
Copy link
Contributor Author

zhenjunMa commented Aug 13, 2021

@zu1k 这个问题非常好,涉及到实际生产使用,首先来明确一下场景:

  1. 如果两个wasm文件完全不相关,但却因为对应同一个ID而被归为一组,类似于现在微服务架构下,两个完全不同的业务方发布了一个同名服务,这种势必会造成服务调用混乱,需要一些其他手段来保证,这类问题可以先不考虑。

  2. 另一种场景或许更为普遍,比如用户发布了一个v1.0的wasm文件并且已经在运行了,之后用户做了一些更新发布了v2.0的wasm文件,接下来就涉及到如何把一个组中的wasm实例从v1.0全部升级到v2.0,在理想情况下,我们应该有一个灰度过程,比如让layotto先针对v2.0创建一个实例,然后把1%的流量导入到这个实例进行验证,验证通过之后再进行逐步全量升级。

layotto + wasm在FaaS方向上的探索会是一个长时间的投入,因此我建议我们可以先从简单的功能开始,然后再逐步迭代达到生产可用状态,这一版的功能可以先不考虑你说的这种复杂的场景,我们假设同一组中的wasm文件都是相同的(当然,这种场景我们以后势必要解决)。

@zu1k
Copy link
Member

zu1k commented Aug 13, 2021

看起来layotto需要使用mosn提供的RegisterStream方法来注册一个StreamFilterFactoryCreator

api.RegisterStream(LayottoWasm, createProxyWasmFilterFactory)

StreamFilterFactoryCreator接受的config类型为map[string]interface{}

func createProxyWasmFilterFactory(conf map[string]interface{}) (api.StreamFilterChainFactory, error) {}

// StreamFilterFactoryCreator creates a StreamFilterChainFactory according to config
type StreamFilterFactoryCreator func(config map[string]interface{}) (StreamFilterChainFactory, error)

而对于我们的配置,我们需要类型为[]map[string]interface{}

"stream_filters": [
  {
    "type": "Layotto",
    "config": [
        {},
        {},
    ]
  }
]

就目前而言应该有几个解决方案:

  1. 对mosn下手,修改其stream filter的结构(感觉太break了,不太现实)
  2. layotto注册多个steam filter,不清楚这样搞的话路由和调度该怎么做
  3. 我们的config格式修改为如下,真实使用的时候忽略config id即可:
"stream_filters": [
  {
    "type": "Layotto",
    "config": [
        "config_id_1": {},
        "config_id_2": {},
    ]
  }
]

@zhenjunMa
Copy link
Contributor Author

@zu1k 我刚才看了下代码,确实不能直接把config改成数组形式,这里可以按照你提出的第三种方案来做。

@zu1k
Copy link
Member

zu1k commented Aug 13, 2021

读了一下代码,发现目前对于wasm多instance的调度是交由mosn来管理的,LB策略是轮询。

func (w *wasmPluginImpl) GetInstance() types.WasmInstance {
	w.lock.RLock()
	defer w.lock.RUnlock()

	for i := 0; i < len(w.instances); i++ {
		idx := int(atomic.LoadInt32(&w.instancesIndex)) % len(w.instances)
		atomic.AddInt32(&w.instancesIndex, 1)

		instance := w.instances[idx]
		if !instance.Acquire() {
			continue
		}

		atomic.AddInt32(&w.occupy, 1)
		return instance
	}
	log.DefaultLogger.Errorf("[wasm][plugin] GetInstance fail to get available instance, instance num: %v", len(w.instances))

	return nil
}

目前layotto能做的应该是多个wasm文件对应相同ID的情况,在同ID对应的Group中对选择的wasm文件进行调度。

至于同一个wasm文件对应的多个instances还是交给mosn来做吧

@zu1k
Copy link
Member

zu1k commented Aug 13, 2021

Any ideas about exporting func ID() string ?

@zhenjunMa
Copy link
Contributor Author

@zu1k

关于ID这个问题,我看了你的pr,是导出了一个变量,我本来想的是导出一个函数,其实跟变量类似,就是在函数上面增加// export ID注释就行了,其他你在pr里面提出的其他疑惑,我会尽快review并回复,感谢你对社区的贡献。

@zu1k
Copy link
Member

zu1k commented Aug 16, 2021

现在导出的这个变量用不了,实际编译的时候并没有导出貌似。

我也想直接导出函数,一开始尝试的是导出函数,遇到了困难。

因为wasi接口的数据类型不支持string,看样子只能导出int32类型的指针,并且golang的string是封装过的,一般需要通过C.CString转为* C.char,也就是c的string的指针。实际操作中发现tinygo编译的时候说导入C库失败?具体原因不清楚,貌似是tinygo对C支持不好,tinygo-org/tinygo#854

并且如果返回*char指针的话需要遍历到0x00结束,会不会有安全问题?

@zhenjunMa
Copy link
Contributor Author

不好意思,这块是我的问题,之前忽略了不支持string类型这个问题,确实跟你说的一样,我们需要通过指针及长度来传递string,具体的操作其实可以参考之前实现的proxy_call_foreign_function如何传递目标函数名,示例代码如下:

源码路径github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm

//在wasm侧,可以通过这种方式获取string的起始指针,长度用len()即可
func stringBytePtr(msg string) *byte {
	if len(msg) == 0 {
		return nil
	}
	bt := *(*[]byte)(unsafe.Pointer(&msg))
	return &bt[0]
}

在layotto侧可以通过如下方式把指针转换成string

源码路径:mosn.io/proxy-wasm-go-host/proxywasm

func ProxyCallForeignFunction(instance common.WasmInstance, funcNamePtr int32, funcNameSize int32,
	paramPtr int32, paramSize int32, returnData int32, returnSize int32) int32 {
         //这里
	funcName, err := instance.GetMemory(uint64(funcNamePtr), uint64(funcNameSize))
	
}

@zu1k
Copy link
Member

zu1k commented Aug 16, 2021

在layotto侧可以通过如下方式把指针转换成string

源码路径:mosn.io/proxy-wasm-go-host/proxywasm

func ProxyCallForeignFunction(instance common.WasmInstance, funcNamePtr int32, funcNameSize int32,
	paramPtr int32, paramSize int32, returnData int32, returnSize int32) int32 {
         //这里
	funcName, err := instance.GetMemory(uint64(funcNamePtr), uint64(funcNameSize))
	
}

wasm的ID方法返回只能一个参数,没办法同时传递字符串指针和长度两个变量,我不太理解你这里表达的意思

@zu1k
Copy link
Member

zu1k commented Aug 16, 2021

// TODO: get the plugin content ID corresponding to the caller wasm plugin
func (f *Filter) GetRootContextID() int32 {
	return f.factory.RootContextID
}

// TODO: get the plugin vm config corresponding to the caller wasm plugin
func (f *Filter) GetVmConfig() common.IoBuffer {
	return f.factory.GetVmConfig()
}

// TODO: get the plugin config corresponding to the caller wasm plugin
func (f *Filter) GetPluginConfig() common.IoBuffer {
	return f.factory.GetPluginConfig()
}

现有的代码中有这类方法,因为之前是单一wasm plugin,所以没有筛选条件。

现在一个filter包含多个wasm plugin,看样子是导出给wasm调用的方法,如何判断caller是哪个wasm?

还有一个疑问,RootContextID和ContextID是什么含义,我没找到相关注释说明

@zu1k
Copy link
Member

zu1k commented Aug 16, 2021

@zhenjunMa

@zhenjunMa
Copy link
Contributor Author

之前写的代码有段时间没动了,有点生疏,下午又重新看了下,简单讨论下:

一、ID传递问题

这块确实比预期要繁琐一点,大致流程可能要改成如下逻辑:

  1. layotto调用wasm导出的ID函数
  2. wasm中ID函数的实现逻辑是把ID写会给layotto,这里要借助rawhostcall.ProxySetBufferBytes
  3. LayottoHandler中应该要实现GetFuncCallData函数定义,返回一个自定有byte[]数组,这样上述ID就会写到这个数组中了

二、vmConfig跟pluginConfig

我看你说的Filter里面的这两个函数应该是没有用到,真正用到的是在FilterConfigFactory.OnPluginStart,目的是把每个实例对应的配置回调给它们,在我们的场景里可以先认为同一个ID对应的所有实例配置都是相同的。我看目前的实现这两个配置是从factory里面取出,改成多实例以后这里就只能改成从plugin里面取出来再按照factory里面的方式进行转换了。

三、RootContextID vs ContextID

ContextID是请求粒度的,每次layotto回调wasm的接口时都需要传入对应的ContextID,这样才能关联到正确的上下文中,在我们自己开发的wasm.go中看不到这个信息是因为被SDK屏蔽了,可以参考下abi_l7.go里面的内容。

RootContextID暂时应该没有用到,是固定值,不过如果是多个不同wasm的话应该是对应不同的RootContextID,我看倒是没有影响。

@zu1k
Copy link
Member

zu1k commented Aug 17, 2021

@zhenjunMa rawhostcall.ProxySetBufferBytes 返回not found,不知道什么原因,没找到相关文档说明这个方法。
麻烦看一下最新的提交

@zhenjunMa
Copy link
Contributor Author

@zu1k
仔细读了你的代码,这个任务远比我想象的要复杂,不过我看你提交的代码应该很快就能完成这个任务了,只是有部分细节理解的有偏差,我们一起讨论一下。

这里主要对整体逻辑进行说明,更多的评论我会留在你的PR里面 🍺。

FilterConfigFactory在启动时就会执行,因此wasm实例初始化逻辑会放在这里。Filter则是Layotto收到请求时实时为每个请求创建并用于处理请求,其中包括后续处理这个请求时跟wasm实例的各种交互逻辑。

列几个当前实现存在问题的地方:

  1. 关于路由表Router的构建,不应该放在Filter里面,而应该放在FilterConfigFactory中,因为整个路由信息在初始化阶段就可以确定,不需要针对每个请求重复创建,具体逻辑可以考虑放在OnPluginStart

  2. 关于路由表Router的内容,应该是id:plugin:instance=1:M:N的关系,转发请求的比例最终应该按照实例数来,也就是说如果一个id对应两个plugin(两个不同的wasm文件),它们又分别有9个实例跟1个实例,那么从plugin粒度来说它们处理的请求比例应该是9:1,现在的逻辑是1:1

  3. 之前单plugin的时候,可以在NewFilter中进行初始化,现在改成多plugin以后需要把这部分逻辑迁移到OnReceive中,因为只有在这里才能确定本次请求要使用哪个实例,并对它进行初始化。

说一个比较绕的地方,也正是你疑惑的rawhostcall.ProxySetBufferBytes为什么返回not found

AbiV2Implv1.ABIContextproxywasm.ABIContext,三者是"继承"关系,所以它们三个是一体的,也就是说
abi.SetABIImports(ih)abi.SetImports(ih)本质上是一回事,而在你的实现里,针对每个请求,先后调用了这两个方法,让importHandler发生了替换,导致增加的GetFuncCallData逻辑并没有生效。

这个任务需要理清楚factory、filter、wasm三者的职责,等你完成这个任务以后就可以完全cover整块wasm逻辑了。ヾ(◍°∇°◍)ノ゙

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed kind/enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants