将单体服务拆分为微服务后,为了服务高可用,一般会做集群多实例。但在分布式下,怎么进行高效、便捷的进行服务访问问题,出现了各类服务注册和服务发现框架。这里使用的是Zookeeper。ZooKeeper 官网 https://zookeeper.apache.org。

我们的业务系统使用的开发语言是JAVA,但是部分页面请求是先到nodejs 做的webportal服务,进行权限校验,校验通过后调用Java提供的API。当前阶段Java端已经微服务化,使用Zookeeper作为注册中心,目前只需要让nodejs端,也接入到Zookeeper,作为服务消费者,就能搭建机器环境。

找轮子

通过查找,发现npm有现成的库 node-zookeeper-client ,避免重复造轮子,就用它了。

接入思路

由于我们只是作为服务消费者,不需要使用服务注册的api,大部分可以直接在文档中找到API。

编码过程

npm 安装

1 npm i node-zookeeper-client

连接ZK

 1 const Zookeeper = require('node-zookeeper-client');
2 const CONNECTION_STRING = "127.0.0.1:2181"; // ZK的服务地址
3 const OPTIONS = {
4 sessionTimeout: 5000
5 }
6 const zk = Zookeeper.createClient(CONNECTION_STRING, OPTIONS);
7 zk.on('connected', function(){
8 console.log("zk=====", zk);
9 });
10 //获取根节点下的子节点数据
11 zk.getChildren('/', function(error, children, stat){
12 if(error){
13 console.log(error.stack);
14 return;
15 }
16 console.log(children);
17 })
18 zk.connect();

其他API(仅供参考)

 1 // 判断节点是否已存在
2 zk.exists('/phpnode',function(error,stat){
3 if(stat){
4 console.log("节点存在");
5 }else{
6 console.log("节点不存在");
7 }
8 })
9
10 // 创建/注册节点
11 zk.create('/phpnode',new Buffer('hello'),function(error,path){
12 console.log(path);
13 })
14
15 // 获取节点数据
16 zk.getData('/phpnode',function(error,data,stat){
17 console.log(data.toString());
18 });
19
20 //节点删除
21 zk.remove('/phpnode',function(error){
22 if(!error){
23 console.log('node 节点删除成功');
24 }
25 })

于是有了第一版本代码

 1 const zookeeper = require('node-zookeeper-client');
2
3
4 // ZK基础配置信息,正式项目需要从环境文件导入
5 export const ZK = {
6 clientAddress: 'localhost:2181/zk/test', // ZK地址
7 servicePath: '/test-service', // 服务路径
8 };
9
10 let zkClient = null;
11
12 // 获取服务ip+port
13 export const getZKServiceBaseUrl = (servicePath) => {
14 return new Promise((resolve, reject) => {
15 try {
16 // 防止重复连接
17 if (zkClient) {
18 disconnectZKService();
19 }
20
21 // 新建连接
22 zkClient = zookeeper.createClient(ZK.clientAddress);
23 // 连接后执行一次
24 zkClient.once('connected', async function () {
25 // 获取服务节点信息
26 const res = await listChildren(zkClient, servicePath);
27 res.message ? reject(res) : resolve(res);
28 });
29
30 zkClient.connect();
31 } catch (error) {
32 reject(error);
33 }
34 });
35 };
36
37 // 断开链接
38 export const disconnectZKService = () => {
39 if (zkClient) {
40 zkClient.close();
41 }
42 };
43
44 // 获取节点信息,ip+port
45 function listChildren(client, path) {
46 return new Promise((resolve, reject) => {
47 client.getChildren(path,
48 function () {},
49 function (error, children) {
50 if (error) {
51 reject({
52 ...error,
53 message: `获取ZK节点error,Path: ${path}`
54 });
55 }
56 try {
57 let addressPath = path + '/';
58 if (children.length > 1) {
59 //若存在多个地址,则随机获取一个地址
60 addressPath += children[Math.floor(Math.random() * children.length)];
61 } else {
62 //若只有唯一地址,则获取该地址
63 addressPath += children[0];
64 }
65 //获取服务地址
66 client.getData(addressPath, function (err, data) {
67 if (err) {
68 reject({
69 ...error,
70 message: `获取ZK服务地址error,Stack: ${err.stack}`
71 });
72 }
73 if (!data) {
74 reject({
75 ...error,
76 message: `ZK data is not exist`
77 });
78 }
79 const serviceInfo = JSON.parse(data);
80
81 const url = serviceInfo.address + ':' + serviceInfo.port;
82 resolve(url);
83 });
84 } catch (error) {
85 reject({
86 ...error,
87 message: `list ZK children error`
88 });
89 }
90 }
91 );
92 });
93 }

通过测试代码,可以实现调用Java服务。可能一般的程序员实现功能了就好了,可是作为一个有点追求的,感觉代码哪里有问题。具体是哪里呢,盯着屏幕瞅了两分钟,发现每次获取服务都取 ZK 注册中心获取,这个过程涉及到的网络请求而且还不是一次HTTP,如果只是这么简单的改造,程序单纯在性能响应上很有可能还不如老版本。我们可以在获取服务的真实远程地址前,添加一个本地缓存。通过ZK订阅机制,更新本地缓存数据。

思路虽然明确了,可以api扫了扫,没有我们想要的监听器,如下所示

这怎么办,按理说的应该会有一个,节点数据改变推送的监听器,例如新增,删除,修改等等。找了半天也没找到合适的。

没办法,接着看源码吧,看了一会,忽然,看到一个似乎可用的,类

这不就是我需要的类吗,但是居然在一方法中注入监听器,先试试吧。

试了一下,嘿,真的可以了,当服务端节点数据发生变动后,会自动触发监听器 watcher 的回调逻辑。这就好办了,改造开始。

改进后的代码

const zookeeper = require('node-zookeeper-client');
var ZK = require('../config/env.js').zk; const client = Object.freeze({
zkClient: zookeeper.createClient(ZK.connectionString),
serviceSet: [], //
serviceCache: Object.freeze({
map: new Map(), /**
* 更新缓存
* @param {String} path 服务路径
* @param {Array<String>} arr 真实访问集合
*/
updateCache: function (path, arr) {
this.map.set(path, arr);
}, /**
* 从缓存中获取访问地址
*
* @param {String} path 服务路径
* @returns String 真实访问地址
*/
getRealPath: function (path) {
let arr = this.map.get(path); if (arr.length > 1) //若存在多个地址,则随机获取一个地址
return arr[Math.floor(Math.random() * arr.length)];
else //若只有唯一地址,则获取该地址
return arr[0];
}
}), connect: function () {
console.info("连接 zookeeper"); this.zkClient.once('connected', function () {
console.info("连接成功");
}); this.zkClient.connect();
}, getRealPath: function (serviceName) {
return new Promise(async (resolve, reject) => {
if (this.serviceSet.includes(serviceName)) {
resolve(this.serviceCache.getRealPath(serviceName));
} else {
// 加载服务节点信息
this.loadChildren(serviceName).then(url => resolve(url)).catch(error => reject(error));
}
});
}, loadChildren: function (path) {
console.info("进入 loadChildren ");
return new Promise((resolve, reject) => {
this.zkClient.getChildren(path, (event) => {
console.info(" loadChildren watcher ", path, event); this.getChildren(event.path);
}, (error, ids) => {
console.info(" loadChildren callback ", path, error, ids);
if (error) {
reject({
...error,
message: `获取ZK节点error,Path: ${path}`
});
} else {
resolve(this.getData(path, ids));
}
});
});
}, getChildren: function (path) {
console.info("进入 getChildren ");
return new Promise((resolve, reject) => {
this.zkClient.getChildren(path, (error, ids) => {
console.info(" getChildren callback ", path, error, ids);
if (error) {
reject({
...error,
message: `获取ZK节点error,Path: ${path}`
});
} resolve(this.getData(path, ids));
});
});
}, getData: function (path, ids) {
console.info("进入 getData "); let pros = ids.map(id => new Promise((resolve, reject) => {
//获取服务地址
this.zkClient.getData(path + "/" + id, (error, data) => {
console.info(" getData callback ", path, id);
if (error) {
reject({
...error,
message: `获取ZK服务地址error,Stack: ${err.stack}`
});
}
if (!data) {
reject({
...error,
message: `ZK data is not exist`
});
} const node = JSON.parse(data).payload;
const protocol = node.ssl ? "https://" : "http://"; resolve(`${protocol}${node.host}:${node.port}`);
});
})); return Promise.all(pros).then(arr => this.serviceCache.updateCache(path, arr)).then(() => this.serviceCache.getRealPath(path));
}, disconnect: function () { //断开连接
console.info("进入 disconnect ")
if (this.zkClient) {
console.info("执行 close")
this.zkClient.close();
}
},
}); client.connect(); module.exports = {
getServiceUrl: (path) => client.getRealPath(path),
disconnect: () => client.disconnect(),
}

这样终于,好一点了。

未完待续(多节点的选择问题)

多节点选择策略:随机,轮转,粘性 等等,一般不同的项目使用的策略也不太一样,实例中使用的是简单随机策略,后续再进行节点选择的策略问题优化啦。

关机,收工!!!

