你好,我是aoho,今天和大家分享的是动手实现 Go 的服务注册与发现!
通过服务发现与注册中心,可以很方便地管理系统中动态变化的服务实例信息。与此同时,它也可能成为系统的瓶颈和故障点。因为服务之间的调用信息来自于服务注册与发现中心,当它不可用时,服务之间的调用可能无法正常进行。因此服务发现与注册中心一般会多实例部署,提供高可用性和高稳定性。
我们将基于 Consul 实现 Golang Web 的服务注册与发现。首先我们会通过原生态的方式,直接通过 HTTP 方式与 Consul 进行交互;然后我们会通过 Go Kit 框架提供的 Consul Client 接口实现与 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 地址,如图所示:
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{}
}
代码中提供了三个接口,分别是:
接着我们定义一个简单的服务 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 函数中,主要进行了以下的工作:
在服务关闭之前,我们会调用 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