《独立开发者的商业指南·合同、应收与风险》12.2 业务连续性计划

业务连续性计划不是抽屉里的文档,而是把 RTO/RPO 换算成现金的四个动作:识别关键业务、设定恢复目标、准备替代方案、定期演练。本节算出 LedgerLite 停机一天约 2,670 美元、Tidecast 停服一周约 2,658 美元、Nord 停工一周约 3,644 美元,并用冗余成本除以单次损失,得到值不值得买的平衡次数。

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

业务连续性计划(BCP)常被写成一份放在抽屉里的文档:机房着火怎么办、云供应商跑路怎么办、合伙人住院怎么办。写完就算交差,直到真的出事,才发现文档里没有一个人名、没有一个电话号码、没有一条能在一小时内执行的替代路径。

对独立开发者,BCP 的价值不在「写得全」,而在「算得清」。停一天损失多少、停一周损失多少、恢复要多久、丢多少数据、买冗余划不划算——这些都能用收入、SLA 和备份成本算出来。算不出来的 BCP,和没有 BCP 的区别,只是多了一份心理安慰。

BCP 的四个要素

BCP 有四个要素:关键业务识别、RTO/RPO、替代方案、演练。四者对应四个不同的问题,不能互相替代。

要素回答的问题现金口径常见失败
关键业务识别哪些业务一停就立刻少收钱日收入、周收入把「所有业务」都列为关键
RTO/RPO多久恢复、最多丢多少数据停机损失 + 重建成本只写目标,不写对应的钱
替代方案主路径断了走哪条替代方案年成本写了方案,没有触发条件
演练方案是否真的可用演练成本 + 发现缺口只做桌面推演,不碰真实切换

关键业务识别要窄。LedgerLite 的关键业务不是「整个产品」,而是「计费与对账 API」:客户可以一天不登录,但不能一天不能对账。Tidecast 的关键业务不是「游戏本身」(单机部分在玩家机器上),而是「商店页面与下载」:页面挂了,新玩家买不到,收入立刻归零。Nord 的关键业务是「咨询交付与订阅计费」:咨询是 72.7% 的收入,订阅是剩下的 27.3%,两者停机的影响不同,但都不能停一周。

RTO(恢复时间目标)和 RPO(恢复点目标)要写成小时数,并且配一个现金影响。只写「尽快恢复」没有意义,因为「尽快」不能用来比较两条方案。下面是三家的教学假设:

案例关键业务RTORPO替代方案演练频率
LedgerLite计费与对账 API4 小时15 分钟热备 + 每日备份每季度
Tidecast商店页面与下载24 小时24 小时官网直售 + 构建产物备份每半年
Nord咨询交付与订阅计费8 小时4 小时文档化 + 合伙人互备每季度

这些 RTO/RPO 是教学假设,不是行业标准。LedgerLite 的 4 小时来自「客户上午对账」这个使用习惯;Tidecast 的 24 小时来自「单机游戏可以晚一天上架」;Nord 的 8 小时来自「咨询会议可以改期,但计费不能停」。你的业务如果依赖实时交易,RTO 可能是分钟级;如果依赖月度结算,RTO 可能是天级。目标要与收入节奏匹配,而不是与「技术先进」匹配。

替代方案要写触发条件和成本。LedgerLite 的热备是「主区域失效后 4 小时内切到备用区域」,触发条件是「主区域连续 15 分钟不可用」;Tidecast 的官网直售是「Steam 页面不可用时,把下载链接切到官网」,触发条件是「Steam 后台连续 2 小时不可用」;Nord 的合伙人互备是「一位合伙人连续 3 天无法工作,另一位接管其客户」,触发条件是「医疗或家庭事件」。没有触发条件的替代方案,出事时没有人知道该不该启动。

演练是四要素里最容易被跳过的一环。演练不是开会念文档,而是真的切换一次:LedgerLite 每季度做一次备份恢复,记录从触发到 API 可用的实际小时数;Tidecast 每半年做一次官网直售演练,确认下载链接和支付流程可用;Nord 每季度做一次客户交接演练,确认另一位合伙人能拿到项目文件和沟通记录。演练的成本是几个小时,收益是发现「文档里写了但实际做不到」的缺口。

