千万级入口服务[Gateway]框架设计(二)

news/2024/11/28 10:33:23/

本文将以技术调研模式编写,非技术同学可跳过。

文章目录

    • 背景
      • 实现二:开源 go-plugin
      • Demo 实现
      • Benchwork 基准性能
      • 小结
    • 附录

背景

基于组件(插件)模式设计构建的入口服务,在使用 Go 原生包 plugin 实现的时候,会存在功能缺陷问题,不足以支撑预期能力。

注:详细见上文 《千万级入口服务[Gateway]框架设计(一)》

千万级入口服务[Gateway]框架设计(一)
本文将继续介绍另一种关于 go-plugin 的开源实现。

实现二:开源 go-plugin

针对前文提到的几个问题,go-plugin 提出以 rpc 协议通信的方式进行组件搭建。
在这里插入图片描述

将每个组件进行服务封装,主程序、组件之间以通信方式进行交互,这样可以完全规避原生的不足。

  1. 组件是 Go 接口的实现:这让组件的编写、使用非常自然。对于组件的作者来说,他只需要实现一个Go 接口即可;对于组件的用户来说,他只需要调用一个Go 接口即可。
  2. 跨语言支持:组件可以基于任何主流语言编写,同样可以被任何主流语言消费
  3. 支持复杂的参数、返回值:go-plugin 可以处理接口、io.Reader/Writer 等复杂类型
  4. 双向通信:为了支持复杂参数,宿主进程能够将接口实现发送给组件,组件也能够回调到宿主进程
  5. 内置日志系统:任何使用 log 标准库的的组件,都会将日志信息传回宿主机进程。宿主进程会在这些日志前面加上组件二进制文件的路径,并且打印日志
  6. 协议版本化:支持一个简单的协议版本化,增加版本号后可以基于老版本协议的组件无效化。
  7. 标准输出/错误同步:组件以子进程的方式运行,这些子进程可以自由的使用标准输出/错误,并且打印的内容会被自动同步到宿主进程,宿主进程可以为同步的日志指定一个 io.Writer
  8. TTY Preservation:组件子进程可以链接到宿主进程的 stdin 文件描述符,以便要求 TTY 的软件能正常工作
  9. 宿主进程升级:宿主进程升级的时候,组件子进程可以继续允许,并在升级后自动关联到新的宿主进程
  10. 加密通信:gRPC 信道可以加密
  11. 完整性校验:支持对组件的二进制文件进行 Checksum
  12. 稳定性保障:组件崩溃了,不会导致宿主进程崩溃
  13. 容易安装:只需要将组件放到某个宿主进程能够访问的目录

Demo 实现

实现分为三部分:主程序、组件程序、公共库。公共库可与主程序所属库共同,组件程序进行包引用即可。

  • 主程序
package mainimport ("fmt""log""os""os/exec""time""github.com/hashicorp/go-hclog""github.com/hashicorp/go-plugin"util "XXXXX"
)var pluginMap = map[string]plugin.Plugin{// 插件名称到插件对象的映射关系"s":   &util.GreeterPlugin{},"bar": &util.GreeterPlugin{},
}func main() {// 创建hclog.Logger类型的日志对象logger := hclog.New(&hclog.LoggerOptions{Name:   "plugin",Output: os.Stdout,Level:  hclog.Debug,})req := util.Req{}for i := 0; i < 10; i++ {fmt.Println("for i:", i) // 验证热加载功能req.Str = RandStr(i)var mod stringif mod = Dispatch(req.Str); len(mod) < 1 {fmt.Println("don't deal str")os.Exit(1)}// 两种方式选其一// 以exec.Command方式启动插件进程,并创建宿主机进程和插件进程的连接// 或者使用Reattach连接到现有进程client := plugin.NewClient(&plugin.ClientConfig{HandshakeConfig: util.HandshakeConfig,Plugins:         pluginMap,// 创建新进程,或使用Reattach连接到现有进程中Cmd:    exec.Command(mod),Logger: logger,})// 关闭client,释放相关资源,终止插件子程序的运行defer client.Kill()// 返回协议客户端,如rpc客户端或grpc客户端,用于后续通信rpcClient, err := client.Client()if err != nil {log.Fatal(err)}// 根据指定插件名称分配新实例raw, err := rpcClient.Dispense(req.Str)if err != nil {log.Fatal(err)}// 像调用普通函数一样调用接口函数就ok,很方便是不是?greeter := raw.(util.Greeter)fmt.Println(greeter.Greet())fmt.Println(greeter.Speak(req.Str))fmt.Println(greeter.Execute(req).Msg)time.Sleep(1 * time.Second)}
}func Dispatch(str string) string {var mod stringswitch str {case "A":mod = "./X/A"case "B":mod = "./X/B"default:}return mod
}func RandStr(i int) string {var str = "B"if i%2 == 0 {str = "A"}return str
}
  • 组件程序
