> 文档中心 > 分布式系统设计策略和网络通信

分布式系统设计策略和网络通信


分布式系统设计策略

分布式系统本质是通过低廉的硬件攒在一起以获得更好的吞吐量、性能以及可用性等。
在分布式环境下,有几个问题是普遍关心的,我们称之为设计策略:

  • 如何检测当前节点还活着?
  • 如何保障高可用?
  • 容错处理
  • 负载均衡

心跳检测

在分布式环境中,我们提及过存在非常多的节点(Node),其实质是这些节点分担任务的运行、计算或者程序逻辑处理。那么就有一个非常重要的问题,如何检测一个节点出现了故障乃至无法工作了?

通常解决这一问题是采用心跳检测的手段,如同通过仪器对病人进行一些检测诊断一样。

心跳顾名思义,就是以固定的频率向其他节点汇报当前节点状态的方式。收到心跳,一般可以认为一个节点和现在的网络拓扑是良好的。当然,心跳汇报时,一般也会携带一些附加的状态、元数据信息,以便管理

如图所示,Client请求Server,Server转发请求到具体的Node获取请求结果。Server需要与三个Node节点保持心跳连接,确保Node可以正常工作。
在这里插入图片描述
若Server没有收到Node3的心跳时,Server认为Node3失联。但是失联是失去联系,并不确定是否是Node3故障,有可能是Node3处于繁忙状态,导致调用检测超时;也有可能是Server与Node3之间链路出现故障或闪断。所以心跳不是万能的,收到心跳可以确认节点正常,但是收不到心跳也不能认为该节点就已经宣告“死亡”。
此时,可以通过一些方法帮助Server做决定: 周期检测心跳机制、累计失效检测机制

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

通过周期检测心跳机制、累计失效检测机制可以帮助判断节点是否“死亡”,如果判断“死亡”,可以把该节点踢出集群

高可用设计

高可用(High Availability)是系统架构设计中必须考虑的因素之一,通常是指,经过设计来减少系统不能提供服务的时间 。
在这里插入图片描述
系统高可用性的常用设计模式包括三种:主备(Master-SLave)、互备(Active-Active)和集群(Cluster)模式

  • 主备模式
    主备模式就是Active-Standby模式,当主机宕机时,备机接管主机的一切工作,待主机恢复正常后,按使用者的设定以自动(热备)或手动(冷备)方式将服务切换到主机上运行。在数据库部分,习惯称之为MS模式。MS模式即Master/Slave模式,这在数据库高可用性方案中比较常用,如MySQL、Redis等就采用MS模式实现主从复制。保证高可用,如图所示。
    在这里插入图片描述MySQL之间数据复制的基础是二进制日志文件(binary log file)。一台MySQL数据库一旦启用二进制日志后,作为master,它的数据库中所有操作都会以“事件”的方式记录在二进制日志中,其他数据库作为slave通过一个I/O线程与主服务器保持通信,并监控master的二进制日志文件的变化,如果发现master二进制日志文件发生变化,则会把变化复制到自己的中继日志中,然后slave的一个SQL线程会把相关的“事件”执行到自己的数据库中,以此实现从数据库和主数据库的一致性,也就实现了主从复制。

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

    我们使用过的、构建过的MySQL服务绝大多数都是Single-Master,整个拓扑中只有一个Master承担写请求。比如,基于Master-Slave架构的主从复制,但是也存在由于种种原因,我们可能需要MySQL服务具有Multi-Master的特性,希望整个拓扑中可以有不止一个Master承担写请求
    在这里插入图片描述

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

容错性

容错顾名思义就是IT系统对于错误包容的能力

容错的处理是保障分布式环境下相应系统的高可用或者健壮性,一个典型的案例就是对于缓存穿透 问题的解决方案。

我们来具体看一下这个例子,如图所示
在这里插入图片描述

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

如频繁发起为id为“-1”的条件进行查询,可能DB就挂掉了。

那这种问题有什么好办法解决呢?

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

负载均衡

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

如图,不同的用户User1、User2、User3访问应用,通过负载均衡器分配到不同的节点。
在这里插入图片描述
负载均衡器有硬件解决方案,也有软件解决方案。
硬件解决方案有著名的F5,软件有LVS、HAProxy、Nginx等。

以Nginx为例,负载均衡有以下几种策略:

  • 轮询:即Round Robin,根据Nginx配置文件中的顺序,依次把客户端的Web请求分发到不同的后端服务器。
  • 最少连接:当前谁连接最少,分发给谁。
  • IP地址哈希:确定相同IP请求可以转发给同一个后端节点处理,以方便session保持。
  • 基于权重的负载均衡:配置Nginx把请求更多地分发到高配置的后端服务器上,把相对较少的请求分发到低配服务器。

