Assiduous-donkey / Distributed-File-System

使用Go语言实现的简单分布式文件系统

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

分布式文件系统

项目规划————初始版本

项目框架

采用客户端-服务器模型,设置目录服务器和文件服务器。目录服务器记录文件的所在服务器,文件服务器存储独立文件,向客户端提供整个文件。客户端请求文件时首先访问目录服务器,获取文件当前的位置,再根据取得的位置访问文件服务器请求对应的文件。每个文件服务器配备一个备份服务器,备份服务器存储它对应的主文件服务器上的文件,使文件有多个副本,且仅与它对应的主文件服务器交互,以确保数据的可用性。主文件服务器与客户端交互,提供基础的文件操作。

实现要点

目录服务器

目录服务器需要记录的消息有:

  1. 文件路径:文件所在服务器

  2. 文件路径:最后修改时间

需要执行的操作有:

  1. 创建目录:接收到客户端的创建目录命令时先检查有没有该目录,有则返回“目录已存在”,无则向文件服务器发送创建目录的命令,等待服务器的响应,若文件服务器返回错误信息则将错误信息转发给客户端;若文件服务器返回成功消息则创建目录路径到服务器的键值对。(应该考虑超时机制——服务器失效)

  2. 创建文件:接收到客户端的命令之后检查有无该文件,有则返回“文件已存在”,否则向文件服务器发送创建文件的命令,等待服务器响应,若文件服务器返回错误信息则将错误信息转发给客户端;若文件服务器返回成功消息则创建文件路径到服务器的键值对。(应该考虑超时机制——服务器失效)

  3. 读文件:接收到客户端的命令之后检查有无该文件,若文件不存在则返回“文件不存在”,否则返回给客户端文件对应的服务器地址以及该文件的最后修改时间

  4. 写文件:接收到客户端的命令之后检查有无该文件,若文件不存在则返回“文件不存在”,否则返回给客户端文件对应的服务器地址以及该文件的最后修改时间

  5. 删除文件:接收到客户端的命令之后检查有无该文件,若文件不存在则返回“文件不存在”,否则向文件服务器发送删除文件的命令,等待服务器响应,若文件服务器返回错误信息则将错误信息转发给客户端;若文件服务器返回成功消息则删除该文件对应的键值对。(应该考虑超时机制——服务器失效)

  6. 删除目录:接收到客户端的命令之后检查有无该目录,若文件不存在则返回“目录不存在”,否则向文件服务器发送删除目录的命令,等待服务器响应,若文件服务器返回错误信息则将错误信息转发给客户端;若文件服务器返回成功消息则删除该目录对应的键值对。(应该考虑超时机制——服务器失效)

客户端

客户端初始时处于根目录,使用命令可切换到其他目录下,所以客户端要时刻记录自己当前所处的目录。 客户端添加本地缓存,若本地缓存的文件已经是服务器上的最新文件,则没必要再下载。 因此本地缓存要保存文件路径对应的最后修改时间。 最好建一个目录专门缓存这些文件,文件名修改为文件在文件系统中的路径,其中“/”用下划线“_”代替

  1. 创建目录:发送命令给目录服务器,等待目录服务器响应。

  2. 创建文件:发送命令给目录服务器,等待目录服务器响应。

  3. 读文件:发送命令给目录服务器,等待目录服务器响应。接收到返回的信息后,查看本地缓存,若该文件有本地缓存且缓存的最后修改时间与目录服务器返回的时间一致,则直接读取本地缓存即可,不必再向文件服务器发请求;若有缓存但目录服务器返回的最后修改时间比缓存的修改时间新,或者本地没缓存,则向文件服务器发送请求请求获取文件。

  4. 写文件:发送命令(write)给目录服务器,等待目录服务器响应。接收到正确的返回消息后,向文件服务器发送写文件请求,若成功则可下载到要写的文件以及写锁的钥匙,缓存下来。在本地写好文件后向文件服务器发送命令(upload)上传文件,发送upload要带上解开写锁的钥匙(这个钥匙用于区别不同用户的提交操作)。

  5. 删除文件:发送命令给目录服务器,等待目录服务器的响应。

  6. 删除目录:发送命令给目录服务器,等待目录服务器的响应(若目录中有文件,删除其中所有文件)

服务端

服务器端除了要存储文件外还要实现对文件加锁的功能。 加锁功能需要缓存文件的写锁对应的钥匙。

  1. 创建目录:接收到目录服务器的请求后在本地创建目录,返回成功或失败消息给目录服务器。

  2. 创建文件:接收到目录服务器的请求后在本地创建目录,返回成功或失败消息给目录服务器。

  3. 读文件:接收客户端发送的请求,为文件加读锁然后将文件传输给客户端。

  4. 写文件:接收客户端发送的请求,收到write命令时,若文件本身有写锁,则拒绝客户端的请求;若无则加写锁,然后发送文件给客户端并生成一个写锁的钥匙发送给客户端。收到upload命令后匹配客户端的钥匙是否可以打开写锁,不可则返回错误信息,可以则更新文件,返回成功信息。然后发送文件的最后修改时间给目录服务器,最后释放锁。

  5. 删除文件:删除本地文件后返回信息给目录服务器

  6. 删除目录:删除目录后返回信息给目录服务器

