etcd是一个可靠的分布式KV存储数据库,由CoreOS开源。Kuberentes使用etcd作为其存储引擎,随着云原生的火热,etcd也逐渐广泛应用起来。
etcd除了作为普通的KV存储、配置存储,还可以用在以下分布式场景中:
- 服务发现
- 分布式锁
- 选主
- 分布式队列
- 分布式系统协调
本文结合Golang来编写对应的中间件,所有代码见 github.com/qingwave/go…
服务发现
在分布式系统中,如何能找到所需要访问的服务即服务发现。服务较少时可以直接访问其IP,但随着业务规模的扩大,维护其地址越来越复杂,如果服务频繁的扩缩容,必须能够实时感应服务的断点变化。 通常有多种方式可以解决
- 系统级别,如LVS、DNS、Kubernetes中的Service、Istio等
- 微服务注册中心,如Spring Cloud中的Enruka,Dubbo等
- 借助分布式协调系统etcd、ZK、Consul等
服务发现提供的功能包括:
- 服务注册、注销
- 服务宕机或异常时,自动注销
- 感知服务端点变化
借助etcd实现服务发现
- 可以通过将端点写同一个目录(相同前缀,如/services/job/endpoint1, /services/job/endpoint2),并通过Lease设置一个过期时间,不断刷新Lease,如果服务宕机,Lease过期对应端点会自动删除
- 通过Watch API可以监听端点变化
主要代码如下
func New(config EtcdDiscoveryConfig) (*EtcdDiscovery, error) { // 创建session,session会自动续约 session, err := concurrency.NewSession(config.Client, concurrency.WithTTL(config.TTLSeconds)) if err != nil { return nil, err } config.Prefix = strings.TrimSuffix(config.Prefix, "/") + "/" return &EtcdDiscovery{ EtcdDiscoveryConfig: config, session: session, myKey: config.Prefix + config.Key, services: make(map[string]string), }, nil } func (d *EtcdDiscovery) Register(ctx context.Context) error { lease := d.session.Lease() // 注册服务 _, err := d.Client.Put(ctx, d.myKey, d.Val, clientv3.WithLease(lease)) return err } func (d *EtcdDiscovery) UnRegister(ctx context.Context) error { // 注销服务 _, err := d.Client.Delete(ctx, d.myKey) return err } // 监听端点变化 func (d *EtcdDiscovery) Watch(ctx context.Context) error { // context用来停止监听 d.watchContext, d.watchCancel = context.WithCancel(ctx) // 首先获取所有端点 resp, err := d.Client.Get(d.watchContext, d.Prefix, clientv3.WithPrefix()) services := make(map[string]string) for _, kv := range resp.Kvs { services[string(kv.Key)] = string(kv.Value) } d.setServices(services) // 回调点,用户可自定义 if d.Callbacks.OnStartedDiscovering != nil { d.Callbacks.OnStartedDiscovering(d.ListServices()) } defer func() { if d.Callbacks.OnStoppedDiscovering != nil { d.Callbacks.OnStoppedDiscovering() } }() defer d.watchCancel() // 监听目录,通过WithPrefix可以添加子目录变化 ch := d.Client.Watch(d.watchContext, d.Prefix, clientv3.WithPrefix()) for { select { case <-d.watchContext.Done(): return nil case wr, ok := <-ch: if !ok { return fmt.Errorf("watch closed") } if wr.Err() != nil { return wr.Err() } // 将添加事件同步到本地端点列表 for _, ev := range wr.Events { key, val := string(ev.Kv.Key), string(ev.Kv.Value) switch ev.Type { case mvccpb.PUT: d.addService(key, val) case mvc