An open API service indexing awesome lists of open source software.

https://github.com/treasersimplifies/tinydfs

Tiny Distributed File System, simple implementation of HDFS/GFS.
https://github.com/treasersimplifies/tinydfs

Last synced: 3 months ago
JSON representation

Tiny Distributed File System, simple implementation of HDFS/GFS.

Awesome Lists containing this project

README

        

# Tiny Distributed File System
It is a simple distributed file system, implemented using golang, under referrence of GFS(Google File System) and HDFS(Hadoop Distributed File System). It's a curriculum homework —— distributed system design.

The demenstration of the TDFS is on BiliBili: https://www.bilibili.com/video/av75265326

整个系统的演示视频位于:https://www.bilibili.com/video/av75265326

## Requirements
实现一个分布式系统(而非基于分布式系统的应用),其能提供分布式通信能力;提供分布式并行计算能力;提供分布式缓存能力;提供分布式文件处理能力等。要求编程实现,编程语言不限。整个系统不要求一定全部实现,如果不能实现,详细说明难点和缘由。

## Abstract/Overview
Tiny Distributed File System(以下简称TDFS)是我所设计的分布式系统的名称。其实现了一个简单的分布式文件系统。从功能上讲,TDFS实现了在分布式文件系统中的文件存储、文件读取、文件删除三项最基本的文件操作功能。从架构上讲,TDFS与HDFS/GFS有着类似的Master/Slave架构,TDFS包括客户端Client、名称节点NameNode(以下简称NN)、数据节点DataNode(以下简称DN)三部分构成。从编程实现上讲,我使用Go语言编写了TDFS,并且使用了Go语言中的Gin框架来编写NN、DN服务器的后端。**整个TDFS项目一共有1200余行代码,皆为自主编写。**

## 1 架构设计
### 1.1 基本架构
TDFS使用主从式(Master/Slave)架构。一个集群分为名称节点NN和多个数据节点DN。NN存放了文件目录和TDFS的全部控制信息。而DN存放所有文件的块副本数据及其哈希。客户端通过NN读取相关信息并通过NN与DN交互通信(这一点与HDFS不同)。

其中文件存储实现了分块存储:文件通过客户端发送给名称节点NN,然后NN对文件切分成等大小的文件块,然后把这些块发给特定的DN进行存储。而且还实现了冗余存储(TDFS的默认冗余系数是2):一个文件块会被发给多个DN进行存储,这样在一份文件块数据失效之后,还有一块备份。冗余存储也是分布式文件系统中标配的。DN上不仅会存储文件块数据,还会存储文件块数据的哈希值(TDFS默认使用sha256哈希函数)。

另外文件读取实现了数据完整性检测。被分布式存储的文件在读取时NN会去相应的DN上进行读取文件块副本数据。然后NN会对返回的文件块副本数据进行校验和(Checksum)检测,即对其数据内容进行哈希计算,然后和存储在DN上的文件哈希进行对比,如果不一致说明数据块已经遭到破坏,此时NN会去其他DN上读取另一个块副本数据。

文件删除就是将本文件所分布在各个DN服务器上的数据块清空。

文件存储、文件读取、文件删除是文件最基础的操作。后续的文件追加(append)、文件覆盖写(write)都可以由这三种操作组合来实现。

### 1.2 存储文件(Put)流程
TDFS的存储文件的流程如下图所示:
![](Pics/TDFS_Put.jpg)

### 1.3 读取文件(Get)流程
TDFS的读取文件的流程如下图所示:
![](Pics/TDFS_Get.jpg)

### 1.4 删除文件(Del)流程
TDFS的删除文件的流程如下图所示:
![](Pics/TDFS_Del.jpg)

## 2 代码实现
### 2.1 项目结构
在TDFS的项目下所有文件及文件夹如下所示:

```shell
├── Client.go // 调用src/client.go里面的函数来模拟一个TDFS的Client
├── DN1.go // 调用src/datanode.go里面的函数来启动一个TDFS的DataNode
├── DN2.go // 调用src/datanode.go里面的函数来启动一个TDFS的DataNode
├── DN3.go // 调用src/datanode.go里面的函数来启动一个TDFS的DataNode
├── NN.go // 调用src/namenode.go里面的函数来启动一个TDFS的DataNode
├── DataNode1 // 在本地伪分布式演示时DN1的工作目录
│   └── achunkhashs // DN的工作目录下存储文件块数据哈希值的目录
├── DataNode2 // 在本地伪分布式演示时DN2的工作目录
│   └── achunkhashs
├── DataNode3 // 在本地伪分布式演示时DN3的工作目录
│   └── achunkhashs
├── NameNode // 在本地伪分布式演示时NN的工作目录
├── TDFS.md // TDFS系统说明
├── TDFSLog.txt // TDFS系统的日志
└── src // TDFS源码,需将tdfs目录拷贝到$GOPATH/src下面让整个Go工程跑起来
└── tdfs
├── client.go // client相关的所有操作
├── datanode.go // datanode相关的所有操作
├── namenode.go // namenode相关的所有操作
├── tdfslog.go // 系统日志的定义
├── config.go // 系统的所有数据结构定义、参数相关
└── utils.go // 文件操作的一些工具函数
```

