架构设计学习


架构设计相关的特性

架构设计的关键思维是判断和取舍,程序设计的关键思维是逻辑和实现

架构设计方法论
架构基础:我会先介绍架构设计的本质、历史背景和目的,然后从复杂度来源以及架构设计的原则和流程来详细介绍架构基础。
高性能架构模式:我会从存储高性能、计算高性能方面,介绍几种设计方案的典型特征和应用场景。
高可用架构模式:我会介绍CAP原理、FMEA分析方法,分析常见的高可用存储架构和高可用计算架构,并给出一些设计方法和技巧。
可扩展架构模式:我会介绍可扩展模式及其基本思想,分析一些常见架构模式。
架构实战:我会将理论和案例结合,帮助你落地前面提到的架构原则、架构流程和架构模式。

架构指的是什么
系统与子系统
系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。它的意思是“总体”“整体”或“联盟”。
关联
规则
能力
子系统也是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。

模块与组件
软件模块(Module)是一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。模块的接口表达了由该模块提供的功能和调用它时所需的元素。模块是可能分开被编写的单位。这使它们可再用和允许人员同时协作、编写及研究不同的模块。
软件组件定义为自包含的、可编程的、可重用的、与语言无关的软件单元,软件组件可以很容易被用于组装应用程序中。
从逻辑的角度来拆分系统后,得到的单元就是“模块”;从物理的角度来拆分系统后,得到的单元就是“组件”。

框架与架构
软件框架(Softwareframework)通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品。
框架关注的是“规范”,架构关注的是“结构”

软件架构指软件系统的顶层结构。

架构设计的目的
架构设计的主要目的是为了解决软件系统复杂度带来的问题。

通过熟悉和理解需求,识别系统复杂性所在的地方,然后针对这些复杂点进行架构设计。
架构设计并不是要面面俱到,不需要每个架构都具备高性能、高可用、高扩展等特点,而是要识别出复杂点然后有针对性地解决问题。
理解每个架构方案背后所需要解决的复杂点,然后才能对比自己的业务复杂点,参考复杂点相似的方案。

复杂度识别
性能
可扩展性
高可用
安全性

高性能
软件系统中高性能带来的复杂度主要体现在两方面,一方面是单台计算机内部为了高性能带来的复杂度;另一方面是多台计算机集群为了高性能带来的复杂度。

单机的复杂度
任务->进程->多进程->多线程
多进程独立
进程间通信的各种方式被设计出来了,包括管道、消息队列、信号量、共享存储等。
并行
多线程

真正的并行就是让多个CPU能够同时执行计算任务,从而实现真正意义上的多任务并行。目前这样的解决方案有3种:
SMP(SymmetricMulti-Processor,对称多处理器结构)、
NUMA(Non-UniformMemoryAccess,非一致存储访问结构)、
MPP(MassiveParallelProcessing,海量并行处理结构)。
其中SMP是我们最常见的,目前流行的多核处理器就是SMP方案

在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合,这个过程同样很复杂

集群的复杂度
任务分配
1台服务器演变为2台服务器后
需要增加一个任务分配器,这个分配器可能是硬件网络设备(例如,F5、交换机等),可能是软件网络设备(例如,LVS),也可能是负载均衡软件(例如,Nginx、HAProxy),还可能是自己开发的系统。选择合适的任务分配器也是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等各方面的因素。

任务分配器和真正的业务服务器之间有连接和交互(即图中任务分配器到业务服务器的连接线),需要选择合适的连接方式,并且对连接进行管理。例如,连接建立、连接检测、连接中断后如何处理等。

任务分配器需要增加分配算法。例如,是采用轮询算法,还是按权重分配,又或者按照负载进行分配。如果按照服务器的负载进行分配,则业务服务器还要能够上报自己的状态给任务分配器。

1台变成了多台
任务分配器从1台变成了多台(对应图中的任务分配器1到任务分配器M),这个变化带来的复杂度就是需要将不同的用户分配到不同的任务分配器上(即图中的虚线“用户分配”部分),常见的方法包括DNS轮询、智能DNS、CDN(ContentDeliveryNetwork,内容分发网络)、GSLB设备(GlobalServerLoadBalance,全局负载均衡)等。

任务分配器和业务服务器的连接从简单的“1对多”(1台任务分配器连接多台业务服务器)变成了“多对多”(多台任务分配器连接多台业务服务器)的网状结构。

机器数量从3台扩展到30台(一般任务分配器数量比业务服务器要少,这里我们假设业务服务器为25台,任务分配器为5台),状态管理、故障处理复杂度也大大增加

任务分解
把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统

简单的系统更加容易做到高性能
可以针对单个任务进行扩展

虽然系统拆分可能在某种程度上能提升业务处理性能,但提升性能也是有限的,不可能系统不拆分的时候业务处理耗时为50ms,系统拆分后业务处理耗时只要1ms,
因为最终决定业务处理性能的还是业务逻辑本身,业务逻辑本身没有发生大的变化下,理论上的性能是有一个上限的,系统拆分能够让性能逼近这个极限,
但无法突破这个极限。因此,任务分解带来的性能收益是有一个度的,并不是任务分解越细越好,而对于架构设计来说,如何把握这个粒度就非常关键了

高可用
系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一

系统的高可用方案五花八门,但万变不离其宗,本质上都是通过“冗余”来实现高可用

高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元

复杂性
计算高可用

双机
需要增加一个任务分配器,选择合适的任务分配器也是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等各方面因素。

任务分配器和真正的业务服务器之间有连接和交互,需要选择合适的连接方式,并且对连接进行管理。例如,连接建立、连接检测、连接中断后如何处理等。

任务分配器需要增加分配算法。例如,常见的双机算法有主备、主主,主备方案又可以细分为冷备、温备、热备

存储与计算相比,有一个本质上的区别:将数据从一台机器搬到到另一台机器,需要经过线路进行传输
“数据+逻辑=业务”这个公式来套的话,数据不一致,即使逻辑一致,最后的业务表现就不一样了

存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。
通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确

决策方式
独裁式
独裁式决策指的是存在一个独立的决策主体,我们姑且称它为“决策者”,负责收集信息然后进行决策;
所有冗余的个体,我们姑且称它为“上报者”,都将状态信息发送给决策者

独裁式的决策方式不会出现决策混乱的问题,因为只有一个决策者,
但问题也正是在于只有一个决策者。当决策者本身故障时,整个系统就无法实现准确的状态决策。如果决策者本身又做一套状态决策,那就陷入一个递归的死循环了

协商式
协商式决策指的是两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策

2台服务器启动时都是备机。
2台服务器建立连接。
2台服务器交换状态信息。
某1台服务器做出决策,成为主机;
另一台服务器继续保持备机身份

协商式决策的架构不复杂,规则也不复杂,其难点在于,如果两者的信息交换出现问题(比如主备连接中断),此时状态决策应该怎么做
如果备机在连接中断的情况下认为主机故障,那么备机需要升级为主机,但实际上此时主机并没有故障,那么系统就出现了两个主机,这与设计初衷(1主1备)是不符合的。
如果备机在连接中断的情况下不认为主机故障,则此时如果主机真的发生故障,那么系统就没有主机了,这同样与设计初衷(1主1备)是不符合的
如果为了规避连接中断对状态决策带来的影响,可以增加更多的连接。例如,双连接、三连接。这样虽然能够降低连接中断对状态带来的影响(注意:只能降低,不能彻底解决),但同时又引入了这几条连接之间信息取舍的问题,即如果不同连接传递的信息不同,应该以哪个连接为准?实际上这也是一个无解的答案,无论以哪个连接为准,在特定场景下都可能存在问题。

综合分析,协商式状态决策在某些场景总是存在一些问题的

民主式
民主式决策和协商式决策比较类似,其基础都是独立的个体之间交换信息,每个个体做出自己的决策,然后按照“多数取胜”的规则来确定最终的状态。
不同点在于民主式决策比协商式决策要复杂得多,ZooKeeper的选举算法ZAB,绝大部分人都看得云里雾里,更不用说用代码来实现这套算法了

除了算法复杂,民主式决策还有一个固有的缺陷:脑裂。这个词来源于医学,指人体左右大脑半球的连接被切断后,左右脑因为无法交换信息,导致各自做出决策,然后身体受到两个大脑分别控制,会做出各种奇怪的动作。例如:当一个脑裂患者更衣时,他有时会一只手将裤子拉起,另一只手却将裤子往下脱。脑裂的根本原因是,原来统一的集群因为连接中断,造成了两个独立分隔的子集群,每个子集群单独进行选举,于是选出了2个主机,相当于人体有两个大脑了

为了解决脑裂问题,民主式决策的系统一般都采用“投票节点数必须超过系统总节点数一半”规则来处理
这种方式虽然解决了脑裂问题,但同时降低了系统整体的可用性,即如果系统不是因为脑裂问题导致投票节点数过少,而真的是因为节点故障(例如,节点1、节点2、节点3真的发生了故障),此时系统也不会选出主节点,整个系统就相当于宕机了,尽管此时还有节点4和节点5是正常的

综合分析,无论采取什么样的方案,状态决策都不可能做到任何场景下都没有问题,但完全不做高可用方案又会产生更大的问题,如何选取适合系统的高可用方案,也是一个复杂的分析、判断和选择的过程

可扩展性
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。

