上一期我讲了架构设计流程第 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 步, 设计备选方案。
Kafka 是成熟的开源消息队列方案,功能强大,性能非常高,而且已经比较成熟,很多大公司都 在使用。
首先考虑单服务器高性能。高性能消息读取属于“计算高可用”的范畴,单服务器高性能备选方 案有很多种。考虑到团队的开发语言是 Java,虽然有人觉得 C/C++ 语言更加适合写高性能的中间件系统,但架构师综合来看,认为无须为了语言的性能优势而让整个团队切换语言,消息队 列系统继续用 Java 开发。由于 Netty 是 Java 领域成熟的高性能网络库,因此架构师选择基于 Netty 开发消息队列系统。
由于系统设计的 QPS 是 13800,即使单机采用 Netty 来构建高性能系统,单台服务器支撑这 么高的 QPS 还是有很大风险的,因此架构师选择采取集群方式来满足高性能消息读取,集群的 负载均衡算法采用简单的轮询即可。
同理,“高可用写入”和“高性能读取”一样,可以采取集群的方式来满足。因为消息只要写入 集群中一台服务器就算成功写入,因此“高可用写入”的集群分配算法和“高性能读取”也一样 采用轮询,即正常情况下,客户端将消息依次写入不同的服务器;某台服务器异常的情况下,客 户端直接将消息写入下一台正常的服务器即可。
整个系统中复杂的是“高可用存储”和“高可用读取”,“高可用存储”要求已经写入的消息 在单台服务器宕机的情况下不丢失;“高可用读取”要求已经写入的消息在单台服务器宕机的情 况下可以继续读取。架构师第一时间想到的就是可以利用 MySQL 的主备复制功能来达到“高可 用存储“的目的,通过服务器的主备方案来达到“高可用读取”的目的。
具体方案:
简单描述一下方案:
在备选方案 2 的基础上,将 MySQL 存储替换为自研实现存储方案,因为 MySQL 的关系型数 据库的特点并不是很契合消息队列的数据特点,参考 Kafka 的做法,可以自己实现一套文件存 储和复制方案(此处省略具体的方案描述,实际设计时需要给出方案)。
可以看出,高性能消息读取单机系统设计这部分时并没有多个备选方案可选,备选方案 2 和备 选方案 3 都采取基于 Netty 的网络库,用 Java 语言开发,原因就在于团队的 Java 背景约束了 备选的范围。通常情况下,成熟的团队不会轻易改变技术栈,反而是新成立的技术团队更加倾向 于采用新技术。
上面简单地给出了 3 个备选方案用来示范如何操作,实践中要比上述方案复杂一些。架构师的 技术储备越丰富、经验越多,备选方案也会更多,从而才能更好地设计备选方案。例如,开源方案选择可能就包括 Kafka、ActiveMQ、RabbitMQ;集群方案的存储既可以考虑用 MySQL, 也可以考虑用 HBase,还可以考虑用 Redis 与 MySQL 结合等;自研文件系统也可以有多个, 可以参考 Kafka,也可以参考 LevelDB,还可以参考 HBase 等。限于篇幅,这里就不一一展开 了。
架构设计第三步:评估和选择备选方案https://blog.csdn.net/qq_41936805/article/details/101990182