分布式相关理论

1. 分布式相关理论

1.1 CAP定理

CAP 理论含义是,一个分布式系统不可能同时满足一致性(C:Consistency),可用性(A: Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中的2个。

选项描述

C 一致性分布式系统当中的一致性指的是所有节点的数据一致,或者说是所有副本的数据一致 。如何实现:写入主数据库后,在向从数据库同步期间要将从数据库锁定, 等待同步完成后在释放锁。

A 可用性Reads and writes always succeed. 也就是说系统一直可用,而且服务一直保持正常。如何实现:写入主数据库后,要向从数据库同步,数据未同步成功时,也要返回查询数据,不能返回错误和超时。

P 分区容错性系统在遇到一些节点或者网络分区故障的时候,仍然能够提供满足一致性和可用性的服务。实现:使用异步数据从主数据同步到从数据库,添加数据库节点。一个节点挂掉,从其它节点同步。

1.2 BASE 理论

BASE:全称:Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)。BASE是对CAP中一致性和可用性权衡的结果,BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

①Basically Available(基本可用):系统出现故障时,损失部分可用性。如12306查询有票,下单时提示已经无票。

②Soft state(软状态):允许系统中的数据存在中间状态,该状态不影响系统的整体可用性,即允许副本在不同的多个节点同步时存在延迟。

③Eventually consistent(最终一致性):经过一段时间的同步后,最终能够达到一个一致的状态。

1.3 分布式事务

数据库事务回顾:

Atomicity(原子性): 事务是一个不可分割的整体,要么全做,要么不做。

Consistency(一致性):事务执行前后,数据从一个状态到另一个状态必须是一致的。如转账:不能发生一个扣钱,一个没扣钱的情况。

Isolation(隔离性):多个并发事务之间相互隔离,不能互相干扰。

Durablity(持久性):事务完成后,对数据库的更改是永久保存的。

分布式事务:

一致性协议 2PC:是将整个事务流程分为两个阶段,准备阶段(Preparephase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。

优点:原理简单,实现方便。 缺点:同步阻塞,单点问题,数据不一致,过于保守。

一致性协议 3PC:CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议

2PC对比3PC:首先对于协调者和参与者都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败),主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。 2.通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的 。3.PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。

1.4 一致性算法 Paxos

 Paxos解决了分布式系统一致性问题。

相关概念:提案 (Proposal):Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)。Client:客户端, 客户端向分布式系统发出请求,并等待响应。Proposer:提案发起者,提案者提倡客户请求,试图说服Acceptor对此达成一致,并在发生冲突时充当协调者以推动协议向前发展。Acceptor:决策者,可以批准提案。Learners:最终决策的学习者,学习者充当该协议的复制因素。

Paxos算法实现过程:

阶段一:

(a) Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。

(b) 如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的所有Prepare请求的编号,那么它就会将它已经接受过的编号最大的提案(如果有的话)作为响应反馈给Proposer,同时该Acceptor承诺不再接受任何编号小于N的提案。

阶段二:

(a) 如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它就会发送一个针对[N,V]提案的Accept请求给半数以上的Acceptor,否则编号+1后重新开始。注意:V就是收到的响应中编号最大的提案的value,如果

响应中不包含任何提案,那么V就由Proposer自己决定。(b) 如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求做出过响应,它就接受该提案。

当然,实际运行过程中,每一个Proposer都有可能产生多个提案,但只要每个Proposer都遵循如上所述的算法运行,就一定能够保证算法执行的正确性。

Learner学习被选定的value方案: Acceptor将批准的提案发送给一个特定的Learner集合,该集合中每个Learner都可以在一个提案被选定后通知其他的Learner。这个Learner集合中的Learner个数越多,可靠性就越好,但同时网络通信的复杂度也就越高。

如何保证Paxos算法的活性:极端情况下提交提案会陷入死循环,可以通过选取主Proposer,并规定只有主Proposer才能提出议案。这样一来只要主Proposer和过半的Acceptor能够正常进行网络通信,那么但凡主Proposer提出一个编号更高的提案,该提案终将会被批准,这样通过选择一个主Proposer,整套Paxos算法就能够保持活性。

1.5 一致性算法 Raft

Raft 是一种为了管理复制日志的一致性算法。Raft提供了和Paxos算法相同的功能和性能,但是它的算法结构和Paxos不同。Raft算法更加容易理解并且更容易构建实际的系统。Raft算法分为两个阶段,首先是选举过程,然后在选举出来的领导人带领进行正常操作,比如日志复制。

日志复制(保证数据一致性)过程。

1. 客户端的每一个请求都包含被复制状态机执行的指令。

2. leader把这个指令作为一条新的日志条目添加到日志中,然后并行发起 RPC 给其他的服务器,让他们复制这条信息。

3. 跟随者响应ACK,如果 follower 宕机或者运行缓慢或者丢包,leader会不断的重试,直到所有的 follower 最终都复制了所有的日志条目。

