<p data-nodeid="355">上一讲我们介绍了基于 etcd 实现微服务注册与发现的案例。由于服务实例是动态部署的，每个服务实例的地址和服务信息都可能动态变化，势必需要一个中心化的组件对各个服务实例的信息进行管理。该组件管理了各个部署好的服务实例元数据，包括但不限于服务名、IP 地址、端口号、服务描述和服务状态等。</p>
<p data-nodeid="356">现有的主流微服务框架大都集成了服务注册与发现的功能，这一讲我们就来介绍并实践如何集成 etcd 到主流的 Go 微服务框架中。</p>
<h3 data-nodeid="357">go-micro 集成 etcd</h3>
<p data-nodeid="358">在构建微服务时，使用服务发现可以减少配置的复杂性，go-micro 也是 Go 语言中常用的微服务框架。go-micro 的发现机制是可插拔的，支持多种组件，如 etcd 和 ZooKeeper 等，具体详见<a href="https://github.com/micro/go-plugins?fileGuid=xxQTRXtVcqtHK6j8" data-nodeid="481">micro/go-plugins</a>。</p>
<h4 data-nodeid="359">go-micro 介绍</h4>
<p data-nodeid="360">首先介绍一下 go-micro 微服务框架。go-micro 是一个<strong data-nodeid="489">可插拔的 RPC 框架</strong>，用于分布式系统的开发，具有以下特性。</p>
<ul data-nodeid="361">
<li data-nodeid="362">
<p data-nodeid="363"><strong data-nodeid="494">服务发现</strong>（Service Discovery）：自动服务注册与名称解析。</p>
</li>
<li data-nodeid="364">
<p data-nodeid="365"><strong data-nodeid="499">负载均衡</strong>（Load Balancing）：在服务发现之上构建了智能的负载均衡机制。</p>
</li>
<li data-nodeid="366">
<p data-nodeid="367"><strong data-nodeid="504">同步通信</strong>（Synchronous Comms）：基于 RPC 的通信，支持双向流。</p>
</li>
<li data-nodeid="368">
<p data-nodeid="369"><strong data-nodeid="509">异步通信</strong>（Asynchronous Comms）：内置发布/订阅的事件驱动架构。</p>
</li>
<li data-nodeid="370">
<p data-nodeid="371"><strong data-nodeid="514">消息编码</strong>（Message Encoding）：基于 Content-Type 的动态编码，支持 ProtoBuf、JSON，开箱即用。</p>
</li>
<li data-nodeid="372">
<p data-nodeid="373"><strong data-nodeid="519">服务接口</strong>（Service Interface）：所有特性都被打包在简单且高级的接口中，方便开发微服务。</p>
</li>
</ul>
<p data-nodeid="374">go-micro 旨在利用接口使微服务架构抽象化，并且提供了一系列默认且完整的开箱即用的插件。</p>
<h4 data-nodeid="375">定义消息格式</h4>
<p data-nodeid="376">go-micro 使用 ProtoBuf 定义消息格式。我们创建一个类型为 proto 的文件 hi.proto，其中定义了调用接口的参数以及返回的对象：</p>
<pre class="lang-go" data-nodeid="377"><code data-language="go">syntax = <span class="hljs-string">"proto3"</span>;
<span class="hljs-keyword">package</span> hello;
service Greeter {
    rpc Hello(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
    <span class="hljs-keyword">string</span> from = <span class="hljs-number">1</span>;
    <span class="hljs-keyword">string</span> to = <span class="hljs-number">2</span>;
    <span class="hljs-keyword">string</span> msg = <span class="hljs-number">3</span>;
}
message HelloResponse {
    <span class="hljs-keyword">string</span> from = <span class="hljs-number">1</span>;
    <span class="hljs-keyword">string</span> to = <span class="hljs-number">2</span>;
    <span class="hljs-keyword">string</span> msg = <span class="hljs-number">3</span>;
}
</code></pre>
<p data-nodeid="378">如上的代码定义了 Greeter 的接口，Hello 方法的参数为 HelloRequest ，结果返回了 HelloResponse 对象。</p>
<p data-nodeid="379">接着生成 API 接口。我们需要使用 protoc 来生成 protobuf 代码文件，以此生成对应的 Go 语言代码。包括如下的三个插件：</p>
<ul data-nodeid="380">
<li data-nodeid="381">
<p data-nodeid="382">protoc</p>
</li>
<li data-nodeid="383">
<p data-nodeid="384">protoc-gen-go</p>
</li>
<li data-nodeid="385">
<p data-nodeid="386">protoc-gen-micro</p>
</li>
</ul>
<p data-nodeid="387">使用如下命令分别安装这几个插件：</p>
<pre class="lang-java" data-nodeid="388"><code data-language="java">go get github.com/golang/protobuf/{proto,protoc-gen-go}
go get github.com/micro/protoc-gen-micro
</code></pre>
<p data-nodeid="389">接着在当前目录下运行如下的命令，生成两个模板文件：</p>
<pre class="lang-java" data-nodeid="390"><code data-language="java"> $ protoc  --micro_out=. --go_out=. greeter.proto
</code></pre>
<p data-nodeid="391">运行之后，当前目录的结构如下所示：</p>
<pre class="lang-java" data-nodeid="392"><code data-language="java">$ tree     
.
├── hello.pb.go
├── hello.pb.micro.go
└── hello.proto
</code></pre>
<p data-nodeid="393">可以看到，我们通过工具生成了两个文件，一个是 Go 结构文件，另一个属于 go-micro RPC 的接口文件。基于生成的两个文件，我们可以创建“打招呼”的请求。下面是部分生成的代码：</p>
<pre class="lang-go" data-nodeid="394"><code data-language="go"><span class="hljs-comment">// Greeter service 客户端的 API</span>
<span class="hljs-keyword">type</span> GreeterService <span class="hljs-keyword">interface</span> {
	Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error)
}
<span class="hljs-keyword">type</span> greeterService <span class="hljs-keyword">struct</span> {
	c    client.Client
	name <span class="hljs-keyword">string</span>
}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">NewGreeterService</span><span class="hljs-params">(name <span class="hljs-keyword">string</span>, c client.Client)</span> <span class="hljs-title">GreeterService</span></span> {
	<span class="hljs-keyword">if</span> c == <span class="hljs-literal">nil</span> {
		c = client.NewClient()
	}
	<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(name) == <span class="hljs-number">0</span> {
		name = <span class="hljs-string">"hello"</span>
	}
	<span class="hljs-keyword">return</span> &amp;greeterService{
		c:    c,
		name: name,
	}
}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *greeterService)</span> <span class="hljs-title">Hello</span><span class="hljs-params">(ctx context.Context, in *HelloRequest, opts ...client.CallOption)</span> <span class="hljs-params">(*HelloResponse, error)</span></span> {
	req := c.c.NewRequest(c.name, <span class="hljs-string">"Greeter.Hello"</span>, in)
	out := <span class="hljs-built_in">new</span>(HelloResponse)
	err := c.c.Call(ctx, req, out, opts...)
	<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
		<span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
	}
	<span class="hljs-keyword">return</span> out, <span class="hljs-literal">nil</span>
}
<span class="hljs-comment">// Greeter service 服务端</span>
<span class="hljs-keyword">type</span> GreeterHandler <span class="hljs-keyword">interface</span> {
	Hello(context.Context, *HelloRequest, *HelloResponse) error
}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">RegisterGreeterHandler</span><span class="hljs-params">(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption)</span> <span class="hljs-title">error</span></span> {
	<span class="hljs-keyword">type</span> greeter <span class="hljs-keyword">interface</span> {
		Hello(ctx context.Context, in *HelloRequest, out *HelloResponse) error
	}
	<span class="hljs-keyword">type</span> Greeter <span class="hljs-keyword">struct</span> {
		greeter
	}
	h := &amp;greeterHandler{hdlr}
	<span class="hljs-keyword">return</span> s.Handle(s.NewHandler(&amp;Greeter{h}, opts...))
}
<span class="hljs-keyword">type</span> greeterHandler <span class="hljs-keyword">struct</span> {
	GreeterHandler
}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(h *greeterHandler)</span> <span class="hljs-title">Hello</span><span class="hljs-params">(ctx context.Context, in *HelloRequest, out *HelloResponse)</span> <span class="hljs-title">error</span></span> {
	<span class="hljs-keyword">return</span> h.GreeterHandler.Hello(ctx, in, out)
}
</code></pre>
<p data-nodeid="395">gRPC 的调用方法装在生成的 go-micro RPC 的接口文件中。为了演示，我们只定义了一个<code data-backticks="1" data-nodeid="533">Hello</code>接口，可以看到上面的代码实现还是比较简单的。</p>
<h4 data-nodeid="396">server 服务端</h4>
<p data-nodeid="397">下面我们开始实现服务端，服务端需要<strong data-nodeid="541">注册 handlers 处理器</strong>，用以对外提供服务并接收请求。服务端的具体实现代码如下所示：</p>
<pre class="lang-go" data-nodeid="398"><code data-language="go"><span class="hljs-keyword">package</span> main
<span class="hljs-keyword">import</span> (
	<span class="hljs-string">"context"</span>
	hello <span class="hljs-string">"github.com/keets2012/etcd-book-code/ch10/micro/srv/proto"</span>
	<span class="hljs-string">"log"</span>
	<span class="hljs-string">"github.com/micro/go-micro"</span>
	<span class="hljs-string">"github.com/micro/go-micro/registry"</span>
	<span class="hljs-string">"github.com/micro/go-plugins/registry/etcdv3"</span>
)
<span class="hljs-keyword">type</span> Greet <span class="hljs-keyword">struct</span>{}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *Greet)</span> <span class="hljs-title">Hello</span><span class="hljs-params">(ctx context.Context, req *hello.HelloRequest, rsp *hello.HelloResponse)</span> <span class="hljs-title">error</span></span> {
	log.Printf(<span class="hljs-string">"received req %#v \n"</span>, req)
	rsp.From = <span class="hljs-string">"server"</span>
	rsp.To = <span class="hljs-string">"client"</span>
	rsp.Msg = <span class="hljs-string">"ok"</span>
	<span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
	reg := etcdv3.NewRegistry(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(op *registry.Options)</span></span> {
		op.Addrs = []<span class="hljs-keyword">string</span>{<span class="hljs-string">"127.0.0.1:2379"</span>,
		}
	})
	service := micro.NewService(
		micro.Name(<span class="hljs-string">"hello.srv.say"</span>),
		micro.Registry(reg),
	)
	service.Init()
  <span class="hljs-comment">// 注册 GreeterHandler，传入服务和处理器</span>
	hello.RegisterGreeterHandler(service.Server(), <span class="hljs-built_in">new</span>(Greet))
  <span class="hljs-comment">// 运行服务</span>
	<span class="hljs-keyword">if</span> err := service.Run(); err != <span class="hljs-literal">nil</span> {
		<span class="hljs-built_in">panic</span>(err)
	}
}
</code></pre>
<p data-nodeid="399">micro.NewService 用于初始化服务，然后返回一个 Service 接口的实例。</p>
<p data-nodeid="400">上述实现中，使用 etcd 替换了默认的 Consul 作为服务注册与发现组件。处理器会与服务一起被注册，就像 HTTP 处理器一样，通过调用 server.Run 服务启动，同时绑定代码配置中的地址作为接收请求的地址。服务启动时向注册中心注册自身服务的相关信息，并在接收到关闭信号时注销。</p>
<h4 data-nodeid="401">client 调用</h4>
<p data-nodeid="402">下面我们来看客户端如何调用。客户端应用发起到服务端的远程调用请求，实现客户端与服务端“打招呼”的功能，代码如下所示：</p>
<pre class="lang-go" data-nodeid="403"><code data-language="go"><span class="hljs-keyword">package</span> main
<span class="hljs-keyword">import</span> (
	<span class="hljs-string">"context"</span>
	hello <span class="hljs-string">"github.com/keets2012/etcd-book-code/ch10/micro/srv/proto"</span>
	<span class="hljs-string">"log"</span>
	<span class="hljs-string">"github.com/micro/go-micro"</span>
	<span class="hljs-string">"github.com/micro/go-micro/registry"</span>
	<span class="hljs-string">"github.com/micro/go-plugins/registry/etcdv3"</span>
)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
	reg := etcdv3.NewRegistry(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(op *registry.Options)</span></span> {
		op.Addrs = []<span class="hljs-keyword">string</span>{
			<span class="hljs-string">"127.0.0.1:2379"</span>,
		}
	})
	<span class="hljs-comment">//创建 service</span>
	service := micro.NewService(
		micro.Registry(reg),
	)
	service.Init()
	 <span class="hljs-comment">// 创建 greet 客户端，需要传入服务名与服务客户端方法构建的对象</span>
	greetClient := hello.NewGreeterService(<span class="hljs-string">"hello.srv.say"</span>, service.Client())
	param := &amp;hello.HelloRequest{
		From: <span class="hljs-string">"client"</span>,
		To:   <span class="hljs-string">"server"</span>,
		Msg:  <span class="hljs-string">"hello aoho"</span>,
	}
	rsp, err := greetClient.Hello(context.Background(), param)
	<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
		<span class="hljs-built_in">panic</span>(err)
	}
	log.Println(rsp)
}
</code></pre>
<p data-nodeid="404">proto 生成的 RPC 接口已经将调用方法的流程封装好。<code data-backticks="1" data-nodeid="547">hello.NewGreeterService</code>需要使用服务名与客户端对象来请求指定的接口，即<code data-backticks="1" data-nodeid="549">hello.srv.say</code>，然后调用 Hello 方法。</p>
<pre class="lang-go" data-nodeid="405"><code data-language="go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *sayService)</span> <span class="hljs-title">Hello</span><span class="hljs-params">(ctx context.Context, in *SayParam, opts ...client.CallOption)</span> <span class="hljs-params">(*SayResponse, error)</span></span> {
    req := c.c.NewRequest(c.name, <span class="hljs-string">"Say.Hello"</span>, in)
    out := <span class="hljs-built_in">new</span>(SayResponse)
    err := c.c.Call(ctx, req, out, opts...)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }
    <span class="hljs-keyword">return</span> out, <span class="hljs-literal">nil</span>
}
</code></pre>
<p data-nodeid="406">主要的流程都在 c.c.Call 方法里。我们简单梳理一下整个流程，首先得到服务节点的地址，根据该地址查询连接池里是否有连接，如果有则取出来，如果没有则创建。然后进行数据传输，传输完成后把 client 连接放回到连接池内。</p>
<h4 data-nodeid="407">运行结果</h4>
<p data-nodeid="408">上述操作实现了客户端与服务端的“打招呼”功能，下面我们分别运行服务端和客户端的应用程序，注意执行的先后顺序，得到的结果如下所示：</p>
<pre class="lang-java" data-nodeid="409"><code data-language="java"><span class="hljs-comment">// 服务端的控制台输出</span>
<span class="hljs-number">2021</span>-<span class="hljs-number">03</span>-<span class="hljs-number">16</span> <span class="hljs-number">23</span>:<span class="hljs-number">00</span>:<span class="hljs-number">23.365137</span> I | Transport [http] Listening on [::]:<span class="hljs-number">65331</span>
<span class="hljs-number">2021</span>-<span class="hljs-number">03</span>-<span class="hljs-number">16</span> <span class="hljs-number">23</span>:<span class="hljs-number">00</span>:<span class="hljs-number">23.365230</span> I | Broker [http] Connected to [::]:<span class="hljs-number">65332</span>
<span class="hljs-number">2021</span>-<span class="hljs-number">03</span>-<span class="hljs-number">16</span> <span class="hljs-number">23</span>:<span class="hljs-number">00</span>:<span class="hljs-number">23.365474</span> I | Registry [etcd] Registering node: hello.srv.say-<span class="hljs-number">6407</span>b896-<span class="hljs-number">66</span>d4-<span class="hljs-number">4</span>cb1-<span class="hljs-number">81f</span>d-d743ff6a97ec
<span class="hljs-number">2021</span>-<span class="hljs-number">03</span>-<span class="hljs-number">16</span> <span class="hljs-number">23</span>:<span class="hljs-number">01</span>:<span class="hljs-number">16.946948</span> I | received req &amp;hello.SayRequest{From:<span class="hljs-string">"client"</span>, To:<span class="hljs-string">"server"</span>, Msg:<span class="hljs-string">"hello aoho"</span>, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:<span class="hljs-number">0</span>}
<span class="hljs-comment">//客户端的控制台输出</span>
<span class="hljs-number">2021</span>-<span class="hljs-number">03</span>-<span class="hljs-number">16</span> <span class="hljs-number">23</span>:<span class="hljs-number">01</span>:<span class="hljs-number">16.947531</span> I | from:<span class="hljs-string">"server"</span> to:<span class="hljs-string">"client"</span> msg:<span class="hljs-string">"ok"</span>
</code></pre>
<p data-nodeid="410">依次启动服务端、客户端，客户端发起一个打招呼的请求给服务端，可以看到服务端的控制台输出了收到的请求，并返回了 ok 响应给到客户端，符合我们的实现预期。</p>
<p data-nodeid="411">至此，我们成功在 go-micro 框架中集成了 etcd 作为服务注册与发现组件。</p>
<h3 data-nodeid="412">Go-kit 集成 etcd</h3>
<p data-nodeid="413">介绍完 go-micro 集成 etcd，我们来看另一个流行的 Go 微服务框架 Go-kit 如何集成 etcd。</p>
<h4 data-nodeid="414">Go-kit 介绍</h4>
<p data-nodeid="415">Go-kit 提供了用于实现<strong data-nodeid="564">系统监控和弹性模式组件</strong>的库，例如日志记录、跟踪、限流和熔断等，这些库协助工程师提高微服务架构的性能和稳定性。Go-kit 框架分层如下图所示。</p>
<p data-nodeid="2142" class=""><img src="https://s0.lgstatic.com/i/image6/M00/2C/97/CioPOWBlXcKAU3tWAAC2wBgqaYw872.png" alt="Drawing 0.png" data-nodeid="2146"></p>
<div data-nodeid="2143"><p style="text-align:center">Go-kit 框架分层图</p></div>