把 RTO/RPO 换成现金

停机损失由两部分组成:停下来的收入,和合同里的 SLA 抵扣。前者是「没赚到的钱」,后者是「要退回去的钱」,两者都要算进现金影响。

LedgerLite 的 MRR 是 $20,020,按 30 天月折算,每天的收入是 $20,020 ÷ 30 = $667。SLA 是 99.5%,允许的月停机时间是 0.5% × 720 小时 = 3.6 小时;一旦超过,抵扣比例是月费的 10%,即 $20,020 × 10% = $2,002。停机 1 天(24 小时)超过 3.6 小时的允许值,所以:

收入损失 = 20,020 ÷ 30 = 667
SLA 抵扣 = 20,020 × 10% = 2,002
合计 = 2,669

RTO 4 小时的损失小一些,但抵扣仍然全额触发:

收入损失 = 20,020 ÷ 30 ÷ 24 × 4 = 111
SLA 抵扣 = 2,002
合计 = 2,113

RPO 15 分钟对应的是数据重建成本。假设重录一小时数据的成本是 $120(教学假设),15 分钟就是 $30。于是 LedgerLite 在 RTO/RPO 内的一次事件,现金影响是 $2,113 + $30 = $2,143。

Tidecast 没有明确的 SLA 抵扣(平台条款按平台规则处理,教学假设取 0),停服损失就是净收入。年净得 $138,171,按 52 周折算,平均一周是 $2,657;按单份净得 $138,171 ÷ 9,600 = $14.39 算,一周 184.6 份,两者一致。12 月是旺季,月销 2,400 份,按 30 天折算每天 80 份,一周 560 份,损失 $560 × $14.39 = $8,060。所以「停服一周损失多少」有两个答案:平均周 $2,657,12 月周 $8,060。

Nord 的月收入是 $12,860,按 30 天折算每天 $429,一周 $3,001。SLA 是 99.0%,允许的月停机时间是 1% × 720 小时 = 7.2 小时;超过后抵扣 5%,即 $12,860 × 5% = $643。停工一周(168 小时)远超 7.2 小时,所以:

收入损失 = 12,860 ÷ 30 × 7 = 3,001
SLA 抵扣 = 12,860 × 5% = 643
合计 = 3,644

RTO 8 小时的损失是 $143 的收入加 $643 的抵扣,合计 $786;RPO 4 小时按每小时 $90 的重建成本算,是 $360。Nord 在 RTO/RPO 内的一次事件,现金影响是 $786 + $360 = $1,146。

把三家的数字放在一张表里:

案例事件收入损失SLA 抵扣合计
LedgerLite停机 1 天$667$2,002$2,669
LedgerLiteRTO 4 小时$111$2,002$2,113
Tidecast停服 1 周(平均)$2,657$0$2,657
Tidecast停服 1 周(12 月)$8,060$0$8,060
Nord停工 1 周$3,001$643$3,644
NordRTO 8 小时$143$643$786

这张表最反直觉的地方是 LedgerLite:RTO 4 小时的收入损失只有 $111,但 SLA 抵扣 $2,002 是它的 18 倍。也就是说,LedgerLite 的停机成本主要由合同条款决定,而不是由停机的实际时长决定。只要超过 3.6 小时的允许值,抵扣就全额触发;从 4 小时到 24 小时,收入损失增加了 $556,但抵扣不变。对 LedgerLite 来说,把 RTO 从 4 小时压到 3 小时,比把 RTO 从 4 小时压到 2 小时更值钱,因为 3 小时能避开整笔抵扣。

Tidecast 相反:没有 SLA 抵扣,损失完全跟销量走。12 月一周的损失是平均周的 3 倍,所以同样的冗余成本,在 12 月能防住的损失是平时的 3 倍。冗余的维护窗口应该避开 12 月,而不是安排在 12 月。

Nord 介于两者之间:抵扣只占周损失的 17.6%($643 ÷ $3,644),大部分损失来自咨询计费的中断。Nord 的关键不是「恢复得快」,而是「有没有人能接手」;如果一位合伙人停工,另一位能不能在 8 小时内接管客户,决定了 RTO 能不能兑现。

备份与冗余成本 vs 停机损失

