---
title: '学架构笔记10、11、12、13：架构设计四部曲'
pubDate: 2018-06-20
tags:
  - 'architect'
  - '笔记'
description: '学架构笔记10、11、12、13：架构设计四部曲'
---

结合复杂度来源和架构设计原则，我们一起来看看实践中如何进行架构设计。

## 识别复杂度

由于架构设计的目的是为了解决软件系统的复杂性，因此我们在设计架构之前就要先分析系统的复杂度。

如果一个系统的复杂度本来是业务逻辑太复杂，功能耦合严重，架构师却设计了一个TPS达到50000的高性能架构，那么这个设计没有任何意义，没有解决当前的复杂性问题。

高性能、高可用、可扩展等几个方面，一般情况下复杂度只是其中的一个，而如果当前架构需要解决三个或者三个以上的复杂度问题，要么设计时出了问题，要么对当前架构的判断是不正确的。

举个例子，一个（业务普通的）小应用，非要对标腾讯QQ的架构，硬是要设计高TPS高用户量的架构，除了设计和开发时间长外，还出现了各种各样的问题：

1. 系统复杂，运维效率低下
2. 迭代开发效率低
3. 问题定位复杂
4. 目标为高性能，却表现出低性能

同时，面对这样一个烫手山芋，想要改造也不简单：

1. 需要改的地方太多，无从下手
2. 落地时间太久远
3. 多个解决方案冲突，不好操作

这时，最好将主要的复杂度问题都列出来，然后根据实际情况排序，按优先级解决。

存在可能：按优先级解决多个复杂度，前一个问题刚解决成功落地，后一个问题却需要推翻前一个重来。因此，解决方案的敲定也需要斟酌，一般可能存在一个方案或引入一种新技术，能同时解决前后的问题。

### 案例

假设一个微博like的系统：

1. 用户发微博后，微博子系统通知审核子系统，再通知统计子系统，再通知广告子系统...即在一条微博发出后，微博子系统需要调用十几个子系统的接口。开发、测试效率低下。
2. 如果用户有多个等级，比如VIP、SVIP..（现在很流行有很多种乱七八糟的会员啊），那么上述的调用将会更复杂。

通过对当前架构的了解，分析得问题的根源是系统强耦合，较好的方法就是引入消息队列解耦。

该系统相关人员立项，并总结当前信息：

1. 中间件人员大约6人
2. 团队熟悉Java，少数精通C/C++
3. 平台选用Linux，数据库MySQL
4. 单机房部署

#### 高性能复杂度分析

假设该系统的用户每天发送1000万条微博，平均一条微博有10个子系统读取，那么子系统读取大约为1亿次。换算过来，每秒写入115条，每秒读取1150条，按峰值三倍算，TPS=345，QPS=3450，总体来说目前阶段性能需求就这样。

考虑后续业务发展，性能需要再向后考虑多一点，按四倍扩展算，TPS=1380，QPS=13800，这种程度下，高性能读取已经是复杂度之一了。

#### 高可用复杂度分析

如审核子系统的高可用是必须要保证的，如等级子系统的高可用的必要性虽然没那么高，但是却十分影响业务的正常功能和用户的满意度。

因此高可用也是需要的。

#### 高可扩展复杂度分析

消息队列整个系统中只充当了一个中间件，只需要保证队列功能即可。因此功能需求较为明确，无需过多考虑可扩展性。

#### 分析总结

该中间件的复杂性主要表现在：

- 高性能消息读取
- 高性能消息写入
- 高可用消息存储
- 高可用消息读取

## 设计备选方案

架构师需要对已存在的技术非常熟悉，对已验证过的架构模式烂熟于心，只有目前的模式无法满足需求的情况下，才需要进行方案创新。而目前大部分情况下，创新方案也是基于已有的成熟技术，如：

- NoSQL，KV存储与数据库索引类似
- Hadoop，基础是集群方案+数据复制方案
- Docker虚拟化，基础LXC
- LevelDB，其文件存储结构是Skip List

> 新技术都是在现有技术的基础上发展起来的，现有技术又来源与先前的技术。将技术进行功能性分组，可以大大简化设计过程，这是技术“模块化”的首要原因。技术的”组合“和”递归“特征，将彻底改变我们对技术本质的认识。

### 设计备选方案容易犯的错

#### 设计最优秀的方案

