ETCD核心机制解析

微信扫一扫,分享到朋友圈

ETCD核心机制解析

ETCD整体机制

etcd 是一个分布式的、可靠的 key-value 存储系统,它适用于存储分布式系统中的关键数据。

etcd 集群中多个节点之间通过Raft算法完成分布式一致性协同,算法会选举出一个主节点作为 leader,由 leader 负责数据的同步与分发。当 leader 出现故障后系统会自动地重新选取另一个节点成为 leader,并重新完成数据的同步。

etcd集群实现高可用主要是基于quorum机制,即:集群中半数以上的节点可用时,集群才可继续提供服务,quorum机制在分布式一致性算法中应用非常广泛,此处不再详细阐述。

raft数据更新和etcd调用是基于两阶段机制:

第一阶段 leader记录log (uncommited); 日志复制到
follower;follower响应,操作成功,响应客户端; 调用者调用
leader,leader会将kv数据存储在日志中,并利用实时算法raft进行复制

第二阶段 leader commit; 通知
follower; 当复制给了
N+1个节点后,本地提交,返回给客户端,最后leader异步通知follower完成通知

ETCD核心API分析

etcd提供的api主要有kv相关、lease相关及watch,查看其源码可知:

kv相关接口:

type KV interface {
// Put puts a key-value pair into etcd.
// Note that key,value can be plain bytes array and string is
// an immutable representation of that bytes array.
// To get a string of bytes, do string([]byte{0x10, 0x20}).
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
// Get retrieves keys.
// By default, Get will return the value for "key", if any.
// When passed WithRange(end), Get will return the keys in the range [key, end).
// When passed WithFromKey(), Get returns keys greater than or equal to key.
// When passed WithRev(rev) with rev > 0, Get retrieves keys at the given revision;
// if the required revision is compacted, the request will fail with ErrCompacted .
// When passed WithLimit(limit), the number of returned keys is bounded by limit.
// When passed WithSort(), the keys will be sorted.
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
// Delete deletes a key, or optionally using WithRange(end), [key, end).
Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
// Compact compacts etcd KV history before the given rev.
Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
// Txn creates a transaction.
Txn(ctx context.Context) Txn
}

主要有Put、Get、Delete、Compact、Do和Txn方法;Put用于向etcd集群中写入消息,以key value的形式存储;Get可以根据key查看其对应存储在etcd中的数据;Delete通过删除key来删除etcd中的数据;
Compact 方法用于压缩 etcd 键值对存储中的事件历史,避免
事件历史无限制的持续增长; Txn 方法在单个事务中处理多个请求,etcd事务


模式为:

if compare

then op

else op

commit

lease相关接口:

type Lease interface {
// Grant creates a new lease.
Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)
// Revoke revokes the given lease.
Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)
// TimeToLive retrieves the lease information of the given lease ID.
TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)
// Leases retrieves all leases.
Leases(ctx context.Context) (*LeaseLeasesResponse, error)
// KeepAlive keeps the given lease alive forever. If the keepalive response
// posted to the channel is not consumed immediately, the lease client will
// continue sending keep alive requests to the etcd server at least every
// second until latest response is consumed.
//
// The returned "LeaseKeepAliveResponse" channel closes if underlying keep
// alive stream is interrupted in some way the client cannot handle itself;
// given context "ctx" is canceled or timed out. "LeaseKeepAliveResponse"
// from this closed channel is nil.
//
// If client keep alive loop halts with an unexpected error (e.g. "etcdserver:
// no leader") or canceled by the caller (e.g. context.Canceled), the error
// is returned. Otherwise, it retries.
//
// TODO(v4.0): post errors to last keep alive message before closing
// (see https://github.com/coreos/etcd/pull/7866)
KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)
// KeepAliveOnce renews the lease once. The response corresponds to the
// first message from calling KeepAlive. If the response has a recoverable
// error, KeepAliveOnce will retry the RPC with a new keep alive message.
//
// In most of the cases, Keepalive should be used instead of KeepAliveOnce.
KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error)
// Close releases all resources Lease keeps for efficient communication
// with the etcd server.
Close() error
}

lease 是分布式系统中一个常见的概念,用于代表一个分布式租约。典型情况下,在分布式系统中需要去检测一个节点是否存活的时,就需要租约机制。



Grant方法用于创建一个租约,当服务器在给定 time to live 时间内没有接收到 keepAlive 时租约过期;
Revoke撤销一个租约,
所有附加到租约的key将过期并被删除;
TimeToLive 获取租约信息; KeepAlive 通过从客户端到服务器端的流化的 keep alive 请求和从服务器端到客户端的流化的 keep alive 应答来维持租约;






检测分布式系统中一个进程是否存活,可以在进程中去创建一个租约,并在该进程中周期性的调用
KeepAlive 的方法。如果一切正常,该节点的租约会一致保持,如果这个进程挂掉了,最终这个租约就会自动过期,



etcd 中,允许将多个 key 关联在同一个 lease 之上, 可以大幅减少
lease 对象刷新带来的开销。

watch相关接口:

