基于上一篇的 “基于Docker、Registrator、Zookeeper实现的做事自动注册”,完成了 “做事注册与创造” 的上半部分(即上图中的1)。本文就来讲讲图中的2、3、4、5 分别是如何实现的。
功能点做事订阅1.动态获取做事列表
2.获取做事节点信息(IP、Port)

1.缓存做事路由表
做事调用1.做事要求的负载均衡策略
2.反向代理
变更关照1.监听做事节点变革
2.更新做事路由表
技能方案做事创造办法关于做事创造的办法,紧张分为两种办法:客户端创造与做事端创造。它们的紧张差异为:前者是由调用者本身去调用做事,后者是将调用者要求统一指向类似做事网关的做事,由做事网关代为调用。
这里采取做事端创造机制,即做事网关(把稳:做事网关的浸染不仅仅是做事创造)。
与客户端创造比较,可见的上风有:
做事调用的统一管理;减少客户端与注册中央不必要的连接数;将后端做事与调用者相隔离,降落做事对外暴露的风险;所选技能本文采取 NodeJs 作为做事网关的实现技能。当然,这不是唯一的技能手段,像nginx+lua,php等都能实现类似的功能。我这里采取 NodeJs 紧张出于以下几个缘故原由:
NodeJs 采取的是事宜驱动、非壅塞 I/O 模型,具有天生的异步性。在处理做事网关这种以IO密集型为主的业务时,正是 NodeJs 所善于的。NodeJs 基于Chrome V8 引擎的 JavaScript 措辞的运行环境,对付有一定 JavaScript 根本的同学,上手相对大略。所有技能都有其利害所在,NodeJs 在这里的利用也存在一定的问题(本文末了会讲述它的高可用策略):
NodeJs 是基于单进程单线程的办法,这种办法存在一定的不可靠性。一旦进程崩溃,对应的做事将变得不可用;单进程单线程办法,也导致了只能利用单核CPU。为了充分利用打算机资源,还需进行做事的水平扩展;代码示例代码地址:https://github.com/jasonGeng88/service_registry_discovery
代码目录本文紧张先容做事创造干系实现,其他部分已在上篇中先容过,感兴趣的同学可查看上篇。
目录构造(discovery项目)依赖配置(package.json){
"name": "service-discovery",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"debug": "~2.6.3",
"express": "~4.15.2",
"http-proxy": "^1.16.2",
"loadbalance": "^0.2.7",
"node-zookeeper-client": "^0.2.2"
}
}
debug:用于开拓调试;express:作为 NodeJs 的Web运用框架,这里紧张用到了它的相应HTTP要求以及路由规则功能;http-proxy:用作反向代理;loadbalance:负载均衡策略,目前供应随机、轮询、权重;node-zookeeper-client:ZK 客户端,用作获取注册中央做事信息与节点监听;常量设置(constants.js)"use strict";
function define(name, value) {
Object.defineProperty(exports, name, {
value: value,
enumerable: true
});
}
define('ZK_HOSTS', '${PRIVATE_IP}:2181,${PRIVATE_IP}:2182,${PRIVATE_IP}:2183');
define('SERVICE_ROOT_PATH', '/services');
define('ROUTE_KEY', 'services');
define('SERVICE_NAME', 'service_name');
define('API_NAME', 'api_name');
功能点详细实现下面会对上面供应的功能点依次进行实现(展示代码中只保留核心代码,详细请见代码)
做事订阅 - 动态获取做事列表var zookeeper = require('node-zookeeper-client');
var constants = require('../constants');
var debug = require('debug')('dev:discovery');
var zkClient = zookeeper.createClient(constants.ZK_HOSTS);
/
连接ZK
/
function connect() {
zkClient.connect();
zkClient.once('connected', function() {
console.log('Connected to ZooKeeper.');
getServices(constants.SERVICE_ROOT_PATH);
});
}
/
获取做事列表
/
function getServices(path) {
zkClient.getChildren(
path,
null,
function(error, children, stat) {
if (error) {
console.log(
'Failed to list children of %s due to: %s.',
path,
error
);
return;
}
// 遍历做事列表,获取做事节点信息
children.forEach(function(item) {
getService(path + '/' + item);
})
}
);
}
做事订阅 - 获取做事节点信息(IP、Port)/
获取做事节点信息(IP,Port)
/
function getService(path) {
zkClient.getChildren(
path,
null,
function(error, children, stat) {
if (error) {
console.log(
'Failed to list children of %s due to: %s.',
path,
error
);
return;
}
// 打印节点信息
debug('path: ' + path + ', children is ' + children);
}
);
}
本地缓存 - 缓存做事路由表// 初始化缓存
var cache = require('./local-storage');
cache.setItem(constants.ROUTE_KEY, {});
/
获取做事节点信息(IP,Port)
/
function getService(path) {
...
// 打印节点信息
debug('path: ' + path + ', children is ' + children);
if (children.length > 0) {
//设置本地路由缓存
cache.getItem(constants.ROUTE_KEY)[path] = children;
}
...
}
做事调用 - 负载均衡策略/
获取做事节点信息(IP,Port)
/
function getService(path) {
...
if (children.length > 0) {
//设置负载策略(轮询)
cache.getItem(constants.ROUTE_KEY)[path] = loadbalance.roundRobin(children);
}
...
}
要求的负载均衡,实质是对路由表中要求地址进行记录与分发。记录:上一次要求的地址;分发:按照策略选择接下来要求的地址。这里为了简便起见,将负载与缓存并在一起。
做事调用 - 反向代理var proxy = require('http-proxy').createProxyServer({});
var cache = require('../middlewares/local-storage');
var constants = require("../constants");
var debug = require('debug')('dev:reserveProxy');
/
根据headers的 service_name 与 api_name 进行代理要求
/
function reverseProxy(req, res, next) {
var serviceName = req.headers[constants.SERVICE_NAME];
var apiName = req.headers[constants.API_NAME];
var serviceNode = constants.SERVICE_ROOT_PATH + '/' + serviceName;
debug(cache.getItem(constants.ROUTE_KEY)[serviceNode]);
var host = cache.getItem(constants.ROUTE_KEY)[serviceNode].pick();
var url = 'http://' + host + apiName;
debug('The proxy url is ' + url);
proxy.web(req, res, {
target: url
});
}
变更关照 - 监听做事节点 && 更新路由缓存/
获取做事列表
/
function getServices(path) {
zkClient.getChildren(
path,
// 监听列表变革
function(event) {
console.log('Got Services watcher event: %s', event);
getServices(constants.SERVICE_ROOT_PATH);
},
function(error, children, stat) {
...
}
);
}
/
获取做事节点信息(IP,Port)
/
function getService(path) {
zkClient.getChildren(
path,
// 监听做事节点变革
function(event) {
console.log('Got Serivce watcher event: %s', event);
getService(path);
},
function(error, children, stat) {
...
}
);
}
主文件(src/app.js)var express = require('express');
var reverseProxy = require('./routes/reverse-proxy');
var discovery = require('./middlewares/discovery');
var app = express();
// service discovery start
discovery();
// define the home page route
app.get('/', function(req, res) {
res.send('This is a Service Gateway Demo')
});
// define proxy route
app.use('/services', reverseProxy);
启动脚本(src/bin/www)脚本中含有单进程与多进程两种启动办法,由于 NodeJs 单进程的不可靠性,一样平常生产环境中采取多进程办法启动,担保它的稳定性。
#!/usr/bin/env node
var app = require('../app');
var http = require('http');
var port = 8080;
//单进程运行
//http.createServer(app).listen(port);
//多进程运行
var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log("master start...");
// Fork workers.
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('listening',function(worker,address){
console.log('listening: worker ' + worker.process.pid +', Address: '+address.address+":"+address.port);
});
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
cluster.fork();
});
} else if (cluster.isWorker) {
http.createServer(app).listen(port);
}
镜像构建为演示方便,采取单进程办法
# Dockerfile
FROM node:6.10.2
MAINTAINER jasongeng88@gmail.com
ENV TZ="Asia/Shanghai" HOME="/usr/src/app"
WORKDIR ${HOME}
COPY src/ ${HOME}/
RUN npm install
EXPOSE 8080
ENTRYPOINT ["npm", "run", "start"]
# 构建命令
docker build -t node_discovery .
场景演示改动坑:由于 docker --net=host 在 Mac 上存在问题,以是对 Registrator 做出调度。原来向注册中央注册的是 127.0.0.1 改为 内网IP,担保容器内可以访问。
Linux 环境下不须要此改动,做事网关网络模式应为host。
准备事情为了方便演示,对原来的做事模块进行调度,供应如下做事:
做事模块启动如下(service_1:2个,service_2:1个)
启动做事网关
cd discovery && docker-compose up -d
输出效果(docker logs -f discovery_discovery_1 看出日志输出):
场景1:GET办法,要求做事2,API路径为 / ,无要求参数
场景2:GET办法,要求做事1,API路径为 /user ,要求参数为id=1场景3:GET办法,多次要求做事1,API路径为 / ,查看负载均衡情形场景4:启停做事2实例,不雅观察路由表变革// 停用 serivce_2
cd services && docker-compose stop service_2
查看网关监听变革:
高可用NodeJs 自身通过 cluster 模块,进行多进程启动,防止单进程崩溃的不稳定性;通过 Docker 容器化启动,在启动时设置restart策略,一旦做事崩溃将立即重启;上述的利用场景都在单机上运行,在分布式情形下,可以将 NodeJs 容器多主机支配,采取 nginx + NodeJs 的架构进行水平扩展;总结
本文以上篇 “做事自动化注册” 遗留的功能点开头,讲述了做事创造的2种实现办法,以及其利害。并以 NodeJs 作为做事网关的实现手段,详细先容了个中各功能点的实现细节。末了通过场景代入的办法,展示了厥后果。
对付网关的高可用,也通过了2种办法进行了担保。自身高可用通过多进程、失落败重启策略进行担保;分布式下则以 nginx + NodeJs 的架构进行了担保。
文中也提到,做事创造实则只是做事网关的一个部分,做事网关还包括做事鉴权、访问掌握等。这里的代码仅是个Demo示例,目的是让大家更好的看清它的实质,希望对大家有所帮助~
江苏立维成立于2015年,核心团队来自焦点、华为、复兴,专注于企业信息化领域的安全、运维产品的开拓和做事,为企业供应包含业务迁移上云、业务稳定性保障、安全运维是海内首批专注于企业业务安全稳定运行做事保障的公司。