我们给几十个商家做 SaaS,每家都要用生图。上游是一个该服务 密钥,下游是几十个租户。
问题随之而来:某个商家批量刷图,把并发占满,其他家全都变慢——而且月底账单是一整笔,分不清谁用了多少。
两件事要解决
一、用量归属。 每次调用要能算到具体租户头上。 二、资源隔离。 一家刷爆不能影响其他家。
用量归属:在自己这层记账
上游给的是总量,拆分只能自己做。每次调用落一条记录:
async function genForTenant(tenantId, params) {
const t0 = Date.now();
let ok = false;
try {
const r = await callGenApi(params);
ok = true;
return r;
} finally {
await db.usage.insert({
tenantId,
model: params.model,
// 不同模型成本不同,只记次数月底对不上账
units: costUnits(params.model),
costMs: Date.now() - t0,
ok,
at: new Date(),
});
}
}finally 里记,不是 then 里记。 失败的调用也可能产生消耗,只记成功的会导致自己这边的统计低于上游账单,月底对不上。
按模型折算 units,别只记次数。nano-banana-pro 和 nano-banana2 的成本差好几倍,混在一起算等于让用便宜模型的租户补贴用贵模型的。
资源隔离:按租户限流 + 全局限流
两层都要。
class TenantLimiter {
constructor({ perTenant = 2, global = 8 }) {
this.perTenant = perTenant;
this.global = global;
this.running = new Map(); // tenantId -> 正在跑的数量
this.total = 0;
}
canRun(tenantId) {
return this.total < this.global
&& (this.running.get(tenantId) || 0) < this.perTenant;
}
acquire(tenantId) {
this.total++;
this.running.set(tenantId, (this.running.get(tenantId) || 0) + 1);
}
release(tenantId) {
this.total--;
const n = (this.running.get(tenantId) || 1) - 1;
if (n <= 0) this.running.delete(tenantId); else this.running.set(tenantId, n);
}
}- 单租户上限保证一家占不满
- 全局上限保证总量不超过上游能扛的并发
只有全局限流的话,一个租户塞几百个任务照样能把队列占死;只有单租户限流的话,租户一多总并发还是会爆。两层缺一不可。
配额:软限 + 硬限
我们给每个租户配了两个阈值:
- 软限(比如月度额度的 80%)→ 继续服务,但发通知
- 硬限(100%)→ 拒绝新请求,返回明确的错误码
const used = await db.usage.sumUnits(tenantId, thisMonth);
if (used >= tenant.quotaHard) {
throw Object.assign(new Error('本月额度已用完'), { code: 'QUOTA_EXCEEDED' });
}
if (used >= tenant.quotaSoft) notifyQuotaWarning(tenantId, used);错误码要明确。 返回一个笼统的"生成失败",商家会以为是系统故障来找客服;返回 QUOTA_EXCEEDED 并带上剩余额度,他自己就知道该充值了。
密钥要不要一家一个
理论上一家一个密钥最干净,用量上游就分好了。但实际上:
- 密钥管理成本高,几十个租户就是几十个密钥要存要轮换
- 上游的并发是按密钥算的,拆开之后每家的并发反而变小,高峰期更容易堵
我们最后选的是共用一个密钥 + 自己这层记账和限流。该服务的并发给得比较宽,共用一个池子在整体利用率上更划算——某家闲着的时候,容量可以给到忙的那家。
代价是记账逻辑必须自己写严,尤其是失败调用的处理。这部分省不掉。
接口服务:甜甜圈API(dashengfenshen.cn)
&spm=1001.2101.3001.5002&articleId=163820463&d=1&t=3&u=9816db9ea58347e598f000e0059e645f)
44

被折叠的 条评论
为什么被折叠?



