golang微服务之注册与发现
在微服务中,服务注册与发现是必不可少的一环,其中etcd,zookeeper,consul在golang中较为常用。
程序对于这种中间件的依赖,都建议加一层接口,基于接口去实现
下面会分层三大板块去说明
接口定义
因为是上层调用是基于接口调用的,所以需要将discover和register接口化。 在discover发现endpoint有变化时,需要调用callback去更新本地的缓存,所以也需要endpoint的接口。如下
const (
EtcdBackend = "etcd"
ZookeeperBackend = "zookeeper"
ConsulBackend = "consul"
)
// 服务发现接口
type Discover interface {
// Start watch with block, 需要一个callback去更新本地endpoint
Start(callback EndpointCacher)
Stop()
}
// 服务注册接口
type Register interface {
Start() error
Stop() error
}
// endpoint接口
type EndpointCacher interface {
AddOrUpdate(endpoint string, attribute []byte)
Delete(endpoint string)
AddError(err error)
Error(err error)
}
接口定义完了,需要有连接注册中心的信息配置。
// 服务发现端配置
type DiscoverConfig struct {
BackendType string // one of etcd|consul|zookeeper
BackendEndPoints []string // register backend endpoint
DiscoverPrefix string
ServiceName string
HostName string
}
// 注册端配置
type RegisterConfig struct {
BackendType string // one of etcd|consul|zookeeper
BackendEndPoints []string // register backend endpoint
DiscoverPrefix string
ServiceName string
HeartBeatPeriod int64
ServiceEndPoint string // register service endpoint to backend
Attr string // custom attribute. like: {"hostname": "xxx", "weight": 1}
HealthCheckEndPoint string
}
有了配置之后,需要有注册和服务发现实例的创建方法
// NewDiscover 创建一个服务发现实例
func NewDiscover(cfg *DiscoverConfig) (Discover, error) {
switch cfg.BackendType {
case EtcdBackend:
return newEtcdDiscover(cfg)
case ConsulBackend:
return newConsulDiscover(cfg)
case ZookeeperBackend:
return newZookeeperDiscover(cfg)
}
return nil, fmt.Errorf("unknown backend: %s, use etcd|consul|zookeeper", cfg.BackendType)
}
// NewRegister 创建一个注册实例
func NewRegister(cfg *RegisterConfig) (Register, error) {
switch cfg.BackendType {
case EtcdBackend:
return newEtcdRegister(cfg)
case ConsulBackend:
return newConsulRegister(cfg)
case ZookeeperBackend:
return newZookeeperRegister(cfg)
}
return nil, fmt.Errorf("unknown backend: %s, use etcd|consul|zookeeper", cfg.BackendType)
}
这里实现了简易版的endpoint,可供参考,主要是将endpoint存在map中,然后有变化时做变更
// LiteEndpoint EndpointCacher lite impl
type LiteEndpoint struct {
Endpoints map[string][]byte `json:"value"`
lock sync.Mutex
Err error
}
func NewLiteEndpoint() *LiteEndpoint {
return &LiteEndpoint{
Endpoints: map[string][]byte{
},
lock: sync.Mutex{
},
}
}
func (e *LiteEndpoint) AddOrUpdate(endpoint string, attribute []byte) {
e.lock.Lock()
defer e.lock.Unlock()
e.Endpoints[endpoint] = attribute
}
func (e *LiteEndpoint) Delete(endpoint string) {
e.lock.Lock()
defer e.lock.Unlock()
delete(e.Endpoints, endpoint)
}
func (e *LiteEndpoint) Error(err error) {
e.Err = err
}
func (e *LiteEndpoint) List() []string {
var endpointSlice []string
for k, _ := range e.Endpoints {
endpointSlice = append(endpointSlice, k)
}
return endpointSlice
}
func (e *LiteEndpoint) Attr(endpoint string) []byte {
return e.Endpoints[endpoint]
}
接口的具体实现
etcd
注册服务
服务注册到etcd后,利用etcd租期的特性,每次续租几秒,在续期过期前完成续租。当实例异常时无法续租,则会在etcd端该实例会被过期删除,达到下线异常节点的效果。
func newEtcdRegister(cfg *RegisterConfig) (*etcdRegister, error) {
var err error
etcdClient, err := clientv3.New(clientv3.Config{
Endpoints: cfg.BackendEndPoints})
if err != nil {
return nil, err
}
r := &etcdRegister{
etcdEndpoints: cfg.BackendEndPoints,
discoverPrefix: cfg.DiscoverPrefix,
serviceName: cfg.ServiceName,
endpoint: cfg.ServiceEndPoint,
attr: cfg.Attr,
ttl: cfg.HeartBeatPeriod,
stopCh: make(chan struct{
}),
etcdClient: etcdClient,
}
return r, nil
}
// Start 开启一个协程
func (r *etcdRegister) Start() error {
go r.keepAlive()
return nil
}
func (r *etcdRegister) Stop() error {
close(r.stopCh)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err := r.etcdClient.Delete(ctx, r.key())
//if r.grpcResolver != nil {
// return r.grpcResolver.Update(ctx, r.key(), grpcnaming.Update{Op: grpcnaming.Delete, Addr: r.endpoint})
//}
return err
}
// 定时续租
func (r *etcdRegister) keepAlive() {
duration := time.Duration(r.ttl) * time.Second
timer := time.NewTimer(duration)
for {
select {
case <-r.stopCh:
return
case <-timer.C:
if r.leaseID > 0 {
if err := r.leaseRenewal(); err != nil {
logrus.Warnf("%s leaseid[%x] keepAlive err: %s, try to reset...", r.endpoint, r.leaseID, err.Error())
r.leaseID = 0
}
} else {
if err := r.register(); err != nil {
logrus.Warnf("register endpoint %s error: %s", r.endpoint, err.Error())