<p data-nodeid="418">除了用于构建微服务的工具包，Go-kit 还为工程师提供了良好的架构设计原则示范。Go-kit 提倡工程师使用 Alistair Cockburn 提出的 SOLID 设计原则、领域驱动设计（DDD）。所以 Go-kit 不仅仅是微服务工具包，它也非常适合构建优雅的整体结构。</p>
<p data-nodeid="419">Go-kit 提供了三层模型来解耦业务，这也是我们使用它的主要目的，模型由上到下分别是<code data-backticks="1" data-nodeid="571">transport -&gt; endpoint -&gt; service</code>。</p>
<ul data-nodeid="420">
<li data-nodeid="421">
<p data-nodeid="422">传输层用于网络通信，服务通常使用 HTTP、gRPC 等网络传输方式，或使用 NATS 等发布订阅系统相互通信。除此之外，Go-kit 还支持使用 AMQP 和 Thrift 等多种网络通信模式。</p>
</li>
<li data-nodeid="423">
<p data-nodeid="424">接口层是服务器和客户端的基本构建模块。在 Go-kit 中，每个对外提供的服务接口方法都会定义为一个端点（Endpoint），以便在服务器和客户端之间进行网络通信。每个端点利用传输层通过使用 HTTP 或 gRPC 等具体通信模式对外提供服务。</p>
</li>
<li data-nodeid="425">
<p data-nodeid="426">服务层是具体的业务逻辑实现。服务层的业务逻辑包含核心业务逻辑，即你要实现的主要功能。它不会也不应该进行 HTTP 或 gRPC 等具体网络传输，或者请求和响应消息类型的编码和解码。</p>
</li>
</ul>
<p data-nodeid="427">Go-kit 在性能和扩展性等方面表现优异。下面我们就来介绍如何在 Go-kit 中集成 etcd 作为服务注册与发现组件，以及构建用户登录的场景、用户登录系统之后获取认证的令牌，接着实现 Go-kit 的 gRPC 调用。</p>
<h4 data-nodeid="428">定义消息格式</h4>
<p data-nodeid="429">Go-kit 的消息通信也是基于 protobuf 格式。这里我们定义了两个 proto，其中一个定义了登录的 RPC 请求和响应的结构体，另一个则定义了 RPC 请求的方法。分别如下：</p>
<pre class="lang-java" data-nodeid="430"><code data-language="java"><span class="hljs-comment">// user.proto</span>
syntax = <span class="hljs-string">"proto3"</span>;
<span class="hljs-keyword">package</span> pb;
message Login {
    string Account = <span class="hljs-number">1</span>;
    string Password = <span class="hljs-number">2</span>;
}
message LoginAck {
    string Token = <span class="hljs-number">1</span>;
}
user.proto 定义了 Login 请求和 LoginAck 应答的结构体
<span class="hljs-comment">// service.proto</span>
syntax = <span class="hljs-string">"proto3"</span>;
<span class="hljs-keyword">package</span> pb;
<span class="hljs-keyword">import</span> <span class="hljs-string">"user.proto"</span>;
service User {
    <span class="hljs-function">rpc <span class="hljs-title">RpcUserLogin</span> <span class="hljs-params">(Login)</span> <span class="hljs-title">returns</span> <span class="hljs-params">(LoginAck)</span> </span>{
    }
}
</code></pre>
<p data-nodeid="431">service.proto 引用了 user.proto 中定义的结构体，定义了一个方法 RpcUserLogin，请求参数为 Login 对象，响应结果为 LoginAck。</p>
<p data-nodeid="432">生成对应的 gRPC pb 文件，执行如下的命令：</p>
<pre class="lang-java" data-nodeid="433"><code data-language="java">$ protoc --go_out=plugins=grpc:. *.proto
</code></pre>
<p data-nodeid="434">生成 pb 文件后，目录中增加了两个文件，文件结构如下：</p>
<pre class="lang-java" data-nodeid="435"><code data-language="java">$ tree
.
├── make.sh
├── service.pb.go
├── service.proto
├── user.pb.go
└── user.proto
</code></pre>
<p data-nodeid="436">生成的文件基于 gRPC 调用的标准格式生成，这里就不具体列出了。我们接着看 user 服务的实现。</p>
<h4 data-nodeid="437">user 服务</h4>
<p data-nodeid="438">由于 user 服务的实现代码比较多，这里我侧重讲解 Go-kit  集成使用 etcd 部分。我们先来看 user 服务的入口主函数：</p>
<pre class="lang-go" data-nodeid="439"><code data-language="go"><span class="hljs-keyword">var</span> grpcAddr = flag.String(<span class="hljs-string">"g"</span>, <span class="hljs-string">"127.0.0.1:8881"</span>, <span class="hljs-string">"grpcAddr"</span>)
<span class="hljs-keyword">var</span> quitChan = <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> error, <span class="hljs-number">1</span>)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
	flag.Parse()
	<span class="hljs-keyword">var</span> (
		etcdAddrs = []<span class="hljs-keyword">string</span>{<span class="hljs-string">"127.0.0.1:2379"</span>}
		serName   = <span class="hljs-string">"svc.user.agent"</span>
		grpcAddr  = *grpcAddr
		ttl       = <span class="hljs-number">5</span> * time.Second
	)
	utils.NewLoggerServer()
	<span class="hljs-comment">// 初始化 etcd 客户端</span>
	options := etcdv3.ClientOptions{
		DialTimeout:   ttl,
		DialKeepAlive: ttl,
	}
	etcdClient, err := etcdv3.NewClient(context.Background(), etcdAddrs, options)
	<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
		utils.GetLogger().Error(<span class="hljs-string">"[user_agent]  NewClient"</span>, zap.Error(err))
		<span class="hljs-keyword">return</span>
	}
  <span class="hljs-comment">// 基于 etcdClient 初始化 Registar</span>
	Registar := etcdv3.NewRegistrar(etcdClient, etcdv3.Service{
		Key:   fmt.Sprintf(<span class="hljs-string">"%s/%s"</span>, serName, grpcAddr),
		Value: grpcAddr,
	}, log.NewNopLogger())
	<span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
		golangLimit := rate.NewLimiter(<span class="hljs-number">10</span>, <span class="hljs-number">1</span>)
		server := src.NewService(utils.GetLogger())
		endpoints := src.NewEndPointServer(server, golangLimit)
        <span class="hljs-comment">// 构造 EndPointServer</span>
		grpcServer := src.NewGRPCServer(endpoints, utils.GetLogger())
        <span class="hljs-comment">// 监听 tcp 地址和端口</span>
		grpcListener, err := net.Listen(<span class="hljs-string">"tcp"</span>, grpcAddr)
		<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
			utils.GetLogger().Warn(<span class="hljs-string">"[user_agent] Listen"</span>, zap.Error(err))
			quitChan &lt;- err
			<span class="hljs-keyword">return</span>
		}
		Registar.Register()
		baseServer := grpc.NewServer(grpc.UnaryInterceptor(grpctransport.Interceptor))
		pb.RegisterUserServer(baseServer, grpcServer)
		<span class="hljs-keyword">if</span> err = baseServer.Serve(grpcListener); err != <span class="hljs-literal">nil</span> {
			utils.GetLogger().Warn(<span class="hljs-string">"[user_agent] Serve"</span>, zap.Error(err))
			quitChan &lt;- err
			<span class="hljs-keyword">return</span>
		}
	}()
	<span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
		c := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> os.Signal, <span class="hljs-number">1</span>)
		signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
		quitChan &lt;- fmt.Errorf(<span class="hljs-string">"%s"</span>, &lt;-c)
	}()
	utils.GetLogger().Info(<span class="hljs-string">"[user_agent] run "</span> + grpcAddr)
	err = &lt;-quitChan
    <span class="hljs-comment">// 注销连接</span>
	Registar.Deregister()
	utils.GetLogger().Info(<span class="hljs-string">"[user_agent] quit err"</span>, zap.Error(err))
}
</code></pre>
<p data-nodeid="440">user 服务集成 etcd 的主要步骤如下：</p>
<ul data-nodeid="441">
<li data-nodeid="442">
<p data-nodeid="443">初始化 etcd 客户端；</p>
</li>
<li data-nodeid="444">
<p data-nodeid="445">基于 etcdClient 初始化 Registar；</p>
</li>
<li data-nodeid="446">
<p data-nodeid="447">Registar.Register() 注册 user 服务到 etcd，RegisterService 将服务及其实现注册到 gRPC 服务器，必须在调用服务之前调用 RegisterService；</p>
</li>
<li data-nodeid="448">
<p data-nodeid="449">服务关闭时，注销 etcd 连接。</p>
</li>
</ul>
<h4 data-nodeid="450">客户端调用</h4>
<p data-nodeid="451">在微服务架构中，用户登录的操作，一般由 user 服务校验其身份信息的合法性，如果合法则为该用法返回认证的令牌。我们的测试客户端就是模拟 auth 认证服务的实现。</p>
<pre class="lang-go" data-nodeid="452"><code data-language="go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestNewUserAgentClient</span><span class="hljs-params">(t *testing.T)</span></span> {
  <span class="hljs-comment">// 初始化 UserAgent，返回的是一个 UserAgent</span>
	client, err := NewUserAgentClient([]<span class="hljs-keyword">string</span>{<span class="hljs-string">"127.0.0.1:2379"</span>}, logger)
	<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
		t.Error(err)
		<span class="hljs-keyword">return</span>
	}
  <span class="hljs-comment">// 循环调用，为了测试 user 多实例注册到 etcd，客户端调用的情况</span>
	<span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">6</span>; i++ {
		time.Sleep(time.Second)
		userAgent, err := client.UserAgentClient()
		<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
			t.Error(err)
			<span class="hljs-keyword">return</span>
		}
		ack, err := userAgent.Login(context.Background(), &amp;pb.Login{
			Account:  <span class="hljs-string">"aoho"</span>,
			Password: <span class="hljs-string">"123456"</span>,
		})
		<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
			t.Error(err)
			<span class="hljs-keyword">return</span>
		}
		t.Log(ack.Token)
	}
}
</code></pre>
<p data-nodeid="453">上述代码示例是测试的主要代码，首先读取配置，初始化 UserAgent，其实就是得到指定服务的一个 etcdv3 客户端实例。这里获取了 etcd 中键为<code data-backticks="1" data-nodeid="593">svc.user.agent</code>的值。</p>
<pre class="lang-go" data-nodeid="454"><code data-language="go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">NewUserAgentClient</span><span class="hljs-params">(addr []<span class="hljs-keyword">string</span>, logger log.Logger)</span> <span class="hljs-params">(*UserAgent, error)</span></span> {
	<span class="hljs-keyword">var</span> (
		etcdAddrs = addr
		serName   = <span class="hljs-string">"svc.user.agent"</span>
		ttl       = <span class="hljs-number">5</span> * time.Second
	)
	options := etcdv3.ClientOptions{
		DialTimeout:   ttl,
		DialKeepAlive: ttl,
	}
	etcdClient, err := etcdv3.NewClient(context.Background(), etcdAddrs, options)
	<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
		<span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
	}
	instancerm, err := etcdv3.NewInstancer(etcdClient, serName, logger)
	<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
		<span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
	}
	<span class="hljs-keyword">return</span> &amp;UserAgent{
		instancerm: instancerm,
		logger:     logger,
	}, err
}
</code></pre>
<p data-nodeid="455">在 NewUserAgentClient 的实现中，根据传入的 etcdAddrs 构建 etcdClient，并通过 etcdClient 和 serName 构建 instancerm，指向的类型为 Instancer。</p>
<pre class="lang-go" data-nodeid="456"><code data-language="go"><span class="hljs-keyword">type</span> Instancer <span class="hljs-keyword">struct</span> {
	cache  *instance.Cache
	client Client
	prefix <span class="hljs-keyword">string</span>
	logger log.Logger
	quitc  <span class="hljs-keyword">chan</span> <span class="hljs-keyword">struct</span>{}
}
</code></pre>
<p data-nodeid="457">Instancer 选出存储在 etcd 键空间中的实例。同时将 watch 该键空间中的任何事件类型的更改，这些更改将更新实例器的实例信息。</p>
<p data-nodeid="458">至此，我们实现了 user 服务和调用 user 服务的客户端测试方法。</p>
<h4 data-nodeid="459">运行结果</h4>
<p data-nodeid="460">我们启动 3 个服务地址，分别为：127.0.0.1:8881、127.0.0.1:8882、127.0.0.1:8883。</p>
<pre class="lang-shell" data-nodeid="461"><code data-language="shell"><span class="hljs-meta">$</span><span class="bash"> ./user_agent -g 127.0.0.1:8881</span>
2021-03-17 13:31:15     INFO    utils/log_util.go:89    [NewLogger] success
2021-03-17 13:31:15     INFO    user_agent/main.go:75   [user_agent] run 127.0.0.1:8881
<span class="hljs-meta">$</span><span class="bash"> ./user_agent -g 127.0.0.1:8882</span>
2021-03-17 13:31:12     INFO    utils/log_util.go:89    [NewLogger] success
2021-03-17 13:31:12     INFO    user_agent/main.go:75   [user_agent] run 127.0.0.1:8882
<span class="hljs-meta">$</span><span class="bash"> ./user_agent -g 127.0.0.1:8883</span>
2021-03-17 13:31:08     INFO    utils/log_util.go:89    [NewLogger] success
2021-03-17 13:31:08     INFO    user_agent/main.go:75   [user_agent] run 127.0.0.1:8883
</code></pre>
<p data-nodeid="462">依次运行服务端和测试函数，可以得到如下的结果：</p>
<pre class="lang-shell" data-nodeid="463"><code data-language="shell">=== RUN   TestNewUserAgentClient
ts=2021-03-17T05:31:22.605559Z caller=instancer.go:32 prefix=svc.user.agent instances=3
    TestNewUserAgentClient: user_agent_test.go:44: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiYW9obyIsIkRjSWQiOjEsImV4cCI6MTYwMDMyMDcxMywiaWF0IjoxNjAwMzIwNjgzLCJpc3MiOiJraXRfdjQiLCJuYmYiOjE2MDAzMjA2ODMsInN1YiI6ImxvZ2luIn0.Eo-uytDEuAJyPGooXB2mC6uga-C-krVdthEQSYkqG-k
    ...
