分布式系统
CAP
分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。
一致性
一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。
- 最终一致性:这个是弱一致性的一种,不保证在任意时刻同一份数据在所有节点上都是一致的,但是在一段时间之后时间会最终一致。对于我们互联网的应用来说大多数是采用这种策略。
- 强一致性:在任意时刻同一份数据在所有节点上都是一致的。对于银行、金融行业来说基本采用这种策略。
可用性
可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。
在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
分区容忍性
网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。
权衡
在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。
可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。
BASE
BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
基本可用
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
软状态
指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。
最终一致性
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。
ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。
在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
分布式锁的场景
在单机场景下,可以使用语言的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
阻塞锁通常使用互斥量来实现:
互斥量为 0 表示有其它进程在使用锁,此时处于锁定状态;
互斥量为 1 表示未锁定状态。
1 和 0 可以用一个整型值表示,也可以用某个数据是否存在表示。
数据库的唯一索引
获得锁时向表中插入一条记录,释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否处于锁定状态。
存在以下几个问题:
- 锁没有失效时间,解锁失败的话其它进程无法再获得该锁;
- 只能是非阻塞锁,插入失败直接就报错了,无法重试;
- 不可重入,已经获得锁的进程也必须重新获取锁。
Redis 的 SETNX 指令
使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。
SETNX 指令和数据库的唯一索引类似,保证了只存在一个 Key 的键值对,那么可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。
Zookeeper 的有序节点
- Zookeeper 抽象模型
Zookeeper 提供了一种树形结构的命名空间,/app1/p_1 节点的父节点为 /app1。
- 节点类型
永久节点:不会因为会话结束或者超时而消失;
临时节点:如果会话结束或者超时就会消失;
有序节点:会在节点名的后面加一个数字后缀,并且是有序的. - 监听器
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。 - 分布式锁实现
创建一个锁目录 /lock,当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点;
客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
执行业务代码,完成后,删除对应的子节点。
会话超时
如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,这种实现方式不会出现数据库的唯一索引实现方式释放锁失败的问题。羊群效应
一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应,一只羊动起来,其它羊也会一哄而上),而我们只希望它的后一个子节点收到通知。详情参考ZooKeeper—羊群效应
【转载】Zookeeper和Redis实现分布式锁的可靠性分析
分布式事务
指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。
分布式锁和分布式事务区别:
- 锁问题的关键在于进程操作的互斥关系,例如多个进程同时修改账户的余额,如果没有互斥关系则会导致该账户的余额不正确。
- 而事务问题的关键则在于事务涉及的一系列操作需要满足 ACID 特性,例如要满足原子性操作则需要这些操作要么都执行,要么都不执行。
分布式一致性协议
二阶段提交协议(2PC)
两阶段提交,通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
- 准备阶段
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。询问可以看成一种投票,需要参与者都同意才能执行。
- 提交阶段
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
Paxos
用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。
主要有三类节点:
提议者(Proposer):提议一个值;
接受者(Acceptor):对每个提议进行投票;
告知者(Learner):被告知投票的结果,不参与投票过程。
ZAB
Zookeeper 原子广播协议;ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复和原子广播协议。
当 Leader 服务可以正常使用,就进入消息广播模式,当 Leader 不可用时,则进入崩溃恢复模式。基于该协议,Zookeeper 实现了一种 主备模式 的系统架构来保持集群中各个副本之间数据一致性。其中所有客户端写入数据都是写入到 主进程(称为 Leader)中,然后,由 Leader 复制到备份进程(称为 Follower)中。
选举权重:
Serverid:服务器ID。比如有三台服务器,编号分别是1,2,3。编号越大在选择算法中的权重越大。
Zxid:数据ID。服务器中存放的最大数据ID,值越大说明数据越新,在选举算法中数据越新权重越大。节点角色:
Leader:Leader 负责处理客户端请求和状态变更,
Follower:Follower 则从 Leader 处同步状态节点状态:
LOOKING,竞选状态。
FOLLOWING,随从状态,同步leader状态,参与投票。
OBSERVING,观察状态,同步leader状态,不参与投票。
LEADING,领导者状态。选举过程:
- 当一个节点启动时,它首先会进入 LOOKING 状态,表示它正在寻找 Leader。
- 在 LOOKING 状态下,节点会向其他节点发送消息,请求它们投票支持自己成为 Leader。
- 如果收到了超过半数的选票,那么该节点就会成为 Leader,并将状态切换为 LEADING。
- 如果一个节点在一定时间内没有收到足够多的投票或者发生了网络分区等情况,它会重新进入 LOOKING 状态,重新发起选举。
- 在选举过程中,如果某个节点成为 Leader,那么其他节点会将自己的状态切换为 FOLLOWING,并开始与 Leader 同步数据。
Paxos算法用于构建一个分布式的一致性状态机系统,ZAB算法用于构建一个高可用的分布式数据主备系统
Raft
单个 Candidate 的竞选
有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。
一个分布式系统的最初阶段,此时只有 Follower 没有 Leader。Node A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
此时 Node A 发送投票请求给其它所有节点。
其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。
多个 Candidate 竞选
如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票。
由于每个节点设置的随机竞选超时时间不同,因此下一次再次出现多个 Candidate 并获得同样票数的概率很低。
数据同步
来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。
Leader 会把修改复制到所有 Follower。
Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
一致性哈希
解决了什么问题
当我们想提高系统的容量,就会将数据水平切分到不同的节点来存储,也就是将数据分布到了不同的节点。比如一个分布式 KV(key-valu) 缓存系统,某个 key 应该到哪个或者哪些节点上获得,应该是确定的,不是说任意访问一个节点都可以得到缓存结果的。
第一想到的Hash算法,但是该算法有一个很致命的问题,如果节点数量发生了变化,也就是在对系统做扩容或者缩容时,必须迁移改变了映射关系的数据,否则会出现查询不到数据的问题。
扩容前:
扩容后:
一致性哈希算法就很好地解决了分布式系统在扩容或者缩容时,发生过多的数据迁移的问题。
Hash原理
一致哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而一致哈希算法是对 2^32 进行取模运算,是一个固定的值。
我们可以把一致哈希算法是对 2^32 进行取模运算的结果值组织成一个圆环,就像钟表一样,钟表的圆可以理解成由 60 个点组成的圆,而此处我们把这个圆想象成由 2^32 个点组成的圆,这个圆环被称为哈希环,如下图:
一致性哈希要进行两步哈希:
第一步:对存储节点进行哈希计算,也就是对存储节点做哈希映射,比如根据节点的 IP 地址进行哈希;
第二步:当对数据进行存储或访问时,对数据进行哈希映射;
所以,一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上。
如何找到节点
对「数据」进行哈希映射得到一个结果要怎么找到存储该数据的节点呢?映射的结果值往顺时针的方向的找到第一个节点,就是存储该数据的节点:
例如:对要查询的 key-01 进行哈希计算,确定此 key-01 映射在哈希环的位置,然后从这个位置往顺时针的方向找到第一个节点,就是存储该 key-01 数据的节点。往顺时针的方向找到第一个节点就是节点 A。
数据迁移
如果增加一个节点或者减少一个节点会发生大量的数据迁移吗?
假设节点数量从 3 增加到了 4,新的节点 D 经过哈希计算后映射到了下图中的位置:key-01、key-03 都不受影响,只有 key-02 需要被迁移节点 D。
假设节点数量从 3 减少到了 2,比如将节点 A 移除:key-02 和 key-03 不会受到影响,只有 key-01 需要被迁移节点 B。
问题
但是一致性哈希算法并不保证节点能够在哈希环上分布均匀,这样就会带来一个问题,会有大量的请求集中在一个节点上。
比如,下图中 3 个节点的映射位置都在哈希环的右半边:
那么当A节点移除,所有的数据都需要迁移到B节点,显然是有问题的
如何解决
使用虚拟节点提高均衡度,不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。
如果有访问请求寻址到「A-01」这个虚拟节点,接着再通过「A-01」虚拟节点找到真实节点 A,这样请求就能访问到真实节点 A 了。
- 虚拟节点数量越多,起到的均衡效果越好:比如 Nginx 的一致性哈希算法,每个权重为 1 的真实节点就含有160 个虚拟节点。
- 另外,虚拟节点除了会提高节点的均衡度,还会提高系统的稳定性。当节点变化时,会有不同的节点共同分担系统的变化,因此稳定性更高。比如,当某个节点被移除时,对应该节点的多个虚拟节点均会移除,而这些虚拟节点按顺时针方向的下一个虚拟节点,可能会对应不同的真实节点,即这些不同的真实节点共同分担了节点变化导致的压力。
- 而且,有了虚拟节点后,还可以为硬件配置更好的节点增加权重,比如对权重更高的节点增加更多的虚拟机节点即可。
- 因此,带虚拟节点的一致性哈希方法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景。
高可用系统设计
高可用系统设计要点
冗余
单点是系统高可用最大的风险和敌人,应该尽量在系统设计的过程中避免单点。
保证高可用的主要手段是使用冗余,或者叫“集群化”,当某个服务器故障时就请求其它服务器。
- 应用服务器的冗余比较容易实现,只要保证应用服务器不具有状态,那么某个应用服务器故障时,负载均衡器将该应用服务器原先的用户请求转发到另一个应用服务器上,不会对用户有任何影响。
- 存储服务器的冗余需要使用主从复制来实现,当主服务器故障时,需要提升从服务器为主服务器,这个过程称为切换。
有了冗余之后还不够,每次出现故障需要人工介入恢复势必会增加系统的不可服务时间。所以,往往是通过“自动故障转移”来实现系统的高可用。
扩展
扩展是最常见的提升系统可靠性的方法,系统的扩展可以避免单点故障。一个容易扩展的系统,能够通过扩展来成倍的提升系统能力,轻松应对系统访问量的提升。
垂直扩展
比如,当机器内存不够时,我们可以帮机器增加内存,垂直扩展能够提升系统处理能力,但不能解决单点故障问题。我
水平扩展
通过增加一个或多个逻辑单元,并使得它们像整体一样的工作。水平扩展,通过冗余部署解决了单点故障,同时又提升了系统处理能力。
在实际应用中,水平扩展最常见:
- 通常我们在部署应用服务器的时候,都会部署多台,然后使用 nginx 来做负载均衡,nginx 使用心跳机制来检测服务器的正常与否,无响应的服务就从集群中剔除。这样的集群中每台服务器的角色是相同的,同时提供一样的服务。
- 在数据库的部署中,为了防止单点故障,一般会使用一主多从,通常写操作只发生在主库。不同数据库之间角色不同。当主机宕机时,一台从库可以自动切换为主机提供服务。
解耦
在软件工程中,对象之间的耦合度就是对象之间的依赖性。对象之间的耦合越高,维护成本越高,因此对象的设计应使模块之间的耦合度尽量小。在软件架构设计中,模块之间的解耦或者说松耦合有两种,
1. 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。
新增的产品可以通过调用可复用的服务来实现业务逻辑,对其它产品没有影响。
2. 将同步调用转换成异步消息交互。使用消息队列进行解耦,应用之间通过消息传递进行通信.
如果我们将同步调用替换成异步消息,机票支付系统发送机票支付成功的消息到消息中间件,出票系统、代金券系统从消息中间件订阅消息。这样一来,出票系统、代金券系统的宕机也就不会对机票支付系统造成任何影响了。
异步消息解耦,适合那些信息流单向流动(类似发布-订阅这样的),实时性要求不高的系统。常见的开源消息队列框架有:Kafka、RabbitMQ、RocketMQ。
请求幂等
幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用
用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。
一般保证幂等的判断是从数据库查询有没有相同id的记录,但是在分布式系统环境下,可能有主从问题:request1请求过来的时候,查询从库发现没有对应记录,则request1开始操作插入主库record1,但是还没有同步到从库;此时request2查询从库也发现没有相同id的记录,准备插入有相同id的记录record2,这个时候request1成功插入record1,request2开始插入record2,数据库报错.
解决这个问题有两种方法:1、读写都强制走主库;2、采用分布式锁,考虑性能问题,一般都选2
分布式锁
分布式系统一般都有多台机器,常规的多线程锁已经无法解决问题;最简单用redis实现:思路很简单,主要用到的redis函数是setnx()。首先是将某一任务标识名UniqueKey(能唯一识别一个请求的标识)作为键存到redis里,并为其设个过期时间,如果是同样的请求过来,先是通过setnx()看看是否能将UniqueKey插入到redis里,可以的话就返回true,不可以就返回false。
分布式锁设计原则:
- 互斥性,同一时间只有一个线程持有锁
- 容错性,即使某一个持有锁的线程,异常退出,其他线程仍可获得锁
- 隔离性,线程只能解自己的锁,不能解其他线程的锁
服务隔离
是对系统、业务所占有的资源进行隔离,避免一个业务占用整个系统资源,对其他业务造成影响,即发生故障后不会出现滚雪球效应,从而保证只有出问题的服务不可用,其他服务还是可用的。
- 线程池隔离: 不同的业务使用不同的线程池,避免低优先级的任务阻塞高优先级的任务。或者高优先级的任务过多,导致低优先级任务永远不会执行。
- 进程隔离: Linux 中有用于进程资源隔离的 Linux CGroup,通过物理限制的方式为进程间资源控制提供了简单的实现方式,为 Linux Container 技术、虚拟化技术的发展奠定了技术基础。
- 模块隔离、应用隔离: 很多线上故障的发生源于代码修改后,测试不到位导致。按照代码或业务的易变程度来划分模块或应用,把变化较少的划分到一个模块或应用中,变化较多的划分到另一个模块或应用中。减少代码修改影响的范围,也就减少了测试的工作量,减少了故障出现的概率。
- 机房隔离: 主要是为了避免单个机房网络问题或断电。
- 读写分离: 一方面,将对实时性要求不高的读操作,放到 DB 从库上执行,有利于减轻 DB 主库的压力。另一方面,将一些耗时离线业务 sql 放到 DB 从库上执行,能够减少慢 sql 对 DB 主库的影响,保证线上业务的稳定可靠。
异步调用
这种方式在服务端平均处理请求时间过长的业务场景下很好用,不需要关心最后的结果,用户请求完成之后就立即返回结果,具体处理可以后续再做。
除了可以在程序中实现异步之外,常常还使用消息队列,消息队列可以通过异步处理提高系统性能(削峰、减少响应所需时间)并且可以降低系统耦合性。
可伸缩性(有/无状态的服务)
指不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。
伸缩性与性能
如果系统存在性能问题,那么单个用户的请求总是很慢的;
如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。实现伸缩性
应用服务器只要不具有状态,那么就可以很容易地通过负载均衡器向集群中添加新的服务器。
关系型数据库的伸缩性通过 Sharding 来实现,将数据按一定的规则分布到不同的节点上,从而解决单台存储服务器的存储空间限制。
对于非关系型数据库,它们天生就是为海量数据而诞生,对伸缩性的支持特别好。
一致性(补偿事务、重试)
强一致性(ACID)和高可用性(BASE)是对立,顾此失彼;因此,为了可用性,我们要讲业务中需要强一致性的动作和不需要强一致性的动作剥离开,对于非强一致性需求的动作,可以做补偿事务;我们应尽量设计更多非强一致性的业务
模块级自动化测试
解决上述问题可以使用模块级自动化测试。具体方案是:针对某一模块,收集模块线上的输入、输出、运行时环境等信息,在离线测试环境通过数据mock模块线上场景,回放收集的线上输入,相同的输入比较测试场景与线上收集的输出作为测试结果。
模块级自动化测试通过简化复杂系统中的不变因素(mock),将系统的测试边界收拢到改动模块,将复杂系统的整体测试转化为改动模块的单元测试。主要适用于系统业务回归,对系统内部重构场景尤其适用。
具体如何收集线上数据呢?有两种方法:
- AOP:面向切面编程,动态地织入代码,对原有代码的侵入性较小。
- 埋点:很多公司都开发了一下基础组件,可以在这些基础组件中嵌入数据收集的代码。
灰度发布 & 回滚
- 发布之前必须制定详细的回滚步骤,回滚是解决发布引起的故障的最快的方法。
- 在线上出现故障后,第一个要考虑的就是刚刚有没有代码发布、配置发布,如果有的话就先回滚。
- 即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。
- 灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
超时和重试机制设置
一旦用户请求超过某个时间的得不到响应,就抛出异常。
使用一些 RPC 框架的时候,这些框架都自带的超时重试的配置。如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法在处理请求。重试的次数一般设为3次,再多次的重试没有好处,反而会加重服务器压力
应对大流量的能力(熔断、降级、限流)
熔断(慎用)
如果系统中,某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用;熔断主要是应对流量引起的问题,使服务处于关闭、半关闭状态,以保证部分业务成功,或者舍弃次要业务使主业务运行通畅;
在熔断器中有三种状态:
关闭:让请求通过的默认状态。如果请求成功/失败但低于阈值,则状态保持不变。
打开:当熔断器打开的时候,所有的请求都会被标记为失败;这是故障快速失败机制,而不需要等待超时时间完成。
半开:定期的尝试发起请求来确认系统是否恢复。如果恢复了,熔断器将转为关闭状态或者保持打开
下面这张图,就是熔断器的基本原理,包含三个状态:
- 服务正常运行时的 Closed 状态,当服务调用失败量达到阈值时,熔断器进入 Open 状态
- 在 Open 状态,服务调用不会真正去请求外部资源,会快速失败。
- 当进入 Open 状态一段时间后,进入 Half-Open状态,需要去尝试调用几次服务,检查故障的服务是否恢复。如果成功则熔断器关闭,如果失败,则再次进入 Open 状态。
限流
熔断的一种,半开状态,只允许少部分的请求,其他的都拒绝,如果设计得当,被拒绝的请求,客户端会通过重试、补偿操作来完成;
限流策略:
- 计数器算法:
设置一个计数器统计单位时间内某个请求的访问量,在进入下一个单位时间内把计数器清零,对于单位时间内超过计数器的访问,可以放入等待队列、直接拒接访问等策略 - 漏斗算法:
一个固定容量的漏桶,按照常量固定速率流出水滴;可以以任意速率流入水滴到漏桶;如果流入水滴超出了桶的容量,则流入的水滴溢出了,而漏桶容量是不变的。 - 令牌桶算法:
令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可用的令牌、或者直接拒绝。当令牌桶满时,新添加的令牌被丢弃或拒绝 - 滑动窗口计数法
计数法是限流算法里最容易理解的一种,该方法统计最近一段时间的请求量,如果超过一定的阈值,就开始限流。在 TCP 网络协议中,也用到了滑动窗口来限制数据传输速率。
滑动窗口计数有两个关键的因素:窗口时长、滚动时间间隔。滚动时间间隔一般等于上图中的一个桶 bucket,窗口时长除以滚动时间间隔,就是一个窗口所包含的 bucket 数目。 - 动态限流
一般情况下的限流,都需要我们手动设定限流阈值,不仅繁琐,而且容易因系统的发布升级而过时。为此,我们考虑根据系统负载来动态决定是否限流,动态计算限流阈值。可以参考的系统负载参数有:Load、CPU、接口响应时间等。
漏桶算法与令牌桶算法的区别在于:
漏桶算法能够强行限制数据的传输速率,令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。需要注意的是,在某些情况下,漏桶算法不能够有效地使用网络资源,因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。
降级
业务降级,是指牺牲非核心的业务功能,保证核心功能的稳定运行。
要实现优雅的业务降级,需要将功能实现拆分到相对独立的不同代码单元,分优先级进行隔离。在后台通过开关控制,降级部分非主流程的业务功能,减轻系统依赖和性能损耗,从而提升集群的整体吞吐率。
业务降级通常需要通过开关工作,开关一般做成配置放在专门的配置系统,配置的修改最好能够实时生效。开源的配置系统有阿里的diamond、携程的Apollo、百度的disconf。
总结
技术 | 解决问题 |
---|---|
扩展 | 通过冗余部署,避免单点故障 |
隔离 | 避免业务之间的相互影响,机房隔离避免单点故障 |
解耦 | 减少依赖,减少相互间的影响 |
限流 | 遇到突发流量时,保证系统稳定 |
降级 | 牺牲非核心业务,保证核心业务的高可用 |
熔断 | 减少不稳定的外部依赖对核心服务的影响 |
自动化测试 | 通过完善的测试,减少发布引起的故障 |
灰度发布 | 灰度发布是速度与安全性作为妥协,能够有效减少发布故障 |
分层高可用架构实践
常见的互联网分层架构
常见互联网分布式架构如上,分为:
(1)客户端层:典型调用方是浏览器browser或者手机应用APP
(2)反向代理层:系统入口,反向代理
(3)站点应用层:实现核心应用逻辑,返回html或者json
(4)服务层:如果实现了服务化,就有这一层
(5)数据-缓存层:缓存加速访问存储
(6)数据-数据库层:数据库固化数据存储
整个系统的高可用,又是通过每一层的冗余+自动故障转移来综合实现的。
【客户端层->反向代理层】的高可用
【客户端层】到【反向代理层】的高可用,是通过反向代理层的冗余来实现的。以nginx为例:有两台nginx,一台对线上提供服务,另一台冗余以保证高可用,常见的实践是keepalived存活探测,相同virtual IP提供服务。
自动故障转移:当nginx挂了的时候,keepalived能够探测到,会自动的进行故障转移,将流量自动迁移到shadow-nginx,由于使用的是相同的virtual IP,这个切换过程对调用方是透明的。
【反向代理层->站点层】的高可用
【反向代理层】到【站点层】的高可用,是通过站点层的冗余来实现的。假设反向代理层是nginx,nginx.conf里能够配置多个web后端,并且nginx能够探测到多个后端的存活性。
自动故障转移:当web-server挂了的时候,nginx能够探测到,会自动的进行故障转移,将流量自动迁移到其他的web-server,整个过程由nginx自动完成,对调用方是透明的。
【站点层->服务层】的高可用
【站点层】到【服务层】的高可用,是通过服务层的冗余来实现的。“服务连接池”会建立与下游服务多个连接,每次请求会“随机”选取连接来访问下游服务。
自动故障转移:当service挂了的时候,service-connection-pool能够探测到,会自动的进行故障转移,将流量自动迁移到其他的service,整个过程由连接池自动完成,对调用方是透明的(所以说RPC-client中的服务连接池是很重要的基础组件)。
【服务层>缓存层】的高可用
【服务层】到【缓存层】的高可用,是通过缓存数据的冗余来实现的。
缓存层的数据冗余又有几种方式:第一种是利用客户端的封装,service对cache进行双读或者双写。
缓存层也可以通过支持主从同步的缓存集群来解决缓存层的高可用问题。
以redis为例,redis天然支持主从同步,redis官方也有sentinel哨兵机制,来做redis的存活性检测。
自动故障转移:当redis主挂了的时候,sentinel能够探测到,会通知调用方访问新的redis,整个过程由sentinel和redis集群配合完成,对调用方是透明的。
说完缓存的高可用,这里要多说一句,业务对缓存并不一定有“高可用”要求,更多的对缓存的使用场景,是用来“加速数据访问”:把一部分数据放到缓存里,如果缓存挂了或者缓存没有命中,是可以去后端的数据库中再取数据的。
这类允许“cache miss”的业务场景,缓存架构的建议是:
将kv缓存封装成服务集群,上游设置一个代理(代理可以用集群冗余的方式保证高可用),代理的后端根据缓存访问的key水平切分成若干个实例,每个实例的访问并不做高可用。
缓存实例挂了屏蔽:当有水平切分的实例挂掉时,代理层直接返回cache miss,此时缓存挂掉对调用方也是透明的。key水平切分实例减少,不建议做re-hash,这样容易引发缓存数据的不一致。
【服务层>数据库层】的高可用
大部分互联网技术,数据库层都用了“主从同步,读写分离”架构,所以数据库层的高可用,又分为“读库高可用”与“写库高可用”两类。
【服务层>数据库层“读”】的高可用
【服务层】到【数据库读】的高可用,是通过读库的冗余来实现的。
既然冗余了读库,一般来说就至少有2个从库,“数据库连接池”会建立与读库多个连接,每次请求会路由到这些读库。
自动故障转移:当读库挂了的时候,db-connection-pool能够探测到,会自动的进行故障转移,将流量自动迁移到其他的读库,整个过程由连接池自动完成,对调用方是透明的(所以说DAO中的数据库连接池是很重要的基础组件)。
【服务层>数据库层“写”】的高可用
【服务层】到【数据库写】的高可用,是通过写库的冗余来实现的。
以mysql为例,可以设置两个mysql双主同步,一台对线上提供服务,另一台冗余以保证高可用,常见的实践是keepalived存活探测,相同virtual IP提供服务。
自动故障转移:当写库挂了的时候,keepalived能够探测到,会自动的进行故障转移,将流量自动迁移到shadow-db-master,由于使用的是相同的virtual IP,这个切换过程对调用方是透明的。
总结
高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。
方法论上,高可用是通过冗余+自动故障转移来实现的。
整个互联网分层系统架构的高可用,又是通过每一层的冗余+自动故障转移来综合实现的,具体的:
【客户端层】到【反向代理层】的高可用,是通过反向代理层的冗余实现的,常见实践是keepalived + virtual IP自动故障转移
【反向代理层】到【站点层】的高可用,是通过站点层的冗余实现的,常见实践是nginx与web-server之间的存活性探测与自动故障转移
【站点层】到【服务层】的高可用,是通过服务层的冗余实现的,常见实践是通过service-connection-pool来保证自动故障转移
【服务层】到【缓存层】的高可用,是通过缓存数据的冗余实现的,常见实践是缓存客户端双读双写,或者利用缓存集群的主从数据同步与sentinel保活与自动故障转移;更多的业务场景,对缓存没有高可用要求,可以使用缓存服务化来对调用方屏蔽底层复杂性
【服务层】到【数据库“读”】的高可用,是通过读库的冗余实现的,常见实践是通过db-connection-pool来保证自动故障转移
【服务层】到【数据库“写”】的高可用,是通过写库的冗余实现的,常见实践是keepalived + virtual IP自动故障转移
其它
总结一下高可用的设计原理:
- 要做到数据不丢,就必需要持久化
- 要做到服务高可用,就必需要有备用(复本),无论是应用结点还是数据结点
- 要做到复制,就会有数据一致性的问题。
如何实现高可用
入口层
入口层,通常指Nginx和Apache等层面的东西,负责应用(不管是Web应用还是移动应用)的服务入口。我们通常会将服务定位在一个IP,如果这个IP对应的服务器当机了,那么用户的访问肯定会中断。此时,可以用keepalived来实现入口层的高可用。例如,机器A 的IP是 1.2.3.4,机器 B 的 IP 是 1.2.3.5, 那么再申请一个 IP 1.2.3.6(称为⼼跳IP), 平时绑定在机器A上,如果A当机,IP会自动绑定在机器B上;如果B当机,IP会自动绑定在机器A上。对于这种形式,我们将DNS绑定到心跳IP上,即可实现入口层的高可用。
这里要注意,keepalived在使用上会有一些限制。但这个方案有一点小问题。
第一,它的切换可能会有一到两秒的中断,也就是说,如果不是要求到非常严格的毫秒级就不会有问题。
第二,对入口的机器会有些浪费,因为买了两台机器的入口,可能就只有一台机器用上。对一些长连接的应用可能会导致服务中断,这时候就需要客户端做配合做一些重新创建连接的工作。简单来说,对于比较普通的业务来说,这个方案就能解决一部分问题。
两台机器必须在同一个网段,不是在同一个网段,没有办法实现互相抢IP。
内网服务也可以做心跳,但需要注意的是,以前为了安全我们会把内网服务绑定在内网IP上,避免出现安全问题。但为了使用keepalived,必须监听在所有IP上(如果监听在心跳IP上,那么机器没有持有该IP时,服务无法启动),简单的方案是启用 iptables, 避免内网服务被外网访问。
服务器利用率下降,这时可以考虑做混合部署来改善这一点。
比较常见的一个错误是,如果有两台机器,两个公网IP,DNS上把域名同时定位到两个IP,就觉得已经做了高可用了。这完全不是高可用,因为如果一台机器当机,那么就有一半左右的用户无法访问。
除了keepalive,lvs也能用来解决入口层的高可用问题。不过,与keepalived相比,lvs会更复杂一些,门槛也会高一些。
业务层
业务层通常是由PHP、Java、Python、Go等写的逻辑代码构成的,需要依赖于后台数据库及一些缓存层面的东西。如何实现业务层的高可用呢?最核心的就是,业务层不要有状态,将状态分散到缓存层和数据库。目前大家通常喜欢将以下几种数据放入业务层。
第一个是session,即用户登录相关的数据,但好的做法是将session放在数据库里,或者一个比较稳定的缓存系统中。
第二个是缓存,在访问数据库时,如果一个查询很慢,就希望将这些结果暂时放到进程里,下次再做查询时就不用再访问数据库了。这种做法带来的问题是,当业务层服务器不只一台时,数据很难做到一致,从缓存拿到的数据就可能是错误的。。
一个简单的原则就是业务层不要有状态。在业务层没有状态时,一台业务层服务器当掉了之后,Nginx/Apache会自动将所有的请求打到另外一台业务层的服务器上。由于没有状态,两台服务器没有任何差异,所以用户完全感受不到。如果把session放在业务层里面的话,那么面临的问题是,这个用户以前是登录在一台机器上的,这个进程死掉后,用户就会被登出了。
友情提醒:有一段时间比较流行cookie session,就是将session中的数据加密之后放在客户的cookie里,然后下发到客户端,这样也能做到与服务端完全无状态。但这里面有很多坑,如果能绕过这些坑就可以这样使用。第一个坑是怎么保证加密的密钥不泄露,一旦泄露就意味着攻击者可以伪造任何人的身份。第二个坑是重放攻击,如何避免别人通过保存 cookie 去不停地尝试的验证码,当然也还有其他一些攻击手段。如果没有好办法解决这两方面的问题,那么cookie session尽量慎用。最好是将session放在一个性能比较好的数据库中。如果数据库性能不行,那么将session放在缓存中也比放在cookie里要好一点。
缓存层
非常简单的架构里是没有缓存这个概念的。但在访问量上来之后,MySQL之类的数据库扛不住了,比如在SATA盘里跑MySQL,QPS到达200、300甚至500时,MySQL的性能会大幅下降,这时就可以考虑用缓存层来挡住绝大部分服务请求,提升系统整体的容量。
缓存层做高可用一个简单的方法就是,将缓存层分得细一点儿。比如说,缓存层就一台机器的话,那么这台机器当了以后,所有应用层的压力就会往数据库里压,数据库扛不住的话,整个网站(或应用)就会随之当掉。而如果缓存层分在四台机器上的话,每台只有四分之一,这台机器当掉了以后,也只有总访问量的四分之一会压在数据库上面,数据库能扛住的话,网站就能很稳定地等到缓存层重新起来。在实践中,四分之一显然是不够的,我们会将它分得更细,以保证单台缓存当机后数据库还能撑得住即可。在中小规模下,缓存层和业务层可以混合部署,这样可以节省机器。
数据库层
在数据库层面实现高可用,通常是在软件层面来做。例如,MySQL有主从模式(Master-Slave),还有主主模式(Master-Master)都能满足需求。MongoDB也有ReplicaSet的概念,基本都能满足大家的需求。
总之,要想实现高可用,需要做到这几点:入口层做心跳,业务层服务器无状态,缓存层减小粒度,数据库做一个主从模式。对于这种模式来讲,我们做的高可用不需要太多服务器,这些东西都可以同时部署在两台服务器上。这时,两台服务器就能满足早期的高可用需求了。任何一台服务器当机用户完全无感知。
如何实现可伸缩
入口层
在入口层实现伸缩性,可以通过直接水平扩机器,然后DNS加IP来实现。但需要注意,尽管一个域名解析到几十个IP没有问题,但是很多浏览器客户端只会使用前几个IP,部分域名供应商对此有优化(如每次返回的IP顺序随机),但这个优化效果不稳定。
推荐的做法是使用少量的Nginx机器作为入口,业务服务器隐藏在内网(HTTP类型的业务这种方式居多)。另外,也可以把所有IP下发到客户端,然后在客户端做一些调度(特别是非HTTP型的业务,如游戏、直播)。
业务层
业务层的伸缩性如何实现?与做高可用时的解决方案一样,要实现业务层的伸缩性,保证无状态是很好的手段。此外,加机器继续水平部署即可。
缓存层
比较麻烦的是缓存层的伸缩性,最简单粗暴的方式是什么呢?趁着半夜量比较低的时候,把整个缓存层全部下线,然后上线新的缓存层。新的缓存层启动起来之后,再等这些缓存慢慢预热。当然这里一个要求,你的数据库能抗住低估期的请求量。如果扛不住呢?取决于缓存类型,下面我们先可以将缓存的类型区分一下。
强一致性缓存:无法接受从缓存拿到错误的数据 (比如用户余额,或者会被下游继续缓存这种情形)
弱一致性缓存:能接受在一段时间内从缓存拿到错误的数据 (比如微博的转发数)。
不变型缓存:缓存key对应的value不会变更 (比如从SHA1推出来的密码, 或者其他复杂公式的计算结果)。
那什么缓存类型伸缩性比较好呢?弱一致性和不变型缓存的扩容很方便,用一致性Hash即可;强一致性情况稍微复杂一些,稍后再讲。使用一致性Hash,而不用简单Hash的原因是缓存的失效率。如果缓存从9台扩容到10台,简单Hash 情况下90%的缓存会马上失效,而如果使用一致性Hash情况,只有10%的缓存会失效。
那么,强一致性缓存会有什么问题?第一个问题是,缓存客户端的配置更新时间会有微小的差异,在这个时间窗内有可能会拿到过期的数据。第二个问题是,如果扩容之后再裁撤节点,会拿到脏数据。比如 a 这个key之前在机器1,扩容后在机器2,数据更新了,但裁撤节点后key回到机器1,这时候就会拿到脏数据。
要解决问题2比较简单,要么保持永不减少节点,要么节点调整间隔大于数据的有效时间。问题1可以用如下的步骤来解决:
两套hash配置都更新到客户端,但仍然使用旧配置;
逐个客户端改为只有两套hash结果一致的情况下会使用缓存,其余情况从数据库读,但写入缓存;
逐个客户端通知使用新配置。
Memcache 设计得比较早,导致在伸缩性高可用方面的考虑得不太周到。Redis 在这方面有不少改进,特别是 @ngaut 团队基于 redis 开发了 codis 这个软件,一次性地解决了缓存层的绝大部分问题。推荐大家考察一下。
数据库
在数据库层面实现伸缩,方法很多,文档也很多,此处不做过多赘述。大致方法为:水平拆分、垂直拆分和定期滚动。
总之,我们可以在入口层、业务层面、缓存层和数据库层四个层面,使用刚才介绍的方法和技术实现系统高可用和可伸缩性。具体为:在入口层用心跳来做到高可用,用平行部署来伸缩;在业务层做到服务无状态;在缓存层,可以减小一些粒度,以方便实现高可用,使用一致性Hash将有助于实现缓存层的伸缩性;数据库层的主从模式能解决高可用问题,拆分和滚动能解决可伸缩问题。
容灾设计
一:逻辑层容灾
逻辑层服务一般都设计从无状态服务,客户端当前请求和下次请求在逻辑层没有任何的关联,因此客户端可以在在多次请求中分别到不同的机器上,而返回的结果和一直在同一台机器上一样。由于这种特性的存在使得逻辑层可以通过多级备份来实现容灾。
备份可以有 :主备(1+1), 一主多备(1+n),多主一备(n+1), 无备(1+0),互相备份(n)
切换的策略可以为:
- 冷切:即主完全承担所有业务,当主不可服务时再启用备,该方式由于备机一直处于不服务状态,会出现当要切换到备的时候,备也不可服务,可信度低。
- 热切:主和备一起分担所有请求,主备只承担部分业务请求(总和为100%)。这样可以解决之前的冷切遇到的信任度低的问题。同时业务由主备共同分担,节约成本。但是在主完全不可服务时,备有过载的风险。这里可以没有主备之分,在我们的实践中常用多台机器组成的集群来实现互为备份。
- 双在线:主备各跑100%的任务。这个可以同时解决上面的遇到的问题。但是成本高(带宽、电力、机房维护成本等)
二:数据层容灾
相对于逻辑层是无状态的服务,数据层服务需要存储和用户相关的数据,用户的多次请求之间的数据是有关联的。因此如何在多机容灾备份的情况下保证数据一致性成为一个关键问题。
数据一致性的问题:
中心化备份策略:
在传统的数据库、数据服务器设计中往往采用中心化的的模型。每台机器中存储全量的数据,其中有只有一台可写的主机,有一台或多台机器可读的备机去同步主机的数据。当主机不可服务时,其中一台备机升级为主机。
数据增量同步:只对有更新的数据进行同步, 当主机有数据更新时, 会先在本地生成Binlog及同步相关的信息,主机与备机之间通过一定的协议将数据同步到备机。
数据全量同步: 每次都全量同步主机的数据。一般会一段时间内做一次,是增量同步的一个补充。可以一块一数据计算MD5值进行比较,只同步MD5值不相等的数 据快。
问题:只有一台可写容易出现单点问题(虽然可以切换,但是还是会在一定时间内不可服务)、由于网络问题可能出现多主。
去中心化备份策略:
相对于中心化只有一个主可写,其他备机都只是同步主机的数据,去中心化没有主备之分, 所有的机器都可读可写,这样写的性能会有很大的提高,但是要做到数据的一致性比较复杂。目前主要的设计方案有: RNW协议、2pc、3pc协议、paxos协议等。
三:容灾判定
中心主动探测:该方法是中心主动和其他子系统或服务定时发送消息(可用私有协议或ping消息等),根据回包来判定服务是否可用。
等待服务上报:该方法是子系统或者服务,又或者是专门agent主动的将服务的运行状态上报给中心系统,中心系统通过上报消息来判定服务是否运行正常。
请求者判定:即服务的使用方将根据请求的服务状态(延迟、是否成功等信息)来判定服务是否正常,请求者在本地记录这些信息,只将请求发送到那些正常的服务。这种方式相对于中央判定可以防止中心和其他子服务之间由于网络问题而导致误判。
四:异地部署
某些重要的服务为了在局部的网络不可用、自然灾害等问题时也能保证服务的可用性,需要将服务部署在不同的城市、机房。这样一方面可以保证服务的可靠性,也可以让不同地区的用户就近访,提高服务质量。
五、负载均衡
接入负载:通过代理服务将服务分发到不同的机器上,这个可以通过单独部署负载均衡服务器、LVS、DNS、http重定向、nginx方向代理、NAT等实现。
号段负载:通过请求自身的特殊性将不同的请求分发到固定的服务上进行处理。如通过一致性hash将hash值相关的请求路由到固定的服务,也可以将固定号段的qq号路由到固定的服务。如何分配不够离散化,可能会导致某些热点请求都集中在同一台服务,从而导致负载不均衡。
用户自助负载:这种类似于游戏服务器中用户选择不同的服来分配不同的机器,达到将不同的请求分配到不同的机器的目的。
六:过载保护
当负载达到系统处理能力的上限时,系统的处理能力将随着负载的增加急剧下降,俗称滚雪球。在系统设计的时候做好过载保护是一项很重要的工作。过载保护的方法有:
轻重分离、隔离部署:将系统、业务、功能隔离部署(可以分不同的set),确保系统过载状态不会扩散到其他业务系统,对其他业务系统造成影响,使得影响最小化。
频率控制:控制单位时间内的请求量(可以是总的请求量也可以是单个用户的请求量),这样可以保证系统在过载的时候不会被压垮,保证部分用户的请求可以被处理。
设置请求的有效时间: 用户的请求都期望在一定时间范围内返回结果。如果没有返回则会当做超时出错处理。如果服务端一直在处理这样的请求,那么其实是在无用功,对用户没有如何意义。因此我们需要对每个请求做超时限制,在一定的时间内没有处理完则丢弃该请求,理想的状态是从收到请求开始计时,这样可以防止数据包在队列中已经超时。
有损服务:服务做好有损处理,当系统过载时,可以将某些非关键性的服务下掉,从而保证关键业务可以正常服务。如果是底层非关键性的服务过载,则可以不去请求该服务的数据而直接反回给前端。同时前端也可以做相应的有损策略,防止后台服务过载时给用户不好的体验。
七:常见的互联网事故及解决策略
事故 | 解决策略 |
---|---|
服务器硬件故障死机: | 集群部署、多机备份、自动检测并切换 |
网络丢包、光钎断: | 异地部署、自动检测故障并切换 |
服务器雪崩: | 负载均衡、过载保护、容量告警 |
外部依赖故障: | 柔性逻辑、降级服务、限制重试 |
DNS故障 : | 自搭建类DNS服务、使用IP列表替代DNS |
程序CORE: | 自动拉起、实时告警 |
操作失望: | 灰度、保护逻辑、人员备份确认 |
负载均衡
负载均衡,意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。
应用集群:将同一应用部署到多台机器上,组成处理集群,接收负载均衡设备分发的请求,进行处理,并返回相应数据。
负载均衡设备:将用户访问的请求,根据负载均衡算法,分发到集群中的一台处理服务器。(一种把网络请求分散到一个服务器集群中的可用服务器上去的设备)
负载均衡的作用(解决的问题):
- 解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力);
- 提供故障转移,实现高可用;
- 通过添加或减少服务器数量,提供网站伸缩性(扩展性);
- 安全防护;(负载均衡设备上做一些过滤,黑白名单等处理)
负载均衡实现
http重定向协议实现负载均衡
原理:根据用户的http请求计算出一个真实的web服务器地址,并将该web服务器地址写入http重定向响应中返回给浏览器,由浏览器重新进行访问。
优点:比较简单
缺点:
浏览器需要零次请求服务器才能完成一次访问,性能较差。
http重定向服务器自身的处理能力可能成为瓶颈。
使用http302响应重定向,有可能使搜索引擎判断为SEO作弊,降低搜索排名。
【协议层】DNS负载均衡
原理:在DNS服务器上配置多个域名对应IP的记录。例如一个域名www.baidu.com对应一组web服务器IP地址,域名解析时经过DNS服务器的算法将一个域名请求分配到合适的真实服务器上。
优点:
使用简单:负载均衡工作,交给DNS服务器处理,省掉了负载均衡服务器维护的麻烦
提高性能:可以支持基于地址的域名解析,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能;
缺点:
可用性差:DNS解析是多级解析,新增/修改DNS后,解析时间较长;解析过程中,用户访问网站将失败;http重定向服务器自身的处理能力可能成为瓶颈。
扩展性低:DNS负载均衡的控制权在域名商那里,无法对其做更多的改善和扩展;
将DNS作为第一级负载均衡,A记录对应着内部负载均衡的IP地址,通过内部负载均衡将请求分发到真实的Web服务器上。一般用于互联网公司,复杂的业务系统不合适使用。
【网络层】IP负载均衡
在网络层通过修改请求目标地址进行负载均衡。
用户请求数据包,到达负载均衡服务器后,负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法得到一台真实服务器地址,然后将请求目的地址修改为,获得的真实ip地址,不需要经过用户进程处理。
真实服务器处理完成后,响应数据包回到负载均衡服务器,负载均衡服务器,再将数据包源地址修改为自身的ip地址,发送给用户浏览器。如下图:
IP负载均衡,真实物理服务器返回给负载均衡服务器,存在两种方式:
- 负载均衡服务器在修改目的的IP地址的同时修改源地址,将数据源地址设为自身IP,即源地址转换(SNAT),这样Web服务器的响应会再回到负载均衡服务器。
- 将负载均衡服务器同时作为物理服务器集群的网关服务器,这样所有响应数据都会到达负载均衡服务器。
优点:在内核进程完成数据分发,比在应用层分发性能更好;
缺点:所有请求响应都需要经过负载均衡服务器,集群最大吞吐量受限于负载均衡服务器网卡带宽;
【协议层】反向代理负载均衡
原理:在部署位置上,反向代理服务器处于Web服务器前面(这样才可能缓存Web响应,加速访问),反向代理服务器同时提供负载均衡的功能,管理一组Web服务器,将请求根据负载均衡算法转发到不同的Web服务器上。Web服务器处理完成的响应也需要通过反向代理服务器返回给用户。由于Web服务器不直接对外提供访问,因此Web服务器不需要使用外部 IP地址,而反向代理服务器则需要配置双网卡和内部外部两套IP地址。
例如:浏览器访问请求的地址是反向代理服务器的地址114.100.80.10,反向代理服务器收到请求后,根据负载均衡算法计算得到一台真实物理服务器的地址10.0.0.3,并将请求转发给服务器。10.0.0.3处理完请求后将响应返回给反向代理服务器,反向代理服务器再将该响应返回给用户。
优点:部署简单,处于http协议层面。
缺点:使用了反向代理服务器后,web 服务器地址不能直接暴露在外,因此web服务器不需要使用外部IP地址,而反向代理服务作为沟通桥梁就需要配置双网卡、外部内部两套IP地址。
【链路层】链路层负载均衡
在通信协议的数据链路层修改mac地址,进行负载均衡。
数据分发时,不修改ip地址,指修改目标mac地址,配置真实物理服务器集群所有机器虚拟ip和负载均衡服务器ip地址一致,达到不修改数据包的源地址和目标地址,进行数据分发的目的。
实际处理服务器ip和数据请求目的ip一致,不需要经过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。也称为直接路由模式(DR模式),DR模式是目前使用最广泛的一种负载均衡方式。
优点:性能好;
缺点:配置复杂;
基于NAT的负载均衡技术
如Linux VirtualServer,简称LVS
该技术通过一个地址转换网关将每个外部连接均匀转换为不同的内部服务器地址,因此外部网络中的计算机就各自与自己转换得到的地址上的服务器进行通信,从而达到负载均衡的目的。其中网络地址转换网关位于外部地址和内部地址之间,不仅可以实现当外部客户机访问转换网关的某一外部地址时可以转发到某一映射的内部的地址上,还可使内部地址的计算机能访问外部网络。
混合型负载均衡
由于多个服务器群内硬件设备、各自的规模、提供的服务等的差异,可以考虑给每个服务器群采用最合适的负载均衡方式,然后又在这多个服务器群间再一次负载均衡或群集起来以一个整体向外界提供服务(即把这多个服务器群当做一个新的服务器群),从而达到最佳的性能。将这种方式称之为混合型负载均衡。
此种方式有时也用于单台均衡设备的性能不能满足大量连接请求的情况下。是目前大型互联网公司,普遍使用的方式。
- 适合有动静分离的场景
反向代理服务器(集群)可以起到缓存和动态请求分发的作用,当时静态资源缓存在代理服务器时,则直接返回到浏览器。如果动态页面则请求后面的应用负载均衡(应用集群)。
- 适合动态请求场景:
负载均衡算法
轮询
将所有请求,依次分发到每台服务器上,适合服务器硬件同相同的场景。
优点:服务器请求数目相同;
缺点:服务器压力不一样,不适合服务器配置不同的情况;
随机
请求随机分配到各个服务器。
优点:使用简单;
缺点:不适合机器配置不同的场景;
最少链接
将请求分配到连接数最少的服务器(目前处理请求最少的服务器)。
优点:根据服务器当前的请求处理情况,动态分配;
缺点:算法实现相对复杂,需要监控服务器请求连接数;
Hash(源地址散列)
根据IP地址进行Hash计算,得到IP地址。
优点:将来自同一IP地址的请求,同一会话期内,转发到相同的服务器;实现会话粘滞。
缺点:目标服务器宕机后,会话会丢失;
加权
在轮询,随机,最少链接,Hash等算法的基础上,通过加权的方式,进行负载服务器分配。
优点:根据权重,调节转发服务器的请求数目;
缺点:使用相对复杂;