Skip to main content

分布式框架介绍

​ 分布式服务框架,在服务器端口发布业务,客户端可以远程调用业务。使用protobuf进行序列的序列化、反序列化,以及分布式通信的实现。使用 Zookeeper 来作为分布式的注册和发现服务。

​ 使用之前,双方先要约定数据格式,使用protobuf的格式。

对于服务提供方而言,需要继承 对应的Rpc 类,并重写相应调用的方法。

服务提供方:

​ 创建一个RpcProvider 类,调用provider.notifyService(Rpc 对象)方法,传入的是重写的Rpc类的对象。调用provider.run()开启监听。

1. 分布式

考虑一个聊天业务,我们对其进行子模块划分,那么可以大致分为:用户管理、好友管理、群组管理、消息管理、文件管理,其中每一个模块都包含了一个特定的业务。将该服务器部署在一个单机上面,如下图所示:

img

上图中所示的单机服务器模型存在如下缺点:

  1. 受到硬件资源的限制,聊天服务器所能承受的用户的并发量较低;
  2. 修改任意模块的代码,会导致整个项目代码的重新编译、部署。项目如果过大,会很耗时;
  3. 在系统中,有些模块是属于 CPU 密集型的,而有些模块则是属于 I/O 密集型的,造成各个模块对于硬件资源的需求各不相同,而单机服务器只能给所有模块提供同一种硬件资源,无法做到硬件资源的特化;
  4. 如果服务器出现异常,那么整个服务都会挂掉;

为了解决单机服务器所带来的并发量较低的缺陷,我们可以采用集群的方法,增加服务器的数量,同时通过一个负载均衡服务器来分发用户请求即可。常见的硬件负载均衡器有:F5、A10 等,软件层面的负载均衡服务器包括 LVS、Nginx、HAproxy 等。集群服务器模型的结构则如下图所示:

img

对于集群服务器模型而言,它解决了硬件资源受限所导致的用户并发量问题,此外如果其中一台服务器挂掉,还有另外其它几台服务器可以正常提供服务。但是对于项目编译、部署的问题而言,却并未得到改善,项目要分别在每个机器上进行编译、部署,反而变得更加麻烦了。对于不同模块对于硬件资源的需求也并未得到解决。此外,对于一些并发量较低的模块,可能并不需要做到高并发,也就无需通过负载均衡器将用户请求分发到不同的服务器中,但是对于集群模型而言,每一个模块之间都是均衡的,并未做到模块的特化。

为了改进上述缺点,我们需要将其改进为分布式模型,其结构如下图所示。即将不同的模块部署在不同的服务器上,同时对于并发量较大的模块,我们可以通过集群部署来提升它的并发量,此外对于不同的模块,也可以提供不同的硬件资源,同时修改一个模块的代码也无需再编译整个项目,仅仅编译该模块即可:

img

总结而言,集群和分布式的区别如下:

  • 集群:每一台服务器独立运行一个工程的所有模块。
  • 分布式:一个工程拆分了很多模块,每一个模块独立部署运行在一个服务器主机上,所有服务器协同工 作共同提供服务,每一台服务器称作分布式的一个节点,根据节点的并发要求,对一个节点可以再做节点模块集群部署。

2. RPC

尽管分布式模型存在许多优点,但是考虑如下场景:用户正在访问用户管理模块,此时需要获取所有的好友信息,那么需要用到好友管理模块的内容。但是由于分布式部署的原因,用户管理模块和好友管理模块部署在不同的两个分布式节点上,即两台主机上。此时用户管理主机应该如何调用好友管理主机上的相应的业务方法?

这时就需要使用到 RPC 方法,为使用者提供一种透明调用机制而不必显式的区分本地调用和远程调用。RPC 方法的交互过程如下图所示:

img

