分布式理论

1. 分布式理论

1.1 回顾

  • 分布式与集群的区别
    • 集群:多个节点做同样的事
    • 分布式:多个节点做不同的事
  • 分布式系统特点
    • 分布式
    • 对等性:各节点之间没有关系
    • 并发性
    • 缺乏全局时钟
    • 故障总会发生

1.2 分布式系统的发展

原始:

image-20201126000417073

当前:

image-20201126000538617

1.2 分布式系统面临的问题

  • 通信异常:网络不可靠,存在延时甚至丢失的风险
  • 网络分区:由于延时等原因造成网络之间不连通,但各个子网络内部是正常的,那么分布式系统就会出现局部小集群;如果小集群各自完成原本需要整个分布式系统才能完成的功能,会导致数据一致性问题。
  • 节点故障
  • 三态:成功、失败、超时,其中超时有两种原因
    • 由于网络原因,该请求并没有被成功的发送到接收方,而是在发送过程就发生了丢失现象。
    1. 该请求成功的被接收方接收后,并进行了处理,但在响应反馈给发送方过程中,发生了消息丢失现象

1.3 分布式一致性

定义:分布式一致性是指分布式数据一致性,各副本数据是一致的。

理解:在分布式环境下要将数据复制到多台机器上以提升系统的可用性和可靠性,多份副本之间的读写会有一致性问题。

一致性分类及对应的解决方案

  • 强一致性:跟单机一样的要求,读写一致,但实现起来影响性能
  • 弱一致性
    • 读写一致性:自己写读取自己写入的结果保持一致性
    • 单调读一致性:本次读的数据不能比上次读的旧,可以根据用户ID 的hash值映射到具体的库
    • 因果一致性
    • 最终一致性:不考虑中间状态,最终各个副本数据一致性

1.4 CAP 定理

选项描述
Consistency所有副本数据一致
Availability服务可用
Partition tolerance出现分区故障时,仍然能满足一致性和可用性

为什么 CAP 只能三选二?

  • 如果出现网络问题,节点只有两种选择,等待网络畅通获取最新数据,满足一致性,或者提供直接返回旧数据满足可用性
  • 不能忍受分区容错性,如果舍弃P,那么就退化成单节点,而不是分布式

CAP的实现

  • 一致性:未同步的数据副本不提供服务,可以锁定资源直到同步完成
  • 可用性:不锁定资源,及时新数据未同步,也要返回旧数据
  • 分区容错性:尽量使用异步代替同步

1.5 BASE 理论

核心思想:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

  • Basecally Available 基本可用
    • 牺牲响应时间:出现故障后,查询结果增加了响应时间也要返回
    • 牺牲功能:业务高峰期为保护系统稳定性,部分用户可能会被引导到一个降级页面,提示稍后再试
  • Soft state 软状态
    • 允许数据存在中间状态,如支付中、配送中,允许数据副本之间进行数据同步的过程中存在延迟
  • Eventually consistent 最终一致性
    • 在经过一段时间的同步后,最终能够达到一个一致的状态

1.6 一致性协议

1.6.1 2PC

二阶段提交

  • 阶段一
    • 协调者向所有参与者发送事务内容,询问是否可以执行事务,并等待所有参与者的响应
    • 参与者在本地执行事务(写Undo/Redo)后,再向协调者反馈ACK
  • 阶段二
    • 协调者接收到所有参与者的ACK后,继续向所有参与者发送 commit 请求,并开始等待所有参与者的响应
    • 参与者在本地提交事务,释放占用的事务资源,再向协调者反馈ACK
    • 协调者接收到所有参与者的ACK,完成事务

中断事务

  • 阶段一出现某个参与者反馈了 NO 响应或者超时等待之后,事务协调者向所有的参与者发送 rollback 请求

