本文主要是使用Java RMI 实现一个简单的GFS(谷歌文件系统,google file system),这里提供演示运行视频、系统实现以及源代码相关。
大年初二,走亲访友
祝大家新年快乐!
ʰᵅᵖᵖʸ ⁿeᵚ ʸᵉᵅʳ
家人闲坐 灯火可亲
辞旧迎新 新年可期
系统整体介绍、背景以及设计信息:
演示运行视频
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是否在线。具体操作如下:
通过RMI方式来检测Chunk服务器的心跳,直接以try-catch方式判断。若服务器宕机则加入failedChunkServerList中。
检查正常ChunkServer上的所有Chunk的Hash值,若不一致则加入到Chunk失败列表中。
最后进行错误处理。
public synchronized void heartbeatScan() {
System.out.println("heartbeat checking...");
// 错误Chunk列表
Map> failedChunkMap = new LinkedHashMap<>();
// 错误Server列表
List failedChunkServerList = new ArrayList<>();
ChunkServerApi chunkServerApi;
Map 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 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> failedChunk : failedChunkMap.entrySet()) {
String serverName = failedChunk.getKey();
List 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. ChunkServer模块
3.1 内存命中机制
public class ChunkServerMemory {
private final LinkedList 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服务器上的存储。
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 chunkInfoList = masterApi.getChunkInfos(fileName);
for(ChunkInfo chunkInfo : chunkInfoList) {
output.write(downloadChunk(chunkInfo.getChunk(), chunkInfo.getFirstReplicaServerName()));
}
output.close();
return fileAddr;
}
4.3 追加
每一个Chunk默认最大为64Mb,追加操作需要对最后一个Chunk的剩余空间进行判断:
若最后一个Chunk剩余空间 > 所追加文件大小,则直接添加最后一个即可。
若最后一个Chunk剩余空间 < 所追加文件大小,则首先将最后一个Chunk空间加满,然后再新建Chunk直到 > 所追加文件大小
public void appendFile(String fileName, String appendFileAddr) throws Exception {
List 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 fileList = masterApi.getFileList();
if(fileList.size() == 0) {
System.out.println("空");
}
for(String fileName : fileList) {
System.out.println(fileName);
}
}
源代码
具体详情请查看源代码
系统整体介绍、背景以及设计信息,尽在其他篇章: