自己动手实现 Go 的服务注册与发现(中)

2,135 阅读5分钟

通过服务发现与注册中心,可以很方便地管理系统中动态变化的服务实例信息。与此同时,它也可能成为系统的瓶颈和故障点。因为服务之间的调用信息来自于服务注册与发现中心,当它不可用时,服务之间的调用可能无法正常进行。因此服务发现与注册中心一般会多实例部署,提供高可用性和高稳定性。

我们将基于 Consul 实现 Golang Web 的服务注册与发现。首先我们会通过原生态的方式,直接通过 HTTP 方式与 Consul 进行交互;然后我们会通过 Go Kit 框架提供的 Consul Client 接口实现与 Consul 之间的交互,并比较它们之间的不同。

前面一篇文章,我们了解完整个微服务结构,我们将开始编写核心的 ConsulClient 接口的实现,完成这个简单微服务和 Consul 之间服务注册与发现的流程。

服务实例与 Consul 交互

在这一部分中,我们会直接通过 HTTP 的方式与 Consul 完成交互,完成服务注册和服务发现的功能。我们首先定义服务注册时的服务实例结构体 diy.InstanceInfo,源码位于 ch7-discovery/diy/MyConsulClient.go。代码如下所示:

// 服务实例结构体
type InstanceInfo struct {
	ID string `json:"ID"` // 服务实例ID
	Name string `json:"Name"` // 服务名
	Service string `json:"Service,omitempty"` // 服务发现时返回的服务名
	Tags []string `json:"Tags,omitempty"` // 标签,可用于进行服务过滤
	Address string `json:"Address"` // 服务实例HOST
	Port int `json:"Port"` // 服务实例端口
	Meta map[string]string `json:"Meta,omitempty"` // 元数据
	EnableTagOverride bool `json:"EnableTagOverride"` // 是否允许标签覆盖
	Check `json:"Check,omitempty"` // 健康检查相关配置
	Weights `json:"Weights,omitempty"` // 权重
}

type Check struct {
	DeregisterCriticalServiceAfter string `json:"DeregisterCriticalServiceAfter"` // 多久之后注销服务
	Args []string `json:"Args,omitempty"` // 请求参数
	HTTP string `json:"HTTP"` // 健康检查地址
	Interval string `json:"Interval,omitempty"` // Consul 主动进行健康检查
	TTL string `json:"TTL,omitempty"` // 服务实例主动提交健康检查,与Interval只存其一
}

type Weights struct {
	Passing int `json:"Passing"`
	Warning int `json:"Warning"`
}

提交到 Consul 的服务实例信息主要包含:

  • 服务实例ID,用于唯一标记服务实例
  • 服务名,服务实例所属的服务集群
  • Address、Port,服务地址和端口,用于发起服务间调用
  • Check,健康检查信息,包括健康检查地址,健康检查的间隔等。

Consul 中支持由 Consul 主动调用服务实例提供的健康检查接口以维持心跳,和由服务实例主动提交健康检查数据到 Consul 中维持心跳。Check 中的 Interval 和 TTL 的参数分别用于设置两者的检查间隔时长,只能设置其中之一。我们的微服务采用主动检查的方式,提供 /health 接口由 Consul 调用检查。

接着我们定义 diy.ConsulClint 的结构体和它的创建函数,它们位于 ch7-discovery/diy/MyConsulClient.go 下,代码如下所示:

type ConsulClient struct {
	Host string // Consul 的 Host
	Port int // Consul 的 端口
}
func New(consulHost string, consulPort int) *ConsulClient {
	return &ConsulClient{
		Host: consulHost,
		Port: consulPort,
	}
}

获取一个 diy.ConsulClient 需要传递 Consul 的具体地址,即 Consul 的 Host 和 Port。

之后我们在 ch7-discovery/diy/MyConsulClient.go 下实现 ch7-discovery/ConsulClient 接口,并指定方法的接收器为 diy.ConsulClient,空方法代码如下:

func (consulClient *ConsulClient) Register(serviceName, instanceId, healthCheckUrl string, instancePort int, meta map[string]string, logger *log.Logger) bool{
	return false
}