在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题;后来的设计模式,更是将可扩展性做到了极致。得益于设计模式的巨大影响力,几乎所有的技术人员对于可扩展性都特别重视

设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。但要达成这两个条件,本身也是一件复杂的事情

预测变化
预测变化的复杂性在于
不能每个设计点都考虑可扩展性。
不能完全不考虑可扩展性。
所有的预测都存在出错的可能性。

对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,
而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉,
所以架构设计评审的时候经常会出现两个设计师对某个判断争得面红耳赤的情况,
原因就在于没有明确标准,不同的人理解和判断有偏差,而最终又只能选择一个判断。

应对变化
预测变化是一回事,采取什么方案来应对变化,又是另外一个复杂的事情。即使预测很准确,如果方案不合适,则系统扩展一样很麻烦

第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题

1.系统需要拆分出变化层和稳定层
2.需要设计变化层和稳定层之间的接口

第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”
抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。
这种方案典型的实践就是设计模式和规则引擎

复杂度增加

低成本,安全,规模
低成本
低成本本质上是与高性能和高可用冲突的,所以低成本很多时候不会是架构设计的首要目标,而是架构设计的附加约束

我们首先设定一个成本目标,当我们根据高性能、高可用的要求设计出方案时,评估一下方案是否能满足成本目标,如果不行,就需要重新设计架构;如果无论如何都无法设计出满足成本要求的方案,那就只能找老板调整成本目标了

无论是引入新技术,还是自己创造新技术,都是一件复杂的事情。引入新技术的主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合起来;创造新技术的主要复杂度在于需要自己去创造全新的理念和技术,并且新技术跟旧技术相比,需要有质的飞跃

安全
安全可以分为两类:一类是功能上的安全,一类是架构上的安全

功能安全
XSS攻击、CSRF攻击、SQL注入、Windows漏洞、密码破解

架构安全
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流

防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域应用较多。但在互联网领域,防火墙的应用场景并不多。因为互联网的业务具有海量用户访问和高并发的特点,防火墙的性能不足以支撑;尤其是互联网领域的DDoS攻击

互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现


规模
1.功能越来越多,导致系统复杂度指数级上升
2.数据越来越多,系统复杂度发生质变

MySQL单表的数据因不同的业务和应用场景会有不同的最优值,但不管怎样都肯定是有一定的限度的,一般推荐在5000万行左右

当MySQL单表数据量太大时,我们必须考虑将单表拆分为多表,这个拆分过程也会引入更多复杂性
拆表的规则是什么?
拆完表后查询如何处理?

架构设计三原则
优秀程序员和架构师之间还有一个明显的鸿沟需要跨越,这个鸿沟就是“不确定性”
架构设计并没有像编程语言那样的语法来进行约束,更多的时候是面对多种可能性时进行选择

合适原则、简单原则、演化原则

合适原则
合适原则宣言:“合适优于业界领先”。

将军难打无兵之仗->没那么多人,却想干那么多活,是失败的第一个主要原因。
罗马不是一天建成的->没有那么多积累,却想一步登天,是失败的第二个主要原因
冰山下面才是关键->没有那么卓越的业务场景,却幻想灵光一闪成为天才,是失败的第三个主要原因

真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地

简单原则
简单原则宣言:“简单优于复杂”

“复杂”在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是“问题”

软件领域的复杂性体现在两个方面
1.结构的复杂性
组件越多,就越有可能其中某个组件出现故障,从而导致系统故障
定位一个复杂系统中的问题总是比简单系统更加困难
2.逻辑的复杂性
逻辑复杂几乎会导致软件工程的每个环节都有问题
功能复杂的组件,另外一个典型特征就是采用了复杂的算法

演化原则
演化原则宣言:“演化优于一步到位”

对于建筑来说,永恒是主题;而对于软件来说,变化才是主题

软件架构设计同样是类似的过程
首先,设计出来的架构要满足当时的业务需要。
其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。
最后,当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等(类似生物体内的基因)却可以在新架构中延续。

即使是大公司的团队,在设计一个新系统的架构时,也需要遵循演化的原则,而不应该认为团队人员多、资源多,不管什么系统上来就要一步到位,因为业务的发展和变化是很快的,不管多牛的团队,也不可能完美预测所有的业务发展和变化路径

架构设计原则案例

淘宝
手机qq

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

有经验的架构师可能一看需求就知道复杂度大概在哪里;如果经验不足,那只能采取“排查法”,从不同的角度逐一进行分析

高性能
tps/qps

高可用

高扩展

设计备选方案

成熟的架构师需要对已经存在的技术非常熟悉,对已经经过验证的架构模式烂熟于心,然后根据自己对业务的理解,挑选合适的架构模式进行组合,再对组合后的方案进行修改和调整

备选方案的数量以3~5个为最佳
备选方案的差异要比较明显
备选方案的技术不要只局限于已经熟悉的技术

备选阶段关注的是技术选型,而不是技术细节,技术选型的差异要比较明显

评估和选择备选方案
每个方案都是可行的,如果方案不可行就根本不应该作为备选方案。没有哪个方案是完美的。例如,A方案有性能的缺点,B方案有成本的缺点,C方案有新技术不成熟的风险。评价标准主观性比较强,比如设计师说A方案比B方案复杂,但另外一个设计师可能会认为差不多,因为比较难将“复杂”一词进行量化。因此,方案评审的时候我们经常会遇到几个设计师针对某个方案或者某个技术点争论得面红耳赤

指导思想
最简派
最牛派
最熟派
领导派

评估和选择备选方案
360环评
列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案
常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等
在评估这些质量属性时,需要遵循架构设计原则1“合适原则”和原则2“简单原则”,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了

首先是团队规模扩大,20个人的团队在同一个系统上开发,开发效率变将很低,系统迭代速度很慢,经常出现某个功能开发完了要等另外的功能开发完成才能一起测试上线,此时如果要解决问题,就需要将系统拆分为更多子系统。
其次是原来单机房的集群设计不满足业务需求了,需要升级为异地多活的架构

引入开源方案工作量小,但是可运维性和可扩展性差;
自研工作量大,但是可运维和可维护性好;
使用C语言开发性能高,但是目前团队C语言技术积累少;
使用Java技术积累多,但是性能没有C语言开发高,成本会高一些

加权法(没有明确的标准):
每个质量属性给一个权重。例如,性能的权重高中低分别得10分、5分、3分,成本权重高中低分别是5分、3分、1分,
然后将每个方案的权重得分加起来,最后看哪个方案的权重得分最高就选哪个

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

评估和选择备选方案实战

详细方案设计
假如我们确定使用Elasticsearch来做全文搜索,那么就需要确定Elasticsearch的索引是按照业务划分,还是一个大索引就可以了;副本数量是2个、3个还是4个,集群节点数量是3个还是6个等。
假如我们确定使用MySQL分库分表,那么就需要确定哪些表要分库分表,按照什么维度来分库分表,分库分表后联合查询怎么处理等。
假如我们确定引入Nginx来做负载均衡,那么Nginx的主备怎么做,Nginx的负载均衡策略用哪个(权重分配?轮询?ip_hash?)等

轮询(默认)每个请求按时间顺序逐一分配到不同的后端服务器,后端服务器分配的请求数基本一致,如果后端服务器“down掉”,能自动剔除。加权轮询根据权重来进行轮询,权重高的服务器分配的请求更多,主要适应于后端服务器性能不均的情况,如新老服务器混用。

ip_hash每个请求按访问IP的hash结果分配,这样每个访客固定访问一个后端服务器,主要用于解决session的问题,如购物车类的应用。

fair按后端服务器的响应时间来分配请求,响应时间短的优先分配,能够最大化地平衡各后端服务器的压力,可以适用于后端服务器性能不均衡的情况,也可以防止某台后端服务器性能不足的情况下还继续接收同样多的请求从而造成雪崩效应。

url_hash按访问URL的hash结果来分配请求,每个URL定向到同一个后端服务器,适用于后端服务器能够将URL的响应结果缓存的情况。

这几个策略的适用场景区别还是比较明显的,根据我们的业务需要,挑选一个合适的即可。例如,比如一个电商架构,由于和session比较强相关,因此如果用Nginx来做集群负载均衡,那么选择ip_hash策略是比较合适的

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

通过下面方式有效地避免
架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解
通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度

细化设计点

读写分离
大部分情况下,我们做架构设计主要都是基于已有的成熟模式,结合业务和团队的具体情况,进行一定的优化或者调整;
即使少部分情况我们需要进行较大的创新,前提也是需要对已有的各种架构模式和技术非常熟悉

读写分离原理
读写分离的基本原理是将数据库读写操作分散到不同的节点上
读写分离的基本实现是
数据库服务器搭建主从集群,一主一从、一主多从都可以。
数据库主机负责读写操作,从机只负责读操作。
数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
业务服务器将写操作发给数据库主机,将读操作发给数据库从机

读写分离的实现逻辑并不复杂,但有两个细节点将引入设计复杂度:主从复制延迟和分配机制。

解决主从复制延迟有几种常见的方法:

写操作后的读操作指定发给数据库主服务器
例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,
如果哪个新来的程序员不知道这样写代码,就会导致一个bug。

读从机失败后再读一次主机
这就是通常所说的“二次读取”,二次读取和业务无绑定,只需要对底层数据库访问的API进行封装即可,实现代价较小,
不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。
例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。

