consul+grpc最佳实践

一、概述

grpc概念

官网:gRPC

A high performance, open source universal RPC framework

RPC是什么

RPC是远程过程调用(Remote Procedure Call)。 RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制,让使用者不必显式的区分本地调用和远程调用

常见的rpc框架

  • Dubbo:国内最早开源的 RPC 框架,由阿里巴巴公司开发并于 2011 年末对外开源,仅支持 Java 语言。
  • Motan:微博内部使用的 RPC 框架,于 2016 年对外开源,仅支持 Java 语言。
  • Tars:腾讯内部使用的 RPC 框架,于 2017 年对外开源,仅支持 C++ 语言。
  • Spring Cloud:国外 Pivotal 公司 2014 年对外开源的 RPC 框架,提供了丰富的生态组件。
  • gRPC:Google 于 2015 年对外开源的跨语言 RPC 框架,支持多种语言。
  • Thrift:最初是由 Facebook 开发的内部系统跨语言的 RPC 框架,2007 年贡献给了 Apache 基金,成为Apache 开源项目之一,支持多种语言。

rpc框架特征

  • 1、RPC框架一般使用长链接,不必每次通信都要3次握手,减少网络开销。
  • 2、RPC框架一般都有注册中心,有丰富的监控管理。发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作协议私密,安全性较高
  • 3、RPC 协议更简单内容更小,效率更高,服务化架构、服务化治理,RPC框架是一个强力的支撑。

GRPC是什么

grpc的定义

gRPC是一种高性能、开源、通用的远程过程调用(RPC)框架,最初由Google开发。它使用Protocol Buffers作为接口描述语言(IDL)和数据序列化格式,并基于HTTP/2协议进行通信,提供了诸如双向流、流控、头部压缩等高级特性,可以在客户端和服务端之间快速、高效地传输数据和调用远程服务。

在Go语言中,gRPC提供了对Go的支持,使开发者可以方便地使用gRPC来构建高效的分布式系统。gRPC提供了自动生成的客户端和服务端的代码,以及相应的API,可以大大简化开发过程。同时,Go的并发特性和轻量级的协程模型也使得gRPC在Go语言中得到了广泛的应用。

grpc的架构

gRPC 的架构基于 RPC(Remote Procedure Call)模式,它由以下几个主要的组件构成:

  1. 服务定义(Service Definition):使用 Protocol Buffers(一种语言无关的数据序列化格式)定义服务接口,包括服务名称、方法名称、参数和返回值等信息。服务定义文件采用 .proto 后缀名。
  2. 服务器(Server):实现服务定义中定义的接口,处理客户端请求并提供服务。
  3. 客户端(Client):使用服务定义中定义的接口与服务器进行通信,发送请求并接收响应。
  4. 通信协议(Communication Protocol):gRPC 使用 HTTP/2 协议进行通信,HTTP/2 支持双向流、流控、头部压缩等特性,可以提高网络传输效率。
  5. 序列化和反序列化(Serialization and Deserialization):gRPC 使用 Protocol Buffers 进行数据序列化和反序列化,可以高效地压缩和传输数据。

在 gRPC 的架构中,服务定义是核心组件,它定义了客户端和服务器之间的通信接口。客户端和服务器通过通信协议进行通信,并使用序列化和反序列化来传输数据。服务器实现了服务定义中定义的接口,处理客户端请求并提供服务。客户端使用服务定义中定义的接口与服务器进行通信,发送请求并接收响应。

grpc的原理

gRPC 的原理可以概括为以下几点:

  1. 定义服务接口:使用 Protocol Buffers 语言定义服务接口,包括服务名称、方法名称、参数和返回值等信息。
  2. 自动生成代码:根据服务接口定义生成客户端和服务端的代码,包括数据结构和方法。
  3. 序列化数据:客户端将请求数据序列化为二进制格式,通过网络发送给服务端。
  4. 反序列化数据:服务端接收请求数据后进行反序列化,将二进制数据转化为数据结构。
  5. 执行服务方法:服务端根据方法名称和参数调用相应的服务方法,返回结果给客户端。
  6. 序列化响应数据:服务端将返回结果序列化为二进制格式,通过网络发送给客户端。
  7. 反序列化响应数据:客户端接收响应数据后进行反序列化,将二进制数据转化为数据结构。
  8. 处理结果:客户端根据响应数据处理结果。

gRPC 的原理基于 RPC(Remote Procedure Call)模式,它使用 Protocol Buffers 作为接口描述语言和数据序列化格式,并基于 HTTP/2 协议进行通信。客户端和服务端通过生成的代码进行交互,客户端将请求数据序列化为二进制格式,服务端进行反序列化后调用相应的服务方法,返回结果也是经过序列化的。由于使用了高效的数据序列化和网络传输协议,gRPC 可以在客户端和服务端之间快速、高效地传输数据和调用远程服务。

grpc的流程与例子

流程

如果前端使用 gRPC 发送了一个请求给后端,gRPC 框架会按照以下步骤进行处理:

  1. 客户端调用:前端调用客户端生成的 gRPC 方法,并传入相应的参数。
  2. 序列化和发送请求:客户端将请求数据序列化为二进制格式,并使用 HTTP/2 协议发送请求数据给后端。
  3. 接收和反序列化请求:后端接收请求数据,并对请求数据进行反序列化,将二进制数据转化为数据结构。
  4. 执行服务方法:后端根据方法名称和参数调用相应的服务方法,处理请求并返回结果。
  5. 序列化和发送响应:后端将返回结果序列化为二进制格式,并使用 HTTP/2 协议发送响应数据给前端。
  6. 接收和反序列化响应:前端接收响应数据,并对响应数据进行反序列化,将二进制数据转化为数据结构。
  7. 处理结果:前端根据响应数据处理结果,完成调用。

以上是 gRPC 处理前端请求的基本流程。在这个过程中,gRPC 框架会自动处理序列化、反序列化和通信协议等细节,开发者只需要专注于服务方法的实现即可

例子

假设前端需要查询用户信息,后端提供了一个 GetUserInfo 的 gRPC 服务方法,该方法需要接收一个 UserID 参数,返回一个 UserInfo 对象。具体流程如下:

  1. 客户端调用:前端调用 GetUserInfo 方法,并传入 UserID 参数。
  2. 序列化和发送请求:客户端使用 Protocol Buffers 序列化请求数据为二进制格式,并使用 HTTP/2 协议发送请求数据给后端。
  3. 接收和反序列化请求:后端接收请求数据,并使用 Protocol Buffers 反序列化请求数据,将二进制数据转化为 UserID 参数。
  4. 执行服务方法:后端调用 GetUserInfo 方法,根据 UserID 参数查询用户信息,并返回 UserInfo 对象。
  5. 序列化和发送响应:后端使用 Protocol Buffers 序列化 UserInfo 对象为二进制格式,并使用 HTTP/2 协议发送响应数据给前端。
  6. 接收和反序列化响应:前端接收响应数据,并使用 Protocol Buffers 反序列化响应数据,将二进制数据转化为 UserInfo 对象。
  7. 处理结果:前端根据 UserInfo 对象处理结果,完成调用。

