服务注册与发现中心使用的时Consul
,从官网下载https://www.consul.io/docs/agent对应系统的Consul。
windows 案例:
下载安装完后是个exe可执行文件,再所在文件目录执行./consul.exe version
显示版本号,./consul.exe agent -dev
以开发模式启动
linux 案例:
下载完后是个可执行文件consul
, 将该可执行文件加入bin 目录下sudo mv consul /usr/local/bin/
, 以开发模式启动consul agent -dev
注:开发模式不适用与生产,生产模式启动参照官方文档。
使用go-kit 框架通过HTTP的方式与服务与注册中心Consul
交互。
service层
:业务逻辑实现层。实践demo该层接口实现了三个服务方法:
HealthCheck()
, 检查注册到服务发现中心的服务(也就是下边的打招呼方法)是否正常。SayHello()
,需要注册到服务注册与发现中心的方法。DiscoveryService(ctx context.Context, serviceName string) ([]interface{}, error)
,以注册至服务注册中心的服务名(比如上边的"SayHello")为参数,访问服务注册与发现中心提供的服务发现接口,返回服务注册中心已注册的服务实例信息。endpoint层
:一个需要注册到服务与注册中心的项目,并不是所有的业务逻辑代码都需要注册,service 层方法是对业务逻辑代码的封装,那么endpoint 层就是对需要注册到服务注册与发现中心的service 层部分方法的进一步包装,相当于RPC 服务对外提供的方法。
实践demo endpoint 封装的service 方法如下:
type DiscoveryEndpoints struct {
SayHelloEndpoint endpoint.Endpoint
DiscoveryEndpoint endpoint.Endpoint
HealthCheckEndpoint endpoint.Endpoint
}
endpoint
层 需要接受请求并返回响应,所以需要构造每个endpoint
对应的service
服务的请求结构体和响应结构体,demo 的三个endpoint 的请求与响应结构体如下:
// SayHello 服务请求结构体
type SayHelloRequest struct {
}
// SayHello 服务响应结构体
type SayHelloResponse struct {
Message string `json:"message"`
}
// 服务发现请求结构体
type DiscoveryRequest struct {
ServiceName string
}
// 服务发现响应结构体
type DiscoveryResponse struct {
Instances []interface{} `json:"instances"`
Error string `json:"error"`
}
// 健康检查请求结构体
type HealthRequest struct{}
// 健康检查响应结构体
type HealthResponse struct {
Status bool `json:"status"`
}
transport层
:项目对外提供服务的入口,将对应的请求url 转发至对应的endpoint。
项目demo 使用的是go-kit
的github.com/go-kit/kit/transport/http
下的NewServer
方法,该方法需要传入对应的endpoint
以及对应endpoint
反序列化request func和序列化 response func ,以及 其他可选参数。
项目demo 的三个endpoint 请求处理方法如下:
options := []kithttp.ServerOption{
kithttp.ServerErrorHandler(transport.NewLogErrorHandler(logger)),
kithttp.ServerErrorEncoder(encodeError),
}
r.Methods("GET").Path("/say-hello").Handler(kithttp.NewServer(
endpoints.SayHelloEndpoint,
decodeSayHelloRequest,
encodeJsonResponse,
options...,
))
r.Methods("GET").Path("/discovery").Handler(kithttp.NewServer(
endpoints.DiscoveryEndpoint,
decodeDiscoveryRequest,
encodeJsonResponse,
))
r.Methods("GET").Path("/health").Handler(kithttp.NewServer(
endpoints.HealthCheckEndpoint,
decodeHealthCheckRequest,
encodeJsonResponse,
options...,
))
整个实践demo 项目作为一个微服务注册到服务注册与发现中心Consul
, 项目通过实现Consul
的客户端discoveryClient
, 来与服务注册中心交互:
type DiscoveryClient interface {
/**
* 服务注册接口
* @param serviceName 服务名
* @param instanceId 服务实例Id
* @param instancePort 服务实例端口
* @param healthCheckUrl 健康检查地址
* @param instanceHost 服务实例地址
* @param meta 服务实例元数据
*/
Register(serviceName, instanceId, healthCheckUrl string, instanceHost string, instancePort int,
Meta map[string]string, logger *log.Logger) bool
/**
* @Description 服务注销接口
* @Param instanceId 服务实例Id
* @return bool
**/
DeRegister(instanceId string, logger *log.Logger) bool
/**
* @Description 发现服务实例接口
* @Param serviceName 服务名
* @return []interface{}
**/
DiscoverServices(serviceName string, logger *log.Logger) []interface{}
}
demo 项目的启动入口main
文件流程如下:
先声明demo服务地址和服务名
consul 地址, 全局上shi下文, errChan(将服务错误信息和服务系统终止信号写入)
声明服务发现客户端并初始化客户端,初始化失败关闭服务,初始化成功,使用该客户端声明并初始化 Service
使用初始化后的Service 创建各个endpoint
创建http.handler, 将全局上下文,所有endpoint, logger 传入
创建一个goroutine 启动http server
启动前该goroutine 先通过consul 客户端注册demo 服务至consul ,如果注册失败将失败信息写入errChan, 并关闭服务。
然后使用上边创建的handler 创建并监听服务。
创建一个goroutine 监听系统信号,将ctrl + c 等终止信号写入errChan
主线程从errChan 中取出错误信息,没有错误信息处于阻塞状态,当有错误信息时,服务退出并取消注册
demo 服务注册成功后,查看Consul 的web 界面,发现多了个服务实例:就是我们注册的名称为SayHello
的服务实例:
当停止服务后,注册中心如下:
上边通过HTTP交互的方式实现自定义的与Consul 交互的client , 但是go-kit 框架已内置了与consul服务注册中心交互的包:
import (
"github.com/go-kit/kit/sd/consul"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/api/watch"
"log"
"strconv"
"sync"
)
构造kit discover client:
在于consul 交互时,需要使用"github.com/hashicorp/consul/api"
func NewKitDiscoverClient(consultHost string, consulPort int) (DiscoveryClient, error) {
// 通过Consul Host 和 Consul Port 创建一个 consul.Client
consulConfig := api.DefaultConfig()
consulConfig.Address = consultHost + ":" + strconv.Itoa(consulPort)
apiClient, err := api.NewClient(consulConfig)
if err != nil {
return nil, err
}
client := consul.NewClient(apiClient)
return &KitDiscoverClient{
Host: consultHost,
Port: consulPort,
config: consulConfig,
client: client,
}, err
}
注册:
使用go-kit 框架内置client 与consul 注册时不需要自定义服务实例信息,使用api.AgentServiceRegistration
构建服务实例元数据:
func (consulClient *KitDiscoverClient) Register(serviceName, instanceId, healthCheckUrl string, instanceHost string, instancePort int, Meta map[string]string, logger *log.Logger) bool {
// 1. 构建服务实例元数据
serviceRegistration := &api.AgentServiceRegistration{
ID: instanceId,
Name: serviceName,
Address: instanceHost,
Port: instancePort,
Meta: Meta,
Check: &api.AgentServiceCheck{
DeregisterCriticalServiceAfter: "30s",
HTTP: "http://" + instanceHost + ":" + strconv.Itoa(instancePort) + healthCheckUrl,
Interval: "15s",
},
}
// 2. 发送服务注册到 Consul 中
err := consulClient.client.Register(serviceRegistration)
if err != nil {
log.Println("Register Service Error")
return false
}
log.Println("Register Service Success!")
return true
}
注销:
注销也需要通过api.AgentServiceRegistration
构建服务实例信息:
func (consulClient *KitDiscoverClient) DeRegister(instanceId string, logger *log.Logger) bool {
// 构建包含服务实例 ID 的元数据结构体
serviceRegistration := &api.AgentServiceRegistration{
ID: instanceId,
}
// 发送服务注销请求
err := consulClient.client.Deregister(serviceRegistration)
if err != nil {
logger.Println("Deregister Service Error!")
return false
}
log.Println("Deregister Service Success!")
return true
}
发现服务实例:
发现服务实例,使用了原子锁和字典来缓存服务实例信息列表,并通过Consul提供的Watch 机制监控该服务名下服务实例数据的变化,减少与Consul HTTP交互次数:
func (consulClient *KitDiscoverClient) DiscoverServices(serviceName string, logger *log.Logger) []interface{} {
// 该服务已监控并缓存
instanceList, ok := consulClient.instancesMap.Load(serviceName)
if ok {
return instanceList.([]interface{})
}
// 申请锁
consulClient.mutex.Lock()
defer consulClient.mutex.Unlock()
// 再次检查是否缓存
instanceList, ok = consulClient.instancesMap.Load(serviceName)
if ok {
return instanceList.([]interface{})
}else {
go func() {
// 使用consul 服务实例监控某个服务的实例列表变化
params := make(map[string]interface{})
params["type"] = "service"
params["service"] = serviceName
plan, _ := watch.Parse(params)
plan.Handler = func(u uint64, i interface{}) {
if i == nil {
return
}
v, ok := i.([]*api.ServiceEntry)
if !ok {
return
}
// 没有服务实例在线
if len(v) == 0 {
consulClient.instancesMap.Store(serviceName, []interface{}{})
}
var healthServices []interface{}
for _, service := range v {
if service.Checks.AggregatedStatus() == api.HealthPassing {
healthServices = append(healthServices, service.Service)
}
}
consulClient.instancesMap.Store(serviceName, healthServices)
}
defer plan.Stop()
plan.Run(consulClient.config.Address)
}()
}
// 根据服务名请求服务实例列表
entries, _, err := consulClient.client.Service(serviceName, "", false, nil)
if err != nil {
consulClient.instancesMap.Store(serviceName, []interface{}{})
logger.Println("Discover Service Error!")
return nil
}
instances := make([]interface{}, len(entries))
for i := 0; i < len(instances); i++ {
instances[i] = entries[i].Service
}
consulClient.instancesMap.Store(serviceName, instances)
return instances
}