本文将以技术调研模式编写,非技术同学可跳过。
在历史架构的迭代中,服务的入口级模块从雨后春笋到方兴未艾、以至于现在的如火如荼,最终成为服务定位中的,基础服务之一。
其核心功能一般是对流量进行清洗、漏斗、染色、追踪…等公共、通用性功能。
现行的入口模块存在以下几个问题,急需进行改造、升级。
针对现行模块中暴露出的核心问题,从问题出发,推敲、构建预期框架模式特性。
PHP 服务逐步迁移至 Goland,技术栈进行转换、升级。
对于入口服务调度模块,随着下游业务的扩展,业务方的自定义需求会越来越多,越来越频繁。
为了能够让业务方自定义开发各自的业务逻辑,需要提供一种开发模式或者技术,能够由其他业务开发人员进行扩展,而不需重新编译整个模块的代码。(类似 Nginx 的模式,这也是中小企业,入口服务直接使用 代理 或者 Nginx 的原因)
考虑到插件模式可以帮助我们扩展原有程序的功能,同时它与原有工程是解耦的,可以独立开发,故小结如下:
服务在执行过程中动态加载部分应用程序的能力(可能基于用户定义的配置)在某些设计中可能是一个有用的构建块。特别是,因为应用程序和动态加载的函数可以直接共享数据结构,所以插件可以实现独立部分的非常高性能的集成。
Go 附带一个内置于标准库中的插件包。这个包让我们编写的 Go 程序被编译成共享库而不是可执行二进制文件;此外,它还提供了用于加载共享库和从中获取符号的简单函数。
import (
"plugin"
)
然而,插件机制有许多明显的缺点,在设计过程中应该仔细考虑。例如:
实现分为三部分:主程序、组件程序、公共库。公共库可与主程序所属库共同,组件程序进行包引用即可。
package main
import (
"fmt"
"os"
"plugin"
util "XXXX"
)
func main() {
req := util.Req{Str: "A"}
// 0. qt flow dispatch
var mod string
if mod = Dispatch(req.Str); len(mod) < 1 {
fmt.Println("don't deal str")
os.Exit(1)
}
// 1. open the so file to load the symbols
plug, err := plugin.Open(mod)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 2. look up a symbol (an exported function or variable)
symExecute, err := plug.Lookup("ModExecute")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 3. Assert that loaded symbol is of a desired type
modExecute, ok := symExecute.(util.ModExecute)
if !ok {
fmt.Println("unexpected type from module symbol")
os.Exit(1)
}
// 4. use the module
modExecute.Init(&req)
modExecute.Check()
modExecute.Execute()
res := modExecute.Out()
fmt.Println("main run success res msg:", res.Msg)
}
func Dispatch(str string) string {
var mod string
switch str {
case "A":
mod = "./X/A.so"
case "B":
mod = "./X/B.so"
default:
}
return mod
}
运行:go run main.go
package main
import (
"fmt"
util "XXXX"
)
type A struct {
}
func (s A) Init(req *util.Req) {
fmt.Println("a module:Init:", req.Str)
}
func (s A) Check() {
fmt.Println("a module:Check")
}
func (s A) Execute() {
fmt.Println("a module:Execute")
}
func (s A) Out() util.Res {
fmt.Println("a module:Out")
return util.Res{Msg: "ok"}
}
var ModExecute A
运行: go build -buildmode=plugin -o X/A.so main-plugin.go
package util
type ModExecute interface {
Init(*Req) // 前置初始化操作
Check() // 流量签名&鉴权
Execute() // 执行操作
Out() Res // 输出
}
type Req struct {
Str string
}
type Res struct {
Msg string
}
go test -bench BenchmarkMainDeal -benchtime=5s -benchmem
87883 69740 ns/op 4998 B/op 6 allocs/op
PASS
ok XXXXX 6.822s
总之,这些限制意味着:在实践中,应用程序及其插件必须全部由一个人或系统的一个组件一起构建。在那种情况下,对于那个人或组件来说,生成空白导入所需插件集的 Go 源文件然后以通常的方式编译静态可执行文件可能更简单。
由于这些原因,许多用户认为传统的进程间通信 (IPC) 机制(例如套接字、管道、远程过程调用 (RPC)、共享内存映射或文件系统操作)可能更适合,尽管性能开销很大。
相关 Demo 实现、Benchwork 基准性能、小结 见后续文章。
互联网初期的时候,是没有入口或前置服务的概念的。
请求服务都是从端上发起到后段处理的链路。随着业务种类及规模的膨胀,出现了以下的问题:
基于这些棘手的问题,急需在服务入口提供一公共模块,进行一些前置操作,在节省资源、业务维护成本的同时,提升服务性能、质量。