kratos Consul注册发现使用示例

项目目录

kratos-register  -- 为注册项目示例

kratos-discovery -- 为发现项目示例

kratos-discovery\internal\consul  -- (核心)为本项目封装的基于Consul服务注册发现的demo

kratos-register\cmd\main.go -- 为服务注册代码示例位置

kratos-discovery\api-register  -- 为发现服务调用注册服务的协议文件  

kratos-discovery\api\client.go  -- 为服务发现示例代码位置

kratos-discovery\internal\dao\dao.go  -- 提供service 调用的入口

kratos-discovery\internal\server\service\service.go -- 最终grpc 调用的实际方法

准备前提

服务注册

kratos-register\cmd\main.go

func main(){
    
    // 实例化Discovery
    dis, err := consul.New(&consul.Config{Zone:"zone01", Env:"dev", Region:"region01"})
    if err != nil {
        panic(err)
    }
    // 注册为 resolver
    resolver.Register(dis)

    ip := "192.168.3.87"//你本地服务的ip地址 到时候可以替换
    port := "9002"//你本地grpc 服务的端口
    hn, _ := os.Hostname()
    //dis := discovery.New(nil)
    ins := &naming.Instance{
        Zone:     env.Zone, //时区 从环境变量中读取
        Env:      env.DeployEnv,
        AppID:    "register.service", //服务发现需要用到
        Hostname: hn,
        Addrs: []string{
            "grpc://" + ip + ":" + port,
        },
    }
    cancel, err := dis.Register(context.Background(), ins)
    if err != nil {
        panic(err)
    }
    
    signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
    for {
        s := <-c
        log.Info("get a signal %s", s.String())
        switch s {
        case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
            closeFunc()
            //程序退出时取消consul 注册 windows signal 机制无法命中此方法所以可能在windows环境可能无法模拟取消注册
            cancel()
            log.Info("kratos-register exit")
            time.Sleep(time.Second)
            return
        case syscall.SIGHUP:
            cancel()
        default:
            return
        }
}

服务发现

kratos-discovery\api\client.go  

提供NewRegisterClient 方法给dao层使用。

// AppID .这里的AppId 是你需要发现的服务的AppId
const AppID = "register.service"

func init(){
    // NOTE: 注意这段代码,表示要使用服务发现
    // NOTE: 还需注意的是,resolver.Register是全局生效的,所以建议该代码放在进程初始化的时候执行
    // NOTE: !!!切记不要在一个进程内进行多个不同中间件的Register!!!
    // NOTE: 在启动应用时,可以通过flag(-discovery.nodes) 或者 环境配置(DISCOVERY_NODES)指定discovery节点
    resolver.Register(consul.Builder())
}

// NewRClient new register grpc client 
func NewRClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (register.RegisterClient, error) {
    client := warden.NewClient(cfg, opts...)
    //使用consul的 scheme
    cc, err := client.Dial(context.Background(), fmt.Sprintf("consul://default/%s", AppID))
    if err != nil {
        return nil, err
    }
    return register.NewRegisterClient(cc), nil
}

kratos-discovery\internal\dao\dao.go  

提供注册服务的实例给grpc使用

// Dao dao interface
type Dao interface {
    Close()
    Ping(ctx context.Context) (err error)
    // bts: -nullcache=&model.Article{ID:-1} -check_null_code=$!=nil&&$.ID==-1
    Article(c context.Context, id int64) (*model.Article, error)
    //注册的服务接口约束
    RegisterLogin(ctx context.Context, req *discoverApi.LoginReq) (reply *discoverApi.LoginResp, err error)
}

// dao dao.
type dao struct {
    db          *sql.DB
    redis       *redis.Redis
    mc          *memcache.Memcache
    cache *fanout.Fanout
    demoExpire int32
    //暴露给service 接口使用的register.Client
    client register.RegisterClient
}

func newDao(r *redis.Redis, mc *memcache.Memcache, db *sql.DB) (d *dao, cf func(), err error) {
    var cfg struct{
        DemoExpire xtime.Duration
    }
    if err = paladin.Get("application.toml").UnmarshalTOML(&cfg); err != nil {
        return
    }

    //配置grpc 超时相关配置
    grpccfg := &warden.ClientConfig{
        Dial:              xtime.Duration(time.Second * 10),
        Timeout:           xtime.Duration(time.Second * 1),
        Subset:            50,
        KeepAliveInterval: xtime.Duration(time.Second * 60),
        KeepAliveTimeout:  xtime.Duration(time.Second * 20),
    }
    var rClient register.RegisterClient
    //通过服务发现创建registerClient 实例
    if rClient,err = discoverApi.NewRClient(grpccfg); err != nil {
        panic(err)
    }
    d = &dao{
        db: db,
        redis: r,
        mc: mc,
        cache: fanout.New("cache"),
        demoExpire: int32(time.Duration(cfg.DemoExpire) / time.Second),
        //
        client:rClient,
    }
    cf = d.Close
    return
}

// RegisterLogin 调用服务注册的Login方法
func (d *dao) RegisterLogin(ctx context.Context, req *discoverApi.LoginReq) (reply *discoverApi.LoginResp, err error) {
    reply2, err := d.client.Login(ctx, (*register.LoginReq)(req))
    reply = (*discoverApi.LoginResp)(reply2)
    return
}

kratos-discovery\internal\server\service\service.go

实现协议文件中RegisterLogin 方法

//LoginUrl 登录服务接口逻辑底层调用的是register服务的登录方法
func (s *Service) LoginUrl(ctx context.Context, req *discoverApi.LoginReq) (reply *discoverApi.LoginResp, err error) {
    fmt.Printf("discovery login username: %s, passwd: %s", req.Username, req.Passwd)
    //调用dao层的注册方法
     reply,err=    s.dao.RegisterLogin(ctx,req)
    return
}

kratos-discovery\internal\consul  -- (核心)为本项目封装的基于Consul服务注册发现的demo

package discovery

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "github.com/go-kratos/kratos/pkg/log"
    "github.com/go-kratos/kratos/pkg/naming"
    "github.com/hashicorp/consul/api"
    "github.com/hashicorp/consul/api/watch"
    "github.com/hashicorp/go-hclog"
    "net/url"
    "strconv"
    "strings"
    "sync"
    "sync/atomic"
    "time"
)