在这个过程中,gRPC 框架会自动处理序列化、反序列化和通信协议等细节,开发者只需要实现 GetUserInfo 方法,以及定义 UserIDUserInfo 的数据结构即可。

grpc的Quick startQuick start | Go | gRPC

grpc学习路线概述

  1. 了解 gRPC 的基本概念和原理,可以查看官方文档或者其他资料进行学习。
  2. 学习如何使用 protobuf 定义服务和消息,掌握 protobuf 的基本语法和使用方式。
  3. 编写一个简单的 gRPC 服务端和客户端,并进行通信测试。可以使用任意一种 gRPC 支持的语言,如 Go、Java、Python 等。
  4. 学习如何使用 gRPC 的拦截器、流式 RPC、错误处理等高级特性。
  5. 学习如何使用 gRPC 的安全认证机制,包括 SSL/TLS、Token、OAuth2 等。
  6. 学习如何使用 gRPC 的负载均衡、服务发现、集群部署等高级特性,例如使用 Consul、Etcd、Kubernetes 等工具实现服务发现和负载均衡。
  7. 学习如何使用 gRPC 的代码生成器,例如 protoc 和插件,以及如何生成多语言的客户端和服务端代码。
  8. 学习如何使用 gRPC 的测试框架,例如使用 grpcurl 进行手动测试,使用 grpc-testing 进行自动化测试等。

Gin 框架和 gRPC 框架混用的概念

概念

Gin 框架和 gRPC 框架可以混用。Gin 框架主要用于开发 Web 应用程序,而 gRPC 框架主要用于开发分布式系统中的服务通信。在实际的应用场景中,常常需要将 Web 应用程序和分布式服务结合起来,这时候就需要使用 Gin 和 gRPC 框架进行混用。

具体来说,可以使用 Gin 框架作为 Web 应用程序的前端,接收 HTTP 请求,并将请求转发给后端的 gRPC 服务。在后端,可以使用 gRPC 框架处理 gRPC 请求,并将处理结果返回给前端的 Gin 应用程序。

为了实现 Gin 和 gRPC 框架的混用,需要在 Gin 应用程序中使用 gRPC 的客户端库调用 gRPC 服务。同时,还需要在 gRPC 服务中实现相应的接口,并将 gRPC 服务注册到 gRPC 的服务器中。最后,在 Gin 应用程序中处理 HTTP 请求,并将请求转发给 gRPC 服务。

例子

假设有一个 Web 应用程序,需要查询用户信息,并向用户发送邮件。其中,查询用户信息的服务使用 gRPC 框架实现,发送邮件的服务使用 Gin 框架实现。具体流程如下:

  1. 前端调用:用户在 Web 应用程序中输入用户 ID,点击查询按钮,触发 HTTP 请求。
  2. Gin 应用程序处理 HTTP 请求:Gin 应用程序接收到 HTTP 请求,并根据用户 ID 调用 gRPC 客户端,向 gRPC 服务请求用户信息。
  3. gRPC 服务处理请求:gRPC 服务接收到来自 Gin 应用程序的请求,根据用户 ID 查询用户信息,并将查询结果封装成 Protocol Buffers 格式的消息返回给 Gin 应用程序。
  4. Gin 应用程序处理响应:Gin 应用程序接收到来自 gRPC 服务的响应,将查询到的用户信息发送给用户的邮箱。

在这个过程中,Gin 框架和 gRPC 框架实现了服务之间的协作,实现了查询用户信息和发送邮件的功能。在 Gin 应用程序中,使用 gRPC 客户端库调用 gRPC 服务,实现了查询用户信息的功能;在 gRPC 服务中,实现了相应的接口,并将服务注册到 gRPC 服务器中,实现了服务的发布和调用。

consul和grpc混用的概念

概念

Consul 可以与 gRPC 框架混用。Consul 是一种分布式服务发现和配置管理工具,而 gRPC 是一种高性能的 RPC 框架,可以实现分布式系统中的服务通信。通过将 Consul 和 gRPC 框架结合起来使用,可以实现更加灵活和高效的分布式系统。

具体来说,可以使用 Consul 注册和发现 gRPC 服务,让客户端通过 Consul 进行服务发现和负载均衡。在 gRPC 服务端,可以使用 Consul 客户端库将服务注册到 Consul 中心,以便客户端进行发现。在 gRPC 客户端,可以使用 Consul 客户端库进行服务发现,并根据 Consul 返回的负载均衡策略选择服务实例进行调用。

例子

假设我们有一个分布式电商系统,其中包含多个 gRPC 服务,包括订单服务、商品服务和用户服务。这些服务之间需要相互调用,同时还需要向外部提供接口,让其他客户端可以调用。

在这种情况下,我们可以使用 Consul 进行服务注册和发现,具体的流程如下:

  1. gRPC 服务端注册服务:在每个 gRPC 服务启动时,使用 Consul 客户端库将服务注册到 Consul 中心。
  2. gRPC 客户端发现服务:在客户端启动时,使用 Consul 客户端库进行服务发现,获取可用的服务实例列表,并根据负载均衡策略选择服务实例进行调用。
  3. gRPC 服务互相调用:在服务之间调用时,可以使用 gRPC 的客户端库进行调用,直接通过服务名称和 Consul 返回的服务地址和端口进行通信。例如,当订单服务需要获取商品信息时,可以通过商品服务的名称和 Consul 返回的地址和端口进行调用。
  4. 外部客户端调用:对于外部客户端,可以使用 gRPC 的反向代理(如 nginx)或者 HTTP 转 gRPC 网关(如 grpc-gateway)进行调用。这些代理或网关可以通过 Consul 进行服务发现,从而可以找到可用的服务实例进行调用。例如,当客户端需要查询订单信息时,可以通过 nginx 反向代理进行调用,nginx 可以通过 Consul 获取订单服务的可用实例并进行负载均衡。

consul的概念

consul是什么

Consul是一种开源的服务发现和配置管理工具,由HashiCorp开发。它提供了一个集中式的服务注册和发现系统,可以帮助分布式应用程序自动化地发现和管理服务。Consul还提供了一种分布式键值存储,用于存储和检索应用程序的配置数据。在Go语言中,Consul提供了一组API,使开发人员可以轻松地将其集成到其应用程序中。

举例来说,假设你正在构建一个分布式系统,该系统由多个微服务组成。每个微服务都运行在不同的主机上,它们之间需要相互通信以完成某些任务。在这种情况下,你需要一种方式来发现每个微服务的位置和可用性。

这时候,你可以使用Consul。通过将每个微服务注册到Consul中,Consul就能够维护一个服务目录,并为你提供一个服务发现机制。当一个微服务需要与另一个微服务通信时,它可以向Consul查询目标微服务的位置和端口号,并使用该信息与目标微服务建立连接。

此外,Consul还提供了一种分布式键值存储,用于存储和检索应用程序的配置数据。例如,你可以将应用程序的数据库连接字符串存储在Consul中,并在需要时从Consul中检索该信息。这使得应用程序的配置管理变得更加灵活和可扩展。