关键业务读写操作全部指向主机,非关键业务采用读写分离
例如,对于一个用户管理系统来说,注册+登录的业务读写操作全部访问主机,用户的介绍、爱好、等级等业务,可以采用读写分离,
因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。

分配机制
将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装
程序代码封装
程序代码封装指在代码中抽象一个数据访问层,实现读写操作分离和数据库服务器连接的管理
程序代码封装的方式具备几个特点:
实现简单,而且可以根据业务做较多定制化的功能。
每个编程语言都需要自己实现一次,无法通用,如果一个业务包含多个编程语言写的多个子系统,则重复开发的工作量比较大。
故障情况下,如果主从发生切换,则可能需要所有系统都修改配置并重启

中间件封装
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。
中间件对业务服务器提供SQL兼容的协议,业务服务器无须自己进行读写分离。
对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器
数据库中间件的方式具备的特点:
能够支持多种编程语言,因为数据库中间件对业务服务器提供的是标准SQL接口。
数据库中间件要支持完整的SQL语法和数据库服务器的协议(例如,MySQL客户端和服务器的连接协议),实现比较复杂,细节特别多,很容易出现bug,需要较长的时间才能稳定。
数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求也很高。
数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态。
例如,向某个测试表写入一条数据,成功的就是主机,失败的就是从机

由于数据库中间件的复杂度要比程序代码封装高出一个数量级,一般情况下建议采用程序语言封装的方式,或者使用成熟的开源数据库中间件

分库分表
读写分离分散了数据库读写操作的压力,但没有分散存储压力,
当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,
主要体现在这几个方面:
数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
数据文件越大,极端情况下丢失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)

业务分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器
1.join操作问题业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用SQL的join查询。
2.事务问题
原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。
虽然数据库厂商提供了一些分布式事务的解决方案(例如,MySQL的XA),但性能实在太低,与高性能存储的目标是相违背的

成本问题
业务分库同时也带来了成本的代价,本来1台服务器搞定的事情,现在要3台,如果考虑备份,那就是2台变成了6台

基于上述原因,对于小公司初创业务,并不建议一开始就这样拆分,主要有几个原因
初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正的存储和访问压力,业务分库并不能为业务带来价值。
业务分库后,表之间的join查询、数据库事务无法简单实现了。
业务分库后,因为不同的数据要读写不同的数据库,代码中需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量。而业务初创期间最重要的是快速实现、快速验证,业务分库会拖慢业务节奏

而对于业界成熟的大公司来说,由于已经有了业务分库的成熟解决方案,并且即使是尝试性的新业务,用户规模也是海量的,这与前面提到的初创业务的小公司有本质区别,因此最好在业务开始设计时就考虑业务分库

分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,
但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分

从上往下切就是垂直切分,因为刀的运行轨迹与蛋糕是垂直的,这样可以把蛋糕切成高度相等(面积可以相等也可以不相等)的两部分,对应到表的切分就是表记录数相同但包含不同的列

从左往右切就是水平切分,因为刀的运行轨迹与蛋糕是平行的,这样可以把蛋糕切成面积相等(高度可以相等也可以不相等)的两部分,对应到表的切分就是表的列相同但包含不同的行数据

单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定,并不强制要求单表切分为多表后一定要分散到不同数据库中。原因在于单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,是可以不拆分到多台数据库服务器的,毕竟我们在上面业务分库的内容看到业务分库也会引入很多复杂性的问题;如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了

分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性
1.垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去

2.水平分表
水平分表适合表行数特别大的表,有的公司要求单表行数超过5000万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过1000万就要分表了;而对于一些简单的表,即使存储数据超过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

配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。

配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据),性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题

join操作
水平分表后,数据分散在多个表中,如果需要与其他表进行join查询,需要在业务代码或者数据库中间件中进行多次join查询,然后将结果合并

count()操作
水平分表后,虽然物理上数据分散到多个表中,但某些业务逻辑上还是会将这些表当作一个表来处理。例如,获取记录总数用于分页或者展示,水平分表前用一个count()就能完成的操作,在分表后就没那么简单了。常见的处理方式有下面两种

count()相加:具体做法是在业务代码或者数据库中间件中对每个表进行count()操作,然后将结果相加。这种方式实现简单,缺点就是性能比较低。例如,水平分表后切分为20张表,则要进行20次count(*)操作,如果串行的话,可能需要几秒钟才能得到结果

记录数表:具体做法是新建一张表,假如表名为“记录数表”,包含table_name、row_count两个字段,每次插入或者删除子表数据成功后,都更新“记录数表”

这种方式获取表记录数的性能要大大优于count()相加的方式,因为只需要一次简单查询就可以获取数据。缺点是复杂度增加不少,对子表的操作要同步操作“记录数表”,如果有一个业务逻辑遗漏了,数据就会不一致;且针对“记录数表”的操作和针对子表的操作无法放在同一事务中进行处理,异常的情况下会出现操作子表成功了而操作记录数表失败,同样会导致数据不一致

此外,记录数表的方式也增加了数据库的写压力,因为每次针对子表的insert和delete操作都要update记录数表,所以对于一些不要求记录数实时保持精确的业务,也可以通过后台定时更新记录数表。定时更新实际上就是“count()相加”和“记录数表”的结合,即定时通过count()相加计算表的记录数,然后更新记录数表中的数据

orderby操作
水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序

数据库读写分离类似,分库分表具体的实现方式也是“程序代码封装”和“中间件封装”,但实现会更复杂。读写分离实现时只要识别SQL操作是读操作还是写操作,通过简单的判断SELECT、UPDATE、INSERT、DELETE几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断SQL中具体需要操作的表、操作函数(例如count函数)、orderby、groupby操作等,然后再根据不同的操作进行不同的处理。例如orderby操作,需要先从多个库查询到各个库的数据,然后再重新orderby才能得到最终的结果

nosql
关系数据库存储的是行记录,无法存储数据结构

关系数据库的schema扩展很不方便
关系数据库的表结构schema是强约束,操作不存在的列会报错,业务变化时扩充列也比较麻烦,需要执行DDL(datadefinitionlanguage,如CREATE、ALTER、DROP等)语句修改,而且修改时可能会长时间锁表(例如,MySQL可能将表锁住1个小时)

关系数据库在大数据场景下I/O较高

关系数据库的全文搜索功能比较弱

NoSQL方案带来的优势,本质上是牺牲ACID中的某个或者某几个特性,因此我们不能盲目地迷信NoSQL是银弹,而应该将NoSQL作为SQL的一个有力补充

常见的NoSQL方案分为4类。
K-V存储:解决关系数据库无法存储数据结构的问题,以Redis为代表。
文档数据库:解决关系数据库强schema约束的问题,以MongoDB为代表。
列式数据库:解决关系数据库大数据场景下的I/O问题,以HBase为代表。
全文搜索引擎:解决关系数据库的全文搜索性能问题,以Elasticsearch为代表。

kv
Redis的缺点主要体现在并不支持完整的ACID事务,Redis虽然提供事务功能,但Redis的事务和关系数据库的事务不可同日而语,Redis的事务只能保证隔离性和一致性(I和C),无法保证原子性和持久性(A和D)。

文档
文档数据库的no-schema特性,给业务开发带来了几个明显的优势
1.新增字段简单
2.历史数据不会出错
3.可以很容易存储复杂数据
文档数据库no-schema的特性带来的这些优势也是有代价的,最主要的代价就是不支持事务
因此某些对事务要求严格的业务场景是不能使用文档数据库的
文档数据库另外一个缺点就是无法实现关系数据库的join操作

列式
关系数据库按照行式来存储数据,主要有以下几个优势
业务同时读取多个列时效率高,因为这些列都是按行存储在一起的,一次磁盘操作就能够把一行数据中的各个列都读取到内存中。
能够一次性完成对一行中的多个列的写操作,保证了针对行数据写操作的原子性和一致性;
否则如果采用列存储,可能会出现某次写操作,有的列成功了,有的列失败了,导致数据不一致
典型的场景就是海量数据进行统计
因为单个列的数据相似度相比行来说更高,能够达到更高的压缩率。

同样,如果场景发生变化,列式存储的优势又会变成劣势。典型的场景是需要频繁地更新多个列。
因为列式存储将不同列存储在磁盘上不连续的空间,导致更新多个列时磁盘是随机写操作;
而行式存储时同一行多个列都存储在连续的空间,一次磁盘写操作就可以完成,列式存储的随机写效率要远远低于行式存储的写效率。
此外,列式存储高压缩率在更新场景下也会成为劣势,因为更新时需要将存储数据解压后更新,然后再压缩,最后写入磁盘

一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景主要是针对部分列单列进行操作,且数据写入后就无须再更新删除

全文
全文搜索引擎的技术原理被称为“倒排索引”(Invertedindex),也常被称为反向索引、置入档案或反向档案,是一种索引方法,其基本原理是建立单词到文档的索引
全文搜索引擎的索引对象是单词和文档,而关系数据库的索引对象是键和行,两者的术语差异很大,不能简单地等同起来

高性能缓存架构
需要经过复杂运算后得出的数据,存储系统无能为力
读多写少的数据,存储系统有心无力

分析缓存的架构设计要点
缓存穿透
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:

1.存储数据不存在第一种情况是被访问的数据确实不存在。一般情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。
通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。
这种情况的解决办法比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统

2.缓存数据生成耗费大量时间或者资源
第二种情况是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。
如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况

典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。
通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题

