<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          hashicorp go-plugin構(gòu)建golang插件系統(tǒng)

          共 25181字,需瀏覽 51分鐘

           ·

          2021-04-28 16:43

          一、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 的特性包括:

          1. 插件是 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é)
          2. 跨語(yǔ)言支持:插件可以被任何主流語(yǔ)言編寫(和使用),該庫(kù)支持通過(guò) gRPC 提供服務(wù)插件,而基于 gRPC 的插件是允許被任何語(yǔ)言編寫的。
          3. 支持復(fù)雜的參數(shù)、返回值: go-plugin 可以處理接口、io.Reader/Writer 等復(fù)雜類型,我們?yōu)槟峁┝艘粋€(gè)庫(kù)(MuxBroker),用于在客戶端/服務(wù)器之間創(chuàng)建新連接,以服務(wù)于附加接口或傳輸原始數(shù)據(jù)。
          4. 雙向通信: 為了支持復(fù)雜參數(shù),宿主進(jìn)程能夠?qū)⒔涌趯?shí)現(xiàn)發(fā)送給插件,插件也能夠回調(diào)到宿主進(jìn)程(這點(diǎn)還需要看官網(wǎng)的雙向通信的例子好好理解下)
          5. 內(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)化。
          6. 協(xié)議版本化: 支持一個(gè)簡(jiǎn)單的協(xié)議版本化,可增加版本號(hào)使之前插件無(wú)效。當(dāng)接口簽名變化、協(xié)議版本改變等情況時(shí),協(xié)議版本話是很有用的。當(dāng)協(xié)議版本不兼容時(shí),會(huì)發(fā)送錯(cuò)誤消息給終端用戶。
          7. 標(biāo)準(zhǔn)輸出/錯(cuò)誤同步: 插件以子進(jìn)程的方式運(yùn)行,這些插件可以自由的使用標(biāo)準(zhǔn)輸出/錯(cuò)誤,并且輸出會(huì)被鏡像回到宿主進(jìn)程。
          8. TTY Preservation: 插件子進(jìn)程可以鏈接到宿主進(jìn)程的 stdin 標(biāo)準(zhǔn)輸入文件描述符,允許以TTY方式運(yùn)行的軟件。
          9. 插件運(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。
          10. 加密通信: gRPC信道可以加密

          1.3 架構(gòu)優(yōu)勢(shì)

          1. 插件不影響宿主機(jī)進(jìn)程:插件崩潰了,不會(huì)導(dǎo)致宿主進(jìn)程崩潰
          2. 插件容易編寫:僅僅寫個(gè) go 應(yīng)用程序并執(zhí)行 go build?;蛘呤褂闷渌Z(yǔ)言來(lái)編寫 gRPC 服務(wù) ,加上少量的模板來(lái)支持 go-plugin。
          3. 易于安裝:只需要將插件放到宿主進(jìn)程能夠訪問(wèn)的目錄即可,剩下的事情由宿主進(jìn)程來(lái)處理。
          4. 完整性校驗(yàn):支持對(duì)插件的二進(jìn)制文件進(jìn)行 Checksum
          5. 插件是相對(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ì)?。?!


          點(diǎn)擊這里閱讀原文,即刻報(bào)名~



          瀏覽 204
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91操比| 亚洲中文字幕在线视频 | 五月情色天| 婷婷五月天丁香在线视频 | 久久久久久久视频 |