业务千变万化,技术层出不穷,设计理念也是百花齐放,看起来似乎很难有一套通用的规范来适 用所有的架构设计场景。但是在研究了架构设计的发展历史、多个公司的架构发展过程(QQ、 淘宝、Facebook 等)、众多的互联网公司架构设计后,有几个共性的原则隐含其中,这就是:合适原则、简单原则、演化原则,架构设计时遵循这几个原则,有助于我们做出最好的选择。
合适原则宣言:“合适优于业界领先”
优秀的技术人员都有很强的技术情结,当他们做方案或者架构时,总想不断地挑战自己,想达到甚至优于业界领先水平是其中一个典型表现,因为这样才能够展现自己的优秀,才能在年终 KPI 绩效总结里面骄傲地写上“设计了 XX 方案,达到了和 Google 相同的技术水平”“XX 方案的性能测试结果大大优于阿里集团的 YY 方案”。
但现实是,大部分这样想和这样做的架构,最后可能都以失败告终!
为什么会这样呢?再好的梦想,也需要脚踏实地实现!这里的“脚踏实地”主要体现在下面几个方面。
将军难打无兵之仗
大公司的分工比较细,一个小系统可能就是一个小组负责。而大部分公司,整个研发团队可能就100 多人,某个业务团队可能就十几个人。十几个人的团队,想做几十个人的团队的事情,而且 还要做得更好,不能说绝对不可能,但难度是可想而知的。
罗马不是一天建成的
业界领先的很多方案,其实并不是一堆天才某个时期灵机一动,然后加班加点就做出来的,而是 经过几年时间的发展才逐步完善和初具规模的。
没有那么多积累,却想一步登天,是失败的第二个主要原因。
冰山下面才是关键
可能有人认为,业界领先的方案都是天才创造出来的,所以自己也要造一个业界领先的方案,以此来证明自己也是天才。确实有这样的天才,但更多的时候,业界领先的方案其实都是“逼”出来的!简单来说,“业务”发展到一定阶段,量变导致了质变,出现了新的问题,已有的方式已经不能应对这些问题,需要用一种新的方案来解决,通过创新和尝试,才有了业界领先的方案。
没有那么卓越的业务场景,却幻想灵光一闪成为天才,是失败的第三个主要原因。
简单原则宣言:“简单优于复杂”。
软件架构设计是一门技术活。所谓技术活,从历史上看,无论是汽车,还是火车,飞机,我们用的手机,电脑,乃至冰箱等家电,无一不是越来越精细、越来越复杂。因此当我们进行架构设计时,会自然而然地想把架构做精美、做复杂,这样才能体现我们的技术实力,也才能够将架构做成一件艺术品。
团队的压力有时也会有意无意地促进我们走向复杂的方向,因为大部分人在评价一个方案水平高低的时候,复杂性是其中一个重要的参考指标。例如设计一个主备方案,如果我们用心跳来实现, 可能大家都认为这太简单了。但如果我们引入 ZooKeeper 来做主备决策,可能很多人会认为这个方案更加“高大上”一些,毕竟 ZooKeeper 使用的是 ZAB 协议,而 ZAB 协议本身就很复杂。 其实,真正理解 ZAB 协议的人很少,但并不妨碍我们都知道 ZAB 协议很优秀。
然而,“复杂”在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是“问题”。
软件领域的复杂性体现在两个方面:
结构的复杂性
结构复杂的系统几乎毫无例外具备两个特点:
1:组成复杂系统的组件数量更多;
2:同时这些组件之间的关系也更加复杂;
结构上的复杂性存在的第一个问题是,组件越多,就越有可能其中某个组件出现故障,从而导致系统故障。这个概率可以算出来,假设组件的故障率是 10%(有 10% 的时间不可用),那么有 3 个组件的系统可用性是(1-10%)×(1-10%)×(1-10%)= 72.9%,有 5 个组件的系统可 用性是(1-10%)×(1-10%)×(1-10%)×(1-10%)×(1-10%)=59%,两者的可用性 相差 13%。
结构上的复杂性存在的第二个问题是,某个组件改动,会影响关联的所有组件,这些被影响的组件同样会继续递归影响更多的组件。还以上面图中 5 个组件组成的系统为例,组件 A 修改或者异常时,会影响组件 B/C/E,D 又会影响 E。这个问题会影响整个系统的开发效率,因为一旦变更涉及外部系统,需要协调各方统一进行方案评估、资源协调、上线配合。
结构上的复杂性存在的第三个问题是,定位一个复杂系统中的问题总是比简单系统更加困难。首先是组件多,每个组件都有嫌疑,因此要逐一排查;其次组件间的关系复杂,有可能表现故障的组件并不是真正问题的根源。
逻辑的复杂性
意识到结构的复杂性后,我们的第一反应可能就是“降低组件数量”,毕竟组件数量越少,系统结构越简。最简单的结构当然就是整个系统只有一个组件,即系统本身,所有的功能和逻辑都在这一个组件中实现。
不幸的是,这样做是行不通的,原因在于除了结构的复杂性,还有逻辑的复杂性,即如果某个组件的逻辑太复杂,一样会带来各种问题。
逻辑复杂的组件,一个典型特征就是单个组件承担了太多的功能。以电商业务为例,常见的功能有:商品管理、商品搜索、商品展示、订单管理、用户管理、支付、发货、客服…把这些功能 全部在一个组件中实现,就是典型的逻辑复杂性。
但是,为什么复杂的电路就意味更强大的功能,而复杂的架构却有很多问题呢?根本原因在于电路一旦设计好后进入生产,就不会再变,复杂性只是在设计时带来影响;而一个软件系统在投入使用后,后续还有源源不断的需求要实现,因此要不断地修改系统,复杂性在整个系统生命周期中都有很大影响。
演化原则宣言:“演化优于一步到位”。
软件架构从字面意思理解和建筑结构非常类似,事实上“架构”这个词就是建筑领域的专业名词,维基百科对“软件架构”的定义中有一段话描述了这种相似性:从和目的、主题、材料和结构的联系上来说,软件架构可以和建筑物的架构相比拟。
但是软件架构与建筑架构有一个本质的差异,那就是建筑一旦完成(甚至一旦开建)就不可再变,而软件却需要根据业务的发展不断地变化!
对于软件来说,变化才是主题。软件架构需要根据业务的发展而不断变化。
如果没有把握“软件架构需要根据业务发展不断变化”这个本质,在做架构设计的时候就很容易陷入一个误区:试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。
合适优于先进>演化优于一步到位>简单优于复杂
合适也就是适应当前需要是首位的,连当前需求都满足不了谈不到其他。 架构整体发展是要不断演进的,在这个大前提下,尽量追求简单,但也有该复杂的时候,就要复杂,比如生物从单细胞一直演化到如今,复杂是避免不了的。
在设计架构时,不要妄想一步到位,考虑团队的情况,业务情况来设计架构,选出当下合适的架构,不要把架构设计的过于复杂,同时也要判断出后期的架构调整,扩容,业务需求的变化。
架构设计的本质目的是为了解决软件系统的复杂性,所以在我们设计架构时,首先就要分析系统的复杂性。只有正确分析出了系统的复杂性,后续的架构设计方案才不会偏离方向;否则,如果对系统的复杂性判断错误,即使后续的架构设计方案再完美再先进,都是南辕北辙,做的越多,错的越多、越离谱。
架构的复杂度主要来源于“高性能”“高可用”“可扩展”等几个方面,但架构师在具体判断复杂性的时候,不能生搬硬套,认为任何时候架构都必须同时满足这三方面的要求。实际上大部分场景下,复杂度只是其中的某一个,少数情况下包含其中两个,如果真的出现同时需要解决三个或者三个以上的复杂度,要么说明这个系统之前设计的有问题,要么可能就是架构师的判断出现了失误,即使真的认为要同时满足这三方面的要求,也必须要进行优先级排序。
识别复杂度对架构师来说是一项挑战,因为原始的需求中并没有哪个地方会明确地说明复杂度在哪里,需要架构师在理解需求的基础上进行分析。有经验的架构师可能一看需求就知道复杂度大概在哪里;如果经验不足,那只能采取“排查法”,从不同的角度逐一进行分析。
架构设计由需求所驱动,本质目的是为了解决软件系统的复杂性;为此,我们在进行架构设计时,需要以理解需求为前提,首要进行系统复杂性的分析。具体做法是:
构建复杂度的来源清单——高性能、可用性、扩展性、安全、低成本、规模等。
结合需求、技术、团队、资源等对上述复杂度逐一分析是否需要?是否关键?
“高性能”主要从软件系统未来的TPS、响应时间、服务器资源利用率等客观指标,也可以从用户的主观感受方面去考虑。
“可用性”主要从服务不中断等质量属性,符合行业政策、国家法规等方面去考虑。
“扩展性”则主要从功能需求的未来变更幅度等方面去考虑。
按照上述的分析结论,得到复杂度按照优先级的排序清单,越是排在前面的复杂度,就 越关键,就越优先解决。
需要特别注意的是:随着所处的业务阶段不同、外部的技术条件和环境的不同,得到的复杂度问题的优先级排序就会有所不同。一切皆变化。
对于架构师来说,常见系统的性能量级需要烂熟于心,例如nginx负载均衡性能是3万左右, mc的读取性能5万左右,kafka号称百万级,zookeeper写入读取2万以上,http请求访问大概在2万左右。具体的数值和机器配置以及测试案例有关,但大概的量级不会变化很大。如果是业务系统,由于业务复杂度差异很大,有的每秒500请求可能就是高性能了,因此需要针对业务进行性能测试,确立性能基线,方便后续架构设计做比较。
TPS | QPS计算:
我们将数据按照秒来计算,一天内平均每秒写入消息数为 115 条,每秒读取的消息数是 1150 条;再考虑系统的读写并不是完全平均的,设计的目标应该以峰值来计算。峰值一般取平均值的 3 倍,那么消息队列系统的 TPS 是 445,QPS 是 4450。峰值的倍数可以根据业务进行调整。
成熟的架构师需要对已经存在的技术非常熟悉,对已经经过验证的架构模式烂熟于心,然后根据自己对业务的理解,挑选合适的架构模式进行组合,再对组合后的方案进行修改和调整。
虽然软件技术经过几十年的发展,新技术层出不穷,但是经过时间考验,已经被各种场景验证过的成熟技术其实更多。例如,高可用的主备方案、集群方案,高性能的负载均衡、多路复用,可扩展的分层、插件化等技术,绝大部分时候我们有了明确的目标后,按图索骥就能够找到可选的解决方案。
虽说基于已有的技术或者架构模式进行组合,然后调整,大部分情况下就能够得到我们需要的方案,但并不意味着架构设计是一件很简单的事情。因为可选的模式有很多,组合的方案更多,往往一个问题的解决方案有很多个;如果再在组合的方案上进行一些创新,解决方案会更多。因此,如何设计最终的方案,并不是一件容易的事情,这个阶段也是很多架构师容易犯错的地方。
根据架构设计原则中“合适原则”和“简单原则“的要求,挑选合适自己业务、团队、技术能力的方案才是好方案;否则要么浪费大量资源开发了无用的系统(例如,某个平台的TPS峰值只有500却设计了 TPS 50000 的系统),要么根本无法实现。
很多架构师在做方案设计时,可能心里会简单地对几个方案进行初步的设想,再简单地判断哪个最好,然后就基于这个判断开始进行详细的架构设计了。
这样做有很多弊端:
因此,架构师需要设计多个备选方案,但方案的数量可以说是无穷无尽的,架构师也不可能穷举所有方案,那合理的做法应该是什么样的呢?
备选方案的数量以 3 ~ 5 个为最佳。少于 3 个方案可能是因为思维狭隘,考虑不周全;多于 5 个则需要耗费大量的精力和时间,并且方案之间的差别可能不明显。
备选方案的差异要比较明显。例如,主备方案和集群方案差异就很明显,或者同样是主备方 案,用 ZooKeeper 做主备决策和用 Keepalived 做主备决策的差异也很明显。但是都用 ZooKeeper 做主备决策,一个检测周期是 1 分钟,一个检测周期是 5 分钟,这就不是架构上的差异,而是细节上的差异了,不适合做成两个方案。
备选方案的技术不要只局限于已经熟悉的技术。设计架构时,架构师需要将视野放宽,考虑更多可能性。不要一味用自己熟悉的技术,对于新的技术有一种不放心的感觉。就像那句俗语说的:“如果你手里有一把锤子,所有的问题在你看来都是钉子”。例如,架构师对 MySQL 很熟悉,因此不管什么存储都基于 MySQL 去设计方案,系统性能不够了,首先考虑的就是 MySQL 分库分表,而事实上也许引入一个 Memcache 缓存就能够解决问题。
第三种常见的错误:备选方案过于详细
有的架构师或者设计师在写备选方案时,错误地将备选方案等同于最终的方案,每个备选方案都写得很细。这样做的弊端显而易见:
正确的做法是备选阶段关注的是技术选型,而不是技术细节,技术选型的差异要比较明显。例如,采用redis和memcached来做Nosql的存储差异就非常大。
经过架构设计流程第 1 步——识别复杂度,确定了系统面临的主要复杂度问题,进而明确了设计方案的目标,就可以开展架构设计流程第 2 步——设计备选方案。架构设计备选方案的工作更多的是从需求、团队、技术、资源等综合情况出发,对主流、成熟的架构模式进行选 择、组合、调整、创新。
几种常见的架构设计误区:
备选方案设计的注意事项:
问题思考:
除了从开源的角度来设计架构方案,根据架构设计的三原则,也可以考虑上云方案,使用阿里云,腾讯云,直接采用商业解决方案。例如,文件存储可以使用OSS,避免自身服务器的压力。
通过以上,再结合自己做技术研发的最大感触是:做事情最好是“三方案”,又叫“第三选择”,可以防止思维狭隘,目光短浅,思维盲区等决策陷阱。
在完成备选方案设计后,如何挑选出最终的方案也是一个很大的挑战,主要原因有:
正因为选择备选方案存在这些困难,所以实践中很多设计师或者架构师就采取了下面几种指导思想:
最简派
设计师挑选一个看起来最简单的方案。例如,我们要做全文搜索功能,方案 1 基于 MySQL,方 案 2 基于 Elasticsearch。MySQL 的查询功能比较简单,而 Elasticsearch 的倒排索引设计要复杂得多,写入数据到 Elasticsearch,要设计 Elasticsearch 的索引,要设计 Elasticsearch 的分 布式…全套下来复杂度很高,所以干脆就挑选 MySQL 来做吧。
最牛派
最牛派的做法和最简派正好相反,设计师会倾向于挑选技术上看起来最牛的方案。例如,性能最高的、可用性最好的、功能最强大的,或者淘宝用的、微信开源的、Google 出品的等。
最熟派
设计师基于自己的过往经验,挑选自己最熟悉的方案。以编程语言为例,假如设计师曾经是一个 C++ 经验丰富的开发人员,现在要设计一个运维管理系统,由于对 Python 或者 Ruby on Rails 不熟悉,因此继续选择 C++ 来做运维管理系统。
领导派
领导派就更加聪明了,列出备选方案,设计师自己拿捏不定,然后就让领导来定夺,反正最后方案选的对那是领导厉害,方案选的不对怎么办?那也是领导“背锅”。
其实这些不同的做法本身并不存在绝对的正确或者绝对的错误,关键是不同的场景应该采取不同的方式。也就是说,有时候我们要挑选最简单的方案,有时候要挑选最优秀的方案,有时候要挑选最熟悉的方案,甚至有时候真的要领导拍板。
有那么多指导思想,真正应该选择哪种方法来评估和选择备选方案呢?答案就是“360 度环评”!具体的操作方式为:列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。
常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。在评估这些质量属性时,需要遵循架构设计原则 1“合适原则”和原则 2“简单原则”,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了。
心得: 架构设计流程— —评估和选择备选方案
评估和选择备选方案的方法
按优先级选择,即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等 因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再 看第二优先级…以此类推。
RocketMQ 和 Kafka 有什么区别?
1.适用场景
Kafka适合日志处理;RocketMQ适合业务处理
2.性能
Kafka单机写入TPS号称在百万条/秒;RocketMQ大约在10万条/秒。Kafka单机性能更高
3.可靠性
RocketMQ支持异步/同步刷盘;异步/同步Replication;Kafka使用异步刷盘方式,异步Repli cation。RocketMQ所支持的同步方式提升了数据的可靠性
4.实时性
均支持pull长轮询,RocketMQ消息实时性更好
5.支持的队列数
Kafka单机超过64个队列/分区,消息发送性能降低严重;RocketMQ单机支持最高5万个队 列,性能稳定(这也是适合业务处理的原因之一)
简单来说,详细方案设计就是将方案涉及的关键技术细节给确定下来。
假如我们确定使用 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 策略是比较合适的。
详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性。
这种情况可以通过下面方式有效地避免:
架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。例如,架构师选择了 Elasticsearch 作为全文搜索解决方案,前提必须是架构师自己对 Elasticsearch 的设计原理有深入的理解,比如索引、副本、集群等技术点;而不能道听途说 Elasticsearch 很牛,所以选择它,更不能成为把“细节我们不讨论”这句话挂在嘴边 的“PPT 架构师”。
通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度,方案本身的复杂度越高,某个 细节推翻整个方案的可能性就越高,适当降低复杂性,可以减少这种风险。
如果方案本身就很复杂,那就采取设计团队的方式来进行设计,博采众长,汇集大家的智慧和经验,防止只有 1~2 个架构师可能出现的思维盲点或者经验盲区。
在确定架构后,对架构进行详细的设计时,根据业务选择出当前技术的合适点,对每个技术的基本原理,优点缺点,关键设计点,架构师至少要安装过,编写demo体验过,确定选型后,要进行性能和可用性测试例如es的索性设计就是关键设计点。
高性能数据库集群的第一种方式是“读写分离”,其本质是将访问压力分散到集群中的多个节点,但是没有分散存储压力。第二种方式是“分库分表”,即可以分散访问压力,也可分散存储压力。
读写分离原理是将数据库读写操作分散到不同的节点,下图是读写分离架构基本架构图。
读写分离基本实现是:
读写分离的实现逻辑并不复杂,但有两个细节点将引入设计复杂度:复制延迟和分配机制
以Mysql为例,主从复杂延迟可能达到1秒,如果有大量数据同步,延迟1分钟也是有可能的。主从复制延迟会带来一个问题:就是主机写入后,立刻读取从机,此时主机还没有将数据复制过来,导致读取从机的数据不是最新的数据,业务上就可能会出现问题。
解决主从复制延迟几种常见的方法:
写操作后的读操作指定发送给数据库主机
例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,如果哪个新来的程序员不知道这样写代码,就会导致一个bug。
读从机失败后再读一次主机
这就是通常所说的“二次读取”,二次读取和业务无绑定,只需要对底层数据库访问的API进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。
关键业务读写操作全部指向主机,非关键业务采用读写分离
例如,对于一个用户管理系统来说,注册+登录的业务读写操作全部访问主机,用户的介绍、爱好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。
将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。
程序代码封装
程序代码封装指在代码中抽象一个数据访问层(所以有的文章也称这种方式为“中间层封装”),实现读写操作分离和数据库服务器连接的管理。例如,基于Hibernate进行简单封装,就可以实现读写分离,基本架构如下图:
程序代码封装的方式具备几个特点:
目前开源的实现方案中,淘宝的TDDL(Taobao Distributed Data Layer,外号:头都大了)是比较有名的。它是一个通用数据访问层,所有功能封装在jar包中提供给业务代码调用。其基本原理是一个基于集中式配置的 jdbc datasource实现,具有主备、读写分离、动态数据库配置等功能,基本架构如下图:
中间件封装
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供SQL兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。其基本架构如下图:
数据库中间件的方式具备的特点是:
目前的开源数据库中间件方案中,MySQL官方先是提供了MySQL Proxy,但MySQL Proxy一直没有正式GA,现在MySQL官方推MySQL Router。MySQL Router的主要功能有读写分离、故障自动切换、负载均衡、连接池等,其基本架构如下图。
读写分离适用于单机并发无法支撑读取请求更多的情形。在单机的情况下,给表增加索引会影响写入的速度,读写分离后可以单独对从库进行优化,主机检索索引,对读写性能都有提示,而且对读取的性能提示的更高一些。
不适用情况:
读写分离分散了数据库读取操作的压力,但没有分散存储压力,当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面:
基于上述原因,单个数据库服务器存储的数据量不能太大,需要控制在一定的范围内。为了满足业务数据存储的需求,就需要将存储分散到多台数据库服务器上。
常见的分散存储的方法“分库分表”,其中包括“分库”和“分表”两大类。
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。
虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题:
join操作问题
业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用SQL的join查询。
事务问题
原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。虽然数据库厂商提供了一些分布式事务的解决方案(例如,MySQL的XA),但性能实在太低,与高性能存储的目标是相违背的。
成本问题
业务分库同时也带来了成本的代价,本来1台服务器搞定的事情,现在要3台,如果考虑备份,那就是2台变成了6台。
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。
单表数据拆分有两种方式:垂直分表和水平分表
单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定,并不强制要求单表切分为多表后一定要分散到不同数据库中。原因在于单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,是可以不拆分到多台数据库服务器的,毕竟我们在上面业务分库的内容看到业务分库也会引入很多复杂性的问题;如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了。
分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性。
垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,用户表的nickname和description字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用age和sex两个字段进行查询,而nickname和description两个字段主要用于展示,一般不会在业务查询中用到。description本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询age和sex时,就能带来一定的性能提升。
垂直分表引入的复杂性主要体现在表操作的数量要增加。例如,原来只要一次查询就可以获取name、age、sex、nickname、description,现在需要两次查询,一次查询获取name、age、sex,另外一次查询获取nickname、description。
水平分表
水平分表适合表行数特别大的表,有的公司要求单表行数超过5000万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过1000万就要分表了;而对于一些简单的表,即使存储数据超过1亿行,也可以不分表。但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶颈或者隐患。
水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:
1:路由
水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性
常见的路由算法有:
范围路由:选取有序的数据列(例如,整形、时间戳等)作为路由的条件,不同分段分散到不同的数据库表中。以最常见的用户ID为例,路由算法可以按照1000000的范围大小进行分段,1 ~ 999999放到数据库1的表中,1000000 ~ 1999999放到数据库2的表中,以此类推。
范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在100万至2000万之间,具体需要根据业务选取合适的分段大小。
范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是100万,如果增加到1000万,只需要增加新的表就可以了,原有的数据不需要动。
范围路由的一个比较隐含的缺点是分布不均匀,假如按照1000万来进行分表,有可能某个分段实际存储的数据量只有1000条,而另外一个分段实际存储的数据量有900万条。
Hash路由:选取某个列(或者某几个列组合也可以)的值进行Hash运算,然后根据Hash结果分散到不同的数据库表中。同样以用户ID为例,假如我们一开始就规划了10个数据库表,路由算法可以简单地用user_id % 10的值来表示数据所属的数据库表编号,ID为985的用户放到编号为5的子表中,ID为10086的用户放到编号为6的字表中。
Hash路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了Hash路由后,增加字表数量是非常麻烦的,所有数据都要重分布。
Hash路由的优缺点和范围路由基本相反,Hash路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。
配置路由:配置路由就是路由表,用一张独立的表来记录路由信息。同样以用户ID为例,我们新增一张user_router表,这个表包含user_id和table_id两列,根据user_id就可以查询对应的table_id。
配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。
配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据),性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。
2:join操作:
水平分表后,数据分散在多个表中,如果需要与其他表进行join查询,需要在业务代码或者数据库中间件中进行多次join查询,然后将结果合并。
3:count()操作:
水平分表后,虽然物理上数据分散到多个表中,但某些业务逻辑上还是会将这些表当作一个表来处理。例如,获取记录总数用于分页或者展示,水平分表前用一个count()就能完成的操作,在分表后就没那么简单了。常见的处理方式有下面两种:
count()相加:具体做法是在业务代码或者数据库中间件中对每个表进行count()操作,然后将结果相加。这种方式实现简单,缺点就是性能比较低。例如,水平分表后切分为20张表,则要进行20次count(*)操作,如果串行的话,可能需要几秒钟才能得到结果。
记录数表:具体做法是新建一张表,假如表名为“记录数表”,包含table_name、row_count两个字段,每次插入或者删除子表数据成功后,都更新“记录数表”。这种方式获取表记录数的性能要大大优于count()相加的方式,因为只需要一次简单查询就可以获取数据。缺点是复杂度增加不少,对子表的操作要同步操作“记录数表”,如果有一个业务逻辑遗漏了,数据就会不一致;且针对“记录数表”的操作和针对子表的操作无法放在同一事务中进行处理,异常的情况下会出现操作子表成功了而操作记录数表失败,同样会导致数据不一致。此外,记录数表的方式也增加了数据库的写压力,因为每次针对子表的insert和delete操作都要update记录数表,所以对于一些不要求记录数实时保持精确的业务,也可以通过后台定时更新记录数表。定时更新实际上就是“count()相加”和“记录数表”的结合,即定时通过count()相加计算表的记录数,然后更新记录数表中的数据。
3:order by操作:
水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。
实现方法:
和数据库读写分离类似,分库分表具体的实现方式也是“程序代码封装”和“中间件封装”,但实现会更复杂。读写分离实现时只要识别SQL操作是读操作还是写操作,通过简单的判断SELECT、UPDATE、INSERT、DELETE几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断SQL中具体需要操作的表、操作函数(例如count函数)、order by、group by操作等,然后再根据不同的操作进行不同的处理。例如order by操作,需要先从多个库查询到各个库的数据,然后再重新order by才能得到最终的结果。
分库分表带来的问题,要么使用数据库中间件要么代码中间层来解决。业务程序处理总是很麻烦。
那么什么时候引入分库分表是合适的?是数据库性能不够的时候就开始分库分表么?
关系数据库经过几十年的发展后已经非常成熟,强大的SQL功能和ACID的属性,使得关系数据库广泛应用于各式各样的系统中,但这并不意味着关系数据库是完美的,关系数据库存在如下缺点。
关系数据库存储的是行记录,无法存储数据结构
以微博的关注关系为例,“我关注的人”是一个用户ID列表,使用关系数据库存储只能将列表拆成多行,然后再查询出来组装,无法直接存储一个列表。
关系数据库的schema(模式)扩展很不方便
关系数据库的表结构schema是强约束,操作不存在的列会报错,业务变化时扩充列也比较麻烦,需要执行DDL(data definition language,如CREATE、ALTER、DROP等)语句修改,而且修改时可能会长时间锁表(例如,MySQL可能将表锁住1个小时)
关系数据库在大数据场景下I/O较高
如果对一些大量数据的表进行统计之类的运算,关系数据库的I/O会很高,因为即使只针对其中某一列进行运算,关系数据库也会将整行数据从存储设备读入内存。
关系数据库的全文搜索功能比较弱
关系数据库的全文搜索只能使用like进行整表扫描匹配,性能非常低,在互联网这种搜索复杂的场景下无法满足业务要求。
针对上述问题,分别诞生了不同的NoSQL解决方案,这些方案与关系数据库相比,在某些应用场景下表现更好。但是没有完美的技术产品,NoSQL方案带来的优势,本质上是牺牲ACID中的某个或者某几个特性,因此我们不能盲目地迷信NoSQL是银弹,而应该将NoSQL作为SQL的一个有力补充,NoSQL != No SQL,而是NoSQL = Not Only SQL。
各种高性能NoSQL方案的典型特征和应用场景:
K-V存储
K-V存储的全称是Key-Value存储,其中Key是数据的标识,和关系数据库中的主键含义一样,Value就是具体的数据。Redis是K-V存储的典型代表,它是一款开源(基于BSD许可)的高性能K-V缓存和存储系统。
Redis的Value是具体的数据结构,包括string、hash、list、set、sorted set、bitmap和hyperloglog,所以常常被称为数据结构服务器
以List数据结构为例,Redis提供了下面这些典型的操作:
以上这些功能,如果用关系数据库来实现,就会变得很复杂。例如,LPOP操作是移除并返回 key对应的list的第一个元素。如果用关系数据库来存储,为了达到同样目的,需要进行下面的操作:
可以看出关系数据库的实现很麻烦,而且需要进行多次SQL操作,性能很低
Redis的缺点主要体现在并不支持完整的ACID事务,Redis虽然提供事务功能,但Redis的事务和关系数据库的事务不可同日而语,Redis的事务只能保证隔离性和一致性(I和C),无法保证原子性和持久性(A和D)。
虽然Redis并没有严格遵循ACID原则,但实际上大部分业务也不需要严格遵循ACID原则。以微博关注操作为例,即使系统没有将A加入B的粉丝列表,其实业务影响也非常小,因此我们在设计方案时,需要根据业务特性和要求来确定是否可以用Redis,而不能因为Redis不遵循ACID原则就直接放弃。
文档数据库
为了解决关系数据库schema带来的问题,文档数据库应运而生。文档数据库最大的特点就是no-schema,可以存储和读取任意的数据。目前绝大部分文档数据库存储的数据格式是JSON(或者BSON),因为JSON数据是自描述的,无须在使用前定义字段,读取一个JSON中不存在的字段也不会导致SQL那样的语法错误。
文档数据库的no-schema特性,给业务开发带来了几个明显的优势:
新增字段简单
业务上增加新的字段,无须再像关系数据库一样要先执行DDL语句修改表结构,程序代码直接读写即可
历史数据不会出错
对于历史数据,即使没有新增的字段,也不会导致错误,只会返回空值,此时代码进行兼容处理即可
可以很容易存储复杂数据
JSON是一种强大的描述语言,能够描述复杂的数据结构
文档数据库的这个特点,特别适合电商和游戏这类的业务场景。即使是同类商品也有不同的属性。例如,LCD和LED显示器,两者有不同的参数指标。这种业务场景如果使用关系数据库来存储数据,就会很麻烦,而使用文档数据库,会简单、方便许多,扩展新的属性也更加容易。
文档数据库no-schema的特性带来的这些优势也是有代价的,最主要的代价就是不支持事务。例如,创建订单,减库存,如果使用关系数据库的事务就很简单了,但是用文档数据库就非常麻烦了,很难保证一致性。因此某些对事务要求严格的业务场景是不能使用文档数据库的。
文档数据库另外一个缺点就是无法实现关系数据库的join操作。例如,我们有一个用户信息表和一个订单表,订单表中有买家用户id。如果要查询“购买了苹果笔记本用户中的女性用户”,用关系数据库来实现,一个简单的join操作就搞定了;而用文档数据库是无法进行join查询的,需要查两次:一次查询订单表中购买了苹果笔记本的用户,然后再查询这些用户哪些是女性用户。
列式数据库
顾名思义,列式数据库就是按照列来存储数据的数据库,与之对应的传统关系数据库被称为“行式数据库”,因为关系数据库是按照行来存储数据的。
关系数据库按照行式来存储数据,主要有以下几个优势:
可以看到,行式存储的优势是在特定的业务场景下才能体现,如果不存在这样的业务场景,那么行式存储的优势也将不复存在,甚至成为劣势,典型的场景就是海量数据进行统计。例如,计算某个城市年龄超过60的人员数据,实际上只需要读取年龄这一列的数据进行统计即可。而行式存储即使最终只使用一列,也会将所有行数据都读取出来。如果单行用户信息有1KB,其中体重只有4个字节,行式存储还是会将整行1KB数据全部读取到内存中,这是明显的浪费。而如果采用列式存储,每个用户只需要读取4字节的体重数据即可,I/O将大大减少。
除了节省I/O,列式存储还具备更高的存储压缩比,能够节省更多的存储空间。普通的行式数据库一般压缩率在3:1到5:1左右,而列式数据库的压缩率一般在8:1到30:1左右,因为单个列的数据相似度相比行来说更高,能够达到更高的压缩率。
同样,如果场景发生变化,列式存储的优势又会变成劣势。典型的场景是需要频繁地更新多个列。因为列式存储将不同列存储在磁盘上不连续的空间,导致更新多个列时磁盘是随机写操作;而行式存储时同一行多个列都存储在连续的空间,一次磁盘写操作就可以完成,列式存储的随机写效率要远远低于行式存储的写效率。此外,列式存储高压缩率在更新场景下也会成为劣势,因为更新时需要将存储数据解压后更新,然后再压缩,最后写入磁盘。
基于上述列式存储的优缺点,一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景主要是针对部分列单列进行操作,且数据写入后就无须再更新删除。
全文搜索引擎
传统的关系型数据库通过索引来达到快速查询的目的,但是在全文搜索的业务场景下,索引也无能为力,主要体现在:
全文搜索基本原理:
全文搜索引擎的技术原理被称为“倒排索引”(Inverted index),也常被称为反向索引、置入档案或反向档案,是一种索引方法,其基本原理是建立单词到文档的索引。之所以被称为“倒排”索引,是和“正排“索引相对的,“正排索引”的基本原理是建立文档到单词的索引。
正排索引适用于根据文档名称来查询文档内容。例如,用户在网站上单击了“面向对象葵花宝典是什么”,网站根据文章标题查询文章的内容展示给用户。
倒排索引适用于根据关键词来查询文档内容。例如,用户只是想看“设计”相关的文章,网站需要将文章内容中包含“设计”一词的文章都搜索出来展示给用户。通俗的来讲就是根据文档中包含的内容,来搜索。
全文搜索引擎能够基于JSON文档建立全文索引,然后快速进行全文搜索。以Elasticsearch为例,其索引基本原理如下:
如何合理的使用NoSql:
缓存是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。
缓存能够带来性能的大幅提升,以Memcache为例,单台Memcache服务器简单的key-value查询能够达到TPS 50000以上,其基本的架构如下图”Memcache基本架构图“
缓存虽然能够大大减轻存储系统的压力,但同时也给架构引入了更多复杂性。架构设计时如果没有针对缓存的复杂性进行处理,某些场景下甚至会导致整个系统崩溃。
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:
存储数据不存在
第一种情况是被访问的数据确实不存在。一般情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。
通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。
这种情况的解决办法比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。
缓存数据生成耗费大量时间或者资源
第二种情况是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
缓存雪崩的常见解决方法有两种:更新锁机制和后台更新机制
更新锁
对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
对于采用分布式集群的业务系统,由于存在几十上百台服务器,即使单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如ZooKeeper。
后台更新
由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。
后台定时机制需要考虑一种特殊的场景,当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:
后台更新既适应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些。后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。
缓存热点
虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如,某明星微博发布“我们”来宣告恋爱了,短时间内上千万的用户都会来围观。
缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。
缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。
由于缓存的各种访问策略和存储的访问策略是相关的,因此上面的各种缓存设计方案通常情况下都是集成在存储访问方案中,可以采用“程序代码实现”的中间层方式,也可以采用独立的中间件来实现。
好的缓存设计方案应该从以下几个方面入手:
高性能是每个程序员的追求,无论我们是做一个系统还是写一行代码,都希望能够达到高性能的效果,而高性能又是最复杂的一环,磁盘、操作系统、CPU、内存、缓存、网络、编程语言、架构等,每个都有可能影响系统达到高性能,一行不恰当的debug日志,就可能将服务器的性能从TPS 30000降低到8000;因此,要做到高性能计算是一件很复杂很有挑战的事情,软件系统开发过程中的不同阶段都关系着高性能最终是否能够实现。
高性能架构设计主要集中在两方面:
除了以上两点,最终系统能否实现高性能,还和具体的实现及编码相关。但架构设计是高性能的基础,如果架构设计没有做到高性能,则后面的具体实现和编码能提升的空间是有限的。形象地说,架构设计决定了系统性能的上限,实现细节决定了系统性能的下限。
单服务器高性能的关键之一就是服务器采取的并发模型,并发模型有如下两个关键设计点:
以上两个设计点最终都和操作系统的I/O模型及进程模型相关
PPC是Process Per Connection的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的UNIX网络服务器所采用的模型。
PPC模式中,当连接进来时才fork新进程来处理连接请求,由于fork进程代价高,用户访问时可能感觉比较慢,prefork模式的出现就是为了解决这个问题。
顾名思义,prefork就是提前创建进程(pre-fork)。系统在启动的时候就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去fork进程的操作,让用户访问更快、体验更好。
TPC是Thread Per Connection的缩写,其含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。与进程相比,线程更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的,线程通信相比进程通信更简单。因此,TPC实际上是解决或者弱化了PPC fork代价高的问题和父子进程通信复杂的问题。
单服务器无论如何优化,无论采用多好的硬件,总会有一个性能天花板,当单服务器的性能无法满足业务需求时,就需要设计高性能集群来提升系统整体的处理性能。
高性能集群的本质很简单,通过增加更多的服务器来提升系统整体的计算能力。由于计算本身存在一个特点:同样的输入数据和逻辑,无论在哪台服务器上执行,都应该得到相同的输出。因此高性能集群设计的复杂度主要体现在任务分配这部分,需要设计合理的任务分配策略,将计算任务分配到多台服务器上执行。
高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法。对于任务分配器,现在更流行的通用叫法是“负载均衡器”。但这个名称有一定的误导性,会让人潜意识里认为任务分配的目的是要保持各个计算单元的负载达到均衡状态。而实际上任务分配并不只是考虑计算单元的负载均衡,不同的任务分配算法目标是不一样的,有的基于负载考虑,有的基于性能(吞吐量、响应时间)考虑,有的基于业务考虑。需要牢记,负载均衡不只是为了计算单元的负载达到均衡状态。
常见的负载均衡系统包括3种:DNS负载均衡、硬件负载均衡和软件负载均衡
DNS是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。例如,北方的用户访问北京的机房,南方的用户访问深圳的机房。DNS负载均衡的本质是DNS解析同一个域名可以返回不同的IP地址。例如,同样是www.baidu.com,北方用户解析后获取的地址是61.135.165.224(这是北京机房的IP),南方用户解析后获取的地址是14.215.177.38(这是深圳机房的IP)。
DNS负载均衡实现简单、成本低,但也存在粒度太粗、负载均衡算法少等缺点。仔细分析一下优缺点
优点有:
缺点有:
硬件负载均衡是通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。目前业界典型的硬件负载均衡设备有两款:F5和A10。这类设备性能强劲、功能强大,但价格都不便宜,一般只有“土豪”公司才会考虑使用此类设备。
硬件负载均衡的优点是:
硬件负载均衡的缺点是:
软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有Nginx和LVS,其中Nginx是软件的7层负载均衡,LVS是Linux内核的4层负载均衡。4层和7层的区别就在于协议和灵活性,Nginx支持HTTP、E-mail协议;而LVS是4层负载均衡,和协议无关,几乎所有应用都可以做,例如,聊天、数据库等。
软件负载均衡的优点:
和硬件负载均衡,软件负载均衡有以下缺点:
3种常见的负载均衡机制:DNS负载均衡、硬件负载均衡、软件负载均衡,每种方式都有一些优缺点,但并不意味着在实际应用中只能基于它们的优缺点进行非此即彼的选择,反而是基于它们的优缺点进行组合使用。具体来说,组合的基本原则为:DNS负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。
整个系统的负载均衡分为三层:
需要注意的是,这只是一个示例,一般在大型业务场景下才会这样用,如果业务量没这么大,则没有必要严格照搬这套架构。例如,一个大学的论坛,完全可以不需要DNS负载均衡,也不需要F5设备,只需要用Nginx作为一个简单的负载均衡就足够了。
每种负载均衡方式有其优缺点,根据实际的情况,负载情况来选择合适的负载均衡,可以组合3种方式的负载均衡。
负载均衡算法数量较多,而且可以根据一些业务特性进行定制开发,抛开细节上的差异,根据算法期望达到的目的,大体上可以分为下面几类:
负载均衡系统收到请求后,按照顺序轮流分配到服务器上。
轮询是最简单的一个策略,无须关注服务器本身的状态,例如:
负载均衡系统根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配置的,采用动态的方式计算会更加契合业务,但复杂度也会更高。
加权轮询是轮询的一种特殊形式,其主要目的就是为了解决不同服务器处理能力有差异的问题。例如,集群中,新采购的集群是32核的,而老机器是16核的,那么我们可以假设新机器的性能是老机器的2倍,负载均衡系统可以按照2:1的比列分配更多的任务给新机器,从而充分利用机器的性能。
加权轮询解决了轮询算法中无法根据服务器的配置差异进行任务分配的问题,但同样存在无法根据服务器的状态差异进行任务分配的问题。
负载均衡系统将任务分配给当前负载最低的服务器,这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量。例如:
最少连接数优先的算法要求负载均衡系统统计每个服务器当前建立的连接,其应用场景仅限于负载均衡接收的任何连接请求都会转发给服务器进行处理,如果负载均衡系统和服务器之间是固定的连接池方式,就不适合采取这种算法。例如,LVS可以采取这种算法进行负载均衡,而一个通过连接池的方式连接MySQL集群的负载均衡系统就不适合采取这种算法进行负载均衡。
CPU负载最低优先的算法要求负载均衡系统以某种方式收集每个服务器的CPU负载,而且要确定是以1分钟的负载为标准,还是以15分钟的负载为标准,不存在1分钟肯定比15分钟要好或者差。不同业务最优的时间间隔是不一样的,时间间隔太短容易造成频繁波动,时间间隔太长又可能造成峰值来临时响应缓慢。
负载最低优先算法基本上能够比较完美地解决轮询算法的缺点,因为采用这种算法后,负载均衡系统需要感知服务器当前的运行状态。当然,其代价是复杂度大幅上升。通俗来讲,轮询可能是5行代码就能实现的算法,而负载最低优先算法可能要1000行才能实现,甚至需要负载均衡系统和服务器都要开发代码。负载最低优先算法如果本身没有设计好,或者不适合业务的运行特点,算法本身就可能成为性能的瓶颈,或者引发很多莫名其妙的问题。所以负载最低优先算法虽然效果看起来很美好,但实际上真正应用的场景反而没有轮询(包括加权轮询)那么多。
负载最低优先类算法是站在服务器的角度来进行分配的,而性能最优优先类算法则是站在客户端的角度来进行分配的,优先将任务分配给处理速度最快的服务器,通过这种方式达到最快响应客户端的目的。
和负载最低优先类算法类似,性能最优优先类算法本质上也是感知了服务器的状态,只是通过响应时间这个外部标准来衡量服务器状态而已。因此性能最优优先类算法存在的问题和负载最低优先类算法类似,复杂度都很高,主要体现在:
负载均衡系统根据任务中的某些关键信息进行Hash运算,将相同Hash值的请求分配到同一台服务器上,这样做的目的主要是为了满足特定的业务需求。例如:
源地址Hash
将来源于同一个源IP地址的任务分配给同一个服务器进行处理,适合于存在事务、会话的业务。例如,当我们通过浏览器登录网上银行时,会生成一个会话信息,这个会话是临时的,关闭浏览器后就失效。网上银行后台无须持久化会话信息,只需要在某台服务器上临时保存这个会话就可以了,但需要保证用户在会话存在期间,每次都能访问到同一个服务器,这种业务场景就可以用源地址Hash来实现。
ID Hash
将某个ID标识的业务分配到同一个服务器中进行处理,这里的ID一般是临时性数据的ID(如session id)。例如,用户登录的例子,用session id hash同样可以实现同一个会话期间,用户每次都是访问到同一台服务器的目的。
CAP定理(CAP定理)又被称布鲁尔定理(Brewer定理),是分布式计算领域公认的一个定理。对于设计分布式系统的架构师来说,CAP是必须掌握的理论。
在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。
一致性(Consistency)
对指定的客户端来说,读取操作保证能够返回最新的写入操作结果。
注:从客户端client的读写角度来描述一致性,定义更加精确。这就意味着实际上对于节点来说,可能同一时刻拥有不同数据(same time + different data),但是只要客户端访问能够拿到最新的数据就OK。
强调client读操作能够获取最新的写结果就没有问题,因为事务在执行过程中,client是无法读取到未提交的数据的,只有等到事务提交后,client才能读取到事务写入的数据,而如果事务失败则会进行回滚,client也不会读取到事务中间写入的数据。
可用性(Availability)
非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)
注:明确了不能超时、不能出错,结果是合理的,注意没有说“正确”的结果。例如,应该返回100但实际上返回了90,肯定是不正确的结果,但可以是一个合理的结果。
分区容忍性(Partition Tolerance)
当出现网络分区后,系统能够继续“履行职责”
注:即使发生了分区现象,不管是什么原因,可能是丢包,也可能是连接中断,断网,网卡坏了,还可能是拥塞,只要导致了网络分区,就通通算在里面。通俗的来讲就是网络发生了故障。
虽然CAP理论定义是三个要素中只能取两个,但放到分布式环境下来思考,我们会发现必须选择P「分区容忍」要素,因为网络本身无法做到100%可靠,有可能出故障,所以分区是一个必然的现象。如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证C,系统需要禁止写入,当有写入请求时,系统返回error(例如,当前系统不允许写入),这又和A冲突了,因为A要求返回no error和no timeout。因此,分布式系统理论上不可能选择CA「一致性+可用性」架构,只能选择CP「一致性+分区容忍性」或者AP「可用性+分区容忍性」架构。
CAP定理是分布架构必须要遵守的规则,然而无法同时保证,必须舍弃其中一个,因为网络无法做到100%的可靠,所有P(分区容忍性) 是必须的,那么我们在选择的时候就需要根据业务需要来选择使用CP「一致性+分区容忍性」架构,还是AP「可用性+分区容忍性」架构。如果是对数据的一致性要求高,例如电商平台,那么选择CP架构,保证数据的一致性。如果是今日头条这类的平台那么选择AP架构,保证系统的可用性。
C「一致性」和 A「 可用性」应该是站在客户端的角度来审视,而「P 分区容忍性」则是站在集群node 在遇到网络分区(即网络故障)的问题时,选择如何去影响客户端的C「一致性」和A「可用性」。
CAP关注的粒度是数据,而不是整个系统
在实际设计过程中,每个系统不可能只处理一种数据,而是包含多种类型的数据,有的数据必须选择CP,有的数据必须选择AP。而如果我们做设计时,从整个系统的角度去选择CP还是AP,就会发现顾此失彼,无论怎么做都是有问题的。
以一个最简单的用户管理系统为例,用户管理系统包含用户账号数据(用户ID、密码)、用户信息数据(昵称、兴趣、爱好、性别、自我介绍等)。通常情况下,用户账号数据会选择CP,而用户信息数据会选择AP,如果限定整个系统为CP,则不符合用户信息数据的应用场景;如果限定整个系统为AP,则又不符合用户账号数据的应用场景
CAP是忽略网络延迟的
这是一个非常隐含的假设,CAP作者在定义一致性时,并没有将延迟考虑进去。也就是说,当事务提交时,数据能够瞬间复制到所有节点。但实际情况下,从节点A复制数据到节点B,总是需要花费一定时间的。如果是相同机房,耗费时间可能是几毫秒;如果是跨地域的机房,例如北京机房同步到广州机房,耗费的时间就可能是几十毫秒。这就意味着,CAP理论中的C在实践中是不可能完美实现的,在数据复制的过程中,节点A和节点B的数据并不一致。
正常运行情况下,不存在CP和AP的选择,可以同时满足CA
CAP理论告诉我们分布式系统只能选择CP或者AP,但其实这里的前提是系统发生了“分区”现象。如果系统没有发生分区现象,也就是说P不存在的时候(节点间的网络连接一切正常),我们没有必要放弃C或者A,应该C和A都可以保证,这就要求架构设计的时候既要考虑分区发生时选择CP还是AP,也要考虑分区没有发生时如何保证CA。
放弃并不等于什么都不做,需要为分区恢复后做准备
CAP理论告诉我们三者只能取两个,需要“牺牲”(sacrificed)另外一个,这里的“牺牲”是有一定误导作用的,因为“牺牲”让很多人理解成什么都不做。实际上,CAP理论的“牺牲”只是说在分区过程中我们无法保证C或者A,但并不意味着什么都不做。因为在系统整个运行周期中,大部分时间都是正常的,发生分区现象的时间并不长。例如,99.99%可用性(俗称4个9)的系统,一年运行下来,不可用的时间只有50分钟;99.999%(俗称5个9)可用性的系统,一年运行下来,不可用的时间只有5分钟。分区期间放弃C或者A,并不意味着永远放弃C和A,我们可以在分区期间进行一些操作,从而让分区故障解决后,系统能够重新达到CA的状态。
BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency),核心思想是即使无法做到强一致性(CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。
基本可用(Basically Available)
分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
软状态(Soft State)
也叫作弱状态或柔性状态。许多系统存在中间状态,而该中间状态不会影响系统整体可用性。比如,订单系统,在下单完成进行支付的过程中,我们可以让页面显示“支付中”,等待支付系统彻底同步完数据,订单系统才显示支付完成。允许系统存在中间状态,这个中间状态又不会影响系统整体可用性。 这里的中间状态就是CAP理论中的数据不一致。
最终一致性(Eventual Consistency)
系统中的所有数据节点经过一定时间后,最终能够达到一致的状态。
这里的关键词是“一定时间” 和 “最终”,“一定时间”和数据的特性是强关联的,不同的数据能够容忍的不一致时间是不同的。举一个微博系统的例子,用户账号数据最好能在1分钟内就达到一致状态,因为用户在A节点注册或者登录后,1分钟内不太可能立刻切换到另外一个节点,但10分钟后可能就重新登录到另外一个节点了;而用户发布的最新微博,可以容忍30分钟内达到一致状态,因为对于用户来说,看不到某个明星发布的最新微博,用户是无感知的,会认为明星没有发布微博。“最终”的含义就是不管多长时间,最终还是要达到一致性的状态。
BASE理论本质上是对CAP的延伸和补充,更具体地说**,是对CAP中AP方案的一个补充**。在剖析CAP理论时,提到了其实和BASE相关的两点:
CAP理论是忽略延时的,而实际应用中延时是无法避免的
这一点就意味着完美的CP场景是不存在的,即使是几毫秒的数据复制延迟,在这几毫秒时间间隔内,系统是不符合CP要求的。因此CAP中的CP方案,实际上也是实现了最终一致性,只是“一定时间”是指几毫秒而已。
AP「一致性+分区容忍性」方案中牺牲一致性只是指分区期间,而不是永远放弃一致性
这一点其实就是BASE理论延伸的地方,分区期间牺牲一致性,但分区故障恢复后,系统应该达到最终一致性。
分布是系统无法保证保证数据完全一致性,只能保证最终一致性。
FMEA「Failure mode and effects analysis,故障模式与影响分析」又称为失效模式与后果分析、失效模式与效应分析、故障模式与后果分析等,因为这个中文翻译更加符合可用性的语境。FMEA是一种在各行各业都有广泛应用的可用性分析方法,通过对系统范围内潜在的故障模式加以分析,并按照严重程度进行分类,以确定失效对于系统的最终影响。
在架构设计领域,FMEA的具体分析方法是:
FMEA分析的方法其实就是一个FMEA分析表,常见的FMEA分析表格包含下面部分:
功能点
前的FMEA分析涉及的功能点,注意这里的“功能点”指的是从用户角度来看的,而不是从系统各个模块功能点划分来看的。例如,对于一个用户管理系统,使用FMEA分析时 “登录”“注册”才是功能点,而用户管理系统中的数据库存储功能、Redis缓存功能不能作为FMEA分析的功能点。
故障模式
故障模式指的是系统会出现什么样的故障,包括故障点和故障形式。需要特别注意的是,这里的故障模式并不需要给出真正的故障原因,我们只需要假设出现某种故障现象即可,例如MySQL响应时间达到3秒。造成MySQL响应时间达到3秒可能的原因很多:磁盘坏道、慢查询、服务器到MySQL的连接网络故障、MySQL bug等,我们并不需要在故障模式中一一列出来,而是在后面的“故障原因”一节中列出来。因为在实际应用过程中,不管哪种原因,只要现象是一样的,对业务的影响就是一样的。
此外,故障模式的描述要尽量精确,多使用量化描述,避免使用泛化的描述。例如,推荐使用“MySQL响应时间达到3秒”,而不是“MySQL响应慢”。
故障影响
当发生故障模式中描述的故障时,功能点具体会受到什么影响。常见的影响有:功能点偶尔不可用、功能点完全不可用、部分用户功能点不可用、功能点响应缓慢、功能点出错等。
故障影响也需要尽量准确描述。例如,推荐使用“20%的用户无法登录”,而不是“大部分用户无法登录”。要注意这里的数字不需要完全精确,比如21.25%这样的数据其实是没有必要的,我们只需要预估影响是20%还是40%。
严重程度
严重程度指站在业务的角度故障的影响程度,一般分为“致命/高/中/低/无”五个档次。严重程度按照这个公式进行评估:严重程度 = 功能点重要程度 × 故障影响范围 × 功能点受损程度。同样以用户管理系统为例:登录功能比修改用户资料要重要得多,80%的用户比20%的用户范围更大,完全无法登录比登录缓慢要更严重。因此我们可以得出如下故障模式的严重程度。
致命:超过70%用户无法登录;
高:超过30%的用户无法登录;
中:所有用户登录时间超过5秒;
低:10%的用户登录时间超过5秒;
中:所有用户都无法修改资料;
低:20%的用户无法修改头像;
故障原因
故障模式”中只描述了故障的现象,并没有单独列出故障原因。主要原因在于不管什么故障原因,故障现象相同,对功能点的影响就相同。那为何这里还要单独将故障原因列出来呢?主要原因有这几个:
不同的故障原因发生概率不相同
例如,导致MySQL查询响应慢的原因可能是MySQL bug,也可能是没有索引。很明显“MySQL bug”的概率要远远低于“没有索引”;而不同的概率又会影响我们具体如何应对这个故障
不同的故障原因检测手段不一样
例如,磁盘坏道导致MySQL响应慢,那我们需要增加机器的磁盘坏道检查,这个检查很可能不是当前系统本身去做,而是另外运维专门的系统;如果是慢查询导致MySQL慢,那我们只需要配置MySQL的慢查询日志即可。
不同的故障原因的处理措施不一样
例如,如果是MySQL bug,我们的应对措施只能是升级MySQL版本;如果是没有索引,我们的应对措施就是增加索引。
故障概率
这里的概率就是指某个具体故障原因发生的概率。例如,磁盘坏道的概率、MySQL bug的概率、没有索引的概率。一般分为“高/中/低”三档即可,具体评估的时候需要有以下几点需要重点关注:
硬件:硬件随着使用时间推移,故障概率会越来越高。例如,新的硬盘坏道几率很低,但使用了3年的硬盘,坏道几率就会高很多。
开源系统:成熟的开源系统bug率低,刚发布的开源系统bug率相比会高一些;自己已经有使用经验的开源系统bug率会低,刚开始尝试使用的开源系统bug率会高。
自研系统:和开源系统类似,成熟的自研系统故障概率会低,而新开发的系统故障概率会高。
高中低是相对的,只是为了确定优先级以决定后续的资源投入,没有必要绝对量化,因为绝对量化是需要成本的,而且很多时候都没法量化。
风险程度
风险程度就是综合严重程度和故障概率来一起判断某个故障的最终等级,风险程度 = 严重程度 × 故障概率。因此可能出现某个故障影响非常严重,但其概率很低,最终来看风险程度就低。“某个机房业务瘫痪”对业务影响是致命的,但如果故障原因是“地震”,那概率就很低。
已有措施
规避措施
规避措施指为了降低故障发生概率而做的一些事情,可以是技术手段,也可以是管理手段。
技术手段:为了避免新引入的MongoDB丢失数据,在MySQL中冗余一份。
管理手段:为了降低磁盘坏道的概率,强制统一更换服务时间超过2年的磁盘;。
解决措施
解决措施指为了能够解决问题而做的一些事情,一般都是技术手段。例如:
后续规划
综合前面的分析,就可以看出哪些故障我们目前还缺乏对应的措施,哪些已有措施还不够,针对这些不足的地方,再结合风险程度进行排序,给出后续的改进规划。这些规划既可以是技术手段,也可以是管理手段;可以是规避措施,也可以是解决措施。同时需要考虑资源的投入情况,优先将风险程度高的系统隐患解决。
根据FMEA,结合业务来分析可能存在的问题,从而最大程度上规避。
存储高可用方案的本质都是通过将数据复制到多个存储设备,通过数据冗余的方式来实现高可用,其复杂性主要体现在如何应对复制延迟和中断导致的数据不一致问题。因此,对任何一个高可用存储方案,我们需要从以下几个方面去进行思考和分析:
主备复制是最常见也是最简单的一种存储高可用方案,几乎所有的存储系统都提供了主备复制的功能,例如MySQL、Redis、MongoDB等。架构图如下:
基本实现
整体架构比较简单,主备架构中的“备机”主要还是起到一个备份作用,并不承担实际的业务读写操作,如果要把备机改为主机,需要人工操作。
优缺点分析
主备复制架构的优点就是简单,表现有:
对于应用程序来说,不需要感知备机的存在,即使灾难恢复后,原来的备机被人工修改为主机后,对于应用程序来说,只是认为主机的地址换了而已,无须知道是原来的备机升级为主机。
对于主机和备机来说,双方只需要进行数据复制即可,无须进行状态判断和主备切换这类复杂的操作。
主备复制架构的缺点主要有:
备机仅仅只为备份,并没有提供读写操作,硬件成本上有浪费。
故障后需要人工干预,无法自动恢复。人工处理的效率是很低的,可能打电话找到能够操作的人就耗费了10分钟,甚至如果是深更半夜,出了故障都没人知道。人工在执行恢复操作的过程中也容易出错,因为这类操作并不常见,可能1年就2、3次,实际操作的时候很可能遇到各种意想不到的问题。
综合主备复制架构的优缺点,内部的后台管理系统使用主备复制架构的情况会比较多,例如学生管理系统、员工管理系统、假期管理系统等,因为这类系统的数据变更频率低,即使在某些场景下丢失数据,也可以通过人工的方式补全。
主从复制和主备复制只有一字之差,“从”意思是“随从、仆从”,“备”的意思是备份。我们可以理解为仆从是要帮主人干活的,这里的干活就是承担“读”的操作。也就是说,主机负责读写操作,从机只负责读操作,不负责写操作。架构图如下:
基本实现
与主备复制架构比较类似,主要的差别点在于从机正常情况下也是要提供读的操作。
优缺点分析
主从复制与主备复制相比,优点有:
缺点有:
综合主从复制的优缺点,一般情况下,写少读多的业务使用主从复制的存储架构比较多。例如,论坛、BBS、新闻网站这类业务,此类业务的读操作数量是写操作数量的10倍甚至100倍以上。
主备复制和主从复制方案存在两个共性的问题:
要实现一个完善的切换方案,必须考虑这几个关键的设计点:
主备|主从之间状态判断:
主要包括两方面:状态传递的渠道,以及状态检测的内容
切换决策:
主要包括三方面:切换时机、切换策略、自动程度。
数据冲突解决:
当原有故障的主机恢复后,新旧主机之间可能存在数据冲突。例如,用户在旧主机上新增了一条ID为100的数据,这个数据还没有复制到旧的备机,此时发生了切换,旧的备机升级为新的主机,用户又在新的主机上新增了一条ID为100的数据,当旧的故障主机恢复后,这两条ID都为100的数据,应该怎么处理?
以上设计点并没有放之四海而皆准的答案,不同的业务要求不一样,所以切换方案比复制方案不只是多了一个切换功能那么简单,而是复杂度上升了一个量级。形象点来说,如果复制方案的代码是1000行,那么切换方案的代码可能就是10000行,多出来的那9000行就是用于实现上面我所讲的3个设计点的。
根据状态传递渠道的不同,常见的主备切换架构有三种形式:互连式、中介式和模拟式。
故名思议,互连式就是指主备机or从机直接建立状态传递的渠道。架构图如下:
可以看到,在主备复制的架构基础上,主机和备机多了一个“状态传递”的通道,这个通道就是用来传递状态信息的。这个通道的具体实现可以有很多方式:
为了充分利用切换方案能够自动决定主机这个优势,应用程序这里也会有一些相应的改变,常见的方式有:
互连式主备切换主要的缺点在于:
中介式指的是在主备两者之外引入第三方中介,主备机or主从机之间不直接连接,而都去连接中介,并且通过中介来传递状态信息。架构图如下:
对比一下互连式切换架构,可以看到,主机和备机不再通过互联通道传递状态信息,而是都将状态上报给中介这一角色。单纯从架构上看,中介式似乎比互连式更加复杂了,首先要引入中介,然后要各自上报状态。然而事实上,中介式架构在状态传递和决策上却更加简单了,这是为何呢?
连接管理更简单:主备机无须再建立和管理多种类型的状态传递连接通道,只要连接到中介即可,实际上是降低了主备机的连接管理复杂度。
例如,互连式要求主机开一个监听端口,备机来获取状态信息;或者要求备机开一个监听端口,主机推送状态信息到备机;如果还采用了串口连接,则需要增加串口连接管理和数据读取。采用中介式后,主备机都只需要把状态信息发送给中介,或者从中介获取对方的状态信息。无论是发送还是获取,主备机都是作为中介的客户端去操作,复杂度会降低。
状态决策更简单:主备机的状态决策简单了,无须考虑多种类型的连接通道获取的状态信息如何决策的问题,只需要按照下面简单的算法即可完成状态决策。
无论是主机还是备机,初始状态都是备机,并且只要与中介断开连接,就将自己降级为备机,因此可能出现双备机的情况。
主机与中介断连后,中介能够立刻告知备机,备机将自己升级为主机。
如果是网络中断导致主机与中介断连,主机自己会降级为备机,网络恢复后,旧的主机以新的备机身份向中介上报自己的状态。
如果是掉电重启或者进程重启,旧的主机初始状态为备机,与中介恢复连接后,发现已经有主机了,保持自己备机状态不变。
主备机与中介连接都正常的情况下,按照实际的状态决定是否进行切换。例如,主机响应时间超过3秒就进行切换,主机降级为备机,备机升级为主机即可。
虽然中介式架构在状态传递和状态决策上更加简单,但并不意味着这种优点是没有代价的,其关键代价就在于如何实现中介本身的高可用。如果中介自己宕机了,整个系统就进入了双备的状态,写操作相关的业务就不可用了。这就陷入了一个递归的陷阱:为了实现高可用,引入中介,但中介本身又要求高可用,于是又要设计中介的高可用方案……如此递归下去就无穷无尽了。
模拟式指主备机之间并不传递任何状态数据,而是备机模拟成一个客户端,向主机发起模拟的读写操作,根据读写操作的响应情况来判断主机的状态。其基本架构如下:
主主复制指的是两台机器都是主机,互相将数据复制给对方,客户端可以任意挑选其中一台机器进行读写操作,下面是基本架构图:
相比主备切换架构,主主复制架构具有如下特点:
从上面的描述来看,主主复制架构从总体上来看要简单很多,无须状态信息传递,也无须状态决策和状态切换。然而事实上主主复制架构也并不简单,而是有其独特的复杂性,具体表现在:如果采取主主复制架构,必须保证数据能够双向复制,而很多数据是不能双向复制的。例如:
主备、主从、主主架构本质上都有一个隐含的假设:主机能够存储所有数据,但主机本身的存储和处理能力肯定是有极限的。硬件的发展速度是挺快的,但是同业务相比的话,还是差挺远。早在2013年,Facebook就有2500亿张上传照片,当时这些照片的容量就已经达到了250 PB字节(250 × 1024TB),平均一天上传的图片有3亿5000万张。如此大量的数据,单台服务器肯定是无法存储和处理的,必须使用多台服务器来存储数据,这就是数据集群架构。
简单来说,集群就是多台机器组合在一起形成一个统一的系统,这里的“多台”,数量上至少是3台;相比而言,主备、主从都是2台机器。根据集群中机器承担的不同角色来划分,集群可以分为两类:数据集中集群、数据分散集群。
数据集中集群与主备、主从这类架构相似,我们也可以称数据集中集群为1主多备或者1主多从。无论是1主1从、1主1备,还是1主多备、1主多从,数据都只能往主机中写,而读操作可以参考主备、主从架构进行灵活多变。架构图如下:
虽然架构上是类似的,但由于集群里面的服务器数量更多,导致复杂度整体更高一些,具体体现在:
备机如何检测主机状态:
主机故障后,如何决定新的主机:
数据分散集群指多个服务器组成一个集群,每台服务器都会负责存储一部分数据;同时,为了提升硬件利用率,每台服务器又会备份一部分数据。
数据分散集群的复杂点在于如何将数据分配到不同的服务器上,算法需要考虑这些设计点:
均衡性
算法需要保证服务器上的数据分区基本是均衡的,不能存在某台服务器上的分区数量是另外一台服务器的几倍的情况。
容错性
当出现部分服务器故障时,算法需要将原来分配给故障服务器的数据分区分配给其他服务器。
可伸缩性
当集群容量不够,扩充新的服务器后,算法能够自动将部分数据分区迁移到新服务器,并保证扩容后所有服务器的均衡性。
数据分散集群和数据集中集群的不同点在于,数据分散集群中的每台服务器都可以处理读写请求,因此不存在数据集中集群中负责写的主机那样的角色。但在数据分散集群中,必须有一个角色来负责执行数据分配算法,这个角色可以是独立的一台服务器,也可以是集群自己选举出的一台服务器。如果是集群服务器选举出来一台机器承担数据分区分配的职责,则这台服务器一般也会叫作主机,但我们需要知道这里的“主机”和数据集中集群中的“主机”,其职责是有差异的。
Hadoop的实现就是独立的服务器负责数据分区的分配,这台服务器叫作Namenode。
数据集中集群架构中,客户端只能将数据写到主机;数据分散集群架构中,客户端可以向任意服务器中读写数据。正是因为这个关键的差异,决定了两种集群的应用场景不同。一般来说,数据集中集群适合数据量不大,集群机器数量不多的场景。例如,ZooKeeper集群,一般推荐5台机器左右,数据量是单台服务器就能够支撑;而数据分散集群,由于其良好的可伸缩性,适合业务数据量巨大、集群机器数量庞大的业务场景。例如,Hadoop集群、HBase集群,大规模的集群可以达到上百台甚至上千台服务器。
数据分区指将数据按照一定的规则进行分区,不同分区分布在不同的地理位置上,每个分区存储一部分数据,通过这种方式来规避地理级别的故障所造成的巨大影响。采用了数据分区的架构后,即使某个地区发生严重的自然灾害或者事故,受影响的也只是一部分数据,而不是全部数据都不可用;当故障恢复后,其他地区备份的数据也可以帮助故障地区快速恢复业务。
设计一个良好的数据分区架构,需要从多方面去考虑:
数据量
数据量的大小直接决定了分区的规则复杂度,数据量越大,分区规则会越复杂,考虑的情况也越多。
分区规则
地理位置有近有远,因此可以得到不同的分区规则,包括洲际分区、国家分区、城市分区。具体采取哪种或者哪几种规则,需要综合考虑业务范围、成本等因素。
复制规则
数据分区指将数据分散在多个地区,在某些异常或者灾难情况下,虽然部分数据受影响,但整体数据并没有全部被影响,本身就相当于一个高可用方案了。但仅仅做到这点还不够,因为每个分区本身的数据量虽然只是整体数据的一部分,但还是很大,这部分数据如果损坏或者丢失,损失同样难以接受。因此即使是分区架构,同样需要考虑复制方案。
常见的分区复制规则有三种:集中式、互备式和独立式。
集中式备份指存在一个总的备份中心,所有的分区都将数据备份到备份中心,其基本架构如下:
互备式备份指每个分区备份另外一个分区的数据,其基本架构如下:
独立式备份指每个分区自己有独立的备份中心,其基本架构如下:
有一个细节需要特别注意,各个分区的备份并不和原来的分区在一个地方。例如,北京分区的备份放到了天津,上海的放到了杭州,广州的放到了汕头,这样做的主要目的是规避同城或者相同地理位置同时发生灾难性故障的极端情况。如果北京分区机房在朝阳区,而备份机房放在通州区,整个北京停电的话,两个机房都无法工作。
数据分散集群具有均衡性、容错性、可伸缩性的特点,响应较快。
远距离分布的集群有以下特点:
计算高可用的主要设计目标是当出现部分硬件损坏时,计算任务能够继续正常运行。因此计算高可用的本质是通过冗余来规避部分故障的风险,单台服务器是无论如何都达不到这个目标的。所以计算高可用的设计思想很简单:通过增加更多服务器来达到计算高可用。
计算高可用架构的设计复杂度主要体现在任务管理方面,即当任务在某台服务器上执行失败后,如何将任务重新分配到新的服务器进行执行。因此,计算高可用架构设计的关键点有下面两点:
哪些服务器可以执行任务
第一种方式和计算高性能中的集群类似,每个服务器都可以执行任务。例如,常见的访问网站的某个页面。
第二种方式和存储高可用中的集群类似,只有特定服务器(通常叫“主机”)可以执行任务。当执行任务的服务器故障后,系统需要挑选新的服务器来执行任务。例如,ZooKeeper的Leader才能处理写操作请求。
任务如何重新执行
第一种策略是对于已经分配的任务即使执行失败也不做任何处理,系统只需要保证新的任务能够分配到其他非故障服务器上执行即可。
第二种策略是设计一个任务管理器来管理需要执行的计算任务,服务器执行完任务后,需要向任务管理器反馈任务执行结果,任务管理器根据任务执行结果来决定是否需要将任务重新分配到另外的服务器上执行。
需要注意的是:“任务分配器”是一个逻辑的概念,并不一定要求系统存在一个独立的任务分配器模块。例如:
常见的计算高可用架构:主备、主从和集群。
主备架构是计算高可用最简单的架构,和存储高可用的主备复制架构类似,但是要更简单一些,因为计算高可用的主备架构无须数据复制。只需要代码同步即可!其架构如下:
主备方案的详细设计:
根据备机状态的不同,主备架构又可以细分为冷备架构和温备架构:
主备架构的优点就是简单,主备机之间不需要进行交互,状态判断和切换操作由人工执行,系统实现很简单!缺点也是简单,需要人工操作,人工操作的时间不可把控,而且容易出错。
和存储高可用中的主备复制架构类似,计算高可用的主备架构也比较适合与内部管理系统、后台管理系统这类使用人数不多、使用频率不高的业务,不太适合在线的业务。
和存储高可用中的主从复制架构类似,计算高可用的主从架构中的从机也是要执行任务的。任务分配器需要将任务进行分类,确定哪些任务可以发送给主机执行,哪些任务可以发送给备机执行。其架构如下:
主从方案详细设计:
主从架构与主备架构相比,优缺点有:
高可用计算的集群方案根据集群中服务器节点角色的不同,可以分为两类:一类是对称集群,即集群中每个服务器的角色都是一样的,都可以执行所有任务;另一类是非对称集群,集群中的服务器分为多个不同的角色,不同的角色执行不同的任务,例如最常见的Master-Slave角色。
需要注意的是,计算高可用集群包含2台服务器的集群,这点和存储高可用集群不太一样。存储高可用集群把双机架构和集群架构进行了区分;而在计算高可用集群架构中,2台服务器的集群和多台服务器的集群,在设计上没有本质区别,因此不需要进行区分。
对称集群更通俗的叫法是负载均衡集群,因此接下来我使用“负载均衡集群”这个通俗的说法,典型代表架构示意图如下:
负载均衡集群详细设计:
负载均衡集群的设计关键点在于两点:
任务分配策略比较简单,轮询和随机基本就够了。状态检测稍微复杂一些,既要检测服务器的状态,例如服务器是否宕机、网络是否正常等;同时还要检测任务的执行状态,例如任务是否卡死、是否执行时间过长等。常用的做法是任务分配器和服务器之间通过心跳来传递信息,包括服务器信息和任务信息,然后根据实际情况来确定状态判断条件。
非对称集群中不同服务器的角色是不同的,不同角色的服务器承担不同的职责。以Master-Slave为例,部分任务是Master服务器才能执行,部分任务是Slave服务器才能执行。其架构如下:
非对称集群架构详细设计:
非对称集群相比负载均衡集群,设计复杂度主要体现在两个方面:
顾名思义,异地多活架构的关键点就是异地、多活,其中异地就是指地理位置上不同的地方,类似于“不要把鸡蛋都放在同一篮子里”;多活就是指不同地理位置上的系统都能够提供业务服务,这里的“活”是活动、活跃的意思。判断一个系统是否符合异地多活,需要满足两个标准:
正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务。
某个地方业务异常的时候,用户访问其他地方正常的业务系统,能够得到正确的业务服务。
与“活”对应的是字是“备”,备是备份,正常情况下对外是不提供服务的,如果需要提供服务,则需要大量的人工干预和操作,花费大量的时间才能让“备”变成“活”。
异地多活架构不是没有代价的,相反其代价很高,具体表现为:
根据地理位置上的距离来划分,异地多活架构可以分为同城异区、跨城异地、跨国异地。
同城异区指的是将业务部署在同一个城市不同区的多个机房。例如,在北京部署两个机房,一个机房在海淀区,一个在通州区,然后将两个机房用专用的高速网络连接在一起。
同城的两个机房,距离上一般大约就是几十千米,通过搭建高速的网络,同城异区的两个机房能够实现和同一个机房内几乎一样的网络传输速度。这就意味着虽然是两个不同地理位置上的机房,但逻辑上我们可以将它们看作同一个机房,这样的设计大大降低了复杂度,减少了异地多活的设计和实现复杂度及成本。
但是同城异去,如果遇到了极端情况,如这个城市发送了地震、水灾等自然灾难,那么是无能为力的。同城异区主要用来解决机房级别的故障、如机房断电等。
跨城异地指的是业务部署在不同城市的多个机房,而且距离最好要远一些。例如,将业务部署在北京和广州两个机房,而不是将业务部署在广州和深圳的两个机房。
为何跨城异地要强调距离要远呢?前面在介绍同城异区的架构时提到同城异区不能解决水灾这种问题,而两个城市离得太近又无法应对如停电这种问题,跨城异地其实就是为了解决这两类问题的,因此需要在距离上比较远,才能有效应对这类极端灾难事件。
跨城异地虽然能够有效应对极端灾难事件,但“距离较远”这点并不只是一个距离数字上的变化,而是量变引起了质变,导致了跨城异地的架构复杂度大大上升。距离增加带来的最主要问题是两个机房的网络传输速度会降低。除了距离上的限制,中间传输各种不可控的因素也非常多。例如,挖掘机把光纤挖断、中美海底电缆被拖船扯断、骨干网故障等,这些线路很多是第三方维护,针对故障我们根本无能为力也无法预知。
跨城异地距离较远带来的网络传输延迟问题,给异地多活架构设计带来了复杂性,如果要做到真正意义上的多活,业务系统需要考虑部署在不同地点的两个机房,在数据短时间不一致的情况下,还能够正常提供业务。这就引入了一个看似矛盾的地方:数据不一致业务肯定不会正常,但跨城异地肯定会导致数据不一致。
对余额这类数据,一般不会做跨城异地的多活架构,而只能采用同城异区这种架构。而对数据一致性要求不那么高,或者数据不怎么改变,或者即使数据丢失影响也不大的业务,跨城异地多活就能够派上用场了。例如,用户登录(数据不一致时用户重新登录即可)、新闻类网站(一天内的新闻数据变化较少)、微博类网站(丢失用户发布的微博或者评论影响不大),这些业务采用跨城异地多活,能够很好地应对极端灾难的场景。
跨国异地指的是业务部署在不同国家的多个机房。相比跨城异地,跨国异地的距离就更远了,因此数据同步的延时会更长,正常情况下可能就有几秒钟了。
跨国异地的“多活”,和跨城异地的“多活”,实际的含义并不完全一致。跨国异地多活的主要应用场景一般有这几种情况:
为不同地区用户提供服务
例如,亚马逊中国是为中国用户服务的,而亚马逊美国是为美国用户服务的,亚马逊中国的用户如果访问美国亚马逊,是无法用亚马逊中国的账号登录美国亚马逊的。
只读类业务做多活
例如,谷歌的搜索业务,由于用户搜索资料时,这些资料都已经存在于谷歌的搜索引擎上面,无论是访问英国谷歌,还是访问美国谷歌,搜索结果基本相同,并且对用户来说,也不需要搜索到最新的实时资料,跨国异地的几秒钟网络延迟,对搜索结果是没有什么影响的。
优先实现核心业务的异地多活架构!例如,微信系统,优先保证用户可以正常登陆,聊天!这两块优先做异地多活,其余的功能可以先不做。
异地多活本质上是通过异地的数据冗余,来保证在极端异常的情况下业务也能够正常提供给用户,因此数据同步是异地多活架构设计的核心。
数据冗余是要将数据从A地同步到B地,从业务的角度来看是越快越好,最好和本地机房一样的速度最好。但让人头疼的问题正在这里:异地多活理论上就不可能很快,因为这是物理定律决定的。
因此异地多活架构面临一个无法彻底解决的矛盾:业务上要求数据快速同步,物理上正好做不到数据快速同步,因此所有数据都实时同步,实际上是一个无法达到的目标。
既然是无法彻底解决的矛盾,那就只能想办法尽量减少影响。有几种方法可以参考:
数据同步是异地多活架构设计的核心,幸运的是基本上存储系统本身都会有同步的功能。例如,MySQL的主备复制、Redis的Cluster功能、Elasticsearch的集群功能。这些系统本身的同步功能已经比较强大,能够直接拿来就用,但这也无形中将我们引入了一个思维误区:只使用存储系统的同步功能!
既然说存储系统本身就有同步功能,而且同步功能还很强大,为何说只使用存储系统是一个思维误区呢?因为虽然绝大部分场景下,存储系统本身的同步功能基本上也够用了,但在某些比较极端的情况下,存储系统本身的同步功能可能难以满足业务需求。
以MySQL为例,MySQL 5.1版本的复制是单线程的复制,在网络抖动或者大量数据同步时,经常发生延迟较长的问题,短则延迟十几秒,长则可能达到十几分钟。而且即使我们通过监控的手段知道了MySQL同步时延较长,也难以采取什么措施,只能干等。
Redis又是另外一个问题,Redis 3.0之前没有Cluster功能,只有主从复制功能,而为了设计上的简单,Redis 2.8之前的版本,主从复制有一个比较大的隐患:从机宕机或者和主机断开连接都需要重新连接主机,重新连接主机都会触发全量的主从复制。这时主机会生成内存快照,主机依然可以对外提供服务,但是作为读的从机,就无法提供对外服务了,如果数据量大,恢复的时间会相当长。
综合上面的案例可以看出,存储系统本身自带的同步功能,在某些场景下是无法满足业务需要的。尤其是异地多机房这种部署,各种各样的异常情况都可能出现,当我们只考虑存储系统本身的同步功能时,就会发现无法做到真正的异地多活。
解决的方案就是拓开思路,避免只使用存储系统的同步功能,可以将多种手段配合存储系统的同步来使用,甚至可以不采用存储系统的同步方案,改用自己的同步方案。
以“用户子系统”为例,我们可以可以采用如下几种方式同步数据:
消息队列方式
对于账号数据,由于账号只会创建,不会修改和删除(假设我们不提供删除功能),我们可以将账号数据通过消息队列同步到其他业务中心。
二次读取方式
某些情况下可能出现消息队列同步也延迟了,用户在A中心注册,然后访问B中心的业务,此时B中心本地拿不到用户的账号数据。为了解决这个问题,B中心在读取本地数据失败时,可以根据路由规则,再去A中心访问一次(这就是所谓的二次读取,第一次读取本地,本地失败后第二次读取对端),这样就能够解决异常情况下同步延迟的问题。
存储系统同步方式
对于密码数据,由于用户改密码频率较低,而且用户不可能在1秒内连续改多次密码,所以通过数据库的同步机制将数据复制到其他业务中心即可,用户信息数据和密码类似。
回源读取方式
对于登录的session数据,由于数据量很大,我们可以不同步数据;但当用户在A中心登录后,然后又在B中心登录,B中心拿到用户上传的session id后,根据路由判断session属于A中心,直接去A中心请求session数据即可;反之亦然,A中心也可以到B中心去获取session数据。
重新生成数据方式
对于“回源读取”场景,如果异常情况下,A中心宕机了,B中心请求session数据失败,此时就只能登录失败,让用户重新在B中心登录,生成新的session数据。
某些场景下我们无法保证100%的业务可用性,总是会有一定的损失。例如,密码不同步导致无法登录、用户信息不同步导致用户看到旧的信息等,这个问题怎么解决呢?
很遗憾,答案是没有!异地多活也无法保证100%的业务可用,这是由物理规律决定的,光速和网络的传播速度、硬盘的读写速度、极端异常情况的不可控等,都是无法100%解决的。所以针对这个思维误区,我的答案是“忍”!也就是说我们要忍受这一小部分用户或者业务上的损失,否则本来想为了保证最后的0.01%的用户的可用性,做一个完美方案,结果却发现99.99%的用户都保证不了了。
虽然我们无法做到100%可用性,但并不意味着我们什么都不能做,为了让用户心里更好受一些,我们可以采取一些措施进行安抚或者补偿,例如:
挂公告
说明现在有问题和基本的问题原因,如果不明确原因或者不方便说出原因,可以发布“技术哥哥正在紧急处理”这类比较轻松和有趣的公告。
事后对用户进行补偿
例如,送一些业务上可用的代金券、小礼包等,减少用户的抱怨。
补充体验
对于为了做异地多活而带来的体验损失,可以想一些方法减少或者规避。以“转账申请”为例,为了让用户不用确认转账申请是否成功,我们可以在转账成功或者失败后直接给用户发个短信,告诉他转账结果,这样用户就不用时不时地登录系统来确认转账是否成功了。
异地多活设计的理念可以总结为一句话:采用多种手段,保证绝大部分用户的核心业务异地多活!
异地多活,保证核心业务,保证数据最终一致性,而不是实时一致性。只保证大部分的用户可以异地多活。
按照一定的标准将业务进行分级,挑选出核心的业务,只为核心业务设计异地多活,降低方案整体复杂度和实现成本。
常见的分级标准有下面几种:
访问量大的业务
以博客系统为例,业务包括登录、注册、用户信息管理,发博客,浏览博客,其中登录和发博客,浏览博客的访问量肯定是最大的。
核心业务
以微信为列,聊天肯定是最重要的,朋友固然也非常重要,但是比起聊天就差了很多。如果要从聊天和朋友圈选一个做异地多活的话,肯定是选择聊天(当然微信肯定是全部做了异地多活)。
产生大量收入的业务
同样以微信为例,聊天可能很难为腾讯带来收益,因为聊天没法插入广告;而朋友圈反而可能带来更多收益,因为朋友圈可以插入很多广告,因此如果从收入的角度来看,朋友圈做异地多活的优先级反而高于聊天了。
挑选出核心业务后,需要对核心业务相关的数据进一步分析,目的在于识别所有的数据及数据特征,这些数据特征会影响后面的方案设计。
常见的数据特征分析维度有:
数据量
这里的数据量包括总的数据量和新增、修改、删除的量。对异地多活架构来说,新增、修改、删除的数据就是可能要同步的数据,数据量越大,同步延迟的几率越高,同步方案需要考虑相应的解决方案。
唯一性
唯一性指数据是否要求多个异地机房产生的同类数据必须保证唯一。例如用户ID,如果两个机房的两个不同用户注册后生成了一样的用户ID,这样业务上就出错了。
数据的唯一性影响业务的多活设计,如果数据不需要唯一,那就说明两个地方都产生同类数据是可能的;如果数据要求必须唯一,要么只能一个中心点产生数据,要么需要设计一个数据唯一生成的算法。
实时性
实时性指如果在A机房修改了数据,要求多长时间必须同步到B机房,实时性要求越高,对同步的要求越高,方案越复杂。
可丢失性
可丢失性指数据是否可以丢失。例如,写入A机房的数据还没有同步到B机房,此时A机房机器宕机会导致数据丢失,那这部分丢失的数据是否对业务会产生重大影响。
例如,登录过程中产生的session数据就是可丢失的,因为用户只要重新登录就可以生成新的session;而用户ID数据是不可丢失的,丢失后用户就会失去所有和用户ID相关的数据,例如用户的好友、用户的钱等。
可恢复性
可恢复性指数据丢失后,是否可以通过某种手段进行恢复,如果数据可以恢复,至少说明对业务的影响不会那么大,这样可以相应地降低异地多活架构设计的复杂度。
例如,用户在微信发的朋友圈丢失后,用户重新发一篇一模一样的朋友圈,这个就是可恢复的;或者密码丢失,可以通过找回密码来重新设置一个新密码,这也算是可以恢复的;但是如果用户账号如果丢失,就无法登录系统,系统也无法通过其他途径来恢复这个账号,这就是不可恢复的数据。
确定数据的特点后,我们可以根据不同的数据设计不同的同步方案。常见的数据同步方案有:
存储系统同步
这是最常用也是最简单的同步方式。例如,使用MySQL的数据主从数据同步、主主数据同步。
这类数据同步的优点是使用简单,因为几乎主流的存储系统都会有自己的同步方案;缺点是这类同步方案都是通用的,无法针对业务数据特点做定制化的控制。例如,无论需要同步的数据量有多大,MySQL都只有一个同步通道。因为要保证事务性,一旦数据量比较大,或者网络有延迟,则同步延迟就会比较严重。
消息队列同步
采用独立消息队列进行数据同步,常见的消息队列有Kafka、ActiveMQ、RocketMQ等。消息队列同步适合无事务性或者无时序性要求的数据。
重复生成
数据不同步到异地机房,每个机房都可以生成数据,这个方案适合于可以重复生成的数据。例如,登录产生的cookie、session数据、缓存数据等。
无论数据同步方案如何设计,一旦出现极端异常的情况,总是会有部分数据出现异常的。例如,同步延迟、数据丢失、数据不一致等。异常处理就是假设在出现这些问题时,系统将采取什么措施来应对。异常处理主要有以下几个目的:
常见的异常处理措施有这几类:
多通道同步
多通道同步的含义是采取多种方式来进行数据同步,其中某条通道故障的情况下,系统可以通过其他方式来进行同步,这种方式可以应对同步通道处故障的情况。
多通道同步设计的方案关键点有:
同步和访问结合
这里的访问指异地机房通过系统的接口来进行数据访问。例如业务部署在异地两个机房A和B,B机房的业务系统通过接口来访问A机房的系统获取账号信息。
同步和访问结合方案的设计关键点有:
日志记录
日志记录主要用于用户故障恢复后对数据进行恢复,其主要方式是每个关键操作前后都记录相关一条日志,然后将日志保存在一个独立的地方,当故障恢复后,拿出日志跟数据进行对比,对数据进行修复。
为了应对不同级别的故障,日志保存的要求也不一样,常见的日志保存方式有:
用户补偿
无论采用什么样的异常处理措施,都只能最大限度地降低受到影响的范围和程度,无法完全做到没有任何影响。例如,双同步通道有可能同时出现故障、日志记录方案本身日志也可能丢失。因此,无论多么完美的方案,故障的场景下总是可能有一小部分用户业务上出问题,系统无法弥补这部分用户的损失。但我们可以采用人工的方式对用户进行补偿,弥补用户的损失。简单来说,系统的方案是为了保证99.99%的用户在故障的场景下业务不受影响,人工的补偿是为了弥补0.01%的用户的损失。
接口级故障的典型表现就是系统并没有宕机,网络也没有中断,但业务却出现问题了。例如,业务响应缓慢、大量访问超时、大量访问出现异常(给用户弹出提示“无法连接数据库”),这类问题的主要原因在于系统压力太大、负载太高,导致无法快速处理业务请求,由此引发更多的后续问题。例如,最常见的数据库慢查询将数据库的服务器资源耗尽,导致读写超时,业务读写数据库时要么无法连接数据库、要么超时,最终用户看到的现象就是访问很慢,一会访问抛出异常,一会访问又是正常结果。
导致接口级故障的原因一般有下面几种:
内部原因
程序bug导致死循环,某个接口导致数据库慢查询,程序逻辑不完善导致耗尽内存等。
外部原因
黑客攻击、促销或者抢购、秒杀等活动 引入了超出平时几倍甚至几十倍的用户,第三方系统大量请求,第三方系统响应缓慢等。
解决接口级故障的核心思想和异地多活基本类似:优先保证核心业务和优先保证绝大部分用户。
降级指系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。例如,博客可以降级为只能看博客,不能发博客子;也可以降级为只能看博客和评论,不能发评论;而App的日志上传接口,可以完全停掉一段时间,这段时间内App都不能上传日志。
降级的核心思想就是丢车保帅,优先保证核心业务。例如,对于博客来说,90%的流量是看博客,那就优先保证看博客的功能;对于一个App来说,日志上传接口只是一个辅助的功能,故障时完全可以停掉。
常见的实现降级的方式有:
系统后门降级
简单来说,就是系统预留了后门用于降级操作。例如,系统提供一个降级URL,当访问这个URL时,就相当于执行降级指令,具体的降级指令通过URL的参数传入即可。但是这种方式有安全隐患,要做好安全机制。
系统后门降级的方式实现成本低,但主要缺点是如果服务器数量多,需要一台一台去操作,效率比较低,这在故障处理争分夺秒的场景下是比较浪费时间的。
独立降级系统
为了解决系统后门降级方式的缺点,将降级操作独立到一个单独的系统中,可以实现复杂的权限管理、批量操作等功能。
熔断和降级是两个比较容易混淆的概念,因为单纯从名字上看好像都有禁止某个功能的意思,但其实内在含义是不同的,原因在于降级的目的是应对系统自身的故障,而熔断的目的是应对依赖的外部系统故障的情况。
例如,A服务需要请求B服务的某个接口,而B服务出现了故障,或者响应变的非常慢,此时A服务的其他功能也会被影响,导致A服务被卡在了B服务的接口上,这个时候我们就需要熔断机制,比如限制请求次数,等待的时间,如果超过的阀值,就停止对该接口的请求,把影响降到最低。
熔断说白了就是监控客户端发出请求的成功和失败数量,如果失败的比例超过一定次数,就启动断路器,让后续的调用立刻失效。
而限流则是从用户访问压力的角度来考虑如何应对故障。限流指只允许系统能够承受的访问量进来,超出系统访问能力的请求将被放弃。
虽然放弃部分访问,让其无法访问,但保证一部分请求能够正常响应,总比全部请求都不能响应要好得多。
限流一般都是系统内实现的,常见的限流方式可以分为两类:
基于请求限流
基于请求限流指从外部访问的请求角度考虑限流,常见的方式有:限制总量、限制时间量。
限制总量的方式是限制某个指标的累积上限,常见的是限制当前系统服务的用户总量,例如某个直播间限制总用户数上限为100万,超过100万后新的用户无法进入;某个抢购活动商品数量只有100个,限制参与抢购的用户上限为1万个,1万以后的用户直接拒绝。限制时间量指限制一段时间内某个指标的上限,例如,1分钟内只允许10000个用户访问,每秒请求峰值最高为10万。
无论是限制总量还是限制时间量,共同的特点都是实现简单,但在实践中面临的主要问题是比较难以找到合适的阈值。即使找到了合适的阈值,基于请求限流还面临硬件相关的问题。例如一台32核的机器和64核的机器处理能力差别很大,阈值是不同的,这不是通过数学运算可以准确的计算出来的,64核的机器比32核的机器,业务处理性能并不是2倍的关系,可能是1.5倍,甚至可能是1.1倍。
为了找到合理的阈值,通常情况下可以采用性能压测来确定阈值,但性能压测也存在覆盖场景有限的问题,可能出现某个性能压测没有覆盖的功能导致系统压力很大;另外一种方式是逐步优化,即:先设定一个阈值然后上线观察运行情况,发现不合理就调整阈值。
根据阈值来限制访问量的方式更多的适应于业务功能比较简单的系统,例如负载均衡系统、网关系统、抢购系统等。
基于资源限流
基于请求限流是从系统外部考虑的,而基于资源限流是从系统内部考虑的,即:找到系统内部影响性能的关键资源,对其使用上限进行限制。常见的内部资源有:连接数、文件句柄、线程数、请求队列等。
基于资源限流相比基于请求限流能够更加有效地反映当前系统的压力,但实践中设计也面临两个主要的难点:如何确定关键资源,如何确定关键资源的阈值。通常情况下,这也是一个逐步调优的过程,即:设计的时候先根据推断选择某个关键资源和阈值,然后测试验证,再上线观察,如果发现不合理,再进行优化。
排队实际上是限流的一个变种,限流是直接拒绝用户,排队是让用户等待一段时间,最有名的排队当属12306网站排队了。排队虽然没有直接拒绝用户,但用户等了很长时间后进入系统,体验并不一定比限流好。
由于排队需要临时缓存大量的业务请求,单个系统内部无法缓存这么多数据,一般情况下,排队需要用独立的系统去实现,例如使用Kafka这类消息队列来缓存用户请求。
例如电商的抢购,秒杀活动,对于登录,可以适当的限流,保证部分用户的可用性,对于有请求第三方服务API出现问题,直接进行熔断,放弃请求保证其他功能的使用。那么秒杀,抢购这些核心功能,就采用排队来完成,同时对于用户的收获地址信息采用降级,优先保证抢购,秒杀的活动可以正常进行。
软件系统的可扩展的特性,既是魅力所在,又是难点所在。魅力体现在我们可以通过修改和扩展,不断地让软件系统具备更多的功能和特性,满足新的需求或者顺应技术发展的趋势。而难点体现在如何以最小的代价去扩展系统,因为很多情况下牵一发动全身,扩展时可能出现到处都要改,到处都要推倒重来的情况。这样做的风险不言而喻:改动的地方越多,投入也越大,出错的可能性也越大。因此,如何避免扩展时改动范围太大,是软件架构可扩展性设计的主要思考点。
可扩展性架构的设计方法很多,但万变不离其宗,所有的可扩展性架构设计,背后的基本思想都可以总结为一个字:拆!
拆,就是将原本大一统的系统拆分成多个规模小的部分,扩展时只修改其中一部分即可,无须整个系统到处都改,通过这种方式来减少改动范围,降低改动风险。例如,我们可以把一个单体应用根据业务、功能、拆分成多个微服务。
按照不同的思路来拆分软件系统,就会得到不同的架构。常见的拆分思路有以下三种:
面向流程拆分
将整个业务流程拆分为几个阶段,每个阶段作为一部分。
经典架构:分层架构,MVC模式。
面向服务拆分
将系统提供的服务拆分,每个服务作为一部分。
经典架构:微服务、SOA。
面向功能拆分
将系统提供的功能拆分,每个功能作为一部分。
经典架构:微内核。
不同的拆分方式,本质上决定了系统的扩展方式。
合理的拆分,能够强制保证即使程序员出错,出错的范围也不会太广,影响也不会太大。
不同拆分方式应对扩展时的优势:
面向流程拆分
扩展时大部分情况只需要修改某一层,少部分情况可能修改关联的两层,不会出现所有层都同时要修改。例如后台管理系统,如果我们将存储层从MySQL扩展为同时支持MySQL和Oracle,那么只需要扩展存储层和数据层即可,展示层和业务层无须变动。
面向服务拆分
对某个服务扩展,或者要增加新的服务时,只需要扩展相关服务即可,无须修改所有的服务。
面向功能拆分
对某个功能扩展,或者要增加新的功能时,只需要扩展相关功能即可,无须修改所有的服务。
这几个系统架构并不是非此即彼的,而是可以在系统架构设计中进行组合使用的。例如,可以把面向流程拆分和面向服务拆分组合使用,关键性数据,提供微服务,通过MVC架构模式根据业务流程,请求对应的服务数据。
分层架构是很常见的架构模式,它也叫N层架构,通常情况下,N至少是2层。常见的是3层架构(例如,MVC、MVP架构)、4层架构,5层架构的比较少见,一般是比较复杂的系统才会达到或者超过5层,比如操作系统内核架构。
无论采取何种分层维度,分层架构设计最核心的一点就是需要保证各层之间的差异足够清晰,边界足够明显,让人看到架构图后就能看懂整个架构,这也是分层不能分太多层的原因。
分层结构的这种约束,好处在于强制将分层依赖限定为两两依赖,降低了整体系统复杂度。例如,MVC模式,Controller依赖于model,而view又依赖于Controller。
分层架构一个典型的缺点就是性能,因为每一次业务请求都需要穿越所有的架构分层,有一些事情是多余的,多少都会有一些性能的浪费。但是以现在的硬件和网络,分层模式理论上的这点性能损失,在实际应用中,绝大部分场景下都可以忽略不计。
为了应对传统IT系统存在的问题,SOA提出了3个关键概念:
服务
所有业务功能都是一项服务,服务就意味着要对外提供开放的能力,当其他系统需要使用这项功能时,无须定制化开发。
ESB
ESB的全称是Enterprise Service Bus,中文翻译为“企业服务总线”。从名字就可以看出,ESB参考了计算机总线的概念。计算机中的总线将各个不同的设备连接在一起,ESB将企业中各个不同的服务连接在一起。因为各个独立的服务是异构的,如果没有统一的标准,则各个异构系统对外提供的接口是各式各样的。SOA使用ESB来屏蔽异构系统对外提供各种不同的接口方式,以此来达到服务间高效的互联互通。
松耦合
松耦合的目的是减少各个服务间的依赖和互相影响。因为采用SOA架构后,各个服务是相互独立运行的,甚至都不清楚某个服务到底有多少对其他服务的依赖。如果做不到松耦合,某个服务一升级,依赖它的其他服务全部故障,这样肯定是无法满足业务需求的。
SOA是把多个系统整合,而微服务是把单个系统拆分成独立的服务,整合相反。
微内核架构(Microkernel Architecture),也被称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,通常用于实现基于产品(原文为product-based,指存在多个版本、需要下载安装才能使用,与web-based相对应)的应用。例如Eclipse这类IDE软件、UNIX这类操作系统、淘宝App这类客户端软件等,也有一些框架使用微内核架构,如go-micro微服务框架。
微内核架构包含两类组件:核心系统(core system)和插件模块(plug-in modules)。核心系统负责和具体业务功能无关的通用功能,例如模块加载、模块间通信等;插件模块负责实现具体的业务逻辑。例如go-micro微服务框架,有核心的系统:API模块,CLI模块。有专门管理插件的模块Plugins。
微内核的核心系统设计的关键技术有:插件管理、插件连接和插件通信。
插件管理
核心系统需要知道当前有哪些插件可用,如何加载这些插件,什么时候加载插件。常见的实现方法是插件注册表机制。
核心系统提供插件注册表(可以是配置文件,也可以是代码,还可以是数据库),插件注册表含有每个插件模块的信息,包括它的名字、位置、加载时机(启动就加载,还是按需加载)等。
插件连接
插件连接指插件如何连接到核心系统。通常来说,核心系统必须制定插件和核心系统的连接规范,然后插件按照规范实现,核心系统按照规范加载即可。例如,go-micro微服务框架就是通过在编译的时候加载plugin.go来加载plugin.go中引入了哪些插件。
插件通信
插件通信指插件间的通信。虽然设计的时候插件间是完全解耦的,但实际业务运行过程中,必然会出现某个业务流程需要多个插件协作,这就要求两个插件间进行通信。由于插件之间没有直接联系,通信必须通过核心系统,因此核心系统需要提供插件通信机制。这种情况和计算机类似,计算机的CPU、硬盘、内存、网卡是独立设计的配件,但计算机运行过程中,CPU和内存、内存和硬盘肯定是有通信的,计算机通过主板上的总线提供了这些组件之间的通信功能。微内核的核心系统也必须提供类似的通信机制,各个插件之间才能进行正常的通信。
SQL即我们通常所说的关系数据。前几年NoSQL火了一阵子,很多人都理解为NoSQL是完全抛弃关系数据,全部采用非关系型数据。但经过几年的试验后,大家发现关系数据不可能完全被抛弃,NoSQL不是No SQL,而是Not Only SQL,即NoSQL是SQL的补充。
所以互联网行业也必须依赖关系数据,考虑到Oracle太贵,还需要专人维护,一般情况下互联网行业都是用MySQL、PostgreSQL这类开源数据库。这类数据库的特点是开源免费,拿来就用;但缺点是性能相比商业数据库要差一些。随着互联网业务的发展,性能要求越来越高,必然要面对一个问题:将数据拆分到多个数据库实例才能满足业务的性能需求(其实Oracle也一样,只是时间早晚的问题)。
数据库拆分满足了性能的要求,但带来了复杂度的问题:数据如何拆分、数据如何组合?这个复杂度的问题解决起来并不容易,如果每个业务都去实现一遍,重复造轮子将导致投入浪费、效率降低,业务开发想快都快不起来。
所以互联网公司流行的做法是业务发展到一定阶段后,就会将这部分功能独立成中间件,例如百度的DBProxy、淘宝的TDDL。不过这部分的技术要求很高,将分库分表做到自动化和平台化,不是一件容易的事情,所以一般是规模很大的公司才会自己做。中小公司建议使用开源方案,例如MySQL官方推荐的MySQL Router、360开源的数据库中间件Atlas。
假如公司业务继续发展,规模继续扩大,SQL服务器越来越多,如果每个业务都基于统一的数据库中间件独立部署自己的SQL集群,就会导致新的复杂度问题,具体表现在:
因此,实力雄厚的大公司此时一般都会在SQL集群上构建SQL存储平台,以对业务透明的形式提供资源分配、数据备份、迁移、容灾、读写分离、分库分表等一系列服务,例如淘宝的UMP(Unified MySQL Platform)系统。
首先NoSQL在数据结构上与传统的SQL的不同,例如典型的Memcache的key-value结构、Redis的复杂数据结构、MongoDB的文档数据结构;其次,NoSQL无一例外地都会将性能作为自己的一大卖点。NoSQL的这两个特点很好地弥补了关系数据库的不足,因此在互联网行业NoSQL的应用基本上是基础要求。
由于NoSQL方案一般自己本身就提供集群的功能,例如Memcache的一致性Hash集群、Redis 3.0的集群,因此NoSQL在刚开始应用时很方便,不像SQL分库分表那么复杂。一般公司也不会在开始时就考虑将NoSQL包装成存储平台,但如果公司发展很快,例如Memcache的节点有上千甚至几千时,NoSQL存储平台就很有意义了。首先是存储平台通过集中管理能够大大提升运维效率;其次是存储平台可以大大提升资源利用效率,2000台机器,如果利用率能提升10%,就可以减少200台机器,一年几十万元就节省出来了。
所以,NoSQL发展到一定规模后,通常都会在NoSQL集群的基础之上再实现统一存储平台,统一存储平台主要实现这几个功能:
一般搭建存储平台,都是大公司才做,像阿里、腾讯都有自己的存储平台。
除了关系型的业务数据,互联网行业还有很多用于展示的数据。例如,淘宝的商品图片、商品描述;Facebook的用户图片;新浪微博的一条微博内容等。这些数据具有三个典型特征:一是数据小,一般在1MB以下;二是数量巨大,Facebook在2013年每天上传的照片就达到了3.5亿张;三是访问量巨大,Facebook每天的访问量超过10亿。
由于互联网行业基本上每个业务都会有大量的小数据,如果每个业务都自己去考虑如何设计海量存储和海量访问,效率自然会低,重复造轮子也会投入浪费,所以自然而然就要将小文件存储做成统一的和业务无关的平台。
和SQL和NoSQL不同的是,小文件存储不一定需要公司或者业务规模很大,基本上认为业务在起步阶段就可以考虑做小文件统一存储。得益于开源运动的发展和最近几年大数据的火爆,在开源方案的基础上封装一个小文件存储平台并不是太难的事情。例如,HBase、Hadoop、Hypertable、FastDFS等都可以作为小文件存储的底层平台,只需要将这些开源方案再包装一下基本上就可以用了。
典型的小文件存储有:淘宝的TFS、腾讯的TFS、Facebook的Haystack。
互联网行业的大文件主要分为两类:一类是业务上的大数据,例如Youtube的视频、电影网站的电影;另一类是海量的日志数据,例如各种访问日志、操作日志、用户轨迹日志等。和小文件的特点正好相反,大文件的数量没有小文件那么多,但每个文件都很大,几百MB、几个GB都是常见的,几十GB、几TB也是有可能的,因此在存储上和小文件有较大差别,不能直接将小文件存储系统拿来存储大文件。
对照Google的论文构建一套完整的大数据处理方案的难度和成本实在太高,而且开源方案现在也很成熟了,所以大数据存储和处理这块反而是最简单的,因为你没有太多选择,只能用这几个流行的开源方案,例如,Hadoop、HBase、Storm、Hive等。
开发框架
在开发之前我们都会指定一个大的技术方向,然后使用统一的开发框架。例如,go相的开发框架go-micro微服务框架,beego框架,PHP的Yii、laval,Python的Django等。使用统一的开发框架能够解决上面提到的开发人员沟通、学习、人员流动的问题,大大提升组织和团队的开发效率。
对于框架的选择,有一个总的原则:优选成熟的框架,避免盲目追逐新技术!
Web服务器
开发框架只是负责完成业务功能的开发,真正能够运行起来给用户提供服务,还需要服务器配合。通常我都是使用Apache,Nginx。
容器
容器是最近几年才开始火起来的,其中以Docker为代表。很多公司已经投入使用。
传统的虚拟化技术是虚拟机,解决了跨平台的问题,但由于虚拟机太庞大,启动又慢,运行时太占资源,在互联网行业并没有大规模应用;而Docker的容器技术,虽然没有跨平台,但启动快,几乎不占资源,推出后立刻就火起来了,估计Docker类的容器技术将是技术发展的主流方向。
不要以为Docker只是一个虚拟化或者容器技术,它将在很大程度上改变目前的技术形势:
服务层的主要目标其实就是为了降低系统间相互关联的复杂度。
配置中心
故名思议,配置中心就是集中管理各个系统的配置。
当系统数量不多的时候,一般是各系统自己管理自己的配置,但系统数量多了以后,这样的处理方式会有以下问题:
实现配置中心主要就是为了解决上面这些问题,将配置中心做成通用的系统的好处有:
服务中心
当系统数量不多的时候,系统间的调用一般都是直接通过配置文件记录在各系统内部的,但当系统数量多了以后,这种方式就存在问题了。
例如 总共有10个系统依赖A系统的X接口,A系统实现了一个新接口Y,能够更好地提供原有X接口的功能,如果要让已有的10个系统都切换到Y接口,则这10个系统的几十上百台机器的配置都要修改,然后重启,可想而知这个效率是很低的。
除此以外,如果A系统总共有20台机器,现在其中5台出故障了,其他系统如果是通过域名访问A系统,则域名缓存失效前,还是可能访问到这5台故障机器的;如果其他系统通过IP访问A系统,那么A系统每次增加或者删除机器,其他所有10个系统的几十上百台机器都要同步修改,这样的协调工作量也是非常大的。
服务中心就是为了解决上面提到的跨系统依赖的“配置”和“调度”问题。
服务中心的实现一般来说有两种方式:
消息队列
消息队列系统基本功能的实现比较简单,但要做到高性能、高可用、消息时序性、消息事务性则比较难。业界已经有很多成熟的开源实现方案,如果要求不高,基本上拿来用即可,例如,RocketMQ、Kafka、ActiveMQ等。但如果业务对消息的可靠性、时序、事务性要求较高时,则要深入研究这些开源方案,否则很容易踩坑。
顾名思议,负载均衡就是将请求均衡地分配到多个系统上。使用负载均衡的原因也很简单:每个系统的处理能力是有限的,为了应对大容量的访问,必须使用多个系统。
DNS
DNS是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。例如,北方的用户访问北京的机房,南方的用户访问广州的机房。一般不会使用DNS来做机器级别的负载均衡,因为太耗费IP资源了。
Nginx 、LVS 、F5
DNS用于实现地理级别的负载均衡,而Nginx、LVS、F5用于同一地点内机器级别的负载均衡。其中Nginx是软件的7层负载均衡,LVS是内核的4层负载均衡,F5是硬件的4层负载均衡。
软件和硬件的区别就在于性能,硬件远远高于软件,Ngxin的性能是万级,一般的Linux服务器上装个Nginx大概能到5万/秒;LVS的性能是十万级,没有具体测试过,据说可达到80万/秒;F5性能是百万级,从200万/秒到800万/秒都有。硬件虽然性能高,但是单台硬件的成本也很高,一台最便宜的F5都是几十万,但是如果按照同等请求量级来计算成本的话,实际上硬件负载均衡设备可能会更便宜,例如假设每秒处理100万请求,用一台F5就够了,但用Nginx,可能要20台,这样折算下来用F5的成本反而低。因此通常情况下,如果性能要求不高,可以用软件负载均衡;如果性能要求很高,推荐用硬件负载均衡。
4层和7层的区别就在于协议和灵活性。Nginx支持HTTP、E-mail协议,而LVS和F5是4层负载均衡,和协议无关,几乎所有应用都可以做,例如聊天、数据库等。
CDN
CDN是为了解决用户网络访问时的“最后一公里”效应,本质上是一种“以空间换时间”的加速策略,即将内容缓存在离用户最近的地方,用户访问的是缓存的内容,而不是站点实时的内容。
CDN经过多年的发展,已经变成了一个很庞大的体系:分布式存储、全局负载均衡、网络重定向、流量控制等都属于CDN的范畴,尤其是在视频、直播等领域,如果没有CDN,用户是不可能实现流畅观看内容的。
开源项目对团队和业务有很大好处,但对于技术人员来说,如果只是简单的采取“拿来主义”,那就变成一个陷阱:看似很快的用开源项目实现了需求,但自己的技术水平并没有什么提升;甚至可能出现看起来用了很多开源项目,知道很多项目名称,但技术水平止步不前的窘境。
因此,对于开源项目,不能简单的采取“拿来主义”,而要比较深入的去学习开源项目,做到“知其然,知其所以然”,一方面是为了更好地应用这些开源项目,另一方面也是为了通过学习优秀的开源项目来提升自己的能力。
“自顶向下”的学习方法和步骤:
第一步:安装
很多人看到“安装”这个步骤都可能会觉得有点不以为然:“不就是对照手册执行一下命令么,没什么技术含量,用的时候装一下就可以了”。事实上,安装步骤远远不止这么简单,通过具体的安装过程,你可以获取到如下一些关键信息:
这个系统的依赖组件,而依赖的组件是系统设计和实现的基础。
以Nginx为例,源码安装Nginx依赖的库有pcre、pcre-devel、openssl、openssl-devel、zlib,光从名字上看都能够了解一些信息,例如openssl可能和https有关,zlib可能和压缩有关。
例如,Nginx安装完成后,目录如下:
这个目录提供的信息有:conf是存放配置文件的,logs是存放日志的,sbin是运行程序,但是html是什么呢?这个疑问会促使你继续去研究和学习。
第二步:运行
安装完成后,我们需要真正将系统运行起来,运行系统的时候有两个地方要特别关注:命令行和配置文件,它们主要提供了两个非常关键的信息:系统具备哪些能力和系统将会如何运行。
通常情况下,如果我们将每个命令行参数和配置项的作用和原理都全部掌握清楚了的话,基本上对系统已经很熟悉了,原作者的一个习惯是不管三七二十一,先把所有的配置项全部研究一遍,包括配置项的原理、作用、影响,并且尝试去修改配置项然后看看系统会有什么变化。
第三步:原理研究
完成前两个步骤后,我们对系统已经有了初步的感觉和理解,此时可以更进一步去研究其原理。其实在研究命令行和配置项的时候已经涉及一部分原理了,但是还不系统,因此我们要专门针对原理进行系统性的研究。这里的关键就是“系统性”三个字,怎么才算系统性呢?主要体现在如下几个方面:
关键特性的基本实现原理
优缺点对比分析
特别强调的一点,只有清楚掌握技术方案的优缺点后才算真正的掌握这门技术,也只有掌握了技术方案的优缺点后才能在架构设计的时候做出合理的选择。
优缺点主要通过对比来分析,即:我们将两个类似的系统进行对比,看看它们的实现差异,以及不同的实现优缺点都是什么。
了解了什么是“系统性”后,来介绍一下原理研究的手段,主要有三种:
第四步:测试
通常情况下,如果你真的准备在实际项目中使用某个开源项目的话,必须进行测试。
测试阶段需要特别强调的一点就是:测试一定要在原理研究之后做,不能安装完成立马就测试!原因在于如果对系统不熟悉,很可能出现命令行、配置参数没用对,或者运行模式选择不对,导致没有根据业务的特点搭建正确的环境、没有设计合理的测试用例,从而使得最终的测试结果得出了错误结论,误导了设计决策。
第五步:源码研究
源码研究的主要目的是学习原理背后的具体编码如何实现,通过学习这些技巧来提升我们自己的技术能力。例如Redis的RDB快照、Nginx的多Reactor模型等,这些技巧都很精巧,掌握后能够大大提升自己的编码能力。
通常情况下,不建议通读所有源码,因为想掌握每行代码的含义和作用还是非常耗费时间的,尤其是MySQL、Nginx这种规模的项目,即使是他们的开发人员,都不一定每个人都掌握了所有代码。带着明确目的去研究源码,做到有的放矢,才能事半功倍,这也是源码研究要放在最后的原因。
对于一些基础库,除了阅读源码外,还可以自己写个Demo调用基础库完成一些简单的功能,然后通过调试来看具体的调用栈,通过调用栈来理解基础库的处理逻辑和过程,这比单纯看代码去理解逻辑要高效一些。例如,下面是Netty 4.1版本的telnet服务器样例调试的堆栈,通过堆栈我们可以看到完整的调用栈。
“自顶向下”5个步骤,完整执行下来需要花费较长时间,而时间又是大部分技术人员比较稀缺的资源。
通常情况下,以上5个步骤的前3个步骤,不管是已经成为架构师的技术人员,还是立志成为架构师的技术人员,在研究开源项目的时候都必不可少;第四步可以在准备采用开源项目的时候才实施,第五步可以根据你的时间来进行灵活安排。这里的“灵活安排”不是说省略不去做,而是在自己有一定时间和精力的时候做,因为只有这样才能真正理解和学到具体的技术。
如果感觉自己时间和精力不够,与其蜻蜓点水每个开源项目都去简单了解一下,还不如集中精力将一个开源项目研究通透,就算是每个季度只学习一个开源项目,积累几年后这个数量也是很客观的;而且一旦你将一个项目研究透以后,再去研究其他类似项目,你会发现自己学习的非常快,因为共性的部分你已经都掌握了,只需要掌握新项目差异的部分即可。