目录

为可重建而设计

4 5.7~7.3 分钟 2564

前言

人很容易把"系统搭好了、跑起来了"当作终点。但一台长期运行的机器,真正的考验从来不在它能不能跑,而在某天你要换一台机器、或者它意外损坏时,你能不能从容地把它重新立起来。

我自己经历过反面:一套慢慢长出来的系统,各部分彼此缠绕、状态藏在角落、没有一句话能说清它到底是怎么跑起来的。等到要动它,才发现它早已长成一团谁都不敢碰的线缆。那一次之后,我给自己换了一个目标——

搭建系统时,真正要交付的,不是那个正在运行的实例,而是随时把它重建出来的能力

这句话听起来抽象,但它能被翻译成具体的设计决策。下面是这些年我个人的一些经验。


把"配方"和"积累"分开

如果只能记住一条,那就是这条。

把一个系统里的东西分成两类,并且严格地不让它们混在一起:

  • 一类是"它怎么跑"——声明式的配置、构建方式、参数。它是一份"配方",是可以从头写出来的,因此它应该被纳入版本控制,像代码一样被审视和回滚。

  • 另一类是"它积累了什么"——运行中长出来的状态:数据、登录态、上传的文件、签发的证书。这一类无法被"写"出来,只能被保存,因此它是唯一真正需要备份的东西。

一旦这条分界被严格守住,"迁移"这件让人头疼的事,就退化成了三个动作:取回配方、取回积累、按配方重新启动。中间所有负责"计算"的部分——容器、进程、运行环境——都变成了可以随时丢弃、随时重建的东西,因为它们不承载任何不可恢复的价值。

这条分界还有一个常被忽略的另一半:配方本身也要可复现,而不只是"存在"。 一份指向"最新版本"的配方,今天和明天会装出两台不同的机器。所以凡是会随时间漂移的东西,都应当被钉死:版本写明、记录在案。钉死版本不是保守,而是让"曾经能跑的状态"可以被精确重建,也让一个解决过一次的问题,不会在下次重建时原样复发。升级因此变成一个主动的、带着备份去做的决定,而不是某次随手更新的副作用。


把约束当作设计的输入

任何真实的环境都带着一组你改不了的硬约束:一个不稳定或受限的网络、一条平台层面的限制、一份吃紧的资源预算。

不成熟的做法是与约束较劲——想方设法绕过它、对抗它,最后写出一堆脆弱的、随时会断的临时手段。成熟的做法是把约束当作固定的输入,让架构顺着它的形状生长。

举两个抽象的例子。

当一个平台只允许某一种方式访问、堵死其余所有入口时,与其为每个服务单独想办法对外,不如顺势把所有对外流量收敛到唯一的一个入口,让它统一对外、统一调度,其余一切都躲在它身后。一条限制,反而催生了一个更干净的架构。

当你依赖的上游不稳定、时快时慢、时断时续时,正确的反应不是寄希望于它某天变好,而是默认它会失败,于是给关键链路都铺好本地的、冗余的中转,并且接受这些中转本身也会接连失效——所以一次就配好多个兜底。把"会坏"写进设计,比假设"不会坏"要可靠得多。

这里还藏着一条容易被教条化的反例:原则不是教条。我惯用某一套做法,但当它在某个具体场景下恰恰是最差选择时,就该毫不犹豫地换掉。判断的依据永远是当下的实测,而不是"我一向都这么做"。


让系统温和地退化

在为资源耗尽做防护时,有两种思路。

一种是设一道硬上限:到顶就拒绝、就杀掉。它简单,但它的代价是不可控——当压力真的到来,被牺牲掉的往往不是你想牺牲的那个,而是恰好撞在枪口上的无辜者,而且毫无缓冲、当场崩断。

另一种是留一道缓冲:让系统在尖峰来临时先有地方腾挪,把"骤然的崩断"变成"短暂的迟缓",撑过尖峰再恢复。再配上一条克制的策略——只在真有压力时才动用这道缓冲,平时让它退回纯粹的保险,不去打扰正常运行。

这条思路可以推广到防护设计的方方面面:会弯曲的软护栏,优于会折断、还顺手带倒别人的硬护栏。一个系统在意外面前温和地退化,几乎总好过它整齐地崩溃