综上所述,Consul为分布式系统提供了一种可靠的服务发现和配置管理解决方案,帮助开发人员更轻松地构建和维护分布式系统。

二、Hello World gRPC

对应代码:StudyDemo/grpc_consul__stduy_demo/grpc_demo at main · yushen611/StudyDemo (github.com)

1.安装 gRPC 和 protobuf

进入文件夹,创建一个新的mod项目

go mod init test_grpc
go mod tidy

在项目目录下打开终端,进行安装

$ go get -u google.golang.org/grpc
$ go get -u google.golang.org/protobuf/cmd/protoc-gen-go

其中,第一条命令用于安装 gRPC,第二条命令用于安装 protobuf 的 Go 插件 protoc-gen-go。

然后我们还需要去官网安装protoc的编译器(选最新的protoc):Downloads | Protocol Buffers Documentation (protobuf.dev)

并配置protoc的环境变量(不同操作系统配置的方法不一样):export PATH="$PATH:/path/to/protoc/bin"

实际上,安装 google.golang.org/protobuf/cmd/protoc-gen-go 只是安装了 protoc-gen-go 这个 Go 插件,它是 Protocol Buffers 编译器 protoc 的一个插件,可以将 .proto 文件编译成 Go 语言的代码。因此,在安装 google.golang.org/grpcgoogle.golang.org/protobuf/cmd/protoc-gen-go 后,您还需要安装 protoc 编译器才能将 .proto 文件编译成所需的目标代码。

2.编写 proto 文件:定义服务与消息

使用 protobuf 定义服务和消息。创建一个名为 helloworld.proto 的文件,输入以下内容:

这个文件使用的是 Protocol Buffers(简称 protobuf)语言。protobuf 是一种轻量级的数据交换格式,可用于序列化结构化数据。protobuf 文件通常以 .proto 为扩展名,用于定义消息类型、服务、枚举等内容。gRPC 使用 protobuf 作为默认的消息序列化和反序列化格式。

syntax = "proto3";

package helloworld;

option go_package = "test_grpc/helloworld";

// 定义一个 HelloRequest 消息类型,包含一个字符串类型的 name 字段
message HelloRequest {
  string name = 1;
}

// 定义一个 HelloResponse 消息类型,包含一个字符串类型的 message 字段
message HelloResponse {
  string message = 1;
}

// 定义一个 Greeter 服务,包含一个 SayHello 方法,该方法接收一个 HelloRequest 消息类型的参数,返回一个 HelloReply 消息类型的响应
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}


rpc说明

在 Protocol Buffers 中,可以通过两种方式来定义一个 rpc 方法,即“简写”和“完整写法”。

第一种方式是“简写”,例如:

rpc SayHello (HelloRequest) returns (HelloResponse) {}

这种写法中,HelloRequestHelloResponse 是消息类型的名字,由于 rpc 方法中只有一个参数和一个返回值,所以可以省略掉这些消息类型的名称和外层的大括号。

第二种方式是“完整写法”,例如:

rpc SayHello (HelloRequest) returns (HelloResponse);

这种写法中,消息类型的名字被写在括号内,并使用分号来结束这个语句。由于这种写法没有使用外层的大括号,因此看起来更加紧凑。

option go_package 说明

这里的路径是指生成的 Go 代码的包路径,可以是你的 Go 项目的任何有效路径。如果你正在使用 Go Modules,你可以选择一个与你的 Go Module 名称相同的路径,但这不是必需的。例如,如果你的 Go 项目的 Module 名称为 "example.com/myproject",你可以将 "go_package" 设置为 "example.com/myproject/helloworld" 或 "myproject/helloworld",或者任何其他有效的包路径。

如果你还没有创建任何的go文件与go包,那么你可以自由地指定路径名。你可以将其看作是你将来可能会使用的包路径,可以根据你的项目需要自由命名。需要注意的是,路径名需要符合go的包名规范,即全部小写字母、可以包含数字和下划线、不能以数字开头、不能与go的保留字冲突。

3.生成代码

使用 protoc 编译器生成 Go 语言的代码。在命令行中执行以下命令

$ protoc --go_out=plugins=grpc:. helloworld.proto

该命令将会在option go_package目录下生成一个名为 helloworld.pb.go 的文件,包含了生成的 Go 语言代码。

如果option go_packagetest_grpc/helloworld,此时的目录结构为:

D:\PROJECT_FILE\GOLANGVEDIO\CODE\DEMOS\GRPC_DEMO
│  go.mod
│  go.sum
│  helloworld.proto
│  main.go
│
└─test_grpc
    └─helloworld
            helloworld.pb.go

说明

这个命令是使用 Protocol Buffers 编译器(protoc)来生成 Go 语言的代码。具体来说,--go_out 选项指定了生成的代码类型为 Go 语言,并且使用 plugins=grpc 参数来指定同时生成支持 gRPC 的代码。: 后面的路径则指定了生成的代码存放的位置和文件名。

在这个命令中,helloworld.proto 文件是作为编译器的输入,编译器会根据这个文件中定义的消息类型、服务等信息,自动生成对应的 Go 语言代码文件。这些生成的代码文件包括了消息类型的定义、序列化和反序列化的代码、gRPC 服务端和客户端的接口定义等内容,方便开发人员在 Go 语言中使用 gRPC 进行通信。

需要注意的是,protoc 命令需要安装 Protocol Buffers 编译器,并且需要安装相应的插件来支持生成 gRPC 相关的代码。在本命令中,使用了 go_out 插件来生成 Go 语言代码,并且使用了 grpc 插件来生成支持 gRPC 的代码。

生成代码正确语法

go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1.0
$ protoc --go_out=. --go-grpc_out=. helloworld.proto

4.编写服务端代码

创建一个名为server的文件夹,里面创建文件server.go

package main

import (
	"context"
	"log"
	"net"

	pb "test_grpc/test_grpc/helloworld" // 导入生成的 helloworld.pb.go 文件

	"google.golang.org/grpc"
)

const (
	port = ":50051" // 端口号
)

 // 定义 gRPC 服务的 server
type server struct{
    pb.UnimplementedHelloServiceServer
}

// 实现 SayHello 方法,接受客户端的请求,返回 HelloResponse 响应
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	return &pb.HelloResponse{Message: "Hello " + req.Name}, nil
}

func main() {
	lis, err := net.Listen("tcp", port) // 监听 TCP 端口
	if err != nil {
		log.Fatalf("failed to listen: %v", err) // 如果监听失败,则退出程序并打印错误信息
	}
	s := grpc.NewServer()                  // 创建一个 gRPC 服务实例
	pb.RegisterGreeterServer(s, &server{}) // 注册服务,把实现服务的 server 结构体绑定到 gRPC 服务实例中
	log.Printf("start gRPC server on port %s\n", port)
	if err := s.Serve(lis); err != nil { // 开始监听
		log.Fatalf("failed to serve: %v", err) // 开始监听,如果监听失败,则退出程序并打印错误信息
	}
}

