徒手教你使用zookeeper编写服务发现

zookeeper是一个强一致【不严格】的分布式数据库,由多个节点共同组成一个分布式集群,挂掉任意一个节点,数据库仍然可以正常工作,客户端无感知故障切换。客户端向任意一个节点写入数据,其它节点可以立即看到最新的数据。

zookeeper的内部是一个key/value存储引擎,key是以树状的形式构成了一个多级的层次结构,每一个节点既可以存储数据,又可以作为一个目录存放下一级子节点。

zookeeper提供了创建/修改/删除节点的api,如果父节点没有创建,字节点会创建失败。如果父节点还有子节点,父节点不可以被删除。

zookeeper和客户端之间以socket形式进行双向通讯,客户端可以主动调用服务器提供的api,服务器可以主动向客户端推送事件。有多种事件可以watch,比如节点的增删改,子节点的增删改,会话状态变更等。

zookeeper的事件有传递机制,字节点的增删改触发的事件会向上层依次传播,所有的父节点都可以收到字节点的数据变更事件,所以层次太深/子节点太多会给服务器的事件系统带来压力,节点分配要做好周密的规划。

zookeeper满足了CAP定理的分区容忍性P和强一致性C,牺牲了高性能A【可用性蕴含性能】。zookeeper的存储能力是有限的,当节点层次太深/子节点太多/节点数据太大,都会影响数据库的稳定性。所以zookeeper不是一个用来做高并发高性能的数据库,zookeeper一般只用来存储配置信息。

zookeeper的读性能随着节点数量的提升能不断增加,但是写性能会随着节点数量的增加而降低,所以节点的数量不宜太多,一般配置成3个或者5个就可以了。

图中可以看出当服务器节点增多时,复杂度会随之提升。因为每个节点和其它节点之间要进行p2p的连接。3个节点可以容忍挂掉1个节点,5个节点可以容忍挂掉2个节点。

客户端连接zookeeper时会选择任意一个节点保持长链接,后续通信都是通过这个节点进行读写的。如果该节点挂了,客户端会尝试去连接其它节点。

服务器会为每个客户端连接维持一个会话对象,会话的ID会保存在客户端。会话对象也是分布式的,意味着当一个节点挂掉了,客户端使用原有的会话ID去连接其它节点,服务器维持的会话对象还继续存在,并不需要重新创建一个新的会话。

如果客户端主动发送会话关闭消息,服务器的会话对象会立即删除。如果客户端不小心奔溃了,没有发送关闭消息,服务器的会话对象还会继续存在一段时间。这个时间是会话的过期时间,在创建会话的时候客户端会提供这个参数,一般是10到30秒。

也许你会问连接断开了,服务器是可以感知到的,为什么需要客户端主动发送关闭消息呢?

因为服务器要考虑网络抖动的情况,连接可能只是临时断开了。为了避免这种情况下反复创建和销毁复杂的会话对象以及创建会话后要进行的一系列事件初始化操作,服务器会尽量延长会话的生存时间。

zookeeper的节点可以是持久化(Persistent)的,也可以是临时(Ephermeral)的。所谓临时的节点就是会话关闭后,会话期间创建的所有临时节点会立即消失。一般用于服务发现系统,将服务进程的生命期和zookeeper子节点的生命期绑定在一起,起到了实时监控服务进程的存活的效果。

zookeeper还提供了顺序节点。类似于mysql里面的auto_increment属性。服务器会在顺序节点名称后自动增加自增的唯一后缀,保持节点名称的唯一性和顺序性。

还有一种节点叫着保护(Protected)节点。这个节点非常特殊,但是也非常常用。在应用服务发现的场合时,客户端创建了一个临时节点后,服务器节点挂了,连接断开了,然后客户端去重连到其它的节点。因为会话没有关闭,之前创建的临时节点还存在,但是这个时候客户端却无法识别去这个临时节点是不是自己创建的,因为节点内部并不存储会话ID字段。所以客户端会在节点名称上加上一个GUID前缀,这个前缀会保存在客户端,这样它就可以在重连后识别出哪个临时节点是自己之前创建的了。

接下来我们使用Go语言实现一下服务发现的注册和发现功能。

如图所示,我们要提供api.user这样的服务,这个服务有3个节点,每个节点有不一样的服务地址,这3个节点各自将自己的服务注册进zk,然后消费者进行读取zk得到api.user的服务地址,任选一个节点地址进行服务调用。为了简单化,这里就没有提供权重参数了。在一个正式的服务发现里一般都有权重参数,用于调整服务节点之间的流量分配。