隔离是有代价的

把一个系统拆成互相隔离的单元,通常是出于好的理由——为了安全、为了模块化、为了让各部分可以独立替换。但隔离从来不是免费的,它的代价常常隐蔽:

当你把曾经住在一起的东西分开时,你也切断了它们之间那层默认共享的上下文

过去之所以"什么都不用做就能正常工作",恰恰是因为它们共处一地、隐式地共享着同一份环境。一旦拆开,这层隐式的依赖就断了,而最棘手的是——它的断裂只在拆分之后才会暴露,在拆分之前一切看起来都好好的,于是你毫无防备。

正确的应对是:凡是跨越隔离边界传递的东西,都要给它一条显式的通道。要么把它完整地"按值"传过去、不依赖对方能访问你的环境;要么有意识地在双方之间架起一条共享的、约定好的管道。隔离设计真正的功夫,不在切得多干净,而在切开之后,把每一条本来隐式的依赖重新显式地接好。


减法是一种克制

面对一个需求,最容易的反应是去找"功能最全、社区最大、看起来最专业"的那个方案。但在一个资源吃紧的环境里,默认的重量级方案往往是错的

每引入一样东西,都要问它配不配得上这份预算。一个无状态、轻量、恰好够用的选择,在受限环境里不是将就,而是更对的设计。监控与可观测性这类东西尤其需要警惕——稍不留神,你会发现自己花在"观察系统"上的资源,比花在"运行系统"上的还多,监控本身成了压垮机器的那根稻草。

做减法是一种需要克制力的设计能力:抵抗住"既然装了不妨多装点"的诱惑,让系统只保留它真正需要的重量。


一次只做一步

这一条是关于认知方法的,也是我返工最少的来源。

文档会过期,记忆会失真,而正在运行的代码和真实的日志不会说谎。当某样东西的行为出乎意料时,最快的路不是去翻一份可能早已陈旧的说明、或者凭印象下判断,而是直接去读它此刻实际跑着的那份代码、去看真实的状态和日志,再据此决定。对一件事很熟悉,不等于你对它的认识是当下的——很多坑就栽在"我以为它还是那样"上。

与之配套的是一条更朴素的工作纪律:一次只推进一个可验证的步骤。 不要一口气甩出一长串未经验证的动作,而是改一处、看真实结果、再据结果决定下一步。这样每一步的爆炸半径都很小,反馈回路很短,错了也好回退。慢,往往是更快的那条路。


把散落的收拢起来

让一个系统"搬不动"的,往往不是它的主体,而是那些散落在各处的边角:随手挂在角落里的定时任务、手动放置又没人记得的配置、藏在隐藏目录里的状态。它们看不见,所以没人敢动,于是整个系统跟着一起变得不可触碰。

所以"让系统可重建"这件事,很大一部分工作其实是把散落的东西收拢到一处可见、可管理的地方——让调度有统一的入口,让配置有明确的归属,让状态集中在那棵唯一需要备份的树里。可见性本身就是可迁移性的前提。

安全姿态可以看作这条原则的一个推论:把不该对外的东西严格地关在内部,只留一个经过审视的对外入口,其余一律不暴露。攻击面收窄,系统也跟着变得更好理解。一个易于理解的系统,几乎总是一个更安全、也更容易重建的系统。


可重建是一种架构

回到开头那句话。这套方法论真正的产出,不是任何一个具体跑着的服务——那些都是可以被丢弃、被重新立起来的。真正的产出,是那条贯穿始终的分界,和它带来的一种状态:系统始终清楚自己由什么构成、缺了什么、可以从哪里重新长出来。

还有最后一块,常被排除在"系统"之外,其实是它的一部分——那份记录决策与理由的文档。一个你能重建的系统,必须连同"它为什么是这个形状"的知识一起被保存下来;否则你重建出的只是形状,而不是它背后的判断。文档不是工程的附属品,它是架构的最后一块。

说到底,把"可重建"放在第一位,并不只是为了某一次迁移更省事。它是为了让你能在很长的时间里,不断地造东西、改东西、推倒重来,而每一样都不至于在日积月累中,慢慢变成压在自己身上、再也搬不动的负担。能轻装地一直做下去,本身就是目的。