分页缓存的有效期设置为1天,因为设置太长时间的话,缓存不能反应真实的数据。

通常情况下,用户不会从第1页到最后1页全部看完,一般用户访问集中在前10页,因此第10页以后的缓存过期失效的可能性很大。
竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全部遍历,从第1页到最后1页全部都会读取,此时很多分页缓存可能都失效了。
由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能(orderbylimit操作),因此爬虫会将整个数据库全部拖慢

通常的应对方案要么就是识别爬虫然后禁止访问,但这可能会影响SEO和推广;
要么就是做好监控,发现问题后及时处理,因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理

缓存雪崩
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃

缓存雪崩的常见解决方法有两种:更新锁机制和后台更新机制

1.更新锁
对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,
未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值

2.后台更新
由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。

后台定时机制需要考虑一种特殊的场景,当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:

后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1秒或者100毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般

业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好

后台更新既适应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些。

缓存热点
缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力

缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。

由于缓存的各种访问策略和存储的访问策略是相关的,因此上面的各种缓存设计方案通常情况下都是集成在存储访问方案中,可以采用“程序代码实现”的中间层方式,也可以采用独立的中间件来实现

PPC与TPC
磁盘、操作系统、CPU、内存、缓存、网络、编程语言、架构

高性能架构设计主要集中在两方面:
尽量提升单服务器的性能,将单服务器的性能发挥到极致。
如果单服务器无法支撑性能,设计服务器集群方案

架构设计决定了系统性能的上限,实现细节决定了系统性能的下限

并发模型有如下两个关键设计点:
服务器如何管理连接。
服务器如何处理请求。

I/O模型:阻塞、非阻塞、同步、异步。
进程模型:单进程、多进程、多线程。

PPC
PPC是ProcessPerConnection的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的UNIX网络服务器所采用的模型
父进程接受连接(图中accept)。
父进程“fork”子进程(图中fork)。
子进程处理连接的读写请求(图中子进程read、业务处理、write)。
子进程关闭连接(图中子进程中的close)

父进程“fork”子进程后,直接调用了close,看起来好像是关闭了连接,
其实只是将连接的文件描述符引用计数减一,真正的关闭连接是等子进程也调用close后,连接对应的文件描述符引用计数变为0后,操作系统才会真正关闭连接

fork代价高:站在操作系统的角度,创建一个进程的代价是很高的,需要分配很多内核资源,需要将内存映像从父进程复制到子进程。即使现在的操作系统在复制内存映像时用到了CopyonWrite(写时复制)技术,总体来说创建进程的代价还是很大的。

父子进程通信复杂:父进程“fork”子进程时,文件描述符可以通过内存映像复制从父进程传到子进程,但“fork”完成后,父子进程通信就比较麻烦了,需要采用IPC(InterprocessCommunication)之类的进程通信方案。例如,子进程需要在close之前告诉父进程自己处理了多少个请求以支撑父进程进行全局的统计,那么子进程和父进程必须采用IPC方案来传递信息

支持的并发连接数量有限:如果每个连接存活时间比较长,而且新的连接又源源不断的进来,则进程数量会越来越多,操作系统进程调度和切换的频率也越来越高,系统的压力也会越来越大。因此,一般情况下,PPC方案能处理的并发连接数量最大也就几百

prefork
prefork就是提前创建进程(pre-fork)。系统在启动的时候就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去fork进程的操作,让用户访问更快、体验更好

prefork的实现关键就是多个子进程都accept同一个socket,当有新的连接进入时,操作系统保证只有一个进程能最后accept成功。但这里也存在一个小小的问题:“惊群”现象,就是指虽然只有一个子进程能accept成功,但所有阻塞在accept上的子进程都会被唤醒,这样就导致了不必要的进程调度和上下文切换了。幸运的是,操作系统可以解决这个问题,例如Linux2.6版本后内核已经解决了accept惊群问题

prefork模式和PPC一样,还是存在父子进程通信复杂、支持的并发连接数量有限的问题,因此目前实际应用也不多。Apache服务器提供了MPMprefork模式,推荐在需要可靠性或者与旧软件兼容的站点时采用这种模式,默认情况下最大支持256个并发连接

TPC
TPC是ThreadPerConnection的缩写,其含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。与进程相比,线程更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的,线程通信相比进程通信更简单。因此,TPC实际上是解决或者弱化了PPCfork代价高的问题和父子进程通信复杂的问题

父进程接受连接(图中accept)。
父进程创建子线程(图中pthread)。
子线程处理连接的读写请求(图中子线程read、业务处理、write)。
子线程关闭连接(图中子线程中的close)

和PPC相比,主进程不用“close”连接了。原因是在于子线程是共享主进程的进程空间的,连接的文件描述符并没有被复制,因此只需要一次close即可

TPC虽然解决了fork代价高和进程通信复杂的问题,但是也引入了新的问题,具体表现在
创建线程虽然比创建进程代价低,但并不是没有代价,高并发时(例如每秒上万连接)还是有性能问题。
无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。
多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界)

prethread
prethread模式会预先创建线程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去创建线程的操作,让用户感觉更快、体验更好

主进程accept,然后将连接交给某个线程处理。
子线程都尝试去accept,最终只有一个线程accept成功

Apache服务器的MPMworker模式本质上就是一种prethread方案,但稍微做了改进。Apache服务器会首先创建多个进程,每个进程里面再创建多个线程,这样做主要是为了考虑稳定性,即:即使某个子进程里面的某个线程异常导致整个子进程退出,还会有其他子进程继续提供服务,不会导致整个服务器全部挂掉。

prethread理论上可以比prefork支持更多的并发连接,Apache服务器MPMworker模式默认支持16×25=400个并发处理线程

Reactor与Proactor

Reactor
I/O多路复用技术归纳起来有两个关键实现点
当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,
常见的实现方式有select、epoll、kqueue等

当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。

Reactor模式的具体实现方案灵活多变,主要体现在
Reactor的数量可以变化:可以是一个Reactor,也可以是多个Reactor。
资源池的数量可以变化:以进程为例,可以是单个进程,也可以是多个进程(线程类似)

多Reactor单进程”方案仅仅是一个理论上的方案,实际没有应用

单Reactor单进程/线程。
单Reactor多线程。
多Reactor多进程/线程。

Java语言一般使用线程(例如,Netty),
C语言使用进程和线程都可以。
例如,Nginx使用进程,Memcache使用线程

1.单Reactor单进程/线程
只有一个进程,无法发挥多核CPU的性能;只能采取部署多个系统来利用多核CPU,但这样会带来运维复杂度,本来只要维护一个系统,用这种方式需要在一台机器上维护多套系统。

Handler在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈

只适用于业务处理非常快速的场景

单Reactor多线程
多线程数据共享和访问比较复杂。例如,子线程完成业务处理后,要把结果传递给主线程的Reactor进行发送,这里涉及共享数据的互斥和保护机制。以Java的NIO为例,Selector是线程安全的,但是通过Selector.selectKeys()返回的键的集合是非线程安全的,对selectedkeys的处理必须单线程处理或者采取同步措施进行保护

Reactor承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈

多Reactor多进程/线程
父进程和子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理。
父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。
子进程之间是互相独立的,无须同步共享之类的处理(这里仅限于网络模型相关的select、read、send等无须同步共享,“业务处理”还是有可能需要同步共享的)

Proactor
Reactor是非阻塞同步网络模型
Reactor可以理解为“来了事件我通知你,你来处理”,
而Proactor可以理解为“来了事件我来处理,处理完了我通知你”

分类及架构
高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法

DNS负载均衡
DNS负载均衡的本质是DNS解析同一个域名可以返回不同的IP地址

优点
简单、成本低:负载均衡工作交给DNS服务器处理,无须自己开发或者维护负载均衡设备。

就近访问,提升访问速度:DNS解析时可以根据请求来源IP,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能

缺点
更新不及时:DNS缓存的时间比较长,修改DNS配置后,由于缓存的原因,还是有很多用户会继续访问修改前的IP,这样的访问会失败,达不到负载均衡的目的,并且也影响用户正常使用业务。

扩展性差:DNS负载均衡的控制权在域名商那里,无法根据业务特点针对其做更多的定制化功能和扩展特性。

分配策略比较简单:DNS负载均衡支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载);也无法感知后端服务器的状态


硬件负载均衡
硬件负载均衡的优点是:功能强大:全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡。
性能强大:对比一下,软件负载均衡支持到10万级并发已经很厉害了,硬件负载均衡可以支持100万以上的并发。
稳定性高:商用硬件负载均衡,经过了良好的严格测试,经过大规模使用,稳定性高。
支持安全防护:硬件均衡设备除具备负载均衡功能外,还具备防火墙、防DDoS攻击等安全功能。

硬件负载均衡的缺点是:价格昂贵:最普通的一台F5就是一台“马6”,好一点的就是“Q7”了。扩展能力差:硬件设备,
可以根据业务进行配置,但无法进行扩展和定制

硬件负载均衡的缺点
价格昂贵:最普通的一台F5就是一台“马6”,好一点的就是“Q7”了。
扩展能力差:硬件设备,可以根据业务进行配置,但无法进行扩展和定制

软件负载均衡
软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有Nginx和LVS,其中Nginx是软件的7层负载均衡,LVS是Linux内核的4层负载均衡。4层和7层的区别就在于协议和灵活性,Nginx支持HTTP、E-mail协议;而LVS是4层负载均衡,和协议无关,几乎所有应用都可以做,例如,聊天、数据库等

nginx
性能一般:一个Nginx大约能支撑5万并发。功能没有硬件负载均衡那么强大。一般不具备防火墙和防DDoS攻击等安全功能

基于它们的优缺点进行组合使用。具体来说,组合的基本原则为:
DNS负载均衡用于实现地理级别的负载均衡
硬件负载均衡用于实现集群级别的负载均衡
软件负载均衡用于实现机器级别的负载均衡

dns->f5->集群(nginx)

算法
任务平分类:负载均衡系统将收到的任务平均分配给服务器进行处理,这里的“平均”可以是绝对数量的平均,也可以是比例或者权重上的平均。

负载均衡类:负载均衡系统根据服务器的负载来进行分配,这里的负载并不一定是通常意义上我们说的“CPU负载”,而是系统当前的压力,可以用CPU负载来衡量,也可以用连接数、I/O使用率、网卡吞吐量等来衡量系统的压力。

性能最优类:负载均衡系统根据服务器的响应时间来进行任务分配,优先将新任务分配给响应最快的服务器。

Hash类:负载均衡系统根据任务中的某些关键信息进行Hash运算,将相同Hash值的请求分配到同一台服务器上。常见的有源地址Hash、目标地址Hash、sessionidhash、用户IDHash等

轮询
某个服务器当前因为触发了程序bug进入了死循环导致CPU负载很高,负载均衡系统是不感知的,还是会继续将请求源源不断地发送给它。
集群中有新的机器是32核的,老的机器是16核的,负载均衡系统也是不关注的,新老机器分配的任务数是一样的

加权轮询
加权轮询解决了轮询算法中无法根据服务器的配置差异进行任务分配的问题,但同样存在无法根据服务器的状态差异进行任务分配的问题

负载最低优先
LVS这种4层网络负载均衡设备,可以以“连接数”来判断服务器的状态,服务器连接数越大,表明服务器压力越大。
Nginx这种7层网络负载系统,可以以“HTTP请求数”来判断服务器状态(Nginx内置的负载均衡算法不支持这种方式,需要进行扩展)。
如果我们自己开发负载均衡系统,可以根据业务特点来选择指标衡量系统压力。如果是CPU密集型,可以以“CPU负载”来衡量系统压力;如果是I/O密集型,可以以“I/O负载”来衡量系统压力

最少连接数优先的算法要求负载均衡系统统计每个服务器当前建立的连接,其应用场景仅限于负载均衡接收的任何连接请求都会转发给服务器进行处理,否则如果负载均衡系统和服务器之间是固定的连接池方式,就不适合采取这种算法。例如,LVS可以采取这种算法进行负载均衡,而一个通过连接池的方式连接MySQL集群的负载均衡系统就不适合采取这种算法进行负载均衡。CPU负载最低优先的算法要求负载均衡系统以某种方式收集每个服务器的CPU负载,而且要确定是以1分钟的负载为标准,还是以15分钟的负载为标准,不存在1分钟肯定比15分钟要好或者差。不同业务最优的时间间隔是不一样的,时间间隔太短容易造成频繁波动,时间间隔太长又可能造成峰值来临时响应缓慢

性能最优类
性能最优优先类算法本质上也是感知了服务器的状态,只是通过响应时间这个外部标准来衡量服务器状态而已。因此性能最优优先类算法存在的问题和负载最低优先类算法类似,复杂度都很高,主要体现在
负载均衡系统需要收集和分析每个服务器每个任务的响应时间,在大量任务处理的场景下,这种收集和统计本身也会消耗较多的性能。

为了减少这种统计上的消耗,可以采取采样的方式来统计,即不统计所有任务的响应时间,而是抽样统计部分任务的响应时间来估算整体任务的响应时间。采样统计虽然能够减少性能消耗,但使得复杂度进一步上升,因为要确定合适的采样率,采样率太低会导致结果不准确,采样率太高会导致性能消耗较大,找到合适的采样率也是一件复杂的事情。

无论是全部统计还是采样统计,都需要选择合适的周期:是10秒内性能最优,还是1分钟内性能最优,还是5分钟内性能最优……没有放之四海而皆准的周期,需要根据实际业务进行判断和选择,这也是一件比较复杂的事情,甚至出现系统上线后需要不断地调优才能达到最优设计

hash
负载均衡系统根据任务中的某些关键信息进行Hash运算,将相同Hash值的请求分配到同一台服务器上,这样做的目的主要是为了满足特定的业务需求

源地址
Hash将来源于同一个源IP地址的任务分配给同一个服务器进行处理,适合于存在事务、会话的业务。例如,当我们通过浏览器登录网上银行时,会生成一个会话信息,这个会话是临时的,关闭浏览器后就失效。网上银行后台无须持久化会话信息,只需要在某台服务器上临时保存这个会话就可以了,但需要保证用户在会话存在期间,每次都能访问到同一个服务器,这种业务场景就可以用源地址Hash来实现。

IDHash
将某个ID标识的业务分配到同一个服务器中进行处理,这里的ID一般是临时性数据的ID(如sessionid)。例如,上述的网上银行登录的例子,用sessionidhash同样可以实现同一个会话期间,用户每次都是访问到同一台服务器的目的

cap
CAP关注的是对数据的读写操作
保持一致性违背可用性cp
保持可用性不满足一致性ap

C与A之间的取舍可以在同一系统内以非常细小的粒度反复发生,而每一次的决策可能因为具体的操作,乃至因为牵涉到特定的数据或用户而有所不同

CAP理论中的C在实践中是不可能完美实现的,在数据复制的过程中,节点A和节点B的数据并不一致。
例如和金钱相关的用户余额,或者和抢购相关的商品库存,技术上是无法做到分布式场景下完美的一致性的。而业务上必须要求一致性,因此单个用户的余额、单个商品的库存,理论上要求选择CP而实际上CP都做不到,只能选择CA。也就是说,只能单点写入,其他节点做备份,无法做到分布式情况下多点写入

正常运行情况下,不存在CP和AP的选择,可以同时满足CA

CAP理论告诉我们分布式系统只能选择CP或者AP,但其实这里的前提是系统发生了“分区”现象。如果系统没有发生分区现象,也就是说P不存在的时候(节点间的网络连接一切正常),我们没有必要放弃C或者A,应该C和A都可以保证,这就要求架构设计的时候既要考虑分区发生时选择CP还是AP,也要考虑分区没有发生时如何保证CA

放弃并不等于什么都不做,需要为分区恢复后做准备

日志恢复

acid

base

FMEA方法
故障模式与影响分析
具体的分析方法
给出初始的架构设计图
假设架构中某个部件发生故障
分析此故障对系统功能造成的影响
根据分析结果,判断架构是否需要进行优化

功能点
故障模式
故障影响
严重程度
故障原因
故障概率(硬件/开源系统/自研系统)
风险程度
已有措施(检测告警/容错/自恢复)
规避措施
解决措施
后续规范

双机架构
存储高可用方案的本质都是通过将数据复制到多个存储设备,通过数据冗余的方式来实现高可用,其复杂性主要体现在如何应对复制延迟和中断导致的数据不一致问题。

数据如何复制?
各个节点的职责是什么?
如何应对复制延迟?
如何应对复制中断?

主备/主从/主主/集群/分区

主备
主备复制是最常见也是最简单的一种存储高可用方案,几乎所有的存储系统都提供了主备复制的功能,例如MySQL、Redis、MongoDB

主从复制
写少读多的业务使用主从复制的存储架构比较多

双机切换
主备间状态判断
切换决策
数据冲突解决

互联式,中介式,模拟式

主主复制
主主复制架构对数据的设计有严格的要求,一般适合于那些临时性、可丢失、可覆盖的数据场景。例如,用户登录产生的session数据(可以重新登录生成)、用户行为的日志数据(可以丢失)、论坛的草稿数据(可以丢失)等

集群和分区

数据集群
数据集中式集群
一主多从/一主多备

主机如何将数据复制给备机

备机如何检测主机状态

主机故障后,如何决定新的主机

数据分散集群
均衡性
容错性
可伸缩性

数据分区
数据量
分区规则
复制规则(集中式/互备式/独立式)

如何设计计算高可用架构

那些服务器可以执行任务
第一种方式和计算高性能中的集群类似,每个服务器都可以执行任务。例如,常见的访问网站的某个页面。第二种方式和存储高可用中的集群类似,只有特定服务器(通常叫“主机”)可以执行任务。当执行任务的服务器故障后,系统需要挑选新的服务器来执行任务。例如,ZooKeeper的Leader才能处理写操作请求

任务如何重新执行
第一种策略是对于已经分配的任务即使执行失败也不做任何处理,系统只需要保证新的任务能够分配到其他非故障服务器上执行即可。第二种策略是设计一个任务管理器来管理需要执行的计算任务,服务器执行完任务后,需要向任务管理器反馈任务执行结果,任务管理器根据任务执行结果来决定是否需要将任务重新分配到另外的服务器上执行

主备
主备方案的详细设计
主机执行所有计算任务。例如,读写数据、执行操作等。

当主机故障(例如,主机宕机)时,任务分配器不会自动将计算任务发送给备机,此时系统处于不可用状态。

如果主机能够恢复(不管是人工恢复还是自动恢复),任务分配器继续将任务发送给主机。

