Skip to content

20如何在微服务框架中集成etcd?

上一讲我们介绍了基于 etcd 实现微服务注册与发现的案例。由于服务实例是动态部署的,每个服务实例的地址和服务信息都可能动态变化,势必需要一个中心化的组件对各个服务实例的信息进行管理。该组件管理了各个部署好的服务实例元数据,包括但不限于服务名、IP 地址、端口号、服务描述和服务状态等。

现有的主流微服务框架大都集成了服务注册与发现的功能,这一讲我们就来介绍并实践如何集成 etcd 到主流的 Go 微服务框架中。

go-micro 集成 etcd

在构建微服务时,使用服务发现可以减少配置的复杂性,go-micro 也是 Go 语言中常用的微服务框架。go-micro 的发现机制是可插拔的,支持多种组件,如 etcd 和 ZooKeeper 等,具体详见micro/go-plugins

go-micro 介绍

首先介绍一下 go-micro 微服务框架。go-micro 是一个可插拔的 RPC 框架,用于分布式系统的开发,具有以下特性。

  • 服务发现(Service Discovery):自动服务注册与名称解析。

  • 负载均衡(Load Balancing):在服务发现之上构建了智能的负载均衡机制。

  • 同步通信(Synchronous Comms):基于 RPC 的通信,支持双向流。

  • 异步通信(Asynchronous Comms):内置发布/订阅的事件驱动架构。

  • 消息编码(Message Encoding):基于 Content-Type 的动态编码,支持 ProtoBuf、JSON,开箱即用。

  • 服务接口(Service Interface):所有特性都被打包在简单且高级的接口中,方便开发微服务。

go-micro 旨在利用接口使微服务架构抽象化,并且提供了一系列默认且完整的开箱即用的插件。

定义消息格式

go-micro 使用 ProtoBuf 定义消息格式。我们创建一个类型为 proto 的文件 hi.proto,其中定义了调用接口的参数以及返回的对象:

