本文将以技术调研模式编写,非技术同学可跳过。
文章目录
- 背景
- 问题[不涉及具体业务]
- 目标
- 技术选型
- 语言
- 框架模式
- 实现一:go 原生组件
- Demo 实现
- Benchwork 基准性能
- 小结
- 实现二:开源 go-plugin
- 附录
- 入口服务演变
背景
在历史架构的迭代中,服务的入口级模块从雨后春笋到方兴未艾、以至于现在的如火如荼,最终成为服务定位中的,基础服务之一。
其核心功能一般是对流量进行清洗、漏斗、染色、追踪…等公共、通用性功能。
问题[不涉及具体业务]
现行的入口模块存在以下几个问题,急需进行改造、升级。
- 框架过重
- 框架控制单次请求中下游服务的调用模型
- 框架显式支持串行调用,隐式支持并行调用
- 框架嵌套基础组件SDK,部分功能为黑盒
目标
针对现行模块中暴露出的核心问题,从问题出发,推敲、构建预期框架模式特性。
- 轻量
- 收敛、抽象模块功能,解锁业务与框架之间的绑定、耦合
- 提倡并行调用,串行辅助的调用策略
- 承接云原生可观测组件,深度、全面刨析服务状态,杜绝黑盒、盲区
- 支撑 兜底、降级…等必备手段
技术选型
语言
PHP 服务逐步迁移至 Goland,技术栈进行转换、升级。
框架模式
对于入口服务调度模块,随着下游业务的扩展,业务方的自定义需求会越来越多,越来越频繁。
为了能够让业务方自定义开发各自的业务逻辑,需要提供一种开发模式或者技术,能够由其他业务开发人员进行扩展,而不需重新编译整个模块的代码。(类似 Nginx 的模式,这也是中小企业,入口服务直接使用 代理 或者 Nginx 的原因)
考虑到插件模式可以帮助我们扩展原有程序的功能,同时它与原有工程是解耦的,可以独立开发,故小结如下:
- 插件模式的核心功能/优势
- 为系统提供灵活的扩展能力
- 主服务 与 插件 的代码解耦,独立开发
- 主服务 只关注插件的接口,不关注实现细
- 不侵入系统现有功能
- 主服务 动态引入插件,因而可以自由定制所需的能力,避免部署包的体积过大
- 插件可以独立升级
- 为系统提供灵活的扩展能力
实现一:go 原生组件
服务在执行过程中动态加载部分应用程序的能力(可能基于用户定义的配置)在某些设计中可能是一个有用的构建块。特别是,因为应用程序和动态加载的函数可以直接共享数据结构,所以插件可以实现独立部分的非常高性能的集成。
Go 附带一个内置于标准库中的插件包。这个包让我们编写的 Go 程序被编译成共享库而不是可执行二进制文件;此外,它还提供了用于加载共享库和从中获取符号的简单函数。
import (
"plugin"
)
然而,插件机制有许多明显的缺点,在设计过程中应该仔细考虑。例如:
- 插件目前仅在 Linux、FreeBSD 和 macOS 上受支持,因此它们不适合用于可移植的应用程序。
- 使用插件的应用程序可能需要仔细配置,以确保程序的各个部分在文件系统(或容器映像)的正确位置可用。相比之下,部署由单个静态可执行文件组成的应用程序非常简单。
- 当某些包可能在应用程序开始运行很长时间后才初始化时,关于程序初始化的推理会更加困难。
- 攻击者可以利用加载插件的应用程序中的错误来加载危险或不受信任的库。
- 除非程序的所有部分(应用程序及其所有插件)都使用完全相同版本的工具链、相同的构建标签以及某些标志和环境变量的相同值进行编译,否则很可能会发生运行时崩溃。
Demo 实现
实现分为三部分:主程序、组件程序、公共库。公共库可与主程序所属库共同,组件程序进行包引用即可。
- 主程序
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
}
Benchwork 基准性能
- 基本性能十分优秀,32 核机器简单 Demo 基准性能测试
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)、共享内存映射或文件系统操作)可能更适合,尽管性能开销很大。
实现二:开源 go-plugin
相关 Demo 实现、Benchwork 基准性能、小结 见后续文章。
附录
- https://eli.thegreenplace.net/2021/plugins-in-go/
入口服务演变
互联网初期的时候,是没有入口或前置服务的概念的。
请求服务都是从端上发起到后段处理的链路。随着业务种类及规模的膨胀,出现了以下的问题:
- 端上承接大量的请求发起任务重担
- 各业务接受请求后,都需要流量清洗、作弊判定…等公共处理举措
- 网络带宽等资源数量级的增加
- 用户体验对服务平响、数据质量…等性能要求的提升
- …
基于这些棘手的问题,急需在服务入口提供一公共模块,进行一些前置操作,在节省资源、业务维护成本的同时,提升服务性能、质量。