这段代码用于启动一个 gRPC 服务。它首先调用 net.Listen() 监听指定的 TCP 端口,如果监听失败,则会退出程序并打印错误信息。然后,它创建一个 gRPC 服务实例,通过调用 pb.RegisterGreeterServer() 注册服务,把实现服务的 server 结构体绑定到 gRPC 服务实例中。最后,它通过调用 s.Serve(lis) 开始监听,并在监听失败时退出程序并打印错误信息。

5.编写客户端代码

创建一个名为client的文件夹,里面创建文件,创建一个client.go

package main

import (
	"context"
	"log"
	"os"
	"time"

	pb "test_grpc/test_grpc/helloworld" // 这里修改为实际的 proto 文件路径

	"google.golang.org/grpc"
)

const (
	address     = "localhost:50051"
	defaultName = "world"
)

func main() {
	conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	name := defaultName
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.Message)
}

这段代码是一个 gRPC 的客户端,用来向服务端发送请求并接收响应。代码中首先通过 Dial() 方法连接到了一个名为 address 的服务端。如果连接成功,将会调用 SayHello() 方法向服务端发送请求,参数为一个 HelloRequest 类型的消息,其中包含一个名为 name 的字段。请求发送成功后,客户端会等待服务端返回响应,将响应中的 Message 字段打印出来。在代码的最后,通过 defer 关键字关闭了连接。

6.分别运行

现在可以运行服务端和客户端程序,看看是否能够正常工作。首先运行服务端程序:

进入server下面的终端

go run server.go

进行client下面的终端

go run client.go

客户端将会输出类似以下内容的信息:

2023/04/04 17:19:42 Greeting: Hello world

三、Hello World consul

对应代码:StudyDemo/grpc_consul__stduy_demo/consul_demo at main · yushen611/StudyDemo (github.com)

1. 安装concul

Install | Consul | HashiCorp Developer

下载到你想要的目录,然后配置环境变量

2.创建demo项目目录

进项目文件夹,新建mod项目

go mod init test_consul

项目目录

demo/
├── main.go
├── go.mod
├── go.sum
└── consul.go

3.编写Consul客户端

consul.go文件中,你需要编写一个Consul客户端,用于向Consul服务注册和发现服务。下面是一个简单的实现:

package main

import (
	"fmt"
	consulapi "github.com/hashicorp/consul/api"
)

type ConsulClient struct {
	client *consulapi.Client
}

func NewConsulClient() (*ConsulClient, error) {
	config := consulapi.DefaultConfig()
	client, err := consulapi.NewClient(config)
	if err != nil {
		return nil, err
	}
	return &ConsulClient{client: client}, nil
}

// RegisterService 将服务注册到 Consul 中。
// serviceID:服务 ID,必须唯一。
// serviceName:服务名称。
// serviceHost:服务主机名或 IP 地址。
// servicePort:服务端口号。
// 返回值:错误对象,如果成功则为 nil。

func (c *ConsulClient) RegisterService(serviceID, serviceName, serviceHost string, servicePort int) error {
	// 创建服务对象
    service := &consulapi.AgentServiceRegistration{
		ID:   serviceID,
		Name: serviceName,
		Address: serviceHost,
		Port: servicePort,
	}
    // 调用 Consul API 将服务注册到 Consul 中
	return c.client.Agent().ServiceRegister(service)
}


// DiscoverService 根据服务名称从 Consul 中发现服务。
// serviceName:服务名称。
// 返回值:
//     - string:服务地址,格式为 "host:port"。
//     - error:错误对象,如果成功则为 nil。
func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
	services, _, err := c.client.Catalog().Service(serviceName, "", nil)
	if err != nil {
		return "", err
	}
	if len(services) == 0 {
		return "", fmt.Errorf("service not found")
	}
	return fmt.Sprintf("%s:%d", services[0].ServiceAddress, services[0].ServicePort), nil
}

这个客户端使用Consul Go语言API提供的功能,可以向Consul注册服务和发现服务。NewConsulClient函数用于创建一个新的Consul客户端实例,RegisterService函数用于向Consul注册一个新的服务,DiscoverService函数用于发现一个指定名称的服务。这里我们只实现了最简单的功能,你可以根据自己的需求进一步扩展。

4.编写main函数

package main

import (
	"fmt"
	"net/http"
)

func main() {
	// 创建Consul客户端
	consulClient, err := NewConsulClient()
	if err != nil {
		panic(err)
	}
	// 注册服务
	serviceID := "demo-service"
	serviceName := "demo"
	serviceHost := "localhost"
	servicePort := 8080
	err = consulClient.RegisterService(serviceID, serviceName, serviceHost, servicePort)
	if err != nil {
		panic(err)
	}

	// 发现服务
	serviceAddr, err := consulClient.DiscoverService(serviceName)
	if err != nil {
		panic(err)
	}

	// 处理HTTP请求
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello World!")
	})
	fmt.Printf("Starting server at %s...\n", serviceAddr)
	err = http.ListenAndServe(serviceAddr, nil)
	if err != nil {
		panic(err)
	}
}

5.运行程序

启动Consul Agent,默认端口是8500

consul agent -dev

运行consul与服务

go run main.go consul.go

如果一切正常,你应该能够在终端中看到类似下面的输出:

Starting server at localhost:8080...

现在,你可以在浏览器中访问http://localhost:8080,应该能够看到一个"Hello World!"字符串。

补充1:如何设置Consul Agent的ip与端口

Consul Agent 的 IP 地址和端口可以通过启动 Consul Agent 时的命令行参数来指定。具体来说,可以使用 -bind-client 参数来设置 Consul Agent 的 IP 地址和端口。

例如,要将 Consul Agent 的 IP 地址设置为 192.168.1.100,端口设置为 8500,可以使用以下命令启动 Consul Agent:

consul agent -bind=192.168.1.100 -client=0.0.0.0:8500

其中,-bind 参数指定 Consul Agent 绑定的 IP 地址,-client 参数指定 Consul Agent 监听的客户端地址和端口。

如果要在程序中指定 Consul Agent 的地址和端口,可以在创建 Consul 客户端时传入一个 consulapi.Config 类型的对象,并设置该对象的 Address 字段。例如:

config := consulapi.DefaultConfig()
config.Address = "192.168.1.100:8500"

client, err := consulapi.NewClient(config)
if err != nil {
    log.Fatalf("Failed to create Consul client: %v", err)
}

这段代码将创建一个 consulapi.Config 类型的对象,并将其地址设置为 192.168.1.100:8500,然后使用该对象创建一个 Consul 客户端。这样,客户端就会连接到指定的 Consul Agent。

补充2:如何停止运行consul

方式1:在终端ctrl +c

方式2:使用 consul leave 命令离开 Consul 集群并停止 Consul Agent 的运行。

6. 总结

至此,你已经成功地编写了一个使用Consul进行服务发现和注册的Go语言demo项目。这个项目只是一个最基础的示例,你可以根据自己的需求进一步扩展。同时,你还可以使用Consul的其他功能,如健康检查、分布式键值存储等,来更好地管理你的分布式系统。

四、Hello world consul & grpc

