7 Etcd服务端实现
7.1 Etcd启动
Etcd有多种启动方式,我们从最简单的方式入手,也就是从embed的etcd.go开始启动,最后会启动EtcdServer。
先看看etcd.go中的启动代码:
func StartEtcd(inCfg *Config) (e *Etcd, err error)
从StartEtcd方法启动etcd服务,参数是初始配置信息config,启动集群间监听进程和客户端监听进程,最后启动EtcdServer。
主要代码:
e = &Etcd{cfg: *inCfg, stopc: make(chan struct{})}
cfg := &e.cfg
if e.Peers, err = startPeerListeners(cfg); err != nil {
return
}
if e.sctxs, err = startClientListeners(cfg); err != nil {
return
}
if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {
return
}
e.Server.Start()
startPeerListeners启动Peer监听,等待集群中其他机器连接自己。startClientListeners启动客户端监听Socket,等待客户端请求并响应。最后调用Start方法启动EtcdServer。
7.2 EtcdServer
EtcdServer位于etcdserver/server.go,定义了Server接口和EtcdServer对象。EtcdServer从逻辑上讲代表了一个完整的Etcd服务。
图7.1 Etcd服务端的功能示意图
Etcd服务端主要提供两大类客户端接口:
(1)集群配置
由memberHandler负责,提供添加集群成员,删除成员,更新成员信息三种接口服务。
(2)KV键值:由keysHandler负责。
KeysHandler接收到客户端请求后,调用EtcdServer的Do方法处理请求,Watcher类的客户端请求信息同样包含在keysHandler中了。KV键值响应主要在v2_server.go中定义,etcd新版本同时还提供了v3操作命令集,本文不讨论v3的源码实现。
7.2.1 接口定义
Etcdserver/server.go中定义了Server接口,是服务端的主接口,其中Do方法处理客户端请求。
Server.go中定义了EtcdServer对象,它是Server接口的实现类。Server中的Do接口是专门用来响应客户端请求的。
Server接口定义:
-
start
读取配置文件,启动本Server。
-
stop
停止本Server
-
ID
获取本节点server的ID,集群中所有的机器都有唯一ID,用于标识自己。
-
Leader
获取leader的ID
-
Do
处理客户群请求,返回处理结果。
定义:
func (s *EtcdServer) Do(ctx context.Context, r pb.Request) (Response, error)
在server.go中并没有看到Go接口的实现,其实它是在v2_server.go文件中定义的。
-
Process
Process(ctx context.Context, m raftpb.Message) error
处理Raft消息。
-
AddMember
向Etcd集群中增加一台服务器,新增服务器的ID必须唯一标识。
-
RemoveMember
从集群删除一台服务器,删除服务器的ID必须已经存在于集群中。
-
UpdateMember
修改集群成员属性,如果成员ID不存在则返回ErrIDNotFound错误。
7.2.2 实体定义
EtcdServer表示一个独立运行的Etcd节点。
type EtcdServer struct {
inflightSnapshots int64
appliedIndex uint64
committedIndex uint64.
consistIndex consistentIndex
Cfg *ServerConfig
readych chan struct{}
r raftNode
snapCount uint64
w wait.Wait
readMu sync.RWMutex
readwaitc chan struct{}
readNotifier *notifier
stop chan struct{}
stopping chan struct{}
done chan struct{}
errorc chan error
id types.ID
attributes membership.Attributes
cluster *membership.RaftCluster
store store.Store
applyV2 ApplierV2
applyV3 applierV3
applyV3Base applierV3
applyWait wait.WaitTime
kv mvcc.ConsistentWatchableKV
lessor lease.Lessor
bemu sync.Mutex
be backend.Backend
authStore auth.AuthStore
alarmStore *alarm.AlarmStore
stats *stats.ServerStats
lstats *stats.LeaderStats
SyncTicker *time.Ticker
compactor *compactor.Periodic
peerRt http.RoundTripper
reqIDGen *idutil.Generator
forceVersionC chan struct{}
wgMu sync.RWMutex
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
leadTimeMu sync.RWMutex
leadElectedTime time.Time
}
7.2.3 Do
Do定义在v2_server.go中,处理客户群请求包,调用raftNode的Propose方法。在上一章已经介绍过。
对于KV键值请求,Do方法是在etcdServer/v2_server.go中定义的,它的相关代码逻辑如下:
func (s *EtcdServer) Do(ctx context.Context, r pb.Request) (Response, error) {
r.ID = s.reqIDGen.Next()
if r.Method == "GET" && r.Quorum {
r.Method = "QGET"
}
v2api := (v2API)(&v2apiStore{s})
switch r.Method {
case "POST":
return v2api.Post(ctx, &r)
case "PUT":
return v2api.Put(ctx, &r)
case "DELETE":
return v2api.Delete(ctx, &r)
case "QGET":
return v2api.QGet(ctx, &r)
case "GET":
return v2api.Get(ctx, &r)
case "HEAD":
return v2api.Head(ctx, &r)
}
return Response{}, ErrUnknownMethod
}
可以看到对客户端的KV键值请求,最终是通过v2apiStore的相关方法来实现。客户端的命令前缀为"/v2/keys"。支持的命令有以下这些:
-
GET/QGET:读取键值
-
POST:创建一个新的KV键值
-
PUT:重新设置键值的值
-
DELETE:删除已有键值
v2apiStore包含了EtcdServer引用。
type v2apiStore struct{ s *EtcdServer }
除了GET命令,其余Post,Put和Delete每个写操作请求最后都是通过processRaftRequest方法来处理的。
我们先看看GET命令的处理:
func (a *v2apiStore) Get(ctx context.Context, r *pb.Request) (Response, error) {
if r.Wait {
wc, err := a.s.store.Watch(r.Path, r.Recursive, r.Stream, r.Since)
if err != nil {
return Response{}, err
}
return Response{Watcher: wc}, nil
}
ev, err := a.s.store.Get(r.Path, r.Recursive, r.Sorted)
if err != nil {
return Response{}, err
}
return Response{Event: ev}, nil
}
看到对于普通的GET操作,直接调用store.Get方法获取KV值返回给客户端,如果是Watcher操作,则返回Watcher给客户端,客户端后续通过Watcher接口读取变化值。
对于POST,PUT,DELETE命令,走下述Propose流程处理。
图7.2 Propose流程示意图
比如"DELETE"命令。
func (a *v2apiStore) Delete(ctx context.Context, r *pb.Request) (Response, error) {
return a.processRaftRequest(ctx, r)
}
processRaftRequest方法的源码如下:
func (a *v2apiStore) processRaftRequest(ctx context.Context, r *pb.Request) (Response, error) {
data, err := r.Marshal()
if err != nil {
return Response{}, err
}
ch := a.s.w.Register(r.ID)
start := time.Now()
a.s.r.Propose(ctx, data)
proposalsPending.Inc()
defer proposalsPending.Dec()
select {
case x := <-ch:
resp := x.(Response)
return resp, resp.err
case <-ctx.Done():
proposalsFailed.Inc()
a.s.w.Trigger(r.ID, nil) // GC wait
return Response{}, a.s.parseProposeCtxErr(ctx.Err(), start)
case <-a.s.stopping:
}
return Response{}, ErrStopped
}
-
data, err := r.Marshal()语句:
这条语句从pb.request得到请求数据data
-
ch := a.s.w.Register(r.ID)语句:
注册chain,一直等待直到ch有响应数据。
Register方法是wait的Register方法。该方法直到调用wait的Trigger方法后才会有数据从而触发select在该Register Id上线程被唤醒。Wait在pkg/wait中定义。
-
a.s.r.Propose(ctx, data)
Propose方法在node中定义,raftNode在etcdserver/node.go文件中。Propose将写事务请求发给Leader,等待集群间同步。Propose集群间同步消息完成后会唤醒a.s.w.Register语句。
调用raft/node的Propose方法处理写事务请求,进一步调用step方法将写事务封装成MsgProp消息并传递给集群中其他机器。
func (n *node) Propose(ctx context.Context, data []byte) error { return n.step(ctx, pb.Message{Type: pb.MsgProp, Entries: []pb.Entry{{Data: data}}}) }
step会调用StepFunc函数来处理MsgProp消息,根据leader,follower,candidate等运行状态分别调用不同的实现函数。
-
select语句
select … case …语句类似于Socket通信中的select语句,它的含义是只要任意一个case语句有数据返回就往下执行,否则就阻塞在这里让出CPU给其他线程执行。
case x := <-ch:当ch有值时,将ch赋值给x变量,同时唤醒case语句被执行,这里将执行以下代码:
resp := x.(Response) return resp, resp.err
此时将ch中的返回结果Response回复给调用者(即客户端)。
case <-ctx.Done():说明上下文被中断,Context的Done()被触发,此时写事务执行失败,返回空Response。
7.2.4 初始化
Etcd服务端主要由5大组件构成,他们的分工如下:
-
etcdServer:主进程,相当于整个Etcd的容器,包含了raftNode,WAL,snapshotter等多个关键组件。
-
raftNode:执行raft协议,保证写事务的集群一致性维护。
-
Store:管理维护Etcd数据库
-
Wal:管理事务日志
-
Snapshotter:负责数据快照,管理store数据库在内存中和磁盘上的相互转换。
raftNode除了负责集群间raft消息交互,还负责事务和快照的存储,保持数据一致性。
Etcd定义了一个storage数据结构,一起负责事务和快照。
type storage struct {
*wal.WAL
*snap.Snapshotter
}
storage中没有指定WAL和Snapshotter的变量名称,这两个类的方法都可直接通过storage来调用,比如WAL的Save方法,可以通过storage.Save来调用,也可以通过storage.WAL.Save来调用,这两者是等价的,在阅读源码的时候要注意这一点,否则对Go语法不太了解的读者会感到迷惑。
func NewServer(cfg *ServerConfig) (srv *EtcdServer, err error) {
st := store.New(StoreClusterPrefix, StoreKeysPrefix)
var (
w *wal.WAL
n raft.Node
s *raft.MemoryStorage
id types.ID
cl *membership.RaftCluster
)
haveWAL := wal.Exist(cfg.WALDir())
ss := snap.New(cfg.SnapDir())
bepath := filepath.Join(cfg.SnapDir(), databaseFilename)
beExist := fileutil.Exist(bepath)
switch {
case haveWAL:
snapshot, err = ss.Load()
if snapshot != nil {
if err = st.Recovery(snapshot.Data); err != nil {
plog.Panicf("recovered store from snapshot error: %v", err)
}
}
cfg.Print()
if !cfg.ForceNewCluster {
id, cl, n, s, w = restartNode(cfg, snapshot)
} else {
id, cl, n, s, w = restartAsStandaloneNode(cfg, snapshot)
}
cl.SetStore(st)
cl.SetBackend(be)
cl.Recover(api.UpdateCapability)
}
if terr := fileutil.TouchDirAll(cfg.MemberDir()); terr != nil {
return nil, fmt.