BCP 的替代方案不是免费的。热备、异地备份、官网直售通道、合伙人互备,都要花钱。判断「值不值得买」的方法,是把年冗余成本除以单次停机损失,得到「一年要防住几次才回本」。

案例年冗余成本单次事件损失平衡次数/年判断
LedgerLite$3,600$2,6691.35 天一年停机超过 1.35 天,冗余划算
Tidecast$1,200$2,6570.45 周一年停服超过 0.45 周(约 3 天),冗余划算
Nord$2,400$3,6440.66 周一年停工超过 0.66 周(约 4.6 天),冗余划算

年冗余成本是教学假设,从现有成本项按比例推导:LedgerLite 的云成本 $18,000,热备增加 20%,即 $3,600;Tidecast 的服务器成本 $3,000,备份与官网通道增加 40%,即 $1,200;Nord 的工具成本 $6,000,文档化与互备增加 40%,即 $2,400。这些比例不是报价,只是让「成本 vs 损失」能放在同一个天平上。

LedgerLite 的平衡点是 1.35 天/年。这个数看起来很低,但要注意:一次超过 3.6 小时的停机就会触发 $2,002 的抵扣,所以「一天」不是唯一的计量单位;两次 4 小时的停机,损失是 2 × $2,113 = $4,226,已经超过 $3,600 的年冗余成本。判断时要把「事件的次数」和「每次的时长」分开算,而不是只算总停机天数。

Tidecast 的平衡点是 0.45 周/年,约 3 天。平均周损失 $2,657,12 月周损失 $8,060;如果一次停服发生在 12 月,平衡点降到 $1,200 ÷ $8,060 = 0.15 周,也就是一天多。Tidecast 的冗余价值高度集中在 Q4,这也是为什么演练要安排在 11 月之前,而不是 1 月。

Nord 的平衡点是 0.66 周/年,约 4.6 天。Nord 的周损失 $3,644 里,$3,001 是咨询计费,$643 是 SLA 抵扣;咨询计费占 82.4%。这意味着 Nord 的冗余重点不是服务器,而是「人」:合伙人互备、客户文档、项目交接清单。服务器冗余只能防住订阅计费那一小部分,防不住咨询停工。

写成代码:bcp.ts

下面的 bcp.ts 把停机损失拆成 outageLoss 和 assessBcp 两层。outageLoss 算一次停机的收入损失和 SLA 抵扣;assessBcp 把 RTO、RPO、计划事件和年冗余成本放在一起,算出 breakEvenEventsBps,即「一年要发生多少次计划事件,冗余成本才回本」的 bps 表示。

// file: src/contract/bcp.ts
// tier: L1
// verify: node --test test/contract/bcp.test.ts

import { type Cents, add } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";

export type BcpInput = {
  name: string;
  revenueCentsPerHour: Cents;
  availabilityBps: Bps;
  creditBps: Bps;
  rtoHours: number;
  rpoHours: number;
  reworkCostPerHourCents: Cents;
  planningEventHours: number;
  annualRedundancyCostCents: Cents;
  hoursPerMonth?: number;
};

export type OutageLoss = {
  hours: number;
  revenueLossCents: Cents;
  slaCreditCents: Cents;
  totalLossCents: Cents;
};

export type BcpSummary = {
  name: string;
  allowedDowntimeHours: number;
  rtoLossCents: Cents;
  rpoLossCents: Cents;
  totalAtRiskCents: Cents;
  planningEventLossCents: Cents;
  annualRedundancyCostCents: Cents;
  breakEvenEventsBps: Bps;
};

function validAmount(value: Cents): boolean {
  return Number.isInteger(value) && value >= 0;
}

function validRate(value: Bps): boolean {
  return Number.isInteger(value) && value >= 0 && value <= 10000;
}

function validHours(value: number): boolean {
  return Number.isFinite(value) && value >= 0;
}

/**
 * Downtime loss has two parts: revenue that does not arrive while the service
 * is down, and a service credit once downtime exceeds the availability
 * allowance. The credit is charged against one month of revenue, not against
 * the lost hours, because that is how the clause is written.
 */
