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

你好,我是aoho,今天和大家分享的是动手实现 Go 的服务注册与发现!

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

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

Consul 的安装与启动

在此之前,我们首先需要搭建一个简单的 Consul 服务,Consul 的下载地址为 https://www.consul.io/downloads.html,根据操作系统的不同进行下载。在 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 地址,如图所示:


自己动手实现 Go 的服务注册与发现(上)
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"10086nil, 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获取,https://github.com/longjoy/micro-go-book

往期推荐

  1. 如何学习 etcd?|我的新书出版啦
  2. 如何与 etcd 服务端进行通信?客户端 API 实践与核心方法介绍
  3. 今年更新的Go 语言入门系列文章汇总,未完...


 

觉得好的话记得打赏赞助小灰灰哦,小灰灰灰更有动力的,谢谢

小灰灰

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: