因为做者才刚开始学习NodeJs,水平实在有限,本文更像是一篇学习笔记,适合同刚开始学习NodeJs的朋友阅读。javascript
若是你的团队正在探索微服务的搭建,那么大家可能就在寻找一种机制,这个机制让每一个服务能动态的建立地址,同时调用方要能感知到这些服务地址的动态变化。服务注册与服务发现就是这其中一种机制,大概的流程为:前端
其中:java
ZooKeeper的身份是管理者,它是一个分布式数据一致性的解决方案,分布式任务能够基于它实现数据的发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、领导选举、分布式锁、分布式队列等。本文并不会对全部的方面都展开讲解,由于做者也还没涉及到。咱们的目标是利用ZooKeeper来实现一个服务的注册中心,若是你感兴趣,能够本身去研究看看,后面我研究了会再来分享的。node
zk内部有一个树状的内存模型,相似于文件系统,有若干目录,每一个目录又能够有若干文件夹、文件,以下图:api
zk有4种节点(会话指客户端链接zk的长链接):promise
只有持久节点才能有子节点。缓存
如今通常是集群部署应用,因此咱们来看下集群部署下的服务地址情况。例如,当你拥有应用A,应用A部署在2台机器上,机器IP分别为:127.0.0.1和127.0.0.2,应用服务端口6666,应用A就有这么两个服务地址:127.0.0.1:666六、127.0.0.2:6666数据结构
咱们指定一个节点来做为全部服务地址的根节点(相似命名空间),因此该节点应该为一个持久节点。咱们有n个应用,每一个应用下有n台机器,因此应用节点也拥有子节点,也应该是持久节点。每台机器在启动应用服务的时候要向zk注册一个地址,在服务下线的时候要删除zk中的地址,因此使用临时节点特色正好符合这个行为,同时可使用顺序节点自动帮咱们管理节点名称。app
由于咱们都是使用node操做,因此使用zk的node客户端node-zookeeper-client。负载均衡
原本我是用eggjs插件写的,这里将框架的东西剔除,其余提取出来,这样就不和框架挂钩了。
const { createClient, ACL, CreateMode } = require('node-zookeeper-client');
const zkClient = createClient('127.0.0.1:2181');
const promisify = require('util').promisify;
zkClient.connect();
zkClient.once('connected', () => {
registerService();
});
// 让zkClient支持promise
const proto = Object.getPrototypeOf(zkClient);
Object.keys(proto).forEach(fnName => {
const fn = proto[fnName];
if (proto.hasOwnProperty(fnName) && typeof fn === 'function') {
zkClient[`${fnName}Async`] = promisify(fn).bind(zkClient);
}
});
// host和port应该和部署系统结合分配
// serviceName要求惟一
const { serviceName, host, port } = config;
async function registerService() {
try {
// 建立根节点,持久节点
const rootNode = await zkClient.existsAsync('/services');
if (rootNode == null) {
await zkClient.createAsync('/services', null, ACL.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
// 建立服务节点,持久节点
const servicePath = `/services/${serviceName}`;
const serviceNode = await zkClient.existsAsync(servicePath);
if (serviceNode == null) {
await zkClient.createAsync(servicePath, null, ACL.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
// 建立地址节点,临时顺序节点,这样name就不须要咱们去维护了,递增
const addressPath = `${servicePath}/address-`;
const serviceAddress = `${host}:${port}`;
const addressNode = await zkClient.createAsync(addressPath, Buffer.from(serviceAddress), ACL.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
} catch (error) {
throw new Error(error);
}
}
复制代码
上面的代码其实很简单,就是链接zk后先判断根节点是否是建立,若是没有就建立(第一个应用),而后判断应用节点是否建立,没有就建立(集群第一台机器),最后就是建立机器节点,这里使用临时顺序节点,省去了咱们维护惟一name的麻烦,让其递增,注意,host和port做为存储内容,这个须要app部署的时候部署系统提供(若是是使用自动部署系统的话),而后地址转为Buffer存起来。
这样其实服务注册就完成了。
上面已经在服务启动的时候都注册到zk中了,当前端调用接口访问服务的时候,咱们须要知道服务的地址,这就是服务发现过程。
API Gateway如它的字面意思来看,是API的入口,用来路由请求。其实,不仅仅是路由请求,API Gateway还能够转换协议,整合数据、认证、限速等逻辑。
例如前端有个用户获取的请求,应该这样写:
fetch('/api/user/get', {
method: 'POST',
body: { id: 1 },
headers: {
// header的方式指定service
'servive-name': 'user'
}
})
复制代码
API Gateway本质也是是个服务,使用Eggjs编写,咱们的服务发现封装成一个中间件,因此这里只展现中间件的内容,其余的本身看egg的文档。
const proxy = require('koa-proxies');
module.exports = (options, app) => {
return async (ctx, next) => {
const serviceName = ctx.request.headers['servive-name'];
if (!serviceName) {
ctx.throw(404, 'no service found.');
}
const servicePath = `/services/${serviceName}`;
const addressNodes = await app.zookeeper.getChildrenAsync(servicePath);
const size = addressNodes.length;
if (size === 0) {
ctx.throw(404, 'no service found.');
}
let addressPath = `${servicePath}/`;
if (size === 1) {
addressPath += addressNodes[0];
} else {
// 这里你能够作负载均衡
addressPath += addressNodes[parseInt(Math.random() * size)];
}
const serviceAddress = await app.zookeeper.getDataAsync(addressPath);
if (!serviceAddress) {
ctx.throw(404, 'no service found.');
}
await proxy('/', {
target: `http://${serviceAddress}/`,
})(ctx, next);
};
};
复制代码
上面的中间件中根据headers中的service-name去获取到该应用下全部的服务地址,而后根据某个策略选择一个服务,使用代理转发到对应的服务。
以上很简陋地实现了,虽然可使用,还有不少细节要处理,好比API Gateway中对于已经拿到的服务地址能够缓存起来,而后订阅zk变化;好比在选择服务的时候能够作负载均衡。