二阶段的优点和缺点

  • 优点:原理简单,实现方便
  • 缺点:
    • 同步阻塞:必须要等到一阶段所有的参与者反馈之后才能进行二阶段
    • 单点问题:如果事务协调者宕机,那么参与者会一直锁定事务资源
    • 数据不一致:如果事务协调者宕机,部分参与者还没有提交commit,会有严重数据不一致问题
    • 过于保守:如果事务协调者没有获取参与者的反馈,只能依靠自身的超时机制来判断是否要中断事务

1.6.2 3PC

image-20201127000348153

三阶段提交

  • 阶段一
    • 协调者向所有参与者发送 canCommit 请求,询问是否可以执行事务,并等待所有参与者的响应
    • 参与者向协调者反馈事务询问的响应
  • 阶段二
    • 如果协调者接收到所有参与者的响应都是 YES,那么继续向所有参与者发送 preCommit 请求,无需等待反馈就进入 prepared 阶段
    • 参与者在本地执行事务(写Undo/Redo)后,再向协调者反馈ACK
  • 阶段三
    • 协调者得到所有参与的响应都是 ACK,那么继续向所有参与者发送 doCommit 请求
    • 参与者接收到 doCommit 请求,正式提交事务,释放占用的事务资源,再向协调者反馈ACK
    • 协调者接收到所有参与者的ACK,完成事务

3PC 对比 2PC

  • 3PC 的协调者和参与者都有超时机制,参与者长时间无法与协调者通讯时,会自动进行本地 commit,从而避免协调者单点故障时参与者锁定事务资源
  • 通过3个阶段的设计,多了一个 preCommit 缓冲阶段,可以保证提交前所有参与者的状态是一致的

但是,3PC仍然没有完全解决数据一致性问题

1.7 一致性算法

1.7.1 Paxos

Paxos算法是Lamport提出的一种基于消息传递的分布式一致性算法,解决了分布式系统一致性问题。

image-20201127084721862

  • Proposal 提案:包括提案编号(Proposal ID)和提议的值(Value)
  • Client 客户端:向分布式系统发出请求,等待响应
  • Proposer 提案发起者:提案者提倡客户请求,试图说服Acceptor对此达成一致,并在发生冲突时充当协调者以推动协议向前发展
  • Acceptor 决策者:可以接受(accept)提案,如果提案被选定(chosen),那么提案里的value就是最终的值
  • Learners:最终决策者的学习者,充当该协议的复制因素

假设有一组可以提出提案的进程集合,那么对于一个一致性算法需要保证以下几点:

  • 在这些被提出的提案中,只有一个会被选定
  • 如果没有提案被提出,就不应该有被选定的提案
  • 当一个提案被选定后,那么所有进程都应该能学习(learn)到这个被选定的value
1.7.1.1 推导过程
1.7.1.1.1 推导提案
  • P1:一个Acceptor必须接受它收到的第一个提案
  • 规定:一个提案被选定需要被半数以上的Acceptor接受(避免各自为政)
  • P2:如果某个value为v的提案被选定了,那么每个编号更高的被选定提案的value必须也是v
  • P2a:如果某个value为v的提案被选定了,那么每个编号更高的被Acceptor接受的提案的value必须也是v
  • P2b:如果某个value为v的提案被选定了,那么之后任何Proposer提出的编号更高的提案的value必须也是
  • P2c:对于任意的Mn和Vn,如果提案[Mn,Vn]被提出,那么肯定存在一个由半数以上的Acceptor组成的集合S,满足以下两个条件
    中的任意一个:
    • 要么S中每个Acceptor都没有接受过编号小于Mn的提案。
    • 要么S中所有Acceptor批准的所有编号小于Mn的提案中,编号最大的那个提案的value值为Vn
1.7.1.1.2 Proposer 生成提案

