1. 架构设计的目的
架构设计的主要目的是为了解决软件复杂度带来的问题(复杂度又包括业务复杂度技术复杂度等)
2. 复杂度的来源
-
1. 高性能
高性能带来的复杂度主要包括两方面 ,一方面是单台计算机内部为了提高性能带来的复杂度;另一方面是多台计算机集群为了提高性能带来的复杂度。
- 单机复杂度
计算机的复杂度关键是操作系统,操作系统的复杂度直接决定了软件系统的复杂度,而操作系统和性能最相关的就是进程和线程。
多进程让人物能并行处理,但内部只能穿行处理,所以又有了线程,有了线程又有了互斥锁机制,操作系统的最小调度单位变成了线程,而进程变成了操作系统分配资源的最小单位。
如果我们要设计一个高性能软件,需要考虑多进程、多线程、进程通信、多线程并发等技术,而这些技术不是最新最好。比如,Nginx可以用多线程也可以用多进程,JBoss采用多线程;Redis采用多进程,Memcache采用多线程,这些系统都实现了高性能,但内部实现差异很大。
总而言之就是你要实现高性能必然要带来复杂度,而我们要做的就是权衡好复杂度和性能
- 单机复杂度
-
- 集群复杂度
互联网时代,单机支撑不了业务,采用集群方式是必然的。
-
① 任务分配
任务分配器可以是F5、交换机、LVS,Nginx、HAProxy,还可以自己开发。并且任务分配器需要增加算法,所以我们明显感觉到提升了复杂度
如果我们要继续提高性能,任务分配器就不够用了,架构又会变成这个样子
任务分配器1台变成多台,这个变化带来的复杂度就是需要将不同的用户分配到不同的分配器上常见的方法包括:DNS轮训、智能DNS、CDN(内容分发网络)、GSLB(全局负载均衡)设备等 -
② 任务分解
任务分配可以突破单台机器的性能瓶颈,但如果越来越复杂,单纯通过任务分配的方式收益越来越低。例如10台提升8倍,20台10倍,为了提升性能我们使用任务分解
以微信后台架构为例
通过任务分解,把原来大一统但复杂的业务系统进行拆分,但是不同任务拆分应该控制一个合理的范围不能无限拆分,因为拆分过多系统间调用次数会成指数递增,而系统间通信都是通过网络,下图进行说明:
系统调用过多会导致网络耗时过长,虽然计算时间缩短。
- 集群复杂度
-
2. 高可用
系统无中断的地执行功能的能力,代表系统的可用程度,是进行系统设计时的准则
系统的高可用方案五花八门,但万变不离其宗,本质都是通过冗余来实现高可用。通俗讲就是增加机器,高性能增加机器目的在于“扩展”性能;高可用增加机器目的在于“冗余”处理单元 。
- 计算高可用
这里的计算是指业务逻辑的处理,无论在哪台机器预算输出结果是一样的。所以将计算从一台机器迁移到另一台机器对业务没影响。先看一个单机变双机
你可能发现和之前的“高性能”讲到的双机架构图是一样的,因此复杂度也类似,具体表现为:增加人物分配器,分配器和业务机器之间连接和交互,人物分配器增加分配算法。再看一个高可用集群架构:
分配算法更加复杂可以是 1主3备、2主2备、3主1备、4主0备。例如ZooKeeper 是1主多备,Memcached是全主0备
- 计算高可用
- 存储高可用
需要存储数据的系统,关键点和难点在于“存储高可用”,存储和计算相比本质区别是:将数据从一台机器搬到另一台机器,需要经过线路进行传输,传输就有延时。按照数据+逻辑=业务来说,数据不一致逻辑一致业务最终表现也将不一样。
比如在北京存了1万块,到上海查一下钱还没到账。
除了物理上传输速度限制,传输线路本身也存在高可用,传输线路可能中断、拥塞、丢包,支付宝2015年就被挖断过光缆影响超过4小时才恢复
综合分析:数据不一致会导致业务问题,但如果不做冗余系统整体高可用无法得到保证,所以存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务的造成的影响。
- 存储高可用
-
- 高可用状态决策
无论计算高可用还是存储高可用,其基础都是“状态决策”,就是系统需要判断当前的状态是否异常,如果异常了采取行动保证高可用。(我理解就是要有人监控系统是否不可用,不可用就赶紧切换到好用的机器上)
-
独裁式
这种方式问题在于又要实现决策者的高可用,要加机器,导致进入死循环。
-
- 协商式
是指两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策
问题在于两个协商的服务器之间网络可能出现波动,无法协商出正确的结果,比如主机没事但是网络不好备机以为主机挂了自己升级为主机此时就有两个主机了。
连接不稳定我们可以多加连接但是又会带来问题,对多连接出来数据的取舍问题,反正又会增加复杂度。
综合分析协商式在某些场景总是存在一些问题。
- 协商式
- 高可用状态决策
-
民主式
是多个独立的个体通过投票进行状态决策。例如ZooKeeper集群选举leader就是采用这种方式,ZooKeeper的选举算法Paxos。
-
-
3. 可扩展
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新需求时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
-
4. 成本、安全、规模
顾名思义自行理解
3.架构设计的原则
- 合适原则
合适优于业界领先
- 合适原则
- 简单原则
简单优于复杂
- 简单原则
- 演化原则
演化优于一步到位
- 演化原则
4. 架构设计流程
-
1.识别复杂度
复杂度主要来源于“高性能”、“高可用”、“可扩展”等几个方面,但并不意味着我们的架构必须要同时满足这三个条件。
比如一个“亿级用户平台”一开始就设计了40多个子系统,上线之后发现过渡设计了,带来诸多问题:
- 系统复杂无比
- 系统太多升级开发麻烦效率低
- 难以定位问题
此时正确的做法是将主要的复杂度列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的主要复杂度问题,“亿级用户平台”团队首先需要将系统的数量降下来 -
2.设计备选方案
-
3.评估和选择备选方案
-
4.详细方案设计
接下来逐一介绍“高性能架构模式”,“高可用架构模式”,“可扩展架构模式”
5.高性能数据库集群
-
1. 读写分离
读写分离的基本原理是将数据库读写操作分散到不同的节点上
基本架构图如图:
读写分离的基本实现:
- 数据库搭建主从集群,一主一从、一主多从。
- 主机负责读写,从机只负责读
- 主机通过复制将数据同步到从机,每台数据库都存储了所有的业务数据。
-
业务服务器将写操作给主机,读操作给发给从机
这里说的是主从而不是主备,从是要干活的,而备不用只是用来存储数据。
读写分离实现逻辑简并不复杂,但是引入了设计复杂度:主从复制延迟和分配机制
复制延迟
主从复制延迟可能达到一秒,解决方式有:
- 写操作后的读操作发送给数据库主服务器。例如注册完之后登录,要实现的话需要入侵代码,在代码里对所有的登录指定查询主服务器。
- 读从机失败再读主机,也就是所谓的“二次读取”,但是这样子会增大主机的读压力
- 关键业务读写指向主机,非关键业务采用读写分离。
分配机制
-
程序代码封装
指在代码中抽象一个数据访问层,实现读写分离和数据库连接的管理。例如基于。Hibernate进行简单封装,就可以实现读写分离,基本架构是:
特点:一是实现简单,根据业务实现更多的定制化,二每个编程语言都要实现一次无法通用,三故障情况下主从切换配置需要改需要重启服务。
目前的开源方案比如有淘宝的TDDL,它是一个通用的数据访问层,所有功能封装在jar包中供业务代码调用 - 数据库中间件
中间件封装指的是独立一套系统出来,实现读写分离操作和数据库服务器访问连接管理。中间件对服务器提供sql兼容的协议,对于服务器来说访问中间件和访问数据库没有区别,在业务服务器来说中间件就是一个数据库服务器。基本架构如下:
特点是:- 支持多种编程语言。
- 实现复杂,细节多容易出现bug。
- 中间件不执行读写但是读写都要经过中间件,对中间件性能要求高。
- 数据库主从切换对业务服务器无感知,中间件可以探测数据库服务器的主从状态。例如向一个测试表写入数据成是主,失败是从。
由于中间件比程序代码封装复杂度高出一个量级,一般情况下不建议采用。目前开源的数据库中间件方案中有,MYSQL官方的MySQL Proxy 360 的Atlas.
-
2. 分库分表
当数据量上升到亿级的时候,单台数据库的存储能力就会成为瓶颈,主要体现在
- 数据量大,读写性能下降,即使有索引,索引性能一样会下降。
- 数据文件很大,备份恢复需要时间边长。
- 数据文件变大,极端情况下丢失数据风险高
为了满足数据存储的需求,就需要将存储分到到多台机器上。
业务分库
按照业务模块将数据分散到不同的数据库服务器。
但是同时带来了问题
- join问题
业务分库后,表分散在不同数据库,导致无法使用sql的join查询
- join问题
- 事务问题
原本在一个数据库中不同表可以在同一个事务中修改,业务分库后,分散在不同的数据中,无法通过事务同意修改。虽然数据厂商提供了分布式事务的解决方案(MySQL的XA),但性能太低,与高性能的目标是违背的。
- 事务问题
- 成本问题
本来1台搞定现在要3台加上备份要6台。
- 成本问题
分表
单表数据量变大需要分表,拆分方式有两种:水平拆垂直拆。示意图如下:
6.高性能NoSQL
常见的NoSQL的方案分为四类:
- K-V存储:解决关系数据库无法存储数据结构的问题,以Redis为代表
- 文档数据库: 解决关系数据库schema约束的问题,以MongoDB为代表
- 列式数据库: 解决关系数据库大数据场景下的IO问题,以HBase为代表
- 全文搜索引擎: 解决关系数据库的全文搜索性能问题,以Elasticsearch为代表
7.高性能缓存架构
在一些场景下,单纯依靠存储系统的性能提升不够,典型场景如下:
- 需要经过复杂运算后得出结果,存储系统无能为力
-
读多写少的数据,存储系统有心无力
缓存基本原理就是将重复使用的数据放到内存中,一次生成多次使用,避免每次都访问存储系统。
以Memcache为例单台服务器简单的key-value查询能够达到TPS 50000以上加入如下:
缓存虽然能够大大减轻存储系统压力,但是同时也给架构带来了更多的复杂性,架构如果没有针对复杂性进行处理,某些场景下会导致整个系统崩溃。
缓存穿透
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据但是缓存中没有数据,业务系统需要再次到存储系统查询数据。通常有两种情况:
- 1.存储数据不存在
存储系统确实不存在数据,所以缓存也没有数据,导致每次查询都还是查询存储系统。通常情况下业务上出现数据不存在的问题并不常见,但是出现了容易被黑客攻击,解决办法如果存储系统数据不存在,就放一个默认值到缓存中。 - 缓存数据生成耗费大量时间或资源。
典型场景就是电商商品分页,不能把所有商品都缓存起来,只能按照分页缓存。通常用户也不会看到最后那些页,但是如果被爬虫爬取就没有办法了。
- 缓存数据生成耗费大量时间或资源。
缓存雪崩
指的是大量缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
缓存雪崩常见解决方式有两种:更新锁机制和后台更新机制。
- 更新锁
对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放然后读取缓存,要么直接返回空值。对于分布式业务系统,由于存在多台服务器,单台服务器有一个线程更新缓存,多台就有多个线程更新缓存,因此要实现锁机制,需要用到分布式锁,如ZooKeeper。- 缓存大面积失效
- 线程1 读取缓存,判断缓存为空,获得更新锁,从数据库读取,写入缓存
- 同时线程2 读取缓存,判断缓存为空,无法获得更新锁,等待锁释放重新读取缓存,也可以返回默认值
- 线程1 未释放锁,将缓存放入数据之前所有的线程都无法查询数据库
- 更新锁
- 后台更新
由后台线程更新,而不是由业务线程来更新,缓存本身有效期设置为永久,后台线程定时更新缓存
- 后台更新
缓存热点
缓存中的某些Key(可能对应用与某个促销商品)对应的value存储在集群中一台机器,使得所有流量涌向同一机器,成为系统的瓶颈,该问题的挑战在于它无法通过增加机器容量来解决。
缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。
将热点key复制多个副本,然后存储到缓存集群的不同机器上。当通过热点key去查询数据时,通过某种hash算法随机选择一个副本机器访问缓存,将热点分散到了不同机器上。
以微博为例,对于粉丝超过100万的明星,每条微博生成100万份缓存,缓存的数据是一样的,通过在缓存的key里加上编号进行区分,每次读取缓存的时候都随机读取其中的某份缓存。
缓存副本设计有一个细节,就是不同的缓存副本不要设置统一的过期时间,否则就会出现缓存副本同时失效的情况。
8.单服务器高性能模式
高性能架构主要集中在两个方面:
1. 尽量提升单服务器的性能,将单服务器的性能发挥到极致。
2. 如果单服务器无法支撑,设置服务器集群方案。
单服务器高性能的关键之一就是服务器采取的并发模型,并发模型设计有如下两个关键设计点
- 服务器如何管理连接
-
服务器如何处理请求
以上两个设计点最终都和操作系统的I/O模型及进程模型相关。 - I/O模型:阻塞、非阻塞、同步、异步
- 进程模型: 单进程、多进程、多线程
但服务器高性能模式:PPC与TPC
PPC(Process Per Connction)
PPC其含义是指每次有新的连接就新建一个进程专门处理这个请求,这是传统的UNIX网络服务器所采用的模型。
基本流程图是:
- 父进程“fork”子进程(途中fork)。
- 子进程处理连接的读写请求(途中read,业务处理,writee)。
- 子进程关闭连接(图中子进程的close)
图中父进程“fork"子进程后,直接调用了close,看起来是关闭了父进程,其实只是将连接的文件的描述引用减一,真正关闭连接是等子进程也调用close后,连接对应的文件描述引用计数变为0后,操作系统才会真正关闭,更多细节参考《UNIX网络编程: 卷一》(mark一下记得学习)。
PPC模式适合服务器连接没那么多的情况,例如数据库服务器。因为fork代价高,父子进程通讯复杂,支持并发连接数有限。
prefork
PPC模式,当连接进来的时候才fork新进程,由于fork进程代价太高,用户访问就会比较慢
prefork提前创建进程,系统在启动的时候预先创建进程,然后才开始接受用户的请求,当新进程进来的时候,就可以省去fork进程的操作,基本示意图:
prefork的实现关键就是多个子进程都accept同一个socket,当有新的连接进入时,操作系统保证只有一个进程能够accept成功。但存在”惊群”现象,虽然只有一个进程accept成功,但所有阻塞在accept上的进程都会被唤醒,这样就导致了不必要的进程调度和上下文切换。幸运的是Linux2.6已经解决了accept惊群问题。
prfork和ppc模式一样存在父子进程通讯复杂问题。
TPC(Thread Per Connection)
新连接就建线程处理。更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的,线程通信相比进程通信更简单。因此,TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题。
TPC基本流程
父进程接受连接(图中 accept)。
父进程创建子线程(图中 pthread)。
子线程处理连接的读写请求(图中子线程 read、业务处理、write)。
子线程关闭连接(图中子线程中的 close)。
和PPC相比,主进程不用“close”连接,原因是在于子线程是共享主进程的进程空间的,连接的文件描述符并没有被复制,因此只需要一次 close 即可。
TPC 虽然解决了 fork 代价高和进程通信复杂的问题,但是也引入了新的问题,具体表现在:
创建线程虽然比创建进程代价低,但并不是没有代价,高并发时(例如每秒上万连接)还是有性能问题。
无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。
多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界)。
除了引入了新的问题,TPC 还是存在 CPU 线程调度和切换代价的问题。因此,TPC 方案本质上和 PPC 方案基本类似,在并发几百连接的场景下,反而更多地是采用 PPC 的方案,因为 PPC 方案不会有死锁的风险,也不会多进程互相影响,稳定性更高。
prethread
TPC模式中,当连接进来的时候才创建线程来处理请求,虽然创建线程比创建进程更加轻量级,但是又一定代价,而prethread就是为了解决这个问题。
和prefork模式相似,prethread模式会预先创建线程,然后才开始接受用户请求。由于多线程之间数据共享和通信比较方便,因此实际上 prethread 的实现方式相比 prefork 要灵活一些,常见的实现方式有下面几种:
主进程 accept,然后将连接交给某个线程处理。
-
子线程都尝试去 accept,最终只有一个线程 accept 成功,方案的基本示意图如下:
Apache 服务器的 MPM worker 模式本质上就是一种 prethread 方案,但稍微做了改进。Apache 服务器会首先创建多个进程,每个进程里面再创建多个线程,这样做主要是为了考虑稳定性,即:即使某个子进程里面的某个线程异常导致整个子进程退出,还会有其他子进程继续提供服务,不会导致整个服务器全部挂掉。
prethread 理论上可以比 prefork 支持更多的并发连接,Apache 服务器 MPM worker 模式默认支持 16 × 25 = 400 个并发处理线程。
Reactor
(有时间再学习)
Proactor
(有时间再学习)
9.高性能负载均衡
负载均衡分为三类:DNS负载均衡、硬件负载均衡和软件负载均衡
DNS负载均衡
DNS是最简单的负载均衡,一般用来实现地理级别的负载均衡。比如北方的用户访问北京的机房,南方的用户访问深圳的机房,下面是DNS负载均衡示意图:
DNS负载均衡存在的缺点:
- 更新不及时
- 扩展性差,控制权在域名商那里
- 分配策略简单
硬件负载均衡
目前典型硬件负载均衡设备F5和A10。这类设备性能强大,但是比较贵。可以抗住百万并发。
软件负载均衡
软件负载均衡有Nginx和LVS,Nginx是7层(应用层)负载均衡,LVS是4层(传输层)负载均衡。
一般Nginx大概能达到5万/秒的并发;LVS性能是十万级。下面是Nginx架构示意图:
如果实现Nginx 高可用就需要搭建Nginx集群,通过Keepalive监听Nginx服务器的状态,然后虚拟出一个对外的ip,谁挂了然后通过ip漂移将虚拟ip重新指向另一台Nginx
负载均衡典型架构
算法
- 轮询
- 加权轮询
- 负载最低优先
- 性能最优优先
- Hash
- 源地址Hash
- ID Hash
CAP(Consistency、Availability、Partition Tolerance)
CAP理论:对于一个分布式计算系统,不可能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)
-
1.一致性
对某个指定的客户端来说,读操作保证能返回最新写操作的结果 -
2.可用性
非故障节点在合理的时间返回合理的响应(不是错误和超时的响应) -
3.分区容错性
当系统出现分区的时候,系统能继续履行职责
10.CAP应用
虽然CAP理论定义三个要素只能取两个,但放到分布式环境中我们必须选择P要素,因为网络本身无法做到100%可靠,有可能出故障,所以分区是一个必然现象。如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证C系统要禁止写入,当有写入时系统返回error(例如当前系统禁止写入),这又和A冲突了,因为A要返回no error 和no timeout 。因此分布式系统架构理论上不能选择CA,只能选择CP或者AP。
CP
如下图所示,为了保证一致性,当发生分区现象后,N1的数据已经更新到y,但是由于N1和N2的通信中断,数据无法实现复制。N2上的数据还是x,这时候要想要实现一致性。那么N2查询返回的数据不能是x,不然和N1的返回就不一致了,所以N2只能返回Error,但是这样子就违背了可用性。
AP
如下图所示,当发生分区的时候,N1的数据已经更新到y,但是N2的数据还是x。当C访问N2的时候,将数据x返回给C了。而实际当前数据已经更新为y了,这就不满足一致性了,因此CAP只能满足AP.
注意:这里N2返回的x不是一个正确的值,但是是一个合理的值,只是不是最新的数据而已。
CAP关键细节点
并不是整个系统要么选择CP要么选择AP。在实际的设计过程中,每个系统不可能只处理一种数据,而包含多种类型的数据,有的数据必须选择CP,有的数据必须选择AP,而如果们做设计时从整个系统的角度去选择CP或者AP,就会顾此失彼。
举个例子:
用户管理系统包括用户账号数据、用户信息数据。通常账号数据会选择CP,用户信息数据会选择AP,而如果限定整个系统为CP,则不符合用户信息数据的应用场景;
在CAP理论落地实践时,我们需要将系统内部数据按照不同的应用场景进行分类,每类数据选择不同的策略,而不是限制整个系统数据都是用同一策略。
-
CAP是忽略网络延迟的
当事务提交时,数据并不能够瞬间复制到所有节点。例如北京机房同步到广州机房可能十几毫秒,这就意味着CAP中的C是不能完美实现的,在复制的过程中,节点A和节点B数据并不一致。
不要小看这十几毫秒,对于严苛的业务场景,例如和金钱相关的用户余额,或者和抢购相关商品库存,技术上是无法做到分布式场景下的一致性的。而业务上要求必须要实现一致性,因此单个用户的余额、单个商品的库存理论上要求选择CP但是CP都做不到,只能选择CA。也就是说只能单点写入,其他节点做备份,无法做到分布式情况下多点写入。
需要注意的是,这并不意味着系统无法应用分布式架构,只是说“单个用户余额、单个商品库存”无法做分布式,单系统整体还是可以用分布式架构的。例如下面将用户分区的架构:
对于单个用户来说,读写操作只能在某个节点上运行,对于所有用户来说一部分用户在Node1上一部分用户在Node2上。当一个节点出现故障的时候也只是部分用户受到影响,这也就是为什么支付宝被挖断光缆,部分用户出现故障的原因。 -
正常运行情况下,不存在CP和AP的选择,可以同时满足CA
CAP理论告诉我们分布式系统只能选择CP或者CP,但这里的前提是发生了分区现象,如果没有发生分区,也就是说P不存在的时候(节点网络连接一切正常)。我们没有必要放弃C或者A,应该C和A都可以保证,这就要求架构设计的时候既要考虑分区发生的时候CP还是AP,也要考虑分区没有发生如何保证CA。 -
放弃并不等于什么都不做,需要为分区恢复做准备
分区期间放弃C和A,并不意味着永远放弃,我们可以在分区期间进行一些操作。从而分区故障解除后,系统能重新达到CA状态。
最典型的就是在分区期间记录一些日志,当出现分区故障后,系统根据日志进行数据恢复,使得重新达到CA状态。
BASE
BASE 是指基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventual Consistency),核心思想是即使无法做到强一致性(CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。
- 基本可用
分布式系统出现故障时,允许损失部分可用性,即保证核心可用 - 软装态
允许系统存在中间状态,而中间状态不会影响系统的整体可用性。这里的中间状态就是CAP理论中的数据不一致 - 最终一致性
系统中的所有数据副本经过一定时间后,最终能够达到一致状态。
BASE理论是对CAP的延伸和补充 - CAP理论是忽略延时的,而实际应用中延时是无法避免的。
这点意味着CP场景是不存在的,即使是几毫秒的复制延迟,在这几毫秒的时间内系统是不符合CP要求的。因此CAP中的CP方案,也是实现了最终一致性,只是一定时间是几毫秒而已 - AP方案牺牲一致性是指分区期间,而不是永远放弃一致性。
这其实是BASE理论延伸的地方,分区期间牺牲一致性,单分区故障结束后系统应该恢复最终一致性。
综上分析ACID是数据库事务完整性理论,CAP是分布式系统设计理论,BASE是CAP理论中AP方案的延伸
11.排除架构可用性隐患的利器FMEA方法
FMEA介绍
FMEA(Failure mode and effects analysis,故障模式与影响分析)
12.高可用存储架构
常用的存储架构有主备、主从、主主、集群、分区
双机架构
主备复制
主备复制是最常见也是最简单的存储高可用方案,几乎所有存储系统都提供了主备方案,例如MySQL、Redis、MongoDB等。下面是标准主备方案结构图:
备机主要是备份作用,并不承担实际的业务读写,如果需要改备机为主机需要人工操作
主从复制
下面是标准的主从架构图:
双机切换
主从和主备存在两个共性问题:
- 主机故障后,无法进行写操作
- 如果主机无法恢复,需要人工指定主机角色
双机切换就是为了解决这两个问题而产生的,包括主备切换和主从切换。
要实现完善的切换方案,必须考虑这几个关键点:
- 主备状态判断
- 切换决策
- 切换时机:什么情况下升级为主机
- 切换策略:原来主机变为备机还是,故障好了仍然作为主机
- 自动程度:是自动还是人工
- 数据冲突解决
常见架构
-
互连式
顾名思义,主备机直接建立状态传递的渠道
可以看到在主备复制的架构基础上,主机和备机多了一个状态传递的通道,这个通道就是来传递状态信息的。
-
中介式
中介式指的是在主备两者之外引入第三方中介,主备之间不直接连接,而都去连接中介,并且通过中介来传递状态信息,其架构图如下:
但是有个一个缺点就是中介如何实现自己的高可用。MongoDB的Replia Set采取的就是这种方式,其架构如下:
MondoDB(M)表示主节点,MongoDB(S)表示备节点,MongoDB(A)表示仲裁节点。主备节点存储数据,仲裁节点不存储。幸运的是开源方案已经有比较成熟的解决方案,例如Zookeeper和Keepalived。 -
模拟式
模拟式是指主备之间不传递任何状态数据,而是备机模拟成一个客户端,向主机发起模拟的读写操作,根据读写操作的响应情况来判断主机的状态,其架构图如下:
主主复制
主主复制指两台机器都是主机,互相将数据复制给对方,客户端可以任意挑选一台机器进行读写操作,架构图如下:
如果采用主主复制必须保证数据能够双向复制,而很多数据是不能双向复制的
集群和分区
数据集群
主从、主备、主主都是以主机能够存储所有数据为前提的。但是单台机器明显不够用了,所以需要使用集群。集群可以划分为两类:数据集中集群、数据分散集群。
-
数据集中集群
数据集中集群与主从、主备这类架构类似。下面是读写全到主机的一种架构:
- 数据分散集群
数据分散集群指多个服务器组成一个集群,每个服务器都会存储一部分数据;同时为了提升硬件利用率,每台服务器又会备份一部分数据。算法需要考虑这些设计点:- 均衡性
算法需要保证服务器上数据分布是均匀的。 - 容错性
当部分服务器故障时,算法需要将原来的分配给故障服务器的数据分区分配给其他服务器。 - 可伸缩性
当容器容量不够时,扩充新的服务器后,算法能够自动将部分数据分区迁移到新的服务器,并保证扩容后所有服务器的均衡性。
- 均衡性
数据分散集群中每台机器都可以处理读写请求。但在数据分散集群中,必须有一个角色负责执行数据分配算法,这个角色可以是独立的服务器也可以是集群选举出的服务器。
一般来说数据集中集群适合数据量不大,集群机器数量不多的场景。例如,Zookeeper集群,一般推荐5台机器左右,数据量是单台机器就能支撑的。而数据分散机群,由于其良好的伸缩性,适合业务数据巨大、机器数量庞大的业务场景。例如,Hadoop集群、HBase集群,大规模的集群可以达到上百甚至上千台服务器
数据分区
前面讨论的都是基于硬件故障场景去考虑和设计的,但对于影响非常大的灾难或者事故来说,有可能所有硬件全部故障。例如,奥尔良水灾、洛杉矶大地震等,这种情况下基于硬件设计的高可用架构不再适用,这就是数据分区产生的背景。
数据分区指将数据按照一定规则进行分区,不同分区分布在不同的地理位置上,每个分区存储一部分数据,通过这种方式来规避地理级别的故障造成的巨大影响。当故障恢复后,其他地区备份的数据也可以帮助故障地区快速恢复业务。
设计一个好的分区架构需要考虑多方面。
-
数据量
数据量大小直接决定了分区规则的复杂度 -
分区规则
地理位置有近有远,因此可以得到不同的分区规则,包括洲际分区、国家分区、城市分区。 -
复制规则
每个分区虽然只是整体数据的一部分,但还是很大,这部分数据如果丢失或损坏,同样难以接受。因此使用分区架构,同样考虑复制方案。常见的复制规则有:集中式、互备式和独立式-
集中式
集中式备份指存在一个总的备份中心所有分区都将数据备份到备份中心,架构如下:
但是成本高需要建一个备份中心
-
互备式
互备式指每个分区备份另外一个分区的数据,其基本架构如下:
-
独立式
独立式每个分区有自己的备份中心,其基本架构如下:
数据分区并不涉及多个分区之间业务数据的调用,而数据分散集群中各个服务器之间是要相互调用的。分散集群在地里位置上是相同的,分区分布在不同的地理位置。
-
13 .高可用计算架构
计算高可用架构的设计思想很简单:通过增加更多服务器达到计算高可用
计算高可用架构设计复杂度主要体现在任务管理方面,即当任务在某台服务器上执行失败后,如何将任务重新分配到新的服务器进行执行。计算高可用设计的关键点有下面两点:
- 1.哪些服务器可以执行任务
第一种方式和计算高性能中的集群类似,每个服务器都可以执行任务。
第二种方式和存储高可用中的集群类似,只有特定服务器(通常叫“主机”)可以执行任务。 - 2.任务如何重新执行
第一种策略对于已经分配的任务即使执行失败也不做任何处理
第二种策略设计一个任务管理器来管理需要执行的计算任务,服务器执行完后,需要将执行结果反馈给任务管理器,管理器根据执行结果决定是否将执行任务重新分配到另外的服务器上执行。“任务分配器”是一个逻辑概念,并不一定要求系统存在一个独立的任务分配模块
接下来我们阐述常见的高可用架构:主备、主从和集群
主备
主备架构是计算高可用最简单的架构,和存储高可用架构类似,但更简单些
根据备机的状态不同又分为冷备架构和温备架构
- 冷备:备机上程序包配置文件备好,但是业务系统没有启动。
- 温备: 备机上业务系统已经启动但是不对外提供服务。
主从
和存储高可用的主从架构类似,计算高可用的从机也要执行任务的。任务分配器需要将任务进行分类,确定哪些任务发给主机哪些任务发给备机。架构如下:
集群
主备主从通过冗余一台服务器来提升性能,且需要人工来切换。高可用集群根据集群中服务器节点角色不同分为两类:一类是对称集群,一类是非对称集群
计算高可用包含两台服务器的集群和存储高可用不一样。
-
对称集群
对称集群通俗的叫法交负载均衡集群,因此我们使用“负载均衡集群”这个通俗的说法
-
非对称集群
非对称集群中不同服务器的角色是不同的,不同服务器承担不同职责。非对称集群基本架构如下:
14.异地多活架构
异地、多活,多活就是指不同地理位置上的系统都能够提供业务服务,这里的“活”是活动、活跃的意思。
判断一个系统是否符合异地多活,需要满足两个标准:
- 正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务。
- 某个地方业务异常的时候,用户访问其他地方正常的业务系统,能够得到正确的业务服务。
与“活”对应的是字是“备”,备是备份,正常情况下对外是不提供服务的,如果需要提供服务,则需要大量的人工干预和操作,花费大量的时间才能让“备”变成“活”。
- 某个地方业务异常的时候,用户访问其他地方正常的业务系统,能够得到正确的业务服务。
保证在灾难的情况下业务都不受影响。代价很高,具体表现为:
- 系统复杂度会发生质的变化,需要设计复杂的异地多活架构。
- 成本会上升,一个或者多个机房搭建独立的一套业务系统。
新闻网站、企业内部的 IT 系统、游戏、博客站点等,如果无法承受异地多活带来的复杂度和成本,可以不做,只做异地备份即可。而共享单车、滴滴出行、支付宝、微信这类业务,就需要做异地多活了。
架构模式
异地多活架构可以分为同城异区、跨城异地、跨国异地。
- 同城异区
一个机房在海淀区,一个在通州区,然后将两个机房用专用的高速网络连接在一起。
同城的两个机房,距离上一般大约就是几十千米,通过搭建高速的网络,同城异区的两个机房能够实现和同一个机房内几乎一样的网络传输速度。这就意味着虽然是两个不同地理位置上的机房,但逻辑上我们可以将它们看作同一个机房,这样的设计大大降低了复杂度,减少了异地多活的设计和实现复杂度及成本。其次,除了这类灾难,机房火灾、机房停电、机房空调故障这类问题发生的概率更高,而且破坏力一样很大。而这些故障场景,同城异区架构都可以很好地解决。因此,结合复杂度、成本、故障发生概率来综合考虑,同城异区是应对机房级别故障的最优架构。 - 跨城异地
部署在北京和广州两个机房。
有效应对这类极端灾难事件。跨城异地的架构复杂度大大上升。距离增加带来的最主要问题是两个机房的网络传输速度会降低,这不是以人的意志为转移的,而是物理定律决定的,即光速真空传播大约是每秒 30 万千米,在光纤中传输的速度大约是每秒 20 万千米,再加上传输中的各种网络设备的处理,实际还远远达不到理论上的速度。
数据不一致业务肯定不会正常,但跨城异地肯定会导致数据不一致。如果是强一致性要求的数据,例如银行存款余额、支付宝余额等,这类数据实际上是无法做到跨城异地多活的。系统分别部署在广州和北京,那么如果挖掘机挖断光缆后,会出现如下场景:
用户 A 余额有 10000 元钱,北京和广州机房都是这个数据。
用户 A 向用户 B 转了 5000 元钱,这个操作是在广州机房完成的,完成后用户 A 在广州机房的余额是 5000 元。
由于广州和北京机房网络被挖掘机挖断,广州机房无法将余额变动通知北京机房,此时北京机房用户 A 的余额还是 10000 元。
用户 A 到北京机房又发起转账,此时他看到自己的余额还有 10000 元,于是向用户 C 转账 10000 元,转账完成后用户 A 的余额变为 0。
用户 A 到广州机房一看,余额怎么还有 5000 元?于是赶紧又发起转账,转账 5000 元给用户 D;此时广州机房用户 A 的余额也变为 0 了。
最终,本来余额 10000 元的用户 A,却转了 20000 元出去给其他用户。
金融只能采用同城异区这种架构。
用户登录(数据不一致时用户重新登录即可)、新闻类网站(一天内的新闻数据变化较少)、微博类网站(丢失用户发布的微博或者评论影响不大),能够很好地应对极端灾难的场景。 - 跨国异地
跨国异地指的是业务部署在不同国家的多个机房。相比跨城异地,跨国异地的距离就更远了,因此数据同步的延时会更长,正常情况下可能就有几秒钟了。这种程度的延迟已经无法满足异地多活标准的第一条:“正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务”。例如,假设有一个微博类网站,分别在中国的上海和美国的纽约都建了机房,用户 A 在上海机房发表了一篇微博,此时如果他的一个关注者 B 用户访问到美国的机房,很可能无法看到用户 A 刚刚发表的微博。虽然跨城异地也会有此类同步延时问题,但正常情况下几十毫秒的延时对用户来说基本无感知的;而延时达到几秒钟就感觉比较明显了。
因此,跨国异地的“多活”,和跨城异地的“多活”,实际的含义并不完全一致。跨国异地多活的主要应用场景一般有这几种情况:
为不同地区用户提供服务
例如,亚马逊中国是为中国用户服务的,而亚马逊美国是为美国用户服务的,亚马逊中国的用户如果访问美国亚马逊,是无法用亚马逊中国的账号登录美国亚马逊的。
只读类业务做多活
例如,谷歌的搜索业务,由于用户搜索资料时,这些资料都已经存在于谷歌的搜索引擎上面,无论是访问英国谷歌,还是访问美国谷歌,搜索结果基本相同,并且对用户来说,也不需要搜索到最新的实时资料,跨国异地的几秒钟网络延迟,对搜索结果是没有什么影响的。