go get github.com/samuel/go-zookeeper/zk

首先我们定义一个ServiceNode结构,这个结构数据会存储在节点的data中,表示服务发现的地址信息。

type ServiceNode struct {
	Name string `json:"name"` // 服务名称,这里是user
	Host string `json:"host"`
	Port int    `json:"port"`}

在定义一个服务发现的客户端结构体SdClient。

type SdClient struct {
	zkServers []string // 多个节点地址
	zkRoot    string // 服务根节点,这里是/api
	conn      *zk.Conn // zk的客户端连接}

编写构造器,创建根节点

func NewClient(zkServers []string, zkRoot string, timeout int) (*SdClient, error) {
	client := new(SdClient)
	client.zkServers = zkServers
	client.zkRoot = zkRoot
	// 连接服务器
	conn, _, err := zk.Connect(zkServers, time.Duration(timeout)*time.Second)
	if err != nil {
		return nil, err
	}
	client.conn = conn
	// 创建服务根节点
	if err := client.ensureRoot(); err != nil {
		client.Close()
		return nil, err
	}
	return client, nil}// 关闭连接,释放临时节点func (s *SdClient) Close() {
	s.conn.Close()}func (s *SdClient) ensureRoot() error {
	exists, _, err := s.conn.Exists(s.zkRoot)
	if err != nil {
		return err
	}
	if !exists {
		_, err := s.conn.Create(s.zkRoot, []byte(""), 0, zk.WorldACL(zk.PermAll))
		if err != nil && err != zk.ErrNodeExists {
			return err
		}
	}
	return nil}

值得注意的是代码中的Create调用可能会返回节点已存在错误,这是正常现象,因为会存在多进程同时创建节点的可能。如果创建根节点出错,还需要及时关闭连接。我们不关心节点的权限控制,所以使用zk.WorldACL(zk.PermAll)表示该节点没有权限限制。Create参数中的flag=0表示这是一个持久化的普通节点。

接下来我们编写服务注册方法

func (s *SdClient) Register(node *ServiceNode) error {
	if err := s.ensureName(node.Name); err != nil {
		return err
	}
	path := s.zkRoot + "/" + node.Name + "/n"
	data, err := json.Marshal(node)
	if err != nil {
		return err
	}
	_, err = s.conn.CreateProtectedEphemeralSequential(path, data, zk.WorldACL(zk.PermAll))
	if err != nil {
		return err
	}
	return nil}func (s *SdClient) ensureName(name string) error {
	path := s.zkRoot + "/" + name
	exists, _, err := s.conn.Exists(path)
	if err != nil {
		return err
	}
	if !exists {
		_, err := s.conn.Create(path, []byte(""), 0, zk.WorldACL(zk.PermAll))
		if err != nil && err != zk.ErrNodeExists {
			return err
		}
	}
	return nil}

先要创建/api/user节点作为服务列表的父节点。然后创建一个保护顺序临时(ProtectedEphemeralSequential)子节点,同时将地址信息存储在节点中。什么叫保护顺序临时节点,首先它是一个临时节点,会话关闭后节点自动消失。其它它是个顺序节点,zookeeper自动在名称后面增加自增后缀,确保节点名称的唯一性。同时还是个保护性节点,节点前缀增加了GUID字段,确保断开重连后临时节点可以和客户端状态对接上。

接下来我们实现消费者获取服务列表方法

func (s *SdClient) GetNodes(name string) ([]*ServiceNode, error) {
	path := s.zkRoot + "/" + name
	// 获取字节点名称
	childs, _, err := s.conn.Children(path)
	if err != nil {
		if err == zk.ErrNoNode {
			return []*ServiceNode{}, nil
		}
		return nil, err
	}
	nodes := []*ServiceNode{}
	for _, child := range childs {
		fullPath := path + "/" + child
		data, _, err := s.conn.Get(fullPath)
		if err != nil {
			if err == zk.ErrNoNode {
				continue
			}
			return nil, err
		}
		node := new(ServiceNode)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}
		nodes = append(nodes, node)
	}
	return nodes, nil}

获取服务节点列表时,我们先获取字节点的名称列表,然后依次读取内容拿到服务地址。因为获取字节点名称和获取字节点内容不是一个原子操作,所以在调用Get获取内容时可能会出现节点不存在错误,这是正常现象。