--- PASS: TestNewUserAgentClient (6.11s)
PASS
</code></pre>
<p data-nodeid="464">根据测试函数的运行结果，svc.user.agent 有三个服务实例。客户端 6 次调用 user 服务的登录结果都是成功的，TestNewUserAgentClient 输出了获取到的 JWT Token。同时在启动的三个 user 服务端控制台输出了如下的日志信息：</p>
<pre class="lang-shell" data-nodeid="465"><code data-language="shell">// 8883
2021-03-17 13:31:24     DEBUG   src/middleware_server.go:31     [9f4221fd-ec8c-53f2-b2ac-26e9cb4501ba]  {"调用 Login logMiddlewareServer": "Login", "req": "Account:\"aoho\"assword:\"123456\" ", "res": "Token:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiYW9obyIsIkRjSWQiOjEsImV4cCI6MTYwMDMyMDcxNCwiaWF0IjoxNjAwMzIwNjg0LCJpc3MiOiJraXRfdjQiLCJuYmYiOjE2MDAzMjA2ODQsInN1YiI6ImxvZ2luIn0.atzewyzrwRtBVCCg_4eZo7iiJKXGV6nJs-_BA9JDSLQ\" ", "time": "188.861 µ s", "err": null}
// 8882
2021-03-17 13:31:26     DEBUG   src/middleware_server.go:31     [9ece68d5-9e56-515c-a417-77f371b04910]  {"调用 Login logMiddlewareServer": "Login", "req": "Account:\"aoho\"assword:\"123456\" ", "res": "Token:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiYW9obyIsIkRjSWQiOjEsImV4cCI6MTYwMDMyMDcxNiwiaWF0IjoxNjAwMzIwNjg2LCJpc3MiOiJraXRfdjQiLCJuYmYiOjE2MDAzMjA2ODYsInN1YiI6ImxvZ2luIn0.KLjK_mf11C_ssO_X5sKyzr55ftUEh2D5mfxS5xTKbP4\" ", "time": "195.477 µ s", "err": null}
2021-03-17 13:31:27     DEBUG   src/middleware_server.go:31     [de1d3e65-d389-5232-9254-33e4cb6c9060]  {"调用 Login logMiddlewareServer": "Login", "req": "Account:\"aoho\"assword:\"123456\" ", "res": "Token:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiYW9obyIsIkRjSWQiOjEsImV4cCI6MTYwMDMyMDcxNywiaWF0IjoxNjAwMzIwNjg3LCJpc3MiOiJraXRfdjQiLCJuYmYiOjE2MDAzMjA2ODcsInN1YiI6ImxvZ2luIn0.2jkryvYTJVnsrXuNWB_SyYqKxQB-l5dos7bGUP2aLyo\" ", "time": "104.817 µ s", "err": null}
// 8881
2021-03-17 13:31:23     DEBUG   src/middleware_server.go:31     [c521bfb2-5a48-58c8-aa74-fdf78adc443f]  {"调用 Login logMiddlewareServer": "Login", "req": "Account:\"aoho\"assword:\"123456\" ", "res": "Token:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiYW9obyIsIkRjSWQiOjEsImV4cCI6MTYwMDMyMDcxMywiaWF0IjoxNjAwMzIwNjgzLCJpc3MiOiJraXRfdjQiLCJuYmYiOjE2MDAzMjA2ODMsInN1YiI6ImxvZ2luIn0.Eo-uytDEuAJyPGooXB2mC6uga-C-krVdthEQSYkqG-k\" ", "time": "173.146 µ s", "err": null}
2021-03-17 13:31:25     DEBUG   src/middleware_server.go:31     [9ffc9f63-d925-5999-9b9b-2bf544654010]  {"调用 Login logMiddlewareServer": "Login", "req": "Account:\"aoho\"assword:\"123456\" ", "res": "Token:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiYW9obyIsIkRjSWQiOjEsImV4cCI6MTYwMDMyMDcxNSwiaWF0IjoxNjAwMzIwNjg1LCJpc3MiOiJraXRfdjQiLCJuYmYiOjE2MDAzMjA2ODUsInN1YiI6ImxvZ2luIn0.OwMi33WbWz4SuIIRsTO0uOzg2d7qx5CDyISetnsbiiE\" ", "time": "174.443 µ s", "err": null}
2021-03-17 13:31:28     DEBUG   src/middleware_server.go:31     [c5459a23-0999-5861-80d2-fea508815ac5]  {"调用 Login logMiddlewareServer": "Login", "req": "Account:\"aoho\"assword:\"123456\" ", "res": "Token:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiYW9obyIsIkRjSWQiOjEsImV4cCI6MTYwMDMyMDcxOCwiaWF0IjoxNjAwMzIwNjg4LCJpc3MiOiJraXRfdjQiLCJuYmYiOjE2MDAzMjA2ODgsInN1YiI6ImxvZ2luIn0.TR6gcjlZ7rb2PXQg5XJz1AX0cGJc706UAuT9VyWR1Wg\" ", "time": "68.345 µ s", "err": null}
</code></pre>
<p data-nodeid="466">从上面的日志信息可以知道，客户端根据 etcd 中存储的实例信息发起调用，成功实现了负载均衡。如果我们关闭某一个实例，客户端会监测到服务实例的变更，本地的服务实例列表会踢掉该实例，这种机制使得 Go-kit 的负载均衡依然奏效。</p>
<h3 data-nodeid="467">小结</h3>
<p data-nodeid="468">这一讲我们主要介绍了在常见的两种微服务框架 go-micro 和 Go-kit 中集成 etcd 作为服务注册与发现组件。go-micro 把分布式系统的各种细节抽象出来，方便我们进行组件切换。go-micro 的新版本工具集弃用了 Consul，建议使用 etcd。Go-kit 是 Go 语言工具包的集合，可以帮助你构建强大、可靠和可维护的微服务，不过 Go 目前还不支持泛型，interface 的定义相对来说也比较烦琐。</p>
<p data-nodeid="469">本讲内容总结如下：</p>
<p data-nodeid="2653" class="te-preview-highlight"><img src="https://s0.lgstatic.com/i/image6/M00/2C/97/CioPOWBlXdmAR6ozAAD6gNf9pMQ337.png" alt="Drawing 1.png" data-nodeid="2656"></p>

<p data-nodeid="471">总的来说，两个微服务框架都支持方便地集成 etcd，但是微服务框架本身也有优缺点。通过两个常用的微服务框架集成 etcd 的案例学习，可以帮助你对 etcd 的使用有一个更深的理解，在此基础上自行封装适合业务场景的框架。</p>
<p data-nodeid="472">最后，我们来做一个互动：你在项目中使用的是哪种服务发现与注册组件，又使用什么样的微服务框架呢？欢迎你在留言区和我分享。下一讲我们将介绍 etcd 在 Kubernetes 中如何保证容器的调度。</p>