从0到1编写分布式文件系统——Master之通信协议

1,239 阅读7分钟

从0到1编写分布式文件系统——Master之通信协议

  虽然在上篇文章(从0到1编写分布式文件系统——框架搭建)中定义了Master作为管理节点,主要负责元数据的管理工作,但具体的功能模块还需要进一步细化。但是,凭空设计Master的细分功能难免会有些考虑不周的情况。作为软件工程师,我们知道功能是由需求驱动的,可以先提出一些对于Master节点的需求内容,并在此基础上完善其功能设计。

  首先,通过定义一种基础业务需求来分析Master节点应该包含哪些功能模块,那应该定义哪种业务需求较为合适呢?既然是文件系统,那就有文件夹的管理功能,那该业务需求可以为:客户端发送创建文件夹的消息到master节点,master节点接收该消息并成功创建文件夹
要处理该请求,则Master节点应包含以下几部分的功能:
1、消息结构及处理机制。必须定义一种通用的通信协议以及处理机制,能处理客户端发送过来不同的消息请求;
2、网络通讯模块。master必须接收不同客户端发送过来的请求;
3、元数据结构及处理机制。必须定义一种目录树结构来存储客户端发送过来文件夹处理请求。

通信协议

  因为TCP/IP协议是通过字节传输内容的,所以作为网络中通信的双方,必须定义一种通信协议,双方这种协议去解析传输的字节,才能知道对方具体发送的内容。例如HTTP Request协议,如下图所示。该报文分为三部分:报文首部、空行以及报文主体。
image.png

  1. 报文首部。包含请求行(方法、URL以及版本)和首部字段,用CR和LF组合字符分隔各部分之间的内容;
  2. 空行。仅包含CR和LF字符,作为首部和主体间的分隔符;
  3. 报文主体。请求的主要内容。 下面的示例是访问/index时,请求报文的首部信息,HTTP服务端便根据请求报文定义的格式,解析客户端(一般是浏览器)发送过来的请求内容。
    GET /index HTTP/1.1
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
    Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*; q=0.8
    Accept-Language: ja,en-us;q=0.7,en;q=0.3

  所以,为了让Master节点和客户端双方保持良好的通信交流,需要定义一种通用灵活的通信协议,保证Master节点能解析客户端发送过来的各式各样的请求。以下表格便是定义的通信协议内容:

protocol contentsizedescription
magic1 byte每个数据包的头个字节都定义了0xDF魔数,用作检测
type1 byte每种request和reponse都定义了唯一一种类型
id8 bytes为每个request定义了唯一的请求ID
serialize1 byte每个数据包指定了序列化的算法
length4 bytes数据包的大小
bodyX bytes数据包的内容

  通信协议的内容如表格所示(具体可以查看类Packet),无论是Request还是Response都是发送packet数据包,Request或Response的具体内容为协议的body内容。但是这里如何识别是Request请求还是Response回复呢,主要是通过type字段去识别。type字段是一个byte,通过字节的第一位来区分,0是request请求,1是response回复,以mkdir request(创建文件夹)为例,mkdir reponse则是将mkdir request的第一位变为1。 image.png 整体报文的格式如下图所示:

image.png

序列化

  既然网络中都是通过字节传输内容,那任何请求是必须转化为字节才能经过网络传播,这个转化过程称为序列化,而将字节转化为请求信息则为反序列化。Java序列化就是指把Java对象转换为字节序列的过程。

  通信协议中有个serialize字段,表示客户端可以选择不同的序列化算法对数据包进行序列化和反序列化。于是,接下来开始编写不同的序列化算法,SDFS暂时只用了Kryo算法。

  定义一个通用的序列化接口,只包含两个方法,序列化(serialize)和反序列化(deserialize)。

package org.simpledfs.core.serialize;

public interface Serializer {
    byte[] serialize(Object object);
    <T> T deserialize(byte[] bytes, Class<T> clazz);
}

  使用Kryo框架,因为KryoSerializer内部使用池化对象处理,所以采用了单例模式构建了KryoSerializer对象,其构造函数为私有。

public class KryoSerializer implements Serializer {

    private KryoSerializer() {
    }
    ...

    private static class PoolHolder {

        private static Pool<Kryo> kryoPool = new Pool<Kryo>(true, false, 8) {
            @Override
            protected Kryo create() {
                Kryo kryo = new Kryo();
                kryo.setRegistrationRequired(false);
                kryo.setReferences(true);
                return kryo;
            }
        };
    }
    ...
 
}

  定义一个序列化算法选择器SerializerChooser,根据协议中serialize字节,来获取不同的序列化算法对数据包进行解析,但目前只实现了Kryo,所以在choose方法默认只返回KryoSerializer实例。

public class SerializerChooser {

    private SerializerChooser() {
    }

    public Serializer choose(byte type){
        return KryoSerializer.getInstance();
    }

    private static class SerializerChooserHolder {
        private static SerializerChooser chooser = new SerializerChooser();
    }
    public static SerializerChooser getInstance() {
        return SerializerChooserHolder.chooser;
    }
}

  以“创建文件夹”请求为示例,首先定义好Packet包的各个字段内容:

  1. 魔数。默认为0xDF;
  2. Type。创建文件夹的类型值为1;
  3. ID。暂时随机生成,ID值为20211220112334004;
  4. Serialize。Kryo算法的序列值为1;
  5. Length。假设MkdirRequest的parent(父文件夹)值为"/user",name(将要创建的文件夹名)值为"name",则经Kryo序列化长度为52;
  6. 内容。MkdirRequest对象内容。 则序列化之后的字节内容如下图所示(内容字段忽略)。

image.png

处理机制

  因为客户端会发送各种类型的请求,如何针对不同的请求进行处理呢?

  首先定义顶层的processor接口,该接口只有一个process方法,并且集成了线程接口Runable,以后每个processor能包装成一个线程在线程池中运行

public interface Processor extends Runnable {
    public void process();
}

  之后定义一个通用的抽象类AbstractRequestProcessor,该类implements Processor,并声明了几个变量,这几个变量在后续做说明。Processor接口的process方法在AbstractRequestProcessor中声明为抽象方法,由具体请求的proceeor来编写自己的处理方法。

public abstract class AbstractRequestProcessor implements Processor {

    protected ChannelHandlerContext ctx;

    protected Request request;

    protected Context context;

    protected long packetId;

    ...

    public abstract void process();

    @Override
    public void run() {
        process();
    }

    ...

}

  最后,为mkdirRequest 定义了具体的MkdirRequestProcessor来处理创建文件夹的请求,MkdirRequestProcessor具体处理的业务逻辑后续做说明,还涉及到master元数据的修改。那MkdirRequestProcessor通过什么方式构造出来的呢?

  通过所有request中继承的buildSelfProcessor抽象方法,各自的request构造各自processor,通过这种灵活的方式对代码进行解耦,添加每一个request的同时只要添加对应的processor,不需要对其他代码进行修改。

public class MkdirRequest extends AbstractRequest {

    ...

    @Override
    public Processor buildSelfProcessor(ChannelHandlerContext ctx, Request request, Context context, Long packetId){
        MkdirRequestProcessor processor = new MkdirRequestProcessor(ctx, request, context, packetId);
        return processor;
    }

    ...
}

总结

  客户端和服务端之间通信的模型如下图所示。client的请求经过序列化后由网络服务传输给服务端,服务端反序列化后,处理该请求,并将处理后的信息返回给客户端。 image.png

其他说明

上篇文章从0到1编写分布式文件系统——框架搭建
下篇文章从0到1编写分布式文件系统——Master节点之通络通信机制
详细的代码:Simple Distributed File System