软件可靠性的代价,是对极致简洁的追求
软件系统本质上是动态的,也因此天然并不稳定。对于 SRE(站点可靠性工程)而言,提升软件可靠性,并不只是增加更多流程、工具或代码,而是持续在系统稳定性、开发敏捷性与架构简洁性之间寻找平衡。本文将从复杂性治理、无用代码清理、极简 API、模块化设计和简化发布等角度,讨论为什么软件简洁性是系统可靠性的前提。
一个软件系统只有在与外界完全隔绝时,才可能保持绝对稳定:如果我们停止修改代码库,就不会再引入新的缺陷;如果底层硬件和依赖库永远不变,它们也不会成为新的故障来源;如果用户规模被永久固定,系统也就永远不需要扩展。

软件可靠性的代价,是对极致简洁的追求
事实上,SRE 管理系统的核心思想正是:我们的工作,说到底就是在系统的敏捷性与稳定性之间维持平衡。
系统稳定性与开发敏捷性
为了获得敏捷性,有时牺牲一定的稳定性是合理的。我经常通过所谓的“探索性编码”来处理陌生领域:为自己编写的代码设定明确的有效期,并承认只有先尝试一次,哪怕失败,才能真正理解接下来需要完成什么。
带有有效期的代码,可以在测试覆盖和发布管理上保持更大的灵活性,因为它们不会被部署到生产环境,也不会被用户看到。
对于大多数生产软件系统而言,我们希望稳定性与敏捷性能够达到某种平衡。SRE 致力于建立流程、实践和工具,以提升软件系统的可靠性;同时,SRE 也会确保这些工作尽量少影响开发人员的敏捷性。
事实上,SRE 的经验表明,可靠的流程往往反而能够提升开发效率:快速且可靠的生产部署,使生产环境中的变更更容易被识别和追踪。因此,一旦出现 bug,我们就能更快定位并修复。将可靠性融入开发流程,可以让开发人员把精力集中在真正重要的事情上——软件和系统的功能与性能。
在实践层面,这也要求研发团队尽量让需求、开发、测试、发布和知识沉淀形成可追踪的闭环。例如,PingCode 这类智能化研发管理工具,可以帮助团队把目标制定、客户反馈、需求清理、评审排期、项目开发、测试发布和 Wiki 知识沉淀串联起来,让研发管理更加自动化、数据化和智能化,从而减少流程割裂带来的额外复杂性。
“无聊”的优点:可预测的软件更可靠
与生活中几乎所有其他事物不同,在软件领域,“无聊”其实是一种优势。我们并不希望程序充满自发性和戏剧性;我们希望它们按部就班地执行既定脚本,并按预期完成业务目标。
正如海外某公司工程师曾说:“不同于侦探小说,源代码如果缺乏刺激、悬念和谜题,反而是一种理想特性。” 对 SRE 来说,生产环境中的意外情况正是最大的敌人。
正如弗雷德·布鲁克斯在《没有银弹》(No Silver Bullet)[Bro95] 一文中指出的,区分本质复杂性与偶然复杂性至关重要。
本质复杂性,是指特定问题中固有的、无法从问题定义中消除的复杂性;而偶然复杂性则更具可变性,通常可以通过工程手段加以削减。例如,编写一个 Web 服务器,需要处理如何快速提供网页服务这一类本质复杂性。然而,如果我们用 Java 编写 Web 服务器,为了尽量减少垃圾回收对性能的影响,就可能引入额外的偶然复杂性。
为了尽量减少偶然复杂性,SRE 团队应该做到:
当偶然复杂性被引入其负责的系统时,及时识别并加以抵制。
持续清理其负责系统中的复杂性,并承担相应的运营责任。
删除无用代码:不要舍不得你的代码
工程师也是人,往往会对自己的作品产生情感依恋。因此,大规模清理源代码树时引发冲突并不罕见。
有人可能会抗议:“如果以后还需要这段代码怎么办?”“为什么不把它注释掉,以后再加回来不是更方便吗?”或者“为什么不加一个标志位来控制它,而要直接删除呢?”
这些建议都并不可取。源代码控制系统本来就让回滚变得简单;而数百行被注释掉的代码,只会造成干扰和混乱,尤其是在源文件不断演进的情况下。至于那些被标志位控制、永远不会执行或始终处于禁用状态的代码,则像一颗随时可能爆炸的定时炸弹。海外某金融机构就曾因类似问题付出惨痛代价,相关事件也曾被海外监管机构记录在案。
这听起来或许有些极端,但对于一个需要全天候运行的 Web 服务来说,任何新增代码在某种程度上都可能成为一种风险。SRE 所倡导的实践,正是为了确保所有代码都服务于核心功能:仔细审查代码,确认它确实服务于业务目标;定期移除无用代码;并在不同层级的测试中建立机制,识别和控制代码膨胀。
用“负代码行数”控制软件膨胀
“软件膨胀”一词用于描述这样一种趋势:随着新功能不断加入,软件逐渐变得迟缓、庞大而笨重。
软件膨胀本身已经令人反感,而从 SRE 的角度来看,它的负面影响更为明显:项目中每新增或修改一行代码,都可能引入新的缺陷和漏洞。规模较小的项目更容易理解,更容易测试,通常也更少出错。
基于这一点,当我们想为项目添加新功能时,或许应该更加克制。我做过的最令人满足的编码工作之一,就是在代码不再有用时,一次性删除数千行代码。
极简 API:少即是多的软件设计原则
法国作家安托万·德·圣埃克苏佩里写道:“完美并非在无可增加时达成,而是在再无可删时实现。”[Sai39] 这一原则同样适用于软件的设计与构建。API(应用程序接口)正是这一原则的鲜明体现。
编写清晰、简洁的 API,是管理软件系统简洁性的关键。我们提供给 API 使用者的方法和参数越少,API 就越容易理解,我们也越能集中精力打磨这些方法。
这再次说明:有意识地选择不解决某些问题,能够帮助我们专注于核心问题,并显著提升我们明确承诺解决的问题的质量。在软件开发中,少即是多。一个小而简洁的 API,往往也意味着设计者对问题有更深入的理解。
模块化设计:降低分布式系统复杂性
从 API 和单个二进制文件扩展到分布式系统,许多适用于面向对象编程的经验法则,同样适用于分布式系统设计。能够独立修改系统的不同部分,对于构建可维护系统至关重要。
具体来说,二进制文件之间、二进制文件与配置之间保持松耦合,是一种重要的简化模式:它既能提升开发人员的敏捷性,也有助于保障系统稳定性。如果在大型系统的某个组件中发现 bug,就可以单独修复该组件,并独立于系统其他部分部署到生产环境。
API 所提供的模块化特性相对容易理解,但模块化同样也体现在 API 的变更方式上,而这一点往往不那么显而易见。一次 API 变更,可能迫使开发人员重建整个系统,并带来引入新 bug 的风险。
API 版本控制则允许开发人员继续使用系统当前依赖的版本,同时以安全、可控的方式逐步升级到新版本。这样,不同系统可以拥有各自的发布节奏,而不必在每次添加或改进功能时,都对整个系统进行一次完整的生产发布。
随着系统日益复杂,API 与二进制文件之间的职责分离也变得越来越重要。这一点与面向对象中的类设计直接对应:就像编写一个塞满无关函数的“杂物袋”类是一种糟糕实践一样,创建并部署一个名为“工具”或“杂项”的二进制文件,也同样不是好的设计。
设计良好的分布式系统,应当由多个协作组件组成,每个组件都有清晰而明确的职责边界。对团队协作而言也是如此:任务、项目、文档、目标、日历和审批如果分散在多个割裂工具中,也会增加管理复杂度;像 Worktile 这样的通用项目协作系统,则可以帮助团队把这些协作要素统一到同一工作空间中,让职责、进度和上下文更加清晰。
模块化的概念同样适用于数据格式。某些广泛使用的序列化协议,其核心优势和设计目标之一,就是创建一种同时具备向前兼容性与向后兼容性的传输格式。
简化发布:小批量变更更容易保障可靠性
一般而言,简单的发布优于复杂的发布。衡量并理解单个变更的影响,远比衡量和理解一批同时发布的变更要容易得多。
如果我们一次性向系统发布 100 个互不相关的变更,而系统性能随后下降,那么要弄清楚到底哪些变更影响了性能,以及它们是如何产生影响的,将需要大量工作,或者依赖额外工具。
如果采用更小批次的发布方式,我们就能更快、更有信心地推进。因为每一次代码变更,都可以放在更大的系统背景中被独立理解。
这种发布方式有些类似于机器学习中的梯度下降法:每次只迈出一小步,观察这一步究竟让系统变得更好,还是更差。
结论:软件简洁性是可靠性的前提
本章反复强调同一个主题:软件简洁性是可靠性的前提。
我们追求简洁,并不是因为偷懒,而是因为我们在认真思考如何简化特定任务中的每一个步骤。换句话说,我们是在明确自己真正想达成的目标,并寻找实现它的最轻量路径。
每一次我们对某个功能说“不”,都不是在限制创新;恰恰相反,这是在保持开发环境的整洁,减少不必要的干扰,从而确保我们能够把注意力集中在真正重要的创新上,并扎扎实实地完成工程工作。
作者提示含AI生成内容。
