回顾下分布式理论...

218 阅读13分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

分布式理论

分布式系统是一个硬件或软件分布在不同的网络计算机上,彼此之间通过消息传递进行通信和协调的系统

面临问题

  • 通信异常: 网络的不可靠性,存在延时
  • 网络分区: 脑裂
  • 三态: 成功, 失败和超时
  • 节点故障: 宕机, 僵死现象

一致性

  • 强一致性

    最符合用户直觉,系统写入什么,读出什么

  • 弱一致性

    约束系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据达到一致, 但会尽可能地保证到某个时间级别后数据达到一致

  • 最终一致性

    弱一致性的一个特例,系统保证在一定时间内,能够达到一个数据一致的状态。

分布式事务

分布式事务指事务的参与者, 支持事务的服务器, 资源服务器以及事务服务器分别位于分布式系统的不同节点上, 通常一个分布式事务会涉及多个数据源或业务系统的操作。

CAP

Consistency 一致性

Availablility 可用性

Patition tolerance 分区容错性

分布式系统只能满足其中两个条件。

如果一个系统由两个节点配合组成, 之间通过网络通信, 节点A更新同时更新节点B。 如果发生了网络分区, 一致性无法满足, 如果强行满足一致性,必须停止服务,放弃可用性。

CA:所有数据放在一个分布式节点上, 放弃了系统的可扩展性。

CP: 放弃可用性, 如果网络分区,受到影响的服务会等待一段时间,在等待时间内无法提供服务

AP: 当出现网络分区, 必须让节点继续对外服务,失去一致性,这个一致性是强一致性, 保留数据的最终一致性。能够承诺的是数据最终达到一致性,引入了时间窗口的概念,取决于系统的设计。

BASE

Basically Available Soft State Eventually consistent

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

Basically Available 基本可用

分布式系统在出现不可预知的故障,允许损失部分可用性。

Soft State

允许系统中的数据存在中间状态, 并认为此状态不影响系统的整体可用性,系统在多个节点的数据副本之间进行数据同步的过程中存在延迟

Eventually consistent

最终一致性强调系统中所有的数据副本在一定时间的同步后,最终能够达到一致的状态。

最终一致性存在五种主要变种

  • 因果一致性(Causal consistency)

    节点A更新完数据后通知了节点B, 节点B之后对数据的访问和修改基于节点A更新后的值。 和节点A无因果关系的节点C的数据访问没有这样的限制。

  • 读己之所写(Read your writes)

    节点A更新完一个数据后,自身总能读到自己更新的最新值

  • 会话一致性(Session consistency)

    执行更新操作后, 同一个有效会话中始终读取到数据项的最新值

  • 单调读一致性(Monotonic read consistency)

    如果一个节点从系统中读取出一个数据项的值, 那么对于该节点后续的任何数据访问不应该返回更旧的值

  • 单调写一致性(Monotonic write consistency)

    一个系统保证来自同一个节点的写操作被顺序的执行。

2PC

两阶段提交, 一致性协议, 用来保证分布式系统数据的一致性。主要有协调者和参与者两个角色。强一致性事务。

阶段一 投票

  • 事务询问 协调者向参与者发送事务内容 询问是否执行事务提交操作
  • 执行事务 各节点执行事务操作, 将undo和redo信息记入事务日志
  • 各参与者向协调者反馈事务询问的响应

阶段二

两种情况

事务提交:

  • 发送提交请求
  • 事务提交
  • 反馈事务提交结果
  • 完成事务

中断事务:

  • 发送回滚请求
  • 事务回滚
  • 返回事务回滚结果
  • 中断事务

优缺点

优点: 原理简单,实现方便

缺点:

  • 同步阻塞

    各参与者等待其他参与者响应过程中无法进行其他操作

  • 单点问题

    协调者的单点问题

  • 数据不一致

    如果commit的过程中,有部分参与者崩溃或者网络异常, 就会数据不一致

  • 过于保守

    提交询问阶段, 参与者出现故障, 协调者无法获取所有参与者响应信息, 协调者只能根据超时机制判断是否需要中断事务。过于保守,没有设计完善的容错机制

