jupiter-etcd 客户端介绍

950 阅读4分钟

创建客户端

我们来看看创建客户端的代码:

func newClient(config *Config) *Client {
    // etcdv3 的配置
	conf := clientv3.Config{
		Endpoints:            config.Endpoints,
		DialTimeout:          config.ConnectTimeout,
		DialKeepAliveTime:    10 * time.Second,
		DialKeepAliveTimeout: 3 * time.Second,
		DialOptions: []grpc.DialOption{
			grpc.WithBlock(),
			grpc.WithUnaryInterceptor(grpcprom.UnaryClientInterceptor),
			grpc.WithStreamInterceptor(grpcprom.StreamClientInterceptor),
		},
		AutoSyncInterval: config.AutoSyncInterval,
	}
    ...
    
    // 调用 clientv3 方法连接
	client, err := clientv3.New(conf)

	if err != nil {
		config.logger.Panic("client etcd start panic", xlog.FieldMod(ecode.ModClientETCD), xlog.FieldErrKind(ecode.ErrKindAny), xlog.FieldErr(err), xlog.FieldValueAny(config))
	}
}

获取和存储

etcdv3 的 kv 接口:


type KV interface {
	Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
	Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
	Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
	Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
	Do(ctx context.Context, op Op) (OpResponse, error)
	Txn(ctx context.Context) Txn
}
  • Put,Get,Delete 三个方法其实是 Do() 方法的具体行为的封装。

获取和通过前缀获取


// GetKeyValue queries etcd key, returns mvccpb.KeyValue
func (client *Client) GetKeyValue(ctx context.Context, key string) (kv *mvccpb.KeyValue, err error) {
	rp, err := client.Client.Get(ctx, key)
	...
}

// GetPrefix get prefix
func (client *Client) GetPrefix(ctx context.Context, prefix string) (map[string]string, error) {
	resp, err := client.Get(ctx, prefix, clientv3.WithPrefix())
	if err != nil {
		return vars, err
	}
	...
}

获取单个 key/value 和 通过前缀获取 key/value 的区别在于通过一个 OpOption 方法 WithPrefix() 。

存储

存储在 jupiter 中没有单独进行封装,而是直接调用 etcdv3 的客户端方法。