func (consulClient *ConsulClient) DeRegister(instanceId string, logger *log.Logger) bool {
	return false
}

func (consulClient *ConsulClient) DiscoverServices(serviceName string) []interface{}{
	return nil
}


服务注册与健康检查

我们首先实现服务注册的功能,即实现 Register 接口,指定方法的接收器为 diy.ConsulClient。源码位于 ch7-discovery/diy/MyConsulClient.go 下,代码如下所示:

func (consulClient *ConsulClient) Register(serviceName, instanceId, healthCheckUrl string, instancePort int, meta map[string]string, logger *log.Logger) bool{

	// 获取服务的本地IP
	instanceHost := ch7_discovery.GetLocalIpAddress()

	// 1.封装服务实例的元数据
	instanceInfo := &InstanceInfo{
		ID:      instanceId,
		Name:    serviceName,
		Address: instanceHost,
		Port:    instancePort,
		Meta: meta,
		EnableTagOverride: false,
		Check: Check{
			DeregisterCriticalServiceAfter: "30s",
			HTTP:                           "http://" + instanceHost + ":" + strconv.Itoa(instancePort) + healthCheckUrl,
			Interval:						"15s",
		},
		Weights: Weights{
			Passing: 10,
			Warning: 1,
		},
	}

	byteData,_ := json.Marshal(instanceInfo)

	// 2. 向 Consul 发送服务注册的请求
	req, err := http.NewRequest("PUT",
		"http://" + consulClient.Host + ":" + strconv.Itoa(consulClient.Port) + "/v1/agent/service/register",
		bytes.NewReader(byteData))

	if err == nil {
		req.Header.Set("Content-Type", "application/json;charset=UTF-8")
		client := http.Client{}
		resp, err := client.Do(req)
		// 3. 检查注册结果
		if err != nil {
			log.Println("Register Service Error!")
		} else {
			resp.Body.Close()
			if resp.StatusCode == 200 {
				log.Println("Register Service Success!")
				return true;
			} else {
				log.Println("Register Service Error!")
			}
		}
	}
	return false
}

Register 方法中主要执行了以下操作:

  1. 将服务实例数据封装为 InstanceInfo,这其中我们设定了服务实例ID、服务名、服务地址、服务端口等关键数据,并指定了健康检查的地址为 /health,检查时间间隔为 15s。DeregisterCriticalServiceAfter 参数定义了如果 30s 内健康检查失败,该服务实例将被 Consul 主动下线。
  2. 通过 HTTP 的方式向 Consul 发起注册请求,将上一步的封装好的 InstanceInfo 提交到注册表中,服务注册的地址为 /v1/agent/service/register。

在 main 函数中,我们定义了服务启动时会首先调用 ConsulClient#Register 发起服务注册。可以通过在 ch7-discovery 目录下启动该微服务以验证服务注册和健康检查的效果,启动命令如下:

 go run main/SayHelloService.go

可以看到命令行中打出了对应的启动和健康检查日志:

2019/07/08 20:45:21 Register Service Success!
2019/07/08 20:45:22 Health check starts!

访问 Consul 的主页面 http://localhost:8500,可以看到 SayHello 服务已经注册到 Consul 中,如图所示:

Register1.png

直接点击页面中的 SayHello 服务,能够进入到服务集群页面,查看该集群下的服务实例信息,如图所示:

Register2.png

上图中显示了我们注册上去的服务实例ID、地址和端口等关键信息。

小结

本文主要实现了微服务实例与 Consul 交互过程,以及服务注册与健康检查的实现。那么服务注册之后如何注销,以及如何让其他服务发现呢?

下面的文章将会继续实现服务注销与服务发现的功能。

完整代码,从我的Github获取,github.com/longjoy/mic…

往期推荐

  1. 你知道“现代计算机之父” 冯·诺依曼提出的博弈论吗?
  2. 如何学习 etcd?|我的新书出版啦 3.自己动手实现 Go 的服务注册与发现(上)

阅读最新文章,关注公众号:aoho求索