3PC

CanCommit

  • 事务询问

    协调者向所有的参与者发送包含事务内容的canCommit请求, 询问是否可以执行事务提交操作,等待响应。

  • 各参与者向协调者反馈事务询问的响应

PreCommit

执行事务提交:

  • 发送预提交请求
  • 事务预提交
  • 各参与者向协调者反馈事务执行的结果

中断事务:

  • 发送中断请求
  • 中断事务

doCommit

执行事务提交:

  • 发送提交请求
  • 事务提交
  • 反馈事务提交结果
  • 完成事务

中断事务:

  • 发送中断请求
  • 事务回滚
  • 返回事务回滚结果
  • 中断事务

一旦进入阶段三,可能出现两种故障

  • 协调者出现问题
  • 协调者和参与者之间的网络故障

出现了任一一种情况, 都会导致参与者无法收到doCommit请求或者abort请求,参与者在等待超时后,继续进行事务提交。

优缺点:

优点: 降低了参与者的阻塞范围(第一个阶段不阻塞), 在单点故障后继续达成一致(2PC提交阶段出现问题, 3PC根据协调者的状态进行回滚或者提交)

缺点: 参与者收到preCommit后, 出现网络分区,参与者等待超时后会进行事务提交, 必然出现分布式事务不一致问题。

3PC对比2PC

对协调者和参与者都设置了超时机制(2PC 中,只有协调者拥有超时机制). PreCommit是一个缓冲,保证了最后提交阶段之前各节点状态一致。

Paxos

概念

提案(Proposal): 包含提案编号和提议的值

角色:

  • Proposer 提案发起者
  • Acceptor 决策者 可以批准提案
  • Learner: 最终决策的学习者

一个进程可能既是Proposer,也是Acceptor, 也是Learners。

Proposer: 只要Proposer发起的提案被半数以上Acceptor接受

Acceptor: 只要Acceptor接受提案

Learner: 只要Acceptor告诉Learner

算法描述

阶段一

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

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

阶段二

  • 如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它会发送针对[N,V]提案的Accept请求给半数以上的Acceptor。
  • 如果Acceptor收到一个针对N的提案的Accept请求, 只要该Acceptor没有对编号大于N的Prepare请求作出过响应,它就会接受提案。

Learner 学习被选定的Value

  • 第一种, Acceptor选定后直接通知 M(Acceptor) * N(Proposer)
  • 第二种, Acceptor选定后通知一个主learner, 主learner通知其他learner。
  • 第三种, Acceptor选定后通知一个learner组, 由这个组通知其他learner。

如何保证Paxos的活性

活性: 最终一定要选定value

通过选取主Proposer, 规定只有主Proposer才能提出议案。 只要主Proposer和过半的Acceptor能够正常进行网络通信, 整个Paxos算法就能保持活性。

Raft

raft是为了管理复制日志的一致性算法。

领导人选举

角色:

  • 领导者
  • 候选者
  • 跟随者

影响角色变化的是选举,raft通过心跳机制触发选举, Server启动时,初始状态都是follower, 每个server有定时器, election timeout。 如果server没有超时的情况下收到来自领导者或者候选者的任何消息,定时器重启,如果超时, 开始一次选举。

日志复制

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

分布式系统设计

心跳机制

  • 保证可靠性

  • 应对异常机制

    • 周期检测心跳机制

      Server端每间隔t秒向Node集群发起监测请求

    • 累计失效检测机制

      在周期检测心跳机制基础上,统计一定周期内节点的返回情况, 计算'死亡'概率。进行有限次重试

高可用设计

  • 主备

    Active-Standby模式。

  • 互备

    MM模式, 每个Master都具有read-write能力。

  • 集群

    多个节点运行, 主控节点分担服务请求。

容错

保证分布式环境下响应系统的高可用或者健壮性

负载均衡

硬件负载: F5

软件负载: HAProxy Nginx LVS

主要策略:

  • 轮询
  • 最小连接
  • IP地址哈希
  • 基于权重

分布式架构网络通信

RPC