如果主机不能够恢复(例如,机器硬盘损坏,短时间内无法恢复),则需要人工操作,将备机升为主机,然后让任务分配器将任务发送给新的主机(即原来的备机);同时,为了继续保持主备架构,需要人工增加新的机器作为备机

根据备机状态的不同,主备架构又可以细分为冷备架构和温备架构
冷备:备机上的程序包和配置文件都准备好,但备机上的业务系统没有启动(注意:备机的服务器是启动的),主机故障后,需要人工手工将备机的业务系统启动,并将任务分配器的任务请求切换发送给备机。

温备:备机上的业务系统已经启动,只是不对外提供服务,主机故障后,人工只需要将任务分配器的任务请求切换发送到备机即可。冷备可以节省一定的能源,但温备能够大大减少手工操作时间,因此一般情况下推荐用温备的方式

而缺点正好也体现在“人工操作”这点上,因为人工操作的时间不可控,可能系统已经发生问题了,发现后人工切换的操作效率也比较低,可能需要半个小时才完成切换操作,而且手工操作过程中容易出错。例如,修改配置文件改错了、启动了错误的程序等

计算高可用的主备架构也比较适合与内部管理系统、后台管理系统这类使用人数不多、使用频率不高的业务,不太适合在线的业务

主从
主从方案详细设计
正常情况下,主机执行部分计算任务(如图中的“计算任务A”),备机执行部分计算任务(如图中的“计算任务B”)

当主机故障(例如,主机宕机)时,任务分配器不会自动将原本发送给主机的任务发送给从机,而是继续发送给主机,不管这些任务执行是否成功

如果主机能够恢复(不管是人工恢复还是自动恢复),任务分配器继续按照原有的设计策略分配任务,即计算任务A发送给主机,计算任务B发送给从机

如果主机不能够恢复(例如,机器硬盘损坏,短时间内无法恢复),则需要人工操作,将原来的从机升级为主机(一般只是修改配置即可),增加新的机器作为从机,新的从机准备就绪后,任务分配器继续按照原有的设计策略分配任务

优点:主从架构的从机也执行任务,发挥了从机的硬件性能。
缺点:主从架构需要将任务分类,任务分配器会复杂一些

集群
在可用性要求更加严格的场景中,我们需要系统能够自动完成切换操作,这就是高可用集群方案

高可用计算的集群方案根据集群中服务器节点角色的不同,可以分为两类:
一类是对称集群,即集群中每个服务器的角色都是一样的,都可以执行所有任务;
另一类是非对称集群,集群中的服务器分为多个不同的角色,不同的角色执行不同的任务,例如最常见的Master-Slave角色

对称集群更通俗的叫法是负载均衡集群,因此接下来我使用“负载均衡集群”这个通俗的说法

负载均衡集群详细设计
正常情况下,任务分配器采取某种策略(随机、轮询等)将计算任务分配给集群中的不同服务器。

当集群中的某台服务器故障后,任务分配器不再将任务分配给它,而是将任务分配给其他服务器执行。

当故障的服务器恢复后,任务分配器重新将任务分配给它执行。

负载均衡集群的设计关键点在于两点:
任务分配器需要选取分配策略。
任务分配器需要检测服务器状态

非对称集群中不同服务器的角色是不同的,不同角色的服务器承担不同的职责。以Master-Slave为例,部分任务是Master服务器才能执行,部分任务是Slave服务器才能执行

非对称集群架构详细设计
集群会通过某种方式来区分不同服务器的角色。例如,通过ZAB算法选举,或者简单地取当前存活服务器中节点ID最小的服务器作为Master服务器。

任务分配器将不同任务发送给不同服务器。例如,图中的计算任务A发送给Master服务器,计算任务B发送给Slave服务器。

当指定类型的服务器故障时,需要重新分配角色。例如,Master服务器故障后,需要将剩余的Slave服务器中的一个重新指定为Master服务器;如果是Slave服务器故障,则并不需要重新分配角色,只需要将故障服务器从集群剔除即可

非对称集群相比负载均衡集群,设计复杂度主要体现在两个方面

任务分配策略更加复杂:需要将任务划分为不同类型并分配给不同角色的集群节点。
角色分配策略实现比较复杂:例如,可能需要使用ZAB、Raft这类复杂的算法来实现Leader的选举

ZooKeeper为例:
任务分配器:ZooKeeper中不存在独立的任务分配器节点,每个Server都是任务分配器,Follower收到请求后会进行判断,如果是写请求就转发给Leader,如果是读请求就自己处理。

角色指定:ZooKeeper通过ZAB算法来选举Leader,当Leader故障后,所有的Follower节点会暂停读写操作,开始进行选举,直到新的Leader选举出来后才继续对Client提供服务

异地多活架构
一个系统是否符合异地多活,需要满足两个标准
正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务。
某个地方业务异常的时候,用户访问其他地方正常的业务系统,能够得到正确的业务服务

其代价很高,具体表现为

系统复杂度会发生质的变化,需要设计复杂的异地多活架构
成本会上升,毕竟要多在一个或者多个机房搭建独立的一套业务系统

异地多活虽然功能很强大,但也不是每个业务不管三七二十一都要上异地多活。例如,常见的新闻网站、企业内部的IT系统、游戏、博客站点等,如果无法承受异地多活带来的复杂度和成本,是可以不做异地多活的,只需要做异地备份即可。因为这类业务系统即使中断,对用户的影响并不会很大

根据地理位置上的距离来划分,异地多活架构可以分为同城异区、跨城异地、跨国异地

1.同城异区
机房火灾、机房停电、机房空调故障这类问题发生的概率更高,而且破坏力一样很大。而这些故障场景,同城异区架构都可以很好地解决
同城异区是应对机房级别故障的最优架构

技巧
关键在于搭建高速网络将两个机房连接起来,达到近似一个本地机房的效果。架构设计上可以将两个机房当作本地机房来设计,无须额外考虑

2.跨城异地
跨城异地指的是业务部署在不同城市的多个机房
距离增加带来的最主要问题是两个机房的网络传输速度会降低

技巧
关键在于数据不一致的情况下,业务不受影响或者影响很小,这从逻辑的角度上来说其实是矛盾的,架构设计的主要目的就是为了解决这个矛盾

数据不一致业务肯定不会正常,但跨城异地肯定会导致数据不一致
如果是强一致性要求的数据,例如银行存款余额、支付宝余额等,这类数据实际上是无法做到跨城异地多活的
支付宝等金融相关的系统,对余额这类数据,一般不会做跨城异地的多活架构,而只能采用同城异区这种架构

3.跨国异地
为不同地区用户提供服务
只读类业务做多活

技巧
主要是面向不同地区用户提供业务,或者提供只读业务,对架构设计要求不高

技巧1:保证核心业务的异地多活
技巧2:保证核心数据最终一致性
尽量减少异地多活机房的距离,搭建高速网络
尽量减少数据同步,只同步核心业务相关的数据
保证最终一致性,不保证实时一致性
技巧3:采用多种手段同步数据
虽然绝大部分场景下,存储系统本身的同步功能基本上也够用了,但在某些比较极端的情况下,存储系统本身的同步功能可能难以满足业务需求
解决的方案就是拓开思路,避免只使用存储系统的同步功能,可以将多种手段配合存储系统的同步来使用,甚至可以不采用存储系统的同步方案,改用自己的同步方案

消息队列方式
二次读取方式
存储系统同步方式
回源读取方式
重新生成数据方式

技巧4:只保证绝大部分用户的异地多活
采用多种手段,保证绝大部分用户的核心业务异地多活!

第1步:业务分级
常见的分级标准有下面几种
访问量大的业务
核心业务
产生大量收入的业务

第2步:数据分类
常见的数据特征分析维度有
数据量
唯一性
实时性
可丢失性
可恢复性

第3步:数据同步
存储系统同步
消息队列同步
重复生成
数据不同步到异地机房,每个机房都可以生成数据,这个方案适合于可以重复生成的数据。例如,登录产生的cookie、session数据、缓存数据等

第4步:异常处理
问题发生时,避免少量数据异常导致整体业务不可用
问题恢复后,将异常的数据进行修正
对用户进行安抚,弥补用户损失

常见的异常处理措施有这几类
1.多通道同步
多通道同步设计的方案关键点有
一般情况下,采取两通道即可,采取更多通道理论上能够降低风险,但付出的成本也会增加很多。
数据库同步通道和消息队列同步通道不能采用相同的网络连接,否则一旦网络故障,两个通道都同时故障;可以一个走公网连接,一个走内网连接。
需要数据是可以重复覆盖的,即无论哪个通道先到哪个通道后到,最终结果是一样的。例如,新建账号数据就符合这个标准,而密码数据则不符合这个标准

2.同步和访问结合
接口访问通道和数据库同步通道不能采用相同的网络连接,不能让数据库同步和接口访问都走同一条网络通道,可以采用接口访问走公网连接,数据库同步走内网连接这种方式。
数据有路由规则,可以根据数据来推断应该访问哪个机房的接口来读取数据。例如,有3个机房A、B、C,B机房拿到一个不属于B机房的数据后,需要根据路由规则判断是访问A机房接口,还是访问C机房接口。
由于有同步通道,优先读取本地数据,本地数据无法读取到再通过接口去访问,这样可以大大降低跨机房的异地接口访问数量,适合于实时性要求非常高的数据