var (
    ERR_INS_ADDRS_EMPTY = errors.New("len of ins.Addrs should not be 0")
)

type logWrapper struct {
}

func (wrapper logWrapper) Write(p []byte) (n int, err error) {
    log.Info(string(p))
    return len(p), nil
}

// Config discovery configures.
type Config struct {
    Nodes  []string
    Region string
    Zone   string
    Env    string
    Host   string
}

// Resolver resolve naming service
type Resolver struct {
    appID   string
    c       chan struct{}
    client  *api.Client
    agent   *api.Agent
    plan    *watch.Plan
    builder *Discovery
    ins     atomic.Value
}

func (resolver *Resolver) watch() error {
    var params map[string]interface{}
    watchKey := fmt.Sprintf(`{"type":"service", "service":"%s"}`, resolver.appID)
    if err := json.Unmarshal([]byte(watchKey), &params); err != nil {
        return err
    }
    plan, err := watch.Parse(params)
    if err != nil {
        return err
    }
    plan.Handler = func(idx uint64, raw interface{}) {
        if raw == nil {
            return // ignore
        }
        v, ok := raw.([]*api.ServiceEntry)
        if !ok {
            return // ignore
        }
        log.Info("consul watch service %s notify, len %d", resolver.appID, len(v))
        ins := resolver.coverServiceEntry2Ins(v)
        resolver.ins.Store(ins)
        resolver.c <- struct{}{}
    }

    logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{}) // replace logger
    go func() {
        err := plan.RunWithClientAndHclog(resolver.client,logger)
        if err != nil {
            log.Error("watch service %s error %s", resolver.appID, err.Error())
        }
    }()
    resolver.plan = plan
    return nil
}

func (resolver *Resolver) Watch() <-chan struct{} {
    return resolver.c
}

func (resolver *Resolver) coverServiceEntry2Ins(serviceArr []*api.ServiceEntry) *naming.InstancesInfo {
    instancesInfo := &naming.InstancesInfo{}
    //instancesInfo.Scheduler = make([]naming.Zone, 0, 10)
    instancesInfo.Instances = make(map[string][]*naming.Instance)
    for _, service := range serviceArr {
        if service.Checks.AggregatedStatus() == api.HealthPassing {
            log.Info("appid %s ip %s port %d pass", resolver.appID, service.Service.Address, service.Service.Port)
            ins := resolver.coverService2Instance(service.Service)
            if _, ok := instancesInfo.Instances[ins.Zone]; !ok {
                instancesInfo.Instances[ins.Zone] = make([]*naming.Instance, 0, 10)
            }
            instancesInfo.Instances[ins.Zone] = append(instancesInfo.Instances[ins.Zone], ins)
        }
    }
    instancesInfo.LastTs = time.Now().Unix()
    return instancesInfo
}

