实现etcd服务注册与发现
0.1、目录结构
.
├── api
│ └── main.go
├── common
│ └── common.go
├── docker-compose.yml
├── etcd
│ └── Dockerfile
├── go.mod
├── go.sum
├── rpc
│ ├── courseware
│ │ ├── courseware.pb.go
│ │ └── courseware_grpc.pb.go
│ ├── courseware.proto
│ └── main.go
└── server
├── service_discovery.go
└── service_registration.go
1、docker-compose部署一个3节点的集群
项目根目录下创建etcd目录,并在目录下新增Dockerfile文件
FROM bitnami/etcd:latest
LABEL maintainer="liuyuede123 <liufutianoppo@163.com>"
项目根目录下新增docker-compose.yml
version: '3.5'
# 网络配置
networks:
backend:
driver: bridge
# 服务容器配置
services:
etcd1: # 自定义容器名称
build:
context: etcd # 指定构建使用的 Dockerfile 文件
environment:
- TZ=Asia/Shanghai
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd1
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd1:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd1:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
ports: # 设置端口映射
- "12379:2379"
- "12380:2380"
networks:
- backend
restart: always
etcd2: # 自定义容器名称
build:
context: etcd # 指定构建使用的 Dockerfile 文件
environment:
- TZ=Asia/Shanghai
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd2
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd2:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd2:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
ports: # 设置端口映射
- "22379:2379"
- "22380:2380"
networks:
- backend
restart: always
etcd3: # 自定义容器名称
build:
context: etcd # 指定构建使用的 Dockerfile 文件
environment:
- TZ=Asia/Shanghai
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_NAME=etcd3
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd3:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd3:2379
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
- ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380
- ETCD_INITIAL_CLUSTER_STATE=new
ports: # 设置端口映射
- "32379:2379"
- "32380:2380"
networks:
- backend
restart: always
相关参数概念:
- ETCD_INITIAL_ADVERTISE_PEER_URLS:该成员节点在整个集群中的通信地址列表,这个地址用来传输集群数据的地址。因此这个地址必须是可以连接集群中所有的成员的。
- ETCD_LISTEN_PEER_URLS:该节点与其他节点通信时所监听的地址列表,多个地址使用逗号隔开,其格式可以划分为scheme://IP:PORT,这里的scheme可以是http、https
- ETCD_LISTEN_CLIENT_URLS:该节点与客户端通信时监听的地址列表
- ETCD_ADVERTISE_CLIENT_URLS:广播给集群中其他成员自己的客户端地址列表
- ETCD_INITIAL_CLUSTER_TOKEN:初始化集群token
- ETCD_INITIAL_CLUSTER:配置集群内部所有成员地址,其格式为:ETCD_NAME=ETCD_INITIAL_ADVERTISE_PEER_URLS,如果有多个使用逗号隔开
- ETCD_INITIAL_CLUSTER_STATE:初始化集群状态,new表示新建
启动集群
docker-compose up -d
Creating network "etcd_backend" with driver "bridge"
Creating etcd_etcd1_1 ... done
Creating etcd_etcd2_1 ... done
Creating etcd_etcd3_1 ... done
测试集群可用性
# 登录其中一个节点
docker exec -it 5f97bf0b446f6e6514576fc1eb46c2f60d2c2b3e3f3ee3b1ad6219414fa915c8 /bin/sh
# 写入一个键值
etcdctl put name "liuyuede"
OK
# 查看
etcdctl get name
name
liuyuede
# 登录另外俩个节点
docker exec -it a6ccc9b6e5cc81ee7c779e2b9e7235cd6d814e92fbc66b7e4846798acff8ee2a /bin/sh
etcdctl get name
name
liuyuede
docker exec -it 6817fa89e3e9e422628e0049910b672df389c62d41bf2349a0f77e22c99e5270 /bin/sh
etcdctl get name
name
liuyuede
etcd集群采用的是raft协议,一般至少为俩个集群,只有一个master,如果删除到只剩一个节点当前节点也不能提供服务
查看集群情况
etcdctl --endpoints=http://0.0.0.0:12379,0.0.0.0:22379,0.0.0.0:32379 endpoint status --write-out=table
+----------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+----------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| http://0.0.0.0:12379 | ade526d28b1f92f7 | 3.5.4 | 20 kB | true | false | 3 | 13 | 13 | |
| 0.0.0.0:22379 | d282ac2ce600c1ce | 3.5.4 | 20 kB | false | false | 3 | 13 | 13 | |
| 0.0.0.0:32379 | bd388e7810915853 | 3.5.4 | 20 kB | false | false | 3 | 13 | 13 | |
+----------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
2、增加服务注册功能
服务注册的流程
- 向etcd新增一个包含rpc服务信息的键值对,并设置租约(比如5秒过期)
- 利用保活函数KeepAlive不断续约
package server
import (
"context"
"encoding/json"
"errors"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
type ServiceInfo struct {
Name string
Ip string
}
type Service struct {
ServiceInfo ServiceInfo
stop chan error
leaseId clientv3.LeaseID
client *clientv3.Client
}
func NewService(serviceInfo ServiceInfo, endpoints []string) (service *Service, err error) {
client, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: time.Second * 10,
})
if err != nil {
return nil, err
}
service = &Service{
ServiceInfo: serviceInfo,
client: client,
}
return
}
func (s *Service) Start(ctx context.Context) (err error) {
alive, err := s.KeepAlive(ctx)
if err != nil {
return
}
for {
select {
case err = <-s.stop: // 服务端关闭返回错误
return err
case <-s.client.Ctx().Done(): // etcd关闭
return errors.New("server closed")
case _, ok := <-alive:
if !ok { // 保活通道关闭
return s.revoke(ctx)
}
}
}
}
func (s *Service) KeepAlive(ctx context.Context) (<-chan *clientv3.LeaseKeepAliveResponse, error) {
info := s.ServiceInfo
key := s.getKey()
val, _ := json.Marshal(info)
// 创建租约
leaseResp, err := s.client.Grant(ctx, 5)
if err != nil {
return nil, err
}
// 写入etcd
_, err = s.client.Put(ctx, key, string(val), clientv3.WithLease(leaseResp.ID))
if err != nil {
return nil, err
}
s.leaseId = leaseResp.ID
return s.client.KeepAlive(ctx, leaseResp.ID)
}
// 取消租约
func (s *Service) revoke(ctx context.Context) error {
_, err := s.client.Revoke(ctx, s.leaseId)
return err
}
func (s *Service) getKey() string {
return s.ServiceInfo.Name + "/" + s.ServiceInfo.Ip
}
3、增加服务发现
服务发现流程
- 实现grpc中resolver.Builder接口的Build方法
- 通过etcdclient获取并监听grpc服务(是否有新增或者删除)
- 更新到resolver.State,State 包含与 ClientConn 相关的当前 Resolver 状态,包括grpc的地址resolver.Address
package server
import (
"context"
"encoding/json"
"fmt"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
"google.golang.org/grpc/resolver"
)
type Discovery struct {
endpoints []string
service string
client *clientv3.Client
clientConn resolver.ClientConn
}
func NewDiscovery(endpoints []string, service string) resolver.Builder {
return &Discovery{
endpoints: endpoints,
service: service,
}
}
func (d *Discovery) ResolveNow(rn resolver.ResolveNowOptions) {
}
func (d *Discovery) Close() {
}
func (d *Discovery) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
var err error
d.client, err = clientv3.New(clientv3.Config{
Endpoints: d.endpoints,
})
if err != nil {
return nil, err
}
d.clientConn = cc
go d.watch(d.service)
return d, nil
}
func (d *Discovery) Scheme() string {
return "etcd"
}
func (d *Discovery) watch(service string) {
addrM := make(map[string]resolver.Address)
state := resolver.State{}
update := func() {
addrList := make([]resolver.Address, 0, len(addrM))
for _, address := range addrM {
addrList = append(addrList, address)
}
state.Addresses = addrList
err := d.clientConn.UpdateState(state)
if err != nil {
fmt.Println("更新地址出错:", err)
}
}
resp, err := d.client.Get(context.Background(), service, clientv3.WithPrefix())
if err != nil {
fmt.Println("获取地址出错:", err)
} else {
for i, kv := range resp.Kvs {
info := &ServiceInfo{}
err = json.Unmarshal(kv.Value, info)
if err != nil {
fmt.Println("解析value失败:", err)
}
addrM[string(resp.Kvs[i].Key)] = resolver.Address{
Addr: info.Ip,
ServerName: info.Name,
}
}
}
update()
dch := d.client.Watch(context.Background(), service, clientv3.WithPrefix(), clientv3.WithPrevKV())
for response := range dch {
for _, event := range response.Events {
switch event.Type {
case mvccpb.PUT:
info := &ServiceInfo{}
err = json.Unmarshal(event.Kv.Value, info)
if err != nil {
fmt.Println("监听时解析value报错:", err)
} else {
addrM[string(event.Kv.Key)] = resolver.Address{Addr: info.Ip}
}
fmt.Println(string(event.Kv.Key))
case mvccpb.DELETE:
delete(addrM, string(event.Kv.Key))
fmt.Println(string(event.Kv.Key))
}
}
update()
}
}
4、grpc课件服务
common参数
package common
const CoursewareRpc = "rpc.courseware"
var Endpoints = []string{"127.0.0.1:12379", "127.0.0.1:22379", "127.0.0.1:32379"}
生成课件服务grpc
syntax = "proto3";
package rpc;
option go_package = "./courseware";
message GetRequest {
uint64 Id = 1;
}
message GetResponse {
uint64 Id = 1;
string Code = 2;
string Name = 3;
uint64 Type = 4;
}
service Courseware {
rpc Get(GetRequest) returns(GetResponse);
}
protoc --go_out=./ --go-grpc_out=./ courseware.proto
课件服务入口
package main
import (
"context"
"fmt"
"go-demo/etcd/common"
"go-demo/etcd/rpc/courseware"
"go-demo/etcd/server"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"net"
"os"
"strings"
"time"
)
var Port string
type service struct {
courseware.UnsafeCoursewareServer
}
func (s *service) Get(ctx context.Context, req *courseware.GetRequest) (res *courseware.GetResponse, err error) {
fmt.Println("获取课件详情 port:", Port, " time:", time.Now())
return &courseware.GetResponse{
Id: 1,
Code: "HD4544",
Name: "多媒体课件",
Type: 4,
}, nil
}
func main() {
args := os.Args[1:]
if len(args) == 0 {
panic("缺少port参数:port=8400")
}
for _, arg := range args {
ports := strings.Split(arg, "=")
if len(ports) < 2 || ports[0] != "port" {
panic("port参数格式错误:port=8400")
}
Port = ports[1]
}
listen, err := net.Listen("tcp", ":"+Port)
if err != nil {
fmt.Println("failed to listen", err)
return
}
s := grpc.NewServer()
courseware.RegisterCoursewareServer(s, &service{})
reflection.Register(s)
// 注册到etcd
newService, err := server.NewService(server.ServiceInfo{
Name: common.CoursewareRpc,
Ip: "127.0.0.1:" + Port,
}, common.Endpoints)
if err != nil {
fmt.Println("添加到etcd失败:", err)
return
}
go func() {
err = newService.Start(context.Background())
if err != nil {
fmt.Println("开启服务注册失败:", err)
}
}()
if err = s.Serve(listen); err != nil {
fmt.Println("开启rpc服务失败:", err)
}
}
5、api服务
package main
import (
"context"
"fmt"
"go-demo/etcd/common"
"go-demo/etcd/rpc/courseware"
"go-demo/etcd/server"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
"time"
)
func main() {
d := server.NewDiscovery(common.Endpoints, common.CoursewareRpc)
resolver.Register(d)
for {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// 通过etcd注册中心和grpc服务建立连接
conn, err := grpc.DialContext(ctx,
fmt.Sprintf(d.Scheme()+":///"+common.CoursewareRpc),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
grpc.WithBlock(),
)
if err != nil {
fmt.Println("和rpc建立连接失败:", err)
return
}
client := courseware.NewCoursewareClient(conn)
get, err := client.Get(ctx, &courseware.GetRequest{Id: 1})
if err != nil {
fmt.Println("获取课件失败:", err)
return
}
fmt.Println(get)
time.Sleep(3 * time.Second)
cancel()
}
}
6、测试
开启3个服务,可以看到客户端通过负载均衡随机到一个服务请求
go run main.go port=8400
获取课件详情 port: 8400 time: 2022-08-25 18:47:43.784942 +0800 CST m=+78.228450885
获取课件详情 port: 8400 time: 2022-08-25 18:47:52.925858 +0800 CST m=+87.369721731
获取课件详情 port: 8400 time: 2022-08-25 18:48:02.001177 +0800 CST m=+96.445393312
获取课件详情 port: 8400 time: 2022-08-25 18:48:05.060066 +0800 CST m=+99.504401028
获取课件详情 port: 8400 time: 2022-08-25 18:48:14.154148 +0800 CST m=+108.598836458
go run main.go port=8500
获取课件详情 port: 8500 time: 2022-08-25 18:47:46.832479 +0800 CST m=+62.822399701
获取课件详情 port: 8500 time: 2022-08-25 18:47:49.844536 +0800 CST m=+65.834573960
获取课件详情 port: 8500 time: 2022-08-25 18:47:55.955638 +0800 CST m=+71.945912584
获取课件详情 port: 8500 time: 2022-08-25 18:48:17.168293 +0800 CST m=+93.159391485
获取课件详情 port: 8500 time: 2022-08-25 18:48:20.182787 +0800 CST m=+96.174002796
go run main.go port=8600
获取课件详情 port: 8600 time: 2022-08-25 18:47:58.968283 +0800 CST m=+1.317052360
获取课件详情 port: 8600 time: 2022-08-25 18:48:08.106493 +0800 CST m=+10.455617422
获取课件详情 port: 8600 time: 2022-08-25 18:48:11.125212 +0800 CST m=+13.474453269
实现etcd服务注册与发现的更多相关文章
- Golang etcd服务注册与发现
//sevice.go package discovery import ( "context" "errors" "sync" " ...
- Docker+Jenkins持续集成环境(4):使用etcd+confd实现容器服务注册与发现
前面我们已经通过jenkins+docker搭建了基本的持续集成环境,实现了服务的自动构建和部署,但是,我们遇到一个问题,jenkins构建出来的镜像部署后,需要通过ip:port去访问,有什么更好的 ...
- 分布式服务注册和发现consul 简要介绍
Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置.与其他分布式服务注册与发现的方案,Consul的方案更"一站式",内置了服务注册与发现框 架 ...
- Docker+Consul+Registrator 实现服务注册与发现
Docker+Consul+Registrator实现服务注册与发现 逻辑图 实现nginx节点自动化加入容器IP代理 1.三台Consul agent server作为高可用通过Consul Tem ...
- 024.微服务架构之服务注册与发现(kubernetes / SpringCloud)
微服务 微服务是一种架构模式,一种分布式的架构风格. 顾名思义,micro service,将一个庞大的单体应用拆分成若干个“微小”的服务,服务间通过进程通讯完成原本在单体应用中的调用. 其中必要的六 ...
- (8)ASP.NET Core3.1 Ocelot Consul服务注册与发现
1.服务注册与发现(Service Discovery) ●服务注册:我们通过在每个服务实例写入注册代码,实例在启动的时候会先去注册中心(例如Consul.ZooKeeper.etcd.Eureka) ...
- 一个故事,一段代码告诉你如何使用不同语言(Golang&C#)提供相同的能力基于Consul做服务注册与发现
目录 引言 什么是微服务 传统服务 微服务 什么是服务注册与服务发现 为什么要使用不同的语言提供相同的服务能力 服务协调器 服务注册 Golang C#(.NetCore3.1) 服务发现 通过Htt ...
- Spring cloud实现服务注册及发现
服务注册与发现对于微服务系统来说非常重要.有了服务发现与注册,你就不需要整天改服务调用的配置文件了,你只需要使用服务的标识符,就可以访问到服务. 本文属于<7天学会spring cloud系列& ...
- Spring Cloud构建微服务架构(一)服务注册与发现
Spring Cloud简介 Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理.服务发现.断路器.智能路由.微代理.控制总线.全局锁 ...
随机推荐
- GreatSQL特性介绍及前景展望 | 数据技术嘉年华2021分享PPT发布
欢迎来到 GreatSQL社区分享的MySQL技术文章,如有疑问或想学习的内容,可以在下方评论区留言,看到后会进行解答 GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 全 ...
- Reactive UI -- 反应式编程UI框架入门学习(一)
推荐一个反应式编程的MVVM跨平台框架. 反应式编程 反应式编程是一种相对于命令式的编程范式,由函数式的组合声明来构建异步数据流.要理解这个概念,可以简单的借助Excel中的单元格函数. 上图中,A1 ...
- 从零开始Blazor Server(9)--修改Layout
目前我们的MainLayout还是默认的,这里我们需要修改为BootstrapBlazor的Layout,并且处理一下菜单. 修改MainLayout BootstrapBlazor已经自带了一个La ...
- Apache DolphinScheduler 2.X保姆级源码解析,中国移动工程师揭秘服务调度启动全流程
2022年1月,科学技术部高新技术司副司长梅建平在"第六届中国新金融高峰论坛"上表示,当前数据量已经大大超过了处理能力的上限,若信息技术仍然是渐进式发展,则数据处理能力的提升将远远 ...
- 搞定面试官 - 你可以介绍一下在 MySQL 中,哪些情况下 索引会失效嘛?
大家好,我是程序员啊粥,前边给大家分享了 *MySQL InnoDB 索引模型 在 MySQL InnoDB 中,为什么 delete 删除数据之后表数据文件大小没有变 如何计算一个索引的长度 如何查 ...
- 10种有用的Linux Bash_Completion 命令示例
摘要:我们可以对这个 bash 补全进行加速,并使用 complete 命令将其提升到一个新的水平. 本文分享自华为云社区<有用的 Linux Bash_Completion 命令示例(Ster ...
- UE蓝图---实现场景物体Transform状态重置效果
在工业领域应用中,通常会遇到操作场景模型变换的操作,经过了移动.旋转.缩放后,要求可一键重置还原最初的Transform状态. 思路:1.在模型阶段设置每个模型Tag值为Oper,表明是可被操作的对象 ...
- PowerPoint 母版与版式
母版 使用母版可以对幻灯片进行统一的样式编辑,让幻灯片具有相同的外观效果,这样无须在多张幻灯片上重复输入相同的信息.母版分为幻灯片母版.讲义母版和备注母版. 幻灯片母版 幻灯片母版决定着幻灯片的外观, ...
- React报错之Invalid hook call
正文从这开始~ 总览 导致"Invalid hook call. Hooks can only be called inside the body of a function compone ...
- 动态规划——leetcode5、最长回文子串
1.题目描述: 2.解题方法:动态规划 动态规划解题步骤: 1.确定状态 最后一步:如果s[i,...,j]是回文子串,那么需要满足两个条件 ① s[i] == s[j]: ② s[i+1,...,j ...