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

4,509 阅读6分钟

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

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

Consul 的安装与启动

在此之前,我们首先需要搭建一个简单的 Consul 服务,Consul 的下载地址为 www.consul.io/downloads.h… Unix 环境下(Mac、Linux),下载下来的文件是一个二进制可执行文件,可以直接通过它执行 Consul 的相关命令。Window 环境下是一个 .exe 的可执行文件。

以笔者自身的 Linux 环境为例,直接在 consul 文件所在的目录执行:

./consul version

能够直接获取到刚才下载的 consul 的版本:

Consul v1.5.1
Protocol 2 spoken by default,
understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

如果我们想要将 consul 归于系统命令下,可以使用以下命令将 consul 移动到 /usr/local/bin 文件下:

sudo mv consul /usr/local/bin/

接着我们通过以下命令启动 Consul:

consul agent -dev

-dev 选项说明 Consul 以开发模式启动,该模式下会快速部署一个单节点的 Consul 服务,部署好的节点既是 Server 也是 Leader。在生产环境不建议以这种模式启动,因为它不会持久化任何数据,数据仅存在于内存中。

启动好之后就可以在浏览器访问 http://localhost:8500 地址,如图所示:

Consul UI.png

服务注册与发现接口

为了减少代码的重复度,我们首先定义一个 Consul 客户端接口,源码位于 ch7-discovery/ConsulClient.go 下,代码如下所示,

type ConsulClient interface {

	/**
	 * 服务注册接口
	 * @param serviceName 服务名
	 * @param instanceId 服务实例Id
	 * @param instancePort 服务实例端口
	 * @param healthCheckUrl 健康检查地址
	 * @param meta 服务实例元数据
	 */
	Register(serviceName, instanceId, healthCheckUrl string, instancePort int, meta map[string]string, logger *log.Logger) bool

	/**
	 * 服务注销接口
	 * @param instanceId 服务实例Id
	 */
	DeRegister(instanceId string, logger *log.Logger) bool

	/**
	 * 服务发现接口
	 * @param serviceName 服务名
	 */
	DiscoverServices(serviceName string) []interface{}
}

代码中提供了三个接口,分别是:

  • Register,用于服务注册,服务实例将自身所属服务名和服务元数据注册到 Consul 中;
  • DeRegister,用于服务注销,服务关闭时请求 Consul 将自身元数据注销,避免无效请求;
  • DiscoverServices,用于服务发现,通过服务名向 Consul 请求对应的服务实例信息列表。

接着我们定义一个简单的服务 main 函数,它将启动 Web 服务器,使用 ConsulClient 将自身服务实例元数据注册到 Consul,提供一个 /health 端点用于健康检查,并在服务下线时从 Consul 注销自身。源码位于 ch7-discovery/main/SayHelloService.go 中,代码如下所示:

var consulClient ch7_discovery.ConsulClient
var logger *log.Logger

func main()  {

	// 1.实例化一个 Consul 客户端,此处实例化了原生态实现版本
	consulClient = diy.New("127.0.0.1", 8500)
	// 实例失败,停止服务
	if consulClient == nil{
		panic(0)
	}

	// 通过 go.uuid 获取一个服务实例ID
	instanceId := uuid.NewV4().String()
	logger = log.New(os.Stderr, "", log.LstdFlags)
	// 服务注册
	if !consulClient.Register("SayHello", instanceId, "/health", 10086, nil, logger) {
		// 注册失败,服务启动失败
		panic(0)
	}

	// 2.建立一个通道监控系统信号
	exit := make(chan os.Signal)
	// 仅监控 ctrl + c
	signal.Notify(exit, syscall.SIGINT, syscall.SIGTERM)
	var waitGroup sync.WaitGroup
	// 注册关闭事件,等待 ctrl + c 系统信号通知服务关闭
	go closeServer(&waitGroup, exit, instanceId, logger)

	// 3. 在主线程启动http服务器
	startHttpListener(10086)

	// 等待关闭事件执行结束,结束主线程
	waitGroup.Wait()
	log.Println("Closed the Server!")

}