// unused
func (resolver *Resolver) fetch(c context.Context) (*naming.InstancesInfo, bool) {
    _, infoArr, err := resolver.agent.AgentHealthServiceByName(resolver.appID)
    if err != nil {
        log.Error("get AgentHealthServiceByName %s err %s", resolver.appID, err.Error())
        return nil, false
    }
    instancesInfo := &naming.InstancesInfo{}
    //instancesInfo.Scheduler = make([]naming.Zone, 0, 10)
    instancesInfo.Instances = make(map[string][]*naming.Instance)
    log.Info("get AgentHealthServiceByName %s info len %d", resolver.appID, len(infoArr))
    for _, info := range infoArr {
        log.Info("get AgentHealthServiceByName %s info addr %s:%d status: %s", resolver.appID, info.Service.Address, info.Service.Port, info.AggregatedStatus)
        if info.AggregatedStatus != "passing" {
            continue
        }
        ins := resolver.coverService2Instance(info.Service)
        if _, ok := instancesInfo.Instances[ins.Zone]; !ok {
            instancesInfo.Instances[ins.Zone] = make([]*naming.Instance, 0, 10)
        }
        instancesInfo.Instances[ins.Zone] = append(instancesInfo.Instances[ins.Zone], ins)
    }
    instancesInfo.LastTs = time.Now().Unix()
    return instancesInfo, true
}

func (resolver *Resolver) Fetch(c context.Context) (ins *naming.InstancesInfo, ok bool) {
    v := resolver.ins.Load()
    ins, ok = v.(*naming.InstancesInfo)
    return
}

func (resolver Resolver) Close() error {
    if resolver.plan != nil && !resolver.plan.IsStopped() {
        resolver.plan.Stop()
    }
    return nil
}

func (resolver *Resolver) coverService2Instance(service *api.AgentService) *naming.Instance {
    meta := service.Meta
    addr := []string{
        service.Address + ":" + strconv.Itoa(service.Port),
    }
    ins := &naming.Instance{
        Region:   meta["region"],
        Zone:     meta["zone"],
        Env:      meta["env"],
        Hostname: meta["hostname"],
        Version:  meta["version"],
        AppID:    service.Service,
        Addrs:    addr,
    }
    ins.Metadata = make(map[string]string)
    for key, value := range meta {
        if key == "region" || key == "env" || key == "zone" || key == "version" || key == "hostname" {
            continue
        }
        ins.Metadata[key] = value
    }
    ins.LastTs = time.Now().Unix()
    return ins
}

func (builder Discovery) coverIns2AgentService(ins *naming.Instance) ([]*api.AgentServiceRegistration, error) {
    if len(ins.Addrs) == 0 {
        return nil, ERR_INS_ADDRS_EMPTY
    }
    registrationArr := make([]*api.AgentServiceRegistration, len(ins.Addrs))
    meta := make(map[string]string)
    meta["region"] = ins.Region
    meta["zone"] = ins.Zone
    meta["env"] = ins.Env
    meta["hostname"] = ins.Hostname
    meta["version"] = ins.Version
    meta["last_ts"] = strconv.FormatInt(ins.LastTs, 10)

    for key, value := range ins.Metadata {
        meta[key] = value
    }
    for i, addr := range ins.Addrs {
        urlVal, err := url.Parse(addr)
        if err != nil {
            return nil, err
        }
        port, _ := strconv.Atoi(urlVal.Port())
        service := &api.AgentServiceRegistration{
            ID:      ins.AppID + "-" + urlVal.Hostname() + "-" + urlVal.Port(),
            Name:    ins.AppID,
            Kind:    api.ServiceKindTypical,
            Port:    port,
            Address: urlVal.Scheme + "://" + urlVal.Hostname(),
            Meta:    meta,
        }
        registrationArr[i] = service
    }
    return registrationArr, nil
}

func (builder Discovery) Register(ctx context.Context, ins *naming.Instance) (cancel context.CancelFunc, err error) {
    serviceArr, err := builder.coverIns2AgentService(ins)
    if err != nil {
        return
    }

    ctx, cancel = context.WithCancel(ctx)
    defer func() {
        if err != nil { // avoid register partition
            cancel()
        }
    }()
    for _, service := range serviceArr { //@todo 批量注册
        service.Check = &api.AgentServiceCheck{
            TTL:    "15s",
            Status: api.HealthPassing,
        }
        var status string
        var info *api.AgentServiceChecksInfo
        status, info, err = builder.agent.AgentHealthServiceByID(service.ID)
        if err != nil {
            return
        }
        if info == nil && status == api.HealthCritical {
            err = builder.agent.ServiceRegister(service) // @todo check had registered
            if err != nil {
                return
            }
        } else {
            err = builder.agent.PassTTL(fmt.Sprintf("service:%s", service.ID), "I am good :)")
            if err != nil {
                return
            }
        }

        go func(service *api.AgentServiceRegistration) {
            for {
                select {
                case <-ctx.Done():
                    log.Info("ServiceDeregister %s", service.ID)
                    err := builder.agent.ServiceDeregister(service.ID)
                    if err != nil {
                        log.Error("consul: ServiceDeregister %s err: %s", service.ID, err.Error())
                    }
                    return
                case <-time.After(time.Second * 5):
                    err := builder.agent.PassTTL(fmt.Sprintf("service:%s", service.ID), "I am good :)")
                    if err == nil {
                        continue
                    }
                    log.Error("consul: PassTTL %s err: %s", service.ID, err.Error())
                    if strings.Index(err.Error(), "does not have associated TTL") > 0 { // 注册已经失效
                        err = builder.agent.ServiceRegister(service) // consul 下线会导致 有这个 error
                        if err != nil {
                            log.Error("consul: PassTTL %s reRegister err: %s", service.ID, err.Error())
                        }
                    }
                }
            }
        }(service)
    }
    return
}