分布式架构网络通信

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

基本原理

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

什么是RPC

RPC全称为remote procedure call,即远程过程调用。

借助RPC可以做到像本地调用一样调用远程服务,是一种进程间的通信方式

比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的方法,由于两个应用不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。

需要注意的是RPC并不是一个具体的技术,而是指整个网络远程调用过程

RPC架构
一个完整的RPC架构里面包含了四个核心的组件,分别是Client,Client Stub,Server以及Server Stub,这个Stub可以理解为存根。

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

在这里插入图片描述

在这里插入图片描述

  1. 客户端(client)以本地调用方式(即以接口的方式)调用服务;
  2. 客户端存根(client stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对象序列化为二进制);
  3. 客户端通过sockets将消息发送到服务端;
  4. 服务端存根( server stub)收到消息后进行解码(将消息对象反序列化);
  5. 服务端存根( server stub)根据解码结果调用本地的服务;
  6. 本地服务执行并将结果返回给服务端存根( server stub);
  7. 服务端存根( server stub)将返回结果打包成消息(将结果消息对象序列化);
  8. 服务端(server)通过sockets将消息发送到客户端;
  9. 客户端存根(client stub)接收到结果消息,并进行解码(将结果消息发序列化);
  10. 客户端(client)得到最终结果。

RPC的目标是要把2、3、4、7、8、9这些步骤都封装起来。

注意:无论是何种类型的数据,最终都需要转换成二进制流在网络上进行传输,数据的发送方需要将对象转换为二进制流,而数据的接收方则需要把二进制流再恢复为对象。

在java中RPC框架比较多,常见的有Hessian、gRPC、Thrift、HSF (High Speed Service Framework)、Dubbo等,其实对 于RPC框架而言,核心模块 就是通讯和序列化。

RMI

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

  1. 客户端:
    • 存根/桩(Stub):远程对象在客户端上的代理;
    • 远程引用层(Remote Reference Layer):解析并执行远程引用协议;
    • 传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。
  2. 服务端:
    • 骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法,
      并接收方法执行后的返回值;
    • 远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用;
    • 传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。
  3. 注册表(Registry):以URL形式注册远程对象,并向客户端回复对远程对象的引用。
    在这里插入图片描述

远程调用过程:

  • 客户端从远程服务器的注册表中查询并获取远程对象引用。
  • 桩对象与远程对象具有相同的接口和方法列表,当客户端调用远程对象时,实际上是由相应的桩对象代理完成的。
  • 远程引用层在将桩的本地引用转换为服务器上对象的远程引用后,再将调用传递给传输层(Transport),由传输层通 过TCP协议发送调用;
  • 在服务器端,传输层监听入站连接,它一旦接收到客户端远程调用后,就将这个引用转发给其上层的远程引用层;
  • 服务器端的远程引用层将客户端发送的远程应用转换为本地虚拟机的引用后,再将请求传递给骨架(Skeleton);
  • 骨架读取参数,又将请求传递给服务器,最后由服务器进行实际的方法调用。

结果返回过程:

  • 如果远程方法调用后有返回值,则服务器将这些结果又沿着“骨架->远程引用层->传输层”向下传递;
  • 客户端的传输层接收到返回值后,又沿着“传输层->远程引用层->桩”向上传递,然后由桩来反序列化这些返回值,并 将最终的结果传递给客户端程序。

BIO、NIO、AIO

同步和异步

同步(synchronize)、异步(asychronize)是指应用程序和内核的交互而言的.

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

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

    使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,OS需要支持异步IO操作

阻塞和非阻塞

阻塞和非阻塞是针对于进程访问数据的时候,根据IO操作的就绪状态来采取不同的方式。
简单点说就是一种读写操作方法的实现方式。

  • 阻塞方式下读取和写入将一直等待
  • 非阻塞方式下,读取和写入方法会理解返回一个状态值

举个例子:
阻塞:
ATM机排队取款,你只能等待排队取款(使用阻塞IO的时候,Java调用会一直阻塞到读写完成才返回。)

非阻塞:
柜台取款,取个号,然后坐在椅子上做其他事,等广播通知,没到你的号你就不能去,但你可以不断的问大堂经理排到了没有。(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)

例子

老张煮开水。 老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。 1 老张把水壶放到火上,站立着等水开。(同步阻塞) 2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)3 老张把响水壶放到火上,立等水开。(异步阻塞) 4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)