### 2.2 客户端 Client
**客户端需要支持的操作:**

1. 新建文件(putfile):读取自己本地的文件,将文件数据发给NN。
2. 读取文件(getfile):传送文件名,从NN处获取文件。
3. 删除文件(delfile):传送文件名,让NN处删除文件。

**客户端需要管理的数据:**

1. NameNode的位置(域名/IP地址,端口);
2. 其他数据。

**数据结构:**
因此,客户端的数据结构定义(位于config.go)如下:
```go
type Client struct{
NameNodeAddr string
Mode int
}
```

**函数或服务:**
客户端的主要操作定义如下(位于client.go):
```go
func (client *Client) SetConfig(nnaddr string) // 配置Client的数据
func (client *Client) PutFile(fPath string) // 实现向NN发起存储文件的网络请求
func (client *Client) GetFile(fName string) // 实现向NN发起获取文件的网络请求
func (client *Client) DelFile(fName string) // 实现向NN发起删除文件的网络请求
func (client *Client) uploadFileByMultipart(fPath string)// 上传文件至服务器的一种方式
```

PutFile、GetFile、DelFile函数分别实现了客户端需要支持的新建、读取、删除文件的操作。其中PutFile调用了uploadFileByMultipart来进行文件传输。Multipart是Go语言Gin框架下服务器接收文件的一种方式。

### 2.3 名称节点 NameNode
**名称节点需要支持的操作:**

1. 新建文件(putfile):接收Client发送过来的文件数据;将文件分块;从DN的存储空间中挑选出空闲块以进行数据存储;将块数据、挑选出来的块位置发给挑选出来的多个DN进行putchunk请求(一个空闲块的定位是:DN位置+该块在DN上的编号,比如http://localhost:11091/chunk-0 )。
2. 读取文件(getfile):接收Client发送过来的文件名;根据该文件名找到其各个文件块所在的位置;向这些块副本所在的DN发起读取相应块的请求(getchunk)。
3. 删除文件(delfile):接收Client发送过来的文件名;根据该文件名找到其各个文件块所在的位置;向这些块副本所在的DN发起删除相应块的请求(delchunk)。

**名称节点需要管理的数据:**

1. 每个数据节点所有信息;
2. 整个TDFS的目录(暂时只支持一级目录),存储文件的信息:文件名映射到文件存储地址、最后一个块的偏移量等;
3. 其他数据。

**数据结构:**

因此,NN的数据结构定义(位于config.go)如下:
```go
type NameNode struct{
NameSpace NameSpaceStruct
Location string
Port int
DNNumber int
DNLocations []string
DataNodes []DataNode
NAMENODE_DIR string
REDUNDANCE int
}
```

其中的```NameSpaceStruct```数据类型是我定义的名称空间类型,定义如下:
```go
type NameSpaceStruct map[string]File
type File struct{
Info string
Length int
Chunks []FileChunk
Offset_LastChunk int
}
type FileChunk struct{
Info string
ReplicaLocationList [REDUNDANCE]ReplicaLocation
}
type ReplicaLocation struct{
ServerLocation string
ReplicaNum int
}
```

这里的```File```类型是用来描述一个存储在TDFS中的文件的,其最重要的属性是```Chunks```,也就是文件数据所存储的块的信息。

```Chunks```的类型是```[]FileChunk```。```FileChunk```是一个是用来描述一个文件块的信息的,其最重要的属性是```ReplicaLocationList```。

```ReplicaLocationList```是用来描述文件块的多个副本的信息的,其类型是```[REDUNDANCE]ReplicaLocation```。其中```REDUNDANCE```是一个常数,就是TDFS的冗余系数,默认是2。而副本信息由```ReplicaLocation```类型进行了描述。

**函数或服务:**

作为服务器,namenode需要支持一些路由服务:
```go
router.POST("/putfile", func(c *gin.Context){...} // 注册接受客户端存储文件的服务
router.GET("/getfile/:filename", func(c *gin.Context){...} // 注册接受客户端读取文件的服务
router.DELETE("/delfile/:filename", func(c *gin.Context){...} // 注册接受客户端删除文件的服务
```

namenode.go中还包括了很多函数:
```go
func (namenode *NameNode) SetConfig(location string, dnnumber int, redundance int, dnlocations []string) // 配置NN的数据
func (namenode *NameNode) Reset() // 重置整个NN服务器
func (namenode *NameNode) GetDNMeta() //向所有DN请求元数据以便更新自身的元数据
func (namenode *NameNode) ShowInfo() //把NameNode的内容全部打印出来
func PutChunk(dataChunkPath string, chunkNum int, replicaLocationList [REDUNDANCE]ReplicaLocation) // 存某个文件的某个Chunk
func (namenode *NameNode) GetChunk(file File, filename string, num int)// 取某个文件的某个Chunk
func (namenode *NameNode) DelChunk(file File, filename string, num int)// 删某个文件的某个Chunk
func (namenode *NameNode) Run() // 启动NN来提供服务
func (namenode *NameNode) AllocateChunk() (rlList [REDUNDANCE]ReplicaLocation) // 为文件块分配存储的位置
func (namenode *NameNode)AssembleFile(file File, filename string) ([]byte) // 将读到的多个文件块数据重新组合成一整个文件
func (namenode *NameNode) recvFrom(c *gin.Context) (string) // 从客户端传来的数据中进行读取
```

### 2.4 数据节点 DataNode
**数据节点需要支持的操作:**

1. 存储块数据(putchunk):按照NN发过来的{块位置, 块数据}进行存储。
2. 读取块数据(getchunk):按照NN发送过来的{块位置}返回块数据。
3. 删除块数据(delchunk):按照NN发送过来的{块位置}进行块数据清空。

**数据节点需要管理的数据:**
这里数据节点存储的信息其实在名称节点里是有的,只是防止其宕机而做的冗余。

1. 空闲块表及块相关数据;
2. 其他信息。

**数据结构:**
因此,DN的数据结构定义(位于config.go)如下:
```go
type DataNode struct{
Location string `json:"Location"`
Port int `json:"Port"`
StorageTotal int `json:"StorageTotal"`
StorageAvail int `json:"StorageAvail"`
ChunkAvail []int `json:"ChunkAvail"`
LastEdit int64 `json:"LastEdit"`
DATANODE_DIR string `json:"DATANODE_DIR"`
}
```

**函数或服务:**
作为服务器,datanode需要支持一些路由服务(位于datanode.go):

```go
router.POST("/putchunk", func(c *gin.Context) {...} // 注册存储块数据的服务
router.GET("/getchunk/:chunknum", func(c *gin.Context) {...} // 注册读取块数据的服务
router.GET("/getchunkhash/:chunknum", func(c *gin.Context) {...} // 注册存储块数据哈希值的服务
router.DELETE("/delchunk/:chunknum", func(c *gin.Context) {...} // 注册删除块数据的服务
router.GET("/getmeta",func(c *gin.Context){...} // 注册获取DN元数据的服务(NN需要用到)
```

datanode.go中还包括了很多函数:

```go
func (datanode *DataNode) SetConfig(location string, storageTotal int) // 配置DN服务器
func (datanode *DataNode) Reset() // 重置整个DN服务器
func (datanode *DataNode) ShowInfo() //把NameNode的内容全部打印出来
func (datanode *DataNode) RecvChunkAndStore(ReplicaList []ReplicaLocation, chunkData ChunkUnit) //将接受到的块数据存储下来
```

### 2.5 接口
从以上的内容可以看出,在指令流传输上,名称节点和数据节点都实现了RESTful API(GET、POST、DELETE)接口。客户端与NN通信、NN与DN和客户端通信都是通过http方式。

在用户使用上,Client通过go语言的flag包提供了命令行操作,操作非常简单:
```shell
// 存储:
go run TinyDFS/Client.go -putfile "AFile.txt"
// 读取:
go run TinyDFS/Client.go -getfile "BFile"
// 删除:
go run TinyDFS/Client.go -delfile "CFile"
```

### 2.6 一致、容错&冗余、日志
**一致:**
由于时间有限,当前的TDFS还没有设计用户管理模块。计划先实现单用户模式,用单用户模式来保证一致性问题。

**容错与冗余:**
在容错与冗余上,首先TDFS中文件的块数据会存储到多个DN上,进行副本备份,这个前面也提到过来。在Putchunk函数(namenode.go)中如下代码实现存储时冗余:

```go
for i:=0; i