识别复杂度

将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。

例子 1

一个“亿级用户平台”失败的案例,设计对标腾讯的 QQ,按照腾讯 QQ 的用户量级和功能复杂度进行设计,高性能、高可用、可扩展、安全等技术一应俱全,一开始就设计出了 40 多个子系统,然后投入大量人力开发了将近 1 年时间才跌跌撞撞地正式上线。上线后发现之前的过度设计完全是多此一举,而且带来很多问题:

  • 系统复杂无比,运维效率低下,每次业务版本升级都需要十几个子系统同步升级,操作步骤复杂,容易出错,出错后回滚还可能带来二次问题。
  • 每次版本开发和升级都需要十几个子系统配合,开发效率低下。
  • 子系统数量太多,关系复杂,小问题不断,而且出问题后定位困难。
  • 开始设计的号称 TPS 50000/ 秒的系统,实际 TPS 连 500 都不到。

由于业务没有发展,最初的设计人员陆续离开,后来接手的团队,无奈又花了 2 年时间将系统重构,合并很多子系统,将原来 40 多个子系统合并成不到 20 个子系统,整个系统才逐步稳定下来。

如果运气真的不好,接手了一个每个复杂度都存在问题的系统,那应该怎么办呢?答案是一个个来解决问题,不要幻想一次架构重构解决所有问题。例如这个“亿级用户平台”的案例,后来接手的团队其实面临几个主要的问题:系统稳定性不高,经常出各种莫名的小问题;系统子系统数量太多,系统关系复杂,开发效率低;不支持异地多活,机房级别的故障会导致业务整体不可用。如果同时要解决这些问题,就可能会面临这些困境:

  • 要做的事情太多,反而感觉无从下手。
  • 设计方案本身太复杂,落地时间遥遥无期。
  • 同一个方案要解决不同的复杂性,有的设计点是互相矛盾的。例如,要提升系统可用性,就需要将数据及时存储到硬盘上,而硬盘刷盘反过来又会影响系统性能。

“亿级用户平台”这个案例,团队就优先选择将子系统的数量降下来,后来发现子系统数量降下来后,不但开发效率提升了,原来经常发生的小问题也基本消失了,于是团队再在这个基础上做了异地多活方案,也取得了非常好的效果。

例子 2

假想一个创业公司,名称叫作“前浪微博”。前浪微博的业务发展很快,系统也越来越多,系统间协作的效率很低,例如:

  • 用户发一条微博后,微博子系统需要通知审核子系统进行审核,然后通知统计子系统进行统计,再通知广告子系统进行广告预测,接着通知消息子系统进行消息推送……一条微博有十几个通知,目前都是系统间通过接口调用的。每通知一个新系统,微博子系统就要设计接口、进行测试,效率很低,问题定位很麻烦,经常和其他子系统的技术人员产生分岐,微博子系统的开发人员不胜其烦。
  • 用户等级达到 VIP 后,等级子系统要通知福利子系统进行奖品发放,要通知客服子系统安排专属服务人员,要通知商品子系统进行商品打折处理……等级子系统的开发人员也是不胜其烦。

通过梳理问题,发现问题的根源在于架构上各业务子系统强耦合,而消息队列系统正好可以完成子系统的解耦,于是提议要引入消息队列系统。经过一分析二讨论三开会四汇报五审批等一系列操作后,消息队列系统终于立项了。其他背景信息还有:

  • 中间件团队规模不大,大约 6 人左右。
  • 中间件团队熟悉 Java 语言,但有一个新同事 C/C++ 很牛。
  • 开发平台是 Linux,数据库是 MySQL。
  • 目前整个业务系统是单机房部署,没有双机房。

针对前浪微博的消息队列系统,采用“排查法”来分析复杂度,具体分析过程是:

  1. 这个消息队列是否需要高性能?

    我们假设前浪微博系统用户每天发送 1000 万条微博,那么微博子系统一天会产生 1000 万条消息,我们再假设平均一条消息有 10 个子系统读取,那么其他子系统读取的消息大约是 1 亿次。

    将数据按照秒来计算,一天内平均每秒写入消息数为 115 条,每秒读取的消息数是 1150 条;
    再考虑峰值选择平均值的 3 倍,那么消息队列系统的 TPS 是 345,QPS 是 3450。

    考虑到之后的业务增长,将设计目标设定为峰值的 4 倍,因此最终的性能要求是:TPS 为 1380,QPS 为 13800。
    TPS 为 1380 并不高,但 QPS 为 13800 已经比较高了,因此高性能读取是复杂度之一。

  2. 这个消息队列是否需要高可用性?

    如果消息丢了,导致没有审核,然后触犯了国家法律法规,则是非常严重的事情;

    对于等级子系统来说,如果用户达到相应等级后,系统没有给他奖品和专属服务,则 VIP 用户会很不满意,导致用户流失从而损失收入,虽然也比较关键,但没有审核子系统丢消息那么严重。

    综合来看,消息队列需要高可用性,包括消息写入、消息存储、消息读取都需要保证高可用性。

  3. 这个消息队列是否需要高可扩展性?

    消息队列的功能很明确,基本无须扩展,因此可扩展性不是这个消息队列的复杂度关键。

这里只排查了“高性能”“高可用”“扩展性”这 3 个复杂度来作为例子。其他团队例如,金融系统可能需要考虑安全性,有的公司会考虑成本等。

综合分析下来,消息队列的复杂性主要体现在这几个方面:高性能消息读取、高可用消息写入、高可用消息存储、高可用消息读取。

设计备选方案

不要期望直接设计出一个最优的方案。备选方案的数量以 3 ~ 5 个为最佳。少于 3 个方案可能是因为思维狭隘,考虑不周全;多于 5 个则需要耗费大量的精力和时间,并且方案之间的差别可能不明显。

备选方案的差异要比较明显。例如,主备方案和集群方案差异就很明显,或者同样是主备方案,用 ZooKeeper 做主备决策和用 Keepalived 做主备决策的差异也很明显。但是都用 ZooKeeper 做主备决策,一个检测周期是 1 分钟,一个检测周期是 5 分钟,这就不是架构上的差异,而是细节上的差异了,不适合做成两个方案。

备选方案的技术不要只局限于已经熟悉的技术。设计架构时,架构师需要将视野放宽,考虑更多可能性。很多架构师或者设计师积累了一些成功的经验,出于快速完成任务和降低风险的目的,可能自觉或者不自觉地倾向于使用自己已经熟悉的技术,对于新的技术有一种不放心的感觉。就像那句俗语说的:“如果你手里有一把锤子,所有的问题在你看来都是钉子”。例如,架构师对 MySQL 很熟悉,因此不管什么存储都基于 MySQL 去设计方案,系统性能不够了,首先考虑的就是 MySQL 分库分表,而事实上也许引入一个 Memcache 缓存就能够解决问题。

设计方案的时候也不要过于详细正确的做法是备选阶段关注的是技术选型,而不是技术细节,技术选型的差异要比较明显。例如,采用 ZooKeeper 和 Keepalived 两种不同的技术来实现主备,差异就很大;而同样都采用 ZooKeeper,一个方案的节点设计是 /service/node/master,另一个方案的节点设计是 /company/service/master,这两个方案并无明显差异,无须在备选方案设计阶段作为两个不同的备选方案,至于节点路径究竟如何设计,只要在最终的方案中挑选一个进行细化即可。

架构设计原则

  1. 合适优于业界领先:可以参考淘宝,但不要照搬淘宝
  2. 演化优于过度设计:不要设计过于超前的方案,演化也不要推倒重来
  3. 简单优于复杂:轮询大部分情况下都是很好的设计,不确定就穷举,不明确就轮询
  4. 重构优于重写:风险控制、经验传承、成本分散
  5. 硬件优于人工:能够用硬件解决的,不要用人工去解决,例如换 SSD,换更强的机器
  6. 专注优于全面:SRP 原则,一个系统只关注一个事情,JAE 的例子
  7. 开放优于封闭:亚马逊的例子,SOA、微服务
  8. 能用优于完美
  9. 重用优于自研
  10. 业务优于技术:Docker 很火,我们是否要引入?
  11. 存储优于运算:存储设计是架构设计的关键
  12. 技术优于流程:不要试图提升人的能力来保证质量,不要试图投入大量的测试来保证质量,而是尽量做到出问题能够快速发现和处理
  13. 分布优于集中
  14. 优化优于重构

评估备选方案

常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。在评估这些质量属性时,需要遵循架构设计原则 1“合适原则”和原则 2“简单原则”,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了。

避免过度设计,可以大致对未来的增长有一定的期待,但是没必要期待的过多。

最理想的情况是设计一个方案,能够简单地扩容就能够跟上业务的发展。例如,我们设计一个方案,TPS 2000 的时候只要 2 台机器,TPS 20000 的时候只需要简单地将机器扩展到 20 台即可。但现实往往没那么理想,因为量变会引起质变,具体哪些地方质变,是很难提前很长时间能预判到的。举一个最简单的例子:一个开发团队 5 个人开发了一套系统,能够从 TPS 2000 平滑扩容到 TPS 20000,但是当业务规模真的达到 TPS 20000 的时候,团队规模已经扩大到了 20 个人,此时系统发生了两个质变:

  1. 首先是团队规模扩大,20 个人的团队在同一个系统上开发,开发效率变将很低,系统迭代速度很慢,经常出现某个功能开发完了要等另外的功能开发完成才能一起测试上线,此时如果要解决问题,就需要将系统拆分为更多子系统。

  2. 其次是原来单机房的集群设计不满足业务需求了,需要升级为异地多活的架构。

最开始能不能直接设计最终的效果呢?不推荐。因为最开始的时候团队只有 5 个人,5 个人在有限的时间内要完成后来 20 个人才能完成的高性能、异地多活、可扩展的架构,项目时间会遥遥无期,业务很难等待那么长的时间。

没有哪个方案是完美的,极少出现某个方案在所有对比维度上都是最优的。例如:引入开源方案工作量小,但是可运维性和可扩展性差;自研工作量大,但是可运维和可维护性好;使用 C 语言开发性能高,但是目前团队 C 语言技术积累少;使用 Java 技术积累多,但是性能没有 C 语言开发高,成本会高一些……诸如此类。

在选择的时候,不应该按照优点的数量对比 or 加权对比。正确的做法是按优先级选择,即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。

详细方案设计

详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性。

幸运的是,这种情况可以通过下面方式有效地避免:

  • 架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。例如,架构师选择了 Elasticsearch 作为全文搜索解决方案,前提必须是架构师自己对 Elasticsearch 的设计原理有深入的理解,比如索引、副本、集群等技术点;而不能道听途说 Elasticsearch 很牛,所以选择它,更不能成为把“细节我们不讨论”这句话挂在嘴边的“PPT 架构师”。

  • 通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度,方案本身的复杂度越高,某个细节推翻整个方案的可能性就越高,适当降低复杂性,可以减少这种风险。

  • 如果方案本身就很复杂,那就采取设计团队的方式来进行设计,博采众长,汇集大家的智慧和经验,防止只有 1~2 个架构师可能出现的思维盲点或者经验盲区。

参考链接