ETCD 的理解和簡單使用
1. etcd介紹
etcd是使用Go語言開發的一個開源、高可用的分布式key-value存儲系統,可以用于配置共享
和服務注冊和發現
特點:
完全復制
:集群中的每個節點都可以使用完整的存檔。
高可用性
:etcd可用于避免硬件的單點故障和網絡問題。
一致性
:每次讀取都會返回跨多主機的最新寫入。
簡單
:保羅一個定義良好、面向用戶的API。
安全
:實現了帶有可選的客戶端證書身份驗證的自動化TLS。
快速
:每秒1W次寫入的基準速度。
可靠
: RAFT算法實現了強一致、高可用的服務存儲目錄。
2. etcd的應用場景
2.1 服務發現
服務發現:即在同一個分布式集群中的進程或服務,要如何才能找到對方并建立了解?本質上來說,服務發現就是想要了解進群眾是否有進程監聽UDP和TCP端口,并且通過域名就可以查找和連接。
配置中心
將一些配置信息放到etcd上進行集中管理。
應用在啟動的時候主動從etcd獲取一次配置信息,同事,在etcd節點上注冊一個WATCHER并等待,以后每次配置有更新的時候,etcd都會實時通知訂閱者,一次達到獲取配置信息的目的。
分布式鎖
詳細請參考:
https://www.cnblogs.com/jiujuan/p/12147809.html
https://segmentfault.com/a/1190000021603215?utm_source=tag-newest
因為etcd使用Raft算法保持了數據的強一致性,其次操作存儲到集群中的值必然是全局一直的,所以很容易實現分布式鎖。鎖服務有兩種使用方式。
-
保持獨占
即所有獲取鎖的用戶最終之后一個用戶可以得到。etcd為此提供了一套實現分布式鎖算子操作CAS(CompareAndswap)的API. 通過設置prevExist值,可以保證在多個節點同事創建某個目錄時,只有一個成功。而創建成功的用戶就可以認為是獲得了鎖。 -
控制時序
: 即所有想要獲得所的用戶都會被安排執行,但是獲得所的順序也是全局唯一的,同事決定了執行順序。etcd也提供了一套API(自動創建有序鍵),對一個目錄建值同時指定為POST動作,這樣etcd會自動在目錄下生一個當前最大值為鍵,存儲這個新的值(客戶端編號)。同時還可以使用API按順序列出所有當前目錄下的鍵值。此時這些建的值就是客戶端的時序,而這些鍵中存儲的值可以代表客戶端的編號。
etcd集群
etcd作為一個高可用鍵值存儲系統,天生就是為集群哈而設計的。由于Raft算法在做決策時需要多數節點的投票,所以etcd一般部署集群推薦奇數個節點,推薦數量為3、5或者7個節點構成一個集群。
為什么用etcd而不用zookeeper?
- etcd簡單,使用Go語言編寫部署簡單,支持HTTP/JSON API,使用簡單:使用Raft算法保證強一致性,讓用戶易于理解。
- etcd默認數據一更新就進行持久化。
- etcd支持SSL客戶端安全認證。
- zookeeper部署維護復雜,其使用的PAXOS強一致性算法難懂。官方只提供了JAVA和C兩種語言的接口。
- zookeeper 使用JAVA編寫引入大量依賴。運維人員維護起來比較麻煩。
拓展:Raft
etcd下載與安裝
- 源碼碼下載地址
https://github.com/etcd-io/etcd/releases
3. etcd命令簡單使用
1. 啟動etcd
`./etcd`
2. etcdclt 交互
2.1 put
通過put將key和value存儲到etcd集群中。 每個存儲的key都通過Raft協議復制到所有etcd集群成員,以實現一致性和可靠性。
# etcdctl put name dong
OK
2.2 get
通過get可以從一個etcd集群中讀取key的值。
現有K-V對:
age = 18
age2 = 19
name = dong
name2 = zhang
name3 = zhao
name4 = gan
- 1. 通過key來直接讀取valu
$ etcdctl get name
name # key
dong # value
- 2. 通過key來直接讀取value,只顯示value
$ etcdctl get name --print-value-only
dong # value
- 3. 通過key來直接讀取value,只顯示key
$ etcdctl get name --keys-only
name # key
- 4. 讀取指定范圍的key,從name~name3 左閉右開區間
$ etcdctl get name name3
name # k1
dong # v1
name2 # k2
zhang # v2
- 5. 按前綴讀取
$ etcdctl get n --prefix --keys-only
name
name2
name3
name4
- 6. 讀取數量限制
$ etcdctl get n --prefix --keys-only --limit 3
name
name2
name3
- 7. 讀取大于或等于指定鍵的字節值的鍵(從字母b開始,包含b)
$ etcdctl get --from-key b --keys-only
name
name2
name3
name4
- 8.
2.3 del
- 1. 刪除指定的 key
$ etcdctl del name
1 # 返回值,影響的個數
- 2. 刪除指定的鍵值對
$ etcdctl del --prev-kv name
1 # 返回值,影響的個數
name # k
dong # v
- 3. 刪除指定范圍的key
$ etcdctl del name name3
2 # 返回受影響的個數
- 4. 刪除具有前綴的key
$ etcdctl del --prefix a
2
-5. 刪除大于或等于鍵的字節值的鍵的命令
$ etcdctl del --from-key b
4
2.4 watch
Watch 用于監測一個 key-value 的變化,一旦 key-value 發生更新,就會輸出最新的值。
- 在新的終端輸入
etcdctl watch key
監聽對應的key。
$ etcdctl put key 001 OK $ etcdctl put key 002 OK $ etcdctl del key 1 ###### 以下是etcdctl watch key的輸出 $ etcdctl watch key PUT # TYPE key # K 001 # V PUT key 002 DELETE key
2.5 lock(分布式鎖)具體可參考:
etcd 的 lock 指令對指定的 key 進行加鎖。注意,只有當正常退出且釋放鎖后,lock 命令的退出碼是 0,否則這個鎖會一直被占用直到過期(默認 60 秒)。
- 在第一個終端輸入如下命令:
$ etcdctl lock mutex1
mutex1/694d7a4c4cf36947
- 在第二個終端輸入同樣的命令:
$ etcdctl lock mutex1
在此可以發現第二個終端發生了阻塞,并未返回類似 mutex1/694d7a4c4cf36947
的輸出。此時,如果我們使用 Ctrl+C 結束了第一個終端的 lock,然后第二個終端的顯示如下:
mutex1/694d7a4c4cf3694b
可見,這就是一個分布式鎖的實現。
2.6 transactions(事務)
txn支持從標準輸入中讀取多個請求,并將他們看作一個原子性的事務執行。 事務是由條件列表,條件判斷成功時的執行列表(條件列表中全部條件為真表示成功)和條件判斷失敗時的執行列表(條件列表中有一個為假即為失敗)組成的。
$ etcdctl txn -i # 交互
compares:
value("name") = "dong" # 條件,可以寫多個
success requests (get, put, del): # 條件為true,執行的命令
put result ok
failure requests (get, put, del): # 條件為fales,執行的命令
put result failed
SUCCESS
OK
####### 因為value("name") = "dong" 為TRUE,所以命令put result ok 執行
$ etcdctl get result
result
ok
2.7 compact(壓縮)
etcd 會保存數據的修訂版本,以便用戶可以讀取舊版本的 key。但是為了避免累積無盡頭的版本歷史,就需要壓縮過去的修訂版本。壓縮后,etcd 會刪除歷史版本并釋放資源。
$ etcdctl compact 5
compacted revision 5
$ etcdctl get --rev=4 foo
Error: etcdserver: mvcc: required revision has been compacted
2.8 lease(租約)
KEY的TTL(time to live 生存時間)是etcd的重要特征之一,即設置KEY的超時時間。 與Redis不同,etcd需要先闖進lease(租約),通過put --lease=
設置。 而lease又有TTL管理,以此來實現key的超時時間。
-
創建lease
$ etcdctl lease grant 60 # 創建lease,注意時間是60s lease 694d7a4c4cf36969 granted with TTL(60s) $ etcdctl put --lease=694d7a4c4cf36969 name soo # 將lease設置到指定的key中 OK $ etcdctl get name # 獲取對應的key,以及返回值 name soo $ etcdctl get name name soo $ etcdctl get name # 獲取對應的key,超過存活時間,沒有返回值
-
廢除指定的lease
$ etcdctl lease grant 60 # 創建lease lease 694d7a4c4cf36970 granted with TTL(60s) $ etcdctl put --lease=694d7a4c4cf36970 name soo # 將lease設置到指定的key中 OK $ etcdctl lease revoke 694d7a4c4cf36970 # 撤銷對應的lease lease 694d7a4c4cf36970 revoked $ etcdctl get name # 無法獲取key對應的value
-
查看租約期內對應的key,以及租約的狀態
$ etcdctl lease grant 240 # 創建lease lease 694d7a4c4cf3697b granted with TTL(240s) $ etcdctl put --lease=694d7a4c4cf3697b name liu # 設置key OK $ etcdctl put --lease=694d7a4c4cf3697b name2 song # 設置key OK $ etcdctl lease timetolive 694d7a4c4cf3697b # 獲取租約信息 lease 694d7a4c4cf3697b granted with TTL(240s), remaining(184s) $ etcdctl lease timetolive --keys 694d7a4c4cf3697b # 獲取租約信息以及對應的key lease 694d7a4c4cf3697b granted with TTL(240s), remaining(165s), attached keys([name name2]) $ etcdctl lease timetolive --keys 694d7a4c4cf3697b #租約已經過期所對應的返回值 lease 694d7a4c4cf3697b already expired
-
續約
$ etcdctl lease grant 30 lease 694d7a4c4cf36984 granted with TTL(30s) $ etcdctl put --lease=694d7a4c4cf36984 name aaa OK $ etcdctl lease keep-alive 694d7a4c4cf36984 # 該命令不會退出,一直會續約 lease 694d7a4c4cf36984 keepalived with TTL(30) lease 694d7a4c4cf36984 keepalived with TTL(30) lease 694d7a4c4cf36984 keepalived with TTL(30) lease 694d7a4c4cf36984 keepalived with TTL(30)
4. GO Client SDK 交互
下載對應的庫:go get go get go.etcd.io/etcd/clientv3
注: 這里開啟mod可能會有error: undefined: balancer.PickOptions, 需要在mod文件中:replace google.golang.org/grpc => google.golang.org/grpc v1.26.0`
1. put、get 和 del
package main
import (
"context"
"fmt"
"time"
"go.etcd.io/etcd/clientv3"
)
func main() {
// 創建鏈接
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 10 * time.Second,
})
if err != nil {
// handle error!
fmt.Printf("connect to etcd failed, err:%v\n", err)
return
}
fmt.Println("connect to etcd success")
defer cli.Close()
// put
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
_, err = cli.Put(ctx, "name", "dong")
cancel()
if err != nil {
fmt.Printf("put to etcd failed, err:%v\n", err)
return
}
// get
ctx, cancel = context.WithTimeout(context.TODO(), time.Second)
resp, err := cli.Get(ctx, "name")
cancel()
if err != nil {
fmt.Printf("get from etcd failed, err:%v\n", err)
return
}
for _, ev := range resp.Kvs {
fmt.Printf("%s:%s\n", ev.Key, ev.Value)
}
// del
delresp, err := cli.Delete(context.TODO(), "name")
if err != nil {
fmt.Printf("del key failed, err:%v\n", err)
return
}
if delresp.Deleted == 1{
fmt.Println("del key success!")
}
}
2. wathc
package main
import (
"context"
"fmt"
"time"
"go.etcd.io/etcd/clientv3"
)
func main() {
// 創建連接
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 10 * time.Second,
})
if err != nil {
fmt.Printf("connect to etcd failed, err:%v\n", err)
return
}
fmt.Println("connect to etcd success")
defer cli.Close()
// watch
rch := cli.Watch(context.TODO(), "name") // 返回一個channel
// 一直監聽key的變化
for wresp := range rch {
for _, ev := range wresp.Events {
fmt.Printf("Type: %s Key:%s Value:%s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
}
3. lease
package main
import (
"fmt"
"time"
)
// etcd lease
import (
"context"
"log"
"go.etcd.io/etcd/clientv3"
)
func main() {
// 創建連接
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 10 * time.Second,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("connect to etcd success.")
defer cli.Close()
// 創建一個5秒的租約
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
// 5秒鐘之后, name 這個key就會被移除
_, err = cli.Put(context.TODO(), "name", "dong", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
// 設置 keep-alive
ch, err := cli.KeepAlive(context.TODO(), resp.ID)
if err != nil {
log.Fatal(err)
}
for {
ka := <-ch
fmt.Println("ttl:", ka.TTL)
}
4. 事務
package main
import (
"fmt"
"time"
)
// etcd lease
import (
"context"
"log"
"go.etcd.io/etcd/clientv3"
)
func main() {
// 創建連接
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 10 * time.Second,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("connect to etcd success.")
defer cli.Close()
// 先設置了一個key
_, _ = cli.Put(context.TODO(), "name", "dong")
// 獲取事務對象
txn := cli.Txn(context.TODO())
// 這里的if是不成立的,執行else,會將name的值設置為liu
txnResp, err := txn.If(clientv3.Compare(clientv3.Value("name"), "=", "liu")).
Else(clientv3.OpPut("name", "liu")).Commit()
if err != nil{
fmt.Printf("commit failed, err:%v\n", err)
return
}
if !txnResp.Succeeded {
getResp, _ := cli.Get(context.TODO(), "name")
fmt.Println(string(getResp.Kvs[0].Value)) // 打印出liu
}
}
5. 分布式鎖(待研究)
參考: