架构设计流程
结合复杂度来源和架构设计原则,通过一个模拟的设计场景“前浪微博”,和你一起看看在实践中究竟如何进行架构设计。
架构设计第1步:识别复杂度
架构设计的本质目的是为了解决软件系统的复杂性,所以在我们设计架构时,首先就要分析系统的复杂性。只有正确分析出了系统的复杂性,后续的架构设计方案才不会偏离方向;否则,如果对系统的复杂性判断错误,即使后续的架构设计方案再完美再先进,都是南辕北辙,做的越好,错的越多、越离谱。
例如,如果一个系统的复杂度本来是业务逻辑太复杂,功能耦合严重,架构师却设计了一个TPS达到50000/秒的高性能架构,即使这个架构最终的性能再优秀也没有任何意义,因为架构没有解决正确的复杂性问题。
架构的复杂度主要来源于 “高性能”、“高可用”、“可扩展” 等几个方面,但架构师在具体判断复杂性的时候,不能生搬硬套,认为任何时候架构都必须同时满足这三方面的要求。实际上大部分场景下,复杂度只是其中的某一个,少数情况下包含其中两个,如果真的出现同时需要解决三个或者三个以上的复杂度,要么说明这个系统之前设计的有问题,要么可能就是架构师的判断出现了失误,即使真的认为要同时满足这三方面的要求,也必须要进行优先级排序。
例如,专栏前面提到过的“亿级用户平台”失败的案例,设计对标腾讯的QQ,按照腾讯QQ的用户量级和功能复杂度进行设计,高性能、高可用、可扩展、安全等技术一应俱全,一开始就设计出了40多个子系统,然后投入大量人力开发了将近1年时间才跌跌撞撞地正式上线。上线后发现之前的过度设计完全是多此一举,而且带来很多问题:
系统复杂无比,运维效率低下,每次业务版本升级都需要十几个子系统同步升级,操作步骤复杂,容易出错,出错后回滚还可能带来二次问题。
每次版本开发和升级都需要十几个子系统配合,开发效率低下。
子系统数量太多,关系复杂,小问题不断,而且出问题后定位困难。
开始设计的号称TPS 50000/秒的系统,实际TPS连500都不到。
由于业务没有发展,最初的设计人员陆续离开,后来接手的团队,无奈又花了2年时间将系统重构,合并很多子系统,将原来40多个子系统合并成不到20个子系统,整个系统才逐步稳定下来。
如果运气真的不好,接手了一个每个复杂度都存在问题的系统,那应该怎么办呢?答案是一个个来解决问题,不要幻想一次架构重构解决所有问题。例如这个“亿级用户平台”的案例,后来接手的团队其实面临几个主要的问题:系统稳定性不高,经常出各种莫名的小问题;系统子系统数量太多,系统关系复杂,开发效率低;不支持异地多活,机房级别的故障会导致业务整体不可用。如果同时要解决这些问题,就可能会面临这些困境:
要做的事情太多,反而感觉无从下手。
设计方案本身太复杂,落地时间遥遥无期。
同一个方案要解决不同的复杂性,有的设计点是互相矛盾的。例如,要提升系统可用性,就需要将数据及时存储到硬盘上,而硬盘刷盘反过来又会影响系统性能。
因此,正确的做法是将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。“亿级用户平台”这个案例,团队就优先选择将子系统的数量降下来,后来发现子系统数量降下来后,不但开发效率提升了,原来经常发生的小问题也基本消失了,于是团队再在这个基础上做了异地多活方案,也取得了非常好的效果。
对于按照复杂度优先级解决的方式,存在一个普遍的担忧:如果按照优先级来解决复杂度,可能会出现解决了优先级排在前面的复杂度后,解决后续复杂度的方案需要将已经落地的方案推倒重来。这个担忧理论上是可能的,但现实中几乎是不可能出现的,原因在于软件系统的可塑性和易变性。对于同一个复杂度问题,软件系统的方案可以有多个,总是可以挑出综合来看性价比最高的方案。
即使架构师决定要推倒重来,这个新的方案也必须能够同时解决已经被解决的复杂度问题,一般来说能够达到这种理想状态的方案基本都是依靠新技术的引入。例如,Hadoop能够将高可用、高性能、大容量三个大数据处理的复杂度问题同时解决。
识别复杂度对架构师来说是一项挑战,因为原始的需求中并没有哪个地方会明确地说明复杂度在哪里,需要架构师在理解需求的基础上进行分析。有经验的架构师可能一看需求就知道复杂度大概在哪里;如果经验不足,那只能采取“排查法”,从不同的角度逐一进行分析。
识别复杂度实战
我们假想一个创业公司,名称叫作“前浪微博”。前浪微博的业务发展很快,系统也越来越多,系统间协作的效率很低,例如:
用户发一条微博后,微博子系统需要通知审核子系统进行审核,然后通知统计子系统进行统计,再通知广告子系统进行广告预测,接着通知消息子系统进行消息推送……一条微博有十几个通知,目前都是系统间通过接口调用的。每通知一个新系统,微博子系统就要设计接口、进行测试,效率很低,问题定位很麻烦,经常和其他子系统的技术人员产生分岐,微博子系统的开发人员不胜其烦。
用户等级达到VIP后,等级子系统要通知福利子系统进行奖品发放,要通知客服子系统安排专属服务人员,要通知商品子系统进行商品打折处理……等级子系统的开发人员也是不胜其烦。
新来的架构师在梳理这些问题时,结合自己的经验,敏锐地发现了这些问题背后的根源在于架构上各业务子系统强耦合,而消息队列系统正好可以完成子系统的解耦,于是提议要引入消息队列系统。经过一分析二讨论三开会四汇报五审批等一系列操作后,消息队列系统终于立项了。其他背景信息还有:
中间件团队规模不大,大约6人左右。
中间件团队熟悉Java语言,但有一个新同事C/C++很牛。
开发平台是Linux,数据库是MySQL。
目前整个业务系统是单机房部署,没有双机房。
针对前浪微博的消息队列系统,采用“排查法”来分析复杂度,具体分析过程是:
这个消息队列是否需要高性能
我们假设前浪微博系统用户每天发送1000万条微博,那么微博子系统一天会产生1000万条消息,我们再假设平均一条消息有10个子系统读取,那么其他子系统读取的消息大约是1亿次。
1000万和1亿看起来很吓人,但对于架构师来说,关注的不是一天的数据,而是1秒的数据,即TPS和QPS。我们将数据按照秒来计算,一天内平均每秒写入消息数为115条,每秒读取的消息数是1150条;再考虑系统的读写并不是完全平均的,设计的目标应该以峰值来计算。峰值一般取平均值的3倍,那么消息队列系统的TPS是345,QPS是3450,这个量级的数据意味着并不要求高性能。
虽然根据当前业务规模计算的性能要求并不高,但业务会增长,因此系统设计需要考虑一定的性能余量。由于现在的基数较低,为了预留一定的系统容量应对后续业务的发展,我们将设计目标设定为峰值的4倍,因此最终的性能要求是:TPS为1380,QPS为13800。TPS为1380并不高,但QPS为13800已经比较高了,因此高性能读取是复杂度之一。注意,这里的设计目标设定为峰值的4倍是根据业务发展速度来预估的,不是固定为4倍,不同的业务可以是2倍,也可以是8倍,但一般不要设定在10倍以上,更不要一上来就按照100倍预估。
这个消息队列是否需要高可用性
对于微博子系统来说,如果消息丢了,导致没有审核,然后触犯了国家法律法规,则是非常严重的事情;对于等级子系统来说,如果用户达到相应等级后,系统没有给他奖品和专属服务,则VIP用户会很不满意,导致用户流失从而损失收入,虽然也比较关键,但没有审核子系统丢消息那么严重。
综合来看,消息队列需要高可用性,包括消息写入、消息存储、消息读取都需要保证高可用性。
这个消息队列是否需要高可扩展性
消息队列的功能很明确,基本无须扩展,因此可扩展性不是这个消息队列的复杂度关键。
为了方便理解,这里我只排查“高性能”“高可用”“扩展性”这3个复杂度,在实际应用中,不同的公司或者团队,可能还有一些其他方面的复杂度分析。例如,金融系统可能需要考虑安全性,有的公司会考虑成本等。
综合分析下来,消息队列的复杂性主要体现在这几个方面:高性能消息读取、高可用消息写入、高可用消息存储、高可用消息读取。
“前浪微博”的消息队列设计才刚完成第1步,专栏下一期会根据今天识别的复杂度设计备选方案,前面提到的场景在下一期还会用到哦。
架构设计第2步:设计备选方案
架构师的工作并不神秘,成熟的架构师需要对已经存在的技术非常熟悉,对已经经过验证的架构模式烂熟于心,然后根据自己对业务的理解,挑选合适的架构模式进行组合,再对组合后的方案进行修改和调整。
虽然软件技术经过几十年的发展,新技术层出不穷,但是经过时间考验,已经被各种场景验证过的成熟技术其实更多。例如,高可用的主备方案、集群方案,高性能的负载均衡、多路复用,可扩展的分层、插件化等技术,绝大部分时候我们有了明确的目标后,按图索骥就能够找到可选的解决方案。
只有当这种方式完全无法满足需求的时候,才会考虑进行方案的创新,而事实上方案的创新绝大部分情况下也都是基于已有的成熟技术。
-
NoSQL:Key-Value的存储和数据库的索引其实是类似的,Memcache只是把数据库的索引独立出来做成了一个缓存系统。
-
Hadoop大文件存储方案,基础其实是集群方案+ 数据复制方案。
-
Docker虚拟化,基础是LXC(Linux Containers)。
-
LevelDB的文件存储结构是Skip List。
在《技术的本质》一书中,对技术的组合有清晰的阐述:
新技术都是在现有技术的基础上发展起来的,现有技术又来源于先前的技术。将技术进行功能性分组,可以大大简化设计过程,这是技术“模块化”的首要原因。技术的“组合”和“递归”特征,将彻底改变我们对技术本质的认识。
虽说基于已有的技术或者架构模式进行组合,然后调整,大部分情况下就能够得到我们需要的方案,但并不意味着架构设计是一件很简单的事情。因为可选的模式有很多,组合的方案更多,往往一个问题的解决方案有很多个;如果再在组合的方案上进行一些创新,解决方案会更多。因此,如何设计最终的方案,并不是一件容易的事情,这个阶段也是很多架构师容易犯错的地方。
第一种常见的错误:设计最优秀的方案。
很多架构师在设计架构方案时,心里会默认有一种技术情结:我要设计一个优秀的架构,才能体现我的技术能力!例如,高可用的方案中,集群方案明显比主备方案要优秀和强大;高性能的方案中,淘宝的XX方案是业界领先的方案……
根据架构设计原则中“合适原则”和“简单原则“的要求,挑选合适自己业务、团队、技术能力的方案才是好方案;否则要么浪费大量资源开发了无用的系统(例如,之前提过的“亿级用户平台”的案例,设计了TPS 50000的系统,实际TPS只有500),要么根本无法实现(例如,10个人的团队要开发现在的整个淘宝系统)。
第二种常见的错误:只做一个方案。
很多架构师在做方案设计时,可能心里会简单地对几个方案进行初步的设想,再简单地判断哪个最好,然后就基于这个判断开始进行详细的架构设计了。
这样做有很多弊端:
心里评估过于简单,可能没有想得全面,只是因为某一个缺点就把某个方案给否决了,而实际上没有哪个方案是完美的,某个地方有缺点的方案可能是综合来看最好的方案。
架构师再怎么牛,经验知识和技能也有局限,有可能某个评估的标准或者经验是不正确的,或者是老的经验不适合新的情况,甚至有的评估标准是架构师自己原来就理解错了。
单一方案设计会出现过度辩护的情况,即架构评审时,针对方案存在的问题和疑问,架构师会竭尽全力去为自己的设计进行辩护,经验不足的设计人员可能会强词夺理。
因此,架构师需要设计多个备选方案,但方案的数量可以说是无穷无尽的,架构师也不可能穷举所有方案,那合理的做法应该是什么样的呢?
备选方案的数量以3 ~ 5个为最佳。 少于3个方案可能是因为思维狭隘,考虑不周全;多于5个则需要耗费大量的精力和时间,并且方案之间的差别可能不明显。
备选方案的差异要比较明显。 例如,主备方案和集群方案差异就很明显,或者同样是主备方案,用ZooKeeper做主备决策和用Keepalived做主备决策的差异也很明显。但是都用ZooKeeper做主备决策,一个检测周期是1分钟,一个检测周期是5分钟,这就不是架构上的差异,而是细节上的差异了,不适合做成两个方案。
备选方案的技术不要只局限于已经熟悉的技术。 设计架构时,架构师需要将视野放宽,考虑更多可能性。很多架构师或者设计师积累了一些成功的经验,出于快速完成任务和降低风险的目的,可能自觉或者不自觉地倾向于使用自己已经熟悉的技术,对于新的技术有一种不放心的感觉。就像那句俗语说的:“如果你手里有一把锤子,所有的问题在你看来都是钉子”。例如,架构师对MySQL很熟悉,因此不管什么存储都基于MySQL去设计方案,系统性能不够了,首先考虑的就是MySQL分库分表,而事实上也许引入一个Memcache缓存就能够解决问题。
第三种常见的错误:备选方案过于详细。
有的架构师或者设计师在写备选方案时,错误地将备选方案等同于最终的方案,每个备选方案都写得很细。这样做的弊端显而易见:
耗费了大量的时间和精力。
将注意力集中到细节中,忽略了整体的技术设计,导致备选方案数量不够或者差异不大。
评审的时候其他人会被很多细节给绕进去,评审效果很差。例如,评审的时候针对某个定时器应该是1分钟还是30秒,争论得不可开交。
正确的做法是备选阶段关注的是技术选型,而不是技术细节,技术选型的差异要比较明显。例如,采用ZooKeeper和Keepalived两种不同的技术来实现主备,差异就很大;而同样都采用ZooKeeper,一个方案的节点设计是/service/node/master,另一个方案的节点设计是/company/service/master,这两个方案并无明显差异,无须在备选方案设计阶段作为两个不同的备选方案,至于节点路径究竟如何设计,只要在最终的方案中挑选一个进行细化即可。
设计备选方案实战
还是回到“前浪微博”的场景,上期我们通过“排查法”识别了消息队列的复杂性主要体现在:高性能消息读取、高可用消息写入、高可用消息存储、高可用消息读取。接下来进行第2步,设计备选方案。
- 备选方案1:采用开源的Kafka
Kafka是成熟的开源消息队列方案,功能强大,性能非常高,而且已经比较成熟,很多大公司都在使用。
- 备选方案2:集群 + MySQL存储
首先考虑单服务器高性能。高性能消息读取属于“计算高可用”的范畴,单服务器高性能备选方案有很多种。考虑到团队的开发语言是Java,虽然有人觉得C/C++语言更加适合写高性能的中间件系统,但架构师综合来看,认为无须为了语言的性能优势而让整个团队切换语言,消息队列系统继续用Java开发。由于Netty是Java领域成熟的高性能网络库,因此架构师选择基于Netty开发消息队列系统。
由于系统设计的QPS是13800,即使单机采用Netty来构建高性能系统,单台服务器支撑这么高的QPS还是有很大风险的,因此架构师选择采取集群方式来满足高性能消息读取,集群的负载均衡算法采用简单的轮询即可。
同理,“高可用写入”和“高性能读取”一样,可以采取集群的方式来满足。因为消息只要写入集群中一台服务器就算成功写入,因此“高可用写入”的集群分配算法和“高性能读取”也一样采用轮询,即正常情况下,客户端将消息依次写入不同的服务器;某台服务器异常的情况下,客户端直接将消息写入下一台正常的服务器即可。
整个系统中最复杂的是“高可用存储”和“高可用读取”,“高可用存储”要求已经写入的消息在单台服务器宕机的情况下不丢失;“高可用读取”要求已经写入的消息在单台服务器宕机的情况下可以继续读取。架构师第一时间想到的就是可以利用MySQL的主备复制功能来达到“高可用存储“的目的,通过服务器的主备方案来达到“高可用读取”的目的。
具体方案:
简单描述一下方案:
采用数据分散集群的架构,集群中的服务器进行分组,每个分组存储一部分消息数据。
每个分组包含一台主MySQL和一台备MySQL,分组内主备数据复制,分组间数据不同步。
正常情况下,分组内的主服务器对外提供消息写入和消息读取服务,备服务器不对外提供服务;主服务器宕机的情况下,备服务器对外提供消息读取的服务。
客户端采取轮询的策略写入和读取消息。
- 备选方案3:集群 + 自研存储方案
在备选方案2的基础上,将MySQL存储替换为自研实现存储方案,因为MySQL的关系型数据库的特点并不是很契合消息队列的数据特点,参考Kafka的做法,可以自己实现一套文件存储和复制方案(此处省略具体的方案描述,实际设计时需要给出方案)。
可以看出,高性能消息读取单机系统设计这部分时并没有多个备选方案可选,备选方案2和备选方案3都采取基于Netty的网络库,用Java语言开发,原因就在于团队的Java背景约束了备选的范围。通常情况下,成熟的团队不会轻易改变技术栈,反而是新成立的技术团队更加倾向于采用新技术。
上面简单地给出了3个备选方案用来示范如何操作,实践中要比上述方案复杂一些。架构师的技术储备越丰富、经验越多,备选方案也会更多,从而才能更好地设计备选方案。例如,开源方案选择可能就包括Kafka、ActiveMQ、RabbitMQ;集群方案的存储既可以考虑用MySQL,也可以考虑用HBase,还可以考虑用Redis与MySQL结合等;自研文件系统也可以有多个,可以参考Kafka,也可以参考LevelDB,还可以参考HBase等。
架构设计第3步:评估和选择备选方案
在完成备选方案设计后,如何挑选出最终的方案也是一个很大的挑战,主要原因有:
- 每个方案都是可行的,如果方案不可行就根本不应该作为备选方案。
- 没有哪个方案是完美的。例如,A方案有性能的缺点,B方案有成本的缺点,C方案有新技术不成熟的风险。
- 评价标准主观性比较强,比如设计师说A方案比B方案复杂,但另外一个设计师可能会认为差不多,因为比较难将“复杂”一词进行量化。因此,方案评审的时候我们经常会遇到几个设计师针对某个方案或者某个技术点争论得面红耳赤。
正因为选择备选方案存在这些困难,所以实践中很多设计师或者架构师就采取了下面几种指导思想:
- 最简派
设计师挑选一个看起来最简单的方案。例如,我们要做全文搜索功能,方案1基于MySQL,方案2基于Elasticsearch。MySQL的查询功能比较简单,而Elasticsearch的倒排索引设计要复杂得多,写入数据到Elasticsearch,要设计Elasticsearch的索引,要设计Elasticsearch的分布式……全套下来复杂度很高,所以干脆就挑选MySQL来做吧。
- 最牛派
最牛派的做法和最简派正好相反,设计师会倾向于挑选技术上看起来最牛的方案。例如,性能最高的、可用性最好的、功能最强大的,或者淘宝用的、微信开源的、Google出品的等。
我们以缓存方案中的Memcache和Redis为例,假如我们要挑选一个搭配MySQL使用的缓存,Memcache是纯内存缓存,支持基于一致性hash的集群;而Redis同时支持持久化、支持数据字典、支持主备、支持集群,看起来比Memcache好很多啊,所以就选Redis好了。
- 最熟派
设计师基于自己的过往经验,挑选自己最熟悉的方案。我以编程语言为例,假如设计师曾经是一个C++经验丰富的开发人员,现在要设计一个运维管理系统,由于对Python或者Ruby on Rails不熟悉,因此继续选择C++来做运维管理系统。
- 领导派
领导派就更加聪明了,列出备选方案,设计师自己拿捏不定,然后就让领导来定夺,反正最后方案选的对那是领导厉害,方案选的不对怎么办?那也是领导“背锅”。
其实这些不同的做法本身并不存在绝对的正确或者绝对的错误,关键是不同的场景应该采取不同的方式。也就是说,有时候我们要挑选最简单的方案,有时候要挑选最优秀的方案,有时候要挑选最熟悉的方案,甚至有时候真的要领导拍板。因此关键问题是:这里的“有时候”到底应该怎么判断?接下来就来讲讲架构设计流程的第3步:评估和选择备选方案。
评估和选择备选方案的具体的操作方式为:列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。
常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。在评估这些质量属性时,需要遵循架构设计原则1“合适原则”和原则2“简单原则”,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了。
假如我们做一个购物网站,现在的TPS是1000,如果我们预期1年内能够发展到TPS 2000(业务一年翻倍已经是很好的情况了),在评估方案的性能时,只要能超过2000的都是合适的方案,而不是说淘宝的网站TPS是每秒10万,我们的购物网站就要按照淘宝的标准也实现TPS 10万。
有的设计师会有这样的担心:如果我们运气真的很好,业务直接一年翻了10倍,TPS从1000上升到10000,那岂不是按照TPS 2000做的方案不合适了,又要重新做方案?
这种情况确实有可能存在,但概率很小,如果每次做方案都考虑这种小概率事件,我们的方案会出现过度设计,导致投入浪费。考虑这个问题的时候,需要遵循架构设计原则3“演化原则”,避免过度设计、一步到位的想法。按照原则3的思想,即使真的出现这种情况,那就算是重新做方案,代价也是可以接受的,因为业务如此迅猛发展,钱和人都不是问题。例如,淘宝和微信的发展历程中,有过多次这样大规模重构系统的经历。
通常情况下,如果某个质量属性评估和业务发展有关系(例如,性能、硬件成本等),需要评估未来业务发展的规模时,一种简单的方式是将当前的业务规模乘以2 ~4即可,如果现在的基数较低,可以乘以4;如果现在基数较高,可以乘以2。例如,现在的TPS是1000,则按照TPS 4000来设计方案;如果现在TPS是10000,则按照TPS 20000来设计方案。
当然,最理想的情况是设计一个方案,能够简单地扩容就能够跟上业务的发展。例如,我们设计一个方案,TPS 2000的时候只要2台机器,TPS 20000的时候只需要简单地将机器扩展到20台即可。但现实往往没那么理想,因为量变会引起质变,具体哪些地方质变,是很难提前很长时间能预判到的。举一个最简单的例子:一个开发团队5个人开发了一套系统,能够从TPS 2000平滑扩容到TPS 20000,但是当业务规模真的达到TPS 20000的时候,团队规模已经扩大到了20个人,此时系统发生了两个质变:
首先是团队规模扩大,20个人的团队在同一个系统上开发,开发效率变将很低,系统迭代速度很慢,经常出现某个功能开发完了要等另外的功能开发完成才能一起测试上线,此时如果要解决问题,就需要将系统拆分为更多子系统。
其次是原来单机房的集群设计不满足业务需求了,需要升级为异地多活的架构。
如果团队一开始就预测到这两个问题,系统架构提前就拆分为多个子系统并且支持异地多活呢?这种“事后诸葛亮”也是不行的,因为最开始的时候团队只有5个人,5个人在有限的时间内要完成后来20个人才能完成的高性能、异地多活、可扩展的架构,项目时间会遥遥无期,业务很难等待那么长的时间。
完成方案的360度环评后,我们可以基于评估结果整理出360度环评表,一目了然地看到各个方案的优劣点。但是360度环评表也只能帮助我们分析各个备选方案,还是没有告诉我们具体选哪个方案,原因就在于没有哪个方案是完美的,极少出现某个方案在所有对比维度上都是最优的。例如:引入开源方案工作量小,但是可运维性和可扩展性差;自研工作量大,但是可运维和可维护性好;使用C语言开发性能高,但是目前团队C语言技术积累少;使用Java技术积累多,但是性能没有C语言开发高,成本会高一些……诸如此类。
面临这种选择上的困难,有几种看似正确但实际错误的做法。
数量对比法:简单地看哪个方案的优点多就选哪个。例如,总共5个质量属性的对比,其中A方案占优的有3个,B方案占优的有2个,所以就挑选A方案。
这种方案主要的问题在于把所有质量属性的重要性等同,而没有考虑质量属性的优先级。例如,对于BAT这类公司来说,方案的成本都不是问题,可用性和可扩展性比成本要更重要得多;但对于创业公司来说,成本可能就会变得很重要。
其次,有时候会出现两个方案的优点数量是一样的情况。例如,我们对比6个质量属性,很可能出现两个方案各有3个优点,这种情况下也没法选;如果为了数量上的不对称,强行再增加一个质量属性进行对比,这个最后增加的不重要的属性反而成了影响方案选择的关键因素,这又犯了没有区分质量属性的优先级的问题。
加权法:每个质量属性给一个权重。例如,性能的权重高中低分别得10分、5分、3分,成本权重高中低分别是5分、3分、1分,然后将每个方案的权重得分加起来,最后看哪个方案的权重得分最高就选哪个。
这种方案主要的问题是无法客观地给出每个质量属性的权重得分。例如,性能权重得分为何是10分、5分、3分,而不是5分、3分、1分,或者是100分、80分、60分?这个分数是很难确定的,没有明确的标准,甚至会出现为了选某个方案,设计师故意将某些权重分值调高而降低另外一些权重分值,最后方案的选择就变成了一个数字游戏了。
正确的做法是按优先级选择,即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。那会不会出现两个或者多个方案,每个质量属性的优缺点都一样的情况呢?理论上是可能的,但实际上是不可能的。前面我提到,在做备选方案设计时,不同的备选方案之间的差异要比较明显,差异明显的备选方案不可能所有的优缺点都是一样的。
评估和选择备选方案实战
再回到我们设计的场景“前浪微博”。针对上期提出的3个备选方案,架构师组织了备选方案评审会议,参加的人有研发、测试、运维、还有几个核心业务的主管。
- 备选方案1:采用开源Kafka方案
业务主管倾向于采用Kafka方案,因为Kafka已经比较成熟,各个业务团队或多或少都了解过Kafka。
中间件团队部分研发人员也支持使用Kafka,因为使用Kafka能节省大量的开发投入;但部分人员认为Kafka可能并不适合我们的业务场景,因为Kafka的设计目的是为了支撑大容量的日志消息传输,而我们的消息队列是为了业务数据的可靠传输。
运维代表提出了强烈的反对意见:首先,Kafka是Scala语言编写的,运维团队没有维护Scala语言开发的系统的经验,出问题后很难快速处理;其次,目前运维团队已经有一套成熟的运维体系,包括部署、监控、应急等,使用Kafka无法融入这套体系,需要单独投入运维人力。
测试代表也倾向于引入Kafka,因为Kafka比较成熟,无须太多测试投入。
- 备选方案2:集群 + MySQL存储
中间件团队的研发人员认为这个方案比较简单,但部分研发人员对于这个方案的性能持怀疑态度,毕竟使用MySQL来存储消息数据,性能肯定不如使用文件系统;并且有的研发人员担心做这样的方案是否会影响中间件团队的技术声誉,毕竟用MySQL来做消息队列,看起来比较“土”、比较另类。
运维代表赞同这个方案,因为这个方案可以融入到现有的运维体系中,而且使用MySQL存储数据,可靠性有保证,运维团队也有丰富的MySQL运维经验;但运维团队认为这个方案的成本比较高,一个数据分组就需要4台机器(2台服务器 + 2台数据库)。
测试代表认为这个方案测试人力投入较大,包括功能测试、性能测试、可靠性测试等都需要大量地投入人力。
业务主管对这个方案既不肯定也不否定,因为反正都不是业务团队来投入人力来开发,系统维护也是中间件团队负责,对业务团队来说,只要保证消息队列系统稳定和可靠即可。
- 备选方案3:集群 + 自研存储系统
中间件团队部分研发人员认为这是一个很好的方案,既能够展现中间件团队的技术实力,性能上相比MySQL也要高;但另外的研发人员认为这个方案复杂度太高,按照目前的团队人力和技术实力,要做到稳定可靠的存储系统,需要耗时较长的迭代,这个过程中消息队列系统可能因为存储出现严重问题,例如文件损坏导致丢失大量数据。
运维代表不太赞成这个方案,因为运维之前遇到过几次类似的存储系统故障导致数据丢失的问题,损失惨重。例如,MongoDB丢数据、Tokyo Tyrant丢数据无法恢复等。运维团队并不相信目前的中间件团队的技术实力足以支撑自己研发一个存储系统(这让中间件团队的人员感觉有点不爽)。
测试代表赞同运维代表的意见,并且自研存储系统的测试难度也很高,投入也很大。
业务主管对自研存储系统也持保留意见,因为从历史经验来看,新系统上线肯定有bug,而存储系统出bug是最严重的,一旦出bug导致大量消息丢失,对系统的影响会严重。
针对3个备选方案的讨论初步完成后,架构师列出了3个方案的360度环评表:
列出这个表格后,无法一眼看出具体哪个方案更合适,于是大家都把目光投向架构师,决策的压力现在集中在架构师身上了。
架构师经过思考后,给出了最终选择备选方案2,原因有:
排除备选方案1的主要原因是可运维性,因为再成熟的系统,上线后都可能出问题,如果出问题无法快速解决,则无法满足业务的需求;并且Kafka的主要设计目标是高性能日志传输,而我们的消息队列设计的主要目标是业务消息的可靠传输。
排除备选方案3的主要原因是复杂度,目前团队技术实力和人员规模(总共6人,还有其他中间件系统需要开发和维护)无法支撑自研存储系统(参考架构设计原则2:简单原则)。
备选方案2的优点就是复杂度不高,也可以很好地融入现有运维体系,可靠性也有保障。
针对备选方案2的缺点,架构师解释是:
备选方案2的第一个缺点是性能,业务目前需要的性能并不是非常高,方案2能够满足,即使后面性能需求增加,方案2的数据分组方案也能够平行扩展进行支撑(参考架构设计原则3:演化原则)。
备选方案2的第二个缺点是成本,一个分组就需要4台机器,支撑目前的业务需求可能需要12台服务器,但实际上备机(包括服务器和数据库)主要用作备份,可以和其他系统并行部署在同一台机器上。
备选方案2的第三个缺点是技术上看起来并不很优越,但我们的设计目的不是为了证明自己(参考架构设计原则1:合适原则),而是更快更好地满足业务需求。
最后,大家针对一些细节再次讨论后,确定了选择备选方案2。
通过“前浪微博”这个案例我们可以看出,备选方案的选择和很多因素相关,并不单单考虑性能高低、技术是否优越这些纯技术因素。业务的需求特点、运维团队的经验、已有的技术体系、团队人员的技术水平都会影响备选方案的选择。因此,同样是上述3个备选方案,有的团队会选择引入Kafka(例如,很多创业公司的初创团队,人手不够,需要快速上线支撑业务),有的会选择自研存储系统(例如,阿里开发了RocketMQ,人多力量大,业务复杂是主要原因)。
架构设计第4步:详细方案设计
简单来说,详细方案设计就是将方案涉及的关键技术细节给确定下来。
假如我们确定使用 Elasticsearch 来做全文搜索,那么就需要确定 Elasticsearch 的索引是按 照业务划分,还是一个大索引就可以了;副本数量是 2 个、3 个还是 4 个,集群节点数量是 3 个还是 6 个等。
假如我们确定使用 MySQL 分库分表,那么就需要确定哪些表要分库分表,按照什么维度来 分库分表,分库分表后联合查询怎么处理等。
假如我们确定引入 Nginx 来做负载均衡,那么 Nginx 的主备怎么做,Nginx 的负载均衡策 略用哪个(权重分配?轮询?ip_hash?)等。
可以看到,详细设计方案里面其实也有一些技术点和备选方案类似。例如,Nginx 的负载均衡 策略,备选有轮询、权重分配、ip_hash、fair、url_hash 五个,具体选哪个呢?看起来和备选 方案阶段面临的问题类似,但实际上这里的技术方案选择是很轻量级的,我们无须像备选方案阶 段那样操作,而只需要简单根据这些技术的适用场景选择就可以了。
例如,Nginx 的负载均衡策略,简单按照下面的规则选择就可以了:
- 轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,后端服务器分配的请求数基本一致,如果后 端服务器“down 掉”,能自动剔除。
- 加权轮询
根据权重来进行轮询,权重高的服务器分配的请求更多,主要适应于后端服务器性能不均的情 况,如新老服务器混用。
- ip_hash
每个请求按访问 IP 的 hash 结果分配,这样每个访客固定访问一个后端服务器,主要用于解决 session 的问题,如购物车类的应用。
- fair
按后端服务器的响应时间来分配请求,响应时间短的优先分配,能够最大化地平衡各后端服务器 的压力,可以适用于后端服务器性能不均衡的情况,也可以防止某台后端服务器性能不足的情况 下还继续接收同样多的请求从而造成雪崩效应。
- url_hash
按访问 URL 的 hash 结果来分配请求,每个 URL 定向到同一个后端服务器,适用于后端服务器 能够将 URL 的响应结果缓存的情况。
这几个策略的适用场景区别还是比较明显的,根据我们的业务需要,挑选一个合适的即可。例 如,比如一个电商架构,由于和 session 比较强相关,因此如果用 Nginx 来做集群负载均衡, 那么选择 ip_hash 策略是比较合适的。
详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性。 例如,我曾经参与 过一个项目,在备选方案阶段确定是可行的,但在详细方案设计阶段,发现由于细节点太多,方 案非常庞大,整个项目可能要开发长达1年时间,最后只得废弃原来的备选方案,重新调整项目目标、计划和方案。这个项目的主要失误就是在备选方案评估时忽略了开发周期这个质量属性。
幸运的是,这种情况可以通过下面方式有效地避免:
架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。 例 如,架构师选择了 Elasticsearch 作为全文搜索解决方案,前提必须是架构师自己对 Elasticsearch 的设计原理有深入的理解,比如索引、副本、集群等技术点;而不能道听途说 Elasticsearch 很牛,所以选择它,更不能成为把“细节我们不讨论”这句话挂在嘴边 的“PPT 架构师”。
通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度, 方案本身的复杂度越高,某个 细节推翻整个方案的可能性就越高,适当降低复杂性,可以减少这种风险。
如果方案本身就很复杂,那就采取设计团队的方式来进行设计,博采众长,汇集大家的智慧 和经验,防止只有 1~2 个架构师可能出现的思维盲点或者经验盲区。
详细方案设计实战
虽然我们上期在“前浪微博”消息队列的架构设计挑选了备选方案 2 作为最终方案,但备选方 案设计阶段的方案粒度还比较粗,无法真正指导开发人员进行后续的设计和开发,因此需要在备 选方案的基础上进一步细化。
下面我列出一些备选方案 2 典型的需要细化的点供参考,有兴趣的同学可以自己尝试细化更多 的设计点。
- 细化设计点 1:数据库表如何设计?
数据库设计两类表,一类是日志表,用于消息写入时快速存储到 MySQL 中;另一类是消息 表,每个消息队列一张表。
业务系统发布消息时,首先写入到日志表,日志表写入成功就代表消息写入成功;后台线程 再从日志表中读取消息写入记录,将消息内容写入到消息表中。
业务系统读取消息时,从消息表中读取。
日志表表名为 MQ_LOG,包含的字段:日志 ID、发布者信息、发布时间、队列名称、消息 内容。
消息表表名就是队列名称,包含的字段:消息 ID(递增生成)、消息内容、消息发布时间、 消息发布者。
日志表需要及时清除已经写入消息表的日志数据,消息表最多保存 30 天的消息数据。
- 细化设计点 2:数据如何复制?
直接采用 MySQL 主从复制即可,只复制消息存储表,不复制日志表。
- 细化设计点 3:主备服务器如何倒换?
采用 ZooKeeper 来做主备决策,主备服务器都连接到 ZooKeeper 建立自己的节点,主服务器 的路径规则为“/MQ/server/ 分区编号 /master”,备机为“/MQ/server/ 分区编号 /slave”,节点类型为 EPHEMERAL。
备机监听主机的节点消息,当发现主服务器节点断连后,备服务器修改自己的状态,对外提供消 息读取服务。
- 细化设计点 4:业务服务器如何写入消息?
消息队列系统设计两个角色:生产者和消费者,每个角色都有唯一的名称。
消息队列系统提供 SDK 供各业务系统调用,SDK 从配置中读取所有消息队列系统的服务器 信息,SDK 采取轮询算法发起消息写入请求给主服务器。如果某个主服务器无响应或者返回 错误,SDK 将发起请求发送到下一台服务器。
- 细化设计点 5:业务服务器如何读取消息?
消息队列系统提供 SDK 供各业务系统调用,SDK 从配置中读取所有消息队列系统的服务器 信息,轮流向所有服务器发起消息读取请求。
消息队列服务器需要记录每个消费者的消费状态,即当前消费者已经读取到了哪条消息,当 收到消息读取请求时,返回下一条未被读取的消息给消费者。
- 细化设计点 6:业务服务器和消息队列服务器之间的通信协议如何设计?
考虑到消息队列系统后续可能会对接多种不同编程语言编写的系统,为了提升兼容性,传输协议 用 TCP,数据格式为 ProtocolBuffer。
当然还有更多设计细节就不再一一列举,因此这还不是一个完整的设计方案,我希望可以通过这 些具体实例来说明细化方案具体如何去做。