本文使用nodejs作为微服务API网关,从而将消费端的请求,随机路由到一个可用的服务节点上。核心代码如下:
本文参考了《架构探险》轻量级服务架构
本文示例代码:node-zookeeper-demo
var express = require('express');
var zookeeper = require('node-zookeeper-client');
var httpProxy = require('http-proxy');
var cluster = require('cluster');
var os = require('os');
var cache = {};
var CPUS = os.cpus().length;
var PORT = 8084;
var CONNECTION_STRING = '127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183';
var REGISTRY_ROOT = '/registry';
var app = express();
if (cluster.isMaster) {
for (var i = 0; i < CPUS; i++) {
cluster.fork();
}
}
else {
//连接zookeeper
var zk = zookeeper.createClient(CONNECTION_STRING);
zk.connect();
//创建代理服务器对象并监听错误事件
var proxy = httpProxy.createProxyServer();
proxy.on('error', function (err, req, res) {
res.end();//输出空白响应数据
});
//启动web服务器
app.use(express.static('public'));
app.all('*', function (req, res) {
//处理图标请求
if (req.path == '/favicon.ico') {
res.end();
return;
}
//获取服务名称
var serviceName = req.get('Service-Name');
console.log('ServiceName:%s', serviceName);
if (!serviceName) {
console.log('Service-Name request header is not exist');
res.end();
return;
}
//获取服务路径
var servicePath = REGISTRY_ROOT + "/" + serviceName;
console.log('ServicePath:%s', servicePath);
console.log('cache[serviceName]:'+JSON.stringify(cache));
if (cache[serviceName]) {
//if(false){
//TODO
/*zk.exists(servicePath, function (event) {
if (event.NODE_DELETED) {
cache = {};
}
}, function (error, stat) {
if (stat) {
}
})*/
console.log("-----------cache---------------"+cache[serviceName]);
proxy.web(req, res, {
target: 'http://' + cache[serviceName] //目标地址
});
}
else {
//获取服务路径下的地址节点
zk.getChildren(servicePath, function (error, addressNodes) {
if (error) {
console.log(error.stack);
res.end();
return;
}
var size = addressNodes.length;
if (size == 0) {
console.log('address node is not exist');
res.end();
return;
}
//生成地址容器
var addressPath = servicePath + "/";
if (size == 1) {
//若只有唯一地址,则获取该地址
addressPath += addressNodes[0];
} else {
//若存在多个地址,则随机获取一个地址
addressPath += addressNodes[parseInt(Math.random() * size)];
}
console.log('addressPath:%s', addressPath);
//获取服务地址
zk.getData(addressPath, function (err, serviceAddress) {
if (error) {
console.log(error.stack);
res.end();
return;
}
console.log('serviceAddress:%s', serviceAddress);
if (!serviceAddress) {
console.log('serviceAddress is not exist');
res.end();
return;
}
cache[serviceName] = serviceAddress;
console.log("cache"+ serviceName+": "+cache[serviceName]);
//执行反向代理
proxy.web(req, res, {
target: 'http://' + serviceAddress //目标地址
});
});
});
}
});
app.listen(PORT, function () {
console.log('server is running at %d', PORT);
});
}
使用supervisor app-gateway.js
启动API网关后:(supervisor可以定时监听文件的变化,代码更新无需重启)
当然也需要启动在上文《基于ZooKeeper的服务注册实现》提到的两个客户端,让服务在ZooKeeper上注册。
测试页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
<div id="console"></div>
<div id="container">
<h1>
hello world
</h1>
</div>
<button id="btn" >点我一下调用服务</button>
</body>
<script src="js/jquery-2.2.3.min.js"></script>
<script>
$(function () {
$("#btn").click(function () {
//alert("hello world");
$.ajax({
method: 'GET',
url: '/hello',
headers: {
'Service-Name': 'HelloService'
},
success: function (data) {
$("#console").text(data);
}
})
});
})
</script>
</html>
点击测试按钮,在界面服务的显示运行结果"Hello"
AB测试(Apache Bench)模拟1000个用户每次并发100请求:
注 :在测试前,将serviceName设为固定值:var serviceName='HelloService'
阅读本文,请结合上篇文章《基于ZooKeeper的服务注册实现》进行理解。