consul+docker实现服务发现及网关

简介

之前一篇主要介绍了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了。

参考文章:

https://zhuanlan.zhihu.com/p/36471654

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值