etcd实现服务发现
前言
etcd环境安装与使用文章中介绍了etcd的安装及v3 API使用,本篇将介绍如何使用etcd实现服务发现功能。
服务发现介绍
服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。

服务发现需要实现一下基本功能:
服务注册:同一service的所有节点注册到相同目录下,节点启动后将自己的信息注册到所属服务的目录中。健康检查:服务节点定时进行健康检查。注册到服务目录中的信息设置一个较短的TTL,运行正常的服务节点每隔一段时间会去更新信息的TTL ,从而达到健康检查效果。服务发现:通过服务节点能查询到服务提供外部访问的 IP 和端口号。比如网关代理服务时能够及时的发现服务中新增节点、丢弃不可用的服务节点。
接下来介绍如何使用etcd实现服务发现。
服务注册及健康检查
根据etcd的v3 API,当启动一个服务时候,我们把服务的地址写进etcd,注册服务。同时绑定租约(lease),并以续租约(keep leases alive)的方式检测服务是否正常运行,从而实现健康检查。
go代码实现:
package main
import (
"context"
"log"
"time"
"go.etcd.io/etcd/clientv3"
)
//ServiceRegister 创建租约注册服务
type ServiceRegister struct {
cli *clientv3.Client //etcd client
leaseID clientv3.LeaseID //租约ID
//租约keepalieve相应chan
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
key string //key
val string //value
}
//NewServiceRegister 新建注册服务
func NewServiceRegister(endpoints []string, key, val string, lease int64) (*ServiceRegister, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
ser := &ServiceRegister{
cli: cli,
key: key,
val: val,
}
//申请租约设置时间keepalive
if err := ser.putKeyWithLease(lease); err != nil {
return nil, err
}
return ser, nil
}
//设置租约
func (s *ServiceRegister) putKeyWithLease(lease int64) error {
//设置租约时间
resp, err := s.cli.Grant(context.Background(), lease)
if err != nil {
return err
}
//注册服务并绑定租约
_, err = s.cli.Put(context.Background(), s.key, s.val, clientv3.WithLease(resp.ID))
if err != nil {
return err
}
//设置续租 定期发送需求请求
leaseRespChan, err := s.cli.KeepAlive(context.Background(), resp.ID)
if err != nil {
return err
}
s.leaseID = resp.ID
log.Println(s.leaseID)
s.keepAliveChan = leaseRespChan
log.Printf("Put key:%s val:%s success!", s.key, s.val)
return nil
}
//ListenLeaseRespChan 监听 续租情况
func (s *ServiceRegister) ListenLeaseRespChan() {
for leaseKeepResp := range s.keepAliveChan {
log.Println("续约成功", leaseKeepResp)
}
log.Println("关闭续租")
}
// Close 注销服务
func (s *ServiceRegister) Close() error {
//撤销租约
if _, err := s.cli.Revoke(context.Background(), s.leaseID); err != nil {
return err
}
log.Println("撤销租约")
return s.cli.Close()
}
func main() {
var endpoints = []string{"localhost:2379"}
ser, err := NewServiceRegister(endpoints, "/web/node1", "localhost:8000", 5)
if err != nil {
log.Fatalln(err)
}
//监听续租相应chan
go ser.ListenLeaseRespChan()
select {
// case <-time.After(20 * time.Second):
// ser.Close()
}
}
主动退出服务时,可以调用Close()方法,撤销租约,从而注销服务。
服务发现
根据etcd的v3 API,很容易想到使用Watch监视某类服务,通过Watch感知服务的添加,修改或删除操作,修改服务列表。
package main
import (
"context"
"log"
"sync"
"time"
"github.com/coreos/etcd/mvcc/mvccpb"
"go.etcd.io/etcd/clientv3"
)
//ServiceDiscovery 服务发现
type ServiceDiscovery struct {
cli *clientv3.Client //etcd client
serverList map[string]string //服务列表
lock sync.Mutex
}
//NewServiceDiscovery 新建发现服务
func NewServiceDiscovery(endpoints []string) *ServiceDiscovery {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
return &ServiceDiscovery{
cli: cli,
serverList: make(map[string]string),
}
}
//WatchService 初始化服务列表和监视
func (s *ServiceDiscovery) WatchService(prefix string) error {
//根据前缀获取现有的key
resp, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
return err
}
for _, ev := range resp.Kvs {
s.SetServiceList(string(ev.Key), string(ev.Value))
}
//监视前缀,修改变更的server
go s.watcher(prefix)
return nil
}
//watcher 监听前缀
func (s *ServiceDiscovery) watcher(prefix string) {
rch := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
log.Printf("watching prefix:%s now...", prefix)
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT: //修改或者新增
s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
case mvccpb.DELETE: //删除
s.DelServiceList(string(ev.Kv.Key))
}
}
}
}
//SetServiceList 新增服务地址
func (s *ServiceDiscovery) SetServiceList(key, val string) {
s.lock.Lock()
defer s.lock.Unlock()
s.serverList[key] = string(val)
log.Println("put key :", key, "val:", val)
}
//DelServiceList 删除服务地址
func (s *ServiceDiscovery) DelServiceList(key string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.serverList, key)
log.Println("del key:", key)
}
//GetServices 获取服务地址
func (s *ServiceDiscovery) GetServices() []string {
s.lock.Lock()
defer s.lock.Unlock()
addrs := make([]string, 0)
for _, v := range s.serverList {
addrs = append(addrs, v)
}
return addrs
}
//Close 关闭服务
func (s *ServiceDiscovery) Close() error {
return s.cli.Close()
}
func main() {
var endpoints = []string{"localhost:2379"}
ser := NewServiceDiscovery(endpoints)
defer ser.Close()
ser.WatchService("/web/")
ser.WatchService("/gRPC/")
for {
select {
case <-time.Tick(10 * time.Second):
log.Println(ser.GetServices())
}
}
}
运行:
#运行服务发现
$go run discovery.go
watching prefix:/web/ now...
put key : /web/node1 val:localhost:8000
[localhost:8000]
#另一个终端运行服务注册
$go run register.go
Put key:/web/node1 val:localhost:8000 success!
续约成功 cluster_id:14841639068965178418 member_id:10276657743932975437 revision:29 raft_term:7
续约成功 cluster_id:14841639068965178418 member_id:10276657743932975437 revision:29 raft_term:7
...
总结
基于 Raft 算法的 etcd 天生是一个强一致性高可用的服务存储目录,用户可以在 etcd 中注册服务,并且对注册的服务设置key TTL,定时保持服务的心跳以达到监控健康状态的效果。通过在 etcd 指定的主题下注册的服务也能在对应的主题下查找到。
为了确保连接,我们可以在每个服务机器上都部署一个 Proxy 模式的 etcd,这样就可以确保能访问 etcd 集群的服务都能互相连接。
参考:
- https://segmentfault.com/a/1190000020944777
- https://blog.csdn.net/blogsun/article/details/102861648
- https://www.infoq.cn/article/etcd-interpretation-application-scenario-implement-principle/
etcd实现服务发现的更多相关文章
- etcd学习(3)-grpc使用etcd做服务发现
grpc通过etcd实现服务发现 前言 服务注册 服务发现 负载均衡 集中式LB(Proxy Model) 进程内LB(Balancing-aware Client) 独立 LB 进程(Externa ...
- 利用etcd实现服务注册和服务发现
文章目录 服务注册 服务发现 协议编写 服务端实现 客户端实现 实验结果 参考文章 服务注册 主要逻辑在go func函数里面,先是去etcd获取一下服务,没有获取到的话就注册进去. package ...
- 服务发现框架选型: Consul、Zookeeper还是etcd ?
背景 本文并不介绍服务发现的基本原理.除了一致性算法之外,其他并没有太多高深的算法,网上的资料很容易让大家明白上面是服务发现.想直接查看结论的同学,请直接跳到文末.目前,市面上有非常多的服务发现工具, ...
- 服务发现框架选型,Consul还是Zookeeper还是etcd
背景 本文并不介绍服务发现的基本原理.除了一致性算法之外,其他并没有太多高深的算法,网上的资料很容易让大家明白上面是服务发现. 想直接查看结论的同学,请直接跳到文末. 目前,市面上有非常多的服务发现工 ...
- nginx动态配置及服务发现那些事
Reference: http://xiaorui.cc/2016/10/16/nginx%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE%E5%8F%8A%E6%9C%8D% ...
- 使用Consul做服务发现的若干姿势
从2016年起就开始接触Consul,使用的主要目的就是做服务发现,后来逐步应用于生产环境,并总结了少许使用经验.最开始使用Consul的人不多,为了方便交流创建了一个QQ群,这两年微服务越来越火,使 ...
- Consul做服务发现
使用Consul做服务发现的若干姿势 https://www.cnblogs.com/bossma/p/9756809.html 从2016年起就开始接触Consul,使用的主要目的就是做服务发现,后 ...
- etcd:用于服务发现的键值存储系统
etcd是一个高可用的键值存储系统,主要用于共享配置和服务发现.etcd是由CoreOS开发并维护的,灵感来自于 ZooKeeper 和 Doozer,它使用Go语言编写,并通过Raft一致性算法处理 ...
- 服务发现之 Etcd VS Consul
抄自这里 *********************************************************************************************** ...
随机推荐
- ChaosBlade--动态脚本实现 Java 实验场景
动态脚本实现 : 参考文档:https://github.com/chaosblade-io/chaosblade/wiki/%E5%8A%A8%E6%80%81%E8%84%9A%E6%9C%AC% ...
- vue2.x学习笔记(十三)
接着前面的内容:https://www.cnblogs.com/yanggb/p/12595860.html. 组件的注册 注册组件有一些规范约定与注意事项. 组件名的命名规范 在注册一个组件的时候, ...
- 11. this.setState更新问题
this.setState是异步的,所以在this.setState之后不能立刻得到最新的state数据关于如何获取最新的数据,有如下三种方法 1.回调函数 this.setState({ xxx:' ...
- mysql参数max_binlog_cache_size设置不当引发的血案
日常运维中的坑真是防不胜防,不一小心就遇到别人给你挖的坑.最近又遇到经验不足的DBA不知道从哪拷贝的配置文件(据说是当时参加某培训机构视频培训是资料里的模板,真的是误人子弟呀),其中把max_binl ...
- OAuth-授权机制
一.应用场景 有一个"云冲印"的网站,可以将用户储存在Google的照片,冲印出来.用户为了使用该服务,必须让"云冲印"读取自己储存在Google上的照片. 问 ...
- 干货最实用的 Python 多线程代码框架
前言 很多地方都要用到多线程,这是我经常用的多线程代码,放在博客园记录下. 代码 from multiprocessing.pool import ThreadPool thread = 10 ite ...
- JVM相关参数设置
Java启动参数共分为三类: 其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容: 其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足, ...
- weblogic补丁升级详细步骤,18.7.17补丁更新
weblogic打补丁 到weblogic官网下载补丁包 对应的补丁包 如: p22248372_1036012_Generic.zip 一 安装补丁步骤 1.登录linux的weblogic用户 ...
- 2019-2020-1 20199328《Linux内核原理与分析》第二周作业
冯诺依曼体系结构的核心是: 冯诺依曼体系结构五大部分:控制器,运算器,存储器,输入输出设备. 常用的寄存器 AX.BX.CX.DX一般存放一些一般的数据,被称为通用寄存器,分别拥有高8位和低8位. 段 ...
- 在IBM Cloud中运行Fabric
文章目录 打包智能合约 创建IBM Cloud services 创建fabric网络 创建org和相应的节点 创建order org和相应节点 创建和加入channel 导入智能合约 上篇文章我们讲 ...