内存文件目录树,在一些开源的分布式存储系统中,有时也叫做Namespace或者元数据。它是NameNode节点的一种记录客户端文件操作命令的抽象数据结构,维护在NameNode的自身内存中。
在上一章《dfs客户端》中,我提到NameNodeRpcServer在接受到Client的文件操作RPC请求后,会委托给 FSNameSystem 处理:
public class NameNodeServiceImpl extends NameNodeServiceGrpc.NameNodeServiceImplBase {
// 负责管理元数据的核心组件
private FSNameSystem namesystem;
/**
* 创建目录
*/
@Override
public void mkdir(MkDirRequest request, StreamObserver<MkDirResponse> responseObserver) {
try {
MkDirResponse response = null;
if (!isRunning) {
response = MkDirResponse.newBuilder().setStatus(STATUS_SHUTDOWN).build();
} else {
this.namesystem.mkdir(request.getPath());
response = MkDirResponse.newBuilder().setStatus(STATUS_SUCCESS).build();
}
responseObserver.onNext(response);
responseObserver.onCompleted();
} catch (Exception e) {
e.printStackTrace();
}
}
}
FSNameSystem 就是NameNode进行元数据管理的核心组件,本章,我就来实现FSNameSystem。
一、元数据管理
FSNameSystem内部包含了两个核心组件:
- FSDirectory:负责维护元数据,即内存文件目录树;
- FSEditlog:负责管理edits log日志。
/**
* 负责管理元数据的核心组件
*/
public class FSNameSystem {
// 负责管理内存文件目录树的组件
private FSDirectory directory;
// 负责管理edits log写入磁盘的组件
private FSEditlog editlog;
// 最近一次checkpoint更新到的txid
private long checkpointTxid = 0L;
public FSNameSystem() {
this.directory = new FSDirectory();
this.editlog = new FSEditlog();
recoverNamespace();
}
/**
* 创建目录
*
* @param path 目录路径
* @return 是否成功
*/
public Boolean mkdir(String path) throws Exception {
this.directory.mkdir(path);
this.editlog.logEdit("{'OP':'MKDIR','PATH':'" + path + "'}");
return true;
}
//...
}
可以看到,FSNameSystem把对内存文件目录树的操作委托给了FSDirectory
。
关于FSEditlog组件和edits log日志,我会在后续章节详细讲解,本章我们只关注内存文件目录树。
1.1 数据结构
我们需要先考虑下内存文件目录树的结构是怎么样的?内存文件目录树,本质是一个父子层级关系的数据结构,比如:
/root
/usr
/local
/app
/home
/kafka
/data
/access.log
对目录/文件进行增删改,本质都是在更新该内存里的数据结构,所以我们定义一个INode类,代表文件目录树中的一个节点:
/**
* 代表文件目录树中的一个目录
*/
public static class INode {
private String path;
private List<INode> children;
public INode() {
}
public INode(String path) {
this.path = path;
this.children = new LinkedList<INode>();
}
public void addChild(INode inode) {
this.children.add(inode);
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public List<INode> getChildren() {
return children;
}
public void setChildren(List<INode> children) {
this.children = children;
}
@Override
public String toString() {
return "INode [path=" + path + ", children=" + children + "]";
}
}
1.2 FSDirectory
定义完文件目录树的基本结构后,我们还需要一个组件 FSDirectory 对文件和目录进行封装,并暴露一些对外的接口。我这里只实现了mkdir
接口,你们可以根据业务需要实现各种其它维护文件树的接口:
/**
* 管理文件目录树的核心组件
*/
public class FSDirectory {
// 内存中的文件目录树
private INode dirTree;
public FSDirectory() {
// 初始化时只有根节点
this.dirTree = new INode("/");
}
/**
* 创建目录
*
* @param path 目录路径, eg: path = /usr/warehouse/hive
*/
public void mkdir(String path) {
synchronized (dirTree) {
// eg: /usr/warehouse/hive -> ["","usr","warehosue","hive"]
String[] pathes = path.split("/");
INode parent = dirTree;
for (String splitedPath : pathes) {
if (splitedPath.trim().equals("")) {
continue;
}
INode dir = findDirectory(parent, splitedPath);
if (dir != null) {
parent = dir;
continue;
}
INode child = new INode(splitedPath);
parent.addChild(child);
parent = child;
}
}
// printDirTree(dirTree, "");
}
/**
* 查找子目录
* <p>
* 在dir目录下查找子目录path
*/
private INode findDirectory(INode dir, String path) {
if (dir.getChildren().size() == 0) {
return null;
}
for (INode child : dir.getChildren()) {
if (child instanceof INode) {
INode childDir = (INode) child;
if ((childDir.getPath().equals(path))) {
return childDir;
}
}
}
return null;
}
public INode getDirTree() {
return dirTree;
}
public void setDirTree(INode dirTree) {
this.dirTree = dirTree;
}
}
上面需要特别注意的一点是,我使用了synchronized
对内存文件目录树进行加锁,以防止并发修改时出现问题。
二、总结
本章,我对NameNode中的文件目录树结构进行了讲解,同时给出了一个比较简单的实现,很多分布式文件系统的实现思想都是类似的。下一章开始,我将对最核心的edits log日志进行讲解。