hashicorp go-plugin構(gòu)建golang插件系統(tǒng)
一、go-plugin 簡(jiǎn)介
1.1 go-plugin 是什么?
我們知道 Go 語(yǔ)言缺乏動(dòng)態(tài)加載代碼的機(jī)制,Go 程序通常是獨(dú)立的二進(jìn)制文件,因此難以實(shí)現(xiàn)類似于 C++ 的插件系統(tǒng)。即使go的最新標(biāo)準(zhǔn)引入了 go plugin 機(jī)制,但是由于限制性條件比較多導(dǎo)致在生產(chǎn)環(huán)境中不是很好用,比如插件的編寫環(huán)境和插件的使用環(huán)境要保持一致,如 gopath、go sdk 版本等。
HashiCorp 公司開源的 go-plugin 庫(kù)解決了上述問(wèn)題,允許應(yīng)用程序通過(guò)本地網(wǎng)絡(luò)(本機(jī))的 gRPC 調(diào)用插件,規(guī)避了 Go 無(wú)法動(dòng)態(tài)加載代碼的缺點(diǎn)。go-plugin 是一個(gè)通過(guò)RPC 實(shí)現(xiàn)的 Go 插件系統(tǒng),并在 Packer、Terraform, Nomad、Vault 等由 HashiCorp 主導(dǎo)的項(xiàng)目中均有應(yīng)用。
順便說(shuō)一句,Vault 開源代碼,我這幾天看了下,代碼寫的很不錯(cuò),感興趣的小伙伴可以看看vault是怎么使用 go-plugin,很值得借鑒,后續(xù)會(huì)針對(duì) vault 的源代碼的插件部分進(jìn)行剖析。
1.2 特性
go-plugin 的特性包括:
插件是 Go 接口的實(shí)現(xiàn): 這讓插件的編寫、使用非常自然。對(duì)于插件編寫者來(lái)說(shuō),他只需要實(shí)現(xiàn)一個(gè) Go 接口即可;對(duì)于插件的用戶來(lái)說(shuō),就像在同一個(gè)進(jìn)程中使用和調(diào)用函數(shù)即可。go-plugin 會(huì)處理好本地調(diào)用轉(zhuǎn)換為 gRPC 調(diào)用的所有細(xì)節(jié) 跨語(yǔ)言支持:插件可以被任何主流語(yǔ)言編寫(和使用),該庫(kù)支持通過(guò) gRPC 提供服務(wù)插件,而基于 gRPC 的插件是允許被任何語(yǔ)言編寫的。 支持復(fù)雜的參數(shù)、返回值: go-plugin 可以處理接口、io.Reader/Writer 等復(fù)雜類型,我們?yōu)槟峁┝艘粋€(gè)庫(kù)(MuxBroker),用于在客戶端/服務(wù)器之間創(chuàng)建新連接,以服務(wù)于附加接口或傳輸原始數(shù)據(jù)。 雙向通信: 為了支持復(fù)雜參數(shù),宿主進(jìn)程能夠?qū)⒔涌趯?shí)現(xiàn)發(fā)送給插件,插件也能夠回調(diào)到宿主進(jìn)程(這點(diǎn)還需要看官網(wǎng)的雙向通信的例子好好理解下) 內(nèi)置日志系統(tǒng): 任何使用 log 標(biāo)準(zhǔn)庫(kù)的的插件,都會(huì)自動(dòng)將日志信息傳回宿主機(jī)進(jìn)程。宿主進(jìn)程會(huì)鏡像日志輸出,并在這些日志前面加上插件二進(jìn)制文件的路徑。這會(huì)使插件的調(diào)試變簡(jiǎn)單。如果宿主機(jī)使用 hclog,日志數(shù)據(jù)將被結(jié)構(gòu)化。如果插件同樣使用 hclog,插件的日志會(huì)發(fā)往宿主機(jī)并被結(jié)構(gòu)化。 協(xié)議版本化: 支持一個(gè)簡(jiǎn)單的協(xié)議版本化,可增加版本號(hào)使之前插件無(wú)效。當(dāng)接口簽名變化、協(xié)議版本改變等情況時(shí),協(xié)議版本話是很有用的。當(dāng)協(xié)議版本不兼容時(shí),會(huì)發(fā)送錯(cuò)誤消息給終端用戶。 標(biāo)準(zhǔn)輸出/錯(cuò)誤同步: 插件以子進(jìn)程的方式運(yùn)行,這些插件可以自由的使用標(biāo)準(zhǔn)輸出/錯(cuò)誤,并且輸出會(huì)被鏡像回到宿主進(jìn)程。 TTY Preservation: 插件子進(jìn)程可以鏈接到宿主進(jìn)程的 stdin 標(biāo)準(zhǔn)輸入文件描述符,允許以TTY方式運(yùn)行的軟件。 插件運(yùn)行狀態(tài)中,宿主進(jìn)程升級(jí): 插件可以"reattached",所以可以在插件運(yùn)行狀態(tài)中升級(jí)宿主機(jī)進(jìn)程。NewClient 函數(shù)使用 ReattachConfig 選項(xiàng)來(lái)確定是否 Reattach 以及如何 Reattach。 加密通信: gRPC信道可以加密
1.3 架構(gòu)優(yōu)勢(shì)
插件不影響宿主機(jī)進(jìn)程:插件崩潰了,不會(huì)導(dǎo)致宿主進(jìn)程崩潰 插件容易編寫:僅僅寫個(gè) go 應(yīng)用程序并執(zhí)行 go build?;蛘呤褂闷渌Z(yǔ)言來(lái)編寫 gRPC 服務(wù) ,加上少量的模板來(lái)支持 go-plugin。 易于安裝:只需要將插件放到宿主進(jìn)程能夠訪問(wèn)的目錄即可,剩下的事情由宿主進(jìn)程來(lái)處理。 完整性校驗(yàn):支持對(duì)插件的二進(jìn)制文件進(jìn)行 Checksum 插件是相對(duì)安全的:插件只能訪問(wèn)傳遞給它的接口和參數(shù),而不是進(jìn)程的整個(gè)內(nèi)存空間。另外,go-plugin 可以基于 TLS 和插件進(jìn)行通信。
1.4 適用場(chǎng)景
go-plugin 目前僅設(shè)計(jì)為在本地[可靠]網(wǎng)絡(luò)上工作,不支持 go-plugin 在真實(shí)網(wǎng)絡(luò),并可能會(huì)導(dǎo)致未知的行為。
即不能將 go-plugin 用于在兩臺(tái)服務(wù)器之間的遠(yuǎn)程過(guò)程調(diào)用,這點(diǎn)和傳統(tǒng)的RPC有很大區(qū)別,望謹(jǐn)記。
二、核心數(shù)據(jù)結(jié)構(gòu)
2.1 Plugin接口
Plugin 是一個(gè)接口,是插件進(jìn)程和宿主進(jìn)程進(jìn)行通信的橋梁。
不管是插件編寫者還是插件使用者,都需要實(shí)現(xiàn) plugin.Plugin 接口,只是各自的實(shí)現(xiàn)不同。
type Plugin interface {
// Server should return the RPC server compatible struct to serve
// the methods that the Client calls over net/rpc.
Server(*MuxBroker) (interface{}, error)
// Client returns an interface implementation for the plugin you're
// serving that communicates to the server end of the plugin.
Client(*MuxBroker, *rpc.Client) (interface{}, error)
}
Server 接口:Server 接口應(yīng)返回與 RPC server 兼容的結(jié)構(gòu)以提供方法,客戶端可以通過(guò)net/rpc來(lái)調(diào)用此方法。
Client 接口:Client 接口返回你提供服務(wù)的插件的接口實(shí)現(xiàn),該接口實(shí)現(xiàn)將與該插件的服務(wù)器端進(jìn)行通信。
2.2 GRPCPlugin 接口
type GRPCPlugin interface {
// 由于gRPC插件以單例方式服務(wù),因此該方法僅調(diào)用一次
GRPCServer(*GRPCBroker, *grpc.Server) error
// 插件進(jìn)程退出時(shí),context會(huì)被go-plugin關(guān)閉
GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
}
GRPCPlugin的接口實(shí)現(xiàn),在grpc的例子中我們?cè)僭敿?xì)解釋。
2.3 plugin.client接口
這個(gè)接口負(fù)責(zé)管理一個(gè)插件進(jìn)程的完整生命周期,包括創(chuàng)建插件進(jìn)程、連接到插件進(jìn)程、分配接口實(shí)現(xiàn)、處理殺死進(jìn)程。
對(duì)于每個(gè)插件,宿主機(jī)進(jìn)程需要?jiǎng)?chuàng)建一個(gè)plugin.Client實(shí)例。
type Client struct {
// 插件客戶端配置
config *ClientConfig
// 插件進(jìn)程是否已經(jīng)退出
exited bool
l sync.Mutex
// 插件進(jìn)程的RPC監(jiān)聽地址
address net.Addr
// 插件進(jìn)程對(duì)象
process *os.Process
// 協(xié)議客戶端,宿主進(jìn)程需要調(diào)用其Dispense方法來(lái)獲得業(yè)務(wù)接口的Stub
client ClientProtocol
// 通信協(xié)議
protocol Protocol
logger hclog.Logger
doneCtx context.Context
ctxCancel context.CancelFunc
negotiatedVersion int
// 用于管理 插件管理協(xié)程的生命周期
clientWaitGroup sync.WaitGroup
stderrWaitGroup sync.WaitGroup
// 測(cè)試用,標(biāo)記進(jìn)程是否被強(qiáng)殺
processKilled bool
}
2.4 ClientConfig 和 ServeConfig 對(duì)比
ClientConfig 包含了初始化一個(gè)插件客戶端所需的配置信息,一旦初始化,則不可更改。
ServeConfig 包含了初始化一個(gè)插件服務(wù)器端所需的配置信息,一旦初始化,則不可更改。
枚舉這兩個(gè)結(jié)構(gòu)體,對(duì)其字段進(jìn)行對(duì)比和類比分析
type ClientConfig struct {
// 握手信息,用于宿主、插件的匹配。如果不匹配,插件會(huì)拒絕連接
HandshakeConfig
// 可以消費(fèi)的插件列表
Plugins PluginSet
// 版本化的插件列表,用于支持在客戶端、服務(wù)器之間協(xié)商兼容版本
VersionedPlugins map[int]PluginSet
// 啟動(dòng)插件進(jìn)程使用的命令行,不能和Reattach聯(lián)用
Cmd *exec.Cmd
// 連接到既有插件進(jìn)程的必要信息,不能和Cmd聯(lián)用
Reattach *ReattachConfig
// 用于在啟動(dòng)插件時(shí)校驗(yàn)二進(jìn)制文件的完整性
SecureConfig *SecureConfig
// 基于TLS進(jìn)行RPC通信時(shí)需要的信息
TLSConfig *tls.Config
// 標(biāo)識(shí)客戶端是否應(yīng)該被plugin包自動(dòng)管理
// 如果為true,則調(diào)用CleanupClients自動(dòng)清理
// 否則用戶需要負(fù)責(zé)殺掉插件客戶端,默認(rèn)false
Managed bool
// 和子進(jìn)程通信使用的端口范圍,
MinPort, MaxPort uint
// 啟動(dòng)插件的超時(shí)
StartTimeout time.Duration
......
}
type ServeConfig struct {
// 和客戶端匹配的握手配置,其信息必須和客戶端匹配,否則會(huì)拒絕連接
HandshakeConfig
// 調(diào)用此函數(shù)得到tls.Config
TLSProvider func() (*tls.Config, error)
// 可以提供服務(wù)的插件集
Plugins PluginSet
// 版本化的插件列表,用于支持在客戶端、服務(wù)器之間協(xié)商兼容版本
VersionedPlugins map[int]PluginSet
// 如果通過(guò)gRPC提供服務(wù),則此字段不能為空
// 調(diào)用此函數(shù)創(chuàng)建一個(gè)gRPC服務(wù)器對(duì)象
// 公司場(chǎng)景采用grpc通信,所以涉及到grpc的要重點(diǎn)看
GRPCServer func([]grpc.ServerOption) *grpc.Server
Logger hclog.Logger
}
ClientConfig 和 ServeConfig 中都需要填寫 HandshakeConfig,這里要聲明兩點(diǎn)
對(duì)于 rpc 通信的 client 和 server 來(lái)說(shuō),其 ClientConfig 和 ServeConfig 中的配置要保持完全一致,否則會(huì)導(dǎo)致連接失敗,這塊在調(diào)試的時(shí)候,耗費(fèi)了我一些時(shí)間和精力。
2.5 PluginSet結(jié)構(gòu)體
插件進(jìn)程在啟動(dòng)時(shí)設(shè)置Plugins,即ServeConfig中設(shè)置Plugins時(shí),會(huì)指明其實(shí)現(xiàn)者;
宿主機(jī)進(jìn)程在啟動(dòng)時(shí)也設(shè)置Plugins,即ClientConfig中設(shè)置Plugins時(shí),不需要指明其實(shí)現(xiàn)者。
//插件進(jìn)程的插件集
var pluginMap = map[string]plugin.Plugin{
"greeter": &example.GreeterPlugin{Impl: greeter},
}
//宿主機(jī)進(jìn)程的插件集
var pluginMap = map[string]plugin.Plugin{
"greeter": &example.GreeterPlugin{},
}
如上圖所示,其ServeConfig中插件業(yè)務(wù)接口實(shí)現(xiàn)者是greeter
三、架構(gòu)設(shè)計(jì)圖