Proposer生成提案之前,应该先去『学习』已经被选定或者可能被选定的value,然后以该value作为自己提出的提案的value。如果没有value被选定,Proposer才可以自己决定value的值。这样才能达成一致。这个学习的阶段是通过一个『Prepare请求』实现的。于是我们得到了如下的提案生成算法:

  1. Proposer选择一个新的提案编号N,然后向某个Acceptor集合(半数以上)发送请求,要求该集合中的每个Acceptor做出如下响应(response)(a) Acceptor向Proposer承诺保证不再接受任何编号小于N的提案。
    (b) 如果Acceptor已经接受过提案,那么就向Proposer反馈已经接受过的编号小于N的,但为最大编号的提案的值。我们将该请求称为编号为N的Prepare请求。
  2. 如果Proposer收到了半数以上的Acceptor的响应,那么它就可以生成编号为N,Value为V的提案[N,V]。这里的V是所有的响应中编号最大的提案的Value。如果所有的响应中都没有提案,那 么此时V就可以由Proposer自己选择。
    生成提案后,Proposer将该提案发送给半数以上的Acceptor集合,并期望这些Acceptor能接受该提案。我们称该请求为Accept请求
1.7.1.1.3 Acceptor 接受提案
  • Prepare请求:Acceptor可以在任何时候响应一个Prepare请求
  • Accept请求:在不违背Accept现有承诺的前提下,可以任意响应Accept请求

P1a:一个Acceptor只要尚未响应过任何编号大于N的Prepare请求,那么他就可以接受这个编号为N的提案。

1.7.1.2 Acceptor 算法优化

如果Acceptor收到一个编号为N的Prepare请求,在此之前它已经响应过编号大于N的Prepare请求。根据P1a,该Acceptor不可能接受编号为N的提案。因此,该Acceptor可以忽略编号为N的Prepare请求。

image-20201201001525130

1.7.1.3 算法描述

image-20201201001617857

1.7.1.4 Learner学习被选定的value

image-20201201001647698

1.7.1.5 保证 Paxos 算法的活性

活性:最终一定会发生的事情,最终一定要选定value

image-20201201231611750

1.7.2 Raft

Raft 是一种为了管理复制日志的一致性算法

三个模块:领导人选举、日志复制、安全性

两个阶段:选举阶段,然后在选举出来的领导人带领进行正常操作,比如日志复制

动画演示 http://thesecretlivesofdata.com/raft/

1.7.2.1 领导人选举

服务器可以扮演下面的角色

  • 领导者(leader):处理客户端交互,日志复制等动作,一般一次只有一个领导者
  • 候选者(candidate):候选者就是在选举过程中提名自己的实体,一旦选举成功,则成为领导者
  • 跟随者(follower):类似选民,完全被动的角色,这样的服务器等待被通知投票

image-20201201233605696

选举流程:Raft 使用心跳机制触发选举。当 Server 启动时,初始状态都是 follower,每个 Server 都有一个定时器,超时时间为 election timeout(150ms ~ 300ms),如果某 Server 在超时之前收到领导者或者候选者的任何信息,定时器重启,如果超时,它就开始一次选举。

1.7.2.2 日志复制

复制流程:Leader 选出后,开始接受客户端的请求。Leader 把请求作为日志条目(Log entries)加入到它的日志中,然后并行的向其他服务器发起 AppendEntries RPC 复制日志条目。当这条日志被复制到大多数服务器上,Leader 将这条日志应用到它的状态机并向客户端返回执行结果。

image-20201201234350861

4 个步骤

  • 客户端的每一个请求都包含被复制状态机执行的指令。
  • leader 把这个指令作为一条新的日志条目添加到日志中,然后并行发起 RPC 给其他的服务器,让他们复制这条信息。
  • 跟随者响应 ACK,如果 follower 宕机或者运行缓慢或者丢包,leader会不断的重试,直到所有的 follower 最终都复制了所有的日志条目。
  • 通知所有的Follower提交日志,同时领导人提交这条日志到自己的状态机中,并返回给客户端。