在这个简单的微服务 main 函数中,主要进行了以下的工作:

  1. 实例化 ConsulClient,调用 Register 方法完成服务注册。注册的服务名为SayHello,服务实例 ID 由 UUID 生成,健康检查地址为 /health,服务实例端口为 10086;
  2. 注册关闭事件,监控服务关闭事件。在服务关闭时调用 closeServer 方法进行服务注销和关闭 http 服务器;
  3. 启动 http 服务器。

在服务关闭之前,我们会调用 ConsulClient#Deregister 方法,将服务实例从 Consul 中注销,代码位于 closeServer 方法中,如下所示:

func closeServer( waitGroup *sync.WaitGroup, exit <-chan os.Signal, instanceId string, logger *log.Logger)  {
	// 等待关闭信息通知
	<- exit
	// 主线程等待
	waitGroup.Add(1)
	// 服务注销
	consulClient.DeRegister(instanceId, logger)
	// 关闭 http 服务器
	err := server.Shutdown(nil)
	if err != nil{
		log.Println(err)
	}
	// 主线程可继续执行
	waitGroup.Done()
}

closeServer 方法除了进行服务注销,还会将本地服务的 http 服务关闭。在 startHttpListener 方法中,我们注册了三个 http 接口,分别为 /health 用于 Consul 的健康检查,/sayHello 用于检查服务是否可用,以及 /discovery 用于将从 Consul 中发现的服务实例信息打印出来,代码如下所示:

func startHttpListener(port int)  {
	server = &http.Server{
		Addr: ch7_discovery.GetLocalIpAddress() + ":" +strconv.Itoa(port),
	}
	http.HandleFunc("/health", CheckHealth)
	http.HandleFunc("/sayHello", sayHello)
	http.HandleFunc("/discovery", discoveryService)
	err := server.ListenAndServe()
	if err != nil{
		logger.Println("Service is going to close...")
	}
}

checkHealth 用于处理来自 Consul 的健康检查,我们这里仅是直接简单返回,实际使用时可以检测实例的性能和负载情况,返回有效的健康检查信息。代码如下所示:

func CheckHealth(writer http.ResponseWriter, reader *http.Request) c{
	logger.Println("Health check starts!")
	_, err := fmt.Fprintln(writer, "Server is OK!")
	if err != nil{
		logger.Println(err)
	}
}

discoveryService 从请求参数中获取 serviceName,并调用 ConsulClient#DiscoverServices 方法从 Consul 中发现对应服务的服务实例列表,然后将结果返回到 response 中。代码如下所示:

func discoveryService(writer http.ResponseWriter, reader *http.Request)  {
	serviceName := reader.URL.Query().Get("serviceName")
	instances := consulClient.DiscoverServices(serviceName)
	writer.Header().Set("Content-Type", "application/json")
	err := json.NewEncoder(writer).Encode(instances)
	if err != nil{
		logger.Println(err)
	}
}

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

小结

仅有服务注册与发现中心是不够,还需要各个服务实例的鼎力配合,整个服务注册与发现体系才能良好运作。一个服务实例需要完成以下的事情:

  • 在服务启动阶段,提交自身服务实例元数据到服务发现与注册中心,完成服务注册;
  • 服务运行阶段,定期和服务注册与发现中心维持心跳,保证自身在线状态。如果可能,还会检测自身元数据的变化,在服务实例信息发生变化时重新提交数据到服务注册与发现中心;
  • 在服务关闭时,向服务注册与发现中心发出下线请求,注销自身在注册表中的服务实例元数据。

下面的文章将会继续实现微服务与 Consul 的注册与服务查询等交互。

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

往期推荐

  1. 你知道“现代计算机之父” 冯·诺依曼提出的博弈论吗?
  2. 如何学习 etcd?|我的新书出版啦

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