export function outageLoss(
  revenueCentsPerHour: Cents,
  hours: number,
  availabilityBps: Bps,
  creditBps: Bps,
  hoursPerMonth: number,
): Result<OutageLoss> {
  if (!validAmount(revenueCentsPerHour) || revenueCentsPerHour === 0) {
    return fail("revenue per hour must be a positive integer amount in cents");
  }
  if (!validHours(hours) || hours === 0) {
    return fail("outage hours must be greater than zero");
  }
  if (!validRate(availabilityBps) || !validRate(creditBps)) {
    return fail("availability and credit must be between 0 and 10000 bps");
  }
  if (!validHours(hoursPerMonth) || hoursPerMonth === 0) {
    return fail("hours per month must be greater than zero");
  }

  const allowedHours = (hoursPerMonth * (10000 - availabilityBps)) / 10000;
  const revenueLossCents = Math.round(revenueCentsPerHour * hours);
  const slaCreditCents =
    hours > allowedHours
      ? applyRate(Math.round(revenueCentsPerHour * hoursPerMonth), creditBps)
      : 0;

  return ok({
    hours,
    revenueLossCents,
    slaCreditCents,
    totalLossCents: add(revenueLossCents, slaCreditCents),
  });
}

/**
 * A BCP is priced as the loss at RTO plus the cost of rebuilding the data
 * inside RPO. Redundancy is worth buying when the expected number of events
 * per year exceeds the break-even count.
 */
export function assessBcp(input: BcpInput): Result<BcpSummary> {
  if (input.name.trim() === "") {
    return fail("profile name cannot be empty");
  }
  if (!validAmount(input.reworkCostPerHourCents)) {
    return fail("rework cost must be a non-negative integer amount in cents");
  }
  if (!validAmount(input.annualRedundancyCostCents)) {
    return fail("redundancy cost must be a non-negative integer amount in cents");
  }
  if (!validHours(input.rtoHours) || input.rtoHours === 0) {
    return fail("RTO must be greater than zero");
  }
  if (!validHours(input.rpoHours)) {
    return fail("RPO must be a non-negative number");
  }
  if (!validHours(input.planningEventHours) || input.planningEventHours === 0) {
    return fail("planning event hours must be greater than zero");
  }

  const hoursPerMonth = input.hoursPerMonth ?? 720;
  const rto = outageLoss(
    input.revenueCentsPerHour,
    input.rtoHours,
    input.availabilityBps,
    input.creditBps,
    hoursPerMonth,
  );
  if (!rto.ok) {
    return rto;
  }
  const planning = outageLoss(
    input.revenueCentsPerHour,
    input.planningEventHours,
    input.availabilityBps,
    input.creditBps,
    hoursPerMonth,
  );
  if (!planning.ok) {
    return planning;
  }

  const rpoLossCents = Math.round(
    input.rpoHours * input.reworkCostPerHourCents,
  );
  const totalAtRiskCents = add(rto.value.totalLossCents, rpoLossCents);
  const breakEvenEventsBps =
    totalAtRiskCents === 0
      ? 0
      : divide(input.annualRedundancyCostCents, planning.value.totalLossCents);

  return ok({
    name: input.name,
    allowedDowntimeHours:
      (hoursPerMonth * (10000 - input.availabilityBps)) / 10000,
    rtoLossCents: rto.value.totalLossCents,
    rpoLossCents,
    totalAtRiskCents,
    planningEventLossCents: planning.value.totalLossCents,
    annualRedundancyCostCents: input.annualRedundancyCostCents,
    breakEvenEventsBps,
  });
}

把三家的数字换成美分代进去:

  • LedgerLite:每小时收入 2,781 美分,可用性 9,950 bps,抵扣 1,000 bps,RTO 4 小时,RPO 0.25 小时,重建成本 12,000 美分/小时,计划事件 24 小时,年冗余 360,000 美分。结果是 RTO 损失 211,356 美分($2,113.56),RPO 损失 3,000 美分($30),计划事件损失 266,976 美分($2,669.76),平衡点 13,484 bps,即 1.35 次/年。
  • Tidecast:每小时收入 1,582 美分,可用性 10,000 bps,抵扣 0,RTO 24 小时,RPO 24 小时,重建成本 0,计划事件 168 小时,年冗余 120,000 美分。结果是 RTO 损失 37,968 美分($379.68),RPO 损失 0,计划事件损失 265,776 美分($2,657.76),平衡点 4,515 bps,即 0.45 次/年。
  • Nord:每小时收入 1,786 美分,可用性 9,900 bps,抵扣 500 bps,RTO 8 小时,RPO 4 小时,重建成本 9,000 美分/小时,计划事件 168 小时,年冗余 240,000 美分。结果是 RTO 损失 78,584 美分($785.84),RPO 损失 36,000 美分($360),计划事件损失 364,344 美分($3,643.44),平衡点 6,587 bps,即 0.66 次/年。

