etcd是CoreOS团队于2013年6月发起的开源项目,它的目标是构建一个高可用的分布式键值(key-value)数据库。etcd内部采用raft
协议作为一致性算法,etcd基于Go语言实现。
etcd作为服务发现系统,有以下的特点:
etcd项目地址:https://github.com/etcd-io/etcd
etcd比较多的应用场景是用于服务发现,服务发现(Service Discovery)要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。
从本质上说,服务发现就是要了解集群中是否有进程在监听upd或者tcp端口,并且通过名字就可以进行查找和链接。
要解决服务发现的问题,需要下面三大支柱,缺一不可。
基于Ralf算法的etcd天生就是这样一个强一致性、高可用的服务存储目录。
用户可以在etcd中注册服务,并且对注册的服务配置key TTL,定时保持服务的心跳以达到监控健康状态的效果。
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,指令为:
etcdctl get "" --prefix --keys-only
在同级目录分别创建如下三个启动脚本:
默认使用2379端口提供HTTP API服务,2380端口和peer通信。
start01.bat
.\etcd.exe --name etcd01 ^
--data-dir .\data\etcd01 ^
--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
.\etcd.exe --name etcd02 ^
--data-dir .\data\etcd02 ^
--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
.\etcd.exe --name etcd03 ^
--data-dir .\data\etcd03 ^
--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,当输出如下信息时,代表集群创建成功了。
参数 |
使用说明 |
|
---|---|---|
--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都会向其注册自己的地址。 |
使用 etcdctl.exe member list 命令查看集群列表:
D:\etcd\etcd-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了
D:\etcd\etcd-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:
curl http://127.0.0.1:2379/v2/members
返回以下结果(3个节点):
{
"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
v2下;
etcdctl cluster-health
v3下:
etcdctl endpoint health --endpoints=$ENDPOINTS
etcdctl endpoint health --cluster=true
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来自于最近比较火的一个微服务框架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配置文件,配置如下:
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代码:
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类型 | 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 |
标量类型如果没有被赋值,则不会被序列化,解析时,会赋予默认值。
syntax = "proto3";
package pb;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
Server端代码:
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。
客户端:
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框架。