Nodejs 使用 ZooKeeper 做服务发现的更多相关文章

  1. 阿里巴巴为什么不用 ZooKeeper 做服务发现?

    阿里巴巴为什么不用 ZooKeeper 做服务发现? http://jm.taobao.org/2018/06/13/%E5%81%9A%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8 ...

  2. 为什么不应该使用Zookeeper做服务发现?(转载)

    转载自: http://dockone.io/article/78 [编者的话]本文作者通过ZooKeeper与Eureka作为Service发现服务(注:WebServices体系中的UDDI就是个 ...

  3. 为什么不应该使用ZooKeeper做服务发现

    [编者的话]本文作者通过ZooKeeper与Eureka作为Service发现服务(注:WebServices体系中的UDDI就是个发现服务)的优劣对比,分享了Knewton在云计算平台部署服务的经验 ...

  4. 使用Consul做服务发现的若干姿势

    从2016年起就开始接触Consul,使用的主要目的就是做服务发现,后来逐步应用于生产环境,并总结了少许使用经验.最开始使用Consul的人不多,为了方便交流创建了一个QQ群,这两年微服务越来越火,使 ...

  5. 【转帖】为什么不要把ZooKeeper用于服务发现

    http://www.infoq.com/cn/news/2014/12/zookeeper-service-finding ZooKeeper是Apache基金会下的一个开源的.高可用的分布式应用协 ...

  6. Consul做服务发现

    使用Consul做服务发现的若干姿势 https://www.cnblogs.com/bossma/p/9756809.html 从2016年起就开始接触Consul,使用的主要目的就是做服务发现,后 ...

  7. Api网关Kong集成Consul做服务发现及在Asp.Net Core中的使用

    写在前面   Api网关我们之前是用 .netcore写的 Ocelot的,使用后并没有完全达到我们的预期,花了些时间了解后觉得kong可能是个更合适的选择. 简单说下kong对比ocelot打动我的 ...

  8. Go | Go 使用 consul 做服务发现

    Go 使用 consul 做服务发现 目录 Go 使用 consul 做服务发现 前言 一.目标 二.使用步骤 1. 安装 consul 2. 服务注册 定义接口 具体实现 测试用例 3. 服务发现 ...

  9. etcd学习(3)-grpc使用etcd做服务发现

    grpc通过etcd实现服务发现 前言 服务注册 服务发现 负载均衡 集中式LB(Proxy Model) 进程内LB(Balancing-aware Client) 独立 LB 进程(Externa ...

  10. go-micro使用Consul做服务发现的方法和原理

    go-micro v4默认使用mdns做服务发现.不过也支持采用其它的服务发现中间件,因为多年来一直使用Consul做服务发现,为了方便和其它服务集成,所以还是选择了Consul.这篇文章将介绍go- ...

随机推荐

  1. 森林野火故事2.0:一眼看穿!使用 Panel 和 hvPlot 可视化 ⛵

    作者:韩信子@ShowMeAI 数据分析实战系列:https://www.showmeai.tech/tutorials/40 本文地址:https://www.showmeai.tech/artic ...

  2. Pinely Round 1 (Div. 1 + Div. 2)

    比赛链接 A 题意 构造两个长为 \(n\) 排列,使得两排列有长为 \(a\) 公共前缀和长为 \(b\) 的公共后缀. 题解 知识点:构造. 注意到,当 \(a+b\leq n-2\) 时,中间段 ...

  3. 关于sublime text 3写一个插件

    前言 我之前一直想写一个记录自己笔记的软件,可以给因为我都记录在桌面的便签上很乱,以至于便签上满满的全是字母,很难看,但是我有不想写图形化界面,所以最终我选择了写一个sublime 插件,功能就是记录 ...

  4. Dojo dijit/Tree的使用以及样式设置

    如果什么都不设置,默认使用dojo自带的Tree,样式模板使用claro的,效果是这样的. 1.无论是不是叶子节点,前面总要带个+号,必须要点击下才消失. 2.点击树或者某个节点是,回出现蓝色边框. ...

  5. day22 存储过程 & 游标 & 事务

    存储过程 是一种在数据库中存储复杂程序(复杂sql语句),以便外部程序调用的一种数据库对象 存储过程是为了完成特定功能的sql语句集,经编译创建并保存在数据库中,用户可以通过指定存储过程的名字并给定参 ...

  6. JavaEE Day06 JDBC连接池&JDBCTemplate

    今日内容: 数据库连接池 简化操作--Spring JDBC提供的 JDBC Template(JDBC的封装) 一.数据库连接池 1.引入 之前:每一次都要获取连接.释放连接-- 现在:连接重复使用 ...

  7. intel Pin:动态二进制插桩的安装和使用,以及如何开发一个自己的Pintool

    先贴几个你可能用得上的链接 intel Pin的官方介绍Pin: Pin 3.21 User Guide (intel.com) intel Pin的API文档Pin: API Reference ( ...

  8. rpm和yum仓库

    一.rpm rpm从官网下,或者自研 外来的硬件设备连接到Linux上,必须挂载 rpm -qa(all) 显示当前系统中以 RPM 方式安装的所有软件列表 rpm -q 软件名 查询指定软件是否已安 ...

  9. Navicat破解教程

    一.注意: 软件适用于WIN7/8/10/11: 安装全程断网: 下载.解压和安装都应该在英文路径下进行: 解压安装前关闭所有杀毒软件,WIN10/11系统需关闭Windows Defender的实时 ...

  10. Python报SyntaxError: Missing parentheses in call to ‘print’. Did you mean print()

    SyntaxError: Missing parentheses in call to 'print'. Did you mean print()原因:python2.X版本与python3.X版本输 ...