1.7.2.3 节点异常
  • Leader 不可用:剩下的节点会重新选举一个新的 Leader,如果之前的 Leader 重新加入集群,则比较两个 Leader 的步进数,步进数较低的将切换为 follower,剔除不一致的日志,并与现有 Leader 中的日志保持一致。
  • follower 不可用:重新加入时从 Leader 同步日志
  • 多个 candidate 或多个 leader:candidate 重新发起投票

2. 分布式系统设计策略

2.1 心跳检测

以固定的频率向其他节点汇报当前节点状态的方式,如果收到心跳,一般可以认为一个节点和现在的网络拓扑是良好的。

  • 周期检测心跳机制
    Server 每间隔 t 秒向 Node 集群发起监测请求,如果超过超时时间,则判断“死亡”。
  • 累计失效检测机制
    在周期检测心跳机制的基础上,统计一定周期内节点的返回情况(包括超时及正确返回),以此计算节点的“死亡”概率。另外,对于宣告“濒临死亡”的节点可以发起有限次数的重试,以作进一步判断。

2.2 高可用

  • 主备(Master-Slave):当主机宕机时,备机接管主机的工作,如MySQL、Redis等就采用MS模式实现主从复制

  • 互备(Active-Active):两台主机同时运行各自的服务工作且相互监测情况

  • 集群(Cluster)模式:多个节点在运行,同时可以通过主控节点分担服务请求,如Zookeeper

2.3 容错性

对于错误包容的能力,如缓存穿透

2.4 负载均衡

负载均衡的关键在于使用多台集群服务器共同分担计算任务,把网络请求及计算分配到集群可用的不同服务器节点上,从而达到高可用性及较好的用户操作体验。

硬件解决方案有著名的F5,软件有LVS、HAProxy、Nginx等

3. 分布式架构网络通信

3.1 基本原理

传输协议 + 网络IO

传输协议:TCP、UDP等基于 Socket 的传输协议

网络IO:BIO、NIO、AIO

3.2 RPC

远程过程调用 Remote Proccedure Call,简称 RPC,可以做到像本地调用一样调用远程服务。

RPC 架构包含 4 个核心组件

  • 客户端 Client:服务调用方
  • 客户端存根 Client Stub:存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方
  • 服务端 Server:服务提供方
  • 服务端存根 Server Stub:接收客户端的消息,将消息解包,并调用本地方法

image-20201202214153814

Java 常见的 RPC 框架有Hessian、gRPC、Thrift、HSF (High Speed Service Framework)、Dubbo 等,对 RPC 框架而言,核心模块 就是通讯和序列化。

3.3 RMI

RMI 是 Java 原生支持的远程调用 Remote Method Invocation,采用 JRMP 作为通信协议,主要用于虚拟机之间或者虚拟机内部的通信。

3.3.1 RMI 的组件

  • 客户端
    • 存根/桩 Stub:远程对象在客户端的代理
    • 远程引用层:解析并执行远程引用协议
    • 传输层:发送参数调用远程方法,接收远程方法执行结果
  • 服务端
    • 骨架 Skeleton:读取客户端传递的参数,调用服务端实际方法,并接收方法执行后的返回值
    • 远程引用层:处理远程引用后向骨架发送远程方法调用
    • 传输层:监听客户端的入站连接,接收并转发调用到远程引用层
  • 注册表:以 URL 形式注册远程对象,并向客户端恢复对远程对象的引用

image-20201202222256271

3.3.2 使用 RMI 完成 RPC

3.4 网络IO模型

  • 同步和异步:程序与内核的交互
    • 同步:指用户进程触发IO操作等待或者轮训的方式查看IO操作是否就绪
    • 异步:触发IO操作后不会立刻得到结果,完成时会被通知
  • 阻塞和非阻塞:针对于进程访问数据的时候,根据IO操作的就绪状态来采取不同的方式
    • 阻塞:读取和写入将一直等待
    • 非阻塞:读取和写入方法会返回一个状态值,不等待

3.4.1 BIO

