《独立开发者的商业指南·合同、应收与风险》2.3 SLA 与抵扣:可用性怎么变成钱

SLA 把停机时间翻译成收入折减。本节按 365×24=8760 小时为基数,算出 LedgerLite 99.5% 对应全年 43.8 小时允许停机,月度 10% 抵扣为 $2,002;Nord 99.0% 对应 87.6 小时,5% 抵扣为 $175.50。抵扣不是成本,而是收入的直接扣减,一次 4 小时停机就能触发单月毛利 11.9% 的损失。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

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,02010%$2,002$24,024
LedgerLite年度费用$240,24010%—$24,024
Nord(产品线)月度费用$3,5105%$175.50$2,106
Nord(产品线)年度费用$42,1205%—$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,24010.0%
毛利$202,24011.9%
SDE$130,24018.4%
净利润$58,29941.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 差多少 。

常见错误与边界

  1. 把 99.5% 的年度允许停机写成 21.9 小时。 21.9 小时是 99.75%;99.5% 是 43.8 小时,两者相差一倍。用 8760 × (1 − 可用性) 复算一次。
  2. 不写统计周期。 月度统计有 12 次触发机会,年度统计只有 1 次;同一个 99.5%,按月和按年的抵扣频率完全不同。
  3. 混用 720 小时和 730 小时。 30 天月是 720 小时,日历年月均 730 小时;不写基数,3.6 和 3.65 小时的差别会被当成四舍五入。
  4. 把抵扣当成成本。 抵扣减少的是收入,不是增加成本;变动成本不会因为抵扣而下降,毛利减少的是抵扣全额。
  5. 用变动成本率给抵扣打折。 $2,002 的抵扣对毛利的冲击是 $2,002,不是 $2,002 × 15.8%;按 15.8% 折算会低估 84.2% 的影响。
  6. 假设抵扣按停机分钟线性计算。 抵扣是阶梯函数:超过阈值就触发固定比例,没超过就是 0;刚超过阈值的那一小时最贵。
  7. 忽略抵扣上限。 月度抵扣通常封顶在月费的一个比例,年度抵扣封顶在年费的一个比例;上限决定了最大收入折减。
  8. 只看抵扣比例,不看基数。 5% 的基数从 $3,510 变成 $12,860,金额变成 3.66 倍;基数包含哪些收入,比比例更重要。
  9. 忘记停机本身的运营成本。 抵扣是收入折减,停机还会带来支持成本、工程修复成本和客户流失;两者要分开算,不能互相抵消。
  10. 假设平台 SLA 会补偿卖方。 平台协议由平台单方定义,卖方对客户的抵扣义务可能留在自己身上;平台型收入要按「义务在你、权利在平台」评估。

SLA 的财务后果,最终落在两个词上:触发频率和抵扣基数。可用性决定阈值,统计周期决定触发频率,基数决定每次触发值多少钱。三者合起来,才是 SLA 的真实价格。下一节进入账期与应收:SLA 抵扣是收入的减项,账期则是现金的延迟;两者都会改变同一张利润表和同一张现金流表。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查