func (kv *kv) Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error) {

etcdv3 客户端库中的 Put 方法。在 grpc 服务注册就是讲服务信息写入到 etcd 中。

删除


// DelPrefix 按前缀删除
func (client *Client) DelPrefix(ctx context.Context, prefix string) (deleted int64, err error) {
	resp, err := client.Delete(ctx, prefix, clientv3.WithPrefix())
	...
}

根据前缀删除信息。

获取多个 key 的值

在 etcd 中要想获取到多个不同 key 的值,除了一个一个获取之外,可以通过 etcd 的事务方法 Txn() 来查询多个 key 的结果。

// GetValues queries etcd for keys prefixed by prefix.
func (client *Client) GetValues(ctx context.Context, keys ...string) (map[string]string, error) {
	var (
		firstRevision = int64(0)
		vars          = make(map[string]string)
		maxTxnOps     = 128 // 最大的提交数
		getOps        = make([]string, 0, maxTxnOps)
	)

	// 具体的事务查询处理方法
	doTxn := func(ops []string) error {
		txnOps := make([]clientv3.Op, 0, maxTxnOps)

		// 添加查询操作
		for _, k := range ops {
			txnOps = append(txnOps, clientv3.OpGet(k,
				clientv3.WithPrefix(),
				clientv3.WithSort(clientv3.SortByKey, clientv3.SortDescend),
				clientv3.WithRev(firstRevision)))
		}

		// 提交事务
		result, err := client.Txn(ctx).Then(txnOps...).Commit()
		if err != nil {
			return err
		}
		// 处理返回的结果
		for i, r := range result.Responses {
			...
		}
		// 获取修订的最新版本号
		if firstRevision == 0 {
			firstRevision = result.Header.GetRevision()
		}
		return nil
	}

	for _, key := range keys {
		// 添加需要提交的key,同时判断是否到达最大提交数,到达则提交进行事务查询
		getOps = append(getOps, key)
		if len(getOps) >= maxTxnOps {
			if err := doTxn(getOps); err != nil {
				return vars, err
			}
			getOps = getOps[:0]
		}
	}
	// 判断是否存在还未提交的 key,有则进行查询
	if len(getOps) > 0 {
		if err := doTxn(getOps); err != nil {
			return vars, err
		}
	}
	return vars, nil
}

方法开始声明了 maxTxOps 最大提交数。doTxn 方法是实际事务查询函数,然后将 keys 分成 maxTxOps 最大数一份进行提交查询。这样保证不会因为 keys 大多而查询很慢而超时,也保证了速度。

持续监控

下面是创建一个批量持续监控 key 的变化:

// NewWatch 创建持续监控
func (client *Client) WatchPrefix(ctx context.Context, prefix string) (*Watch, error) {
	resp, err := client.Get(ctx, prefix, clientv3.WithPrefix())
	if err != nil {
		return nil, err
	}

	var w = &Watch{
		revision:     resp.Header.Revision,
		eventChan:    make(chan *clientv3.Event, 100),
		incipientKVs: resp.Kvs,
	}

	xgo.Go(func() {
		ctx, cancel := context.WithCancel(context.Background())
		w.cancel = cancel
		// 请求监控
		rch := client.Client.Watch(ctx, prefix, clientv3.WithPrefix(), clientv3.WithCreatedNotify(), clientv3.WithRev(w.revision))
		for {
			for n := range rch {
				...
				for _, ev := range n.Events {
					select {
					case w.eventChan <- ev:
					default:
						xlog.Error("watch etcd with prefix", xlog.Any("err", "block event chan, drop event message"))
					}
				}
			}
			ctx, cancel := context.WithCancel(context.Background())
			w.cancel = cancel
			if w.revision > 0 {
				rch = client.Watch(ctx, prefix, clientv3.WithPrefix(), clientv3.WithCreatedNotify(), clientv3.WithRev(w.revision))
			} else {
				rch = client.Watch(ctx, prefix, clientv3.WithPrefix(), clientv3.WithCreatedNotify())
			}
		}
	})

	return w, nil
}

这里通过 etcdv3 客户端的 Watch() 方法请求到一个监控通道,然后不断的接收 WatchChan 类型的值,它实际是一个 WatchResponse 结构体,有 etcd 推送过来的返回值。然后我们不断的处理返回值中的事件 Events 。

一个 Event 的有 PUT、DELETE 两种类型,表示更新和删除某些 key/value 。

租约机制

首先可以了解一下分布式系统技术系列--租约(lease)

在 etcd 中租约机制的本质是给 k-v 设置一个过期时间,之后服务每隔一段时间需要对 k-v 进行续约,不然 k-v 会被自动清理掉。

创建租约代码:

// 创建一个租约
func (reg *etcdv3Registry) getSession(k string, opts ...concurrency.SessionOption) (*concurrency.Session, error) {
	...
	sess, err := concurrency.NewSession(reg.client.Client, opts...)
	if err != nil {
		return sess, err
	}
	reg.rmu.Lock()
	reg.sessions[k] = sess
	reg.rmu.Unlock()
	return sess, nil
}

// 使用租约
if ttl := reg.Config.ServiceTTL.Seconds(); ttl > 0 {
	sess, err := reg.getSession(key, concurrency.WithTTL(int(ttl)))
	if err != nil {
		return err
	}
	opOptions = append(opOptions, clientv3.WithLease(sess.Lease()))
}

// 提交信息到 etcd
_, err := reg.client.Put(readCtx, key, val, opOptions...)

租约的创建时在 etcd 库的 concurrency 包中。使用的方式也很简单,在请求的 options 中加上 WithLease() 的参数。

分布式锁

etcd 的分布式锁和我们平常使用的 sync.Mutex 很类似,有两个方法 Lock() 和 Unlock() 。


// Mutex ...
type Mutex struct {
	s *concurrency.Session
	m *concurrency.Mutex
}

// NewMutex ...
func (client *Client) NewMutex(key string, opts ...concurrency.SessionOption) (mutex *Mutex, err error) {
	mutex = &Mutex{}
	// 默认session ttl = 60s
	mutex.s, err = concurrency.NewSession(client.Client, opts...)
	if err != nil {
		return
	}
	mutex.m = concurrency.NewMutex(mutex.s, key)
	return
}

// Lock ...
func (mutex *Mutex) Lock(timeout time.Duration) (err error) {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()
	return mutex.m.Lock(ctx)
}

// Unlock ...
func (mutex *Mutex) Unlock() (err error) {
	err = mutex.m.Unlock(context.TODO())
	if err != nil {
		return
	}
	return mutex.s.Close()
}

不同的地方在于分布式锁都有一个过期时间。

参考

文章系列