锁机制

  1. 读锁:对于读操作,设计思路是可以多用户并行读,由于os.Open可以同时打开相同文件,所以并行读时好像也不需要用到读锁。但需考虑在服务器更新文件时有用户发起读请求,这时要阻塞用户。或者服务器要更新文件时,若有用户正在读,则等待用户读完(先设计为读者优先)。所以读锁是加在服务器读取文件的时候。

  2. 写锁:对于写操作,需要两个步骤。服务器接受到write请求时加写锁并生成钥匙返回给客户端,客户端upload的时候再携带钥匙来打开写锁更新文件,然后锁被释放。写操作需要互斥,一个文件只允许有一把锁,可以设置超时机制,若超时仍未收到upload则释放锁。

通信方式

节点之间都采用RPC方式通信:客户端——目录服务器、目录服务器——文件服务器、客户端——文件服务器、文件服务器——备份服务器

一致性

容错

主服务器失效时由备份服务器顶替成为主服务器,顶替过程中不可被用户访问。失效服务器重启后成为备份服务器,与主文件服务器进行数据同步,这一过程也不可被用户访问。 问题在于怎么判断主文件服务器失效。一个简单的方法是若对于该服务器的连续N(一个阈值)个请求都是无效的,则认为该服务器失效,为此可以在目录服务器上添加记录,记录该服务器上连续的无效请求次数。

安全性

  1. 目录服务器单点失效,可以为目录服务器也提供一个备份服务器。

  2. 目录服务器上的记录可以存缓存和磁盘,因此可以采用Redis数据库,Redis运行在内存中但可以将数据持久化到磁盘上。存到磁盘上可以防止掉电丢失等意外情况。

文件存储

采用目录结构,遵从以下规则:

  1. 同一目录下不能有同名文件

  2. 不能有同名目录

交互方式

客户端在控制台运行客户端程序之后通过输入指令来执行操作,命令格式如下:

  1. mkdir 目录名 ———— 创建目录

  2. cd 目录名 ———— 切换目录

  3. cd .. ———— 返回上一级

  4. create 文件名(可包含路径) ———— 新建文件

  5. write 文件名(包含路径) ———— 写文件,实则是下载

  6. upload 文件名(可包含路径) ———— 上传文件 (这是write的后续操作)

  7. read 文件名(可包含路径) ———— 下载文件

  8. rm 文件名(可包含路径) ———— 删除文件

  9. rm-all 目录名 ———— 删除整个目录包括里面所有文件

项目规划————终版

之前计划要做的,看起来更像是一个文件系统应用,而不是简单的文件系统。作为一个文件系统,只需要提供底层的操作即可,其他看起来功能更丰富的做法由应用程序封装即可。需要实现的,比如write函数,写文件,需要提供文件句柄。就像我们自己编程时调用文件操作的接口一样,我们自己也要自定义一套文件操作接口。

自定义文件接口

  1. 创建文件:CreateFile

    • 客户端:需要提供文件名参数,传输给文件服务器

    • 服务器:根据文件名创建文件,返回创建信息给客户端

  2. 读文件:ReadFile

    • 客户端:需要提供文件名以及读取信息的缓冲区,传输给文件服务器

    • 服务器:文件服务器使用文件名打开本地文件,然后返回整个文件给客户端,客户端缓存文件然后再在本地调用读文件函数读取所需信息。

  3. 写文件:WriteFile

    • 客户端:需要提供文件名、要写入的信息以及写入的模式(如覆盖写或者追加写),传输信息给服务器

    • 服务器:文件服务器在本地按照指定模式打开文件,然后写入信息,返回写入情况给客户端。执行写操作时对文件加锁。

  4. 删除文件:DeleteFile

    • 客户端:需要提供文件名,传输给服务器

    • 服务器:根据文件名删除本地文件

具体实现

实现四个功能:创建文件、读文件、写文件和删除文件。 创建文件和删除文件,是客户端与目录服务器交互,目录服务器再与文件服务器交互 读文件和写文件,是客户端与目录服务器交互,客户端再与文件服务器交互