将以上代码凑在一起,一个简单的服务发现包装就实现了。

最后我们看看如果使用以上代码,为了方便起见,我们将多个服务提供者和消费者写在一个main方法里。

func main() {
        // 服务器地址列表
	servers := []string{"192.168.0.101:2118", "192.168.0.102:2118", "192.168.0.103:2118"}
	client, err := NewClient(servers, "/api", 10)
	if err != nil {
		panic(err)
	}
	defer client.Close()
	node1 := &ServiceNode{"user", "127.0.0.1", 4000}
	node2 := &ServiceNode{"user", "127.0.0.1", 4001}
	node3 := &ServiceNode{"user", "127.0.0.1", 4002}
	if err := client.Register(node1); err != nil {
		panic(err)
	}
	if err := client.Register(node2); err != nil {
		panic(err)
	}
	if err := client.Register(node3); err != nil {
		panic(err)
	}
	nodes, err := client.GetNodes("user")
	if err != nil {
		panic(err)
	}
	for _, node := range nodes {
		fmt.Println(node.Host, node.Port)
	}}

值得注意的是使用时一定要在进程退出前调用Close方法,否则zookeeper的会话不会立即关闭,服务器创建的临时节点也就不会立即消失,而是要等到timeout之后服务器才会清理。

Image placeholder
ppang
未设置
  22人点赞

没有讨论,发表一下自己的看法吧

推荐文章
一次Zookeeper 扩展之殇

一、背景基于公司发展硬性需求,生产VM服务器要统一迁移到ZStack虚拟化服务器。检查自己项目使用的服务器,其中zookeeper集群中招,所以需要进行迁移。二、迁移计划为了使迁移不对业务产生影响,所

基于 Zookeeper 的分布式锁实现

1.背景最近在学习Zookeeper,在刚开始接触Zookeeper的时候,完全不知道Zookeeper有什么用。且很多资料都是将Zookeeper描述成一个“类Unix/Linux文件系统”的中间件

手摸手教你搭建简单的 Git 的代码自动发布

