以下情境为教学假设,人物与数字不代表经核实的真实案例。
SLA 通常写成三个数字:可用性百分比、统计周期、抵扣比例。大多数人在谈判时只盯第一个数字,觉得 99.9% 比 99.5% 好;真正决定财务后果的是后两个数字,尤其是抵扣怎么算、按什么基数算。可用性只是一个触发器,抵扣才是钱。
LedgerLite 的 SLA 是 99.5%,抵扣 10%。按 365 × 24 = 8760 小时的年度基数,99.5% 允许全年停机 43.8 小时;按月度基数 730 小时,允许每月停机 3.65 小时。月度费用是 $20,020,10% 抵扣是 $2,002。Nord 的 SLA 是 99.0%,抵扣 5%;99.0% 允许全年停机 87.6 小时,产品线月度费用 $3,510,5% 抵扣是 $175.50。
这一节只做三件事:把可用性换算成停机时间、把抵扣换算成金额、说明抵扣为什么是收入折减而不是成本。它不写 SLA 模板,也不判断条款是否可执行;它只把「可用性」翻译成财务报表上的数字。
可用性怎么换算成停机时间
可用性和停机时间的关系是一条简单的比例:
允许停机小时 = 统计周期小时 × (1 − 可用性)
年度基数用 365 × 24 = 8760 小时;月度基数用 8760 ÷ 12 = 730 小时。两个基数都写清楚,才能比较不同合同的 SLA。
| SLA | 年度允许停机 | 月度允许停机 | 年度允许停机(分钟) | 月度允许停机(分钟) |
|---|---|---|---|---|
| 99.5% | 43.8 小时 | 3.65 小时 | 2,628 分钟 | 219 分钟 |
| 99.0% | 87.6 小时 | 7.3 小时 | 5,256 分钟 | 438 分钟 |
这张表里有一个常见的算术陷阱:有人把 99.5% 的年度允许停机写成 21.9 小时。21.9 小时对应的是 0.25% 的停机比例,也就是 99.75%,不是 99.5%。差 0.25 个百分点,允许停机时间就差一倍。把 21.9 小时代入公式反算:
1 − 21.9 ÷ 8760 = 99.75%
1 − 43.8 ÷ 8760 = 99.50%
这个错误之所以常见,是因为 99.5% 和 99.75% 在口头上都像「五个九附近」,但停机预算差了一倍。SLA 谈判时,百分比的小数点后第二位不能靠感觉;每一个百分比都要用 8760 或 730 反算一次。
| 写法 | 年度允许停机 | 反算可用性 | 常见误读 |
|---|---|---|---|
| 99.5% | 43.8 小时 | 99.50% | 被误写成 21.9 小时 |
| 99.75% | 21.9 小时 | 99.75% | 被误当成 99.5% |
| 99.0% | 87.6 小时 | 99.00% | 被误写成 43.8 小时 |
除了百分比,还要看统计周期是月、季度还是年。月度统计有 12 次触发机会,年度统计只有 1 次;同样的停机时长,放在月度统计里可能触发 12 次抵扣,放在年度统计里可能一次都不触发。统计周期不是技术细节,它直接改变抵扣的频率。
抵扣金额:月度与年度两种基数
抵扣比例通常写成「月度费用的 10%」或「年度费用的 10%」。两种写法的年度总额可能相同,但触发频率和单次金额不同。
LedgerLite 的月度费用是 $20,020,10% 抵扣是 $2,002;年度费用是 $240,240,10% 抵扣是 $24,024。如果每个月都触发,全年抵扣总额是 $24,024,正好是年费的 10%。Nord 的产品线月度费用是 $3,510,5% 抵扣是 $175.50;年度费用是 $42,120,5% 抵扣是 $2,106。
| 案例 | 抵扣基数 | 月度费用 | 抵扣比例 | 单月抵扣 | 年度最大抵扣 |
|---|---|---|---|---|---|
| LedgerLite | 月度费用 | $20,020 | 10% | $2,002 | $24,024 |
| LedgerLite | 年度费用 | $240,240 | 10% | — | $24,024 |
| Nord(产品线) | 月度费用 | $3,510 | 5% | $175.50 | $2,106 |
| Nord(产品线) | 年度费用 | $42,120 | 5% | — | $2,106 |
Nord 的 SLA 基数需要特别说明。Nord 的年收入 $154,320 由咨询 $112,200 和产品订阅 $42,120 组成;咨询按小时计费,产品按月订阅。SLA 通常绑定在产品线上,所以上表用产品线的 $3,510/月作为基数。如果合同把咨询线也纳入同一个 SLA,基数会变成全部 MRR $12,860,5% 抵扣是 $643/月;案例没有披露 SLA 的适用范围,正文采用产品线口径,并把「基数选择」本身写成一个假设。
| Nord 的基数选择 | 月度基数 | 5% 抵扣 | 年度最大抵扣 |
|---|---|---|---|
| 只含产品线 | $3,510 | $175.50 | $2,106 |
| 含咨询 + 产品 | $12,860 | $643.00 | $7,716 |
这张表说明,SLA 谈判里最容易被忽略的问题不是百分比,而是基数。同一个 5%,基数从 $3,510 变成 $12,860,抵扣金额变成 3.66 倍。签约前必须确认:基数包含哪些收入、是否含税、是否含一次性费用、是否含未使用额度。
抵扣是收入折减,不是成本
抵扣最容易被误读成一种「售后成本」。从利润表看,它确实会减少利润,但它减少的路径不是成本增加,而是收入减少。这个区别会影响三件事:毛利率、ARR、以及续约时的价格锚。
LedgerLite 的月度抵扣是 $2,002。如果把它当成一种会随收入同比例变化的成本,可能会用变动成本率去折算,觉得「实际影响是 $2,002 × 15.8% = $316」;这是错的。抵扣是直接从收入里扣掉 $2,002,变动成本不会因为抵扣而下降,所以毛利也减少 $2,002。抵扣对毛利的冲击是 100%,不是 15.8%。
| 口径 | 金额 | 对收入 | 对变动成本 | 对毛利 |
|---|---|---|---|---|
| 把抵扣当成本 | $2,002 | 不变 | 增加 $2,002 | 减少 $2,002 |
| 把抵扣当收入折减 | $2,002 | 减少 $2,002 | 不变 | 减少 $2,002 |
两种口径对毛利的最终影响相同,但对收入、毛利率和 ARR 的影响不同。当成成本,收入不变,毛利率看起来只是略降;当成收入折减,收入下降,毛利率的分母也下降,ARR 直接缩水。对订阅生意,ARR 是估值和续约的锚,收入折减比成本增加更难修复:成本可以下个月砍掉,收入折减会留在客户的价格预期里。
把 LedgerLite 的年度最大抵扣 $24,024 放进财务盘子:
| 指标 | 金额 | 抵扣占该指标的比例 |
|---|---|---|
| 年收入 | $240,240 | 10.0% |
| 毛利 | $202,240 | 11.9% |
| SDE | $130,240 | 18.4% |
| 净利润 | $58,299 | 41.2% |
一年 12 个月全部触发抵扣,才会出现 $24,024 的最大值;单月抵扣 $2,002 占月毛利 $16,853 的 11.9%,占月 SDE $10,853 的 18.4%。这个比例说明,一次月度抵扣不是「小钱」:它相当于把当月的毛利率从 84.2% 拉到 74.2% 左右。具体算法是($16,853 − $2,002)÷ $20,020 = 74.2%。如果一个月内既有抵扣,又有额外的支持成本,毛利率会跌得更快。
抵扣还会影响收入质量。SLA 抵扣是收入的减项,不是独立的一笔费用;它会让同一客户的实际 ARR 低于合同 ARR,也会让续约时的价格锚下移。收入质量的判断口径见4.2 收入质量 。如果抵扣频繁发生,合同 ARR 就不能直接用来做估值倍数,必须先扣掉预期抵扣。
一次停机触发多少抵扣
抵扣不是按停机分钟线性计算的,而是一个阶梯函数:停机超过允许值,就触发一个固定比例;超过多少,抵扣金额不变。这个阶梯让「刚超过允许值」的那一小时特别贵。
以 LedgerLite 的月度 SLA 为例,99.5% 允许 3.65 小时。一次 4 小时停机只超出 0.35 小时,却触发 10% 的月度抵扣 $2,002;按超出的小时数算,每超出 1 小时的成本是 $2,002 ÷ 0.35 = $5,720。一次 1 小时停机没有超过允许值,抵扣是 0;一次 3.65 小时停机刚好在线上,抵扣也是 0。
| 停机时长(月度) | 是否触发 | 超出允许值 | 抵扣金额 | 每超出 1 小时的成本 |
|---|---|---|---|---|
| 1 小时 | 否 | 0 | $0 | — |
| 3.65 小时 | 否 | 0 | $0 | — |
| 4 小时 | 是 | 0.35 小时 | $2,002 | $5,720 |
| 24 小时 | 是 | 20.35 小时 | $2,002 | $98 |
| 43.8 小时 | 是 | 40.15 小时 | $2,002 | $50 |
年度统计的阶梯更陡。99.5% 允许 43.8 小时;一次 44 小时停机只超出 0.2 小时,却触发年度抵扣 $24,024;每超出 1 小时的成本是 $24,024 ÷ 0.2 = $120,120。停机继续扩大,抵扣金额不变,每小时的边际成本迅速下降。
| 停机时长(年度) | 是否触发 | 超出允许值 | 抵扣金额 | 每超出 1 小时的成本 |
|---|---|---|---|---|
| 43.8 小时 | 否 | 0 | $0 | — |
| 44 小时 | 是 | 0.2 小时 | $24,024 | $120,120 |
| 100 小时 | 是 | 56.2 小时 | $24,024 | $427 |
| 200 小时 | 是 | 156.2 小时 | $24,024 | $154 |
这两张表说明,SLA 的风险不在「停机多久」,而在「有没有跨过阈值」。跨过阈值的那一分钟最贵;跨过之后,抵扣被上限封住,边际成本反而下降。对卖方,最危险的场景不是大停机,而是每个月都刚好超过允许值几分钟;对买方,最有价值的条款不是更高的抵扣比例,而是更短的统计周期和更低的触发阈值。
代码:停机预算与抵扣
下面的 sla-credit.ts 把停机预算、停机影响和可用性反算固化成三个函数。downtimeBudget 接受可用性、抵扣比例、统计周期和周期费用,返回允许停机小时、分钟和抵扣金额;outageImpact 接受一次停机时长,返回是否触发、超出多少、抵扣多少、每超出 1 小时的成本;availabilityFromDowntime 从停机时长反算可用性,用来检查合同里的数字是否自洽。
// file: src/contract/sla-credit.ts
// tier: L1
// verify: node --test test/contract/sla-credit.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps, applyRate } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type SlaTerms = {
availabilityBps: Bps;
creditBps: Bps;
periodHours: number;
periodFeeCents: Cents;
};
export type DowntimeBudget = {
availabilityBps: Bps;
periodHours: number;
allowedDowntimeHours: number;
allowedDowntimeMinutes: number;
creditCents: Cents;
};
export type OutageImpact = {
breached: boolean;
allowedDowntimeHours: number;
excessHours: number;
creditCents: Cents;
effectiveCostPerExcessHourCents: Cents;
};
function roundHours(value: number): number {
return Math.round(value * 100) / 100;
}
function checkTerms(terms: SlaTerms): string | null {
if (
!Number.isInteger(terms.availabilityBps) ||
terms.availabilityBps < 0 ||
terms.availabilityBps > 10000
) {
return "availability must be between 0 and 10000 bps";
}
if (
!Number.isInteger(terms.creditBps) ||
terms.creditBps < 0 ||
terms.creditBps > 10000
) {
return "credit must be between 0 and 10000 bps";
}
if (!Number.isFinite(terms.periodHours) || terms.periodHours <= 0) {
return "period hours must be positive";
}
if (!Number.isInteger(terms.periodFeeCents) || terms.periodFeeCents < 0) {
return "period fee must be a non-negative integer amount in cents";
}
return null;
}
/**
* Availability is a share of the measurement period, so the allowed downtime
* is only meaningful together with the period. The annual base used in this
* book is 365 x 24 = 8760 hours; the monthly base is 8760 / 12 = 730 hours.
*/
export function downtimeBudget(terms: SlaTerms): Result<DowntimeBudget> {
const error = checkTerms(terms);
if (error) {
return fail(error);
}
const downtimeShare = (10000 - terms.availabilityBps) / 10000;
const allowedDowntimeHours = roundHours(terms.periodHours * downtimeShare);
return ok({
availabilityBps: terms.availabilityBps,
periodHours: terms.periodHours,
allowedDowntimeHours,
allowedDowntimeMinutes: Math.round(allowedDowntimeHours * 60),
creditCents: applyRate(terms.periodFeeCents, terms.creditBps),
});
}
/**
* A credit is triggered once downtime crosses the allowance; it is not
* proportional to the number of minutes over the line. The effective cost per
* excess hour therefore collapses as the outage grows, while the first hour
* past the allowance is the most expensive one.
*/
export function outageImpact(
terms: SlaTerms,
outageHours: number,
): Result<OutageImpact> {
const error = checkTerms(terms);
if (error) {
return fail(error);
}
if (!Number.isFinite(outageHours) || outageHours < 0) {
return fail("outage hours must be a non-negative number");
}
const budget = downtimeBudget(terms);
if (!budget.ok) {
return budget;
}
const allowed = budget.value.allowedDowntimeHours;
const breached = outageHours > allowed;
const excessHours = roundHours(Math.max(0, outageHours - allowed));
const creditCents = breached
? applyRate(terms.periodFeeCents, terms.creditBps)
: 0;
return ok({
breached,
allowedDowntimeHours: allowed,
excessHours,
creditCents,
effectiveCostPerExcessHourCents:
excessHours > 0 ? Math.round(creditCents / excessHours) : 0,
});
}
/**
* The inverse check is useful when a draft promises an availability number but
* quotes a downtime figure from a different base. 21.9 hours a year is 99.75%,
* not 99.5%; the difference is a factor of two in the downtime allowance.
*/
export function availabilityFromDowntime(
outageHours: number,
periodHours: number,
): Result<Bps> {
if (!Number.isFinite(outageHours) || outageHours < 0) {
return fail("outage hours must be a non-negative number");
}
if (!Number.isFinite(periodHours) || periodHours <= 0) {
return fail("period hours must be positive");
}
if (outageHours > periodHours) {
return fail("outage hours cannot exceed the period");
}
return ok(Math.round((1 - outageHours / periodHours) * 10000));
}
代码里有三个口径值得注意。第一,downtimeBudget 不假设统计周期,调用方必须显式传入 8760 或 730;这样年度和月度不会混在一起。第二,outageImpact 的抵扣是阶梯函数,不是按分钟比例;超过阈值就触发固定比例,没超过就是 0。第三,availabilityFromDowntime 专门用来抓「21.9 小时 = 99.5%」这类错误;它会把 21.9 小时反算成 99.75%,把 43.8 小时反算成 99.50%。
测试里的 LedgerLite 参数是可用性 99.5%、抵扣 10%、月度周期 730 小时、月度费用 $20,020。跑出来的允许停机是 3.65 小时,抵扣是 $2,002;4 小时停机的超出部分是 0.35 小时,每超出 1 小时的成本是 $5,720。Nord 的参数是可用性 99.0%、抵扣 5%、产品线月度费用 $3,510,允许停机 7.3 小时,抵扣 $175.50。
Nord 与 Tidecast 的对照
Nord 的 99.0% 比 LedgerLite 的 99.5% 更宽松:年度允许停机 87.6 小时,是 LedgerLite 43.8 小时的两倍;抵扣比例 5% 也只有 LedgerLite 的一半。按产品线基数,Nord 的单月抵扣 $175.50,只有 LedgerLite $2,002 的 8.8%;但 Nord 的基数小,5% 占产品线月收入的 5%,而 LedgerLite 的 10% 占月收入的 10%。两个案例的绝对金额差别,主要来自收入基数,不是条款松紧。
Tidecast 的平台发行协议里,可用性条款由平台单方定义。卖方没有谈判出来的 SLA 抵扣,平台停机时也不会按卖方的收入损失赔偿;如果 Tidecast 自己对官网客户承诺了可用性,它可能在平台停机时同时承担对客户的抵扣,却拿不到平台的补偿。平台型收入的风险不是「抵扣比例多高」,而是「抵扣义务在你这边、抵扣权利在平台那边」。
小结
- SLA 是「可用性 + 统计周期 + 抵扣比例」三件事;只看百分比,会把停机预算算错一倍。
- 以 365 × 24 = 8760 小时为年度基数,99.5% 允许停机 43.8 小时,99.0% 允许 87.6 小时;月度基数 730 小时,分别允许 3.65 和 7.3 小时。
- 21.9 小时对应 99.75%,不是 99.5%;把 99.5% 的允许停机写成 21.9 小时,是把 0.25% 当成了 0.5%。
- LedgerLite 的月度 10% 抵扣是 $2,002,年度最大抵扣 $24,024,占毛利 11.9%、SDE 18.4%、净利润 41.2%。
- Nord 的产品线 5% 抵扣是 $175.50/月、$2,106/年;如果基数含咨询线,抵扣变成 $643/月、$7,716/年。
- 抵扣是收入折减,不是成本:收入减少 $2,002,变动成本不变,毛利减少 $2,002;不要用毛利率去打折。
- 抵扣是阶梯函数:4 小时月度停机只超出 0.35 小时,却触发 $2,002,每超出 1 小时的成本是 $5,720;年度 44 小时停机只超出 0.2 小时,却触发 $24,024,每超出 1 小时的成本是 $120,120。
- 平台型收入的 SLA 由平台单方定义,卖方可能承担对客户的抵扣,却拿不到平台的补偿。
阅读导航:上一节:2.2 期限、自动续约与退出窗口 · 下一节:3.1 账期的真实成本:Net 15、30、60 差多少 。
常见错误与边界
- 把 99.5% 的年度允许停机写成 21.9 小时。 21.9 小时是 99.75%;99.5% 是 43.8 小时,两者相差一倍。用 8760 × (1 − 可用性) 复算一次。
- 不写统计周期。 月度统计有 12 次触发机会,年度统计只有 1 次;同一个 99.5%,按月和按年的抵扣频率完全不同。
- 混用 720 小时和 730 小时。 30 天月是 720 小时,日历年月均 730 小时;不写基数,3.6 和 3.65 小时的差别会被当成四舍五入。
- 把抵扣当成成本。 抵扣减少的是收入,不是增加成本;变动成本不会因为抵扣而下降,毛利减少的是抵扣全额。
- 用变动成本率给抵扣打折。 $2,002 的抵扣对毛利的冲击是 $2,002,不是 $2,002 × 15.8%;按 15.8% 折算会低估 84.2% 的影响。
- 假设抵扣按停机分钟线性计算。 抵扣是阶梯函数:超过阈值就触发固定比例,没超过就是 0;刚超过阈值的那一小时最贵。
- 忽略抵扣上限。 月度抵扣通常封顶在月费的一个比例,年度抵扣封顶在年费的一个比例;上限决定了最大收入折减。
- 只看抵扣比例,不看基数。 5% 的基数从 $3,510 变成 $12,860,金额变成 3.66 倍;基数包含哪些收入,比比例更重要。
- 忘记停机本身的运营成本。 抵扣是收入折减,停机还会带来支持成本、工程修复成本和客户流失;两者要分开算,不能互相抵消。
- 假设平台 SLA 会补偿卖方。 平台协议由平台单方定义,卖方对客户的抵扣义务可能留在自己身上;平台型收入要按「义务在你、权利在平台」评估。
SLA 的财务后果,最终落在两个词上:触发频率和抵扣基数。可用性决定阈值,统计周期决定触发频率,基数决定每次触发值多少钱。三者合起来,才是 SLA 的真实价格。下一节进入账期与应收:SLA 抵扣是收入的减项,账期则是现金的延迟;两者都会改变同一张利润表和同一张现金流表。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。