架构设计中最常见的错误是“设计最优秀的方案”，即比拼业界最优秀的架构，但适合自己业务、团队、技术能力的方案才是好方案。

#### 只做一个方案

还有一种常见的错误是“只做一个方案”，存在问题：

- 可能第二种方案也有优点，却因为简单的评估被筛选掉了
- 架构师个人能力有限导致判断错误，就会导致方案并不是最合适的
- 单一方案设计会出现过度辩护的情况

因此：

- 设计备选方案3-5个
- 备选方案之间的差异要明显
- 备选方案不要局限于已经熟悉的技术

#### 备选方案过于详细

第三种常见的错误是“备选方案过于详细”，存在问题：

- 会耗费了大量的时间和精力
- 注意了细节会容易忽略整体
- 方案评审容易被细节吸引，导致评审效果不佳

备选方案阶段关注的是技术选型，而不是技术细节，关键是差异要够大。如Zookeeper和Keepalived两种不同技术差异就够大，而Zookeeper的xx方案和xx方案则没有那么大的差异。

### 案例

故事说到微博系统需要一个消息队列，其复杂度为

- 高性能消息读取
- 高性能消息写入
- 高可用消息存储
- 高可用消息读取

现在就设计备选方案：

1. 采用开源的Kafka，优点为方案成熟
2. 集群 + MySQL存储 + Netty，集群负载均衡满足高性能读取，MySQL主备复制满足高可用存储和高可用读取
3. 集群 + 自研存储方案 + Netty，由于方案中MySQL关系型数据库的特点不是很契合消息队列的数据特点，因此也可以参考Kafka重新设计一套

架构师的技术存储越丰富，备选方案可能就越多，如开源方案也能是ActiveMQ、RabbitMQ，集群也能是HBase、Redis等等。

## 评估和选择备选方案

在完成备选方案后，如果挑选出最终的方案也较难，因为：

- 每个方案都可行，因为不可行的方案是不能作为备选方案的
- 每个方案都有自己的优缺点
- 评价标注主观性比较强，因为复杂和困难是很难去量化的，特别是在对某技术部熟悉的情况下

存在那么几个类型的选择方案，不一定适用于所有场合：

- 最简派，哪个原理简单用哪个
- 最牛派，哪个性能好、功能强等等就用哪个
- 最熟派，熟悉哪个选哪个
- 领导派，给领导选

在评估时，遵循“合适原则”、“简单原则”，避免贪大基本上就可以了。同时，不能太过长远地考虑业务暴涨的情况，应该遵循“演化原则”，避免多过地一步到位。如果存在业务暴涨超过预期，那么值得重构。

存在几种看似正确但实际错误的方案：

- 数量对比法，哪个方案优点多就选哪个。主要问题是没有考虑优先级
- 加权法。主要问题是评选会渐渐变成数字游戏

正确的做法：按当前业务、团队规模、技能、业务未来发展的实际情况，将质量属性（性能、可用性、成本、安全、可扩展等）按照优先级排序，首先挑选满足第一优先级的，如果都满足再挑选满足第二优先级的，类推。

### 案例

上回故事说到微博中间件系统设计了三个备选方案，接下来就是方案评选会议。

#### 方案一：采用开源Kafka方案

主管倾向于Kafka方案，因为成熟。

中间件团队支持Kafka，因为能节省开发投入，但部分人员认为Kafka不太适合业务场景。

运维反对Kafka，因为运维团队没有Scala的开发经验，问题处理困难。

测试倾向于Kafka，因为成熟。

#### 方案二：集群 + MySQL存储

中间件研发人员认为较简单，部分对性能持怀疑态度，并觉得用MySQL做消息队列很low。

运维团队赞同该方案，因为MySQL运维难度低。

测试代表认为该方案需要投入较大的人力，包括功能测试、性能测试、可靠性测试。

主管不肯定也不否定，只要保证业务稳定可靠即可。

#### 方案三：集群 + 自研存储系统

中间件团队较为支持，是展现团队实例的一个好机会，性能比方案二要好，但人力研发成本较大、迭代时间较长。

运维不赞成，因为不相信研发团队实力，容易造成运维失误。

测试团队赞同运维团队的意见，测试难度大。

主管保留一键，新系统一般迭代较差。

#### 根据属性评审

| 质量属性 | 方案一                                  | 方案二                                  | 方案三            |
| :------- | :-------------------------------------- | :-------------------------------------- | :---------------- |
| 性能     | 高                                      | 中                                      | 高                |
| 复杂度   | 低，开箱即用                            | 中                                      | 高，研发成本高    |
| 硬件成本 | 低                                      | 高，集群部署                            | 低，和Kafka一样   |
| 可运维性 | 低，无法融入现有运维体系，且无Scala经验 | 高，可以融入现有运维体系，MySQL经验成熟 | 高，无需维护MySQL |
| 可靠性   | 高，成熟方案                            | 高，MySQL成熟方案                       | 低，自研需要迭代  |
| 人力投入 | 低                                      | 中，开发集群服务器                      | 高                |

架构师选择了方案二，原因有：

1. 从可运维性排除了方案一，如果上线了出问题不能技术修复就无法满足业务需求，且Kafka的设计目标为日志传输而非消息传输。
2. 从复杂度排除方案三，因为人力成本不足。
3. 方案二优点是复杂度不高，可靠性有保障。

但方案二也有缺点：

1. 性能容易到瓶颈，但目前业务不高
2. 硬件成本较高
3. 技术不优越，但是适应业务

### 评审总结

备选方案的选择和很多因素有关，并不能单纯地考虑技术因素，不同的业务会造成不同的选择。

## 详细方案设计

详细方案设计就是在选择方案后，将方案设计的关键技术细节给确定下来。

举几个例子：

- 如果确定使用Elasticsearch全文搜索，那么就要确认的索引是按照业务划分的还是直接用一个大索引，副本数量和集群节点数量是多少。
- 如果确定使用MySQL分库分表，那么就要确定那些表要分，按哪个维度分，分了之后的处理方法。
- 如果确定引入Nginx来做负载均衡，那么Nginx的主备怎么做，负载均衡的策略用哪个。

看起来和备选方案类似，但简单了一点，因为无需选择，只需要简单地根据原物场景选择详细技术就好。

详细设计方案阶段可能会遇到一种情况，就是发现备选方案不可行，可能是因为在设计时遗漏，可以通过如下方法有效避免：

- 架构师不但要进行备选方案设计还要对备选方案的关键细节有较深的了解。
- 通过分步骤、分阶段、分系统，能有效降低方案的复杂度。
- 如果方案本身很复杂，那么就采取设计团队的方式进行设计，即避免一两人出现了盲区导致方案出问题。

### 案例

上回故事去到微博消息系统根据方案二设计详细方案。

#### 数据库表的设计

- 设计两类表，一类是日志表，用于消息写入时快速存储到MySQL，另一类是消息表，每个消息队列一张表。
- 发布消息时，先写入日志表，日志表写入成功就代表消息写入成功，后台线程从日志表中读取消息写入消息表中。
- 读取消息时，从消息表中读取。
- 日志表包含字段有：日志ID、发布者信息、发布时间、队列名称、消息内容
- 消息表包含字段有： 消息ID、消息内容、消息发布时间、消息发布者
- 日志表需要及时清楚已经写入消息表的日志，消息表可以设置保存较长的时间

#### 数据如何复制

直接采用MySQL主从复制，而且只复制消息表。

#### 主备服务器如何倒换

采用Zookeeper来做主备决策，主备服务器都连接到Zookeeper建立自己的节点，主服务器路径规则为`/MQ/server/{id}/master`，备机为`/MQ/server/{id}/slave`，节点类型为EPHEMERAL。

当发现主服务器断开，备机修改自己的状态，对外提供消息读取服务。

#### 业务服务器写入消息的方法

消息队列提供SDK供各业务系统调用，SDK从配置中读取所有消息队列系统的服务器信息，SDK轮询发起写入请求给主服务器，如果请求错误则请求下一台。

#### 业务服务器读取消息的方法

消息队列提供SDK供各业务系统调用，轮询方案与写入类似。

消息队列服务器需要记录每个消费者的消费状态，即当前消费者已经读取到哪条消息，当收到消息读取请求时，返回下一条未读消息给消费者

#### 业务服务器和消息队列服务器之间的通信协议

为了提升兼容性，传输协议用TCP，数据格式使用ProtocolBuffer。

## 总结

学习了架构设计四部曲，分别是识别复杂度、设计备选方案、评估和选择备选方案、详细方案设计，以及通过一个微博消息系统的方案设计案例，更深入地熟悉架构设计。

_先这样吧_

若有错误之处请指出，更多地关注[煎鱼](http://www.jianyujianyu.com)。
