<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; RPC</title>
	<atom:link href="https://blog.gmem.cc/tag/rpc/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 13 Apr 2026 08:03:10 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>基于本地gRPC的Go插件系统</title>
		<link>https://blog.gmem.cc/go-plugin-over-grpc</link>
		<comments>https://blog.gmem.cc/go-plugin-over-grpc#comments</comments>
		<pubDate>Thu, 16 Jan 2020 05:54:03 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[RPC]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=31305</guid>
		<description><![CDATA[<p>Go插件化 Go语言缺乏好用的动态加载代码的机制，Go程序通常是单个自包含的二进制文件，因此难以实现类似于Java那样的插件系统。 两种方式 编译期插件 这种插件直接编译到二进制文件中。典型的例子是database/sql包中的数据库驱动。这种插件都是通过空白导入： [crayon-69deb25193213623739948/] 的方式激活，插件包的init方法负责插件的注册和初始化。这类插件的缺点包括： 违反开闭原则，引入插件必须修改依赖它的程序 运行时插件 对于编译型（直接编译为本地代码，不是JVM那样的虚拟机的代码）语言，都可以调用共享库。Go语言也不例外，其内置的plugin包，就是基于共享库实现插件化。plugin包有一些致命的缺点： 限制条件非常苛刻，对于主程序、插件共同使用的包，有任何修改，你都必须重新编译主程序和插件。否则会得到报错：plugin was built with a different version of package 主程序和插件的编译器、编译标记也必须一致 对操作系统的支持不完善，不支持Windows   <a class="read-more" href="https://blog.gmem.cc/go-plugin-over-grpc">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-plugin-over-grpc">基于本地gRPC的Go插件系统</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">Go插件化</span></div>
<p>Go语言缺乏好用的动态加载代码的机制，Go程序通常是单个自包含的二进制文件，因此难以实现类似于Java那样的插件系统。</p>
<div class="blog_h2"><span class="graybg">两种方式</span></div>
<div class="blog_h3"><span class="graybg">编译期插件</span></div>
<p>这种插件直接编译到二进制文件中。典型的例子是database/sql包中的数据库驱动。这种插件都是通过空白导入：</p>
<pre class="crayon-plain-tag">package main
import _ "name"</pre>
<p>的方式激活，插件包的init方法负责插件的注册和初始化。这类插件的缺点包括：</p>
<ol>
<li>违反开闭原则，引入插件必须修改依赖它的程序</li>
</ol>
<div class="blog_h3"><span class="graybg">运行时插件</span></div>
<p>对于编译型（直接编译为本地代码，不是JVM那样的虚拟机的代码）语言，都可以调用共享库。Go语言也不例外，其内置的plugin包，就是基于共享库实现插件化。plugin包有一些致命的缺点：</p>
<ol>
<li>限制条件非常苛刻，对于主程序、插件共同使用的包，有任何修改，你都必须重新编译主程序和插件。否则会得到报错：plugin was built with a different version of package</li>
<li>主程序和插件的编译器、编译标记也必须一致</li>
<li>对操作系统的支持不完善，不支持Windows  </li>
</ol>
<div class="blog_h1"><span class="graybg">go-plugin简介</span></div>
<p><a href="https://github.com/hashicorp/go-plugin"> hashicorp / go-plugin</a>是一个通过RPC实现的Go插件系统，在Packer、Terraform, Nomad、Vault等由HashiCorp主导的项目中均由应用。go-plugin允许应用程序通过<span style="background-color: #c0c0c0;">本地网络（本机）的gRPC调用插件</span>，规避了Go无法动态加载代码的缺点。</p>
<p>go-plugin插件由宿主进程调用，宿主进程会以插件二进制文件为映像创建子进程，并且通过单个网络连接与之通信。网络协议可以是：</p>
<ol>
<li>net/rpc：这种情况下go-plugin使用<a href="https://github.com/hashicorp/yamux">yamux</a>库对连接进行多路复用</li>
<li>gRPC：这种情况下基于HTTP2协议进行多路复用</li>
</ol>
<div class="blog_h2"><span class="graybg">特性</span></div>
<p>go-plugin的特性包括：</p>
<ol>
<li>插件是Go接口的实现：这让插件的编写、使用非常自然。对于插件的作者来说，他只需要实现一个Go接口即可；对于插件的用户来说，他只需要调用一个Go接口即可。go-plugin会处理好本地调用转换为gRPC调用的所有细节</li>
<li>跨语言支持：插件可以基于任何主流语言编写，同样可以被任何主流语言消费</li>
<li>支持复杂的参数、返回值：go-plugin可以处理接口、io.Reader/Writer等复杂类型</li>
<li>双向通信：为了支持复杂参数，宿主进程能够将接口实现发送给插件，插件也能够回调到宿主进程</li>
<li>内置日志系统：任何使用log标准库的的插件，都会将日志信息传回宿主机进程。宿主进程会在这些日志前面加上插件二进制文件的路径，并且打印日志</li>
<li>协议版本化：支持一个简单的协议版本化，增加版本号后可以基于老版本协议的插件无效化。当接口签名变化时应当增加版本</li>
<li>标准输出/错误同步：插件以子进程的方式运行，这些子进程可以自由的使用标准输出/错误，并且打印的内容会被自动同步到宿主进程，宿主进程可以为同步的日志指定一个io.Writer</li>
<li>TTY Preservation：插件子进程可以链接到宿主进程的stdin文件描述符，以便要求TTY的软件能正常工作</li>
<li>宿主进程升级：宿主进程升级的时候，插件子进程可以继续允许，并在升级后自动关联到新的宿主进程</li>
<li>加密通信：gRPC信道可以加密</li>
<li>完整性校验：支持对插件的二进制文件进行Checksum</li>
<li>插件崩溃了，不会导致宿主进程崩溃</li>
<li>容易安装：只需要将插件放到某个宿主进程能够访问的目录即可</li>
</ol>
<div class="blog_h1"><span class="graybg">架构</span></div>
<div class="blog_h2"><span class="graybg">插件接口</span></div>
<p>插件接口是宿主进程、插件进程进行通信的桥梁：</p>
<ol>
<li>宿主进程会将插件接口的实现放到自己的插件集中</li>
<li>插件进程会将插件接口的实现放到自己的插件集中，并为每个插件指定业务接口的实现。<span style="background-color: #c0c0c0;">一个插件可以实现多个业务接口</span></li>
<li>宿主进程可以：
<ol>
<li>主动创建插件进程，从而得到插件进程的监听套接字</li>
<li>关联到既有插件进程，需要手工提供插件进程的监听套接字</li>
</ol>
</li>
<li>宿主进程会调用Client方法，获得插件客户端</li>
<li>插件进程会调用Server方法，创建插件服务器端，并且将请求委托给<span style="background-color: #c0c0c0;">业务接口</span>的实现</li>
<li>宿主进程通过plugin.Client，可以获得业务接口的Stub，调用业务接口的方法会自动转换为RPC远程调用</li>
<li>插件进程上的UDS套接字监听到调用后，会解析RPC请求，并<span style="background-color: #c0c0c0;">（Lazy的）分发（Dispense）</span>给对应的业务接口Impl</li>
</ol>
<p>架构示意如下：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/01/go-plugin.png"><img class="aligncenter  wp-image-31325" src="https://cdn.gmem.cc/wp-content/uploads/2020/01/go-plugin.png" alt="go-plugin" width="701" height="272" /></a></p>
<div class="blog_h3"><span class="graybg">Plugin</span></div>
<p>接口<pre class="crayon-plain-tag">Plugin</pre>用于获得一个插件的服务器、客户端：</p>
<pre class="crayon-plain-tag">type Plugin interface {
	// 返回一个RPC服务器兼容的结构，提供客户端可以通过net/rpc调用的方法
	Server(*MuxBroker) (interface{}, error)

	// 返回一个RPC客户端
	Client(*MuxBroker, *rpc.Client) (interface{}, error)
}</pre>
<div class="blog_h3"><span class="graybg">GRPCPlugin</span></div>
<p>这个接口类似，只是通信方式基于gRPC：</p>
<pre class="crayon-plain-tag">type GRPCPlugin interface {
	// 由于gRPC插件以单例方式服务，因此该方法仅调用一次
	GRPCServer(*GRPCBroker, *grpc.Server) error

	// 插件进程退出时，context会被go-plugin关闭
	GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
}</pre>
<div class="blog_h2"><span class="graybg">plugin.Client</span></div>
<p>这个结构负责管理一个插件应用（进程）的完整生命周期，包括<span style="background-color: #c0c0c0;">创建插件进程、连接到插件进程、Dispense业务接口的实现、最后杀死进程</span>。</p>
<p>对于每个插件（二进制文件），宿主进程需要创建一个plugin.Client实例。</p>
<p>该结构的字段如下：</p>
<pre class="crayon-plain-tag">type Client struct {
	// 插件客户端配置
	config            *ClientConfig
	// 插件进程是否已经退出
	exited            bool
	l                 sync.Mutex
	// 插件进程的RPC监听地址
	address           net.Addr
	// 插件进程对象
	process           *os.Process
	// 协议客户端，宿主进程需要调用其Dispense方法来获得业务接口的Stub
	client            ClientProtocol
	// 通信协议
	protocol          Protocol
	logger            hclog.Logger
	doneCtx           context.Context
	ctxCancel         context.CancelFunc
	negotiatedVersion int

	// 用于管理 插件管理协程的生命周期
	clientWaitGroup sync.WaitGroup

	stderrWaitGroup sync.WaitGroup

	//  测试用，标记进程是否被强杀
	processKilled bool
}</pre>
<div class="blog_h3"><span class="graybg">ClientConfig</span></div>
<p>包含初始化一个插件客户端所需的配置信息，一旦初始化客户端，即不可修改：</p>
<pre class="crayon-plain-tag">type ClientConfig struct {
	// 握手信息，用于宿主、插件的匹配。如果不匹配，插件会拒绝连接
	HandshakeConfig

	// 可以调用的插件列表
	Plugins PluginSet

	// 版本化的插件列表，用于支持在客户端、服务器之间协商兼容版本
	VersionedPlugins map[int]PluginSet

	// 启动插件进程使用的命令行，不能和Reattach联用
	Cmd      *exec.Cmd
	// 连接到既有插件进程的必要信息，不能和Cmd联用
	Reattach *ReattachConfig

	// 用于在启动插件时校验二进制文件的完整性
	SecureConfig *SecureConfig

	// 基于TLS进行RPC通信时需要的信息
	TLSConfig *tls.Config

	// 客户端是否应该被plugin包自动管理
	// 如果为true，则调用CleanupClients自动清理
	// 否则用户需要负责杀掉插件客户端，默认false
	Managed bool

	// 和子进程通信使用的端口范围
	MinPort, MaxPort uint

	// 启动插件的超时
	StartTimeout time.Duration

	Stderr io.Writer
	SyncStdout io.Writer
	SyncStderr io.Writer

	// 支持的协议，如果不指定仅仅支持netrpc
	AllowedProtocols []Protocol

	// 不指定则为hclog的默认logger
	Logger hclog.Logger

	AutoMTLS bool
}</pre>
<div class="blog_h2"><span class="graybg">plugin.Serve</span></div>
<p>插件主函数的结尾，必须调用此函数来启动监听，需要传入一个ServeConfig。</p>
<div class="blog_h3"><span class="graybg">ServeConfig</span></div>
<pre class="crayon-plain-tag">type ServeConfig struct {
	// 和客户端匹配的握手配置
	HandshakeConfig

	// 调用此函数得到tls.Config
	TLSProvider func() (*tls.Config, error)

	// 插件集
	Plugins PluginSet

	// 版本化的插件集
	VersionedPlugins map[int]PluginSet

	// 如果通过gRPC提供服务，则此字段不能为空
	// 调用此函数创建一个gRPC服务器对象
	GRPCServer func([]grpc.ServerOption) *grpc.Server

	Logger hclog.Logger
} </pre>
<div class="blog_h2"><span class="graybg">HandshakeConfig</span></div>
<p>ClientConfig、ServerConfig都会嵌入此结构，此结构用于宿主、插件建立连接前的握手。</p>
<p>宿主进程创建插件进程时，会将此信息通过环境变量传递。这样插件进程能够正常启动。如果没有这些环境变量，则插件进程会退出，并提示你插件必须由宿主进程启动。</p>
<pre class="crayon-plain-tag">type HandshakeConfig struct {
	// 协议版本号
	ProtocolVersion uint

	// 用于简单的校验插件进程是否人为手工启动的
	MagicCookieKey   string
	MagicCookieValue string
}</pre>
<div class="blog_h1"><span class="graybg">示例</span></div>
<div class="blog_h2"><span class="graybg">netrpc</span></div>
<p>基于net/rpc的通信方式目前仅仅用于向后兼容，任何新开发的代码都应该考虑使用gRPC方式。</p>
<div class="blog_h3"><span class="graybg">业务接口</span></div>
<p>很简单，返回一个问候信息：</p>
<pre class="crayon-plain-tag">type Greeter interface {
	Greet() string
}</pre>
<p>需要提供一个基于net/rpc的实现：</p>
<pre class="crayon-plain-tag">import "net/rpc"

// 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(interface{}), &amp;resp)
	if err != nil {
		panic(err)
	}

	return resp
}</pre>
<div class="blog_h3"><span class="graybg">插件接口</span></div>
<p>此结构实现plugin.Plugin接口：</p>
<pre class="crayon-plain-tag">type GreeterPlugin struct {
	// 内嵌业务接口，插件进程会设置此自动，宿主进程则置空
	Impl Greeter
}

// 此方法由插件进程Lazy的调用
func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
	return &amp;GreeterRPCServer{Impl: p.Impl}, nil
}