4. 通知所有的Follower提交日志,同时领导人提交这条日志到自己的状态机中,并返回给客户端。可以看到,直到第四步骤,整个事务才会达成。中间任何一个步骤发生故障,都不会影响日志一致

2.  分布式系统设计策略

2.1  心跳检测

分布式存在非常多的节点(Node),其实质是这些节点分担任务的运行、计算或者程序逻辑处理。那么就有一个非常重要的问题,如何检测一个节点出现了故障乃至无法工作了?通常解决这一问题是采用心跳检测的手段,如同通过仪器对病人进行一些检测诊断一样。心跳顾名思义,就是以固定的频率向其他节点汇报当前节点状态的方式。收到心跳,一般可以认为一个节点和现在的网络拓扑是良好的。当然,心跳汇报时,一般也会携带一些附加的状态、元数据信息,以便管理。

周期检测心跳机制:Server端每间隔 t 秒向Node集群发起监测请求,设定超时时间,如果超过超时时间,则判断“死亡”。

累计失效检测机制:在周期检测心跳机制的基础上,统计一定周期内节点的返回情况(包括超时及正确返回),以此计算节点的“死亡”概率。另外,对于宣告“濒临死亡”的节点可以发起有限次数的重试,以作进一步判断。通过周期检测心跳机制、累计失效检测机制可以帮助判断节点是否“死亡”,如果判断“死亡”,可以把该节点踢出集群。

2.2 高可用设计

系统高可用性的常用设计模式包括三种:主备(Master-SLave)、互备(Active-Active)和集群(Cluster)模式。

主备:MySQL之间数据复制的基础是二进制日志文件(binary log file)。一台MySQL数据库一旦启用二进制日志后,作为master,它的数据库中所有操作都会以“事件”的方式记录在二进制日志中,其他数据库作为slave通过一个I/O线程与主服务器保持通信,并监控master的二进制日志文件的变化,如果发现master二进制日志文件发生变化,则会把变化复制到自己的中继日志中,然后slave的一个SQL线程会把相关的“事件”执行到自己的数据库中,以此实现从数据库和主数据库的一致性,也就实现了主从复制。

互备模式: 互备模式指两台主机同时运行各自的服务工作且相互监测情况。在数据库高可用部分,常见的互备是MM模式。MM模式即Multi-Master模式,指一个系统存在多个master,每个master都具有read-write能力,会根据时间戳或业务逻辑合并版本。

集群模式:是指有多个节点在运行,同时可以通过主控节点分担服务请求。如Zookeeper。集群模式需要解决主控节点本身的高可用问题,一般采用主备模式。

2.3 容错性

举例:我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,或者有人恶意攻击。

解决办法:将这个不存在的key预先设定一个值。比如,key=“null”。在返回这个null值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是null,则可以认为这时候key有值了,从而避免了透传到数据库,把大量的类似请求挡在了缓存之中。

2.4 负载均衡

负载均衡器有硬件解决方案,也有软件解决方案。硬件解决方案有著名的F5,软件有LVS、HAProxy、Nginx等。以Nginx为例,负载均衡有以下几种策略:

·轮询:即Round Robin,根据Nginx配置文件中的顺序,依次把客户端的Web请求分发到不同的后端服务器。

·最少连接:当前谁连接最少,分发给谁。

·IP地址哈希:确定相同IP请求可以转发给同一个后端节点处理,以方便session保持。

·基于权重的负载均衡:配置Nginx把请求更多地分发到高配置的后端服务器上,把相对较少的请求分发到低配服务器。

3 分布式架构网络通信

在分布式服务框架中,一个最基础的问题就是远程服务是怎么通讯的,在Java领域中有很多可实现远程通讯的技术,例如:RMI、Hessian、SOAP、ESB和JMS等。

3.1 基本原理

要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理,在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现,其中传输协议比较出名的有tcp、udp等等,tcp、udp都是在基于Socket概念上为某类应用场景而扩展出的传输协议,网络IO,主要有bio、nio、aio三种方式,所有的分布式应用通讯都基于这个原理而实现,只是为了应用的易用。

3.2 RPC

RPC全称为remote procedure call,即远程过程调用。借助RPC可以做到像本地调用一样调用远程服务,是一种进程间的通信方式。比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的方法,由于两个应用不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。需要注意的是RPC并不是一个具体的技术,而是指整个网络远程调用过程。

RPC架构

客户端(Client):服务的调用方。客户端存根(Client Stub):存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。

服务端(Server):真正的服务提供者。服务端存根(Server Stub):接收客户端发送过来的消息,将消息解包,并调用本地的方法。

3.3 RMI

Java RMI 指的是远程方法调用 (Remote Method Invocation),是java原生支持的远程调用 ,采用JRMP(JavaRemote Messageing protocol)作为通信协议,可以认为是纯java版本的分布式远程调用解决方案, RMI主要用于不同虚拟机之间的通信,这些虚拟机可以在不同的主机上、也可以在同一个主机上,这里的通信可以理解为一个虚拟机上的对象调用另一个虚拟机上对象的方法。