对应代码:StudyDemo/grpc_consul__stduy_demo/consul_grpc_demo at main · yushen611/StudyDemo (github.com)

1. 概述

使用场景

接口A的实现需要调用接口B,但是接口A和接口B不在同一个服务器上。这时候接口A想要调用接口B,接口A就需要知道接口B的ip端口以及调用方式

专业名词

服务发现服务注册:就是接口A怎么找到接口B的IP与Port,j以及接口B怎么让接口A知道自己的Ip与port

远程过程调用:这个就是在接口A知道接口B的IP与PORT的前提下,怎么调用接口B

①接口B Ip与端口的获取

方式1:固定死接口B的IP与端口

​ 假设提供接口B服务的服务器只有1台,且端口不变,那么只要接口A知道接口B的调用方式

方式2:使用Consul获取接口B的IP与端口

​ 如果接口A想要调用接口B,那么接口A与接口B之间需要达成共识,即接口B对它这个IP与Port提供的服务取一个服务名,每次接口A调用时只需要知道服务名,就能查询到这个服务名对应服务的IP与端口。那么接口A向谁查询呢?这时候就需要一个中介提供已知服务名,查询服务Ip与端口的功能

这个中介就是Consul。接口B告诉Consul它提供的服务名称,接口IP与端口。接口A只需要知道Consul的Ip与端口,就能向Consul发送一个查询,查询参数是接口B的服务名称,查询返回结果是接口B的Ip与端口。

②调用方式

​ 现在,已经知道了接口B的Ip与端口,但是怎么知道接口B的入参与响应呢?但是有可能接口A与接口B不是用同一个语言完成的,不同语言直接定义数据结构的方式不同,因此这时候就需要GRPC当做中介,告诉调用方(接口A)你应该怎么写才能调用接口B。所以大致步骤如下:

  • 接口B需要以一种通用的格式(proto格式)定义自己的接口(把这个写在一个proto文件里),即告诉GRPC自己的入参是什么,响应是什么。
  • 接口A需要拿到同样的一份接口定义文件(proto文件),使用GRPC这个工具,根据proto文件,自动生成调用接口B的代码。

步骤总结

当使用 Consul 和 gRPC 混合时,需要完成以下步骤:

  1. 在项目中引入 consulgrpc 相关的依赖包。
  2. 在代码中实现 gRPC 的服务接口,并在其中定义 gRPC 的服务方法。
  3. 启动Consul服务。
  4. 实现 gRPC 的服务端,并将服务接口注册到 gRPC 的服务端,Ip、端口与服务名注册到Consul 中。
  5. 实现基于 gRPC 的客户端,通过服务名在Consul中查询Ip与端口,接着再通过客户端调用 gRPC 服务。

2.服务端代码

服务端:GRPC接口定义与consul服务发现

服务端项目目录如下:

D:.
│  Dockerfile
│  go.mod
│  go.sum
│  server.go
│
├─consul
│      consul.go
│
└─proto
    │  helloworld.proto
    │
    └─server
            helloworld.pb.go
            helloworld_grpc.pb.go

其中用到了go mod init server新建项目

helloworld.proto GRPC接口文件如下:(这个与客户端的需要相同)

syntax = "proto3";

package hello;

option go_package = "./server";

service HelloService {
    rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
    string name = 1;
}

message HelloResponse {
    string message = 1;
}

在proto目录下使用如下命令即可生成对应的go语言接口代码

protoc --go_out=. --go-grpc_out=. helloworld.proto

consul.go里面封装了服务注册与服务发现的两个代码(这个客户端复制一份一样的也行):

package consul

import (
	"fmt"
	"math/rand"

	consulapi "github.com/hashicorp/consul/api"
)

type ConsulClient struct {
	client *consulapi.Client
}

func NewConsulClient() (*ConsulClient, error) {
	config := consulapi.DefaultConfig()
	client, err := consulapi.NewClient(config)
	if err != nil {
		return nil, err
	}
	return &ConsulClient{client: client}, nil
}

//服务注册
func (c *ConsulClient) RegisterService(serviceID, serviceName, serviceHost string, servicePort int) error {
	service := &consulapi.AgentServiceRegistration{
		ID:      serviceID,
		Name:    serviceName,
		Address: serviceHost,
		Port:    servicePort,
	}
	return c.client.Agent().ServiceRegister(service)
}

//服务发现
func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
	// services, _, err := c.client.Catalog().Service(serviceName, "", nil)
	services, _, err := c.client.Health().Service(serviceName, "", true, nil)
	if err != nil {
		return "", err
	}
	if len(services) == 0 {
		return "", fmt.Errorf("service not found")
	}
	// 随机选择一个服务实例
	if len(services) > 0 {
		index := rand.Intn(len(services))
		service := services[index].Service
		address := fmt.Sprintf("%v:%v", service.Address, service.Port)
		return address, nil
	}

	return "", fmt.Errorf("no healthy instances found for service %s", serviceName)
}

server.go里是整个提供服务端的主函数

package main

import (
	"context"
	"log"
	"net"

	cs "server/consul"
	pb "server/proto/server"

	"google.golang.org/grpc"
)

// 定义 实现 gRPC 服务的 server结构体

type serverImp struct {
	pb.UnimplementedHelloServiceServer
}

// 实现 SayHello 方法,接受客户端的请求,返回 HelloResponse 响应
func (s *serverImp) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	return &pb.HelloResponse{Message: "Hello " + req.Name}, nil
}

func main() {

	//Consul-1 创建Consul客户端
	consulClient, err := cs.NewConsulClient()
	if err != nil {
		log.Fatalf("failed to Connect consul: %v", err)
		panic(err)
	}
	//Consul-2 注册服务
	serviceID := "demo-service"
	serviceName := "demo"
	serviceHost := "127.0.0.1"
	servicePort := 50051
	err = consulClient.RegisterService(serviceID, serviceName, serviceHost, servicePort)
	if err != nil {
		log.Fatalf("failed to RegisterService: %v", err)
		panic(err)
	}

	//grpc-1.定义服务启动端口
	port := ":50051"
	lis, err := net.Listen("tcp", port) // 监听 TCP 端口
	if err != nil {
		log.Fatalf("failed to listen: %v", err) // 如果监听失败,则退出程序并打印错误信息
	}

	//grpc-2. 创建一个 gRPC 服务实例
	s := grpc.NewServer()

	//grpc-3.把实现的服务结构体 与 gRPC 服务实例绑定
	pb.RegisterHelloServiceServer(s, &serverImp{}) // 注册服务,把实现服务的 server 结构体绑定到 gRPC 服务实例中

	log.Printf("start gRPC server on port %s\n", port)

	//grpc-4:启动gRPC服务
	err = s.Serve(lis)

	if err != nil { // 开始监听
		log.Fatalf("failed to serve: %v", err) // 开始监听,如果监听失败,则退出程序并打印错误信息
	}
}

3.客户端代码

客户端主要是调用服务端的代码:服务发现与GRPC调用

客户端项目目录如下:

D:.
│  client.go        
│  go.mod
│  go.sum
│
├─consul
│      consul.go    
│
└─proto
        helloworld.pb.go
        helloworld.proto
        helloworld_grpc.pb.go