// 此方法由宿主进程调用
func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
	return &amp;GreeterRPC{client: c}, nil
}

// 这是net/rpc所需要的RPC服务器对象
type GreeterRPCServer struct {
	// This is the real implementation
	Impl Greeter
}
func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error {
	*resp = s.Impl.Greet()
	return nil
}</pre>
<div class="blog_h3"><span class="graybg">宿主进程</span></div>
<p>即插件客户端：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/basic/commons"
	"log"
	"net"
	"os"
)

func main() {
	logger := hclog.New(&amp;hclog.LoggerOptions{
		Name:   "plugin",
		Output: os.Stdout,
		Level:  hclog.Debug,
	})

	// 宿主机调用plugin.NewClient，创建或连接到插件进程
	client := plugin.NewClient(&amp;plugin.ClientConfig{
		HandshakeConfig: handshakeConfig,
		Plugins: map[string]plugin.Plugin{
			// 插件名字到插件的映射关系
			"greeter": &amp;example.GreeterPlugin{},
		},
		// 创建新进程
		Cmd:             exec.Command("./plugin/greeter"),
		// 连接到现有进程
		Reattach: &amp;plugin.ReattachConfig{
			Pid: 2802223,
			Addr: &amp;net.UnixAddr{
				Net:  "unix",
				Name: "/tmp/plugin137476534",
			},
		},
		Logger: logger,
	})
	// 此调用会终止插件子进程的执行，并且进行必要的清理工作（例如收集日志）
	defer client.Kill()

	// 获取RPC客户端对象
	rpcClient, err := client.Client()
	if err != nil {
		log.Fatal(err)
	}

	// 产生具有指定名字的插件的实例
	raw, err := rpcClient.Dispense("greeter")
	if err != nil {
		log.Fatal(err)
	}

	// 插件实例可以强制转型为业务接口
	greeter := raw.(example.Greeter)
	// 像调用本地函数一样调用远程插件，客户端本身是线程安全的，你可以并发的调用业务接口
	fmt.Println(greeter.Greet())
}

// 用于插件和宿主之间的简单握手，用于提升用户体验（而非安全特性）
// 如果握手失败，则提示一个友好的信息
// 可以防止：1、执行错误的插件；2、用户手工启动插件
var handshakeConfig = plugin.HandshakeConfig{
	ProtocolVersion:  1,
	MagicCookieKey:   "BASIC_PLUGIN",
	MagicCookieValue: "hello",
}</pre>
<p>不管是插件客户端还是服务器，都需要（在ClientConfig/ServeConfig中）指定一个一个PluginSet，列出支持的插件。对于服务器，必须指定每个插件的Impl。</p>
<div class="blog_h3"><span class="graybg">插件进程</span></div>
<pre class="crayon-plain-tag">package main

import (
	"os"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/basic/commons"
)

// 业务接口的真实实现
type GreeterHello struct {
	logger hclog.Logger
}

func (g *GreeterHello) Greet() string {
	g.logger.Debug("message from GreeterHello.Greet")
	return "Hello!"
}

// 握手配置
var handshakeConfig = plugin.HandshakeConfig{
	ProtocolVersion:  1,
	MagicCookieKey:   "BASIC_PLUGIN",
	MagicCookieValue: "hello",
}

func main() {
	logger := hclog.New(&amp;hclog.LoggerOptions{
		Level:      hclog.Trace,
		Output:     os.Stderr,
		JSONFormat: true,
	})

	greeter := &amp;GreeterHello{
		logger: logger,
	}
	// 插件集类似于宿主进程，只是插件需要指定Impl字段
	var pluginMap = map[string]plugin.Plugin{
		"greeter": &amp;example.GreeterPlugin{Impl: greeter},
	}

	logger.Debug("message from plugin", "foo", "bar")

	// 启动RPC监听
	plugin.Serve(&amp;plugin.ServeConfig{
		HandshakeConfig: handshakeConfig,
		Plugins:         pluginMap,
	})
}</pre>
<p>服务器调用plugin.Serve方法后，主线程即阻塞。直到客户端调用<pre class="crayon-plain-tag">Dispense</pre>方法请求插件实例时，服务器端才会实例化插件（业务接口的实现）：</p>
<pre class="crayon-plain-tag">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)
	}

	// 调用（下面的那个函数）插件接口的方法
	impl, err := p.Server(d.broker)
	if err != nil {
		return errors.New(err.Error())
	}

	// MuxBroker基于唯一性的ID进行TCP连接的多路复用
	id := d.broker.NextId()
	*response = id

	// 在另外一个协程中处理该请求
	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 &amp;GreeterRPCServer{Impl: p.Impl}, nil
}</pre>
<div class="blog_h2"><span class="graybg">gRPC</span></div>
<p>应当尽量使用grpc而非net/rpc，原因如下：</p>
<ol>
<li>gRPC支持多种语言来实现插件，而net/rpc是Go专有的</li>
<li>在gRPC模式下，go-plugin插件请求通过HTTP2发送，而非私有的yamux</li>
<li>net/rpc使用go类型，尽管比较方便，但却存在潜在的兼容性问题</li>
</ol>
<p>此外需要注意，对于gRPC模式来说，插件进程中只会有单个插件“实例”。对于net/rpc你可能创建多个“实例”。</p>
<p>使用gRPC模式时，你需要：</p>
<ol>
<li>通过Proto文件定义语言中立的接口</li>
<li>从Proto生成gRPC客户端、服务器样板代码</li>
<li>实现<pre class="crayon-plain-tag">plugin.GRPCPlugin</pre>的两个方法，分别调用上面生成的样板代码，获取gRPC客户端、注册gRPC服务实现类</li>
<li>在gRPC服务实现类中，将gRPC接口适配为业务接口</li>
<li>业务接口的实现由插件进程提供</li>
</ol>
<p>样板类模板比较多，有些繁琐。</p>
<div class="blog_h3"><span class="graybg">Proto定义</span></div>
<p>一个简单的键值存储服务：</p>
<pre class="crayon-plain-tag">syntax = "proto3";
package proto;

message GetRequest {
    string key = 1;
}

message GetResponse {
    bytes value = 1;
}

message PutRequest {
    string key = 1;
    bytes value = 2;
}

message Empty {}