go
syntax = "proto3";
package hello;
service Greeter {
    rpc Hello(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
    string from = 1;
    string to = 2;
    string msg = 3;
}
message HelloResponse {
    string from = 1;
    string to = 2;
    string msg = 3;
}

如上的代码定义了 Greeter 的接口,Hello 方法的参数为 HelloRequest ,结果返回了 HelloResponse 对象。

接着生成 API 接口。我们需要使用 protoc 来生成 protobuf 代码文件,以此生成对应的 Go 语言代码。包括如下的三个插件:

  • protoc

  • protoc-gen-go

  • protoc-gen-micro

使用如下命令分别安装这几个插件:

java
go get github.com/golang/protobuf/{proto,protoc-gen-go}
go get github.com/micro/protoc-gen-micro

接着在当前目录下运行如下的命令,生成两个模板文件:

java
 $ protoc  --micro_out=. --go_out=. greeter.proto

运行之后,当前目录的结构如下所示:

java
$ tree     
.
├── hello.pb.go
├── hello.pb.micro.go
└── hello.proto

可以看到,我们通过工具生成了两个文件,一个是 Go 结构文件,另一个属于 go-micro RPC 的接口文件。基于生成的两个文件,我们可以创建"打招呼"的请求。下面是部分生成的代码:

go
// Greeter service 客户端的 API
type GreeterService interface {
	Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error)
}
type greeterService struct {
	c    client.Client
	name string
}
func NewGreeterService(name string, c client.Client) GreeterService {
	if c == nil {
		c = client.NewClient()
	}
	if len(name) == 0 {
		name = "hello"
	}
	return &greeterService{
		c:    c,
		name: name,
	}
}
func (c *greeterService) Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error) {
	req := c.c.NewRequest(c.name, "Greeter.Hello", in)
	out := new(HelloResponse)
	err := c.c.Call(ctx, req, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}
// Greeter service 服务端
type GreeterHandler interface {
	Hello(context.Context, *HelloRequest, *HelloResponse) error
}
func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error {
	type greeter interface {
		Hello(ctx context.Context, in *HelloRequest, out *HelloResponse) error
	}
	type Greeter struct {
		greeter
	}
	h := &greeterHandler{hdlr}
	return s.Handle(s.NewHandler(&Greeter{h}, opts...))
}
type greeterHandler struct {
	GreeterHandler
}
func (h *greeterHandler) Hello(ctx context.Context, in *HelloRequest, out *HelloResponse) error {
	return h.GreeterHandler.Hello(ctx, in, out)
}

gRPC 的调用方法装在生成的 go-micro RPC 的接口文件中。为了演示,我们只定义了一个Hello接口,可以看到上面的代码实现还是比较简单的。

server 服务端

下面我们开始实现服务端,服务端需要注册 handlers 处理器,用以对外提供服务并接收请求。服务端的具体实现代码如下所示:

go
package main
import (
	"context"
	hello "github.com/keets2012/etcd-book-code/ch10/micro/srv/proto"
	"log"
	"github.com/micro/go-micro"
	"github.com/micro/go-micro/registry"
	"github.com/micro/go-plugins/registry/etcdv3"
)
type Greet struct{}
func (s *Greet) Hello(ctx context.Context, req *hello.HelloRequest, rsp *hello.HelloResponse) error {
	log.Printf("received req %#v \n", req)
	rsp.From = "server"
	rsp.To = "client"
	rsp.Msg = "ok"
	return nil
}
func main() {
	reg := etcdv3.NewRegistry(func(op *registry.Options) {
		op.Addrs = []string{"127.0.0.1:2379",
		}
	})
	service := micro.NewService(
		micro.Name("hello.srv.say"),
		micro.Registry(reg),
	)
	service.Init()
  // 注册 GreeterHandler,传入服务和处理器
	hello.RegisterGreeterHandler(service.Server(), new(Greet))
  // 运行服务
	if err := service.Run(); err != nil {
		panic(err)
	}
}

micro.NewService 用于初始化服务,然后返回一个 Service 接口的实例。

上述实现中,使用 etcd 替换了默认的 Consul 作为服务注册与发现组件。处理器会与服务一起被注册,就像 HTTP 处理器一样,通过调用 server.Run 服务启动,同时绑定代码配置中的地址作为接收请求的地址。服务启动时向注册中心注册自身服务的相关信息,并在接收到关闭信号时注销。

client 调用

下面我们来看客户端如何调用。客户端应用发起到服务端的远程调用请求,实现客户端与服务端"打招呼"的功能,代码如下所示:

go
package main
import (
	"context"
	hello "github.com/keets2012/etcd-book-code/ch10/micro/srv/proto"
	"log"
	"github.com/micro/go-micro"
	"github.com/micro/go-micro/registry"
	"github.com/micro/go-plugins/registry/etcdv3"
)
func main() {
	reg := etcdv3.NewRegistry(func(op *registry.Options) {
		op.Addrs = []string{
			"127.0.0.1:2379",
		}
	})
	//创建 service
	service := micro.NewService(
		micro.Registry(reg),
	)
	service.Init()
	 // 创建 greet 客户端,需要传入服务名与服务客户端方法构建的对象
	greetClient := hello.NewGreeterService("hello.srv.say", service.Client())
	param := &hello.HelloRequest{
		From: "client",
		To:   "server",
		Msg:  "hello aoho",
	}
	rsp, err := greetClient.Hello(context.Background(), param)
	if err != nil {
		panic(err)
	}
	log.Println(rsp)
}

proto 生成的 RPC 接口已经将调用方法的流程封装好。hello.NewGreeterService需要使用服务名与客户端对象来请求指定的接口,即hello.srv.say,然后调用 Hello 方法。

go
func (c *sayService) Hello(ctx context.Context, in *SayParam, opts ...client.CallOption) (*SayResponse, error) {
    req := c.c.NewRequest(c.name, "Say.Hello", in)
    out := new(SayResponse)
    err := c.c.Call(ctx, req, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

主要的流程都在 c.c.Call 方法里。我们简单梳理一下整个流程,首先得到服务节点的地址,根据该地址查询连接池里是否有连接,如果有则取出来,如果没有则创建。然后进行数据传输,传输完成后把 client 连接放回到连接池内。

运行结果

上述操作实现了客户端与服务端的"打招呼"功能,下面我们分别运行服务端和客户端的应用程序,注意执行的先后顺序,得到的结果如下所示:

java
// 服务端的控制台输出
2021-03-16 23:00:23.365137 I | Transport [http] Listening on [::]:65331
2021-03-16 23:00:23.365230 I | Broker [http] Connected to [::]:65332
2021-03-16 23:00:23.365474 I | Registry [etcd] Registering node: hello.srv.say-6407b896-66d4-4cb1-81fd-d743ff6a97ec
2021-03-16 23:01:16.946948 I | received req &hello.SayRequest{From:"client", To:"server", Msg:"hello aoho", XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
//客户端的控制台输出
2021-03-16 23:01:16.947531 I | from:"server" to:"client" msg:"ok"

依次启动服务端、客户端,客户端发起一个打招呼的请求给服务端,可以看到服务端的控制台输出了收到的请求,并返回了 ok 响应给到客户端,符合我们的实现预期。

至此,我们成功在 go-micro 框架中集成了 etcd 作为服务注册与发现组件。

Go-kit 集成 etcd

介绍完 go-micro 集成 etcd,我们来看另一个流行的 Go 微服务框架 Go-kit 如何集成 etcd。

Go-kit 介绍

Go-kit 提供了用于实现系统监控和弹性模式组件的库,例如日志记录、跟踪、限流和熔断等,这些库协助工程师提高微服务架构的性能和稳定性。Go-kit 框架分层如下图所示。

Go-kit 框架分层图

除了用于构建微服务的工具包,Go-kit 还为工程师提供了良好的架构设计原则示范。Go-kit 提倡工程师使用 Alistair Cockburn 提出的 SOLID 设计原则、领域驱动设计(DDD)。所以 Go-kit 不仅仅是微服务工具包,它也非常适合构建优雅的整体结构。

Go-kit 提供了三层模型来解耦业务,这也是我们使用它的主要目的,模型由上到下分别是transport -> endpoint -> service

  • 传输层用于网络通信,服务通常使用 HTTP、gRPC 等网络传输方式,或使用 NATS 等发布订阅系统相互通信。除此之外,Go-kit 还支持使用 AMQP 和 Thrift 等多种网络通信模式。

  • 接口层是服务器和客户端的基本构建模块。在 Go-kit 中,每个对外提供的服务接口方法都会定义为一个端点(Endpoint),以便在服务器和客户端之间进行网络通信。每个端点利用传输层通过使用 HTTP 或 gRPC 等具体通信模式对外提供服务。

  • 服务层是具体的业务逻辑实现。服务层的业务逻辑包含核心业务逻辑,即你要实现的主要功能。它不会也不应该进行 HTTP 或 gRPC 等具体网络传输,或者请求和响应消息类型的编码和解码。

Go-kit 在性能和扩展性等方面表现优异。下面我们就来介绍如何在 Go-kit 中集成 etcd 作为服务注册与发现组件,以及构建用户登录的场景、用户登录系统之后获取认证的令牌,接着实现 Go-kit 的 gRPC 调用。

定义消息格式

Go-kit 的消息通信也是基于 protobuf 格式。这里我们定义了两个 proto,其中一个定义了登录的 RPC 请求和响应的结构体,另一个则定义了 RPC 请求的方法。分别如下:

java
// user.proto
syntax = "proto3";
package pb;
message Login {
    string Account = 1;
    string Password = 2;
}
message LoginAck {
    string Token = 1;
}
user.proto 定义了 Login 请求和 LoginAck 应答的结构体
// service.proto
syntax = "proto3";
package pb;
import "user.proto";
service User {
    rpc RpcUserLogin (Login) returns (LoginAck) {
    }
}

service.proto 引用了 user.proto 中定义的结构体,定义了一个方法 RpcUserLogin,请求参数为 Login 对象,响应结果为 LoginAck。

生成对应的 gRPC pb 文件,执行如下的命令:

java
$ protoc --go_out=plugins=grpc:. *.proto

生成 pb 文件后,目录中增加了两个文件,文件结构如下:

java
$ tree
.
├── make.sh
├── service.pb.go
├── service.proto
├── user.pb.go
└── user.proto

生成的文件基于 gRPC 调用的标准格式生成,这里就不具体列出了。我们接着看 user 服务的实现。

user 服务

由于 user 服务的实现代码比较多,这里我侧重讲解 Go-kit 集成使用 etcd 部分。我们先来看 user 服务的入口主函数:

go
var grpcAddr = flag.String("g", "127.0.0.1:8881", "grpcAddr")
var quitChan = make(chan error, 1)
func main() {
	flag.Parse()
	var (
		etcdAddrs = []string{"127.0.0.1:2379"}
		serName   = "svc.user.agent"
		grpcAddr  = *grpcAddr
		ttl       = 5 * time.Second
	)
	utils.NewLoggerServer()
	// 初始化 etcd 客户端
	options := etcdv3.ClientOptions{
		DialTimeout:   ttl,
		DialKeepAlive: ttl,
	}
	etcdClient, err := etcdv3.NewClient(context.Background(), etcdAddrs, options)
	if err != nil {
		utils.GetLogger().Error("[user_agent]  NewClient", zap.Error(err))
		return
	}
  // 基于 etcdClient 初始化 Registar
	Registar := etcdv3.NewRegistrar(etcdClient, etcdv3.Service{
		Key:   fmt.Sprintf("%s/%s", serName, grpcAddr),
		Value: grpcAddr,
	}, log.NewNopLogger())
	go func() {
		golangLimit := rate.NewLimiter(10, 1)
		server := src.NewService(utils.GetLogger())
		endpoints := src.NewEndPointServer(server, golangLimit)
        // 构造 EndPointServer
		grpcServer := src.NewGRPCServer(endpoints, utils.GetLogger())
        // 监听 tcp 地址和端口
		grpcListener, err := net.Listen("tcp", grpcAddr)
		if err != nil {
			utils.GetLogger().Warn("[user_agent] Listen", zap.Error(err))
			quitChan <- err
			return
		}
		Registar.Register()
		baseServer := grpc.NewServer(grpc.UnaryInterceptor(grpctransport.Interceptor))
		pb.RegisterUserServer(baseServer, grpcServer)
		if err = baseServer.Serve(grpcListener); err != nil {
			utils.GetLogger().Warn("[user_agent] Serve", zap.Error(err))
			quitChan <- err
			return
		}
	}()
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
		quitChan <- fmt.Errorf("%s", <-c)
	}()
	utils.GetLogger().Info("[user_agent] run " + grpcAddr)
	err = <-quitChan
    // 注销连接
	Registar.Deregister()
	utils.GetLogger().Info("[user_agent] quit err", zap.Error(err))
}

user 服务集成 etcd 的主要步骤如下:

  • 初始化 etcd 客户端;

  • 基于 etcdClient 初始化 Registar;

  • Registar.Register() 注册 user 服务到 etcd,RegisterService 将服务及其实现注册到 gRPC 服务器,必须在调用服务之前调用 RegisterService;

  • 服务关闭时,注销 etcd 连接。

客户端调用

在微服务架构中,用户登录的操作,一般由 user 服务校验其身份信息的合法性,如果合法则为该用法返回认证的令牌。我们的测试客户端就是模拟 auth 认证服务的实现。

go
func TestNewUserAgentClient(t *testing.T) {
  // 初始化 UserAgent,返回的是一个 UserAgent
	client, err := NewUserAgentClient([]string{"127.0.0.1:2379"}, logger)
	if err != nil {
		t.Error(err)
		return
	}
  // 循环调用,为了测试 user 多实例注册到 etcd,客户端调用的情况
	for i := 0; i < 6; i++ {
		time.Sleep(time.Second)
		userAgent, err := client.UserAgentClient()
		if err != nil {
			t.Error(err)
			return
		}
		ack, err := userAgent.Login(context.Background(), &pb.Login{
			Account:  "aoho",
			Password: "123456",
		})
		if err != nil {
			t.Error(err)
			return
		}
		t.Log(ack.Token)
	}
}

上述代码示例是测试的主要代码,首先读取配置,初始化 UserAgent,其实就是得到指定服务的一个 etcdv3 客户端实例。这里获取了 etcd 中键为svc.user.agent的值。

go
func NewUserAgentClient(addr []string, logger log.Logger) (*UserAgent, error) {
	var (
		etcdAddrs = addr
		serName   = "svc.user.agent"
		ttl       = 5 * time.Second
	)
	options := etcdv3.ClientOptions{
		DialTimeout:   ttl,
		DialKeepAlive: ttl,
	}
	etcdClient, err := etcdv3.NewClient(context.Background(), etcdAddrs, options)
	if err != nil {
		return nil, err
	}
	instancerm, err := etcdv3.NewInstancer(etcdClient, serName, logger)
	if err != nil {
		return nil, err
	}
	return &UserAgent{
		instancerm: instancerm,
		logger:     logger,
	}, err
}

在 NewUserAgentClient 的实现中,根据传入的 etcdAddrs 构建 etcdClient,并通过 etcdClient 和 serName 构建 instancerm,指向的类型为 Instancer。

go
type Instancer struct {
	cache  *instance.Cache
	client Client
	prefix string
	logger log.Logger
	quitc  chan struct{}
}

Instancer 选出存储在 etcd 键空间中的实例。同时将 watch 该键空间中的任何事件类型的更改,这些更改将更新实例器的实例信息。

至此,我们实现了 user 服务和调用 user 服务的客户端测试方法。

运行结果

我们启动 3 个服务地址,分别为:127.0.0.1:8881、127.0.0.1:8882、127.0.0.1:8883。

shell
$ ./user_agent -g 127.0.0.1:8881
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
$ ./user_agent -g 127.0.0.1:8882
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
$ ./user_agent -g 127.0.0.1:8883
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

依次运行服务端和测试函数,可以得到如下的结果:

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

根据测试函数的运行结果,svc.user.agent 有三个服务实例。客户端 6 次调用 user 服务的登录结果都是成功的,TestNewUserAgentClient 输出了获取到的 JWT Token。同时在启动的三个 user 服务端控制台输出了如下的日志信息:

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}

从上面的日志信息可以知道,客户端根据 etcd 中存储的实例信息发起调用,成功实现了负载均衡。如果我们关闭某一个实例,客户端会监测到服务实例的变更,本地的服务实例列表会踢掉该实例,这种机制使得 Go-kit 的负载均衡依然奏效。

小结

这一讲我们主要介绍了在常见的两种微服务框架 go-micro 和 Go-kit 中集成 etcd 作为服务注册与发现组件。go-micro 把分布式系统的各种细节抽象出来,方便我们进行组件切换。go-micro 的新版本工具集弃用了 Consul,建议使用 etcd。Go-kit 是 Go 语言工具包的集合,可以帮助你构建强大、可靠和可维护的微服务,不过 Go 目前还不支持泛型,interface 的定义相对来说也比较烦琐。

本讲内容总结如下:

总的来说,两个微服务框架都支持方便地集成 etcd,但是微服务框架本身也有优缺点。通过两个常用的微服务框架集成 etcd 的案例学习,可以帮助你对 etcd 的使用有一个更深的理解,在此基础上自行封装适合业务场景的框架。

最后,我们来做一个互动:你在项目中使用的是哪种服务发现与注册组件,又使用什么样的微服务框架呢?欢迎你在留言区和我分享。下一讲我们将介绍 etcd 在 Kubernetes 中如何保证容器的调度。