其中需要用到go mod init client创建项目

其中consul.go 文件以及helloworld.proto文件与服务端一致

因此在把proto文件生成对应代码后,剩下需要说明的就是client.go ,也就是项目的主函数。

package main

import (
	"context"
	"log"
	"time"

	cs "client/consul"
	pb "client/proto"

	"google.golang.org/grpc"
)

func main() {
	//使用consul获取指定服务的ip与端口
	//consul-1:连接至consul服务中心
	consulClient, err := cs.NewConsulClient()
	if err != nil {
		panic(err)
	}
	//consul-2:发现指定服务名称的服务地址
	serviceName := "demo"
	serviceAddr, err := consulClient.DiscoverService(serviceName)
	if err != nil {
		panic(err)
	}
	// grpc-1.连接到指定ip,端口的grpc服务器

	// serviceAddr := "localhost:50051"
	conn, err := grpc.Dial(serviceAddr, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	// grpc-2.初始化HelloService服务客户端
	client := pb.NewHelloServiceClient(conn)

	//grpc-3.初始化上下文,设置请求超时时间为1秒
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	//grpc-4.调用远程的grpc方法
	response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Yu Gambler"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", response.GetMessage())
}

4.运行服务端与客户端

运行顺序如下:

  1. 先把consul运行起来

    1. consul agent -dev,默认启动本地127.0.0.1:8500
    2. 再网站输入127.0.0.1:8500可以看到Consul的UI界面
  2. 再把服务端跑起来

    运行成功会提示:

    2023/04/12 22:59:06 start gRPC server on port :50051
    
  3. 最后运行客户端调用接口

​ 调用成功会提示:2023/04/12 23:00:07 Greeting: Hello Yu Gambler

5.Dockerfile编写注意事项

如果你想把服务端写个dockerfile进行部署,下面是一个参考dockerfile。

FROM golang:1.20

WORKDIR /app/server

# 设置环境变量
ENV BUILD_ENV local

# 暴露端口
EXPOSE 50051

# 复制应用程序到工作目录
ADD . .

RUN ls

# 编译应用程序
RUN go build -o server server.go

# RUN go build -ldflags "-w -X main.version=${VERSION}" -o /nats

# 运行应用程序
CMD ["./server"]

虽然dockerfile没有问题,但是你按这个镜像跑会提示连接不上consul。原因事在consul.go里默认是连接localhost:8500的接口来连接consul。但是这个镜像里运行这个代码,就不是连接你电脑里的localhost,而是镜像里的localhost了,所以consul.go里用于连接的代码,需要单独配置consul的ip与端口。

func NewConsulClient() (*ConsulClient, error) {
	config := consulapi.DefaultConfig()
	config.Address = "192.168.3.2:8500" // 指定 Consul 地址,192.168.3.2是你运行consul的电脑的ip地址
	client, err := consulapi.NewClient(config)
	if err != nil {
		return nil, err
	}
	return &ConsulClient{client: client}, nil
}

五、一些问题

1.为什么用grpc(内部原理)为什么不用http

如果您的应用程序需要高效的数据传输,以及支持流式处理和多语言支持,那么选择gRPC可能更加合适。另外,如果您需要更强的类型约束和更好的错误检测,也应该优先考虑gRPC。

另一方面,如果您的应用程序需要与旧的系统集成,这些系统可能只支持HTTP协议,那么选择HTTP可能是更好的选择。此外,如果您的应用程序需要向外部网络发出请求,而该网络只支持HTTP,那么HTTP也是更好的选择。

  1. 传输效率

gRPC使用基于二进制的协议缓冲区(Protocol Buffers)来序列化数据,比HTTP使用的文本格式更加高效。这意味着gRPC可以更快地传输数据,同时也更节省网络带宽。

  1. 支持多语言

gRPC支持多种编程语言,包括Java、Python、C++、Go等,而HTTP只是一个通用的协议,没有特定的编程语言要求。这使得gRPC可以在不同的编程语言之间更容易地交互和通信。

  1. 支持流式处理

gRPC支持流式处理,可以在单个连接上进行多个请求和响应,而HTTP只支持一个请求和一个响应。这使得gRPC更适合实时通信,如聊天应用、在线游戏等。

  1. 强类型约束

gRPC使用Protocol Buffers来定义数据类型和接口规范,这意味着可以进行强类型约束,更容易地检测和处理错误。而HTTP只是一个文本协议,没有强类型约束。

综上所述,gRPC相对于HTTP来说,在性能、多语言支持、流式处理和强类型约束方面具有一定的优势,因此在需要高效、实时、多语言的场景中,选择使用gRPC可能更加合适

2.consul,同服务,多个服务器时,如何选?/consul选择一个服务实例时有哪些策略

1.随机策略

随机策略会随机选择一个可用的服务实例进行通信。这种策略相对简单,但不适合高并发的场景。

以下是随机选择一个服务实例策略的代码,请仿照这个函数写出采用轮询策略时的DiscoverService代码

func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
	// services, _, err := c.client.Catalog().Service(serviceName, "", nil)
	services, _, err := c.client.Health().Service(serviceName, "", true, nil)
	if err != nil {
		return "", err
	}
	if len(services) == 0 {
		return "", fmt.Errorf("service not found")
	}
	// 随机选择一个服务实例
	if len(services) > 0 {
		index := rand.Intn(len(services))
		service := services[index].Service
		address := fmt.Sprintf("%v:%v", service.Address, service.Port)
		return address, nil
	}

	return "", fmt.Errorf("no healthy instances found for service %s", serviceName)

}

2.轮询策略

轮询策略会依次选择每个可用的服务实例进行通信。当所有的服务实例都被选择过一次后,再从头开始进行轮询。这种策略适合在不同服务实例的负载差异不大的情况下使用。

func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
	// services, _, err := c.client.Catalog().Service(serviceName, "", nil)
	services, _, err := c.client.Health().Service(serviceName, "", true, nil)
	if err != nil {
		return "", err
	}
	if len(services) == 0 {
		return "", fmt.Errorf("service not found")
	}

	// 从缓存中获取上一次选择的服务实例索引
	index, err := c.getIndexFromCache(serviceName)
	if err != nil {
		return "", err
	}

	// 选择下一个服务实例
	nextIndex := (index + 1) % len(services)
	for i := 0; i < len(services); i++ {
		service := services[nextIndex].Service
		address := fmt.Sprintf("%v:%v", service.Address, service.Port)
		if err = c.updateIndexToCache(serviceName, nextIndex); err != nil {
			return "", err
		}
		nextIndex = (nextIndex + 1) % len(services)
	}

	return "", fmt.Errorf("no healthy instances found for service %s", serviceName)
}

其中读缓存.写缓存这两个函数 getIndexFromCacheupdateIndexToCache,需要自己去实现