service KV {
    rpc Get(GetRequest) returns (GetResponse);
    rpc Put(PutRequest) returns (Empty);
}</pre>
<p>执行命令：<pre class="crayon-plain-tag">protoc -I proto/ proto/kv.proto --go_out=plugins=grpc:proto/</pre> 生成Go代码。</p>
<div class="blog_h3"><span class="graybg">业务接口</span></div>
<pre class="crayon-plain-tag">// Package shared contains shared data between the host and plugins.
package shared

import (
	"context"
	"net/rpc"

	"google.golang.org/grpc"

	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/grpc/proto"
)


// 业务接口
type KV interface {
	Put(key string, value []byte) error
	Get(key string) ([]byte, error)
}</pre>
<div class="blog_h3"><span class="graybg">插件接口</span></div>
<p>这一节的代码被宿主进程，插件进程共享。</p>
<p>gRPC模式下，你需要实现接口plugin.GRPCPlugin，并嵌入plugin.Plugin接口：</p>
<pre class="crayon-plain-tag">type KVGRPCPlugin struct {
	// 需要嵌入插件接口
	plugin.Plugin
	// 具体实现，仅当业务接口实现基于Go时该字段有用
	Impl KV
}</pre>
<p>plugin.GRPCPlugin接口的规格如下，你需要实现两个方法：</p>
<pre class="crayon-plain-tag">type GRPCPlugin interface {
	// 此方法被插件进程调用，插件进程提供一个grpc.Server
	// 你需要向其注册gRPC服务的实现（服务器端存根）
	// 由于gRPC下服务器端是单例模式，因此该方法仅调用一次
	GRPCServer(*GRPCBroker, *grpc.Server) error

	// 此方法被宿主进程调用
	// 你需要返回一个业务接口的实现（客户端存根），此实现直接将请求转给gRPC客户端即可
	// 传入的context对象会在插件进程销毁时取消
	GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error)
}</pre>
<p>在GRPCServer方法的实现中，你需要向参数提供的grpc.Server<span style="background-color: #c0c0c0;">注册通过Proto文件生成的gRPC服务的Go接口</span>：</p>
<pre class="crayon-plain-tag">type KVServer interface {
	Get(context.Context, *GetRequest) (*GetResponse, error)
	Put(context.Context, *PutRequest) (*Empty, error)
}</pre>
<p>的实现（服务器存根）：</p>
<pre class="crayon-plain-tag">// 在此方法中提供实现：
func (p *KVGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
	proto.RegisterKVServer(s, &amp;KVServerStub{Impl: p.Impl})
	return nil
}

// 实现自动生成的KVServer接口，具体逻辑委托给业务接口KV的实现
type KVServerStub struct {
	// This is the real implementation
	Impl KV
}

// 样板代码
func (m *KVServerStub) Put( ctx context.Context, req *proto.PutRequest) (*proto.Empty, error) {
	return &amp;proto.Empty{}, m.Impl.Put(req.Key, req.Value)
}
func (m *KVServerStub) Get( ctx context.Context, req *proto.GetRequest) (*proto.GetResponse, error) {
	v, err := m.Impl.Get(req.Key)
	return &amp;proto.GetResponse{Value: v}, err
}</pre>
<p>在GRPCClient方法的实现中，你需要<span style="background-color: #c0c0c0;">返回一个业务接口的实现（客户端存根）</span>，此实现只是将请求转发给gRPC服务处理：</p>
<pre class="crayon-plain-tag">func (p *KVGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
	//                         创建gRPC客户端的方法是自动生成的
	return &amp;KVClientStub{client: proto.NewKVClient(c)}, nil
}

// 业务接口KV的实现，通过gRPC客户端转发请求给插件进程
type KVClientStub struct{ client proto.KVClient }

//                   业务接口
func (m *KVClientStub) Put(key string, value []byte) error {
// 转发
	_, err := m.client.Put(context.Background(), &amp;proto.PutRequest{
		Key:   key,
		Value: value,
	})
	return err
}

func (m *KVClientStub) Get(key string) ([]byte, error) {
	resp, err := m.client.Get(context.Background(), &amp;proto.GetRequest{
		Key: key,
	})
	if err != nil {
		return nil, err
	}

	return resp.Value, nil
}</pre>
<div class="blog_h3"><span class="graybg">宿主进程</span></div>
<p>使用gRPC方式时，只需要设置AllowedProtocols，其余的和netrpc模式没有区别。</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"

	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/grpc/shared"
)

func main() {
	log.SetOutput(ioutil.Discard)

	// 创建插件进程
	client := plugin.NewClient(&amp;plugin.ClientConfig{
		HandshakeConfig: plugin.HandshakeConfig{
			ProtocolVersion:  1,
			MagicCookieKey:   "BASIC_PLUGIN",
			MagicCookieValue: "hello",
		},
		Plugins:         map[string]plugin.Plugin{
			"kv_grpc": &amp;KVGRPCPlugin{},
		},
		Cmd:             exec.Command("sh", "-c", os.Getenv("KV_PLUGIN")),
  	 	// 允许的协议类型，默认情况下允许netrpc
		AllowedProtocols: []plugin.Protocol{
			plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
	})
	defer client.Kill()

	// 获取RPC客户端
	rpcClient, err := client.Client()
	if err != nil {
		fmt.Println("Error:", err.Error())
		os.Exit(1)
	}

	// 得到插件实例
	raw, err := rpcClient.Dispense("kv_grpc")
	if err != nil {
		fmt.Println("Error:", err.Error())
		os.Exit(1)
	}

	// 插件实例转换为业务接口
	// ...
}</pre>
<div class="blog_h3"><span class="graybg">插件进程</span></div>
<p>和netrpc方式也没什么差别，只需要指定GRPCServer，提供创建gRPC服务器的函数即可：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"io/ioutil"

	"github.com/hashicorp/go-plugin"
	"github.com/hashicorp/go-plugin/examples/grpc/shared"
)