服务器实现模式为一个连接一个线程,服务器接收到一个连接就启动一个线程进行处理

  • 缺点:如果连接不做任何事情,会造成不必要的线程开销
  • 优点:简单易用(服务端、客户端的代码都简单)
  • 适用场景:Java 4 之前的唯一选择

image-20201202234502042

3.4.2 NIO

服务器实现模式为一个连接一个通道,客户端发送的连接请求都注册到多路复用器上,多路复用器轮询到连接有 IO 请求时,服务器才启动一个线程批量处理

image-20201202235323535

3.4.3 AIO

异步非阻塞IO,当有流可以读时,操作系统会将可以读的流传入read方法的缓冲,并通知应用程序;对于写操作,OS将write方法的流写入完毕时会主动通知应用程序。因此read和write都是异步 的,完成后会调用回调函数。

使用场景:连接数目多且连接比较长(重操作)的架构,比如相册服务器。重点调用了OS参与并发操作,编程比较复杂。Java7开始支持。

3.5 Netty

3.5.1 认识 Netty

Netty是由 JBOSS 提供的一个异步的、基于事件驱动的网络编程框架

使用 Netty的理由:

  • NIO 缺点
    • NIO 的类库和 API 复杂,需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer
    • 可靠性不强,开发工作量和难度都非常大
    • 存在 bug,如Epoll Bug,会导致 Selector 空轮询,最终导致 CPU 100%
  • Netty 优点
    • 对各种传输协议提供统一的 API
    • 高度可定制的线程模型——单线程、一个或多个线程池
    • 更好的吞吐量,更低的等待延迟
    • 更少的资源消耗
    • 最小化不必要的内存拷贝

3.5.2 线程模型

单线程模型

image-20201205143426985

线程池模型

image-20201205143437473

Netty 模型

image-20201205143500772

Netty 抽象出两组线程池, BossGroup 专门负责接收客 户端连接, WorkerGroup 专门负责网络读写操作。NioEventLoop 表示一个不断循环执行处理 任务的线程, 每个 NioEventLoop 都有一个 selector, 用于监听绑定在其上的 socket 网络通道。 NioEventLoop 内部采用串行化设计, 从消息的读取->解码->处理->编码->发送, 始终由 IO 线程 NioEventLoop 负责。

3.5.3 核心组件

  • ChannelHandler:定义了许多事件处理的方法,可以重写实现具体的业务逻辑

    • public void channelActive(ChannelHandlerContext ctx), 通道就绪事件
    • public void channelRead(ChannelHandlerContext ctx, Object msg), 通道读取数据事件
    • public void channelReadComplete(ChannelHandlerContext ctx) , 数据读取完毕事件
    • public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause), 通道发生异常事件
  • ChannelPipeline:一个 Handler 集合,负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的链

    • ChannelPipeline addFirst(ChannelHandler… handlers), 把一个业务处理类(handler) 添加到链中的第一个位置
    • ChannelPipeline addLast(ChannelHandler… handlers), 把一个业务处理类(handler) 添加到链中的最后一个位置

    image-20201205144147527

  • ChannelHandlerContext:事件处理器上下文,是 Pipeline 链的实际处理节点,包含一个具体的事件处理器 ChannelHandler,同时绑定了对应的 pipeline 和 Channel 信息,方便对 ChannelHandler 调用

    • ChannelFuture close(), 关闭通道
    • ChannelOutboundInvoker flush(), 刷新
    • ChannelFuture writeAndFlush(Object msg) ,将数据写到 ChannelPipeline 中当前ChannelHandler 的下一个 ChannelHandler 开始处理(出站)
  • ChannelFuture:表示 Channel 中异步 IO 操作的结果,处理状态

    • Channel channel(), 返回当前正在进行 IO 操作的通道
    • ChannelFuture sync(), 等待异步操作执行完毕
  • EventLoopGroup 和其实现类 NioEventLoopGroup

    • public NioEventLoopGroup(), 构造方法
    • public Future<?> shutdownGracefully(), 断开连接, 关闭线程
  • ServerBootstrap 和 Bootstrap:启动助手,进行一些配置

    • public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),该方法用于服务器端, 用来设置两个 EventLoop
    • public B group(EventLoopGroup group) , 该方法用于客户端, 用来设置一个 EventLoop
    • public B channel(Class<? extends C> channelClass), 该方法用来设置一个服务器端的通道实现
    • public B option(ChannelOption option, T value), 用来给 ServerChannel 添加配置
    • public ServerBootstrap childOption(ChannelOption childOption, T value), 用来给接收到的 通道添加配置
    • public ServerBootstrap childHandler(ChannelHandler childHandler), 该方法用来设置业务处理类(自定 义的 handler)
    • public ChannelFuture bind(int inetPort) , 该方法用于服务器端, 用来设置占用的端口号
    • public ChannelFuture connect(String inetHost, int inetPort) 该方法用于客户端, 用来连接服务器端