RPC: Remote Procedure Call,远程服务调用,Remote Procedure Call Protocol 是一个计算机通信协议, 它允许像调用本地方法一样调用远程服务, 由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

作用:

  • 屏蔽组包解包
  • 屏蔽数据接收发送
  • 提高开发效率
  • 业务发展的必然产物

核心组成:

  • 远程方法对象代理
  • 连接管理
  • 序列化/反序列化
  • 寻址与负载均衡

RPC调用方式:

  • 同步调用
  • 异步调用

RMI

Remote Method Invocation, 是java 原生支持的远程调用, 采用JRMP(Java Remote Messaging protocol)作为通讯协议。主要用于不同虚拟机之间的通信。

客户端

  • stub: 远程对象在客户端上的代理对象
  • Remote Reference Layer: 解析并执行远程引用协议
  • Transport: 发送调用, 传递远程方法参数, 接收远程方法执行结果

服务端

  • Skeleton: 读取客户端传递过来的方法参数,调用实际的方法
  • Remote Reference Layer: 处理远程引用后向骨架发送远程方法调用
  • Transport: 监听客户端的入站连接,接收并转发调用到远程引用层。

注册表

以url 形式注册远程对象,并向客户端回复对远程对象地引用。

代码实现
  • 服务端

/***
 * 必须继承Remote
 */
public interface IHelloService extends Remote {

    /**
     * 必须抛出RemoteException
     * @param name
     * @return
     * @throws RemoteException
     */
    public String sayHello(String name) throws RemoteException;
}



/**
 * 必须继承UnicastRemoteObject
 */
public class HelloService extends UnicastRemoteObject implements IHelloService {


    protected HelloService() throws RemoteException {
        super();
    }

    @Override
    public String sayHello(String name) throws RemoteException {
        return "abc";
    }
}


public class RmiServer {

    public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
        IHelloService helloService = new HelloService();

        LocateRegistry.createRegistry(8888);

        Naming.bind("rmi://localhost:8888/rmiserver", helloService);
    }
}

  • 客户端
public class RMIClient {

    public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
        IHelloService helloService = (IHelloService)Naming.lookup("rmi://localhost:8888/rmiserver");
        System.out.println(helloService.sayHello("dad"));
    }
}

BIO ,NIO, AIO

同步和异步

同步和异步是指应用程序和内核的交互而言的。

同步: 用户进程触发IO操作等待或者轮询的方式查看IO操作是否就绪

异步: 当一个异步进程调用发出之后, 调用者不会立刻得到结果。而是在调用发出之后,被调用者通过状态, 通知来通知调用者,或者通过回调函数来处理这个调用。

阻塞和非阻塞

阻塞和非阻塞针对进程访问数据的时候, 根据IO操作的就绪状态来采取不同的方式。简单来说就是读写操作方法的实现方式。阻塞方式下读取和写入将一直等待, 非阻塞方式下,读取和写入方法会立即返回一个状态值。

BIO

同步阻塞IO, 处理并发请求的时候很慢。

serverSocket().accept 和socket.getInputStream().read(bytes)两个阻塞

NIO

同步非阻塞IO

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

  • 通道(Channel)
  • 缓冲区(Buffer)
  • 选择器(Selector)
public class NIOClient {