代码只算单次事件,不处理事件之间的相关性,也不处理停机后的客户流失。一次持续一周的停机,损失可能不止一周的收入:客户会重新评估供应商,续约率会下降。这部分「声誉损失」很难用 BCP 公式算,但可以用情景分析单独估。代码的用处是把「能算的部分」算准,让「不能算的部分」不至于被用来掩盖「不想算的部分」。

小结

  • BCP 的四个要素是:关键业务识别、RTO/RPO、替代方案、演练;四者对应四个不同的问题。
  • 关键业务要窄:LedgerLite 是计费与对账 API,Tidecast 是商店页面与下载,Nord 是咨询交付与订阅计费。
  • RTO/RPO 要写成小时数并配现金影响;本节的 4 小时/15 分钟、24 小时/24 小时、8 小时/4 小时都是教学假设。
  • LedgerLite 停机 1 天损失 $2,669,其中 $2,002 是 SLA 抵扣,占 75.0%;RTO 4 小时损失 $2,113。
  • Tidecast 停服 1 周平均损失 $2,657,12 月一周 $8,060;没有 SLA 抵扣,损失跟销量走。
  • Nord 停工 1 周损失 $3,644,其中咨询计费 $3,001,占 82.4%;RTO 8 小时损失 $786。
  • 年冗余成本除以单次事件损失,得到平衡次数:LedgerLite 1.35 天/年,Tidecast 0.45 周/年,Nord 0.66 周/年。
  • 代码只算单次事件,不处理相关性和停机后的客户流失;后者要用情景分析单独估。

阅读导航:上一节:12.1 风险登记册:概率、影响、责任人 · 下一节:12.3 保险、备份与应急预案的联动 。

常见错误与边界

  1. 把 BCP 写成抽屉文档。 没有触发条件、责任人、演练记录的 BCP,出事时没有人知道该启动哪一页。
  2. 关键业务列得太宽。 把「所有业务」都列为关键,等于没有优先级;关键业务要窄到能在一小时内判断。
  3. RTO/RPO 只写目标不写钱。 「尽快恢复」不能比较方案;要写出 4 小时、24 小时各自对应多少现金损失。
  4. 忽略 SLA 抵扣。 LedgerLite 的抵扣占停机损失的 75.0%;只算收入损失会低估停机成本。
  5. 把 SLA 抵扣当成收入损失。 抵扣是要退回去的钱,收入损失是没赚到的钱;两者口径不同,但都影响现金。
  6. 替代方案没有触发条件。 写了「切到备用区域」但没写「什么时候切」,出事时就会犹豫到超过 RTO。
  7. 只做桌面推演。 念文档不能验证备份是否可恢复;演练要真的切换一次,记录实际小时数。
  8. 用平均周损失做旺季决策。 Tidecast 12 月一周的损失是平均周的 3 倍;冗余维护窗口要避开旺季。
  9. 把冗余成本当成纯支出。 冗余买的是「停机次数的减少」;用年成本除以单次损失,才能判断回本点。
  10. 用代码替代演练。 代码算的是假设下的损失;真实 RTO 只能通过演练测出来。

本节的边界是「单次停机的现金影响与冗余回本点」:只覆盖关键业务、RTO/RPO、替代方案和演练四个要素,不展开灾备架构、供应商合同谈判和保险理赔。所有 RTO/RPO、重建成本和冗余成本比例都是教学假设;真实目标要按业务节奏和合同条款设定。停机后的保险覆盖见 12.3 保险、备份与应急预案的联动 ;风险如何排序见 12.1 风险登记册:概率、影响、责任人 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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