func (c *ConsulClient) getIndexFromCache(serviceName string) (int, error) {
    // 从缓存中获取上一次选择的服务实例索引
	// 如果缓存中不存在,则返回默认值 0
    cacheKey := fmt.Sprintf("%s_index", serviceName)
    result, err := c.redisClient.Get(cacheKey).Result()
    if err != nil {
        if err == redis.Nil {
            return 0, nil
        }
        return 0, err
    }

    index, err := strconv.Atoi(result)
    if err != nil {
        return 0, err
    }

    return index, nil
}


func (c *ConsulClient) updateIndexToCache(serviceName string, index int) error {
	// 以 serviceName 为 key,将 index 保存到缓存中,例如使用 Redis 实现
	redisKey := fmt.Sprintf("consul:index:%s", serviceName)
	err := c.redisClient.Set(redisKey, index, 0).Err()
	if err != nil {
		return err
	}
	return nil
}

实际上,您需要根据您的具体需求和环境,实现一个适合您自己的缓存方案。常见的缓存方案包括使用 Redis、Memcached 或者在应用程序中使用内存缓存等。

3.加权轮询策略

加权轮询策略会根据服务实例的权重来进行轮询。权重越高的服务实例会被选择的概率越大。这种策略适合在不同服务实例的负载差异较大的情况下使用。

Consul中的加权轮询策略与一般的加权轮询策略基本相同,主要包括以下几个步骤:

  1. 获取服务实例列表:通过Consul API获取指定服务的所有健康实例列表。

  2. 获取上一次选择的实例索引和权重信息:从缓存中获取上一次已经选择的服务实例索引以及每个服务实例对应的权重信息。如果没有缓存,则默认从第一个实例开始选择。

  3. 计算权重总和:计算所有服务实例的权重总和。

  4. 选择下一个服务实例:根据加权轮询算法选择下一个服务实例。

    a. 从上一次选择的实例开始依次轮询所有服务实例,如果当前服务实例权重大于0,则选择该实例。

    b. 如果所有实例权重均为0,则表示没有健康实例可供选择。

  5. 更新缓存:将选择的服务实例索引以及更新后的权重信息更新到缓存中,以便下一次选择服务实例时使用。

  6. 返回选择的服务实例地址:将选择的服务实例的地址(IP地址和端口号)作为结果返回给调用方。

需要注意的是,Consul中的加权轮询策略还可以添加权重重置的功能,即当所有服务实例的权重都为0时,重新将所有实例的权重值重置为原始权重值,从而保证每个实例都有机会被选择。

func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
    // services, _, err := c.client.Catalog().Service(serviceName, "", nil)
    services, _, err := c.client.Health().Service(serviceName, "", true, nil)
    if err != nil {
        return "", err
    }
    if len(services) == 0 {
        return "", fmt.Errorf("service not found")
    }
    
    // 从缓存中获取上一次选择的服务实例索引和权重信息
    index, weights, err := c.getIndexAndWeightsFromCache(serviceName, len(services))
    if err != nil {
        return "", err
    }
    
    // 选择下一个服务实例
    for i := 0; i < len(services); i++ {
        index = (index + 1) % len(services)
        if weights[index] > 0 {
            service := services[index].Service
            address := fmt.Sprintf("%v:%v", service.Address, service.Port)
            if err = c.updateIndexAndWeightsToCache(serviceName, index, weights); err != nil {
                return "", err
            }
            weights[index] -= 1
            return address, nil
        }
    }

    // 如果所有的权重都为0,就重置所有服务实例的权重,并重新计算总权重
    for i := 0; i < len(services); i++ {
        weights[i] = initialWeight
    }
    
    
    // 重新选择服务实例
    for i := 0; i < len(services); i++ {
        index = (index + 1) % len(services)
        if weights[index] > 0 {
            service := services[index].Service
            address := fmt.Sprintf("%v:%v", service.Address, service.Port)
            if err = c.updateIndexAndWeightsToCache(serviceName, index, weights); err != nil {
                return "", err
            }
            weights[index] -= 1
            return address, nil
        }
    }
    
    return "", fmt.Errorf("no healthy instances found for service %s", serviceName)
}



其中,getIndexAndWeightsFromCacheupdateIndexAndWeightsToCache分别用于从缓存中获取上一次选择的服务实例索引和权重信息以及更新缓存中的索引和权重信息。需要根据具体的缓存方案进行实现。

import (
    "encoding/json"
    "errors"
    "fmt"
    "math/rand"
    "strconv"
    "strings"
    "time"

    "github.com/go-redis/redis"
    "github.com/hashicorp/consul/api"
)


func (c *ConsulClient) getIndexAndWeightsFromCache(serviceName string) (int, []int, error) {
	// 从缓存中获取服务实例索引和权重列表
	indexStr, err := c.redisClient.Get(fmt.Sprintf("%s:index", serviceName)).Result()
	if err != nil {
		return 0, nil, err
	}
	weightsStr, err := c.redisClient.Get(fmt.Sprintf("%s:weights", serviceName)).Result()
	if err != nil {
		return 0, nil, err
	}

	// 解析索引和权重列表
	index, err := strconv.Atoi(indexStr)
	if err != nil {
		return 0, nil, err
	}
	weights := make([]int, 0)
	if err = json.Unmarshal([]byte(weightsStr), &weights); err != nil {
		return 0, nil, err
	}

	return index, weights, nil
}
/*getIndexAndWeightsFromCache 函数会从 Redis 缓存中获取指定服务的实例索引和权重列表,并将它们解析出来返回。如果在获取缓存值或解析值时发生错误,函数将返回一个错误。注意,这里返回的权重列表是一个 []int 类型的切片,表示每个服务实例的权重值。*/


func (c *ConsulClient) updateIndexAndWeightsToCache(serviceName string, index int, weights []int) error {
	// Connect to Redis
	conn, err := redis.DialURL(c.redisAddr)
	if err != nil {
		return err
	}
	defer conn.Close()

	// Convert the weights slice to a comma-separated string
	weightsStr := ""
	for _, w := range weights {
		weightsStr += strconv.Itoa(w) + ","
	}
	weightsStr = strings.TrimSuffix(weightsStr, ",")

	// Set the index and weights in Redis
	key := "service:" + serviceName
	_, err = conn.Do("SET", key, fmt.Sprintf("%d:%s", index, weightsStr))
	if err != nil {
		return err
	}

	return nil
}

4.随机加权策略

随机加权策略会随机选择一个可用的服务实例,并根据服务实例的权重来进行选择。权重越高的服务实例会被选择的概率越大。这种策略适合在不同服务实例的负载差异较大的情况下使用。

Consul中的随机加权轮询算法(Randomized Weighted Round-Robin,RWRR)是加权轮询算法的一种改进,它可以避免加权轮询算法的权重累加问题,同时也可以实现动态权重调整。具体步骤如下:

  1. 首先从Consul中获取所有健康的服务实例,并获取它们的权重值。
  2. 计算所有服务实例的权重之和WW
  3. 随机生成一个初始值rr,取值范围为[0, WW]。
  4. 依次遍历所有服务实例,对每个实例的权重值wiw_i进行判断。
  5. 如果wiw_i大于等于rr,则选择该服务实例,并将rr减去wiw_i的值。
  6. 如果wiw_i小于rr,则跳过该服务实例,继续遍历下一个服务实例。
  7. 如果所有服务实例都遍历完了还没有选出任何一个实例,则将rr重置为初始值,并再次执行步骤4-6。
  8. 当选出一个服务实例时,将该实例的权重值减去1,并将更新后的权重值保存到Consul的KV存储中。
  9. 返回选出的服务实例的地址。
  10. 等待下一次请求,重复执行上述步骤。