四、從example入手學(xué)習(xí)
通過(guò)go-plugin庫(kù)自帶的兩個(gè)例子來(lái)展示庫(kù)的使用方法
4.1 basic例子剖析
4.1.1 業(yè)務(wù)接口定義
// Greeter is the interface that we're exposing as a plugin.
type Greeter interface {
Greet() string
}
暴露插件需要實(shí)現(xiàn)的接口,接口的實(shí)現(xiàn)是在插件進(jìn)程中。
4.1.2 宿主機(jī)進(jìn)程剖析
宿主機(jī)進(jìn)程的代碼如下:
func main() {
// 創(chuàng)建hclog.Logger類型的日志對(duì)象
logger := hclog.New(&hclog.LoggerOptions{
Name: "plugin",
Output: os.Stdout,
Level: hclog.Debug,
})
// 兩種方式選其一
// 以exec.Command方式啟動(dòng)插件進(jìn)程,并創(chuàng)建宿主機(jī)進(jìn)程和插件進(jìn)程的連接
// 或者使用Reattach連接到現(xiàn)有進(jìn)程
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
//創(chuàng)建新進(jìn)程,或使用Reattach連接到現(xiàn)有進(jìn)程中
Cmd: exec.Command("./plugin/greeter"),
Logger: logger,
})
// 關(guān)閉client,釋放相關(guān)資源,終止插件子程序的運(yùn)行
defer client.Kill()
// 返回協(xié)議客戶端,如rpc客戶端或grpc客戶端,用于后續(xù)通信
rpcClient, err := client.Client()
if err != nil {
log.Fatal(err)
}
// 根據(jù)指定插件名稱分配新實(shí)例
raw, err := rpcClient.Dispense("greeter")
if err != nil {
log.Fatal(err)
}
// 像調(diào)用普通函數(shù)一樣調(diào)用接口函數(shù)就ok,很方便是不是?
greeter := raw.(example.Greeter)
fmt.Println(greeter.Greet())
}
var pluginMap = map[string]plugin.Plugin{
//插件名稱到插件對(duì)象的映射關(guān)系
"greeter": &example.GreeterPlugin{},
}
其流程一共拆解為5步:
第一步、plugin.NewClient創(chuàng)建宿主機(jī)進(jìn)程和插件進(jìn)程之間的連接
plugin.NewClient創(chuàng)建plugin.Client,可以簡(jiǎn)單理解為宿主機(jī)進(jìn)程和插件進(jìn)程之間連接。其參數(shù)pluginMap表示可被消費(fèi)的插件列表。
第二步、調(diào)用client.Client(),返回當(dāng)前連接的協(xié)議客戶端(即rpcClient)
協(xié)議支持net/rpc或gRPC,所以協(xié)議客戶端可能是gRPC客戶端,也可能是標(biāo)準(zhǔn)net/rpc客戶端。
第三步、調(diào)用rpcClient.Dispense,根據(jù)指定插件名稱分配一個(gè)新實(shí)例
由于此函數(shù)很關(guān)鍵,下面通過(guò)走讀源代碼來(lái)梳理下流程:
func (c *RPCClient) Dispense(name string) (interface{}, error) {
//1、查找插件類型是否支持
p, ok := c.plugins[name]
if !ok {
return nil, fmt.Errorf("unknown plugin type: %s", name)
}
var id uint32
if err := c.control.Call(
"Dispenser.Dispense", name, &id); err != nil {
return nil, err
}
conn, err := c.broker.Dial(id)
if err != nil {
return nil, err
}
//2、非常重要,Dispense函數(shù)會(huì)回調(diào)Plugin的Client接口實(shí)現(xiàn)
return p.Client(c.broker, rpc.NewClient(conn))
}
在Dispense方法中會(huì)調(diào)用自己實(shí)現(xiàn)的插件的Client方法。
前面2.1章節(jié)提過(guò),每個(gè)新插件都會(huì)實(shí)現(xiàn)plugin.Plugin接口(grpc插件實(shí)現(xiàn)的是GRPCPlugin接口),即Server和Client接口
下面是basic例子中的GreeterPlugin插件實(shí)現(xiàn)
type GreeterPlugin struct {
// 內(nèi)嵌業(yè)務(wù)接口
// 插件進(jìn)程會(huì)設(shè)置其為實(shí)現(xiàn)業(yè)務(wù)接口的對(duì)象
// 宿主進(jìn)程則置空
Impl Greeter
}
// 此方法由插件進(jìn)程延遲的調(diào)用
func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &GreeterRPCServer{Impl: p.Impl}, nil
}
// 此方法由宿主進(jìn)程調(diào)用
func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &GreeterRPC{client: c}, nil
}
Server方法必須返回一個(gè)這種插件類型的RPC server服務(wù)器,我們構(gòu)造了GreeterRPCServer。
Client方法必須返回一個(gè)接口的實(shí)現(xiàn),并且能夠通過(guò)RPC client客戶端通信,我們返回了GreeterRPC
這里出個(gè)思考題
1、Server方法為什么需要返回GreeterRPCServer的指針?
2、Client方法為什么需要返回GreeterRPC的指針?
感興趣的小伙伴可在評(píng)論區(qū)留言哈,看看你是否理解到本質(zhì)呢?
綜上所述,Dispense的返回值是指向GreeterRPC的指針。
type GreeterRPC struct{ client *rpc.Client }
func (g *GreeterRPC) Greet() string {
var resp string
err := g.client.Call("Plugin.Greet", new(interface{}), &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
}
GreeterRPC結(jié)構(gòu)體實(shí)現(xiàn)了業(yè)務(wù)接口的Greet(),在方法實(shí)現(xiàn)的函數(shù)體body中,實(shí)際是用rpc client客戶端調(diào)用Call()來(lái)進(jìn)行遠(yuǎn)程過(guò)程調(diào)用,并將響應(yīng)返回,如出錯(cuò)則會(huì)導(dǎo)致panic。
問(wèn)題3:g.client.Call(“Plugin.Greet”, new(interface{}), &resp)中的第一個(gè)參數(shù)"Plugin.Greet"可以更換嗎?
第四步、轉(zhuǎn)換成業(yè)務(wù)接口類型,并調(diào)用對(duì)應(yīng)api
從第三步我們知道Dispense的返回值raw是指向GreeterRPC的指針。而GreeterRPC結(jié)構(gòu)體實(shí)現(xiàn)了業(yè)務(wù)接口example.Greeter。所以兩者之間可以進(jìn)行類型轉(zhuǎn)換。
greeter := raw.(example.Greeter) fmt.Println(greeter.Greet())
上述兩句代碼,將raw轉(zhuǎn)換為業(yè)務(wù)接口類型example.Greeter,然后調(diào)用之前暴露的業(yè)務(wù)接口Greet()函數(shù)。
第五步、關(guān)閉client,釋放資源
調(diào)用client.Kill()函數(shù),來(lái)釋放之前申請(qǐng)的系統(tǒng)資源,防止內(nèi)存泄露。
4.1.3 插件進(jìn)程剖析
// Here is a real implementation of Greeter
// 重點(diǎn):業(yè)務(wù)接口的真正實(shí)現(xiàn)
type GreeterHello struct {
logger hclog.Logger
}
//之前暴露的插件業(yè)務(wù)接口,此處必須實(shí)現(xiàn),供宿主機(jī)進(jìn)程RPC調(diào)用
func (g *GreeterHello) Greet() string {
g.logger.Debug("message from GreeterHello.Greet")
return "Hello!"
}
//握手配置,插件進(jìn)程和宿主機(jī)進(jìn)程,都需要保持一致
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "BASIC_PLUGIN",
MagicCookieValue: "hello",
}
func main() {
logger := hclog.New(&hclog.LoggerOptions{
Level: hclog.Trace,
Output: os.Stderr,
JSONFormat: true,
})
greeter := &GreeterHello{
logger: logger,
}
// pluginMap is the map of plugins we can dispense.
// 插件進(jìn)程必須指定Impl,此處賦值為greeter對(duì)象
var pluginMap = map[string]plugin.Plugin{
"greeter": &example.GreeterPlugin{Impl: greeter},
}
logger.Debug("message from plugin", "foo", "bar")
//調(diào)用plugin.Serve()啟動(dòng)偵聽,并提供服務(wù)
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: handshakeConfig,
Plugins: pluginMap,
})
}
第一步、定義GreeterHello結(jié)構(gòu)體,并實(shí)現(xiàn)插件暴露的業(yè)務(wù)接口Greet()
第二步、整理插件的映射關(guān)系,并在plugin.Serve函數(shù)調(diào)用時(shí),以參數(shù)形式賦值給Plugins
如名稱為greeter的插件,對(duì)應(yīng)&example.GreeterPlugin{Impl: greeter}
var pluginMap = map[string]plugin.Plugin{
//插件名稱到插件對(duì)象的映射關(guān)系
"greeter": &example.GreeterPlugin{Impl: greeter},
}
第三步、在main函數(shù)中調(diào)用plugin.Serve(),啟動(dòng)監(jiān)聽來(lái)提供插件服務(wù)。
服務(wù)器調(diào)用plugin.Serve方法后,主線程會(huì)阻塞。直到客戶端調(diào)用 Dispense方法請(qǐng)求插件實(shí)例時(shí),服務(wù)器端才會(huì)實(shí)例化插件(業(yè)務(wù)接口的實(shí)現(xiàn)):
func (d *dispenseServer) Dispense(
name string, response *uint32) error {
// 從PluginSet中查找
p, ok := d.plugins[name]
if !ok {
return fmt.Errorf("unknown plugin type: %s", name)
}
// 調(diào)用(下面的那個(gè)函數(shù))插件接口的方法
impl, err := p.Server(d.broker)
if err != nil {
return errors.New(err.Error())
}
// MuxBroker基于唯一性的ID進(jìn)行TCP連接的多路復(fù)用
id := d.broker.NextId()
*response = id
// 在另外一個(gè)協(xié)程中處理該請(qǐng)求
go func() {
conn, err := d.broker.Accept(id)
if err != nil {
log.Printf("[ERR] go-plugin: plugin dispense error: %s: %s", name, err)
return
}
serve(conn, "Plugin", impl)
}()
return nil
}
func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &GreeterRPCServer{Impl: p.Impl}, nil
}
4.2 grpc 例子剖析
應(yīng)當(dāng)盡量采用grpc而非net/rpc,原因如下:
1)gRPC支持多種語(yǔ)言來(lái)實(shí)現(xiàn)插件,而net/rpc是Go專有的,不利于程序的可擴(kuò)展性
2)在gRPC模式下,go-plugin插件請(qǐng)求通過(guò)http2發(fā)送,傳輸性能更好
3)對(duì)于gRPC模式來(lái)說(shuō),插件進(jìn)程只會(huì)有單個(gè)插件“實(shí)例”。對(duì)于net/rpc你可能需要?jiǎng)?chuàng)建多個(gè)“實(shí)例”。
使用gRPC模式下的go-plugin,其步驟如下:
xx(未完成)
xx
xx
4.2.1 proto 定義
syntax = "proto3";
package proto;
//請(qǐng)求
message GetRequest {
string key = 1;
}
//應(yīng)答
message GetResponse {
bytes value = 1;
}
message PutRequest {
string key = 1;
bytes value = 2;
}
message Empty {}
//定義service的接口Get和Put
service KV {
rpc Get(GetRequest) returns (GetResponse);
rpc Put(PutRequest) returns (Empty);
}
執(zhí)行命令:protoc -I proto/ proto/kv.proto --go_out=plugins=grpc:proto/ 生成Go代碼。
4.2.2 業(yè)務(wù)接口
在examples/grpc/shared/interface.go文件中,是其定義的業(yè)務(wù)接口
// 業(yè)務(wù)接口
type KV interface {
Put(key string, value []byte) error
Get(key string) ([]byte, error)
}
4.2.3 插件接口
gRPC模式下,你需要實(shí)現(xiàn)接口plugin.GRPCPlugin,并嵌入plugin.Plugin接口:
問(wèn)題4:為什么要嵌入plugin.Plugin插件接口呢?以前只嵌入接口的實(shí)現(xiàn)就行(這點(diǎn)暫時(shí)還沒解決)
type KVGRPCPlugin struct {
// 需要嵌入插件接口
plugin.Plugin
// 具體實(shí)現(xiàn),僅當(dāng)業(yè)務(wù)接口實(shí)現(xiàn)基于Go時(shí)該字段有用
Impl KV
}
plugin.GRPCPlugin接口的規(guī)格如下,你需要實(shí)現(xiàn)兩個(gè)方法:
type GRPCPlugin interface {
// 此方法被插件進(jìn)程調(diào)用
// 你需要向其提供的grpc.ServergRPC參數(shù),注冊(cè)服務(wù)的實(shí)現(xiàn)(服務(wù)器端存根)
// 由于gRPC下服務(wù)器端是單例模式,因此該方法僅調(diào)用一次
GRPCServer(*GRPCBroker, *grpc.Server) error
// 此方法被宿主進(jìn)程調(diào)用
// 你需要返回一個(gè)業(yè)務(wù)接口的實(shí)現(xiàn)(客戶端存根),此實(shí)現(xiàn)直接將請(qǐng)求轉(zhuǎn)給gRPC客戶端即可
// 傳入的context對(duì)象會(huì)在插件進(jìn)程銷毀時(shí)取消
GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
}
其實(shí)現(xiàn)如下:
func (p *KVGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
//向grpc.ServergRPC類型參數(shù)s,注冊(cè)服務(wù)的實(shí)現(xiàn)
proto.RegisterKVServer(s, &GRPCServer{Impl: p.Impl})
return nil
}
func (p *KVGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
//創(chuàng)建gRPC客戶端的方法是自動(dòng)生成的
return &GRPCClient{client: proto.NewKVClient(c)}, nil
}
備注信息:
KVPlugin是對(duì)plugin.Plugin接口的實(shí)現(xiàn)
KVGRPCPlugin是對(duì)GRPCPlugin接口的實(shí)現(xiàn)
GRPCClient是對(duì)KV接口的實(shí)現(xiàn)
GRPCServer是對(duì)KVServer接口的實(shí)現(xiàn)
1、GRPCServer接口實(shí)現(xiàn)
// 實(shí)現(xiàn)自動(dòng)生成的KVServer接口,具體邏輯委托給業(yè)務(wù)接口KV的實(shí)現(xiàn)
type GRPCServer struct {
// This is the real implementation
Impl KV
}
func (m *GRPCServer) Put(
ctx context.Context,
req *proto.PutRequest) (*proto.Empty, error) {
return &proto.Empty{}, m.Impl.Put(req.Key, req.Value)
}
func (m *GRPCServer) Get(
ctx context.Context,
req *proto.GetRequest) (*proto.GetResponse, error) {
v, err := m.Impl.Get(req.Key)
return &proto.GetResponse{Value: v}, err
}
2、GRPCClient接口實(shí)現(xiàn)
在GRPCClient方法的實(shí)現(xiàn)中,你需要返回一個(gè)業(yè)務(wù)接口的實(shí)現(xiàn)(客戶端stub),此實(shí)現(xiàn)只是將請(qǐng)求轉(zhuǎn)發(fā)給gRPC服務(wù)處理:
//業(yè)務(wù)接口KV
type KV interface {
Put(key string, value []byte) error
Get(key string) ([]byte, error)
}
//業(yè)務(wù)接口KV的實(shí)現(xiàn),通過(guò)gRPC客戶端轉(zhuǎn)發(fā)請(qǐng)求給插件進(jìn)程
type GRPCClient struct{ client proto.KVClient }
func (m *GRPCClient) Put(key string, value []byte) error {
_, err := m.client.Put(context.Background(), &proto.PutRequest{
Key: key,
Value: value,
})
return err
}
func (m *GRPCClient) Get(key string) ([]byte, error) {
resp, err := m.client.Get(context.Background(), &proto.GetRequest{
Key: key,
})
if err != nil {
return nil, err
}
return resp.Value, nil
}
4.2.4 宿主機(jī)進(jìn)程
宿主機(jī)進(jìn)程使用gRPC方式時(shí),只需要設(shè)置AllowedProtocols,指明同時(shí)支持plugin.ProtocolNetRPC和plugin.ProtocolGRPC兩種協(xié)議。
func main() {
// We don't want to see the plugin logs.
log.SetOutput(ioutil.Discard)
// We're a host. Start by launching the plugin process.
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: shared.Handshake,
Plugins: shared.PluginMap,
Cmd: exec.Command("sh", "-c", os.Getenv("KV_PLUGIN")),
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
})
defer client.Kill()
// Connect via RPC
rpcClient, err := client.Client()
if err != nil {
fmt.Println("Error:", err.Error())
os.Exit(1)
}
// Request the plugin
raw, err := rpcClient.Dispense("kv_grpc")
if err != nil {
fmt.Println("Error:", err.Error())
os.Exit(1)
}
// We should have a KV store now! This feels like a normal interface
// implementation but is in fact over an RPC connection.
kv := raw.(shared.KV)
os.Args = os.Args[1:]
switch os.Args[0] {
case "get":
result, err := kv.Get(os.Args[1])
if err != nil {
fmt.Println("Error:", err.Error())
os.Exit(1)
}
fmt.Println(string(result))
case "put":
err := kv.Put(os.Args[1], []byte(os.Args[2]))
if err != nil {
fmt.Println("Error:", err.Error())
os.Exit(1)
}
default:
fmt.Printf("Please only use 'get' or 'put', given: %q", os.Args[0])
os.Exit(1)
}
os.Exit(0)
}
4.2.5 插件進(jìn)程
只需要指定GRPCServer,提供創(chuàng)建gRPC服務(wù)器的函數(shù),其他的和以前沒什么區(qū)別。
// Here is a real implementation of KV that writes to a local file with
// the key name and the contents are the value of the key.
type KV struct{}
func (KV) Put(key string, value []byte) error {
value = []byte(fmt.Sprintf("%s\n\nWritten from plugin-go-grpc", string(value)))
return ioutil.WriteFile("kv_"+key, value, 0644)
}
func (KV) Get(key string) ([]byte, error) {
return ioutil.ReadFile("kv_" + key)
}
func main() {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: shared.Handshake,
Plugins: map[string]plugin.Plugin{
"kv": &shared.KVGRPCPlugin{Impl: &KV{}},
},
// A non-nil value here enables gRPC serving for this plugin...
GRPCServer: plugin.DefaultGRPCServer,
})
}
五、總結(jié)
5.1 宿主機(jī)進(jìn)程和插件進(jìn)程在rpc通信中扮演的角色?
扮演客戶端的角色,插件進(jìn)程扮演服務(wù)器的角色,因?yàn)椴寮M(jìn)程在主函數(shù)的末尾會(huì)調(diào)用Serve(opts *ServeConfig)函數(shù)。
5.2 插件編寫者和插件使用者如何來(lái)使用go-plugin庫(kù)?
一般來(lái)說(shuō),步驟如下:
1、選擇插件希望暴露的接口。
2、對(duì)于每個(gè)接口,實(shí)現(xiàn)該接口確保其通過(guò)net/rpc連接或gRPC連接可以通信,你必須同時(shí)實(shí)現(xiàn)客戶端和服務(wù)器。
3、創(chuàng)建Plugin接口的實(shí)現(xiàn),知道如何為給定的插件類型創(chuàng)建RPC client/server。
4、插件編寫者,在main函數(shù)中調(diào)用plugin.Serve(),啟動(dòng)監(jiān)聽來(lái)提供插件服務(wù)。
5、插件使用者,使用plugin.Client啟動(dòng)子進(jìn)程,通過(guò)rpc請(qǐng)求一個(gè)接口實(shí)現(xiàn)。
上述步驟有不妥當(dāng)?shù)牡胤?,?qǐng)及時(shí)反饋給我。
參考鏈接:https://blog.gmem.cc/go-plugin-over-grpc#comment-26043

?? 各位Gopher們,注意啦!
別忘了還有 Gopher China2021 大會(huì)
還沒報(bào)名的童鞋們趕快抓住最后的機(jī)會(huì)?。?!