目录服务器的操作

  1. 创建文件:接收到客户端的命令之后检查有无该文件,有则返回“文件已存在”,否则向文件服务器发送创建文件的命令,等待服务器响应,若文件服务器返回错误信息则将错误信息转发给客户端;若文件服务器返回成功消息则创建文件路径到服务器的键值对。(应该考虑超时机制——服务器失效)

  2. 读文件:接收到客户端的命令之后检查有无该文件,若文件不存在则返回“文件不存在”,否则返回给客户端文件对应的服务器地址以及该文件的最后修改时间

  3. 写文件:接收到客户端的命令之后检查有无该文件,若文件不存在则返回“文件不存在”,否则返回给客户端文件对应的服务器地址以及该文件的最后修改时间

  4. 删除文件:接收到客户端的命令之后检查有无该文件,若文件不存在则返回“文件不存在”,否则向文件服务器发送删除文件的命令,等待服务器响应,若文件服务器返回错误信息则将错误信息转发给客户端;若文件服务器返回成功消息则删除该文件对应的键值对。(应该考虑超时机制——服务器失效)

文件服务器的操作

服务器端除了要存储文件外还要实现对文件加锁的功能。 加锁功能需要缓存文件的写锁对应的钥匙。 每个文件要生成最新版本的时间戳

  1. 创建文件:接收到目录服务器的请求后在本地创建文件,返回成功或失败消息给目录服务器。

  2. 读文件:接收客户端发送的请求,为文件加读锁然后将文件传输给客户端。

  3. 写文件:接收客户端发送的请求,包括文件名、写入模式和要写入的字节。服务器在本地打开文件后写入信息,返回要写入情况给客户端

  4. 删除文件:删除本地文件后返回信息给目录服务器

客户端的操作

客户端添加本地缓存,若本地缓存的文件已经是服务器上的最新文件,则没必要再下载。 因此本地缓存要保存文件路径对应的最后修改时间。

  1. 创建文件:发送命令给目录服务器,等待目录服务器响应。

  2. 读文件:发送命令给目录服务器,等待目录服务器响应。接收到返回的信息后,查看本地缓存,若该文件有本地缓存且缓存的最后修改时间与目录服务器返回的时间一致,则直接读取本地缓存即可,不必再向文件服务器发请求;若有缓存但目录服务器返回的最后修改时间比缓存的修改时间新,或者本地没缓存,则向文件服务器发送请求请求获取文件。

  3. 写文件:发送命令给目录服务器,等待目录服务器响应。接收到返回的信息后,向文件服务器发送写文件请求。

  4. 删除文件:发送命令给目录服务器,等待目录服务器的响应。

文件锁机制

本来想用go的syscall库的方法,但里面的锁机制只适用于Linux环境,所以我改用将锁写进redis的方法。 但实践发现,将锁写进redis需要时间,可能会出现这种情况: 进程1正在写入锁 进程2在进程1还没写入锁的时候查看redis发现没有锁,故继续访问 因此应该将锁检测和加锁封装成原子操作 最终采用的是redis的setnx操作————互斥set,使用该操作用于设置写锁

  1. 读锁:读文件过程中加读锁,加读锁之前检查当前该文件是否有写锁,若有则不可读。 具体流程:(类似信号量机制)

    • 检查是否有写锁 有则退出

    • 检查是否有写者在等待 有则退出

    • 设置读锁 用事务的模式 互斥设置读锁 避免读者数量设置不正确。 先用开启事务,然后检查该文件的读锁是否存在,不存在则设置读锁的值为1 否则设置读锁为读取到的值加1

    • 修改读锁 文件读完后让文件对应的读锁的值减1

  2. 写锁:写文件过程中加写锁,加锁前检查当前该文件是否有写锁,有则不可写。 具体流程:

    • 检查是否有写锁 有则退出

    • 设置写锁

    • 设置“不可读”锁,制止其他进程请求读取该文件

    • 检查是否有读锁 有则等待读者全部读完 然后执行写操作

    • 释放写锁以及“不可读”锁

备份服务器

  1. 只与对应的主文件服务器交互,采用RPC通信

  2. 三个保持一致性的操作:创建文件、删除文件以及写文件。且所有操作都优先于主文件服务器执行

    • 创建文件:由主文件服务器调用备份服务器上的创建文件RPC函数

    • 删除文件:由主文件服务器调用备份服务器上的删除文件RPC函数

    • 写文件:主文件服务器如何写文件,备份文件服务器也如何写文件

  3. 一个用于容错的操作:读文件。若主文件服务器某个文件A丢失,但在目录服务器上仍可查询到该文件,则用户访问主文件服务器时若查找不到文件,则去备份服务器查

  4. 备份服务器不需要关注互斥操作,因为只要主文件服务器是互斥的,备份服务器也必定互斥

目录服务器的调度

采用轮询调度,在已有的文件服务器列表中依次选取服务器返回给客户端,使得文件平均分配在各文件服务器上

About

使用Go语言实现的简单分布式文件系统


Languages

Language:Go 100.0%