好的架构就是刚刚好:不多不少

3 阅读5分钟

前几篇一直在修问题、改代码。这篇停下来想想:到底什么是好的架构。

如果让我用一句话回答"架构是什么",我的理解是:架构是管理复杂度的方式。

管理复杂度

一个系统要做的事有多复杂,它就有多复杂——这是客观的。架构不是消除复杂度,而是让复杂度可控——不会因为功能增加而失控,不会因为一处改动而牵一发动全身。

把复杂度装进盒子

好的架构把复杂度装进盒子里。每个盒子对外只露出简单的接口,内部的复杂度被藏起来。用一个模块的时候,你不需要知道它怎么实现的,只需要知道它的接口。

但隐藏不是转移。

转移是把复杂度从一个地方搬到另一个地方,问题没有真正解决。最常见的例子是不合理的依赖——A 依赖 B,B 依赖 A,形成循环。有人加一个 C 做中间层,看起来循环没了,但 C 只是个垃圾桶,把两边不该耦合的东西都塞了进去。复杂度没有消失,只是换了个藏身之处。

真正的隐藏,盒子里面也是清晰的。

这很重要——因为模块不是封印,你总有一天要打开它:debug 的时候、优化的时候、需求变了要改内部逻辑的时候。如果里面是清晰的,打开改完再合上就好。如果里面是混乱的,一打开就像潘多拉的盒子,复杂度涌出来,你甚至不敢动,怕改了不知道会坏什么。

所以好的隐藏是两层的:对外简单,对内清晰。

解决实际问题

好的架构不是凭空设计出来的,而是从解决实际问题中长出来的。

一个典型的例子是 nginx。2000 年代初,互联网面临 C10K 问题——如何让一台服务器同时处理上万个并发连接。传统的每连接一线程模型在这个量级上撑不住:线程切换的开销、内存的消耗,让服务器在连接数上去之后迅速崩溃。nginx 用事件驱动架构回应了这个挑战——单线程通过 epoll/kqueue 管理大量连接,不为每个连接分配独立线程,把资源消耗降到了极低的水平。

更重要的是,nginx 解决的不只是 C10K 这一个问题,而是"高效管理大量并发连接"这一类问题。所以同样的架构能跑 HTTP、mail、stream,能承载几百个功能模块而不失控。好的架构提供的是框架,不是针对某个具体需求的特解。

好架构的效果

好的架构最终达到三个效果:

稳定性。 加一个功能,复杂度只在局部增长,不扩散到整个系统。nginx 能有几百个模块,整体复杂度仍然可控,就是因为模块系统把每个功能的复杂度限制在了自己的范围内。

对使用者简单。 写一个 nginx 模块,不需要知道事件循环怎么工作;在 jsbench 里调 fetch(),不需要知道 epoll 和连接状态机。好的架构让每一层的使用者都觉得"这很简单"。

局部性。 理解一个模块不需要理解整个系统,修改一个模块不会波及其他模块。开发者的注意力是有限的,好的架构把有限的注意力用在最该用的地方。

但要达到这些效果,需要恰到好处的设计。设计不足和过度设计,都会影响复杂度。

设计不足让复杂度到处泄漏。没有模块边界,没有接口抽象,所有代码搅在一起。加一个功能要读懂半个系统,改一个地方不知道会影响哪里。jsbench 第一版的 fetch() 就是例子——网络 I/O、DNS 解析、事件循环、Promise 包装全塞在一个 400 多行的函数里,没有任何分层。

过度设计让复杂度凭空增加。问题本来很简单,但为了"扩展性"加三层抽象、两个接口、一个工厂模式。代码量翻了几倍,解决的还是同一个问题。更糟的是,这些抽象在需求真正变化的时候往往不好用——因为你猜错了变化的方向。

好的架构在两者之间:刚好足够,不多不少。

"Make it work, make it right, make it fast." — Kent Beck

第二篇引用过这句话,当时只展开了 make it work 和 make it right。这篇补上 make it fast。Make it fast 不只是性能优化,更准确地说,是在前两步做好之后再去解决性能问题——算法优化、数据结构调整、减少内存分配。关键是顺序:先做对,再做快。在架构清晰的系统上做优化,比在一坨混乱的代码上做要容易得多。

这三步的核心其实是 make it right。Work 是起点,fast 是锦上添花,right 才是决定一个软件能走多远的东西。

在我的印象中,nginx 社区的工程师们都很擅长 make it right and make it fast。其中 Igor Sysoev 尤其擅长通过设计来简化复杂问题——nginx 和 unit 的架构就是最好的例子。接下来我打算把 nginx unit 的事件引擎引入 jsbench。

当然,知易行难。这些道理说起来简单,好的架构终究要在实践中打磨。


GitHub: github.com/hongzhidao/…

更多文章和后续更新,关注微信公众号:程序员洪志道