1.为什么我要弄这个? emmmm,因为有个自己的项目每次发布到线上,都要登录一下服务器,然后pull一下代码,执行一些项目初始化的命令(诸如:gitsubmoudleupdate,phpartisa

直男福利!手把手教你做一只口红色号识别器,秒变李佳琦

大数据文摘编辑组出品技术实现:宁静 七夕将至,送礼时节。直男送礼,首选口红。毕竟李佳琦一句”OMG买它”,女朋友披头散发抢购,钱包就空了一半。但是,口红色号千千万,选对了牌子才成功了一半。快乐橙、伤心

手把手教你写几个实用的的AST插件

背景AST是非常有用的。今天下午听了小组一个老哥做的AST分享,深以为然。为了加深印象,就写了篇总结,顺便分享给大家,希望能给朋友们一些启发。AST有用,口说无凭,且看几个具体的案例。且不说: Vue

Python爬虫:手把手教你采集登陆后才能看到数据

课程推荐:Python开发工程师--学习猿地--送9个上线商业项目 爬虫在采集网站的过程中,部分数据价值较高的网站,会限制访客的访问行为。这种时候建议通过登录的方式,获取目标网站的cookie,然后再

DeepFakes进化版DeepNude惊现!一键“脱衣“,火到宕机

大数据文摘出品作者:蒋宝尚、赵伟人工智能的黑暗面能有多黑?这边DeepFake带来的余震还没有被平息,本周,又一AI偏门应用爆出,一键直接“脱掉”女性的衣服!海外媒体Motherboard测试图片显然

老焦专栏 | 用 RACI 模式梳理业务流程,提高业务发布的效率

转载本文需注明出处:微信公众号EAWorld,违者必究。最近经常在不同场合说,技术发展已经进入深水区。IT技术发展已经越来越成熟了,尤其在金融行业,以前是解决从无到有的问题,现在该有的系统都有了,是解

SpringBoot 集成 JWT 实现 token 验证,token 注销

什么是JWT Jsonwebtoken(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形

理解 cookie、session、token、jwt

发展史 1、很久以前,Web基本上就是文档的浏览而已,既然是浏览,作为服务器,不需要记录谁在某一段时间里都浏览了什么文档,每次请求都是一个新的HTTP协议,就是请求加响应,尤其是我不用记住是谁刚刚发了

vue引入swiper vue使用swiper vue脚手架使用swiper /引入js文件/引入css文件

vue引入swipervue使用swipervue脚手架使用swiper/引入js文件/引入css文件欢迎加入前端交流群来获取视频资料以及前端学习资料:749539640转载文章请注明出处! 如果只是

beego 使用 coding 的 webhook 2.0 进行自动部署

beego使用coding的webhook2.0进行自动部署本文介绍beego在coding上如果使用webhook2.0进行自动部署。coding的webhook1.0教程coding平台端的设置这

一步步教你如何在 Django REST API 中构建使用 JWT 验证

基于令牌的身份验证,允许后端服务与前端(无论是web端,原生移动端或其他端)分离,并驻留在不同域中。JSONWebTokens(JWT)是一种流行的令牌认证实现,在本文中,我们使用它来验证,通过Dj

ZooTeam 前端周刊|第 63 期

政采云前端小报第63期浏览更多往期小报,请访问:https://weekly.zoo.team 基于浏览器的实时构建探索之路本文是RichLab花呗借呗前端团队的玄寂童鞋在D2上议题的文字稿。微浏览

002.07 MineSweeper - PsSimleGUI 的应用

建檔日期:2019/12/07 更新日期:None 语言:Python3.7.2,PySimpleGUI4.6.0 系统:Win10Ver.10.0.17763主题:002.07MineSweeper

还在用useState来定义数据吗?教你个更好的方案:useImmer!

以前编写state的方式Hooks上市之前我们是这么定义state的:state={ people:[ { name:'马云', englishName:'JackMa' }, { name:'马化腾

PHP跌出前十,铁打的 Python 连续3年第一:IEEE Spectrum 2019编程语言排行榜出炉

Python势头不减,依旧第一,而且进一步拉开了与其他语言的差距。这一结果,来自IEEESpectrum2019年度编程语言排行榜。这已经是Python连续3年保持第一。在Python之下,第二交椅的

昨天keep突然裁员,我连夜卷铺盖走人

“前辈,我被裁了……”“哪家公司啊?”“keep,就是那个搞社交健身的app”“真的假的?昨天可是程序员节啊!!!”“真的,我已经卷铺盖走人了…”“真会挑日子,给补偿了吗?”“给了,N+1。”刚从朋友

教你阅读 Python 开源项目代码

为什么要阅读开源代码 阅读Python开源项目代码主要有如下三个原因: 在工作过程中遇到一些问题Google和StackOverFlow等网站找不到解决办法,只能去翻源码。 对某些项目或者方向非常感

甜过初恋!浙大博士用200个西瓜130页论文,教你用机器学习科学挑瓜

大数据文摘出品作者:易琬玉刚刚送走了最热七月,转眼就迎来了最热八月。2019年是人类有气象纪录以来最热的几个年份之一,虽然这个夏天还没结束,但气象学家们已经有十足把握做出这个判断。为了应付热,人们想出

10后小学生都能教你学编程了!低龄编程的下限在哪?

大数据文摘出品作者:宁静最近,文摘菌经常收到读者留言,说b站上有一个10后小学生在教编程。小学生???教编程???话说文摘菌小学时候还只知道玩儿贪吃蛇……在感叹长江后浪推前浪的同时,文摘菌也赶紧去这位

「解放双手」老舅教你VS Code Disco

观感度:🌟🌟🌟🌟🌟口味:驴肉蒸饺烹饪时间:15min这是最好的时代,也是最坏的时代。今年听到过最浪漫的一句话:我们在键盘上留下的余温,也将随时代传递到更远的将来。感觉让理性的技术人多了份柔光滤镜。也许

忘记MySQL密码怎么办?一招教你搞定!

课程推荐:PHP开发工程师--学习猿地精品课程 在安装完MySQL或者是在使用MySQL时,最尴尬的就是忘记密码了,墨菲定律也告诉我们,如果一件事有可能出错,那么它一定会出错。那如果我们不小心忘记了M

PHP 编写基本的 Socket 程序

告诫年轻人 空想是没有用的,个人的能力来源于每一天的努力,而不是一步登天,不要畏惧任何新的知识,水滴石穿,总有一天会柳暗花明。 我的目的 因为在以后的学习中,我可能会用到网络方面的内容,但同时很多写P

Hyperf 权限管理组件 hyperf-permission 发布

本人正在申请版主,还望各位多评论,收藏,点赞GITHUB:https://github.com/donjan-deng/hyperf-perm...欢迎star,欢迎pr.Hyperf权限管理组件sp