type Watcher interface {
// Watch watches on a key or prefix. The watched events will be returned
// through the returned channel. If revisions waiting to be sent over the
// watch are compacted, then the watch will be canceled by the server, the
// client will post a compacted error watch response, and the channel will close.
// If the context "ctx" is canceled or timed out, returned "WatchChan" is closed,
// and "WatchResponse" from this closed channel has zero events and nil "Err()".
// The context "ctx" MUST be canceled, as soon as watcher is no longer being used,
// to release the associated resources.
//
// If the context is "context.Background/TODO", returned "WatchChan" will
// not be closed and block until event is triggered, except when server
// returns a non-recoverable error (e.g. ErrCompacted).
// For example, when context passed with "WithRequireLeader" and the
// connected server has no leader (e.g. due to network partition),
// error "etcdserver: no leader" (ErrNoLeader) will be returned,
// and then "WatchChan" is closed with non-nil "Err()".
// In order to prevent a watch stream being stuck in a partitioned node,
// make sure to wrap context with "WithRequireLeader".
//
// Otherwise, as long as the context has not been canceled or timed out,
// watch will retry on other recoverable errors forever until reconnected.
//
// TODO: explicitly set context error in the last "WatchResponse" message and close channel?
// Currently, client contexts are overwritten with "valCtx" that never closes.
// TODO(v3.4): configure watch retry policy, limit maximum retry number
// (see https://github.com/etcd-io/etcd/issues/8980)
Watch(ctx context.Context, key string, opts ...OpOption) WatchChan
// RequestProgress requests a progress notify response be sent in all watch channels.
RequestProgress(ctx context.Context) error
// Close closes the watcher and cancels all watch requests.
Close() error
}

etcd 的Watch 机制可以实时地订阅到 etcd 中增量的数据更新,watch 支持指定单个 key,也可以指定一个 key 的前缀。

Watch 观察将要发生或者已经发生的事件,




输入和输出都是流;输入流用于创建和取消观察,输出流发送事件。








一个观察 RPC 可以在一次性在多个key范围上观察,并为多个观察流化事件,








整个事件历史可以从最后压缩修订版本开始观察。




ETCD数据版本机制





etcd数据版本中主要有term表示leader的任期,




revision 代表的是全局数据的版本。

当集群发生
Leader 切换,term 的值就会 +1,在节点故障,或者 Leader 节点网络出现问题,再或者是将整个集群停止后再次拉起,都会发生 Leader 的切换; 当数据发生变更,包括创建、修改、删除,其
revision 对应的都会 +1,在集群中跨 Leader 任期之间,revision 都会保持全局单调递增, 集群中任意一次的修改都对应着一个唯一的
revision,因此我们可以通过 revision 来支持数据的 MVCC,也可以支持数据的 Watch。

对于每一个
KeyValue 数据节点,etcd 中都记录了三个版本:


  • 第一个版本叫做
    create_revision,是 KeyValue 在创建时对应的 revision;

  • 第二个叫做
    mod_revision,是其数据被操作的时候对应的 revision;

  • 第三个
    version 就是一个计数器,代表了 KeyValue 被修改了多少次。

在同一个
Leader 任期之内,所有的修改操作,其对应的 term 值始终相等,而 revision 则保持单调递增。当重启集群之后,所有的修改操作对应的 term 值都加1了。

ETCD之MVCC并发控制






说起mvcc大家都不陌生,mysql的innodb中





就使用mvcc实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本,同样,在etcd中也使用mvcc进行并发控制。

etcd支持对同一个 Key 发起多次数据修改,每次数据修改都对应一个版本号。etcd记录了每一次修改对应的数据,即一个 key 在 etcd 中存在多个历史版本。在查询数据的时候如果不指定版本号,etcd 会返回 Key 对应的最新版本,同时etcd 也支持指定一个版本号来查询历史数据。

etcd将每一次修改都记录下来,使用 watch订阅数据时,可以支持从任意历史时刻(指定 revision)开始创建一个 watcher,在客户端与 etcd 之间建立一个数据管道,etcd 会推送从指定 revision 开始的所有数据变更。etcd 提供的 watch 机制保证,该 Key 的数据后续的被修改之后,通过这个数据管道即时的推送给客户端。

分析其源码可知:

type revision struct {
// main is the main revision of a set of changes that happen atomically.
main int64
// sub is the the sub revision of a change in a set of changes that happen
// atomically. Each change has different increasing sub revision in that
// set.
sub int64
}
func (a revision) GreaterThan(b revision) bool {
if a.main > b.main {
return true
}
if a.main < b.main {
return false
}
return a.sub > b.sub
}

在etcd的mvcc实现中有一个revision结构体,main 表示当前操作的事务 id,全局自增的逻辑时间戳,sub 表示当前操作在事务内部的子 id,事务内自增,从 0 开始;通过
GreaterThan方法进行事务版本的比较。

ETCD存储数据结构

etcd 中所有的数据都存储在一个 btree的数据结构中,该btree保存在磁盘中,并通过mmap的方式映射到内存用来支持快速的访问,treeIndex的定义如下:

type treeIndex struct {
sync.RWMutex
tree *btree.BTree
}
func newTreeIndex() index {
return &treeIndex{
tree: btree.New(32),
}
}

index所绑定对btree的操作有Put、Get、Revision、Range及Visit等,以Put方法为例,其源码如下:

func (ti *treeIndex) Put(key []byte, rev revision) {
keyi := &keyIndex{key: key}
ti.Lock()
defer ti.Unlock()
item := ti.tree.Get(keyi)
if item == nil {
keyi.put(rev.main, rev.sub)
ti.tree.ReplaceOrInsert(keyi)
return
}
okeyi := item.(*keyIndex)
okeyi.put(rev.main, rev.sub)
}

通过源码可知对btree数据的读写操作都是在加锁下完成的,从而来保证并发下数据的一致性。

微信扫一扫,分享到朋友圈

ETCD核心机制解析

QThread部分源码浅析

上一篇

知名童装品牌史努比入驻拼多多一年多 卖出近千万件童装

下一篇

你也可能喜欢

ETCD核心机制解析

长按储存图像,分享给朋友