代码实现:

1. 创建远程接口。

import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* 远程服务对象接口必须继承Remote接口;同时方法必须抛出RemoteExceptino异常
*/
public interface Hello extends Remote {
  public String sayHello(User user) throws RemoteException;
}

其中有一个引用对象作为参数。

import java.io.Serializable;
  /**
    * 引用对象应该是可序列化对象,这样才能在远程调用的时候:1. 序列化对象 2. 拷贝 3. 在网络中传输* 4. 服务端反序列化 5. 获取参数进行方法调用; 这种方式其实是将远程对象引用传递的方式转化为值传递的方式
  */
public class User implements Serializable {
  private String name;
  private int age;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }
}

2. 实现远程服务对象

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/**
* 远程服务对象实现类写在服务端;必须继承UnicastRemoteObject或其子类
**/
public class HelloImpl extends UnicastRemoteObject implements Hello {
/**
* 因为UnicastRemoteObject的构造方法抛出了RemoteException异常,因此这里默认的构造方法必须写,必须
声明抛出RemoteException异常
*
* @throws RemoteException
*/
  private static final long serialVersionUID = 3638546195897885959L;
  protected HelloImpl() throws RemoteException {
    super();
  // TODO Auto-generated constructor stub
  }
  @Override
  public String sayHello(User user) throws RemoteException {
    System.out.println("this is server, hello:" + user.getName());
    return "success";
  }
}

3. 服务端程序

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
/**
* 服务端程序
**/
public class Server {
  public static void main(String[] args) {
    try {
      Hello hello = new HelloImpl(); // 创建一个远程对象,同时也会创建stub对象、skeleton对象
      //本地主机上的远程对象注册表Registry的实例,并指定端口为8888,这一步必不可少(Java默认端口是1099),必不可缺的一步,缺少注册表创建,则无法绑定对象到远程注册表上
      LocateRegistry.createRegistry(8080); //启动注册服务
      try {
      //绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的)
        Naming.bind("//127.0.0.1:8080/zm", hello); //将stub引用绑定到服务地址上
      } catch (MalformedURLException e) {
      // TODO Auto-generated catch block
        e.printStackTrace();
      }
      System.out.println("service bind already!!");
    } catch (RemoteException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
}

4. 客户端程序

/**
* 客户端程序
* @author zm
*
*/
public class Client {
  public static void main(String[] args) {
    try {
       //在RMI服务注册表中查找名称为RHello的对象,并调用其上的方法
       Hello hello = (Hello) Naming.lookup("//127.0.0.1:8080/zm");//获取远程对象
       User user = new User();
       user.setName("james");
       System.out.println(hello.sayHello(user));
    } catch (MalformedURLException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
    } catch (RemoteException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
    } catch (NotBoundException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
    }
}

3.4 BIO、NIO、AIO

同步和异步:同步和异步关注的是消息通信机制。所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由*调用者*主动等待这个*调用*的结果。

而异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。

阻塞与非阻塞:阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

BIO: 同步阻塞IO。服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改。(不推荐)

NIO:同步非阻塞IO。当一个连接创建后,不会需要对应一个线程,这个连接会被注册到多路复用器,所以一个连接只需要一个线程即可,所有的连接需要一个线程就可以操作,该线程的多路复用器会轮训,发现连接有请求时,才开启一个线程处理。

AIO:异步非阻塞IO。当有流可以读时,操作系统会将可以读的流传入read方法的缓冲区,并通知应用程序,对于写操作,OS将write方法的流写入完毕是操作系统会主动通知应用程序。因此read和write都是异步的,完成后会调用回调函数。使用场景:连接数目多且连接比较长(重操作)的架构,比如相册服务器。重点调用了OS参与并发操作,编程比较复杂。

3.5 Netty

Netty 是由 JBOSS 提供一个异步的、 基于事件驱动的网络编程框架。Netty 可以帮助你快速、 简单的开发出一 个网络应用, 相当于简化和流程化了 NIO 的开发过程。 作为当前最流行的NIO 框架, Netty在互联网领域、 大数据分布式计算领域、 游戏行业、 通信行业等获得了广泛的应用, 知名的 Elasticsearch 、 Dubbo框架内部都采用了Netty。

NIO缺点: NIO的类库和API繁杂,使用麻烦。你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。可靠性不强,开发工作量和难度都非常大NIO的Bug。例如 Epoll Bug,它会导致 Selector空轮询,最终导致CPU100%。

Netty优点:对各种传输协议提供统一的API,高度可定制的线程模型——单线程、一个或多个线程池更好的吞吐量,更低的等待延迟,更少的资源消耗,最小化不必要的内存拷贝。  

posted on 2020-10-20 15:11  lvguoliang(学无止境)  阅读(215)  评论(0编辑  收藏  举报