由于底层网络通信框架使用的是运输层协议,只能发送字节流,因此会涉及到对象的序列化/反序列化问题,即上图中所示的黄色部分,而常见的网络数据传输格式包括如下三种:

  • XML:一种通用和轻量级的数据交换格式语言,是指可扩展标记语言以文本结构进行存储。
  • JSON:一种通用和轻量级的数据交换格式,也是以文本的结构进行存储,是一种简单的消息格式。JSON 作为数据包格式传输时具有更高的效率,这是因为 JSON 不像 XML 那样需要有严格的闭合标签,这就让有效数据量与总数据包比有着显著的提升,从而减少同等数据流量的情况下网络的传输压力。
  • Protobuf:是 Google 开发的一种独立和轻量级的数据交换格式,以二进制结构进行存储,用于不同服务之间序列化数据。它是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者序列化,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

而该项目便是使用 Protobuf 来进行消息的序列化和反序列化,同时使用其来实现RPC框架,其底层的通信流程如下图所示:

img

RpcProvider类

notifyService方法

​ 传入的是重写的Rpc类的对象service,通过service对象获取对应的类名字,方法名,并获取服务对象指定服务方法的描述。通过哈希表保存,通过名字索引。

Run方法:

​ 加载配置,并把rpc节点上要发布的服务全部注册到Zookeeper上面。service_name为永久性节点 method_name为临时性节点,最后保存这个点的IP:Port,创建Sever服务器启动监听。

此外,为了解决TCP的粘包问题,我们设计了如下格式的数据头用来传递服务名称、方法名称以及参数大小,通过该数据头部我们可以确定所要读取的数据长度:

message RpcHeader {
bytes service_name = 1;
bytes method_name = 2;
uint32 args_size = 3;
}

同时,为了确定 RpcHeader 的长度,我们使用了固定的 4 个字节来存储消息头的长度。数据打包和解包的流程图如下所示:

OnMessage:

​ 有新的请求,解析请求,调用相应的(抽象描述)Descriptor函数。

打包流程

  1. 序列化函数参数得到 argsStr,其长度为 argsSize;
  2. 打包 service_name、method_name 和 argsSize 得到 rpcHeader;
  3. 序列化 rpcHeader 得到 rpcHeaderStr,其长度为 headerSize;
  4. 将 headerSize 存储到数据包的前 4 个字节,后面的依次是 rpcHeaderStr 和 argsStr;
  5. 通过网络发送数据包;

解析流程:

  1. 通过网络接收数据包;
  2. 首先取出数据包的前 4 个字节,读取出 headerSize 的大小;
  3. 从第 5 个字节开始,读取 headerSize 字节大小的数据,即为 rpcHeaderStr 的内容;
  4. 反序列化 rpcHeaderStr,得到 service_name、method_name 和 argsSize;
  5. 从 4+headerSize 字节开始,读取 argsSize 字节大小的数据,即为 argsStr 的内容;
  6. 反序列化 argsStr 得到函数参数 args;

img

rpcChannel类

​ 用于服务请求方。请求方创建Rpc_Stub类,并创建rpcChannel类对象当作参数传入。通过Rpc_Stub调用相应的方法,就可以的到对应的响应结果。

rpcChannel重写CallMethod方法,获取对应的类名方法名,以及对应的参数打包。通过名字查询服务对应的IP和端口。序列化之后发送,等待相应后返回。

ZooKeeper

在分布式应用中,为了能够知道自己所需的服务位于哪台主机上,我们需要一个服务注册与发现中心,这也就是该项目中的ZooKeeper。它是一种分布式协调服务,可以在分布式系统中共享配置,协调锁资源,提供命名服务。

Zookeeper通过树形结构来存储数据,它由一系列被称为ZNode的数据节点组成,类似于常见的文件系统。不过和常见的文件系统不同,Zookeeper将数据全量存储在内存中,以此来实现高吞吐,减少访问延迟,其结构如下图所示:

img

ZkClient 类中,调用 start() 方法创建 zkHandler_ 句柄时,其调用过程如下图所示,由于 zookeeper_init 是异步方法,所以该函数返回后,并不代表句柄创建成功,它会在回调函数线程中调用所传入的回调函数,在该回调函数中我们可以来判断句柄是否创建成功,由于API调用线程阻塞于信号量处,所以在回调函数中如果句柄创建成功,则调用 sem_post 方法增加信号量,以通知API调用线程句柄创建成功:

img