Etcd本地部署
Linux
数据源选择${DOWNLOAD_URL}:
发布版本${ETCD_VER}:
下载压缩包指令
wget -c ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -P /tmp
#参数 -c 断点续传
# -P 指定目录下载
#例如:wget -c https://github.com/etcd-io/etcd/releases/download/v3.5.0/etcd-v3.5.0-linux-amd64.tar.gz -P /tmp
解压缩gzip指令
压缩包解压出来有用的就只有etcd、etcdutl、etcdctl,其它均是文档
将etcd、etcdctl、etcdutl移至/usr/local/bin
mv /tmp/etcd /usr/local/bin
mv /tmp/etcdctl /usr/local/bin
mv /tmp/etcdutl /usr/local/bin
启动Etcd
前台启动
etcd #默认端口号是2379
etcd localhost:XXXX #指定端口号
后台启动
nohup etcd & #缺省输出日志文件,则会把所有输出都重定向到nohup.out文件中
nohup etcd >/tmp/etcd-log/etcd.log 2>&1 & #指定输出日志文件 并将标准输出和错误输出都定向到etcd.log
nohup etcd -data-dir=/tmp/etcd_data_dir >/tmp/etcd-log/etcd.log 2>&1 & #指定etcd存储库的目录,默认在data_dir目录下生成default.etcd,可指定存储目录名/tmp/etcd_data_dir/xxxx.etcd
Etcd的简单使用
etcdctl put foo bar #输出OK
etcdctl get foo #输出bar
脚本部署Etcd集群
三节点集群
node_name | ip | port |
etcd_1 | 127.0.0.1 | 2379,2380 |
etcd_2 | 127.0.0.1 | 2479,2480 |
etcd_3 | 127.0.0.1 | 2579,2580 |
start_etcd1.sh
#!/bin/bash
TOKEN=token-cluster
CLUSTER_STATE=new
NAME_1=etcd_1
NAME_2=etcd_2
NAME_3=etcd_3
HOST_1=127.0.0.1
HOST_2=127.0.0.1
HOST_3=127.0.0.1
PORT_API_1=2379
PORT_PEER_1=2380
PORT_API_2=2479
PORT_PEER_2=2480
PORT_API_3=2579
PORT_PEER_3=2580
CLUSTER=${NAME_1}=http://${HOST_1}:${PORT_PEER_1},${NAME_2}=http://${HOST_2}:${PORT_PEER_2},${NAME_3}=http://${HOST_3}:${PORT_PEER_3}
# For every machine
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
THIS_PORT_API=${PORT_API_1}
THIS_PORT_PEER=${PORT_PEER_1}
etcd --data-dir=/tmp/etcd_data_conf/${THIS_NAME}/data_dir \ #指定节点的数据存储目录
--name ${THIS_NAME} \ #节点名
--initial-advertise-peer-urls http://${THIS_IP}:${THIS_PORT_PEER} \ #用于向集群里其它节点告知本节点用于与其它节点通讯的url列表
--listen-peer-urls http://${THIS_IP}:${THIS_PORT_PEER} \ #监听URL,用于与其它节点通讯
--advertise-client-urls http://${THIS_IP}:${THIS_PORT_API} \ # 用于向集群里的其它成员告知本节点的客户端url列表
--listen-client-urls http://${THIS_IP}:${THIS_PORT_API} \ # 用于监听客户端流量的url列表
--initial-cluster ${CLUSTER} \ #集群中的所有节点
--initial-cluster-state ${CLUSTER_STATE} \ #初始集群状态
--initial-cluster-token ${TOKEN} \ #集群的token
start_etcd2.sh
#!/bin/bash
TOKEN=token-cluster
CLUSTER_STATE=new
NAME_1=etcd_1
NAME_2=etcd_2
NAME_3=etcd_3
HOST_1=127.0.0.1
HOST_2=127.0.0.1
HOST_3=127.0.0.1
PORT_API_1=2379
PORT_PEER_1=2380
PORT_API_2=2479
PORT_PEER_2=2480
PORT_API_3=2579
PORT_PEER_3=2580
CLUSTER=${NAME_1}=http://${HOST_1}:${PORT_PEER_1},${NAME_2}=http://${HOST_2}:${PORT_PEER_2},${NAME_3}=http://${HOST_3}:${PORT_PEER_3}
# For every machine
THIS_NAME=${NAME_2}
THIS_IP=${HOST_2}
THIS_PORT_API=${PORT_API_2}
THIS_PORT_PEER=${PORT_PEER_2}
etcd --data-dir=/tmp/etcd_data_conf/${THIS_NAME}/data_dir \
--name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:${THIS_PORT_PEER} \
--listen-peer-urls http://${THIS_IP}:${THIS_PORT_PEER} \
--advertise-client-urls http://${THIS_IP}:${THIS_PORT_API} \
--listen-client-urls http://${THIS_IP}:${THIS_PORT_API} \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} \
--initial-cluster-token ${TOKEN} \
start_etcd3.sh
#!/bin/bash
TOKEN=token-cluster
CLUSTER_STATE=new
NAME_1=etcd_1
NAME_2=etcd_2
NAME_3=etcd_3
HOST_1=127.0.0.1
HOST_2=127.0.0.1
HOST_3=127.0.0.1
PORT_API_1=2379
PORT_PEER_1=2380
PORT_API_2=2479
PORT_PEER_2=2480
PORT_API_3=2579
PORT_PEER_3=2580
CLUSTER=${NAME_1}=http://${HOST_1}:${PORT_PEER_1},${NAME_2}=http://${HOST_2}:${PORT_PEER_2},${NAME_3}=http://${HOST_3}:${PORT_PEER_3}
# For every machine
THIS_NAME=${NAME_3}
THIS_IP=${HOST_3}
THIS_PORT_API=${PORT_API_3}
THIS_PORT_PEER=${PORT_PEER_3}
etcd --data-dir=/tmp/etcd_data_conf/${THIS_NAME}/data_dir \
--name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:${THIS_PORT_PEER} \
--listen-peer-urls http://${THIS_IP}:${THIS_PORT_PEER} \
--advertise-client-urls http://${THIS_IP}:${THIS_PORT_API} \
--listen-client-urls http://${THIS_IP}:${THIS_PORT_API} \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} \
--initial-cluster-token ${TOKEN} \
启动脚本
bash start_etcd1.sh
bash start_etcd2.sh
bash start_etcd3.sh
查看节点信息
etcdctl --write-out=table member list
查看集群状态
etcdctl --endpoints=127.0.0.1:2379,127.0.0.1:2479,127.0.0.1:2579 --write-out=table endpoint status
查看节点健康状况
etcdctl --endpoints=127.0.0.1:2379,127.0.0.1:2479,127.0.0.1:2579 --write-out=table endpoint health
Docker启动Etcd集群
安装镜像
docker pull bitnami/etcd
启动单节点
docker run -p 2380:2380 -p 2379:2379 \
-e ETCD_NAME=etcd1\ #节点名
-e ALLOW_NONE_AUTHENTICATION=yes \ #允许不验证身份
-e ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:2379 \
-e ETCD_INITIAL_ADVERTISE_PEER_URLS=http://127.0.0.1:2380 \
-e ETCD_INITIAL_CLUSTER=etcd1=http://127.0.0.1:2380 \
--name etcd1 bitnami/etcd
将主机的etcd数据目录挂载到docker实例的方法
试过好几种在docker run 附加data-dir参数的方法来挂载主机etcd数据目录的方法,但都失败了,docker实例的数据目录一直是默认的/bitnami/etcd/data,最后在实例里的etcd-env.sh脚本里发现,data-dir环境变量不在可修改的变量列表里。但试出了一种方法,创建docker实例时将本机的etcd数据目录挂载到实例里,再用root身份进入到实例,把想要的本机目录覆盖到实例里的默认目录。
docker run -d \
-p 2379:2379 \
-p 2380:2380 \
-v /tmp/etcd_data_conf/etcd_1/data_dir:/etcd-data \ #将主机目录挂载到实例根目录下的etcd-data
-e ALLOW_NONE_AUTHENTICATION=yes \
--name etcd1 bitnami/etcd
docker exec -it -u root container_id bash #以root身份进入到实例
cp -r /etcd-data/member /bitnami/etcd/data #用挂载的主机目录下的member文件夹覆盖实例里/bitnami/etcd/data目录下的member
exit #退出实例
docker restart container_id #重启实例,数据即生效
启动三个节点
etcd1
docker run -d -p 2380:2380 -p 2379:2379 \
-e ETCD_NAME=etcd1\
-e ALLOW_NONE_AUTHENTICATION=yes \
--network host \
-e ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:2379 \
-e ETCD_LISTEN_CLIENT_URLS=http://127.0.0.1:2379 \
-e ETCD_INITIAL_ADVERTISE_PEER_URLS=http://127.0.0.1:2380 \
-e ETCD_LISTEN_PEER_URLS=http://127.0.0.1:2380 \
-e ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster \
-e ETCD_INITIAL_CLUSTER=etcd1=http://127.0.0.1:2380,etcd2=http://127.0.0.1:2480,etcd3=http://127.0.0.1:2580 \
-e ETCD_INITIAL_CLUSTER_STATE=new \
--name etcd1 bitnami/etcd
etcd2
docker run -d -p 2480:2380 -p 2479:2379 \
-e ETCD_NAME=etcd2\
-e ALLOW_NONE_AUTHENTICATION=yes \
--network host \
-e ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:2479 \
-e ETCD_LISTEN_CLIENT_URLS=http://127.0.0.1:2479 \
-e ETCD_INITIAL_ADVERTISE_PEER_URLS=http://127.0.0.1:2480 \
-e ETCD_LISTEN_PEER_URLS=http://127.0.0.1:2480 \
-e ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster \
-e ETCD_INITIAL_CLUSTER=etcd1=http://127.0.0.1:2380,etcd2=http://127.0.0.1:2480,etcd3=http://127.0.0.1:2580 \
-e ETCD_INITIAL_CLUSTER_STATE=new \
--name etcd2 bitnami/etcd
etcd3
docker run -d -p 2580:2380 -p 2579:2379 \
-e ETCD_NAME=etcd3\
-e ALLOW_NONE_AUTHENTICATION=yes \
--network host \
-e ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:2579 \
-e ETCD_LISTEN_CLIENT_URLS=http://127.0.0.1:2579 \
-e ETCD_INITIAL_ADVERTISE_PEER_URLS=http://127.0.0.1:2580 \
-e ETCD_LISTEN_PEER_URLS=http://127.0.0.1:2580 \
-e ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster \
-e ETCD_INITIAL_CLUSTER=etcd1=http://127.0.0.1:2380,etcd2=http://127.0.0.1:2480,etcd3=http://127.0.0.1:2580 \
-e ETCD_INITIAL_CLUSTER_STATE=new \
--name etcd3 bitnami/etcd
GO操作Etcd方法
安装官方sdk至GOPATH
go get go.etcd.io/etcd/clientv3
导入sdk
import clientv3 "go.etcd.io/etcd/client/v3"
创建连接config.new()
client, err := clientv3.New(clientv3.Config{
Endpoints: []string{"ip:port"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatalln("连接etcd失败")
}
defer client.Close()
PUT
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
_, err = client.Put(ctx, "key", "value")
cancel()
if err != nil {
log.Printf("PUT key to etcd error : %v\n", err)
return
}
GET
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
resp, err := client.Get(ctx, "name")
cancel()
if err != nil {
log.Printf("GET key-value from etcd error : %v\n", err)
return
}
for _, ev := range resp.Kvs {
log.Printf("%s : %s\n", ev.Key, ev.Value)
}
DELETE
//clientv3.WithPrevKV()用于将删除的KV返回到delResp
delResp, err := client.Delete(context.TODO(),"foo1",clientv3.WithPrevKV())
//若成功删除数量大于0且删除参数加上clientv3.WithPrevKV()
if len(delResp.PrevKvs) > 0 {
for _, kvpair := range delResp.PrevKvs {
fmt.Printf("delete key is: %s \n Value: %s \n", string(kvpair.Key), string(kvpair.Value))
}
}
TXN:事务, etcd中事务是原子执行的,只支持if … then … else …这种表达
//创建事务
txn := kv.Txn(context.TODO())
//判断如果k1的值大于v1并且k1的版本号是2,则Put 键值k2和k3,否则Put键值k4和k5
kv.Txn(context.TODO()).If(
clientv3.Compare(clientv3.Value(k1), ">", v1),
clientv3.Compare(clientv3.Version(k1), "=", 2)
).Then(
clientv3.OpPut(k2,v2), clentv3.OpPut(k3,v3)
).Else(
clientv3.OpPut(k4,v4), clientv3.OpPut(k5,v5)
).Commit()
OP:操作
func OpDelete(key string, opts …OpOption) Op
func OpGet(key string, opts …OpOption) Op
func OpPut(key, val string, opts …OpOption) Op
func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op
ops := []clientv3.Op{
clientv3.OpPut("key", "value",),
clientv3.OpGet("key"),
clientv3.OpPut("key", "value")}
for _, op := range ops {
if _, err := cli.Do(context.TODO(), op); err != nil {
log.Fatal(err)
}
}
WATCH:监听某个值的变化
// respCh 是一个通道
respCh := client.Watch(context.Background(), "key")
// 若respCh为空,则会阻塞在这里
for watchResp := range respCh {
for _, v := range watchResp.Events {
log.Printf("type = %s , Key = %s , Value = %s\n", v.Type, v.Kv.Key, v.Kv.Value) //type是对这个值所进行的操作类型
}
}
LEASE:租约,即为一个key设置有效时间
// 创建一个20秒钟的租约
resp, err := client.Grant(context.TODO(), 20)
if err != nil {
log.Printf("client.Grant error : %v\n", err)
return
}
// 20秒钟之后,这个key就会被移除
_, err = client.Put(context.TODO(), "key", "value", clientv3.WithLease(resp.ID))
if err != nil {
log.Printf("client.Put error : %v\n", err)
return
}
KEEP-ALIVE:保活,即使租约永久存活(程序关闭后,使用租约存储的值依旧会消失)
resp, err := client.Grant(context.TODO(), 20)
if err != nil {
log.Printf("client.Grant error : %v\n", err)
return
}
_, err = client.Put(context.TODO(), "key", "value", clientv3.WithLease(resp.ID))
if err != nil {
log.Printf("client.Put error : %v\n", err)
return
}
// key ,将永久被保存
keepAliveChannel, err := client.KeepAlive(context.TODO(), resp.ID)
if err != nil {
log.Fatal(err)
}
/*
租期客户端每秒钟向etcd服务器发送keepalive请求
而返回的keepalive响应由keepAliveChannel来接收
如果这里不用循环去接收通道的值,在通道满了之后会发出警告
通道大小为16
*/
go func(){
for {
ka := <-keepAliveChannel
log.Println("ttl:", ka.TTL)
}
}()
分布式锁:不同会话使用同一个锁,同一个时间只能有一个会话上锁
import "go.etcd.io/etcd/client/v3/concurrency"
// 创建第一个会话
session1, err := concurrency.NewSession(client)
if err != nil {
log.Printf("concurrency.NewSession 1 error : %v\n", err)
return
}
defer session1.Close()
// 第一个会话设置锁
myMu1 := concurrency.NewMutex(session1, "/lock")
// 创建第二个会话
session2, err := concurrency.NewSession(client)
if err != nil {
log.Printf("concurrency.NewSession 2 error : %v\n", err)
return
}
defer session2.Close()
// 第二个会话设置锁
myMu2 := concurrency.NewMutex(session2, "/lock")
// 会话s1获取锁
if err := myMu1.Lock(context.TODO()); err != nil {
log.Printf("myMu1.Lock error : %v\n", err)
return
}
go func() {
//只有当第一个会话解锁后,第二个会话才能上锁
if err := myMu2.Lock(context.TODO()); err != nil {
log.Printf("myMu2.Lock error : %v\n", err)
return
}
}()
// 第一个会话解锁
if err := myMu1.Unlock(context.TODO()); err != nil {
log.Printf("myMu1.Unlock error : %v\n", err)
return
}
Etcd可视化工具
etcd-manager
简介
这是一个免费的、跨平台的ETCD v3 客户端和 GUI。该项目的目标是双重的:
注意,目前不支持ETCD V2 API ,仅支持V3。
支持的系统:macOS,windows,linux
官方下载链接
http://etcdmanager.io/
功能:
密钥管理:
管理(浏览、创建、编辑、删除)密钥。
使用 TTL 创建密钥
关键浏览器有多个视图:树或带分页的列表。
键列表实时更新:当任何键的值发生变化时刷新列表。
管理修订:列出任何键的修订并恢复到任何以前的值。
设置和配置:
身份验证和安全:
基本认证:(用户名/密码)
HTTPS 客户端证书认证
支持 HTTP 和 HTTPS(安全)连接
支持无身份验证(禁用身份验证的 ETCD)
其他功能:
显示ETCD集群及其节点的基本信息,进行健康检查。
管理租约:列出和撤销租约,查看详细信息。
管理用户:创建、更新或删除用户。
管理角色和权限:创建、更新删除角色、分配/撤销权限。
管理观察者。支持的事件响应程序:应用程序或桌面通知、应用程序控制台记录器。
内联网模式:无需互联网连接即可工作。