为什么有的架构师设计方案精美却最终失败?为什么技术最强的团队反而做不出最好的系统?答案藏在架构设计的三大原则之中:合适、简单、演化。这三个原则不是空洞的理论,而是大量成功与失败案例的总结。理解它们,才能真正做出好的架构设计。
编程的结果是确定的——同样的代码,无论谁写、何时执行,结果都应该一致。但架构设计截然不同:同样的系统,A 公司和 B 公司做出来的架构可能差异很大;同样的方案,A 设计师认为该这样做,B 设计师认为该那样做,看起来都有道理。架构设计没有像编程语言那样的语法约束,更多时候是面对多种可能性进行选择。
这种不确定性让架构师常常陷入两难:选最先进的技术还是团队最熟悉的技术?选 Angular 还是 React?选 MySQL 还是 MongoDB?淘宝的电商架构很完善,新做一个电商网站,是否照搬淘宝就可以了?这些困惑的背后,是因为架构设计领域并没有一套通用规范来指导架构师进行选择。
但通过研究架构设计的发展历史、多个公司的架构演进过程(QQ、淘宝、Facebook 等),我发现有几个共性原则隐含其中:合适原则、简单原则、演化原则。
合适原则宣言:"合适优于业界领先"。
优秀的技术人员都有很强的技术情结,做方案或架构时总想达到甚至优于业界领先水平——这样才显得优秀,才能在 KPI 总结里骄傲地写上"设计了 XX 方案,达到了和 Google 相同的技术水平"。但现实是,大部分这样想和这样做的架构,最后都以失败告终。
1. 将军难打无兵之仗
大公司分工细,一个小系统可能就是一个小组负责。比如某个通信大厂做一个 OM 管理系统就有十几个人,阿里的中间件团队有几十个人。而大部分公司整个研发团队可能就 100 多人,某个业务团队可能就十几个人。十几个人的团队,想做几十个人的团队的事情,难度可想而知。没那么多人,却想干那么多活,是失败的第一个主要原因。
2. 罗马不是一天建成的
业界领先的方案并不是一堆天才某个时期灵机一动、加班加点就做出来的,而是经过几年时间的发展才逐步完善和初具规模的。阿里中间件团队 2008 年成立,发展到现在已经十几年了。我们只知道他们抗住了多少次"双 11",做了多少优秀的系统,但经历了什么样的挑战、踩了什么样的坑,只有他们自己知道。没有那么多积累,却想一步登天,是失败的第二个主要原因。
3. 冰山下面才是关键
业界领先的方案其实都是"逼"出来的。业务发展到一定阶段,量变导致质变,出现了新的问题,已有的方式不能应对,需要用新方案来解决,通过创新和尝试,才有了业界领先的方案。GFS 为何在 Google 诞生,而不是在 Microsoft 诞生?因为 Google 有那么庞大的数据,而不是因为 Google 的工程师比 Microsoft 的工程师更聪明。没有那么卓越的业务场景,却幻想灵光一闪成为天才,是失败的第三个主要原因。
真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地。这也是很多 BAT 出来的架构师到了小公司或者创业团队反而做不出成绩的原因——没有了大公司的平台、资源、积累,只是生搬硬套大公司的做法,失败的概率非常高。
简单原则宣言:"简单优于复杂"。
软件架构设计是一门技术活。从历史上看,无论是瑞士的钟表、瓦特的蒸汽机、莱特兄弟发明的飞机,还是摩托罗拉发明的手机,无一不是越来越精细、越来越复杂。因此当我们进行架构设计时,会自然而然地想把架构做精美、做复杂,这样才能体现技术实力,也才能够将架构做成一件艺术品。
由于软件架构和建筑架构表面上的相似性,我们也会潜意识地将对建筑的审美观点移植到软件架构上。我们惊叹于长城的宏伟、泰姬陵的精美、悉尼歌剧院的艺术感、迪拜帆船酒店的豪华感,因此对于我们自己亲手打造的软件架构,我们也希望它宏伟、精美、艺术、豪华。
团队的压力也会促进我们走向复杂的方向,因为大部分人在评价一个方案水平高低时,复杂性是其中一个重要参考指标。例如设计一个主备方案,如果你用心跳来实现,可能大家都认为这太简单了。但如果你引入 ZooKeeper 来做主备决策,可能很多人会认为这个方案更加"高大上"。真正理解 ZAB 协议的人很少,但并不妨碍我们都知道 ZAB 协议很优秀。
这些原因会促使初出茅庐的架构师不自觉地追求架构的复杂性。然而,"复杂"在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,复杂代表的是问题。
结构复杂的系统几乎毫无例外具备两个特点:组成复杂系统的组件数量更多,同时这些组件之间的关系也更加复杂。
以组件故障率为例,假设组件的故障率是 10%(有 10% 的时间不可用),那么有 3 个组件的系统可用性是 (1-10%) × (1-10%) × (1-10%) = 72.9%,有 5 个组件的系统可用性是 (1-10%) × (1-10%) × (1-10%) × (1-10%) × (1-10%) = 59%。两者可用性相差 13%。
结构复杂性还存在其他问题:某个组件改动会影响关联的所有组件,这些被影响组件会递归影响更多组件;定位问题比简单系统更加困难,因为组件多,每个组件都有嫌疑,而组件间关系复杂,表现故障的组件可能并不是真正问题的根源。
除了结构复杂性,还有逻辑复杂性。如果某个组件的逻辑太复杂,一样会带来各种问题。逻辑复杂的组件,典型特征就是单个组件承担了太多的功能。以电商业务为例,常见的功能有:商品管理、商品搜索、商品展示、订单管理、用户管理、支付、发货、客服……把这些功能全部在一个组件中实现,就是典型的逻辑复杂性。
逻辑复杂几乎会导致软件工程每个环节都有问题:如果淘宝将这些功能全部在单一的组件中实现,系统会很庞大,可能是上百万、上千万的代码规模,"clone"一次代码要 30 分钟;几十、上百人维护这一套代码,某个"菜鸟"不小心改了一行代码,可能导致整站崩溃。
为什么复杂的电路意味着更强大的功能,而复杂的架构却有很多问题?根本原因在于电路设计好后进入生产就不会再变,复杂性只在设计时带来影响;而软件系统在投入使用后,后续还有源源不断的需求要实现,复杂性在整个系统生命周期中都有很大影响。
无论是结构的复杂性,还是逻辑的复杂性,都会存在各种问题,所以架构设计时如果简单的方案和复杂的方案都可以满足需求,最好选择简单的方案。《UNIX 编程艺术》总结的 KISS(Keep It Simple, Stupid!)原则一样适用于架构设计。
演化原则宣言:"演化优于一步到位"。
软件架构从字面意思理解和建筑结构非常类似,维基百科对"软件架构"的定义中有一段话描述了这种相似性:从和目的、主题、材料和结构的联系上来说,软件架构可以和建筑物的架构相比拟。然而,字面意思上的相似性却掩盖了一个本质上的差异:建筑一旦完成(甚至一旦开建)就不可再变,而软件却需要根据业务的发展不断地变化。
古埃及的吉萨大金字塔,4000 多年前完成的,到现在还是当初的架构。中国的明长城,600 多年前完成的,现在保存下来的长城还是当年的结构。美国白宫,1800 年建成,200 年来进行了几次扩展,但整体结构并无变化。
对比一下 Windows 系统的发展历史:Windows 1.0(1985 年)和 Windows 8(2012 年),如果对比这两个系统的架构,会发现它们其实是两个不同的系统了。Android 的发展历史同样如此,Android 6.0 和 Android 1.6 的差异也很大。
对于建筑来说,永恒是主题;而对于软件来说,变化才是主题。软件架构需要根据业务的发展而不断变化。设计 Windows 和 Android 的人都是顶尖的天才,即便如此,他们也不可能在 1985 年设计出 Windows 8,不可能在 2009 年设计出 Android 6.0。
如果没有把握"软件架构需要根据业务发展不断变化"这个本质,在做架构设计时就很容易陷入一个误区:试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。为了实现这样的目标,要么照搬业界大公司公开发表的方案,要么投入庞大的资源和时间来做预测和分析。无论哪种做法,后果都很明显:投入巨大,落地遥遥无期。
考虑到软件架构需要根据业务发展不断变化这个本质特点,软件架构设计其实更加类似于大自然"设计"一个生物,通过演化让生物适应环境,逐步变得更加强大:首先生物要适应当时的环境,其次生物需要不断地繁殖,将有利的基因传递下去,将不利的基因剔除或者修复。
以下 Python 示例展示了组件数量对系统可用性的影响:
pythondef calculate_availability(component_count, individual_availability=0.90):
"""计算系统整体可用性,假设每个组件可用性相同且独立"""
return individual_availability ** component_count
# 示例:对比不同组件数量的系统可用性
print("组件故障率 10%,各组件独立")
print("=" * 40)
for n in [1, 2, 3, 5, 10]:
avail = calculate_availability(n, 0.90)
downtime = (1 - avail) * 365 * 24 * 60 # 年故障分钟数
print(f"{n} 个组件: 可用性={avail:.2%}, 年故障约 {downtime:.0f} 分钟")
print()
print("对比: 单组件 99% vs 90% 可用性")
print("=" * 40)
for avail in [0.99, 0.90]:
for n in [1, 3, 5]:
result = calculate_availability(n, avail)
print(f"单组件可用性 {avail:.0%}, {n} 个组件: 整体可用性={result:.4f}")
输出示例:
组件故障率 10%,各组件独立 ======================================== 1 个组件: 可用性=90.00%, 年故障约 52560 分钟 2 个组件: 可用性=81.00%, 年故障约 9878 分钟 3 个组件: 可用性=72.90%, 年故障约 14212 分钟 5 个组件: 可用性=59.00%, 年故障约 21504 分钟 10 个组件: 可用性=34.87%, 年故障约 32846 分钟 对比: 单组件 99% vs 90% 可用性 ======================================== 单组件可用性 99%, 3 个组件: 整体可用性=0.9703 单组件可用性 99%, 5 个组件: 整体可用性=0.9510 单组件可用性 90%, 3 个组件: 整体可用性=0.7290 单组件可用性 90%, 5 个组件: 整体可用性=0.5905
这个示例清楚说明了为什么简单原则强调组件数量越多系统可用性越低。在实际架构设计中,我们需要权衡功能拆分带来的独立性与可用性下降之间的矛盾。