3.日志记录
日志记录主要用于用户故障恢复后对数据进行恢复,其主要方式是每个关键操作前后都记录相关一条日志,然后将日志保存在一个独立的地方,当故障恢复后,拿出日志跟数据进行对比,对数据进行修复

服务器上保存日志,数据库中保存数据,这种方式可以应对单台数据库服务器故障或者宕机的情况。
本地独立系统保存日志,这种方式可以应对某业务服务器和数据库同时宕机的情况。例如,服务器和数据库部署在同一个机架,或者同一个电源线路上,就会出现服务器和数据库同时宕机的情况。
日志异地保存,这种方式可以应对机房宕机的情况

应对接口级的故障
导致接口级故障的原因一般有下面几种
内部原因
外部原因

解决接口级故障的核心思想和异地多活基本类似:优先保证核心业务和优先保证绝大部分用户

降级的目的是应对系统自身的故障,而熔断的目的是应对依赖的外部系统故障的情况

降级
系统后门降
独立降级系统

熔断

限流

基于请求限流
为了找到合理的阈值,通常情况下可以采用性能压测来确定阈值,但性能压测也存在覆盖场景有限的问题,可能出现某个性能压测没有覆盖的功能导致系统压力很大;另外一种方式是逐步优化,即:先设定一个阈值然后上线观察运行情况,发现不合理就调整阈值。

基于资源限流
常见的内部资源有:连接数、文件句柄、线程数、请求队列等
设计的时候先根据推断选择某个关键资源和阈值,然后测试验证,再上线观察,如果发现不合理,再进行优化

排队
缓冲

可扩展架构的基本思想和模式

所有的可扩展性架构设计,背后的基本思想都可以总结为一个字:拆!

面向流程拆分:将整个业务流程拆分为几个阶段,每个阶段作为一部分。
面向服务拆分:将系统提供的服务拆分,每个服务作为一部分。
面向功能拆分:将系统提供的功能拆分,每个功能作为一部分。

不同的拆分方式,本质上决定了系统的扩展方式

面向流程拆分:分层架构。
面向服务拆分:SOA、微服务。
面向功能拆分:微内核架构。

可以在系统架构设计中进行组合使用的。以学生管理系统为例,我们最终可以这样设计架构:

整体系统采用面向服务拆分中的“微服务”架构,拆分为“注册服务”“登录服务”“信息管理服务”“安全服务”,每个服务是一个独立运行的子系统。

其中的“注册服务”子系统本身又是采用面向流程拆分的分层架构。

“登录服务”子系统采用的是面向功能拆分的“微内核”架构

分层架构和soa
按照分层架构进行设计时,根据不同的划分维度和对象,可以得到多种不同的分层架构

C/S架构、B/S架构
MVC架构、MVP架构

分层架构设计最核心的一点就是需要保证各层之间的差异足够清晰,边界足够明显,让人看到架构图后就能看懂整个架构

分层架构之所以能够较好地支撑系统扩展,本质在于隔离关注点(separationofconcerns),即每个层中的组件只会处理本层的逻辑

分层时要保证层与层之间的依赖是稳定的,才能真正支撑快速扩展

分层结构的另外一个特点就是层层传递,也就是说一旦分层确定,整个业务流程是按照层进行依次传递的,不能在层之间进行跳跃
分层结构的代价就是冗余,也就是说,不管这个业务有多么简单,每层都必须要参与处理,甚至可能每层都写了一个简单的包装函数

分层架构另外一个典型的缺点就是性能,因为每一次业务请求都需要穿越所有的架构分层,有一些事情是多余的,多少都会有一些性能的浪费
但到了现在,硬件和网络的性能有了质的飞越,其实分层模式理论上的这点性能损失,在实际应用中,绝大部分场景下都可以忽略不计

ESB虽然功能强大,但现实中的协议有很多种,如JMS、WS、HTTP、RPC等,数据格式也有很多种,如XML、JSON、二进制、HTML等。ESB要完成这么多协议和数据格式的互相转换,工作量和复杂度都很大,而且这种转换是需要耗费大量计算性能的,当ESB承载的消息太多时,ESB本身会成为整个系统的性能瓶颈

微服务架构
微服务是一种和SOA相似但本质上不同的架构理念

服务粒度
服务通信
服务交付
应用场景
soa->企业级
微服务->互联网

微服务的陷阱
服务划分过细,服务间关系复杂
服务数量太多,团队效率急剧下降
调用链太长,性能下降
调用链太长,问题定位困难
没有自动化支撑,无法快速交付

微服务架构方法
微服务拆分过细,过分强调“small”。
微服务基础设施不健全,忽略了“automated”。
微服务并不轻量级,规模大了后,“lightweight”不再适应。

服务粒度
“三个火枪手”原则,即一个微服务三个人负责开发

处于维护期了,无须太多的开发,那么平均1个人维护1个微服务甚至几个微服务都可以。当然考虑到人员备份问题,每个微服务最好都安排2个人维护,每个人都可以维护多个微服务

拆分方法
基于业务逻辑拆分
基于可扩展拆分
基于可靠性拆分
避免非核心服务故障影响核心服务
核心服务高可用方案可以更简单
能够降低高可用成本
基于性能拆分

以上几种拆分方式不是多选一,而是可以根据实际情况自由排列组合,例如可以基于可靠性拆分出服务A,基于性能拆分出服务B,基于可扩展拆分出C/D/F三个服务,加上原有的服务X,最后总共拆分出6个服务(A/B/C/D/F/X)

基础设施

建议按照下面优先级来搭建基础设施
1.服务发现、服务路由、服务容错:这是最基本的微服务基础设施。
2.接口框架、API网关:主要是为了提升开发效率,接口框架是提升内部服务的开发效率,API网关是为了提升与外部服务对接的效率。
3.自动化部署、自动化测试、配置中心:主要是为了提升测试和运维效率。
4.服务监控、服务跟踪、服务安全:主要是为了进一步提升运维效率。

以上3和4两类基础设施,其重要性会随着微服务节点数量增加而越来越重要,但在微服务节点数量较少的时候,可以通过人工的方式支撑,虽然效率不高,但也基本能够顶住。

微服务架构-基础设施

自动化测试
自动化部署
配置中心
接口框架
API网关
服务发现

服务发现主要有两种实现方式:自理式和代理式
自理式结构就是指每个微服务自己完成服务发现
代理式的方式看起来更加清晰,微服务本身的实现也简单了很多,但实际上这个方案风险较大

服务路由
服务容错
服务监控
服务跟踪
服务安全

微内核架构
微内核架构包含两类组件:核心系统和插件模块
微内核的核心系统设计的关键技术有:插件管理、插件连接和插件通信

OSGi联盟的初始目标是构建一个在广域网和局域网或设备上展开业务的基础平台,所以OSGi的最早设计也是针对嵌入式应用的,诸如机顶盒、服务网关、手机、汽车等都是其应用的主要环境。然而,无心插柳柳成荫,由于OSGi具备动态化、热插拔、高可复用性、高效性、扩展方便等优点,它被应用到了PC上的应用开发。尤其是Eclipse这个流行软件采用OSGi标准后,OSGi更是成为了首选的插件化标准。现在我们谈论OSGi,已经和嵌入式应用关联不大了,更多是将OSGi当作一个微内核的架构模式

OSGi框架的逻辑架构
模块层(Module层)
插件被称为Bundle,每个Bundle是一个Java的JAR文件,每个Bundle里面都包含一个元数据文件MANIFEST.MF,这个文件包含了Bundle的基本信息
生命周期层
生命周期层精确地定义了Bundle生命周期的操作(安装、更新、启动、停止、卸载)
服务层(Service层)
服务层实现插件通信的功能

规则引擎架构简析
可扩展
易理解
高效率

规则引擎的基本架构
开发人员将业务功能分解提炼为多个规则,将规则保存在规则库中。
业务人员根据业务需要,通过将规则排列组合,配置成业务流程,保存在业务库中。
规则引擎执行业务流程实现业务功能

规则引擎是具体是如何实现的
插件管理
插件连接
插件通信

可视化

//----------架构模板-----------//

判断技术演进方向
面对层出不穷的新技术,我们应该采取什么样的策略?
潮流派
新技术需要学习,需要花费一定的时间去掌握,这个也是较大的成本;如果等到掌握了技术后又发现不适用,则是一种较大的人力浪费

保守派
如果无视技术的发展,形象一点说就是有了拖拉机,你还偏偏要用牛车

跟风派
即使有风可跟,其实也存在问题。有时候适用于竞争对手的技术,并不一定适用于自己,盲目模仿可能带来相反的效果

只有跳出技术的范畴,从一个更广更高的角度来考虑这个问题,这个角度就是企业的业务发展

影响一个企业业务的发展主要有3个因素:
市场、技术、管理,这三者构成支撑业务发展的铁三角,任何一个因素的不足,都可能导致企业的业务停滞不前

技术创新推动业务发展!
业务发展推动技术的发展!

判断业务当前和接下来一段时间的主要复杂度是什么就非常关键。判断不准确就会导致投入大量的人力和时间做了对业务没有作用的事情,判断准确就能够做到技术推动业务更加快速发展

答案就是基于业务发展阶段进行判断

互联网技术演进的模式

不同时期的差别主要体现在两个方面:

复杂性
平台化
服务化

用户规模
性能
可用性

互联网架构模板:“存储层”技术

互联网技术公司的大部分技术点

测试平台
运维平台

业务层
用户层
网络层
服务层
开发层
存储层

