etcd入門介紹 ========================== ###### tags: `MicroService` `etcd` # 服務之間的服務註冊與發現 因為微服務架構, 允許服務水平擴展. 在服務發現出現之前, 都是透過讀取靜態配置文件或環境變數, 來獲取服務的位置, 然後連線. 這在產線上出現不少問題: 1. 某些服務已經不可用 2. 負載均衡配置複雜, 集中式的負載均衡服務本身可能就是個瓶頸 3. 無法立即感知新增出來的服務節點 # 服務的共享設置 一套系統之間不同模組或是服務中會有很多配置訊息, 例如: 資料庫位址, 連線的配置資訊 這些如果用靜態配置文件, 每次更新配置時, 會很想死, 要更新很多台 [Kubernetes](https://github.com/kubernetes/kubernetes)底層就是依賴于etcd實現了集群狀態跟配置的管理. # 服務註冊與發現中心該用CAP的那一種組合? 對服務註冊來說, 每一個節點存的註冊訊息, 有稍微不一樣, 其實不會有太大的問題. 所以對服務消費者來說, 它更在意是否註冊中心能夠給予回應. 不外乎就是可能新註冊的對象沒太多人去打, 覆載稍微不均衡一小段時間. 或者打了一個失效的節點, 服務消費者自己都會去從可用清單裡retry. 所以比起追求強一致性, 消費者更在意服務是否高度可用. 因此註冊中心, 大部分都選擇**A+P**. # [etcd](https://etcd.io/)帶來的好處 1. 健康檢查: 服務節點定期向etcd發送心跳更新自己訊息的TTL 2. 服務主動註冊: 同一類型的服務啟動後, 主動註冊到相同服務目錄下 3. 方便透過服務名稱就能查詢到服務提供給外部訪問的IP與Port 4. 服務之間能彼此即時感知, 新增節點、丟棄不可用的服務節點. ![](https://i.imgur.com/RYNFgtk.png) 5. 服務之間共享同一份配置文件, 並且監聽著, 有更新, 服務立刻感知 ![](https://i.imgur.com/qYHXPRR.png) 6. 可靠的分散式KV儲存, 底層用Raft保證一致性 7. etcdV3套件使用gRPC跟etcd服務進行溝通; 相較于v2版本的http+json更加輕巧 ## 額外好處 1. 分散式鎖 ![](https://i.imgur.com/S3ZrDdH.png) 2. 分發任務進度匯報 ![](https://i.imgur.com/MiY3KAL.png) 3. 分散式隊列 ![](https://i.imgur.com/D4yQWTN.png) # etcd資料模型 etcd底層使用[BoltDB](https://github.com/etcd-io/bbolt)作為KV儲存. 內部資料以B+Tree做儲存, 所以如果etcd的服務是用SSD的話, 搜尋效能會快上不少; 且支援事務操作, 也支援ACID. BoltDB適合讀取密集或者是順序寫入的工作, 對於隨機寫入的工作就沒那麼突出的效能. 若是提交事務時, 有刪除操作, BoldDB會檢查是否需要重新平衡B+Tree的索引. 如果是寫入操作, 就有可能會檢查是否需要拆分樹的節點. ## 索引與多版本(MVCC)控管的實現 etcd實現了同一個Key的多版本機制; 也就是說同一個Key的每一次更新操作都被單獨紀錄下來了. etcd把原始的key當成Btree的索引存放在記憶體中, 資料節點放的是"keyIndex"的資料節點. 所以每次etcd啟動時, 都會去BoltDB把所有的資料撈出來放在記憶體中. ### 搜尋流程 先用原始key去找到對應的keyIndex, 再拿這keyIndex取得revision訊息, 再去boldDB去硬碟內找到真正的key以及當時的資料. ![](https://i.imgur.com/6aETGTt.png) ### 內部結構 ![](https://i.imgur.com/r5LWRVf.png) ### [revision](https://github.com/etcd-io/etcd/blob/master/mvcc/revision.go)結構 ```go // A revision indicates modification of the key-value space. // The set of changes that share same main revision changes the key-value space atomically. // main, sub的組成, 加上key就能保證key唯一且遞增 type revision struct { // main is the main revision of a set of changes that happen atomically. // 目前的事務操作ID(timestamp), 每次事務操作時, 遞增1 main int64 // sub is the sub revision of a change in a set of changes that happen // atomically. Each change has different increasing sub revision in that // set. // 子ID, 從0開始; 同一個事務內的操作遞增1 sub int64 } ``` 如果一個事務操作是 ``` tx1: 1234567 put keyA value1; delete keyA ``` 那對應的revision 會是 {1234567, 0}, {1234567,1} ### [generation](https://github.com/etcd-io/etcd/blob/master/mvcc/key_index.go#L335)結構 ```go // generation contains multiple revisions of a key. type generation struct { ver int64 created revision // when the generation is created (put in first revision). revs []revision // 每次不斷更新時, 就會追加更新revision訊息 } ``` 因為MVCC(多版本控管), 不可能讓所有版本都追到同一個revs中; 除了不會讓revs長度無限膨脹之外, 所以有generation的概念(分片的概念), 也因為長度有限,便於快速搜尋. **ver**從0開始, 0表示第一個版本; **created**表示這generation被建立的時間 **revs**表示該世代所有版本. ### [keyIndex](https://github.com/etcd-io/etcd/blob/master/mvcc/key_index.go)資料結構 ```go // keyIndex stores the revisions of a key in the backend. // Each keyIndex has at least one key generation. // Each generation might have several key versions. // Tombstone on a key appends an tombstone version at the end // of the current generation and creates a new empty generation. // Each version of a key has an index pointing to the backend. // 紀錄了一個key, 從建立到被刪除的過程. // 每次刪除key的操作, 就會在該generation最後, 添加一個tombstone紀錄; 並且增加一個全新的空generation紀錄到generations內 type keyIndex struct { key []byte //原始Key值 modified revision // 紀錄最新一次修改對應的revision generations []generation //紀錄各generation } ``` **key**當前用戶操作的key **modified**最新一次修改對應的revision **generations**過往修改歷程 ## etcd Client基本操作-Get、Put、Delete ```go package main import ( "context" "fmt" "log" "time" "go.etcd.io/etcd/clientv3" ) var ( dialTimeout = 10 * time.Second requestTimeout = 3 * time.Second ) func main() { var endpoints = []string{"172.16.238.100:2379", "172.16.238.101:2379", "172.16.238.101:2379"} cli, err := clientv3.New(clientv3.Config{ Endpoints: endpoints, DialTimeout: dialTimeout, }) if err != nil { log.Fatalln(err) } defer cli.Close() timeOutCtx, _ := context.WithTimeout(context.Background(), requestTimeout) kv := clientv3.NewKV(cli) GetSingleKeyWithRevision(timeOutCtx, kv) } func GetSingleKeyWithRevision(ctx context.Context, kv clientv3.KV) { fmt.Println("GetSingleKeyWithRevision()") key := "demo" // 刪除之前遺留的Key kv.Delete(ctx, key, clientv3.WithPrefix()) // 新增一組KV {"Demo": "444"} pr, _ := kv.Put(ctx, key, time.Now().UTC().String()) // 保存create revision rev := pr.Header.Revision fmt.Println("Revision:", rev) gr, _ := kv.Get(ctx, key) fmt.Println("Value: ", string(gr.Kvs[0].Value), "Revision: ", gr.Header.Revision) // 修改該Key的值, 會建立新的revision kv.Put(ctx, key, time.Now().UTC().String()) gr, _ = kv.Get(ctx, key) fmt.Println("Value: ", string(gr.Kvs[0].Value), "Revision: ", gr.Header.Revision) // 透過指定的revision取得當時的值 gr, _ = kv.Get(ctx, key, clientv3.WithRev(rev)) fmt.Println("Value: ", string(gr.Kvs[0].Value), "Revision: ", gr.Header.Revision) } /* GetSingleKeyWithRevision() Revision: 1954477 Value: 2020-09-06 15:47:16.398487325 +0000 UTC Revision: 1954477 Value: 2020-09-06 15:47:16.400697144 +0000 UTC Revision: 1954478 Value: 2020-09-06 15:47:16.398487325 +0000 UTC Revision: 1954478 */ ``` 從上面結果能看出, 每次修改都會保存之前的revision與資料.