// 这里是KV的真实实现
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(&amp;plugin.ServeConfig{
		HandshakeConfig: plugin.HandshakeConfig{
			ProtocolVersion:  1,
			MagicCookieKey:   "BASIC_PLUGIN",
			MagicCookieValue: "hello",
		},
		Plugins: map[string]plugin.Plugin{
			"kv": &amp;shared.KVGRPCPlugin{Impl: &amp;KV{}},
		},

		// 该字段不为nil，则基于gRPC协议进行服务
		GRPCServer: plugin.DefaultGRPCServer,
	})
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-plugin-over-grpc">基于本地gRPC的Go插件系统</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/go-plugin-over-grpc/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>gRPC学习笔记</title>
		<link>https://blog.gmem.cc/grpc-study-note</link>
		<comments>https://blog.gmem.cc/grpc-study-note#comments</comments>
		<pubDate>Thu, 08 Feb 2018 12:38:16 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[RPC]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=18243</guid>
		<description><![CDATA[<p>简介 gRPC是Google开源的高性能RPC框架，它在HTTP/2的基础之上实现。gRPC能高效的连接数据中心内部、跨数据中心的服务，并且可以提供名称解析、负载均衡、请求跟踪、健康检查以及身份验证等基础服务。它的主要适用场景包括： 在微服务架构中，有效的将多种语言开发的服务连接到一起  gRPC也可以用来将设备、移动应用程序、浏览器连接到后端服务 生成高效的客户端库 gRPC的核心特性包括： 支持多种主流编程语言 基于HTTP/2的双向流传输 可拔插的基础服务支持 相比起传统的HTTP/REST/JSON方式的RPC，基于HTTP/2实现RPC具有优势：二进制协议、单个连接上的请求多路分发、头压缩。 入门 安装 gRPC要求Go的版本在1.6+。执行下面的命令安装所需的包： [crayon-69deb25193cf6973269441/] 消息定义 下面是一个用户信息查询服务的例子： [crayon-69deb25193cfb221965619/] 生成代码 执行如下命令生成gRPC的Go语言代码： [crayon-69deb25193cfe305901783/] 命令输出为一个Go源码文件，内容如下： [crayon-69deb25193d00839080392/] <a class="read-more" href="https://blog.gmem.cc/grpc-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/grpc-study-note">gRPC学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>gRPC是Google开源的高性能RPC框架，它<span style="background-color: #c0c0c0;">在HTTP/2的基础之上实现</span>。gRPC能高效的连接数据中心内部、跨数据中心的服务，并且可以提供名称解析、负载均衡、请求跟踪、健康检查以及身份验证等基础服务。它的主要适用场景包括：</p>
<ol>
<li>在微服务架构中，有效的将多种语言开发的服务连接到一起</li>
<li> gRPC也可以用来将设备、移动应用程序、浏览器连接到后端服务</li>
<li>生成高效的客户端库</li>
</ol>
<p>gRPC的核心特性包括：</p>
<ol>
<li>支持多种主流编程语言</li>
<li>基于<a href="/http-study-note#http2">HTTP/2</a>的双向流传输</li>
<li>可拔插的基础服务支持</li>
</ol>
<p>相比起传统的HTTP/REST/JSON方式的RPC，基于HTTP/2实现RPC具有优势：二进制协议、单个连接上的请求多路分发、头压缩。</p>
<div class="blog_h1"><span class="graybg">入门</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>gRPC要求Go的版本在1.6+。执行下面的命令安装所需的包：</p>
<pre class="crayon-plain-tag">export https_proxy="http://10.0.0.1:8087/"
# gRPC
go get -u google.golang.org/grpc
# ProtoBuf
go get -u github.com/golang/protobuf/protoc-gen-go</pre>
<div class="blog_h2"><span class="graybg">消息定义</span></div>
<p>下面是一个用户信息查询服务的例子：</p>
<pre class="crayon-plain-tag">syntax = "proto3";
package gmem;

service UserService {
    // gRPC服务的参数和返回值必须是message类型，不能是任何原始类型
    rpc GetUser (GetUserRequest) returns (User) {
    }
}
message GetUserRequest {
    uint32 userId = 1;
}
message User {
    uint32 userId = 1;
    string userName = 2;
    string dob = 3;
}</pre>
<div class="blog_h2"><span class="graybg">生成代码</span></div>
<p>执行如下命令生成gRPC的Go语言代码：</p>
<pre class="crayon-plain-tag">cd /home/alex/Go/workspace/default/src/grpc
# Proto源文件中注意Go包声明 option go_package = "./pkg/grpc/supportplan";
protoc --proto_path=protos --go_out=. --go-grpc_out=. protos/*.proto</pre>
<p>命令输出为一个Go源码文件，内容如下：</p>
<pre class="crayon-plain-tag">package gmem

import (
    proto "github.com/golang/protobuf/proto"
    context "golang.org/x/net/context"
    grpc "google.golang.org/grpc"
)


/* 消息结构体代码 */
// RPC请求消息结构体
type GetUserRequest struct {
    UserId uint32 `protobuf:"varint,1,opt,name=userId" json:"userId,omitempty"`
}
// RPC响应消息结构体
type User struct {
    UserId   uint32 `protobuf:"varint,1,opt,name=userId" json:"userId,omitempty"`
    UserName string `protobuf:"bytes,2,opt,name=userName" json:"userName,omitempty"`
    Dob      string `protobuf:"bytes,3,opt,name=dob" json:"dob,omitempty"`
}
func init() {
    // 将Go语言结构指针映射到对应的ProtoBuf全限定名
    proto.RegisterType((*GetUserRequest)(nil), "gmem.GetUserRequest")
    proto.RegisterType((*User)(nil), "gmem.User")
}


/* gRPC代码 */

// gRPC服务的客户端API
type UserServiceClient interface {
    GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
}
type userServiceClient struct {
    cc *grpc.ClientConn
}
// 调用下面的方法来创建gRPC客户端
func NewUserServiceClient(cc *grpc.ClientConn) UserServiceClient {
    return &amp;userServiceClient{cc}
}
func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error) {
	out := new(User)
	err := grpc.Invoke(ctx, "/gmem.UserService/GetUser", in, out, c.cc, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// gRPC服务的服务器端API
type UserServiceServer interface {
	GetUser(context.Context, *GetUserRequest) (*User, error)
}
// 调用下面的方法来注册gRPC服务器端实现
func RegisterUserServiceServer(s *grpc.Server, srv UserServiceServer) {
	s.RegisterService(&amp;_UserService_serviceDesc, srv)
}
func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(GetUserRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(UserServiceServer).GetUser(ctx, in)
	}
	info := &amp;grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: "/gmem.UserService/GetUser",
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(UserServiceServer).GetUser(ctx, req.(*GetUserRequest))
	}
	return interceptor(ctx, in, info, handler)
}
var _UserService_serviceDesc = grpc.ServiceDesc{
	ServiceName: "gmem.UserService",
	HandlerType: (*UserServiceServer)(nil),
	Methods: []grpc.MethodDesc{
		{
			MethodName: "GetUser",
			Handler:    _UserService_GetUser_Handler,
		},
	},
	Streams:  []grpc.StreamDesc{},
	Metadata: "UserService.proto",
}

func init() { proto.RegisterFile("UserService.proto", fileDescriptor0) }</pre>
<p>可以看到，gRPC的客户端存根、服务器接口是自动生成的，其中<span style="background-color: #c0c0c0;">封装了和远程调用相关的逻辑</span>。我们需要调用存根、实现接口，来编写完整的gRPC客户端或者服务器。 </p>
<div class="blog_h2"><span class="graybg">编写实现</span></div>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">package main

import (
    "context"
    "grpc/gmem"
    "net"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
    "log"
)

// 实现服务器端接口 UserServiceServer
type UserServiceServerImpl struct {
}

func (srv *UserServiceServerImpl) GetUser(ctx context.Context, req *gmem.GetUserRequest) (*gmem.User, error) {
    return &amp;gmem.User{
        UserId:   req.GetUserId(),
        UserName: "Alex Wong",
        Dob:      "1986-09-12",
    }, nil
}

func main() {
    lis, _ := net.Listen("tcp", ":5500")
    grpc := grpc.NewServer()
    gmem.RegisterUserServiceServer(grpc, &amp;UserServiceServerImpl{})
    reflection.Register(grpc)
    err := grpc.Serve(lis)
    if err != nil {
        log.Fatalf("无法创建RPC服务器：%v", err)
    }
}</pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<pre class="crayon-plain-tag">package main

import (
    "google.golang.org/grpc"
    "log"
    "grpc/gmem"
    "context"
    "encoding/json"
)

func main() {
    // 连接gRPC服务器
    conn, _ := grpc.Dial("localhost:5500", grpc.WithInsecure())

    // 不再使用时需要关闭连接
    defer conn.Close()

    // 创建客户端对象
    var client gmem.UserServiceClient = gmem.NewUserServiceClient(conn)
    userRequest := gmem.GetUserRequest{UserId: 10000}
    user, err := client.GetUser(context.Background(), &amp;userRequest)
    if err != nil {
        log.Fatalf("远程调用失败: %v", err)
    }
    userJson, _ := json.Marshal(*user)
    log.Println(string(userJson)) //  {"userId":10000,"userName":"Alex Wong","dob":"1986-09-12"}
} </pre>
<div class="blog_h2"><span class="graybg">其它语言</span></div>
<div class="blog_h3"><span class="graybg">Java</span></div>
<p>可以使用Maven插件protobuf-maven-plugin来生成gRPC的客户端和服务器API：</p>
<pre class="crayon-plain-tag">&lt;project&gt;
    &lt;!-- GRPC相关依赖 --&gt;
    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.grpc&lt;/groupId&gt;
            &lt;artifactId&gt;grpc-netty&lt;/artifactId&gt;
            &lt;version&gt;1.9.0&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.grpc&lt;/groupId&gt;
            &lt;artifactId&gt;grpc-protobuf&lt;/artifactId&gt;
            &lt;version&gt;1.9.0&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.grpc&lt;/groupId&gt;
            &lt;artifactId&gt;grpc-stub&lt;/artifactId&gt;
            &lt;version&gt;1.9.0&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;

    &lt;build&gt;
        &lt;extensions&gt;
            &lt;extension&gt;
                &lt;groupId&gt;kr.motd.maven&lt;/groupId&gt;
                &lt;artifactId&gt;os-maven-plugin&lt;/artifactId&gt;
                &lt;version&gt;1.5.0.Final&lt;/version&gt;
            &lt;/extension&gt;
        &lt;/extensions&gt;
        &lt;plugins&gt;
            &lt;!-- 集成基于ProtoBuf的代码生成 --&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.xolstice.maven.plugins&lt;/groupId&gt;
                &lt;artifactId&gt;protobuf-maven-plugin&lt;/artifactId&gt;
                &lt;version&gt;0.5.0&lt;/version&gt;
                &lt;configuration&gt;
                    &lt;protocArtifact&gt;com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}&lt;/protocArtifact&gt;
                    &lt;pluginId&gt;grpc-java&lt;/pluginId&gt;
                    &lt;pluginArtifact&gt;io.grpc:protoc-gen-grpc-java:1.9.0:exe:${os.detected.classifier}&lt;/pluginArtifact&gt;
                &lt;/configuration&gt;
                &lt;executions&gt;
                    &lt;execution&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;compile&lt;/goal&gt;
                            &lt;goal&gt;compile-custom&lt;/goal&gt;
                        &lt;/goals&gt;
                    &lt;/execution&gt;
                &lt;/executions&gt;
            &lt;/plugin&gt;
            &lt;plugin&gt;
                &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
                &lt;version&gt;3.5.1&lt;/version&gt;
                &lt;configuration&gt;
                    &lt;encoding&gt;${project.build.sourceEncoding}&lt;/encoding&gt;
                    &lt;source&gt;1.8&lt;/source&gt;
                    &lt;target&gt;1.8&lt;/target&gt;
                    &lt;debug&gt;true&lt;/debug&gt;
                &lt;/configuration&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;
&lt;/project&gt;</pre>
<p>需要使用到的插件目标：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">目标</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>protobuf:compile</td>
<td>在target/generated-sources/protobuf/java下生成ProtoBuf消息的代码</td>
</tr>
<tr>
<td>protobuf:compile-custom</td>
<td>在target/generated-sources/protobuf/grpc-java下生成gRPC存根代码</td>
</tr>
</tbody>
</table>
<p>注意：</p>
<ol>
<li>上述目标生成代码会自动添加到工程的源码路径，不需要拷贝到src/main/java</li>
<li>proto文件需要放置在src/main/proto下</li>
</ol>
<p>服务器示例：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.grpc;

import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;

import java.io.IOException;

public class UserServiceServer {

    public static void main( String[] args ) throws IOException, InterruptedException {
        // 启动GRPC服务器并注册RPC接口的实现类
        io.grpc.Server server = ServerBuilder
                .forPort( 5500 )
                .addService( new UserServiceImpl() )
                .build()
                .start();
        server.awaitTermination();
        // 优雅的关闭GRPC服务器
        Runtime.getRuntime().addShutdownHook( new Thread( () -&gt; server.shutdown() ) );
    }

    private static class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {

        @Override
        public void getUser( GetUserRequest request, StreamObserver&lt;User&gt; responseObserver ) {
            User user = User.newBuilder()
                            .setUserId( request.getUserId() )
                            .setUserName( "Alex Wong" )
                            .setDob( "1986-09-12" )
                            .build();
            responseObserver.onNext( user );
            responseObserver.onCompleted();
        }
    }
} </pre>
<p>客户端示例：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.grpc;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class UserServiceClient {

    public static void main( String[] args ) throws InterruptedException, TimeoutException, ExecutionException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress( "localhost", 5500 ).usePlaintext( true ).build();
        // 阻塞的客户端存根
        UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub( channel );
        // 非阻塞的客户端存根
        UserServiceGrpc.UserServiceFutureStub asyncStub = UserServiceGrpc.newFutureStub( channel );

        // 构建请求消息
        GetUserRequest request = GetUserRequest.newBuilder().setUserId( 10000 ).build();
        // 发起同步请求
        User user = stub.getUser( request );
        // 发起异步请求
        asyncStub.getUser( request ).get( 1, TimeUnit.SECONDS );

        // 关闭通道
        channel.shutdown().awaitTermination( 5, TimeUnit.SECONDS );
    }
}</pre>
<div class="blog_h1"><span class="graybg">基础</span></div>
<p>和典型的RPC一样，gRPC允许你定义服务、指定可以被远程调用的接口，为这些接口指定参数和返回值类型。默认的，gRPC使用ProtoBuf作为接口定义语言（IDL，Interface Definition Language ）。gRPC支持四类服务定义。</p>
<div class="blog_h2"><span class="graybg">客户端选项</span></div>
<pre class="crayon-plain-tag">conn, _ := grpc.Dial(ChartAddr, []grpc.DialOption{
        // 不加下面这项可能出现段错误
        grpc.WithInsecure(),
        // 阻塞直到连接成功，默认情况下Dial方法立即返回，在后台连接
	grpc.WithBlock(),
        // 每30s发送keepalive心跳包，防止连接被上游服务器关闭
	grpc.WithKeepaliveParams(keepalive.ClientParameters{
		Time: time.Duration(30) * time.Second,
	}),
        // 增加最大消息限制，默认4MB
	grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024 * 1024 * 20)),
}...)</pre>
<div class="blog_h2"><span class="graybg">一元RPC</span></div>
<p>客户端发起单个请求，然后获得单个响应，就像是典型的函数调用：</p>
<pre class="crayon-plain-tag">rpc SayHello(HelloRequest) returns (HelloResponse){
}</pre>
<div class="blog_h3"><span class="graybg">生命周期</span></div>
<ol>
<li>客户端调用Stub代码</li>
<li>服务器感知到RPC调用的发生，获得客户端的元数据、方法名、Deadline</li>
<li>服务器可以将自己的元数据发送给客户端，或者等待请求消息的到达。元数据必须先于响应发送</li>
<li>服务器接收到客户端的请求消息后，进行处理</li>
<li>响应消息连同状态（状态码、可选的状态信息）一起发送给客户端，可以附加一个trailing元数据一起发送</li>
<li>如果响应码为ok，客户端获取响应并处理</li>
</ol>
<div class="blog_h2"><span class="graybg">服务端流RPC</span></div>
<p>客户端发起单个请求，然后获得一个流，服务端通过此流连续的发送多个响应消息：</p>
<pre class="crayon-plain-tag">rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}</pre>
<div class="blog_h3"><span class="graybg">生命周期</span></div>
<p>和一元RPC类似，但是服务器会发生响应消息的流。当所有响应消息发送完毕后，状态、可选的trailing元数据被发送</p>
<div class="blog_h3"><span class="graybg">示例代码</span></div>
<p>服务器：</p>
<pre class="crayon-plain-tag">func (ms *MediaService) NewPublished(from *google_protobuf.Timestamp, stream digital.MediaService_NewPublishedServer) error {
    medias, error := mediaRepo.queryPublishedFrom(time.Unix(from.Seconds, int64(from.Nanos)))
    for _, media := range *medias {
        stream.Send(&amp;media)
    }
    return error
}</pre>
<p>客户端：</p>
<pre class="crayon-plain-tag">func TestMediaNewPubRPC(t *testing.T) {
    conn, _ := grpc.Dial("localhost:7700", grpc.WithInsecure())
    defer conn.Close()
    client := digital.NewMediaServiceClient(conn)
    from, _ := now.Parse("2017-01-01")
    stream, err := client.NewPublished(context.Background(), &amp;protobuf.Timestamp{Seconds: int64(from.Unix())})
    fatalIf(err)
    for {
        media, err := stream.Recv()
        if err == io.EOF {
            break
        }
        log.Println(media.GetTitle())
    }
}</pre>
<div class="blog_h2"><span class="graybg">客户端流RPC</span></div>
<p>客户端发起一系列的请求消息，并等待服务器的单个响应消息：</p>
<pre class="crayon-plain-tag">rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}</pre>
<div class="blog_h3"><span class="graybg">生命周期</span></div>
<p>和一元RPC类似，但是：</p>
<ol>
<li>客户端发送请求消息的流</li>
<li>服务器可以在尚未接收全部请求消息的情况下给出响应</li>
</ol>
<div class="blog_h2"><span class="graybg">双向流RPC</span></div>
<p>客户端、服务器都使用流来收发消息，请求流、响应流独立运行。消息处理方式很自由：</p>
<ol>
<li>服务器可以等待，接收到客户端的所有消息后，进行处理并产生响应</li>
<li>服务器可以读取到一个消息，然后写入一个响应</li>
<li>其它任意的组合方式 </li>
</ol>
<p>每个流的消息的顺序是被保证的。</p>
<pre class="crayon-plain-tag">rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}</pre>
<div class="blog_h2"><span class="graybg">同步和异步</span></div>
<p>同步化的RPC调用会一直阻塞，直到响应到达客户端，这种通信模型是典型RPC所期望的。</p>
<p>但是，网络传输天生是异步的，在很多情况下，启动一个RPC调用但不阻塞当前线程，可以增强系统容量。gRPC为大部分语言提供了两套（同步/异步）编程接口。</p>
<div class="blog_h2"><span class="graybg">Deadline/超时</span></div>
<p>gRPC允许客户端指定，等待RPC调用完成的最长时间，如果超时未完成会出现<pre class="crayon-plain-tag">DEADLINE_EXCEEDED</pre>错误。在服务器端，可以查看每个RPC调用是否已经超时，还有多久超时。</p>
<p>在Go语言中，超时通过Context设置：</p>
<pre class="crayon-plain-tag">ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

client.GetUser(context.Background(), &amp;userRequest)</pre>
<p>在Java语言中，调用Stub的方法设置超时：</p>
<pre class="crayon-plain-tag">blockingStub = UserServiceGrpc
        .newBlockingStub(channel)
        .withDeadlineAfter(3, TimeUnit.SECONDS);</pre>
<div class="blog_h2"><span class="graybg">调用终结</span></div>
<p>在gRPC中，客户端、服务器分别独立的在本地判定调用是否成功 —— 两者的结论可能不一致。服务器端成功但是客户端没有收到响应是可能的。</p>
<p>在任何时候，客户端、服务器都可以取消RPC调用。</p>
<div class="blog_h2"><span class="graybg">元数据</span></div>
<p>元数据是和某次特定的RPC调用相关的，键值对的列表。其中键为字符串，值通常也是字符串。</p>
<p>元数据包括的信息例如：身份验证的详细信息</p>
<div class="blog_h1"><span class="graybg">ProtoBuf</span></div>
<p>参考：<a href="/intro-to-protocol-buffers">Protocol Buffers初探</a></p>
<div class="blog_h1"><span class="graybg"><a id="keepalive"></a>保活</span></div>
<p>gRPC支持使用<a href="/http-study-note#http2">HTTP/2</a>的PING帧实现保活。</p>
<div class="blog_h2"><span class="graybg">客户端配置</span></div>
<p>创建客户端时，你可以：</p>
<pre class="crayon-plain-tag">var kacp = keepalive.ClientParameters{
	Time:                10 * time.Second, // 如果没有活动，每10秒发送PING帧
	Timeout:             time.Second,      // 等待接收到PING的ACK的超时，超时后认为连接坏掉了
	PermitWithoutStream: true,             // 即使没有活动的stream，也发送PING帧
}
//Dial 中传入 keepalive 配置
conn, err := grpc.Dial(*addr, grpc.WithInsecure(), grpc.WithKeepaliveParams(kacp))</pre>
<div class="blog_h2"><span class="graybg">服务器配置</span></div>
<p>创建服务器时，你可以： </p>
<pre class="crayon-plain-tag">var kaep = keepalive.EnforcementPolicy{
// 客户端在发送Keepalive PING之前，需要等待的最小时间，默认5分钟
	MinTime:             5 * time.Minute,
// 即使没有活动的流，也允许客户端发送PING。如果设置为false，客户端在没有流的情况下发送PING，
// 服务器会答复GOAWAY，并终止HTTP/2连接
	PermitWithoutStream: true,
}

var kasp = keepalive.ServerParameters{
// 连接最大空闲时间，超过此时间服务器主动发送GOAWAY
// 空闲的起点：从连接建立开始、或者没有正常处理中的RPC调用的那一刻开始算
	MaxConnectionIdle:     15 * time.Second,
// 一个连接可以最大存活的时间，超过此时间（服务器增加10%抖动），则发送GOWAY
	MaxConnectionAge:      30 * time.Second,
// 超过MaxConnectionAge之后，再等待MaxConnectionAgeGrace才强制关闭连接
	MaxConnectionAgeGrace: 5 * time.Second,
// 如果经过下面这么长时间，服务器没有看到任何活动，发送PING来探测客户端是否还活着
	Time:                  5 * time.Second,
// 等待上述PING的超时，超时后关闭连接
	Timeout:               1 * time.Second,
}

