本文使用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'
AB測試
閱讀本文,請結合上篇文章《基于ZooKeeper的服務注冊實現》進行理解。