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. 服務之間能彼此即時感知, 新增節點、丟棄不可用的服務節點.

5. 服務之間共享同一份配置文件, 並且監聽著, 有更新, 服務立刻感知

6. 可靠的分散式KV儲存, 底層用Raft保證一致性
7. etcdV3套件使用gRPC跟etcd服務進行溝通; 相較于v2版本的http+json更加輕巧
## 額外好處
1. 分散式鎖

2. 分發任務進度匯報

3. 分散式隊列

# 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以及當時的資料.

### 內部結構

### [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與資料.