func main(){
	...
	s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
	...
}</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">负载均衡</span></div>
<p> 大型的gRPC部署场景中，每个RPC服务都会有多个实例。负载均衡负责把客户端负载优化的（根据服务器能力）</p>
<div class="blog_h2"><span class="graybg">LB实现方式</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong>代理负载均衡（服务器端负载均衡）</strong></p>
<p>这种方式下，客户端向负载均衡代理发送请求，后者将请求转发给后端服务。代理负责跟踪每个后端服务的负载情况，并实现适当的负载均衡算法</p>
<p>这种实现方式常用于面向用户的服务，例如向移动客户端提供的API</p>
<p>优势：</p>
<ol>
<li>简单的客户端实现，客户端不需要了解服务器集群的细节</li>
<li>可以和不受信任的客户端很好的工作</li>
</ol>
<p>劣势：</p>
<ol>
<li>更高的网络延迟</li>
<li>负载均衡器的吞吐能力限制扩容性</li>
</ol>
<p>代理负载均衡可以基于L3/L4（传输）层实现，也可以基于L7（应用）层实现</p>
<p>传输层LB需要进行很少的处理，因而引入<span style="background-color: #c0c0c0;">较小的额外延迟、CPU消耗也低</span></p>
<p>应用层LB需要解析客户端的HTTP/2协议请求，LB可以<span style="background-color: #c0c0c0;">根据请求的细节来</span>分配适当的后端（例如Session Affinity）。LB需要向后端发起新的HTTP/2请求，然后把从客户端接收到的HTTP/2流转发给后端</p>
</td>
</tr>
<tr>
<td>
<p><strong>客户端负载均衡</strong></p>
<p>这种方式下，客户端知晓多个后端服务实例。后端实例向客户端报告其负载情况，客户端的LB算法依赖于这些负载信息。在简单的场景下，不考虑服务器端负载，客户端仅仅使用Round-robin算法（可以带权重）</p>
<p>优势：</p>
<ol>
<li>性能更好，因为减少了网络跳数</li>
</ol>
<p>劣势：</p>
<ol>
<li>客户端实现更加复杂：需要跟踪服务端负载和健康状况、需要实现负载均衡算法</li>
</ol>
<p>客户端负载均衡有两种实现方式：胖客户端、后备负载均衡</p>
<p>胖客户端内置了负载均衡算法的实现</p>
<p>后备负载均衡器，也叫外部负载均衡器（External load balancer）。客户端查询后备负载均衡器获得至少一个后端服务地址，后者维护后端服务状态并实现LB算法。gRPC定义了一个模型，用于客户端和后备负载均衡器的通信</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">库</span></div>
<div class="blog_h2"><span class="graybg">grpc-go</span></div>
<p>这是Go语言的gRPC库。本文Go语言的例子都是基于此库。</p>
<div class="blog_h3"><span class="graybg">并发性</span></div>
<p>ClientConn可以被跨Goroutine安全的访问。但是在我们的一个项目中，长期存在的ClientConn导致很高的CPU占用，原因未知。</p>
<p>使用Stream时，避免从不通Goroutine多次调用同一个Stream的SendMsg、RecvMsg方法。具体来说：</p>
<ol>
<li>对于同一个Stream，从一个Goroutine调用SendMsg，另一个调用RecvMsg是安全的</li>
<li>对于同一个Stream，分别从两个Goroutine调用SendMsg，或者RecvMsg是不安全的</li>
</ol>
<p>在服务器端，每个注册到gRPC服务器的RPC <span style="background-color: #c0c0c0;">Handler</span>，都在<span style="background-color: #c0c0c0;">独立的Goroutine中运行</span>。</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">Expected message type</span></div>
<p>RPC服务的参数、返回值都必须是消息类型，不能是原始类型。</p>
<div class="blog_h2"><span class="graybg">如何返回列表</span></div>
<div class="blog_h3"><span class="graybg">返回stream类型</span></div>
<p>示例：</p>
<pre class="crayon-plain-tag">service MediaService {
    rpc NewPublished (google.protobuf.Timestamp) returns (stream Media);
}</pre>
<p>使用这种方式时，客户端会得到一个迭代器。在服务器尚未把响应完全发送之前，客户端就可以开始处理。</p>
<div class="blog_h3"><span class="graybg">返回消息包装</span></div>
<p>示例：</p>
<pre class="crayon-plain-tag">message MediaResp {
    repeated Media medias = 1;
}
service MediaService {
    rpc NewPublished (google.protobuf.Timestamp) returns (stream Media);
}</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/grpc-study-note">gRPC学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/grpc-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Dubbo知识集锦</title>
		<link>https://blog.gmem.cc/dubbo-faq</link>
		<comments>https://blog.gmem.cc/dubbo-faq#comments</comments>
		<pubDate>Tue, 09 Jan 2018 16:12:54 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[RPC]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=17205</guid>
		<description><![CDATA[<p>简介 RPC 节选自：https://dubbo.apache.org/zh-cn/blog/rpc-introduction.html，稍作改动。 定义 RPC（Remote Procedure Call）即远程过程调用，它是一种通过网络从远程计算机程序上请求服务，而不需要了解底层网络技术的协议。也就是说两台服务器A，B，一个应用部署在A服务器上，想要调用B服务器上应用提供的方法，由于不在一个内存空间，不能直接调用，需要通过网络来表达调用的语义和传达调用的数据。 RPC协议假定某些传输协议的存在，如TCP或UDP，为通信程序之间携带信息数据。在OSI网络通信模型中，RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。现在业界有很多开源的优秀 RPC 框架，例如 Spring Cloud、Dubbo、Thrift 等。 起源 RPC 这个概念术语在上世纪 80 年代由 Bruce Jay Nelson 提出。在 <a class="read-more" href="https://blog.gmem.cc/dubbo-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/dubbo-faq">Dubbo知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">RPC</span></div>
<p>节选自：<a href="https://dubbo.apache.org/zh-cn/blog/rpc-introduction.html">https://dubbo.apache.org/zh-cn/blog/rpc-introduction.html</a>，稍作改动。</p>
<div class="blog_h3"><span class="graybg">定义</span></div>
<p>RPC（Remote Procedure Call）即远程过程调用，它是一种通过网络<span style="background-color: #c0c0c0;">从远程计算机程序上请求服务</span>，而<span style="background-color: #c0c0c0;">不需要了解底层网络技术</span>的协议。也就是说两台服务器A，B，一个应用部署在A服务器上，想要调用B服务器上应用提供的方法，由于不在一个内存空间，不能直接调用，需要通过网络来表达调用的语义和传达调用的数据。</p>
<p>RPC协议假定某些传输协议的存在，如TCP或UDP，为通信程序之间携带信息数据。在OSI网络通信模型中，RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。现在业界有很多开源的优秀 RPC 框架，例如 Spring Cloud、Dubbo、Thrift 等。</p>
<div class="blog_h3"><span class="graybg">起源</span></div>
<p>RPC 这个概念术语在上世纪 80 年代由 Bruce Jay Nelson 提出。在 Nelson 的论文 "Implementing Remote Procedure Calls" 中提到了RPC的几点优势：</p>
<ol>
<li>简单：RPC 概念的语义十分清晰和简单，这样建立分布式计算就更容易</li>
<li>高效：过程调用看起来十分简单而且高效</li>
<li>通用：在单机计算中过程往往是不同算法部分间最重要的通信机制</li>
</ol>
<p>通俗一点说，就是一般程序员<span style="background-color: #c0c0c0;">对于本地的过程调用很熟悉</span>，那么我们把 RPC 作成和本地调用完全类似，那么就更容易被接受，使用起来毫无障碍。Nelson 的论文发表于 30 年前，其观点今天看来确实高瞻远瞩，今天我们使用的 RPC 框架基本就是按这个目标来实现的。</p>
<div class="blog_h3"><span class="graybg">架构</span></div>
<p>Nelson 的论文中的RPC包含以下角色：</p>
<ol>
<li>User（客户端）：像调用本地服务似的调用远程服务</li>
<li>User-stub：接收到调用后，将方法、参数序列化；接收到结果消息，并进行解码（将结果消息反序列化）</li>
<li>RPCRuntime</li>
<li>Server-stub：收到消息后进行解码（将消息对象反序列化）、根据解码结果调用本地的服务、将返回结果打包成消息（将结果消息对象序列化）</li>
<li>Server（服务器）实现服务逻辑</li>
</ol>
<p>它们之间的交互流程如下：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/05/rpc-structure-1.png"><img class="wp-image-32297 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/05/rpc-structure-1.png" alt="rpc-structure-1" width="940" height="391" /></a>RPC框架通常会细化出以下组件：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/05/rpc-structure-2.png"><img class="wp-image-32303 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/05/rpc-structure-2.png" alt="rpc-structure-2" width="936" height="472" /></a></p>
<p>各组件职责：</p>
<ol>
<li>RpcServer：负责导出（export）远程接口</li>
<li>RpcClient：负责导入（import）远程接口的代理实现</li>
<li>RpcProxy：远程接口的代理实现</li>
<li>RpcInvoker：
<ol>
<li>客户方实现：负责编码调用信息和发送调用请求到服务方并等待调用结果返回</li>
<li>服务方实现：负责调用服务端接口的具体实现并返回调用结果</li>
</ol>
</li>
<li>RpcProtocol：负责协议编/解码</li>
<li>RpcConnector：负责维持客户方和服务方的连接通道和发送数据到服务方</li>
<li>RpcAcceptor：负责接收客户方请求并返回请求结果</li>
<li>RpcProcessor：负责在服务方控制调用过程，包括管理调用线程池、超时时间等</li>
<li>RpcChannel：数据传输通道</li>
</ol>
<div class="blog_h1"><span class="graybg">编程接口</span></div>
<div class="blog_h2"><span class="graybg">消费者</span></div>
<p>下面是一段示例代码，以编程的方式创建消费者，并获取RPC接口的Stub：</p>
<pre class="crayon-plain-tag">ApplicationConfig application = new ApplicationConfig();
application.setName( "ITokenVerifyApiStressor" );

RegistryConfig registry = new RegistryConfig();
registry.setAddress( "zookeeper://10.255.223.119:2881" );

ReferenceConfig&lt;ITokenVerifyApi&gt; reference = new ReferenceConfig&lt;ITokenVerifyApi&gt;();
reference.setApplication( application );
reference.setRegistry( registry );
reference.setInterface( ITokenVerifyApi.class );
reference.setVersion( "5.0" );

ITokenVerifyApi api = reference.get();</pre>
<div class="blog_h2"><span class="graybg">提供者</span></div>
<pre class="crayon-plain-tag">// 服务的实现
UploadService uploadService = new UpdateServiceImpl();

// Dubbo应用程序信息
ApplicationConfig application = new ApplicationConfig();
application.setName( "UploadServiceProvider" );

// 注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress( "zookeeper://10.255.223.119:2881" );

// 服务提供者协议配置
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName( "dubbo" );
protocol.setPort( 20880 );
protocol.setThreads( 8 );
protocol.setPayload( 8388608 / 8 / 1024 );

// 暴露服务配置
ServiceConfig&lt;UploadService&gt; service = new ServiceConfig&lt;UploadService&gt;();
service.setApplication( application );
service.setRegistry( registry );
service.setProtocol( protocol );
service.setInterface( UploadService.class );
service.setRef( uploadService );
service.setVersion( "1.0" );
service.export();</pre>
<div class="blog_h1"><span class="graybg">集成Spring</span></div>
<div class="blog_h2"><span class="graybg">POM</span></div>
<pre class="crayon-plain-tag">&lt;parent&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
        &lt;version&gt;2.0.5.RELEASE&lt;/version&gt;
    &lt;/parent&gt;

    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.spring.boot&lt;/groupId&gt;
            &lt;artifactId&gt;dubbo-spring-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;2.0.0&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;com.github.sgroschupf&lt;/groupId&gt;
            &lt;artifactId&gt;zkclient&lt;/artifactId&gt;
            &lt;version&gt;0.1&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;

    &lt;build&gt;
        &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;</pre>
<div class="blog_h2"><span class="graybg">提供者</span></div>
<div class="blog_h3"><span class="graybg">application.properties</span></div>
<pre class="crayon-plain-tag">spring.application.name=dubbo-exporter
spring.dubbo.server=true
spring.dubbo.registry=zookeeper://10.108.94.255:2181</pre>
<div class="blog_h3"><span class="graybg">入口点</span></div>
<pre class="crayon-plain-tag">@SpringBootApplication
@EnableDubboConfiguration
public class Application {

    public static void main( String[] args ) {
        SpringApplication.run( Application.class, args );
    }
}</pre>
<div class="blog_h3"><span class="graybg">提供者类</span></div>
<pre class="crayon-plain-tag">@Service( interfaceClass = MonitorService.class )
@Component
public class MonitorServiceImpl implements MonitorService {
} </pre>
<div class="blog_h2"><span class="graybg">注解驱动</span></div>
<div class="blog_h3"><span class="graybg">@EnableDubbo</span></div>
<p>该注解是 @EnableDubboConfig 和 @DubboComponentScan两者的组合：</p>
<pre class="crayon-plain-tag">@EnableDubboConfig
@DubboComponentScan
public @interface EnableDubbo {
    // 扫描 @Service 的基包
    @AliasFor(annotation = DubboComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

    // 扫描 @Service 的基包（指定的类所在的包）
    @AliasFor(annotation = DubboComponentScan.class, attribute = "basePackageClasses")
    Class&lt;?&gt;[] scanBasePackageClasses() default {};    
}</pre>
<div class="blog_h3"><span class="graybg">@Service</span></div>
<p>用来配置服务提供方： </p>
<pre class="crayon-plain-tag">package org.apache.dubbo.config.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE}) 
@Inherited
public @interface Service {
    // 实现的 interface 的类
    Class&lt;?&gt; interfaceClass() default void.class;
    // 实现的 interface 的类名
    String interfaceName() default "";
    // 服务的版本号
    String version() default "";
    // 服务的分组
    String group() default "";
    // 是否暴露服务
    boolean export() default true;
    // 是否向注册中心注册服务
    boolean register() default true;
    // 应用程序名称
    String application() default "";
    // 模块名称
    String module() default "";
    // 提供者名称
    String provider() default "";
    // 协议配置
    String[] protocol() default {};
    // 监控中心配置
    String monitor() default "";
    // 注册中心配置
    String[] registry() default {};
}</pre>
<div class="blog_h3"><span class="graybg">@Reference</span></div>
<p>用来配置消费者： </p>
<pre class="crayon-plain-tag">package org.apache.dubbo.config.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 
public @interface Reference {
    Class&lt;?&gt; interfaceClass() default void.class; 
    String interfaceName() default ""; 
    String version() default "";
    String group() default "";
    String url() default "";
    
    String application() default "";
    String module() default "";
    String consumer() default "";
    String protocol() default "";
    String monitor() default "";
    String[] registry() default {};
} </pre>
<div class="blog_h2"><span class="graybg">集成Hystrix</span></div>
<p>Hystrix 旨在通过控制那些访问远程系统、服务和第三方库的节点，从而<span style="background-color: #c0c0c0;">对延迟和故障提供更强大的容错能力</span>。Hystrix具备拥有回退机制和断路器功能的线程和信号隔离，请求缓存和请求打包，以及监控和配置等功能。</p>
<div class="blog_h3"><span class="graybg">启用Hystrix</span></div>
<p>spring boot官方提供了对hystrix的集成，直接在pom.xml里加入依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
    &lt;artifactId&gt;spring-cloud-starter-netflix-hystrix&lt;/artifactId&gt;
    &lt;version&gt;1.4.4.RELEASE&lt;/version&gt;
&lt;/dependency&gt;</pre>
<p>然后在Application类上增加@EnableHystrix来启用：</p>
<pre class="crayon-plain-tag">@SpringBootApplication
@EnableHystrix
public class ProviderApplication {
}</pre>
<div class="blog_h3"><span class="graybg">配置提供者</span></div>
<pre class="crayon-plain-tag">@Service(version = "1.0.0")
public class HelloServiceImpl implements HelloService {
    @HystrixCommand(commandProperties = {
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000") })
    @Override
    public String sayHello(String name) {
        throw new RuntimeException("Exception to show hystrix enabled.");
    }
}</pre>
<div class="blog_h3"><span class="graybg">配置消费者</span></div>
<pre class="crayon-plain-tag">@Reference(version = "1.0.0")
private HelloService demoService;

// 降级
@HystrixCommand(fallbackMethod = "reliable")
public String doSayHello(String name) {
    return demoService.sayHello(name);
}
public String reliable(String name) {
    return "hystrix fallback value";
}</pre>
<div class="blog_h1"><span class="graybg">新特性</span></div>
<div class="blog_h2"><span class="graybg">2.7.5</span></div>
<div class="blog_h3"><span class="graybg">粗粒度服务注册</span></div>
<p>新的服务定义/注册机制称为“服务自省”，一个应用只需要注册一条记录，解决了服务推送的性能瓶颈。</p>
<div class="blog_h3"><span class="graybg">gRPC协议支持</span></div>
<p>Dubbo RPC 协议是构建在 TCP 之上，这有很多优势也有一些缺点，缺点比如<span style="background-color: rgb(192, 192, 192);">通用性、协议穿透性不强</span>，对<span style="background-color: rgb(192, 192, 192);">多语言实现不够友好</span>等。</p>
<p>HTTP/2 由于其标准 HTTP 协议的属性，无疑将具有更好的通用性，现在或将来在各层网络设备上肯定都会得到很好的支持，gRPC 之所以选在 HTTP/2 作为传输层载体很大程度上也是因为这个因素。</p>
<p>Dubbo 支持 gRPC 协议带来的直观好处有：</p>
<ol>
<li>正式支持基于 HTTP/2 的远程通信，在协议通用性和穿透性上进一步提升</li>
<li>支持<span style="background-color: rgb(192, 192, 192);">跨进程的 Stream 流式通信，支持 Reactive 风格的 RPC 编程</span></li>
<li>解决了 gRPC 框架难以直接用于微服务开发的问题，将其纳入 Dubbo 的服务治理体系</li>
<li>为连接组织内部已有的 gRPC 或多语言体系提供支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Protobuf支持</span></div>
<p>支持 Protobuf 是为了解决Dubbo的跨语言、易用性问题。协议上 2.7.5 版本支持了 gRPC，而关于服务定义与序列化，Protobuf 则提供了很好的解决方案：</p>
<ol>
<li>服务定义：当前 Dubbo 的服务定义和具体的编程语言绑定，没有提供一种语言中立的服务描述格式，比如 Java 就是定义 Interface 接口，到了其他语言又得重新以另外的格式定义一遍。因此 Dubbo 通过<span style="background-color: rgb(192, 192, 192);">支持 Protobuf 实现了语言中立的服务定义</span></li>
<li>序列化。Dubbo 当前支持的序列化包括 Json、Hessian2、Kryo、FST、Java 等，而这其中<span style="background-color: rgb(192, 192, 192);">支持跨语言的只有 Json、Hessian2</span>，通用的 Json 有固有的性能问题，而 <span style="background-color: rgb(192, 192, 192);">Hessian2 无论在效率还是多语言 SDK 方面都有所欠缺</span>。为此，Dubbo 通过支持 Protobuf 序列化来提供更高效、易用的跨语言序列化方案</li>
</ol>
<div class="blog_h3"><span class="graybg">调用链路优化</span></div>
<p>QPS 性能提升将近 30%、减少了调用过程中的内存分配开销。</p>
<div class="blog_h3"><span class="graybg">Bootstrap API</span></div>
<pre class="crayon-plain-tag">ProtocolConfig protocolConfig = new ProtocolConfig("grpc");
protocolConfig.setSslEnabled(true);

SslConfig sslConfig = new SslConfig();
sslConfig.setXxxCert(...);

DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application(new ApplicationConfig("ssl-provider"))
  .registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
  .protocol(protocolConfig)
  .ssl(sslConfig);

ServiceConfig&lt;GreetingsService&gt; service1 = new ServiceConfig&lt;&gt;();
ServiceConfig&lt;GreetingsService&gt; service2 = new ServiceConfig&lt;&gt;();

bootstrap.service(service1).service(service2);
bootstrap.start();</pre>
<p>在引入 Dubbo Bootstrap 后，新的编程模型变得更简单，并且也为解决了缺少实例级启动入口的问题。 </p>
<div class="blog_h2"><span class="graybg">2.7</span></div>
<div class="blog_h3"><span class="graybg">异步改造</span></div>
<p>直接支持CompletableFuture。</p>
<div class="blog_h3"><span class="graybg">三大中心改造</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 150px;"><strong>注册中心</strong></td>
<td>简化配置模式下，仅仅发布必要的信息到配置中心：<br />
<pre class="crayon-plain-tag">&lt;dubbo:registry address=“zookeeper://127.0.0.1:2181” simplified="true"/&gt;</pre>
</td>
</tr>
<tr>
<td><strong>元数据中心</strong></td>
<td>
<p>包括服务接口名，重试次数，版本号等信息。在2.7之前，元数据存放在注册中心中，导致问题：</p>
<p style="padding-left: 30px;">推送量大 -&gt; 存储数据量大 -&gt; 网络传输量大 -&gt; 延迟严重</p>
<p>生产者端注册 30+ 参数，有接近一半是不需要传递给注册中心；消费者端注册 25+ 参数，只有个别需要传递给注册中心</p>
<p>2.7仅仅将必要的数据发布到注册中心，全量元数据存放到元数据中心。元数据中心支持Redis、ZooKeeper：</p>
<pre class="crayon-plain-tag">&lt;dubbo:metadata-report address="zookeeper://127.0.0.1:2181"/&gt;</pre>
</td>
</tr>
<tr>
<td><strong>配置中心</strong></td>
<td>
<p>Spring Cloud Config、Apollo、Nacos 等分布式配置中心组件，都关注以下角度：
<ol>
<li>分布式配置统一管理</li>
<li>动态变更推送</li>
<li>安全性</li>
</ol>
<p>Dubbo的2.7 之前的版本中，在 zookeeper 中设置了部分节点：configurators、routers，用于管理部分配置和路由信息，可以看作是Dubbo配置中心的雏形</p>
<p>2.7开始，正式支持配置中心，可以对接到Zookeeper、Apollo、Nacos，配置中心的职责：</p>
<ol>
<li>外部化配置。启动配置的集中式存储</li>
<li>服务治理。服务治理规则的存储与通知</li>
</ol>
<p>&nbsp;</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">服务治理增强</span></div>
<p>Dubbo的发展路线是作为服务治理框架，而非简单的RPC框架。在 2.7 中，Dubbo 对其服务治理能力进行了增强，增加了标签路由的能力，并抽象出了应用路由和服务路由的概念。</p>
<p>标签路由可以实现灰度发布。你可以在SPI扩展、过滤器中对请求打标签：<pre class="crayon-plain-tag">RpcContext.getContext().setAttachment()</pre>，这样会优先选择匹配的提供者。</p>
<div class="blog_h1"><span class="graybg">高级特性</span></div>
<div class="blog_h2"><span class="graybg">泛化调用</span></div>
<p>泛化调用主要用于消费端没有 API 接口的情况，不需要引入接口 jar 包，而是直接通过 GenericService 接口来发起服务调用，参数及返回值中的所有 POJO 均用 Map 表示。</p>
<p>你需要声明服务引用为泛化的：</p>
<pre class="crayon-plain-tag">&lt;dubbo:reference id="userService" interface="com.alibaba.dubbo.samples.generic.api.IUserService" 
    generic="true"/&gt;</pre>
<p>执行泛化调用的代码：</p>
<pre class="crayon-plain-tag">GenericService userService = (GenericService) context.getBean("userService");
//                                          方法名    参数类型数组                     参数值数组
String name = (String) userService.$invoke("delete", new String[]{int.class.getName()}, new Object[]{1});
System.out.println(name); </pre>
<div class="blog_h2"><span class="graybg">本地调用</span></div>
<p>从 2.2.0 版本开始，Dubbo 默认<span style="background-color: #c0c0c0;">在本地以 injvm 的方式暴露服务</span>，在同一个进程里对这个服务的调用会<span style="background-color: #c0c0c0;">优先走本地调用</span>。</p>
<p>本地调用可以被明确的关闭掉：</p>
<pre class="crayon-plain-tag">&lt;dubbo:service interface="org.apache.dubbo.samples.local.api.DemoService" 
    ref="target" scope="remote"/&gt;
                 &lt;!-- 显式关闭 --&gt;</pre>
<p>你可以通过URL来明确进行本地调用</p>
<pre class="crayon-plain-tag">&lt;dubbo:reference id="demoService" interface="org.apache.dubbo.samples.local.api.DemoService" 
    url="injvm://127.0.0.1/org.apache.dubbo.samples.local.api.DemoService"/&gt;</pre>
<div class="blog_h2"><span class="graybg">异步增强</span></div>
<p>在2.6.x及之前的版本提供了一定的异步编程能力，包括Consumer端异步调用、参数回调、事件通知。这些老版本中的异步调用存在缺陷：</p>
<ol>
<li>Future获取方式不够直接，需要通过RpcContext获取Future</li>
<li>Future接口无法实现自动回调，而自定义ResponseFuture虽支持回调但支持的异步场景有限，如不支持Future间的相互协调或组合</li>
<li>不支持Provider端异步</li>
</ol>
<p>2.7.0升级了对Java 8的支持，基于CompletableFuture对当前的异步功能进行了增强，现在服务可以直接返回CompletableFuture：</p>
<pre class="crayon-plain-tag">public interface AsyncService {
    CompletableFuture&lt;String&gt; sayHello(String name);
}</pre>
<p>如果不想将接口的返回值定义为Future类型，或者存在定义好的同步类型接口，可以重载现有的服务方法： </p>
<pre class="crayon-plain-tag">public interface GreetingsService {
    String sayHi(String name);
    // 为了保证方法级服务治理规则依然有效，建议保持方法名不变: sayHi
    // 使用default实现，避免给服务端提供者带来额外实现成本
    // placeholer只是为了实现重载而增加
    default CompletableFuture&lt;String&gt; sayHi(String name, boolean placeholer) {
      return CompletableFuture.completedFuture(sayHello(name));
    }
}</pre>
<div class="blog_h3"><span class="graybg">提供者示例</span></div>
<pre class="crayon-plain-tag">public class AsyncServiceImpl implements AsyncService {
    public CompletableFuture&lt;String&gt; sayHello(String name) {
        return CompletableFuture.supplyAsync(() -&gt; {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "async response from provider.";
        });
    }
}</pre>
<div class="blog_h3"><span class="graybg">消费者示例</span></div>
<pre class="crayon-plain-tag">public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
            new String[]{"META-INF/spring/async-consumer.xml"});
        context.start();
        final AsyncService asyncService = (AsyncService) context.getBean("asyncService");
    
        CompletableFuture&lt;String&gt; future = asyncService.sayHello("async call request");
        future.whenComplete((v, t) -&gt; {
            if (t != null) {
                t.printStackTrace();
            } else {
                System.out.println("Response: " + v);
            }
        });
        System.out.println("Executed before response return.");
        System.in.read();
}</pre>
<div class="blog_h3"><span class="graybg">过滤器链问题</span></div>
<p>采用异步调用后，由于异步结果在异步线程中单独执行，所以流经后半段Filter链的Result是空值，当真正的结果返回时已无法被Filter链处理。为了解决这个问题，2.7.0中为Filter增加了回调接口onResponse：</p>
<pre class="crayon-plain-tag">@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})
public class AsyncPostprocessFilter implements Filter {

