简介
之前一篇主要介绍了docker+consul实现服务注册功能,本文将主要介绍服务发现以及网关的实现。项目通过nodeJs子线程对consul server的监听,做到动态得到服务列表,随机从列表中取得ip,访问服务接口。手写网关更加清楚的展示服务发现,后端路由与负载均衡的简单实现。
基本架构
registratior监控service web,一旦service web 状态发生变化,通知consul cluster做出相应处理,api gateway 订阅consul cluster 的服务,根据负载均衡的策略,把请求转发到对应web处理。
例如用户请求service-web的 getRemoteIp接口,整个调用过程如下图:
用户请求发送至网关->>
网关通过consul server集群得到service-web注册信息->>在网关中缓存
根据自定义负载均衡策略,确定访问ip->>根据ip,发起接口访问
网关得到请求结果并返回客户端
源码地址如下:
app.js ------ app启动入口,
discovery.js ------ 服务发现
router.js ------ 暴露getRemoteIp方法
serviceLocalStorage.js ------ 缓存�服务地址
watch.js ------ 监控注册中心的service 是否发生变化
startWatch.js ------ 启动监控,如果发生变化,则通知缓存更新service列表
Dockerfile ------ 制作docker image
docker-compose.yml ------ 服务编排
代码解析
discovery.js
class Discovery {
connect(...args) {
if (!this.consul) {
debug(`与consul server连接中...`);
//建立连接,
//需要注意的时,由于需要动态获取docker内的consul server的地址,
//所以host需要配置为consulserver(来自docker-compose配置的consulserver)
//发起请求时会经过docker内置的dns server,即可把consulserver替换为具体的consul 服务器 ip
this.consul =new Consul({
host:'consulserver',
...args,
promisify: utils.fromCallback //转化为promise类型
});
}
return this;
}
/**
* 根据名称获取服务
* @param {*} opts
*/
async getService(opts) {
if (!this.consul) {
throw new Error('请先用connect方法进行连接');
}
const {service} = opts;
// 从缓存中获取列表
const services = serviceLocalStorage.getItem(service);
if (services.length > 0) {
debug(`命中缓存,key:${service},value:${JSON.stringify(services)}`);
return services;
}
//如果缓存不存在,则获取远程数据
let result = await this
.consul
.catalog
.service
.nodes(opts);
debug(`获取服务端数据,key:${service}:value:${JSON.stringify(result[0])}`);
serviceLocalStorage.setItem(service, result[0])
return result[0];
}
}
connect方法是连接consul server,host指定连接注册中心名, 得到consul对象。
getService方法,通过服务名,得到服务对应的ip列表。
watch.js
用于监听服务节点,一旦发生变化,立即通知对应的订阅者,更新本地服务列表。
class Watch {
/**
* 监控需要的服务
* @param {*} services
* @param {*} onChanged
*/
watch(services, onChanged) {
const consul = this.consul;
if (services === undefined) {
throw new Error('service 不能为空')
}
if (typeof services === 'string') {
serviceWatch(services);
} else if (services instanceof Array) {
services.forEach(service => {
serviceWatch(service);
});
}
// 监听服务核心代码
function serviceWatch(service) {
const watch = consul.watch({method: consul.catalog.service.nodes, options: {
service
}});
// 监听服务如果发现,则触发回调方法
watch.on('change', data => {
const result = {
name: service,
data
};
debug(`监听${service}内容有变化:${JSON.stringify(result)}`);
onChanged(null, result);
});
watch.on('error', error => {
debug(`监听${service}错误,错误的内容为:${error}`);
onChanged(error, null);
});
}
return this;
}
}
由于nodejs是单线程的,需要额外启动一个子进程来监听服务的变化,一旦服务列表有变化,则把服务列表更新到缓存中.
app.js
const Application = require('koa');
const app = new Application();
const debug = require('debug');
const appDebug = debug('dev:app');
const forkDebug = debug('dev:workerProcess');
const child_process = require('child_process');
const router = require('./router');
const serviceLocalStorage = require('./serviceLocalStorage.js');
//监听3000端口
app.listen(3000, '0.0.0.0',() => {
appDebug('Server running at 3000');
});
app
.use(router.routes())
.use(router.allowedMethods);
// fork一个子进程,用于监听服务节点变化
const workerProcess = child_process.fork('./startWatch.js');
// 子进程退出
workerProcess.on('exit', function (code) {
forkDebug(`子进程已退出,退出码:${code}`);
});
workerProcess.on('error', function (error) {
forkDebug(`error: ${error}`);
});
// 接收变化的服务列表,并更新到缓存中
workerProcess.on('message', msg => {
if (msg) {
appDebug(`从监控中数据变化:${JSON.stringify(msg)}`);
//更新缓存中服务列表
serviceLocalStorage.setItem(msg.name, msg.data);
}
});
startWatch.js
const watch = require('./watch');
// 监听服务节点,如果发现变化,则通知主进程的服务列表进行更新
watch.connect().watch(['service-web'],(error,data)=>{
process.send(data);
});
startWatch.js实现监听服务节点,如果发生变化,通知主线程更新缓存列表。
router.js
router.get('/service-web/getRemoteIp', async(ctx, next) => {
//获取具体ip信息
const host = await getServiceHost('service-web');
const fetchUrl = `http://${host}/getRemoteIp`;
// 获取到具体服务的ip信息
const result = await request.get(fetchUrl);
debug(`getRemoteIp:${result.text}`);
ctx.body = result.text;
});
/**
* 根据service name 获取 service 对应host
*/
async function getServiceHost(name) {
//根据服务名称获取注册的服务信息,如果缓存中存在,则从缓存中获取,如果不存在则获取数据
const services = await discovery.getService({service: name});
random = Math.floor(Math.random() * (services.length));
//定义随机数,随机获取ip的负载均衡策略
const host = services[random];
debug(`service host ${services[random]}`)
return host;
}
router定义一个中间件,处理/service-web/getRemoteIp请求。通过discovery.getService方法,得到service-web的所有ip。根据自定义的负载均衡策略(取随机数)请求选定ip的服务的getRemoteIp方法。
docker-compose.yml
与之前的配置文件不同这里加入了gateway的配置。在运行之前还要将gateway服务做成镜像,方法与之前制作相似。在原有基础上加上一下:
gateway:
image: windavid/gateway-test
hostname: gateway
ports:
- "3000:3000"
networks:
- app
接下来用
docker-compose up -d --scale serviceweb=3
启动服务就可以了。
验证服务发现
浏览器请求 http://127.0.0.1:3000/service-web/getRemoteIp, 或使用curl发送请求。
curl http://127.0.0.1:3000/service-web/getRemoteIp
发现得到的ip就是service-web三个服务的随机值。
验证服务监听
还可以验证gateway对consul server的监听。当其中一个service-web服务停止时,gateway里的服务列表应该会更新。我们来验证过一下:
我们先stop一个service-web 服务,再查看下gateway服务的日志。
docker-compose logs gateway
这是发现听见到service-web的变化,172.20.0.6服务已经下线了。
这时再访问http://127.0.0.1:3000/service-web/getRemoteIp,获取接口ip时,发现只有172.20.0.4,172.20.0.5 ,两个ip了。