数据平台
管理平台

sql
nosql
小文件存储
大文件存储

互联网架构模板:“开发层”和“服务层”技术

开发层技术
1.开发框架
对于框架的选择,有一个总的原则:优选成熟的框架,避免盲目追逐新技术!
2.Web服务器
你可能会担心Apache的性能之类的问题,其实不用过早担心这个,等到业务真的发展到Apache撑不住的时候再考虑切换也不迟,那时候你有的是钱,有的是人,有的是时间
3.容器

服务层技术
1.配置中心
2.服务中心
3.消息队列

互联网架构模板:“网络层”技术
负载均衡
CDN
多机房
多中心

互联网架构模板:“用户层”和“业务层”技术

用户层
1.用户管理
单点登录(SSO),又叫统一登录。单点登录的技术实现手段较多,例如cookie、JSONP、token等,目前最成熟的开源单点登录方案当属CAS

需要允许第三方应用接入,由此引申出用户管理的第二个目标:授权登录。现在最流行的授权登录就是OAuth2.0协议,基本上已经成为了事实上的标准,如果要做开放平台,则最好用这个协议

2.消息推送
消息推送根据不同的途径,分为短信、邮件、站内信、App推送

如果涉及敏感数据,需要自己实现消息推送,这时就有一定的技术挑战了。消息推送主要包含3个功能:设备管理(唯一标识、注册、注销)、连接管理和消息管理,技术上面临的主要挑战有

海量设备和用户管理
连接保活
消息管理

3.存储云、图片云

业务层技术

第一阶段:所有功能都在1个系统里面。
第二阶段:将商品和订单拆分到2个子系统里面。
第三阶段:商品子系统和订单子系统分别拆分成了更小的6个子系统。


互联网架构模板:“平台”技术

运维平台
运维平台核心的职责分为四大块:配置、部署、监控、应急

配置:主要负责资源的管理。例如,机器管理、IP地址管理、虚拟机管理等。
部署:主要负责将系统发布到线上。例如,包管理、灰度发布管理、回滚等。
监控:主要负责收集系统上线运行后的相关数据并进行监控,以便及时发现问题。
应急:主要负责系统出故障后的处理。例如,停止程序、下线故障机器、切换IP等

运维平台的核心设计要素是“四化”:标准化、平台化、自动化、可视化。

标准化
需要制定运维标准,规范配置管理、部署流程、监控指标、应急能力等,各系统按照运维标准来实现,避免不同的系统不同的处理方式。标准化是运维平台的基础,没有标准化就没有运维平台

平台化
可以将运维标准固化到平台中,无须运维人员死记硬背运维标准。
运维平台提供简单方便的操作,相比之下人工操作低效且容易出错。
运维平台是可复用的,一套运维平台可以支撑几百上千个业务系统。

自动化
传统手工运维方式效率低下的一个主要原因就是要执行大量重复的操作,运维平台可以将这些重复操作固化下来,由系统自动完成

可视化
能够直观地看到数据的相关属性,例如,汽车仪表盘中的数据最小值是0,最大是100,单位是MPH。
能够将数据的含义展示出来,例如汽车仪表盘中不同速度的颜色指示。
能够将关联数据整合一起展示,例如汽车仪表盘的速度和里程

测试平台
单元测试、集成测试、接口测试、性能测试

1.用例管理
2.资源管理
3.任务管理
4.数据管理

1.数据平台
数据平台的核心职责主要包括三部分:数据管理、数据分析和数据应用

数据管理
数据管理包含数据采集、数据存储、数据访问和数据安全四个核心职责,是数据平台的基础功能

数据采集:从业务系统搜集各类数据。例如,日志、用户行为、业务数据等,将这些数据传送到数据平台。数据存储:将从业务系统采集的数据存储到数据平台,用于后续数据分析。数据访问:负责对外提供各种协议用于读写数据。例如,SQL、Hive、Key-Value等读写协议。数据安全:通常情况下数据平台都是多个业务共享的,部分业务敏感数据需要加以保护,防止被其他业务读取甚至修改,因此需要设计数据安全策略来保护数据

2.数据分析
数据分析包括数据统计、数据挖掘、机器学习、深度学习等几个细分领域

数据统计:根据原始数据统计出相关的总览数据。例如,PV、UV、交易额等。数据挖掘:数据挖掘这个概念本身含义可以很广,为了与机器学习和深度学习区分开,这里的数据挖掘主要是指传统的数据挖掘方式。例如,有经验的数据分析人员基于数据仓库构建一系列规则来对数据进行分析从而发现一些隐含的规律、现象、问题等,经典的数据挖掘案例就是沃尔玛的啤酒与尿布的关联关系的发现。机器学习、深度学习:机器学习和深度学习属于数据挖掘的一种具体实现方式,由于其实现方式与传统的数据挖掘方式差异较大,因此数据平台在实现机器学习和深度学习时,需要针对机器学习和深度学习独立进行设计


3.数据应用
数据应用很广泛,既包括在线业务,也包括离线业务。例如,推荐、广告等属于在线应用,报表、欺诈检测、异常检测等属于离线应用

管理平台
管理平台的核心职责就是权限管理
1.身份认证
2.权限控制

架构重构内功心法第一式:有的放矢

业务已经上线,不能停下来
关联方众多,牵一发动全身
旧架构的约束

期望通过架构重构来解决所有问题当然是不现实的,所以架构师的首要任务是从一大堆纷繁复杂的问题中识别出真正要通过架构重构来解决的问题,集中力量快速解决,而不是想着通过架构重构来解决所有的问题

架构重构内功心法第二式:合纵连横
所以在沟通协调时,将技术语言转换为通俗语言,以事实说话,以数据说话,是沟通的关键!

有效的策略是“换位思考、合作双赢、关注长期”

除了计划上灵活一点,方案上也可以灵活一点:我们可以先不做这个系统相关的重构,先把其他需要重构的做完。因为大部分需要重构的系统,需要做的事情很多,分阶段处理,在风险规避、计划安排等方面更加灵活可控

架构重构内功心法第三式:运筹帷幄
救火
组件化
解耦

将要解决的问题根据优先级、重要性、实施难度等划分为不同的阶段,每个阶段聚焦于一个整体的目标,集中精力和资源解决一类问题

每个阶段都有明确目标,做完之后效果明显,团队信心足,后续推进更加容易。
每个阶段的工作量不会太大,可以和业务并行。
每个阶段的改动不会太大,降低了总体风险

1.优先级排序
2.问题分类
3.先易后难
4.循序渐进

再谈开源项目:如何选择、使用以及二次开发?

不要重复发明轮子,但要找到合适的轮子

如何选择一个开源项目
聚焦是否满足业务
聚焦是否成熟
版本号:除非特殊情况,否则不要选0.X版本的,至少选1.X版本的,版本号越高越好。
使用的公司数量:一般开源项目都会把采用了自己项目的公司列在主页上,公司越大越好,数量越多越好。
社区活跃度:看看社区是否活跃,发帖数、回复数、问题处理速度等。

聚焦运维能力

如何使用开源项目
深入研究,仔细测试
通读开源项目的设计文档或者白皮书,了解其设计原理。
核对每个配置项的作用和影响,识别出关键配置项。
进行多种场景的性能测试。
进行压力测试,连续跑几天,观察CPU、内存、磁盘I/O等指标波动。
进行故障测试:kill、断电、拔网线、重启100次以上、切换等

小心应用,灰度发布
做好应急,以防万一
如果要用MongoDB或者Redis,可以用MySQL做备份存储

如何基于开源项目做二次开发
保持纯洁,加以包装
发明你要的轮子

谈谈App架构的演进
架构是系统的顶层结构。
架构设计的主要目的是为了解决软件系统复杂度带来的问题。
架构设计需要遵循三个主要原则:合适原则、简单原则、演化原则。
架构设计首先要掌握业界已经成熟的各种架构模式,然后再进行优化、调整、创新。

WebApp
原生App
HybridApp
组件化&容器化
跨平台App

架构实战:架构设计文档模板

备选方案模板
1.需求介绍
性能问题
耦合问题
效率问题
2.需求分析
5W指Who、When、What、Why、Where
Who:需求利益干系人,包括开发者、使用者、购买者、决策者等。
When:需求使用时间,包括季节、时间、里程碑等。
What:需求的产出是什么,包括系统、数据、文件、开发库、平台等。
Where:需求的应用场景,包括国家、地点、环境等,例如测试平台只会在测试环境使用。
Why:需求需要解决的问题,通常和需求背景相关
1Hhow
业务子系统发送消息给消息队列
业务子系统从消息队列获取消息
8C
即Constraints,包括性能Performance、成本Cost、时间Time、可靠性Reliability、安全性Security、合规性Compliance、技术性Technology、兼容性Compatibility

3.复杂度分析
高可用
高性能
可扩展

4.备选方案

5.备选方案评估

架构设计模板
1.总体方案
2.架构总览
3.核心流程
4.详细设计
高可用设计
高性能设计
可扩展设计
安全设计
其他设计
部署方案
5.架构演进规划
整个消息队列系统分三期实现:
第一期:实现消息发送、权限控制功能,预计时间3个月。
第二期:实现消息读取功能,预计时间1个月。
第三期:实现主备基于ZooKeeper切换的功能,预计时间2周。

坚持输出!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星月IWJ

曾梦想杖键走天涯,如今加班又挨

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值