go-kit 微服务框架使用总结之——服务注册与发现实践

Go 微服务框架实践

go-kit 框架

启动服务注册与发现中心

服务注册与发现中心使用的时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交互。

  • 使用go-kit 框架创建的项目一般分为三层:

image-20210819155308278.png

  • 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-kitgithub.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, 来与服务注册中心交互:

    image-20210819160548143.png

    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文件流程如下:

image-20210819165309177.png

  1. 先声明demo服务地址和服务名

    consul 地址, 全局上shi下文, errChan(将服务错误信息和服务系统终止信号写入)

  2. 声明服务发现客户端并初始化客户端,初始化失败关闭服务,初始化成功,使用该客户端声明并初始化 Service

  3. 使用初始化后的Service 创建各个endpoint

  4. 创建http.handler, 将全局上下文,所有endpoint, logger 传入

  5. 创建一个goroutine 启动http server

    启动前该goroutine 先通过consul 客户端注册demo 服务至consul ,如果注册失败将失败信息写入errChan, 并关闭服务。

    然后使用上边创建的handler 创建并监听服务。

  6. 创建一个goroutine 监听系统信号,将ctrl + c 等终止信号写入errChan

  7. 主线程从errChan 中取出错误信息,没有错误信息处于阻塞状态,当有错误信息时,服务退出并取消注册

demo 服务注册成功后,查看Consul 的web 界面,发现多了个服务实例:就是我们注册的名称为SayHello的服务实例:

image-20210825091537147.png

image-20210825091957804.png

当停止服务后,注册中心如下:

image-20210825092348584.png

  • 上边通过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"
    )
    

image-20210830104514182.png

构造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交互次数:

image-20210830145742053.png

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
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一切如来心秘密

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值