以下情境为教学假设,人物与数字不代表经核实的真实案例。
业务连续性计划(BCP)常被写成一份放在抽屉里的文档:机房着火怎么办、云供应商跑路怎么办、合伙人住院怎么办。写完就算交差,直到真的出事,才发现文档里没有一个人名、没有一个电话号码、没有一条能在一小时内执行的替代路径。
对独立开发者,BCP 的价值不在「写得全」,而在「算得清」。停一天损失多少、停一周损失多少、恢复要多久、丢多少数据、买冗余划不划算——这些都能用收入、SLA 和备份成本算出来。算不出来的 BCP,和没有 BCP 的区别,只是多了一份心理安慰。
BCP 的四个要素
BCP 有四个要素:关键业务识别、RTO/RPO、替代方案、演练。四者对应四个不同的问题,不能互相替代。
| 要素 | 回答的问题 | 现金口径 | 常见失败 |
|---|---|---|---|
| 关键业务识别 | 哪些业务一停就立刻少收钱 | 日收入、周收入 | 把「所有业务」都列为关键 |
| RTO/RPO | 多久恢复、最多丢多少数据 | 停机损失 + 重建成本 | 只写目标,不写对应的钱 |
| 替代方案 | 主路径断了走哪条 | 替代方案年成本 | 写了方案,没有触发条件 |
| 演练 | 方案是否真的可用 | 演练成本 + 发现缺口 | 只做桌面推演,不碰真实切换 |
关键业务识别要窄。LedgerLite 的关键业务不是「整个产品」,而是「计费与对账 API」:客户可以一天不登录,但不能一天不能对账。Tidecast 的关键业务不是「游戏本身」(单机部分在玩家机器上),而是「商店页面与下载」:页面挂了,新玩家买不到,收入立刻归零。Nord 的关键业务是「咨询交付与订阅计费」:咨询是 72.7% 的收入,订阅是剩下的 27.3%,两者停机的影响不同,但都不能停一周。
RTO(恢复时间目标)和 RPO(恢复点目标)要写成小时数,并且配一个现金影响。只写「尽快恢复」没有意义,因为「尽快」不能用来比较两条方案。下面是三家的教学假设:
| 案例 | 关键业务 | RTO | RPO | 替代方案 | 演练频率 |
|---|---|---|---|---|---|
| LedgerLite | 计费与对账 API | 4 小时 | 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 |
| LedgerLite | RTO 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 |
| Nord | RTO 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,669 | 1.35 天 | 一年停机超过 1.35 天,冗余划算 |
| Tidecast | $1,200 | $2,657 | 0.45 周 | 一年停服超过 0.45 周(约 3 天),冗余划算 |
| Nord | $2,400 | $3,644 | 0.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 保险、备份与应急预案的联动 。
常见错误与边界
- 把 BCP 写成抽屉文档。 没有触发条件、责任人、演练记录的 BCP,出事时没有人知道该启动哪一页。
- 关键业务列得太宽。 把「所有业务」都列为关键,等于没有优先级;关键业务要窄到能在一小时内判断。
- RTO/RPO 只写目标不写钱。 「尽快恢复」不能比较方案;要写出 4 小时、24 小时各自对应多少现金损失。
- 忽略 SLA 抵扣。 LedgerLite 的抵扣占停机损失的 75.0%;只算收入损失会低估停机成本。
- 把 SLA 抵扣当成收入损失。 抵扣是要退回去的钱,收入损失是没赚到的钱;两者口径不同,但都影响现金。
- 替代方案没有触发条件。 写了「切到备用区域」但没写「什么时候切」,出事时就会犹豫到超过 RTO。
- 只做桌面推演。 念文档不能验证备份是否可恢复;演练要真的切换一次,记录实际小时数。
- 用平均周损失做旺季决策。 Tidecast 12 月一周的损失是平均周的 3 倍;冗余维护窗口要避开旺季。
- 把冗余成本当成纯支出。 冗余买的是「停机次数的减少」;用年成本除以单次损失,才能判断回本点。
- 用代码替代演练。 代码算的是假设下的损失;真实 RTO 只能通过演练测出来。
本节的边界是「单次停机的现金影响与冗余回本点」:只覆盖关键业务、RTO/RPO、替代方案和演练四个要素,不展开灾备架构、供应商合同谈判和保险理赔。所有 RTO/RPO、重建成本和冗余成本比例都是教学假设;真实目标要按业务节奏和合同条款设定。停机后的保险覆盖见 12.3 保险、备份与应急预案的联动 ;风险如何排序见 12.1 风险登记册:概率、影响、责任人 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。