    public static void main(String[] args) {
        InetSocketAddress  address = new InetSocketAddress("127.0.0.1",8888);
        SocketChannel channel = null;
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try{
            channel = SocketChannel.open();
            channel.connect(address);
            Scanner sc = new Scanner(System.in);
            while (true){
                String line = sc.nextLine();
                if(line.equals("exit")){
                    break;
                }
                buffer.put(line.getBytes(StandardCharsets.UTF_8));
                buffer.flip();
                channel.write(buffer);
                buffer.clear();

                int readLen = channel.read(buffer);
                if(readLen ==-1){
                    break;
                }
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                System.out.println(new String(bytes));
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(null != channel){
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


public class NIOServer implements Runnable{

    //多路复用器
    private Selector selector;
    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);

    public NIOServer(int port){
        init(port);
    }

    public static void main(String[] args) {
        new Thread(new NIOServer(8888)).start();
    }

    private void init(int port) {

        try{
            this.selector = Selector.open();
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(port));
            serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true){
            try{
                this.selector.select();
                Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
                while(keys.hasNext()){
                    SelectionKey key = keys.next();
                    keys.remove();
                    if(key.isValid()){
                        if(key.isAcceptable()){
                            accept(key);
                        }
                        if(key.isReadable()){
                            read(key);
                        }
                        if(key.isWritable()){
                            write(key);
                        }
                    }
                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void write(SelectionKey key) {
        this.readBuffer.clear();
        SocketChannel channel = (SocketChannel)key.channel();
        Scanner scanner = new Scanner(System.in);
        try{
            String line = scanner.nextLine();
            writeBuffer.put(line.getBytes("UTF-8"));
            writeBuffer.flip();
            channel.write(writeBuffer);
            System.out.println(line);
            channel.register(this.selector, SelectionKey.OP_READ);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private void read(SelectionKey key) {

        try{
            this.readBuffer.clear();
            SocketChannel channel = (SocketChannel)key.channel();
            int readLen = channel.read(readBuffer);
            if(readLen ==-1){
                key.channel().close();
                key.cancel();
                return;
            }

            this.readBuffer.flip();
            byte[] bytes = new byte[readBuffer.remaining()];
            readBuffer.get(bytes);
            System.out.println(new String(bytes, "utf-8"));
            channel.register(this.selector, SelectionKey.OP_WRITE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }



    private void accept(SelectionKey key) {
        try{
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
            SocketChannel channel = serverSocketChannel.accept();
            channel.configureBlocking(false);
            channel.register(this.selector, SelectionKey.OP_READ);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

AIO

异步非阻塞IO, 需要一个连接注册读写事件和回调的方法。

使用场景在于 连接数目多且连接比较长的架构, 比如相册服务器,需要OS参与

Netty

优点

对传输协议提供统一的api

高度可定制的线程模型: 单线程, 一个或多个线程池

更好的吞吐量, 更低的等待延迟

更少的资源消耗

最小化不必要的内存拷贝

线程模型

BossGroup 线程池负责接收客户端连接, WorkerGroup负责网络读写操作。NIOEventLoop 表示不断循环执行处理的线程,每个NIOEventLoop都有一个selector, 用于绑定socket网络通道。NIOEventLoop采用串形化设计,从消息读取-> 解码-> 处理->编码-> 发送,始终由NIOEventLoop负责。

核心组件

  • ChannelHandler

    ChannelInboundHandler ChannelOutboundHandler

  • ChannelPipeline

    一个Handler的集合, 负责处理和拦截inbound或者outbound的事件和操作, 相当于贯穿Netty的链。

  • ChannelHandlerContext

    上下文对象, 实际处理节点,每个节点中包含一个具体的事件处理器ChannelHandler,绑定了对应的pipeline和Channel信息,方便对ChannelHandler进行调用

  • ChannelFuture

    Channel异步I/O操作的结果

  • EventLoop 和EventLoopGroup

    每个EventLoop维护着一个Selector实例。主要有两个EventLoopGroup : BossEventLoopGroup和WorkerEventLoopGroup

  • ServerBootStrap 和BootStrap

    启动助手

代码

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.6.Final</version>
</dependency>
    
public class NettyServer {

    public static void main(String[] args) throws InterruptedException {

        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        ChannelFuture future = new ServerBootstrap().group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        ChannelPipeline pipeline = nioSocketChannel.pipeline();
                        pipeline.addFirst(new StringEncoder());
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String o) throws Exception {
                                System.out.println(o);
                            }
                        });
                    }
                }).bind(9999).sync();
        future.channel().closeFuture();
    }
}
public class NettyClient {

    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel channel) throws Exception {
                        channel.pipeline().addLast(new StringEncoder());
                    }
                });
        Channel channel = bootstrap.connect("127.0.0.1", 9999).channel();
        while (true){
            channel.writeAndFlush("hell");
            Thread.sleep(2000);
        }
    }
}