3.5.4 使用 Netty 收发信息

<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.1.6.Final</version>
</dependency>

定义服务端

  1. 使用 NioEventLoopGroup 创建两个线程池对象
  2. 使用 ServerBootstrap 创建启动类
  3. 配置启动类,添加线程池到组、设置通道类型、绑定初始化监听 Handler
  4. 给 pipeline 设置编码和解码,绑定业务逻辑
  5. 绑定监听端口
  6. 关闭通道
// 1.创建两个线程池对象
// bossGroup负责接收用户乱接
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
// workGroup负责处理用户的IO读写操作
NioEventLoopGroup workGroup = new NioEventLoopGroup();

// 2.创建启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();

// 3.设置启动引导类
serverBootstrap
    // 将线程池添加到组
    .group(bossGroup, workGroup)
    // 设置通道类型
    .channel(NioServerSocketChannel.class)
    // 绑定一个初始化监听
    .childHandler(new ChannelInitializer<NioSocketChannel>() {
        // 事件监听Channel通道
        @Override
        protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
            // 获取pipeLine
            ChannelPipeline pipeline = nioSocketChannel.pipeline();
            // 绑定编码
            pipeline.addFirst(new StringEncoder())
                .addLast(new StringDecoder());
            // 绑定业务逻辑
            pipeline.addLast(new SimpleChannelInboundHandler<String>() {
                @Override
                protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
                    System.out.println(s);
                }
            });
        }
    });

// 4.引导类绑定端口(同步获取处理结果)
ChannelFuture future = serverBootstrap.bind(9999).sync();

// 5.关闭通道(同步获取处理结果)
future.channel().closeFuture().sync();

定义客户端

  1. 使用 NioEventLoopGroup 创建一个连接池对象
  2. 使用 Bootstrap 创建启动类
  3. 配置启动类,添加连接池到组、设置通道类型、绑定初始化监听 Handler
  4. 给 pipeline 设置编码和解码,绑定业务逻辑
  5. 使用启动引导类连接服务器
  6. 给服务器写数据
// 1.创建连接池对象
NioEventLoopGroup group = new NioEventLoopGroup();

// 2.创建启动引导类
Bootstrap bootstrap = new Bootstrap();

// 3.配置启动引导类
bootstrap.group(group)
    // 设置通道
    .channel(NioSocketChannel.class)
    // 设置Channel初始化监听
    .handler(new ChannelInitializer<Channel>() {
        @Override
        protected void initChannel(Channel channel) throws Exception {
            // 设置编码
            channel.pipeline().addLast(new StringEncoder());
        }
    });

// 4.使用启动引导类连接服务器
Channel channel = bootstrap.connect("127.0.0.1", 9999).channel();

// 5. 循环写数据给服务器
while (true) {
    channel.writeAndFlush("hello server...this is client" + LocalDateTime.now());
    Thread.sleep(new Random().nextInt(2000));
}

3.5.5 使用 Netty 完成 RPC

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

火车站卖橘子

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值