Consul 完整健康发现客户端
项目合同
Consul 完整示例 只负责读取 notes-api 健康地址,输出两轮 JSON 快照,不启动业务服务,不自动注册、续报或注销。前置为读者自行准备的 Consul 1.20.x HTTP endpoint、Node.js 22 和最小只读 token。它不执行业务请求,所以 HEALTHY 仅代表检查状态满足筛选。退出码 0 表示两轮非空结果,2 表示至少一轮无可用实例,1 表示 HTTP、网络或响应格式错误;2 是应用约定,不是 Consul 原生状态码。
完整源码与配置
以下 include 与仓库示例为同一文件,没有省略函数或隐含依赖。package.json 声明无第三方运行依赖;service.json 是读者实验时可选择提交的 Agent API 请求体,不是 agent 配置目录文件,后者需要外层 service 字段。
{"private":true,"type":"module","engines":{"node":">=22"},"scripts":{"discover":"node discover.mjs"},"dependencies":{}}{"address":"http://127.0.0.1:8500","service":"notes-api"}// Node.js >=22, no third-party dependencies. Reader-run only.
import { readFile } from 'node:fs/promises';
const cfg = JSON.parse(await readFile(new URL('./client.json', import.meta.url), 'utf8'));
const base = new URL(process.env.CONSUL_HTTP_ADDR || cfg.address);
const headers = process.env.CONSUL_HTTP_TOKEN ? { 'X-Consul-Token': process.env.CONSUL_HTTP_TOKEN } : {};
async function request(path) {
const r = await fetch(new URL(path, base), { headers, signal: AbortSignal.timeout(70000) });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r;
}
try {
const service = encodeURIComponent(cfg.service);
let index = 0;
for (let round = 0; round < 2; round++) {
const r = await request(`/v1/health/service/${service}?passing=true&index=${index}&wait=30s`);
const next = Number(r.headers.get('x-consul-index'));
const rows = await r.json();
if (!Array.isArray(rows) || !Number.isSafeInteger(next) || next < 0) throw new Error('invalid response');
// A lower index invalidates the cursor; zero must not cause a tight loop.
index = next < index ? 0 : Math.max(next, 1);
const endpoints = rows.map(x => ({ address: x.Service.Address || x.Node.Address, port: x.Service.Port }));
console.log(JSON.stringify({ round, index, endpoints, aclFiltered: r.headers.get('x-consul-results-filtered-by-acls') === 'true' }));
if (!rows.length) process.exitCode = 2;
await new Promise(resolve => setTimeout(resolve, 250));
}
} catch (e) { console.error(e.message); process.exitCode = 1; }{
"ID": "notes-api-reader-lab",
"Name": "notes-api",
"Address": "127.0.0.1",
"Port": 8080,
"Check": {"TTL":"30s","Status":"critical","DeregisterCriticalServiceAfter":"10m"}
}service "notes-api" { policy = "read" }
node_prefix "" { policy = "read" }Consul 地址回退 使用 Service.Address,空值时才使用 Node.Address;不能无条件只读 Node.Address,否则容器或显式服务地址会被忽略。地址被记录不代表可路由,程序刻意不宣称进行网络可达性验证。
读者自跑步骤
Consul 示例运行 前先确认在独立实验环境,初始 critical 的实例不会被 passing 查询选中。以下仅供阅读,本轮示例未执行。已有 agent 和注册权限身份的读者可自行提交示例定义,再由实际服务管理者报告 TTL,发现客户端改用只读身份。
cd examples/consul
curl --fail-with-body -X PUT -H "X-Consul-Token: $CONSUL_REGISTER_TOKEN" --data-binary @service.json "$CONSUL_HTTP_ADDR/v1/agent/service/register"
curl --fail-with-body -X PUT -H "X-Consul-Token: $CONSUL_REGISTER_TOKEN" "$CONSUL_HTTP_ADDR/v1/agent/check/pass/service:notes-api-reader-lab"
node discover.mjs预期:第一轮立即输出 endpoints,第二轮可能等待约 30 秒;TTL 为 30 秒,未续报时第二轮可能观察到空集合并最终退出 2。这是生命周期练习,不能把偶然两轮非空当稳定性验收。受影响资源为当前 agent 上固定 ID;读者应使用隔离 agent,结束时调用 /v1/agent/service/deregister/notes-api-reader-lab 清理,不能注销同名其他实例。Windows 读者将变量换成 $env: 并调用 curl.exe。
阶段实践与参考解法
Consul 阶段实践 要求画出注册、critical、passing、TTL 到期的状态线,再预测两轮客户端输出。参考解法:不报告 TTL 时目录仍可能存在该实例,passing 为空;报告一次后有一个短暂可见窗口,超过 TTL 又为空。为长期业务加续报必须由服务生命周期控制,不能在发现客户端里无条件自报成功。进一步练习:用无权限身份查询,输出可能为空或拒绝,需结合 aclFiltered 和 HTTP 状态判断,不能宣称全部实例宕机。
该示例采用官方 health service API 与 agent service API 的合同,核对 2026-10-17;没有安装依赖或执行 HTTP 请求。
继续阅读
- 知识专题
- 问题排查