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

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

文章目录

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

背景

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

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

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

实现二:开源 go-plugin

针对前文提到的几个问题,go-plugin 提出以 rpc 协议通信的方式进行组件搭建。
千万级入口服务[Gateway]框架设计(二)_第1张图片

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

  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 main

import (
	"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 string
		if 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 string
	switch 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 main

import (
	"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 util

import (
	"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() string
	Speak(string) string
	Execute(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 implementation
	Impl 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 string
	err := 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 string
	err := 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 Res
	err := 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+0800
     963           6329922 ns/op          156723 B/op        844 allocs/op
PASS
ok      XXX  6.736s

小结

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

千万级入口服务[Gateway]框架设计(二)_第2张图片

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


附录

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

你可能感兴趣的:(服务端,gateway,分布式,架构)