node 服务接入 java eureka 微服务
背景
项目现有的后端服务主要由 node 微服务与 java 微服务组成,node 与 java 之间的互相调用通过构建各自的网关工程进行接口路由转发来实现接口调用。在代码实现上,java 与 node 的调用主要通过硬编码即在配置中写死 ip 地址的方式拼接请求链接从而发起请求。
eureka 简介
eureka 是 netflix 开发的服务发现组件,本身是一个基于 rest 的服务。基于 rest 服务就意味着我们可以通过 http 请求实现我们所要的功能。 上图是 eureka 官方的架构图,大致描述了 eureka 集群的工作过程:
- application Service 即服务提供者,application client即服务消费者。
- make remote call,可以简单理解为调用 restful api。
- us-east-1c、us-east-1d 等都是 zone ,它们都属于 us-east-1 这个 region。
eureka包含两个组件:eureka server 和 eureka client,它们的作用如下:
- eureka client 是一个 java 客户端,用于简化与 eureka server 的交互。
- eureka server提供服务发现的能力,各个微服务启动时,会通过 eureka client 向 eureka server进行注册自己的信息(例如网络信息),eureka server会存储该服务的信息。
- 微服务启动后,会周期性地向 eureka server发送心跳(默认周期为30秒)以续约自己的信息。如果eureka Server在一定时间内没有接收到某个微服务节点的心跳,eureka server将会注销该微服务节点(默认90秒);
- 每个 eureka server 同时也是 eureka client,多个 eureka server 之间通过复制的方式完成服务注册表的同步。
- eureka client 会缓存 eureka server 中的信息。即使所有的 eureka server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。
思路
eureka 是 netflix 开发的服务发现组件,本身是一个基于 rest 的服务,我们可以通过 http 实现我们想要的功能。eureka rest 服务列表。 npm 社区已经有国外的大佬开发了基础的组件,基于这个包,我们将 node.js 联入 eureka 微服务体系就来得轻松得多。思路主要有下面几点:
- node 服务可以在 eureka 注册中心中注册。
- node 服务可以在 spring admin 管理中心注册。
- node 服务的下线可以即时通知到 eureka 与 spring admin。
- node 调用其他服务通过 eureka 进行服务寻址,并要支持多实例负载均衡。
实现
eureka.js
const router = Router();
const ip = getIp(); //获取本地ip
let serviceName = "" // eureka 服务注册的服务名
const port = ""; // 对应服务的端口号
const client = new Eureka({
instance: {
instanceId: `${ip}:${serviceName.toLowerCase()}:${port}`, //eureka服务的实例id
app: serviceName,
hostName: ip,
ipAddr: ip,
statusPageUrl: `http://${ip}:${port}/info`, // spring admin 注册心跳
healthCheckUrl: `http://${ip}:${port}/health`, // eureka 注册心跳
port: {
$: port,
'@enabled': 'true',
},
vipAddress: serviceName, // Important, otherwise spring-apigateway cannot find instance of book-service
// secureVipAddress: 'book-service',
dataCenterInfo: {
'@class': 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo',
name: 'MyOwn',
},
},
eureka: {
// eureka 只有一个注册中心
// fetchRegistry: false,
// host: '127.0.0.1',
// port: 8761,
// servicePath: '/eureka/apps/',
registryFetchInterval: 3000,
//有多个 eureka 集群
serviceUrls: {
// default: ['http://127.0.0.1:8761/eureka/apps/'],
default: config.eureka.server.serviceUrls,
},
},
});
// eureka 心跳路由
router.get('/info', (req, res) => {
res.json({ name: serviceName, status: 'UP' });
});
// spring admin 心跳路由
router.get('/health', (req, res) => {
res.json({
description: 'Spring Cloud Eureka Discovery Client',
status: 'UP',
hystrix: {
status: 'UP',
},
});
});
// 再调用 eurekaClient 的时候,防止服务在eureka注册中心多次重复注册,使用promise确保服务在启动后只会初始化一次。
const cbs = [];
let isStarted = false;
const isStarting = false;
const eurekaClient = new Promise((resolve, reject) => {//eslint-disable-line
if (isStarted) {
return resolve(client);
} else if (isStarting) {
cbs.push([resolve, reject]);
} else {
cbs.push([resolve, reject]);
client.start((err) => {
cbs.forEach((cb) => {
if (err) return cb[1](err);
else {
isStarted = true;
return cb[0](client);
}
});
});
}
});
eurekaClient.heartBeat = function heartBeat(app) {
app.use(router);
};
// 在服务被强迫停止或因为由于异常导致服务停止时,在服务停止前向 eureka 发起服务下线。
eurekaClient.graceful = function makeGraceful(server) {
const cl = this;
function onSignal() {
return new Promise((resolve, reject) => {
cl.then(value => value.stop(() => resolve())).catch((err) => {
logger.error(err);
reject(err);
});
});
}
terminus(server, {
signals: ['SIGINT', 'SIGTERM'],
onSignal,
});
graceful({
servers: [server],
killTimeout: '5s',
error(error) {
logger.error(error);
cl.then(value => value.stop()).catch((err) => {
logger.error(err);
});
},
});
};
export default eurekaClient;
feign.js
/**
* 这个文件用于根据目标服务名进行接口请求url的拼接
*/
const address = {};
const indexs = {}; // 用于存放各个服务轮询的指针
export default async function getAddressByEureka({ app }) {
const eureka = await client;
const balanceType = 'roundRobin';
// 初次请求该实例,直接将对应的实例信息存入address中
const instances = eureka.getInstancesByAppId(app.toUpperCase());
logger.debug(`get eureka app instances for app(${app}): ${JSON.stringify(instances)}`);
const ips = [];
instances.forEach((instance) => {
const ip = instance.ipAddr;
const port = instance.port.$;
if (instance.status === 'UP') {
const target = `http://${ip}:${port}`;
ips.push(target);
}
});
if (ips.length === 0) throw new Error(`not found able service registered: ${app}`);
address[app] = ips;
const target = loadBalance(balanceType, app);
return target;
}
//简单的负载均衡,支持轮询or随机
function loadBalance(type, serviceName) {
let result;
const ips = address[serviceName];
if (type === 'random') {
const index = Math.floor(Math.random() * ips.length);
result = ips[index];
} else if (type === 'roundRobin') {
if (!indexs[serviceName]) {
indexs[serviceName] = 0;
} else if (indexs[serviceName] >= ips.length) {
indexs[serviceName] = 0;
}
result = address[serviceName][indexs[serviceName]];
indexs[serviceName] += 1;
}
return result;
}
express start.js
const app = express();
eureka.heartBeat(app);
const httpServer = http.Server(app);
httpServer.listen(port, ip, () => {
eureka.graceful(httpServer);
}
待优化点
- graceful在docker容器内不生效,需要再研究下docker里面服务停止会发送什么信号。
- spring admin 的服务信息可以做的更完善一些,包括服务占用的内存等运维相关的信息。