    @Override
    public Result invoke(Invoker&lt;?&gt; invoker, Invocation invocation) throws RpcException {
        return invoker.invoke(invoker, invocation);
    }

    @Override
    public Result onResponse(Result result, Invoker&lt;?&gt; invoker, Invocation invocation) {
        System.out.println("Filter get the return value: " + result.getValue());
        return result;
    }
}</pre>
<div class="blog_h3"><span class="graybg">上下文传递问题</span></div>
<p>你需要在切换业务线程前自己完成Context的传递： </p>
<pre class="crayon-plain-tag">public class AsyncServiceImpl implements AsyncService {
    // 保存当前线程的上下文
    RpcContext context = RpcContext.getContext();
    public CompletableFuture&lt;String&gt; sayHello(String name) {
        return CompletableFuture.supplyAsync(() -&gt; {
            // 设置到新线程中
            RpcContext.setContext(context);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "async response from provider.";
        });
    }
}</pre>
<p>AsyncContext也提供了signalContextSwitch()的方法来实现方便的Context切换： </p>
<pre class="crayon-plain-tag">public class AsyncServiceImpl implements AsyncService {
    public String sayHello(String name) {
        final AsyncContext asyncContext = RpcContext.startAsync();
        new Thread(() -&gt; {
            asyncContext.signalContextSwitch();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            asyncContext.write("Hello " + name + ", response from provider.");
        }).start();
        return null;
    }
} </pre>
<div class="blog_h2"><span class="graybg">服务分组</span></div>
<p>当一个接口有<span style="background-color: #c0c0c0;">多种实现</span>时，可以用 group 区分：</p>
<pre class="crayon-plain-tag">&lt;!-- 提供者 --&gt;
&lt;dubbo:service group="feedback" interface="com.xxx.IndexService" /&gt;
&lt;dubbo:service group="member" interface="com.xxx.IndexService" /&gt;