package mainimport ("os""github.com/hashicorp/go-hclog""github.com/hashicorp/go-plugin"util "XXX"
)// Here is a real implementation of Greeter
type A struct {logger hclog.Logger
}func (g *A) Greet() string {g.logger.Debug("message from A.Greet")return "A Hello!"
}func (g *A) Speak(str string) string {return "Now A-" + str + " is speaking!"
}func (g *A) Execute(req util.Req) util.Res {return util.Res{Msg: "Now A-" + req.Str + " is executing!"}
}func main() {logger := hclog.New(&hclog.LoggerOptions{Level:      hclog.Trace,Output:     os.Stderr,JSONFormat: true,})greeter := &A{logger: logger,}// pluginMap is the map of plugins we can dispense.var pluginMap = map[string]plugin.Plugin{"A": &util.GreeterPlugin{Impl: greeter},}plugin.Serve(&plugin.ServeConfig{HandshakeConfig: util.HandshakeConfig,Plugins:         pluginMap,})
}

运行:go build -o ./X/A main-plugin-A.go

  • 公共库
package utilimport ("net/rpc""github.com/hashicorp/go-plugin"
)type Req struct {Str string
}type Res struct {Msg string
}// Greeter is the interface that we're exposing as a plugin.
type Greeter interface {Greet() stringSpeak(string) stringExecute(Req) Res
}// Here is the RPC server that GreeterRPC talks to, conforming to
// the requirements of net/rpc
type GreeterRPCServer struct {// This is the real implementationImpl Greeter
}func (s *GreeterRPCServer) Greet(args any, resp *string) error {*resp = s.Impl.Greet()return nil
}func (s *GreeterRPCServer) Speak(str string, resp *string) error {*resp = s.Impl.Speak(str)return nil
}func (s *GreeterRPCServer) Execute(req Req, resp *Res) error {*resp = s.Impl.Execute(req)return nil
}// Here is an implementation that talks over RPC
type GreeterRPC struct{ client *rpc.Client }func (g *GreeterRPC) Greet() string {var resp stringerr := g.client.Call("Plugin.Greet", new(any), &resp)if err != nil {// You usually want your interfaces to return errors. If they don't,// there isn't much other choice here.panic(err)}return resp
}func (g *GreeterRPC) Speak(str string) string {var resp stringerr := g.client.Call("Plugin.Speak", str, &resp)if err != nil {// You usually want your interfaces to return errors. If they don't,// there isn't much other choice here.panic(err)}return resp
}func (g *GreeterRPC) Execute(req Req) Res {var resp Reserr := g.client.Call("Plugin.Execute", req, &resp)if err != nil {// You usually want your interfaces to return errors. If they don't,// there isn't much other choice here.panic(err)}return resp
}type GreeterPlugin struct {// 内嵌业务接口// 插件进程会设置其为实现业务接口的对象// 宿主进程则置空Impl Greeter
}// 此方法由插件进程延迟的调用
func (p *GreeterPlugin) Server(*plugin.MuxBroker) (any, error) {return &GreeterRPCServer{Impl: p.Impl}, nil
}// 此方法由宿主进程调用
func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (any, error) {return &GreeterRPC{client: c}, nil
}var HandshakeConfig = plugin.HandshakeConfig{ProtocolVersion:  1,MagicCookieKey:   "BASIC_PLUGIN",MagicCookieValue: "hello",
}

Benchwork 基准性能

go test -bench BenchmarkMainDeal -benchtime=5s -benchmem
......timestamp=2023-06-08T15:07:35.095+0800963           6329922 ns/op          156723 B/op        844 allocs/op
PASS
ok      XXX  6.736s

小结

虽然开源组件打破了原生包的囧境,但其实质是本地的 RPC 调用,存在本地网络开销。对比前文我们原生包的 Benchwork 指标,性能相对不足。尤其是在调用过程中的序列化、反序列化,在频繁交互的场景下,是木桶璧的短板所在。

在这里插入图片描述

当然,针对 Go 来讲,可以使用 GRPC 协议,充分降低短板对整体的影响占比。在一些性能适中的场景下,是完全满足需求的。


附录

  • https://eli.thegreenplace.net/2023/rpc-based-plugins-in-go/

http://www.ppmy.cn/news/380388.html

相关文章

作为一个优秀的项目经理,你需要做什么?

经常看到这样的项目经理&#xff0c;一副整天忙得团团转的样子&#xff0c;整天忙得团团转&#xff0c;发出一大堆指令&#xff0c; 经常事无巨细都要亲自过问&#xff0c;他还会不断抱怨说&#xff1a; " 我很忙 " 或 " 我很累 " &#xff0c; " 我…

contenteditable修改边框样式

[contenteditable] {outline: 1px solid transparent;border: 1px solid #fff;width: 100%; }[contenteditable]:focus {border: 1px solid #00c0ef;border-radius: 3px; }

如何预防个人隐私信息被泄漏?

使用电脑打开一些网站&#xff0c;会弹出无数个广告&#xff0c;浏览器会把你的电脑变得非常卡&#xff0c;显示器全部被霸占了各种弹窗信息&#xff0c;相信经常上网的朋友肯定遇到过。甚至有人发现自己输入的网页打开直接跳转到另一个地址&#xff0c;在这里这个可能是钓鱼网…

集权设施管理-AD域安全策略(二)

活动目录&#xff08;AD&#xff09;凭借其独特管理优势&#xff0c;从众多企业管理服务中脱颖而出&#xff0c;成为内网管理中的佼佼者。采用活动目录来管理的内网&#xff0c;称为AD域。 了解AD域&#xff0c;有助于企业员工更好地与其它部门协作&#xff0c;同时提高安全意…

Excel如何在姓名与字母之间加空格

如下图&#xff0c;是某次考试学生成绩&#xff0c;但是老师在录入时直接将姓名和成绩录入到同一单元格中&#xff0c;现在想在姓名后面添加一个空格 选中成绩数据区域 点击下图选项&#xff08;Excel插件&#xff0c;百度即可了解安装方法&#xff09; 点击【更多】&#xff0…

怎么批量在文件名中加入空格?

怎么批量在文件名中加入空格&#xff1f;你知道怎么在文件名称中插入一个空格吗&#xff1f;相信很多小伙伴会回答太简单了&#xff0c;事实也正是如此&#xff0c;这是一项最基本的电脑操作&#xff0c;只要我们右击文件然后选择“重命名”&#xff0c;之后鼠标选定需要添加空…

如何把所有文件的名字空格删除?

不知道大家有没有发现&#xff0c;有时候在网上下载到的文件或者从某个软件系统下载下来的文件&#xff0c;文件名称中都包含空格&#xff0c;有的甚至有十几个空格&#xff0c;空格过多会让文件名称变得很长&#xff0c;让文件名就很看&#xff0c;这时候我们通常会把这些空格…

带空格字符串的输入

示例1 输入&#xff1a; 234 输出&#xff1a; 3 说明&#xff1a; 标题中共有 3 个字符&#xff0c;这 3 个字符都是数字字符。 示例2 输入&#xff1a; Ca 45 输出&#xff1a; 4 说明&#xff1a; 标题中共有 5 个字符&#xff0c;包括 1 个大写英文字母&#…