
设备科去年把点检和报修流程搬上低代码应用之后,很快冒出一个新需求:设备厂商的远程监控平台有一批设备运行数据,温度、负载、报警码,能不能聚合进应用里,跟点检记录放在同一个视图看。
厂商平台只提供 REST 接口,有签名、有分页、有限流,直接从应用侧轮询不现实。我们最终的方案是用云函数做聚合层:函数定时拉取接口、清洗数据、写回应用的数据表。这篇文章把实现过程和关键细节完整记录下来。
最初考虑过让应用直接配第三方数据源,试了两次就放弃了。头一个障碍是签名鉴权:厂商接口要求按时间戳生成签名,密钥不能暴露在前端配置里,低代码应用的数据源配置很难安全承载这套逻辑。
另一个障碍是分页与限流。设备数据按十分钟一个采样点返回,六十多台设备一天的量要翻几十页,接口还限制每秒请求数。把这个压力放到交互链路里,页面打开速度直接不可接受。
还有一个障碍是数据形状。厂商返回的字段命名和单位跟我们应用内建表完全对不上,温度有摄氏度也有华氏度,设备编号格式也不一致。没有一层清洗,接进来的数据没法直接用。
云函数恰好补上这三个缺口。密钥存在函数的环境变量里,签名逻辑在函数内完成,不出边界;函数按定时触发器调度,拉取节奏自己控制,天然匹配限流;返回数据在函数里完成清洗和映射,写回应用数据表的是规整后的形状。
从架构上讲,云函数扮演的是防腐层的角色:第三方接口怎么变,只影响函数内部逻辑,应用侧的数据表结构保持稳定。这个解耦在后来厂商升级接口版本时立了大功,我们只改了函数,应用内报表一行没动。
定时触发的频率我们定为每十分钟一次,和厂商采样周期对齐。频率定高没有意义,数据源本身不产生更细的粒度,白白消耗调用额度。
厂商接口的签名规则是常规的 HMAC-SHA256:把公共参数按字典序拼接,加上时间戳,用密钥签名后放进请求头。先封装一个签名请求函数:
const crypto = require('crypto');
function signedRequest(path, params, secret) {
const ts = Date.now().toString();
const sorted = Object.keys(params).sort()
.map(k => k + '=' + params[k]).join('&');
const sign = crypto.createHmac('sha256', secret)
.update(sorted + '&ts=' + ts).digest('hex');
return fetch(API_BASE + path + '?' + sorted, {
headers: { 'X-Timestamp': ts, 'X-Signature': sign }
}).then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status + ' on ' + path);
return r.json();
});
}两个细节值得说。时间戳取请求那一刻的值而不是提前算好,避免函数冷启动耗时导致时间偏差校验失败;错误信息里带上路径,分页拉取时一旦出错能立刻定位是哪一页的问题,排查效率差很多。
密钥从环境变量读取,代码仓库里零敏感信息。函数的运行环境变量在平台侧配置,轮换密钥时改配置重启即可,代码不动。
数据接口按页返回,需要在函数里做翻页循环,同时控制请求节奏避免触发限流:
async function fetchAllDevices(secret) {
const all = [];
let page = 1;
while (true) {
const resp = await signedRequest('/api/device/list',
{ page: page, size: 100 }, secret);
if (resp.code !== 0) throw new Error('API error: ' + resp.code);
all.push(...resp.data.items);
if (all.length >= resp.data.total) break;
page++;
await sleep(150); // 限流保护
}
return all;
}循环的退出条件用累计条数对比总数,而不是判断当页是否为空。厂商接口偶尔返回空页但不代表结束,用空页判断会提前截断,这个行为是在一次丢数事故里发现的,教训是退出条件必须锚定在明确的总量字段上。
sleep 一百五十毫秒是按限流阈值倒推的安全值。最初没有这个间隔,设备数量上来之后频繁触发限流,函数整体超时。加上间隔后单次执行拉长到二十秒左右,仍在函数超时上限内,稳定运行至今。
拉回来的原始数据要做三步清洗:字段映射、单位归一、编号对齐。清洗函数保持纯函数特性,输入原始条目,输出规整条目,不掺杂 IO,方便单测:
function normalize(raw) {
return {
deviceCode: raw.dev_id.replace(/^VM-/, ''),
temperature: raw.temp_unit === 'F'
? Math.round((raw.temp - 32) / 1.8 * 10) / 10
: raw.temp,
load: raw.load_pct,
alarmCode: raw.alarm ?? 'NONE',
sampledAt: new Date(raw.sample_time).toISOString()
};
}单位归一统一到摄氏度,保留一位小数。编号对齐是去掉厂商侧的前缀,让它跟我们应用内设备台账的主键一致,这一步是后面联表查询的基础,两边编号对不上,聚合视图就是空谈。
报警码缺失时填 NONE 而不是空值,下游按报警码分组统计就不会冒出一个匿名分组。清洗规则看着琐碎,但每一条都对应着一个曾经真实出现过的脏数据形态,这份映射表是拿事故换来的。
清洗完的数据调用应用开放的数据写入接口批量落表。写入按设备分组、逐批提交,单批一百条:
async function writeBack(rows, appToken) {
for (let i = 0; i < rows.length; i += 100) {
const batch = rows.slice(i, i + 100);
const resp = await fetch(APP_API + '/api/device-metrics/batch', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + appToken },
body: JSON.stringify(batch)
});
if (!resp.ok) throw new Error('write failed at batch ' + (i / 100));
}
}写入失败带着批次号抛出,重跑时能从日志直接判断断点。应用侧数据表对设备编号加采样时间建了唯一约束,重跑插入撞键会被跳过,整条链路因此敢随时重来,幂等性依旧是不变的设计重心。
应用侧的接收接口是我们在搭贝里配的开放数据流,字段映射关系在表单里可视化维护。新增一个监控字段,函数侧加一行映射、表单加一列,链路就通了。
函数每次执行的开始时间、拉取条数、写入条数、耗时写进一张运行日志表。这张表是整个链路的健康晴雨表:拉取量和写入量应该相等,连续不相等就是有清洗环节丢弃了数据。
告警规则很简单:连续两次执行拉取量为零,或者执行直接失败,就发消息到值班群。上线头两周每天都人工看一眼日志表,确认稳定后才转由告警接管,这个节奏和当年对账 job 上线如出一辙。
云函数本身也有额度限制,执行超时、内存上限这些要提前确认。我们把超时设为六十秒,内存够用即可,聚合任务对内存几乎无压力,超时才是真正的约束项。日志表只保留最近九十天,更早的按月归档到对象存储,查历史时再拉回来。这个保留策略是吃过亏之后定的:起初日志无限累积,三个月后表膨胀到影响写入速度,聚合链路自己的观测系统反过来拖侵了主链路,可谓本末倒置。观测数据也是数据,同样需要生命周期管理,这条经验后来被我们写进了科室的运维基线清单。
云函数冷启动的耗时不能忽略。最初函数依赖一个很大的 HTTP 客户端库,冷启动常常两秒多,加上执行时间逼近超时线。后来换成原生 fetch,依赖清零,冷启动降到几百毫秒,整个执行链路宽松了很多。
依赖精简是云函数里最要紧的优化原则:能用运行时自带的就不引包,能用核心模块的就不上全家桶。函数不是常驻服务,每一毫秒的启动开销都在消耗配额。冷启动还影响重试策略的设定。有一回限流导致执行失败,自动重试落在冷启动窗口里,叠加的启动耗时把重试也拖超时了。后来把重试间隔拉到五分钟,避开冷启动离得近的时间段,同类问题再没复现。调度密度、重试间隔、超时上限这三个参数要当作一组来调,单看任何一个都容易得出局部正确、整体失败的配置。
采样时间厂商侧用的是零时区字符串,我们台账里存的是本地时间。头一版直接透传,联表查询时同一台设备的厂商数据和点检数据错着八小时,图表上看着永远对不齐。
修复方案是在 normalize 里统一转成本地时区再落库,同时保留原始字符串做审计。时间口径这种问题不止云函数会遇到,但凡是两个系统对接,时区必须提前对齐,事后补的代价是重刷历史数据。
这套架构里云函数只做三件事:取数、清洗、写回,不做业务计算。指标聚合、同比环比这些全部留在应用侧的报表能力里完成。边界划清之后,函数代码始终没超过两百行,厂商接口变更的适配成本被压到最小。
边界感反过来也成立:应用侧不感知厂商接口的存在,数据表结构就是稳定契约。两侧各自演进互不拖累,这是防腐层思路在低代码场景里的直接收益。
回头看,这套链路最值的投入是那张运行日志表和两条告警规则,成本不到半天,换来了全年安睡。数据集成项目出问题不稀奇,稀奇的是出问题时你能不能在十分钟内定位到环节。
外部数据接入这件事,技术难点从来不是调通接口,而是让它长期稳定地跑下去。把幂等、限流、可观测这三件事做扎实,剩下的都是体力活,这是我把整套方案沉淀成文的初衷。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。