etcd简介
etcd是CoreOS团队于2013年6月发起的开源项目,它的目标是构建一个高可用的分布式键值(key-value)数据库。etcd内部采用raft
协议作为一致性算法,etcd基于Go语言实现。
etcd作为服务发现系统,有以下的特点:
- 简单:安装配置简单,而且提供了HTTP API进行交互,使用也很简单
- 安全:支持SSL证书验证
- 快速:根据官方提供的benchmark数据,单实例支持每秒2k 读操作
- 可靠:采用raft算法,实现分布式系统数据的可用性和一致性
etcd项目地址:https://github.com/etcd-io/etcd
etcd应用场景
etcd比较多的应用场景是用于服务发现,服务发现(Service Discovery)要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。
从本质上说,服务发现就是要了解集群中是否有进程在监听upd或者tcp端口,并且通过名字就可以进行查找和链接。
要解决服务发现的问题,需要下面三大支柱,缺一不可。
- 一个强一致性、高可用的服务存储目录。
基于Ralf算法的etcd天生就是这样一个强一致性、高可用的服务存储目录。
- 一种注册服务和健康服务健康状况的机制。
用户可以在etcd中注册服务,并且对注册的服务配置key TTL,定时保持服务的心跳以达到监控健康状态的效果。
- 一种查找和连接服务的机制。
etcd 安装包的下载地址
etcd地址:https://github.com/etcd-io/etcd/releases
选择对应的版本下载即可,Windows版本解压后文件目录如下:
etcd是服务端,etcdctl 是内置客户端
查看版本号 etcdctl --version
建议API version的版本设为3
windows下设置版本,使用: set ETCDCTL_API=3
etcdctl help查看版本3和2命令和功能方面有不少的差别
etcdctl的V3版本查看所有的keys,指令为:
代码语言:javascript复制etcdctl get "" --prefix --keys-only
在同级目录分别创建如下三个启动脚本:
默认使用2379端口提供HTTP API服务,2380端口和peer通信。
start01.bat
代码语言:javascript复制.etcd.exe --name etcd01 ^
--data-dir .dataetcd01 ^
--advertise-client-urls http://127.0.0.1:2379 ^
--listen-client-urls http://127.0.0.1:2379 ^
--listen-peer-urls http://127.0.0.1:2380 ^
--initial-advertise-peer-urls http://127.0.0.1:2380 ^
--initial-cluster-token etcd-cluster-1 ^
--initial-cluster etcd01=http://127.0.0.1:2380,etcd02=http://127.0.0.1:2381,etcd03=http://127.0.0.1:2382 ^
--initial-cluster-state new
pause
start02.bat
代码语言:javascript复制.etcd.exe --name etcd02 ^
--data-dir .dataetcd02 ^
--advertise-client-urls http://127.0.0.1:3379 ^
--listen-client-urls http://127.0.0.1:3379 ^
--listen-peer-urls http://127.0.0.1:2381 ^
--initial-advertise-peer-urls http://127.0.0.1:2381 ^
--initial-cluster-token etcd-cluster-1 ^
--initial-cluster etcd01=http://127.0.0.1:2380,etcd02=http://127.0.0.1:2381,etcd03=http://127.0.0.1:2382 ^
--initial-cluster-state new
pause
start03.bat
代码语言:javascript复制.etcd.exe --name etcd03 ^
--data-dir .dataetcd03 ^
--advertise-client-urls http://127.0.0.1:4379 ^
--listen-client-urls http://127.0.0.1:4379 ^
--listen-peer-urls http://127.0.0.1:2382 ^
--initial-advertise-peer-urls http://127.0.0.1:2382 ^
--initial-cluster-token etcd-cluster-1 ^
--initial-cluster etcd01=http://127.0.0.1:2380,etcd02=http://127.0.0.1:2381,etcd03=http://127.0.0.1:2382 ^
--initial-cluster-state new
pause
然后在同级目录下创建好对应的data-dir,如/data/etcd01、/data/etcd02、/data/etcd03,不创建也行,启动后会自动创建。
依次启动start01.bat、start02.bat、start03.bat三个脚本,然后使用etcdctl.exe member list,当输出如下信息时,代表集群创建成功了。
etcd集群启动参数说明
参数 | 使用说明 | |
---|---|---|
--name etcd0 | 本member的名字 | |
--initial-advertise-peer-urls http://192.168.2.55:2380 | 其他member使用,其他member通过该地址与本member交互信息。一定要保证从其他member能可访问该地址。静态配置方式下,该参数的value一定要同时在--initial-cluster参数中存在。 memberID的生成受--initial-cluster-token和--initial-advertise-peer-urls影响。 | |
--listen-peer-urls http://0.0.0.0:2380 | 本member侧使用,用于监听其他member发送信息的地址。ip为全0代表监听本member侧所有接口 | |
--listen-client-urls http://0.0.0.0:2379 | 本member侧使用,用于监听etcd客户发送信息的地址。ip为全0代表监听本member侧所有接口 | |
--advertise-client-urls http://192.168.2.55:2379 | etcd客户使用,客户通过该地址与本member交互信息。一定要保证从客户侧能可访问该地址 | |
--initial-cluster-token etcd-cluster-2 | 用于区分不同集群。本地如有多个集群要设为不同。 | |
--initial-cluster etcd0=http://192.168.2.55:2380, etcd1=http://192.168.2.54:2380 ,etcd2=http://192.168.2.56:2380 | 本member侧使用。描述集群中所有节点的信息,本member根据此信息去联系其他member。 memberID的生成受--initial-cluster-token和--initial-advertise-peer-urls影响。 | |
--initial-cluster-state new | 用于指示本次是否为新建集群。有两个取值new和existing。如果填为existing,则该member启动时会尝试与其他member交互。 集群初次建立时,要填为new,经尝试最后一个节点填existing也正常,其他节点不能填为existing。 集群运行过程中,一个member故障后恢复时填为existing,经尝试填为new也正常。 | |
-data-dir | 指定节点的数据存储目录,这些数据包括节点ID,集群ID,集群初始化配置,Snapshot文件,若未指定-wal-dir,还会存储WAL文件;如果不指定会用缺省目录。 | |
-discovery http://192.168.1.163:20003/v2/keys/discovery/78b12ad7-2c1d-40db-9416-3727baf686cb | 用于自发现模式下,指定第三方etcd上key地址,要建立的集群各member都会向其注册自己的地址。 |
- —data-dir 指定节点的数据存储目录,这些数据包括节点ID,集群ID,集群初始化配置,Snapshot文件,若未指定—wal-dir,还会存储WAL文件;
- —wal-dir 指定节点的was文件的存储目录,若指定了该参数,wal文件会和其他数据文件分开存储。
- —name 节点名称
- —initial-advertise-peer-urls 告知集群其他节点url.
- — listen-peer-urls 监听URL,用于与其他节点通讯
- — advertise-client-urls 告知客户端url, 也就是服务的url
- — initial-cluster-token 集群的ID
- — initial-cluster 集群中所有节点
检查etcd服务列表
使用 etcdctl.exe member list 命令查看集群列表:
代码语言:javascript复制D:etcdetcd-v3.3.25-windows-amd64>etcdctl.exe member list
19ac17627e3e396f: name=etcd03 peerURLs=http://127.0.0.1:2382 clientURLs=http://127.0.0.1:4379 isLeader=false
bf9071f4639c75cc: name=etcd01 peerURLs=http://127.0.0.1:2380 clientURLs=http://127.0.0.1:2379 isLeader=true
e7b968b9fb1bc003: name=etcd02 peerURLs=http://127.0.0.1:2381 clientURLs=http://127.0.0.1:3379 isLeader=false
如果出现如下的信息,代表可能etcd启动过程阻塞住了,只要在cmd窗口里按下回车键就ok了
代码语言:javascript复制D:etcdetcd-v3.3.25-windows-amd64>etcdctl.exe member list
client: etcd cluster is unavailable or misconfigured; error #0: dial tcp 127.0.0.1:4001: connectex: No connection could be made because the target machine actively refused it.
; error #1: client: endpoint http://127.0.0.1:2379 exceeded header timeout
或者使用curl访问或网页输入查看http://127.0.0.1:2379/v2/members:
代码语言:javascript复制curl http://127.0.0.1:2379/v2/members
返回以下结果(3个节点):
代码语言:javascript复制{
"members": [{
"id": "19ac17627e3e396f",
"name": "etcd03",
"peerURLs": ["http://127.0.0.1:2382"],
"clientURLs": []
}, {
"id": "bf9071f4639c75cc",
"name": "etcd01",
"peerURLs": ["http://127.0.0.1:2380"],
"clientURLs": ["http://127.0.0.1:2379"]
}, {
"id": "e7b968b9fb1bc003",
"name": "etcd02",
"peerURLs": ["http://127.0.0.1:2381"],
"clientURLs": ["http://127.0.0.1:3379"]
}]
}
在任意节点执行健康检查,查看集群状态:
(注:仅etcdctl 有v2和v3两种api,注意区别),关于v3 api的用法,参见etcdctl的使用[v3版本]:https://blog.csdn.net/huwh_/article/details/80225902
代码语言:javascript复制etcdctl cluster-health
etcd的go客户端的简单操作:
代码语言:javascript复制package main
import (
"context"
"fmt"
"time"
"github.com/coreos/etcd/clientv3"
)
func main() {
//客户端配置
config := clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
}
//建立连接
client, err := clientv3.New(config)
defer client.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("connect success")
//控制超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
//1. 增-存值
_, err = client.Put(ctx, "/demo/demo1_key", "demo1_value")
//操作完毕,cancel掉
cancel()
if err != nil {
fmt.Println("put failed, err:", err)
return
}
//2. 查-获取值, 也设置超时
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
resp, err := client.Get(ctx, "/demo/demo1_key")
// Get查询还可以增加WithPrefix选项,获取某个目录下的所有子元素
//eg: resp, err := client.Get(ctx, "/demo/", clientv3.WithPrefix())
cancel()
if err != nil {
fmt.Println("get failed err:", err)
return
}
for _, item := range resp.Kvs { //Kvs 返回key的列表
fmt.Printf("%s : %s n", item.Key, item.Value)
}
//3. 改-修改值
ctx, _ = context.WithTimeout(context.Background(), time.Second)
_,err = client.Put(ctx, "/demo/demo1_key", "update_value", clientv3.WithPrevKV())
if err != nil {
fmt.Println("get failed err: ", err)
}
//fmt.Println(string(resp.PrevKv.Value))
//4. 删-删除值
ctx, _ = context.WithTimeout(context.Background(), time.Second)
_, err = client.Delete(ctx, "/demo/demo1_key")
if err != nil {
fmt.Println(err)
}
//fmt.Println(resp.PrevKvs)
}
zRPC的简单使用
zRPC来自于最近比较火的一个微服务框架go-zero。go-zero是一个集成了各种工程实践的包含了Web和RPC协议的功能完善的微服务框架,zRPC是其中的一个可独立使用的模块。
zRPC地址:https://github.com/tal-tech/go-zero/tree/master/zrpc
zRPC底层依赖gRPC,内置了服务注册、负载均衡、拦截器等模块,其中还包括自适应降载,自适应熔断,限流等微服务治理方案,是一个简单易用的可直接用于生产的企业级RPC框架。
zRPC支持直连和基于etcd服务发现两种方式,我们以基于etcd做服务发现为例演示zRPC的基本使用:
配置
创建hello.yaml配置文件,配置如下:
代码语言:javascript复制Name: hello.rpc // 服务名
ListenOn: 127.0.0.1:9090 // 服务监听地址
Etcd:
Hosts:
- 127.0.0.1:2379 // etcd服务地址
Key: hello.rpc // 服务注册key
注意文件编码必须为utf-8,格式也要正确。
创建hello.proto文件,并生成对应的go代码。
生成go代码:
代码语言:javascript复制protoc --go_out=plugins=grpc:. hello.proto
protoc的安装,https://github.com/protocolbuffers/protobuf/releases
从 Protobuf Releases 下载最先版本的发布包安装即可,可放到go的bin目录内全局使用。
下载protobuf编译器所需插件 用git下载protoc在go下运行所需插件(执行): go get github.com/golang/protobuf(gopath的bin目录会生成protoc-gen-go.exe)
proto文件标量类型
proto类型 | go类型 | 备注 | proto类型 | go类型 | 备注 |
---|---|---|---|---|---|
double | float64 | float | float32 | ||
int32 | int32 | int64 | int64 | ||
uint32 | uint32 | uint64 | uint64 | ||
sint32 | int32 | 适合负数 | sint64 | int64 | 适合负数 |
fixed32 | uint32 | 固长编码,适合大于2^28的值 | fixed64 | uint64 | 固长编码,适合大于2^56的值 |
sfixed32 | int32 | 固长编码 | sfixed64 | int64 | 固长编码 |
bool | bool | string | string | UTF8 编码,长度不超过 2^32 | |
bytes | []byte | 任意字节序列,长度不超过 2^32 |
标量类型如果没有被赋值,则不会被序列化,解析时,会赋予默认值。
- strings:空字符串
- bytes:空序列
- bools:false
- 数值类型:0
简单示例:
代码语言:javascript复制syntax = "proto3";
package pb;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
服务端:
Server端代码:
代码语言:javascript复制package main
import (
"context"
"flag"
"log"
"testzrpc/msgs/zrpc/pb"
"github.com/tal-tech/go-zero/core/conf"
"github.com/tal-tech/go-zero/zrpc"
"google.golang.org/grpc"
)
type Config struct {
zrpc.RpcServerConf
}
var cfgFile = flag.String("f", "./hello.yaml", "cfg file")
func main() {
flag.Parse()
var cfg Config
conf.MustLoad(*cfgFile, &cfg)
srv, err := zrpc.NewServer(cfg.RpcServerConf, func(s *grpc.Server) {
pb.RegisterGreeterServer(s, &Hello{})
})
if err != nil {
log.Fatal(err)
}
srv.Start()
}
type Hello struct{}
func (h *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "server say,hello " in.Name}, nil
}
需要注意的是:
需要更改下go.mod文件中的grpc版本号。默认最新的1.34,启动服务端会报错的,可改为使用v1.29.1
etcd/clientv3库跟最新的grpc版本不兼容,需要降低 grpc
版本,如1.26.0。
客户端:
代码语言:javascript复制package main
import (
"context"
"log"
"testzrpc/msgs/zrpc/pb"
"github.com/tal-tech/go-zero/core/discov"
"github.com/tal-tech/go-zero/zrpc"
)
func main() {
client := zrpc.MustNewClient(zrpc.RpcClientConf{
Etcd: discov.EtcdConf{
Hosts: []string{"127.0.0.1:2379"},
Key: "hello.rpc",
},
})
conn := client.Conn()
hello := pb.NewGreeterClient(conn)
reply, err := hello.SayHello(context.Background(), &pb.HelloRequest{Name: "go-zero aaaa"})
if err != nil {
log.Fatal(err)
}
log.Println(reply.Message)
}
多个微服务如何使用?如何实现负载均衡和容灾。做个试验,把server.go复制一份,改为server1.go,
把hello.yaml复制一份改为hello1.yaml,改hello1.yaml中的ListenOn,端口变为9091,这时候把server.go和server1.go同时运行起来,看一下:
此时,运行下客户端,发现输出为:
再把server1.go服务停掉,运行下客户端试试输出为:
综上,使用zRPC挺简单的,并且zRPC内置了服务注册、负载均衡、拦截器等模块。
其中还包括自适应降载,自适应熔断,限流等微服务治理方案,是一个简单易用的可直接用于生产的企业级RPC框架。