什么是etcd?
etcd 发音为/ˈɛtsiːdiː/,名字的由来,“distributed etc directory.”,意思是“分布式etc目录”,说明它存的是大型分布式系统的配置信息。
官网的一句话
A distributed, reliable key-value store for the most critical data of a distributed system.
翻译并理解过来就是:一个用于存储分布式系统中最关键的数据的仓库,它是分布式的、可靠的键值对仓库。首先它是个数据存储仓库,它的特性是分布式的、可靠性的,数据存储格式是键值对存储,它主要用于存储分布式系统中的关键数据。
etcd的应用场景
etcd的应用场景:服务发现和服务注册、配置中心、分布式锁。
1、服务发现
服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。
控制时序,即所有想要获得锁的用户都会被安排执行,但是获得锁的顺序也是全局唯一的,同时决定了执行顺序。
2、配置中心
将一些配置信息放到 etcd 上进行集中管理。
这类场景的使用方式通常是这样:应用在启动的时候主动从 etcd 获取一次配置信息,同时,在 etcd 节点上注册一个 Watcher 并等待,以后每次配置有更新的时候,etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。
3、分布式锁
因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。
- 保持独占即所有获取锁的用户最终只有一个可以得到。etcd 为此提供了一套实现分布式锁原子操作 CAS(CompareAndSwap)的API。通过设置prevExist值,可以保证在多个节点同时去创建某个目录时,只有一个成功。而创建成功的用户就可以认为是获得了锁。
- 控制时序,即所有想要获得锁的用户都会被安排执行,但是获得锁的顺序也是全局唯一的,同时决定了执行顺序。etcd 为此也提供了一套API(自动创建有序键),对一个目录建值时指定为POST动作,这样 etcd会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用 API按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号。
为什么用etcd而不用zookeeper?
- etcd简单,使用Go语言编写部署简单,支持HTTP/JSON API,使用简单:使用Raft算法保证强一致性,让用户易于理解。
- etcd默认数据一更新就进行持久化。
- etcd支持SSL客户端安全认证。
- zookeeper部署维护复杂,其使用的PAXOS强一致性算法难懂。官方只提供了JAVA和C两种语言的接口。
- zookeeper 使用JAVA编写引入大量依赖。运维人员维护起来比较麻烦。
下面开始实战
安装etcd
- 所谓安装ectd,实际上就是下载etcd文件,然后解压即可。
etcd下载目录:https://github.com/etcd-io/etcd/releases
(如果上面的地址下载不了。就进入这个地址下载:https://studygolang.com/articles/28682)
- 下载到本地window,通过scp (或者ftp工具)上传到linux目录即可:
scp -r ./etcd-v3.4.7-linux-amd64.tar.gz root@81.68.81.16:/usr/local
- 到linux的环境执行以下操作:
cd /usr/local
tar -xzvf etcd-v3.4.7-linux-amd64.tar.gz
解压成功后能看到下面的文件:
首先,必须开启etcd(需要在etcd的解压目录下执行)
./etcd (后台挂起程序:nohup ./etcd &)
查看etcd默认的端口是否已经开启:netstat -antp | grep 2379
然后就可以在xshell控制台新增和读取key-value(需要在etcd的解压目录下执行)
./etcdctl --endpoints=localhost:2379 put mylove lxz
./etcdctl --endpoints=localhost:2379 get mylove
#删除key:
./etcdctl del /hello
如果没开启etcd,就进行读取或新增key的话,会出现下面的报错:
通过go代码实现etcd的读写操作
- 设置key-value
package main
import (
"fmt"
"context"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
// 设置etcd的值
func main() {
key := "bookname"
value := "收敛之道"
SetEtcdData(key,value)
}
func SetEtcdData(key string,value string) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * 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.Background(), time.Second)
_, err = cli.Put(ctx, key, value)
cancel()
if err != nil {
fmt.Printf("put to etcd failed, err:%v\n", err)
return
}
}
- 读取key-value
package main
import (
"fmt"
"context"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
//获取etcd的值
func main() {
fmt.Println("获取etcd的key")
//获取单个key
// key := "foo"
// GetEtcdData(key)
// GetEtcdData2(key)
//获取多个key
keys := []string{"foo","mylove"}
GetEtcdData3(keys...)
}
func GetEtcdData(key string) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * 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()
//限制请求时间为1秒
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
resp, err := cli.Get(ctx, key)
cancel()
if err != nil {
fmt.Printf("get from etcd failed, err:%v\n", err)
return
}
fmt.Println(resp.Kvs) //[key:"foo" create_revision:2 mod_revision:3 version:2 value:"bar" ]
fmt.Println(resp.Kvs[0])//key:"foo" create_revision:2 mod_revision:3 version:2 value:"bar"
for _, ev := range resp.Kvs {
fmt.Printf("获取etcd的key-value为:%s:%s\n", ev.Key, ev.Value)
}
}
func GetEtcdData2(key string) {
var kv clientv3.KV
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * 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()
//限制请求时间为1秒
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
kv = clientv3.NewKV(cli)//用newKv的方式获取键值对
resp, err := kv.Get(ctx, key)
cancel()
if err != nil {
fmt.Printf("get from etcd failed, err:%v\n", err)
return
}
fmt.Println(resp.Kvs) //[key:"foo" create_revision:2 mod_revision:3 version:2 value:"bar" ]
fmt.Println(resp.Kvs[0])//key:"foo" create_revision:2 mod_revision:3 version:2 value:"bar"
for _, ev := range resp.Kvs {
fmt.Printf("22获取etcd的key-value为:%s:%s\n", ev.Key, ev.Value)
}
}
//获取多个键值对
func GetEtcdData3(keys...string) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * 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()
ctx := context.TODO()//做法1
//限制请求时间为1秒
for _,k := range keys {
//做法2
// ctx, cancel := context.WithTimeout(context.Background(), time.Second)
resp, err := cli.Get(ctx, k)
// cancel()
if err != nil {
fmt.Printf("get from etcd failed, err:%v\n", err)
return
}
for _, ev := range resp.Kvs {
fmt.Printf("获取etcd的多个key-value为:%s:%s\n", ev.Key, ev.Value)
}
}
}
- 监听key的变化
package main
import (
"context"
"fmt"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
// watch demo
//监听某个key的变化
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * 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 key:bookname change
rch := cli.Watch(context.Background(), "bookname") // <-chan WatchResponse
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)
}
}
}
效果:
etcd操作多个key:
#获取某个前缀的key,--keys-only代表不展示value。
./etcdctl --endpoints=localhost:2379 get / --prefix --keys-only
#获取所有key,以及对应的value
./etcdctl --endpoints=localhost:2379 get "" --prefix
#获取所有key
./etcdctl --endpoints=localhost:2379 get "" --prefix --keys-only
#监听某个前缀的所有key的修改
./etcdctl --endpoints=localhost:2379 watch "/k" --prefix
#删除前缀是/hello开头下的所有key:
./etcdctl del /hello --prefix
代码实战:
//获取某个前缀下的所有key
//当pre_key=""的时候,获取全部的key。
func GetEtcdData4(pre_key string) {
var kv clientv3.KV
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * 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()
//限制请求时间为1秒
ctx := context.TODO()//做法1
kv = clientv3.NewKV(cli)//用newKv的方式获取键值对
//获取前缀下面的所有key。当pre_key=""的时候,获取全部的key。
resp, err := kv.Get(ctx, pre_key, clientv3.WithPrefix())
if err != nil {
fmt.Printf("get from etcd failed, err:%v\n", err)
return
}
fmt.Println(resp.Kvs) //[key:"foo" create_revision:2 mod_revision:3 version:2 value:"bar" ]
// fmt.Println(resp.Kvs[0])//key:"foo" create_revision:2 mod_revision:3 version:2 value:"bar"
for _, ev := range resp.Kvs {
fmt.Printf("22获取etcd的key-value为:%s:%s\n", ev.Key, ev.Value)
}
}
推荐阅读:
https://blog.csdn.net/wohu1104/article/details/108552649
https://www.topgoer.com/%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C/go%E6%93%8D%E4%BD%9Cetcd/etcd%E4%BB%8B%E7%BB%8D.html