通过随机生成rr的方式,RWRR算法可以保证服务实例的选择是随机的,并且可以根据实际的权重值进行调整。同时,当所有服务实例的权重值都为0时,RWRR算法也会自动将权重值重置为初始值,从而避免了加权轮询算法的问题。

func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
    services, _, err := c.client.Health().Service(serviceName, "", true, nil)
    if err != nil {
        return "", err
    }
    if len(services) == 0 {
        return "", fmt.Errorf("service not found")
    }

    // 从缓存中获取上一次选择的服务实例索引和权重信息
    index, weights, err := c.getIndexAndWeightsFromCache(serviceName, len(services))
    if err != nil {
        return "", err
    }

    // 选择下一个服务实例
    totalWeight := sum(weights)
    rand.Seed(time.Now().UnixNano())
    for i := 0; i < len(services); i++ {
        index = rand.Intn(len(services))
        if weights[index] > 0 {
            service := services[index].Service
            address := fmt.Sprintf("%v:%v", service.Address, service.Port)
            if err = c.updateIndexAndWeightsToCache(serviceName, index, weights); err != nil {
                return "", err
            }
            weights[index] -= 1
            return address, nil
        }
    }
    
    // 如果所有的权重都为0,就重置所有服务实例的权重,并重新计算总权重
    for i := 0; i < len(services); i++ {
        weights[i] = initialWeight
    }
    // 重新选择服务实例
    for i := 0; i < len(services); i++ {
        index = rand.Intn(len(services))
        if weights[index] > 0 {
            service := services[index].Service
            address := fmt.Sprintf("%v:%v", service.Address, service.Port)
            if err = c.updateIndexAndWeightsToCache(serviceName, index, weights); err != nil {
                return "", err
            }
            weights[index] -= 1
            return address, nil
        }
    }

    return "", fmt.Errorf("no healthy instances found for service %s", serviceName)
}

5.最近最少使用策略

最近最少使用策略会选择最近使用最少的服务实例进行通信。这种策略适合在服务实例的响应时间不同的情况下使用,可以确保请求会被分配到响应时间最短的服务实例上。

最近最少使用(Least Recently Used, LRU)策略是一种基于使用频率来淘汰缓存中的数据的算法。在应用到服务发现中,可以将服务实例的使用次数作为权重,每次选择权重最小的服务实例。实现Consul的DiscoverService函数的LUR策略,可以按照以下步骤进行:

  1. 从Consul中获取服务实例列表。
  2. 从缓存中获取上一次选择的服务实例索引和使用次数列表。
  3. 计算当前所有服务实例使用次数的最小值minCount。
  4. 遍历服务实例列表,找到使用次数等于minCount的实例集合,并在该集合中选择一个实例。
  5. 如果选择的实例与上一次选择的实例不同,则更新缓存中的服务实例索引和使用次数列表,将选择的实例使用次数加1,并返回该实例的地址。
func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
    // services, _, err := c.client.Catalog().Service(serviceName, "", nil)
    services, _, err := c.client.Health().Service(serviceName, "", true, nil)
    if err != nil {
        return "", err
    }
    if len(services) == 0 {
        return "", fmt.Errorf("service not found")
    }

    // 从缓存中获取上一次选择的服务实例索引和使用次数列表
    index, counts, err := c.getIndexAndCountsFromCache(serviceName, len(services))
    if err != nil {
        return "", err
    }

    // 计算使用次数的最小值
    minCount := math.MaxInt32
    for _, count := range counts {
        if count < minCount {
            minCount = count
        }
    }

    // 选择使用次数等于minCount的服务实例
    var candidates []int
    for i, count := range counts {
        if count == minCount {
            candidates = append(candidates, i)
        }
    }
    index = candidates[rand.Intn(len(candidates))]

    // 更新缓存中的服务实例索引和使用次数列表
    if err = c.updateIndexAndCountsToCache(serviceName, index, counts); err != nil {
        return "", err
    }
    counts[index]++

    // 返回选择的服务实例的地址
    service := services[index].Service
    address := fmt.Sprintf("%v:%v", service.Address, service.Port)
    return address, nil
}

需要注意的是,此实现中的缓存可以采用本地内存或者分布式缓存,更新缓存的过程需要使用分布式锁保证操作的原子性。

6.IP哈希策略

IP哈希策略会根据客户端的IP地址来选择一个服务实例。这种策略适合在需要将请求分配到同一服务实例的情况下使用,例如需要保持用户会话状态的场景。具体实现中,将客户端IP地址进行哈希运算,得到一个哈希值,然后使用该哈希值选择一个后端服务器。

func (c *ConsulClient) DiscoverService(serviceName string, clientIP string) (string, error) {
    // services, _, err := c.client.Catalog().Service(serviceName, "", nil)
    services, _, err := c.client.Health().Service(serviceName, "", true, nil)
    if err != nil {
        return "", err
    }
    if len(services) == 0 {
        return "", fmt.Errorf("service not found")
    }
    
    // 将客户端IP地址进行哈希运算,得到一个哈希值
    h := fnv.New32a()
    h.Write([]byte(clientIP))
    hash := h.Sum32()
    
    // 根据哈希值选择一个后端服务器
    index := int(hash) % len(services)
    service := services[index].Service
    address := fmt.Sprintf("%v:%v", service.Address, service.Port)
    return address, nil
}

在这个实现中,我们使用了Go语言标准库中的hash/fnv包来进行哈希运算。对于每个客户端IP地址,我们都会将其哈希成一个32位整数,并使用该整数对服务实例列表进行取模,从而得到要访问的服务实例。

3.如果consul负载大成为性能瓶颈时如何处理?

当Consul的负载增加到一定程度时,可能会成为系统的性能瓶颈。为了解决这个问题,可以考虑以下几个方面:

  1. 增加节点/consul集群:通过增加Consul节点数量,将负载分摊到更多的节点上,从而提高整体性能。
  2. 增加数据中心:如果服务规模足够大,可以考虑在不同地理位置或网络分区内部署不同的Consul数据中心,从而减少单一数据中心的负载压力。
  3. 升级硬件:升级Consul服务器和客户端的硬件,如增加CPU、内存等,可以提高性能。
  4. 优化查询:可以优化Consul查询,如减少查询频率、合并查询、使用更快的网络等,来提高性能。
  5. 使用缓存:可以使用缓存来减轻Consul的负载,如将服务发现结果缓存在本地,避免每次查询都需要访问Consul。
  6. 采用更快的数据存储方案:可以考虑使用更快的数据存储方案,如使用Redis等内存数据库来存储服务发现结果,从而提高查询速度和整体性能。

以上是一些常见的提高Consul性能的方法,具体需要根据实际情况选择最适合的方法。