Java RMI 实现一个简单的GFS(谷歌文件系统)——演示与实现篇

本文主要是使用Java RMI 实现一个简单的GFS(谷歌文件系统,google file system),这里提供演示运行视频、系统实现以及源代码相关。

[为了更好的阅读以及查看其他篇章,请查看原文:https://www.cnblogs.com/maogen/p/gfs_2.html]

🧨 大年初二,走亲访友🏮 🧧

🏮祝大家新年快乐!🏮
ʰᵅᵖᵖʸ ⁿeᵚ ʸᵉᵅʳ

家人闲坐 灯火可亲
辞旧迎新 新年可期

系统整体介绍、背景以及设计信息:

介绍篇:https://www.cnblogs.com/maogen/p/gfs_0.html

背景与设计篇:https://www.cnblogs.com/maogen/p/gfs_1.html

作者:晨星1032-博客园:https://www.cnblogs.com/maogen/


演示运行视频

[MyGFS演示--Java RMI 实现一个简单的GFS_腾讯视频]:https://v.qq.com/x/page/h3226nudkk0.html

1. 系统组织结构

3_gfs_1

如图所示,整个MyGFS分布式文件系统由SPI、Common API,Master,ChunkServer和Client五个模块组成:

  • SPI:定义Master与ChunkServer需要实现的接口,并实现存放Chunk及其信息的抽象类。MasterApi与ChunkServerApi均继承自Remote接口,标识着这是一个远程接口。

  • Master:实现远程接口实现类MasterEngine,继承自UnicastRemoteObject类并实现MasterApi接口,负责与Client的通信与对ChunkServer的管理。

  • ChunkServer:实现远程接口实现类ChunkServerEngine,继承自UnicastRemoteObject类并实现ChunkServerApi接口,接收Master的调度并负责对Chunk的管理。

  • Client:使用分布式文件系统的本地端,通过与Master直接通信来间接地对文件系统进行操作。

  • Common:该模块负责实现工具类与配置文件,例如生成UUID,将文件读入内存等操作。

其具体的三方通讯流程如下图所示:

2. Master模块

2.1 心跳机制

​ 使用Java RMI方式,在Master端检测每个ChunkServer是否在线。具体操作如下:

  1. 通过RMI方式来检测Chunk服务器的心跳,直接以try-catch方式判断。若服务器宕机则加入failedChunkServerList中。

    1. 检查正常ChunkServer上的所有Chunk的Hash值,若不一致则加入到Chunk失败列表中。
  2. 最后进行错误处理。

public synchronized void heartbeatScan() {
        System.out.println("heartbeat checking...");
        // 错误Chunk列表
        Map<String, List<ChunkInfo>> failedChunkMap = new LinkedHashMap<>();
        // 错误Server列表
        List<String> failedChunkServerList = new ArrayList<>();

        ChunkServerApi chunkServerApi;
        Map<Long, String> hashMap;
        int index = 0;
        for(String chunkServer : chunkServerList) {
            // 使用RMI检测心跳
            try{
                chunkServerApi = (ChunkServerApi) Naming.lookup("rmi://" + chunkServer + "/chunkServer");
                // 获取Hash,用来检测Chunk错误
                hashMap = chunkServerApi.getHashMap();
            } catch (Exception e) {
                // 服务器宕机
                System.out.println("ChunkServer: " + chunkServer + " is down!");
                failedChunkServerList.add(chunkServer);
            }

             try {
                 List<ChunkInfo> failedList = new ArrayList<>();
                 for (ChunkInfo chunkInfo : serverInfoMap.get(chunkServer)) {
                     String hash = hashMap.get(chunkInfo.getChunk().getChunkId());

                     if (hash == null || !hash.equals(chunkInfo.getHash())) {
                         System.out.println("chunk:" + chunkInfo.getChunk().getChunkFileName() + " ERROR!");
                         chunkInfo.removeReplicaServerName(chunkServer);
                         int idx = nameNodeList.indexOf(chunkInfo.getNameNode());

                         nameNodeList.get(idx).setChunkInfo(chunkInfo, chunkInfo.getSeq());
                         serverInfoMap.get(chunkServer).set(index, chunkInfo);

                         failedList.add(chunkInfo);
                     }
                     index++;
                 }
                 failedChunkMap.put(chunkServer, failedList);
             }catch (Exception e) {
                 System.out.println("检测chunk失败...");
             }
        }//for

        // 错误处理
        handleFaults(failedChunkMap, failedChunkServerList);
        System.out.println("heartbeat check end...");
    }

2.2 故障恢复和容错机制

​ 若ChunkServer掉线,则需分配新的服务器负载均衡,并将取出该ChunkServer上对应的Chunk文件,对其进行复制。

System.out.println("正在处理宕机的服务器:" + serverName + "...");
// 当宕机服务器没有Chunk时,直接去除
if(serverInfoMap.get(serverName).size() == 0) {
    // 去除此服务器
    chunkServerList.remove(serverName);
    System.out.println("处理宕机服务器成功");
}

for(ChunkInfo chunkInfo : serverInfoMap.get(serverName)) {
    System.out.println("备份failed chunkServer" + serverName + "中的Chunk "
                       + chunkInfo.getChunk().getChunkFileName());
    try {
        chunkServerList.remove(serverName);
        chunkInfo.removeReplicaServerName(serverName);
        // 服务器节点分配
        allocateNode(chunkInfo, chunkInfo.getFirstReplicaServerName());
        // 处理NameNode
        int idx = nameNodeList.indexOf(chunkInfo.getNameNode());
        nameNodeList.get(idx).setChunkInfo(chunkInfo, chunkInfo.getSeq());

        if(chunkInfo.getFirstReplicaServerName() == null) {
            continue;
        }

        chunkServerApi = (ChunkServerApi) Naming.lookup(
            "rmi://" + chunkInfo.getFirstReplicaServerName() + "/chunkServer");
        chunkServerApi.backupChunk(chunkInfo.getChunk(), chunkInfo.getLastReplicaServerName());
        System.out.println("处理宕机服务器成功");
    }catch (Exception e) {
        System.out.println("处理宕机服务器失败!");
        e.printStackTrace();
    }
}

​ 若该ChunkServer上的Chunk文件的Hash数据与Master上不一致则使用该Chunk文件的副本对其进行替换。

// chunk failed! 本地文件恢复
for(Map.Entry<String, List<ChunkInfo>> failedChunk : failedChunkMap.entrySet()) {
    String serverName = failedChunk.getKey();
    List<ChunkInfo> chunkInfos = failedChunk.getValue();
    for(ChunkInfo chunkInfo : chunkInfos) {
        System.out.println("从服务器" + serverName + "上正在恢复错误的Chunk:" + chunkInfo.getChunk().getChunkFileName());
        try {
            if(chunkInfo.getFirstReplicaServerName() == null || 
               chunkInfo.getFirstReplicaServerName().equals(serverName)){
                System.out.println("没有备份,恢复失败!");
                continue;
            }

            chunkInfo.setLastReplicaServerName(serverName);
            int idx = nameNodeList.indexOf(chunkInfo.getNameNode());
            nameNodeList.get(idx).setChunkInfo(chunkInfo, chunkInfo.getSeq());

            chunkServerApi = (ChunkServerApi) Naming.lookup(
                "rmi://" + chunkInfo.getFirstReplicaServerName() + "/chunkServer");
            chunkServerApi.backupChunk(chunkInfo.getChunk(), serverName);
            System.out.println(chunkInfo.getChunk().getChunkFileName() + "恢复成功!");
        }catch (Exception e) {
            System.out.println("恢复失败!");
            e.printStackTrace();
        }
    }
}

​ 运行截图如图所示:

3_gfs_3

3. ChunkServer模块

3.1 内存命中机制

public class ChunkServerMemory {
   private final LinkedList<ChunkMemory> memoryList;
   private final int maxContain;

   public ChunkServerMemory(int maxContain) {
       this.memoryList = new LinkedList<>();
       this.maxContain = maxContain;
   }

   public void push(Chunk chunk,byte[] data) {
       if(memoryList.size()>maxContain){
           memoryList.removeLast();
       }
       memoryList.push(new ChunkMemory(chunk,data));
   }

   public ChunkMemory search(Chunk chunk){
       ChunkMemory res=null;
       for (int i = 0; i < memoryList.size(); i++) {
           if(memoryList.get(i).isMatch(chunk)){
               res=memoryList.get(i);
               moveToHead(i);
               System.out.println(chunk.getChunkFileName()+"内存命中");
           }
       }
       return res;
   }

   private void moveToHead(int i){
       ChunkMemory tmp=memoryList.get(i);
       memoryList.remove(i);
       memoryList.push(tmp);
   }

   public void remove(long chunkId){
       for (int i = 0; i < memoryList.size(); i++) {
           if(memoryList.get(i).isMatch(chunkId)){
               memoryList.remove(i);
               return;
           }
       }
   }
}

3.2 状态维护

​ 一分钟更新一次本地Chunk的Hash值。

try{
   Thread.sleep(60000);
   System.out.println("开始检查Chunk信息");
   for(Long chunkId : chunkIdList) {
       String md5Str = SecurityUtil.getMd5(filePath + getChunkName(chunkId));
       if(md5Str == null) {
           md5Str = "check error: no file!";
       }
       chunkHash.put(chunkId, md5Str);
   }
   System.out.println("检查Chunk信息结束");
} catch (Exception e) {
   e.printStackTrace();
   break;
}

3.3副本管理

​ GFS默认Chunk主副本三个,但为了实际演示方便,这里设置为主副本各一个,下图为windows服务器和Linux服务器上的存储。

3_gfs_4

4. Client模块

4.1 上传

​ 在Client端上传文件时,会先将文件相关信息添加到Master中,同时Master会分配服务器到各个Chunk文件,然后Client通过分配的信息向指定的ChunkServer进行传送数据流。

public void upLoadFile(String fileAddr) {
   System.out.println("文件正在上传...");
   try{
       int length, seq = 0;
       byte[] buffer = new byte[CHUNK_SIZE];

       File file = new File(fileAddr);
       // 向Master添加该Name结点
       masterApi.addNameNode(file.getName());

       InputStream input = new FileInputStream(file);
       input.skip(0);
       while ((length = input.read(buffer, 0, CHUNK_SIZE)) > 0) {
           byte[] upLoadBytes = new byte[length];
           System.arraycopy(buffer, 0, upLoadBytes, 0, length);
           String hash = SecurityUtil.getMd5(upLoadBytes);
           uploadChunk(file.getName(), seq, length, upLoadBytes, hash);
           seq++;
       }
       input.close();
       System.out.println("文件已上传!");
   } catch (Exception e) {
       System.out.println("文件上传失败");
       System.out.println(e.getLocalizedMessage());
   }
}

​ 演示效果如图所示,分别为Client端和ChunkServer端的情况。

4.2 下载

​ 用户在Client端下载文件时,会先向Master请求所下载文件的信息,然后通过Master返回的Chunk所在ChunkServer信息进行数据请求获取。

public String downloadFile(String fileName) throws Exception {
   System.out.println("文件正在下载...");
   String fileAddr = prefixPath + "new_" + fileName;
   File localFile = new File(fileAddr);
   OutputStream output = new FileOutputStream(localFile);

   List<ChunkInfo> chunkInfoList = masterApi.getChunkInfos(fileName);
   for(ChunkInfo chunkInfo : chunkInfoList) {
       output.write(downloadChunk(chunkInfo.getChunk(), chunkInfo.getFirstReplicaServerName()));
   }
   output.close();

   return fileAddr;
}

4.3 追加

​ 每一个Chunk默认最大为64Mb,追加操作需要对最后一个Chunk的剩余空间进行判断:

  1. 若最后一个Chunk剩余空间 > 所追加文件大小,则直接添加最后一个即可。
  2. 若最后一个Chunk剩余空间 < 所追加文件大小,则首先将最后一个Chunk空间加满,然后再新建Chunk直到 > 所追加文件大小
public void appendFile(String fileName, String appendFileAddr) throws Exception {
    List<ChunkInfo> chunkInfoList = masterApi.getChunkInfos(fileName);
    if(chunkInfoList.isEmpty()) {
        System.out.println("Master找不到该文件!");
        return;
    }
    System.out.println("文件正在进行修改...");

    byte[] bytes = ConvertUtil.file2Byte(appendFileAddr);

    // 获取最后一个Chunk信息
    int num = chunkInfoList.size();
    ChunkInfo chunkInfo = chunkInfoList.get(num-1);
    int chunkLen = (int)chunkInfo.getChunk().getByteSize();
    int appendLen = bytes.length;

    int len = CHUNK_SIZE - chunkLen;
    // 可以继续追加
    if(len >= appendLen) {
        byte[] newBytes = new byte[appendLen];
        System.arraycopy(bytes, 0, newBytes, 0, appendLen);
        chunkServerApi = (ChunkServerApi) Naming.lookup("rmi://" + chunkInfo.getFirstReplicaServerName() + "/chunkServer");
        chunkServerApi.appendChunk(chunkInfo.getChunk(), newBytes, chunkInfo.getLastReplicaServerName());
        masterApi.updateNameNode(fileName, chunkLen + appendLen);
    }else {
        // 需要新建Chunk
        // 最后一个Chunk剩余大小->加满
        byte[] leftBytes = new byte[len];
        System.arraycopy(bytes, 0, leftBytes, 0, len);
        // 更新chunkServer
        chunkServerApi = (ChunkServerApi) Naming.lookup("rmi://" + chunkInfo.getFirstReplicaServerName() + "/chunkServer");
        chunkServerApi.appendChunk(chunkInfo.getChunk(), leftBytes, chunkInfo.getLastReplicaServerName());
        // Master更新
        masterApi.updateNameNode(fileName, CHUNK_SIZE);

        // 其余处理
        String hash;
        while (len + CHUNK_SIZE <= appendLen) {
            leftBytes = new byte[CHUNK_SIZE];
            System.arraycopy(bytes, len, leftBytes, 0, CHUNK_SIZE);
            hash = SecurityUtil.getMd5(leftBytes);
            uploadChunk(fileName, num, CHUNK_SIZE, leftBytes, hash);
            len += CHUNK_SIZE;
            num++;
        }
        if (len < appendLen) {
            int lastSize = appendLen - len;
            leftBytes = new byte[lastSize];
            System.arraycopy(bytes, len, leftBytes, 0, lastSize);
            hash = SecurityUtil.getMd5(leftBytes);
            uploadChunk(fileName, num, lastSize, leftBytes, hash);
        }
    }
    System.out.println("文件已修改!");
}

4.4 删除

​ 删除文件仅将Master上的信息进行删除,ChunkServer本地上的文件未删除(软删除)

public void deleteFile(String fileName) throws Exception {
    masterApi.deleteNameNode(fileName);
    System.out.println("文件删除成功!");
}

4.5 文件列表

public void getFileList() throws Exception {
    List<String> fileList = masterApi.getFileList();
    if(fileList.size() == 0) {
        System.out.println("空");
    }
    for(String fileName : fileList) {
        System.out.println(fileName);
    }
}

源代码

​ 具体详情请查看源代码

​ daisy-RMI实现GFS:https://gitee.com/maogen_ymg/daisy


系统整体介绍、背景以及设计信息,尽在其他篇章:

介绍篇:https://www.cnblogs.com/maogen/p/gfs_0.html

背景与设计篇:https://www.cnblogs.com/maogen/p/gfs_1.html

作者:晨星1032-博客园:https://www.cnblogs.com/maogen/

posted @ 2021-02-13 21:18  晨星1032  阅读(1067)  评论(0编辑  收藏  举报