BIO

同步阻塞IO。B代表blocking

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

适用场景:Java1.4之前唯一的选择,简单易用但资源开销太高
在这里插入图片描述

NIO

同步非阻塞IO (non-blocking IO / new io)是指JDK 1.4 及以上版本。

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

  • 通道(Channels)
    NIO 新引入的最重要的抽象是通道的概念。Channel 数据连接的通道。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中 .
  • 缓冲区(Buffers)
    通道channel可以向缓冲区Buffer中写数据,也可以像buffer中存数据。
  • 选择器(Selector)
    使用选择器,借助单一线程,就可对数量庞大的活动 I/O 通道实时监控和维护。
    在这里插入图片描述

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

AIO

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

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

Netty

Netty认识

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

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

在这里插入图片描述
为什么使用Netty

  • NIO缺点

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

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

线程模型

  • 单线程模型
    在这里插入图片描述

  • 线程池模型
    在这里插入图片描述

  • Netty 模型
    在这里插入图片描述

Netty 抽象出两组线程池, BossGroup 专门负责接收客 户端连接, WorkerGroup 专门负责网络读写操作。

NioEventLoop 表示一个不断循环执行处理 任务的线程, 每个 NioEventLoop 都有一个 selector, 用于监听绑定
在其上的 socket 网络通道。 NioEventLoop 内部采用串行化设计, 从消息的读取->解码->处理->编码->发送, 始终由 IO 线 程 NioEventLoop 负责。

Netty核心组件

ChannelHandler 及其实现类
ChannelHandler 接口定义了许多事件处理的方法, 我们可以通过重写这些方法去实现具 体的业务逻辑
我们经常需要自定义一个 Handler 类去继承 ChannelInboundHandlerAdapter, 然后通过 重写相应方法实现业务逻辑, 我们接下来看看一般都需要重写哪些方法:

- 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
ChannelPipeline 是一个 Handler 的集合, 它负责处理和拦截 inbound 或者 outbound 的事 件和操作, 相当于一个贯穿 Netty 的链。

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

在这里插入图片描述
ChannelHandlerContext

这 是 事 件 处 理 器 上 下 文 对 象 , Pipeline 链 中 的 实 际 处 理 节 点 。 每 个 处 理 节 点
ChannelHandlerContext 中 包 含 一 个 具 体 的 事 件 处 理 器 ChannelHandler , 同 时
ChannelHandlerContext 中也绑定了对应的 pipeline 和 Channel 的信息,方便对ChannelHandler 进行调用。
常用方法如下所示

- ChannelFuture close(), 关闭通道 - ChannelOutboundInvoker flush(), 刷新 - ChannelFuture writeAndFlush(Object msg) , 将 数 据 写 到 ChannelPipeline 中 当 前 - ChannelHandler 的下一个 ChannelHandler 开始处理(出站)

ChannelFuture
表示 Channel 中异步 I/O 操作的结果, 在 Netty 中所有的 I/O 操作都是异步的, I/O 的调 用会直接返回, 调用者并不能立刻获得结果, 但是可以通过 ChannelFuture 来获取 I/O 操作 的处理状态。 常用方法如下所示:

- Channel channel(), 返回当前正在进行 IO 操作的通道 - ChannelFuture sync(), 等待异步操作执行完毕

EventLoopGroup 和其实现类 NioEventLoopGroup
EventLoopGroup 是一组 EventLoop 的抽象, Netty 为了更好的利用多核 CPU 资源, 一般 会有多个 EventLoop同时工作, 每个 EventLoop 维护着一个 Selector 实例。 EventLoopGroup 提供 next 接口, 可以从组里面按照一定规则获取其中一个 EventLoop 来处理任务。 在 Netty 服务器端编程中, 我们一般都需要提供两个EventLoopGroup, 例如: BossEventLoopGroup 和 WorkerEventLoopGroup。

- public NioEventLoopGroup(), 构造方法 - public Future shutdownGracefully(), 断开连接, 关闭线程

ServerBootstrap 和 Bootstrap
ServerBootstrap 是 Netty 中的服务器端启动助手,通过它可以完成服务器端的各种配置; Bootstrap 是 Netty 中的客户端启动助手, 通过它可以完成客户端的各种配置。 常用方法如下 所示:

- public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),该方法用于 服务器端, 用来设置两个 EventLoop - public B group(EventLoopGroup group) , 该方法用于客户端, 用来设置一个 EventLoop - public B channel(Class 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) 该方法用于客户端, 用来连接服务器端