type Discovery struct {
    client *api.Client
    agent  *api.Agent
    r      map[string]*Resolver
    locker sync.RWMutex
    c      *Config
    ctx        context.Context
    cancelFunc context.CancelFunc
}

func (builder *Discovery) Build(id string, opts ...naming.BuildOpt) naming.Resolver {
    builder.locker.RLock()
    if r, ok := builder.r[id]; ok {
        builder.locker.RUnlock()
        return r
    }
    builder.locker.RUnlock()
    builder.locker.Lock()
    r := &Resolver{
        appID:   id,
        client:  builder.client,
        agent:   builder.agent,
        builder: builder,
    }
    r.c = make(chan struct{}, 10)
    builder.r[id] = r
    builder.locker.Unlock()
    err := r.watch()
    if err != nil {
        log.Error("watch error %s", err.Error())
    }
    return r
}

func (builder *Discovery) Scheme() string {
    return "consul"
}

var (
    _once    sync.Once
    _builder naming.Builder
)

func Builder() naming.Builder {
    _once.Do(func() {
        _builder,_ = New(nil)
    })
    return _builder
}


func New(c *Config) (builder *Discovery, err error) {
    if c == nil {
        c = new(Config)
    }
    client, err := api.NewClient(api.DefaultConfig())
    ctx, cancel := context.WithCancel(context.Background())
    if err != nil {
        return
    }
    builder= &Discovery{
        client:        client,
        agent: client.Agent(),
        ctx:        ctx,
        cancelFunc: cancel,
        c:      c,
        r:  make(map[string]*Resolver),
    }

    return
}

设置环境变量(以goland示例)

 

 

consul 环境变量参考

// HTTPAddrEnvName defines an environment variable name which sets
// the HTTP address if there is no -http-addr specified.
HTTPAddrEnvName = "CONSUL_HTTP_ADDR"

// HTTPTokenEnvName defines an environment variable name which sets
// the HTTP token.
HTTPTokenEnvName = "CONSUL_HTTP_TOKEN"

// HTTPTokenFileEnvName defines an environment variable name which sets
// the HTTP token file.
HTTPTokenFileEnvName = "CONSUL_HTTP_TOKEN_FILE"

// HTTPAuthEnvName defines an environment variable name which sets
// the HTTP authentication header.
HTTPAuthEnvName = "CONSUL_HTTP_AUTH"

// HTTPSSLEnvName defines an environment variable name which sets
// whether or not to use HTTPS.
HTTPSSLEnvName = "CONSUL_HTTP_SSL"

// HTTPCAFile defines an environment variable name which sets the
// CA file to use for talking to Consul over TLS.
HTTPCAFile = "CONSUL_CACERT"

// HTTPCAPath defines an environment variable name which sets the
// path to a directory of CA certs to use for talking to Consul over TLS.
HTTPCAPath = "CONSUL_CAPATH"

// HTTPClientCert defines an environment variable name which sets the
// client cert file to use for talking to Consul over TLS.
HTTPClientCert = "CONSUL_CLIENT_CERT"

// HTTPClientKey defines an environment variable name which sets the
// client key file to use for talking to Consul over TLS.
HTTPClientKey = "CONSUL_CLIENT_KEY"

// HTTPTLSServerName defines an environment variable name which sets the
// server name to use as the SNI host when connecting via TLS
HTTPTLSServerName = "CONSUL_TLS_SERVER_NAME"

// HTTPSSLVerifyEnvName defines an environment variable name which sets
// whether or not to disable certificate checking.
HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY"

// GRPCAddrEnvName defines an environment variable name which sets the gRPC
// address for consul connect envoy. Note this isn't actually used by the api
// client in this package but is defined here for consistency with all the
// other ENV names we use.
GRPCAddrEnvName = "CONSUL_GRPC_ADDR"

 

posted @ 2021-01-13 14:55  雨V幕  阅读(684)  评论(0编辑  收藏  举报