&lt;!-- 消费者 --&gt;
&lt;dubbo:reference id="feedbackIndexService" group="feedback" interface="com.xxx.IndexService" /&gt;
&lt;dubbo:reference id="memberIndexService" group="member" interface="com.xxx.IndexService" /&gt;
&lt;!-- 不限制组 --&gt;
&lt;dubbo:reference id="barService" interface="com.foo.BarService" group="*" /&gt;</pre>
<div class="blog_h3"><span class="graybg">分组聚合</span></div>
<p>消费者可以同时调用不同分组中的同一服务，并将结果聚合：</p>
<pre class="crayon-plain-tag">&lt;dubbo:reference interface="com.xxx.MenuService" group="*" merger="true" /&gt;

&lt;dubbo:reference interface="com.xxx.MenuService" group="aaa,bbb" merger="true" /&gt;</pre>
<div class="blog_h2"><span class="graybg">多版本</span></div>
<p>当接口实现出现不兼容升级时，可以用版本号过渡，<span style="background-color: #c0c0c0;">版本号不同的服务相互间不引用</span>。版本迁移推荐步骤：</p>
<ol>
<li>在低压力时间段，先升级一半提供者为新版本</li>
<li>再将所有消费者升级为新版本</li>
<li>然后将剩下的一半提供者升级为新版本</li>
</ol>
<p>如果不需要区分版本，可以这样配置消费者：</p>
<pre class="crayon-plain-tag">&lt;dubbo:reference id="barService" interface="com.foo.BarService" version="*" /&gt;</pre>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">2.5.3无法打印日志</span></div>
<p>报错信息：log4j:WARN No appenders could be found for logger (com.alibaba.dubbo.common.logger.LoggerFactory).<br />log4j:WARN Please initialize the log4j system properly.</p>
<p>解决方案：添加JVM参数<pre class="crayon-plain-tag">-Ddubbo.application.logger=slf4j</pre></p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/dubbo-faq">Dubbo知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/dubbo-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
