组内一个服务中有个叫算子的模块,所谓算子可以理解为UDF(User Defined Function),这个模块的核心思想是:在做业务需求时,把业务拆解为几块通用的业务代码(UDF),不同的代码块承担不同的业务功能。这些代码块提供出不同的配置项(或者叫“函数签名”),用户传入对应的参数调用这块代码。
这样做的好处是:后续接业务需求时只需要通过编排算子配置就可以复用通用算子。业务不复杂时,单个算子即可支持业务;业务复杂时,通过多个算子组合为pipeline支持业务。
编个🌰,现在有个业务需求:用户下单后需要统计用户当天完单量,并给下游发送用户的单量消息,下游营销系统根据用户的完单量给用户推送不同的优惠策略。
对整个业务流程做一下抽象:
这里编排schema和转发MQ都是不涉及存储的纯计算型业务需求。现在把这两个功能抽象成算子:
第一个算子为"编排schema",功能是把各种参数编排成一个string格式的字符串并返回。其配置schema可能长这样:
{
"calc_param": ["user_id", "finish_cnt", "time_stamp"],
"calc_options": {
"template": "{\"user_id\":\"{{user_id}}\",\"finish_cnt\":\"{{finish_cnt}}\",\"time_stamp\":\"{{time_stamp}}\"}"
}
}
第二个算子为"发送MQ",功能是把输入的string格式的字符串作为生产者发送到一个MQ中。其配置schema可能长这样:
{
"calc_param": ["last_calc_string_res"],
"calc_options": {
"topic_name": "an_awesome_topic"
}
}
把这两个算子组成一个json list,作为一个业务配置,服务读取配置就实现业务需求了,整个过程中不用写一行代码。
理想很丰满,实现很骨感。在实际开发迭代中,由于团队扩张、人员流动、代码注释缺失的问题,算子模块出现了三个问题:
- 有人不知道系统中有哪些通用算子
- 通用算子使用成本较高(测试文件(如果有) or 看算子源码)
- 通用算子维护成本较高(写完代码之后需要写wiki,更新完代码之后需要改wiki)
这两个问题导致的结果是:有的需求可以通过一些通用算子的配置组合支持业务,但是在不熟悉的情况下有的人会选择写一坨"定制算子(全是具体的业务逻辑)"去支持需求。
这么做倒是无可厚非,相比于纯写配置一行代码都不开发而言,一天写300行业务代码支持一个业务可能更会带来成就感,且上线之后也不出Bug就完事了呗,条条大路通罗马嘛;)
但这样做有点不符合组内系统的核心思想,还是应该去建设更加通用的算子,当通用算子积累到一定程度,并以一种合理的方式被管理起来,那最终的业务收益就可能变成这样:RD在平台上通过拖拉拽组合通用算子就可以支持业务,在系统建设足够完善的情况下,一人一天支持100个业务需求都不在话下;)
所以现在就有一个不太痛的痛点:需要把算子模块管理起来。如果在基于一开始的设想:
算子 == UDF
那完全可以学习编程语言管理UDF的方式对算子模块进行管理,具体的解决方案为:
痛点 | 方案 |
---|---|
新同事不知道系统中有哪些通用算子 | 对通用算子打上不同的分类标签:比如上面的 编排schema 算子可以打上"字符串操作" tag;发送MQ 算子 可以打上 "MQ操作"、"外部rpc" tag |
通用算子使用成本过高 | 搞一个算子平台,提供REPL的能力供用户使用算子 |
通用算子维护成本过高 | 问题发生的本质原因是:代码与wiki编写过程是剥离的,那就把这两个过程放在一起,把wiki放在代码里,并写一些元编程的代码生成这份wiki。 |
实际上这个算子模块管理系统是必须的:在系统刚开始迭代时,系统里可能只有几个通用算子,这时你的使用成本很低,这就好比你随时记得你喜欢的Go语言里有json.Unmarshal
、json.Marshal
、http.ListenAndServe
、strings.Split
这几个系统函数。
但是随着业务的发展,你的系统也会迭代,最终你的代码里可能有几十个、上百个UDF,如果不把算子按照类型管理起来,你就需要随时记忆这么多的UDF,这就好比 你可能并不记得你喜欢的Go语言的strings包里还有一个叫做EqualFold
的系统函数,即使你有一个模糊的印象,那你使用时候也得去看看wiki里是怎么写的。
对于如何把代码编写和wiki维护这两个割裂的步骤放在一起,在下有一些不成熟的想法。在这里写一种思路:
对于每一种段子,都抽象出其配置schema和参数schema,所谓的schema在Golang中即结构体,我们在结构体中写多种tag记录各个属性的元信息,并通过反射把这些元信息同步到DB中,元信息落库后,就可以和前端同学合作建立酷炫的管理平台管理算子了。
Go的反射及其不好用,这里简单写一些反射代码,把上文那两个算子配置读取结构体元信息这块表示一下:
package main
import (
"fmt"
"reflect"
)
type MQOption struct {
TopicName string `json:"topic_name" comment:"消息队列名称"`
}
func getStructTag(v interface{}, tag string) {
structVal := reflect.ValueOf(v).Elem()
structType := structVal.Type()
fmt.Println(structType, structVal)
for i := 0; i < structVal.NumField(); i++ {
fieldType := structType.Field(i)
fmt.Println("fieldType", fieldType)
field := structVal.FieldByName(fieldType.Name)
fmt.Println("field", field)
comment := fieldType.Tag.Get(tag)
fmt.Println("tag", comment)
}
}
func main() {
mo := new(MQOption)
getStructTag(mo, "comment")
}
// ------------ output ------------
// main.MQOption {}
// fieldType {TopicName string json:"topic_name" comment:"消息队列名称" 0 [0] false}
// field
// tag 消息队列名称
// ------------ output ------------
上面这块代码只是玩具,不建议用于生产环境,如果对这块有兴趣,可以去github上找开源项目。
欢迎关注我的公众号:薯条的自我修养