简介:基于Hadoop实现的百度云盘毕设项目,包含完整源代码与配套文档,适合计算机相关专业在校学生、毕业设计者及大数据初学者学习参考。资源包共2000个文件,压缩后77.11MB,涵盖Java源码、JSP动态页面、JS/CSS前端交互样式、HTML页面及JAR依赖库等,源码经过测试运行成功,可直接用于课程设计、项目演示或二次开发。已有535人学习下载,资源按Eclipse工程结构组织,内置README说明文档,并配有部署说明,方便快速理解项目模块与运行流程。借助该资源,读者既能掌握Hadoop分布式存储与大文件分块上传下载的实现思路,也能借鉴一个完整Web系统从后端调度到前端展示的交互与权限管理设计,适合作为毕设修改和大数据实战入门的参考资料。
1. 基于Hadoop的百度云盘:拿HDFS当网盘底座到底行不行
做个仿百度云盘的课程设计或实训项目,底层存储不选MySQL也不选MinIO,而是直接压在Hadoop的HDFS上——这个搭配乍一听有点怪,因为HDFS的吞吐模型是为大文件顺序读写设计的,元数据操作能力很弱,恰好踩中网盘系统最敏感的两块:海量小文件和秒级列目录。但换个角度想,网盘真正的刚需是“文件放得下、不丢、能按需取”,这一点HDFS的分布式冗余和流式读写恰好是强项。把文件分块、把元数据抽到独立表、把HDFS目录结构设计得足够扁平,这套方案完全能支撑起一个课程设计级别的完整网盘系统,而且面试时还能顺手讲清楚HDFS的Block、副本策略和NameNode内存模型。这篇文章就把这套方案从架构选型、环境搭建到上传下载代码和踩坑记录完整过一遍,适合正在做Hadoop课程设计、毕业设计,或者想快速搭一个私有网盘练手的人。
2. 架构与选型:把Hadoop塞进网盘系统的四个关键决定
2.1 客户端形态:Web端配HDFS,后端统一封装存储操作
常见做法是Spring Boot做后端、Vue做前端,Hadoop通过HDFS Java API接入。有人会问要不要走WebHDFS的REST接口,我的建议是:课程设计和中小型项目直接上Java API,因为WebHDFS多一层HTTP转发,定位问题时分不清是网络问题还是HDFS问题,而Java API的报错信息直接给到DataNode级别,排查起来清楚得多。桌面端不是不能做,但要注意客户端机器的Java版本、Hadoop native库和服务器端必须对齐,否则跑起来各种Unsupported class file major version,纯属给自己添堵。
后端封装一个HdfsStorageService,所有Controller只跟这个Service打交道,上层不感知Hadoop的存在。这样后期想把存储层换成MinIO或FastDFS,改动范围控制在一个类里。伪分布式开发阶段,后端服务和NameNode跑在同一台机器,数据本地读写,性能上不会有明显瓶颈。
2.2 元数据设计:文件名直接进HDFS路径,还是单独建元数据表
这是整个项目最核心的选型,直接决定系统能不能撑住网盘的基本操作。网盘要求秒级列目录、改名、移动、搜索,而HDFS的listStatus在目录层级深、文件数量大时延迟明显,NameNode要遍历内存中的文件树,目录一深就慢。所以我的做法是:MySQL建一张file_meta表,存文件ID、用户ID、逻辑路径、文件名、扩展名、大小、上传时间、父目录ID,HDFS上只存物理文件,物理文件名用UUID重命名,落盘路径和用户逻辑目录完全脱钩。
CREATE TABLE file_meta ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, parent_id BIGINT DEFAULT 0 COMMENT '父目录ID,0表示根目录', file_name VARCHAR(255) NOT NULL COMMENT '用户看到的文件名', file_ext VARCHAR(20) DEFAULT '' COMMENT '扩展名,目录则为空', hdfs_path VARCHAR(500) NOT NULL COMMENT 'HDFS物理路径', file_size BIGINT DEFAULT 0, is_dir TINYINT DEFAULT 0 COMMENT '0文件 1目录', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_parent (user_id, parent_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这个表设计里有几个点要注意。parent_id存的是本表自增ID,不是HDFS路径,这样目录改名、移动时只需要更新一行记录,不用动HDFS。hdfs_path存物理路径,用户无论怎么改逻辑目录,物理路径都不变。所有涉及“查目录”“搜文件”的请求全部打在MySQL上,HDFS只承担真正的文件内容读写,两边各干各擅长的事。
2.3 大文件切分:HDFS的Block和网盘分块上传怎么配合
很多人把这两个“块”搞混。HDFS的Block默认128MB,是存储层的数据分布单元,一个文件被切成若干个Block分散到不同DataNode上,这个切分对上层完全透明。而网盘的分块上传是应用层行为,前端把大文件切成几MB一块,逐块传到后端,目的是断点续传和失败重传。两者不是一回事,但可以配合:前端分块上传到后端,后端先落本地临时目录,所有分块到齐后按顺序流式写入HDFS,让HDFS再去切它的Block。
// 分块合并写入HDFS的示意代码 Path target = new Path(hdfsPath); try (FSDataOutputStream out = fs.create(target, true)) { for (File part : sortedParts) { try (FileInputStream in = new FileInputStream(part)) { IOUtils.copyBytes(in, out, 4 * 1024 * 1024, false); } } }这里有一个参数值得留意:fs.create的第二个参数是overwrite,分块合并场景必须传true,因为如果同一任务重试,HDFS上可能已经存在一个半截文件。IOUtils.copyBytes的buffer设成4MB,和前端分块大小对齐,减少磁盘到网络的拷贝次数。所有分块合并完毕后再删除本地临时文件,避免中途失败连底稿都没了。
2.4 命名策略:用UUID拼用户ID,从源头杜绝路径冲突
HDFS物理路径固定为/user/{userId}/files/{uuid}.bin,三段式结构。用户ID做顶层隔离,UUID保证文件名全局唯一,.bin后缀只是占位,真正的文件名、扩展名全在元数据表里。这样设计后,用户随意建同名目录、传同名文件、多层嵌套深目录,都不会产生HDFS路径冲突。
这套策略还有个额外好处:分享功能好做。分享时只需生成一个短链接,把file_meta.id带进去,别人点开链接时后端拿ID查表、拼物理路径、流式输出,HDFS物理路径永远不会暴露给前端。如果直接拿用户原始文件名拼HDFS路径,一旦文件名带特殊字符或路径穿越写法,轻则目录混乱,重则被利用覆盖别人的文件。
3. 环境搭建与目录初始化:先跑通伪分布式,再决定要不要上集群
3.1 基于Ubuntu的Hadoop伪分布式环境配置
课程设计阶段最常见的是Hadoop伪分布式搭建,也就是在一台机器上同时跑NameNode、DataNode和SecondaryNameNode三个进程。网上教程很多,但配置项版本差异极大,我一般按最小必要集来配,核心就三个文件。
# core-site.xml <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> </configuration> # hdfs-site.xml <configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/home/hadoop/data/name</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/home/hadoop/data/data</value> </property> </configuration> # 格式化并启动 hdfs namenode -format start-dfs.sh jpsdfs.replication在伪分布式下必须设成1,因为只有一台DataNode,设成默认3的话副本永远凑不齐,会一直报Not replicated警告。dfs.namenode.name.dir和dfs.datanode.data.dir建议放到Hadoop安装目录之外,因为format操作会清空name目录,放在安装目录里容易误删整个集群数据。jps是验证进程是否启动的最快手段,能看到NameNode、DataNode和SecondaryNameNode三个进程就说明伪分布式起来了。
3.2 HDFS目录结构设计与初始化代码
伪分布式跑通后,先别急着写业务代码,把HDFS的目录骨架建好,后面所有上传下载都往这个结构里填。开发阶段可以用hdfs dfs -mkdir -p手动建,但更推荐在项目启动时用Java代码自动初始化,这样换一台机器部署不用再敲一遍命令。
@Component public class HdfsInitializer implements ApplicationRunner { @Autowired private FileSystem fs; @Override public void run(ApplicationArguments args) throws IOException { Path root = new Path("/user"); if (!fs.exists(root)) { fs.mkdirs(root); fs.setPermission(root, new FsPermission((short) 0777)); } // 预留一个回收站目录,用于软删除 Path trash = new Path("/trash"); if (!fs.exists(trash)) { fs.mkdirs(trash); } log.info("HDFS目录初始化完成"); } }目录骨架就两个:/user放所有用户文件,/trash做软删除中转站。这里有个权限细节,/user给到0777,因为用户ID是动态创建的,注册新用户时再单独mkdirs出来,统一给一次根目录权限能省去每次授权的麻烦。生产环境这么干肯定不行,但课程设计场景下完全够用。
3.3 Hadoop和Zookeeper整合:从伪分布式走向高可用
如果你的项目是毕业设计或者想写进简历的实训作品,建议在伪分布式基础上再叠加Hadoop和Zookeeper整合实战,也就是HA(高可用)模式。原理是让两台NameNode一主一备,Zookeeper负责监控状态,主节点挂掉后自动切换。这个点面试时极加分,因为很多人只会搭伪分布式,能讲清楚HA的人明显少一个身位。
HA搭建的常见做法是两台物理机或虚拟机,各跑一个NameNode和JournalNode,再单独起一台装Zookeeper集群。配置上比伪分布式多出dfs.ha.namenodes、dfs.namenode.shared.edits.dir(JournalNode地址)、dfs.client.failover.proxy.provider这几组参数。需要注意HA模式下不能执行hdfs namenode -format,要先在备节点执行hdfs namenode -bootstrapStandby同步元数据,这一步容易翻车,多留十分钟排错时间。
4. 网盘核心接口实现:上传、下载、列表,三段代码打通全流程
4.1 上传接口:本地临时分块,顺序合并写入HDFS
文件上传是整个网盘最重的链路,我走的路径是:前端切块 → 后端收块落临时目录 → 全部到齐后合并写HDFS → 写元数据表 → 清理临时文件。前端负责把文件切成5MB一块,后端接口接收分块序号和分块数据,存到/tmp/upload/{taskId}/{partIndex},等所有分块到齐后触发合并。
@PostMapping("/upload/merge") public Result<Void> merge(@RequestBody MergeRequest req) throws IOException { // 1. 生成HDFS物理路径,UUID避免重名 String hdfsPath = String.format("/user/%d/files/%s.bin", req.getUserId(), UUID.randomUUID()); Path target = new Path(hdfsPath); // 2. 遍历分块文件,按序号排序后顺序写入 File tempDir = new File("/tmp/upload/" + req.getTaskId()); File[] parts = tempDir.listFiles((dir, name) -> name.endsWith(".part")); if (parts == null || parts.length == 0) { throw new BusinessException("分块文件不存在,请重新上传"); } Arrays.sort(parts, Comparator.comparingInt(f -> Integer.parseInt(f.getName().split("\\.")[0]))); try (FSDataOutputStream out = fs.create(target, true)) { for (File part : parts) { try (FileInputStream in = new FileInputStream(part)) { IOUtils.copyBytes(in, out, 4 * 1024 * 1024, false); } } } // 3. 合并成功后,元数据入库 FileMeta meta = new FileMeta(); meta.setUserId(req.getUserId()); meta.setFileName(req.getFileName()); meta.setHdfsPath(hdfsPath); meta.setFileSize(req.getFileSize()); fileMetaMapper.insert(meta); // 4. 清理本地临时分块 FileUtils.deleteDirectory(tempDir); return Result.ok(); }逻辑说明:合并操作最关键的是分块排序,文件名格式是0.part、1.part这种纯数字前缀,直接按字符串排序也行,但位数不同时10.part会排在2.part前面,所以要用Comparator解析成整数再排。排序错了,文件内容就错位,而且这种错位在写入时不会报错,只有下载打开文件才发现是坏的。FileUtils.deleteDirectory是最后一步,本地临时目录没清干净的话,跑几天磁盘就满了。
参数说明:IOUtils.copyBytes的第四个参数是true表示拷贝结束后关闭流,这里传false是因为out流在循环外统一关,提前关掉后面分块就没法写了。分块大小和buffer对齐到5MB,磁盘读写的缓冲区过大反而导致GC压力,这个体量够用。
4.2 下载接口:流式输出,文件名从元数据表还原
下载接口的核心是“HDFS拿流,HTTP回写”,不能在内存里把整个文件读出来再返回,几十MB的文件就把JVM堆打爆。正确做法是把FSDataInputStream直接接入HttpServletResponse的输出流,边读边写。
@GetMapping("/download") public void download(@RequestParam Long fileId, HttpServletResponse response) throws IOException { FileMeta meta = fileMetaMapper.selectById(fileId); if (meta == null) { throw new BusinessException("文件不存在"); } // 还原用户看到的文件名,处理中文和空格 String fileName = URLEncoder.encode(meta.getFileName(), "UTF-8") .replace("+", "%20"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName); response.setContentType("application/octet-stream"); response.setContentLengthLong(meta.getFileSize()); Path path = new Path(meta.getHdfsPath()); try (FSDataInputStream in = fs.open(path); ServletOutputStream out = response.getOutputStream()) { IOUtils.copyBytes(in, out, 8 * 1024 * 1024, true); } catch (FileNotFoundException e) { log.error("HDFS文件不存在: {}", meta.getHdfsPath()); throw new BusinessException("文件已丢失,请联系管理员"); } }这个接口有两个容易踩的细节。第一个是中文文件名必须URLEncoder编码,否则浏览器下载时中文名直接乱码;第二个是setContentLengthLong要写对,不写的话浏览器下载时没有进度条,看起来像卡住了。IOUtils.copyBytes这里第三个参数传true,让方法内部把两个流都关掉,HDFS的连接用完即释放,避免连接数被占满。
4.3 列表接口:查元数据表,而不是list HDFS
网盘首页打开瞬间要展示目录结构,这个接口如果去调fs.listStatus,每次都要把HDFS的目录树扫一遍,文件多了之后延迟直线上升。正确姿势是查MySQL,一条SQL搞定。
SELECT * FROM file_meta WHERE user_id = #{userId} AND parent_id = #{parentId} ORDER BY is_dir DESC, create_time DESC;is_dir DESC让目录排前面,这是网盘产品的通识,文件按时间倒序排。目录和文件混在同一张表,目录的file_size为0,hdfs_path为空字符串。做重命名和移动时,只需要更新这一行记录的file_name和parent_id,HDFS完全不用动。列表接口响应时间从几百毫秒降到个位数毫秒,这就是元数据分离的价值。
5. 避坑指南:Hadoop做网盘常见的五个坑
5.1 小文件把NameNode内存吃光
现象:上传几百个小文件后,NameNode日志频繁告警,Web UI上的内存曲线飙升,最终整个集群卡死。
原因:NameNode把整个文件系统的元数据都放在内存里,每个文件、每个Block大概占用150字节左右,单个DataNode的Block数量超过上限后集群直接进入安全模式。网盘场景天然产生大量小文件,如果不处理,几百个1KB的文件就能消耗几十MB内存。
解决:方案分三层。第一层,上传入口限制最小分块,小于1MB的文件合并写入,一个物理文件里可以拼多个逻辑文件;第二层,定期用hdfs fsck扫描小文件比例,对历史存量跑一次合并任务;第三层,NameNode堆内存调大,hadoop-env.sh里HADOOP_NAMENODE_OPTS设置-Xmx4g起步,但这是治标,合并才是治本。
5.2 删除文件后空间不释放,回收站里堆满垃圾
现象:用户删了一个1GB的大文件,HDFS的可用空间没有变化,Web UI一看回收站目录越来越大。
原因:HDFS默认开启了回收站机制,fs.trash.interval=0表示不启用,但如果配了非0值,删除操作实际是把文件移到/user/{user}/.Trash目录,不是真删。
解决:确认业务上是否真的需要回收站。网盘的“删除”功能本身有逻辑删除兜底,元数据表里加个deleted字段就够,HDFS层面直接物理删除更干净。在hdfs-site.xml里设置fs.trash.interval=0禁用回收站,或者定时任务执行hdfs dfs -expunge强制清空。
5.3 伪分布式启动时DataNode进程起不来
现象:start-dfs.sh执行后,jps只看到NameNode,没有DataNode,日志里报Incompatible clusterIDs。
原因:这是伪分布式搭建时最经典的翻车现场。第一次hdfs namenode -format生成了一个clusterID,后来因为配置改动又format了一次,NameNode换了新clusterID,但DataNode的数据目录里还留着旧clusterID,两边对不上就拒绝启动。
解决:把dfs.datanode.data.dir配置的目录整个删掉,然后重新执行hdfs namenode -format。注意name目录和data目录都要删干净,只删一个等于没删。这个坑几乎每个搭伪分布式的人都会踩一次,踩过一次就记住了:format之前先看清楚当前目录结构。
5.4 Permission denied:HDFS权限报错排查半天
现象:后端代码调用fs.copyFromLocalFile上报错Permission denied: user=root, access=WRITE,换成命令行hdfs dfs -put却正常。
原因:Java进程以root系统用户运行,映射到HDFS的权限体系里就是root用户,对/user/{userId}目录没有写权限。命令行hdfs dfs默认用当前系统用户,如果之前用hdfs dfs -chmod 777授权过,命令行的权限是够的,但Java进程拿到的用户不同,权限判断就过不去。
解决:两种思路。入门阶段直接在后端代码里设UserGroupInformation.createRemoteUser("hdfs"),把所有HDFS操作统一以hdfs超级用户身份执行,绕过权限判断;进阶做法会给每个用户建独立的HDFS目录并设置owner,走正规授权。课程设计推荐第一种,三行代码解决问题,把时间留给业务功能。
5.5 上传大文件时客户端超时,连接被NameNode掐断
现象:本地局域网传一个5GB的大文件,传到一半抛SocketTimeoutException,再看HDFS上只有个半截文件。
原因:HDFS客户端和DataNode之间的数据传输有超时限制,默认的dfs.client.socket-timeout是60秒,如果某段时间内网络抖动或者DataNode忙着写磁盘,超过60秒没有心跳数据包,连接就被判定超时断开。
解决:适当调大超时参数,同时检查网络环境。
<property> <name>dfs.client.socket-timeout</name> <value>120000</value> </property> <property> <name>dfs.datanode.socket.write.timeout</name> <value>180000</value> </property>顺带说一句,很多人以为上传大文件超时是Hadoop配置问题,调了半天参数发现原来是本地磁盘满了,临时分块落不进去。遇到超时先看客户端所在机器的磁盘空间,再怀疑Hadoop。
6. 验证方案与后续扩展:从“能跑”到“敢说”
项目做完别急着交,先按清单验一遍,避免答辩现场翻车。基础功能清单:单文件100MB上传,确认HDFS上能看到对应Block数量和副本数;并发场景用脚本模拟5个用户同时上传下载,观察NameNode内存和DataNode IO是否平稳;断点续传模拟,传一半断网再恢复,验证分块合并逻辑能正确识别缺失分块并重新拉起。进阶清单:下载一个中文文件名文件,确认浏览器拿到正常文件名;重命名一个非空目录,确认元数据表更新而HDFS物理路径不变;停掉一个DataNode进程,确认集群写入仍然正常。
扩容方向有两个。小文件治理方面,可以做一个后台定时任务,扫描file_meta表里小于1MB的文件,合并成SequenceFile存储,元数据表加一个offset字段记录每个逻辑文件在物理文件里的偏移量。高可用方面,如果用了Hadoop和Zookeeper整合,可以写一个故障切换的验证脚本,手动kill掉Active NameNode进程,观察备节点能否在30秒内接管服务。
最后说个做这个项目最值得养成的习惯:每个向HDFS写入的接口,都先想清楚“如果写了一半挂掉,HDFS上会残留什么”。上传接口残留半截文件,合并接口残留本地分块,覆盖写残留垃圾Block。在代码里写清理逻辑,比出了问题后再跑脚本扫目录省心得多。我早期做这个项目时偷懒没写清理,一周后HDFS被临时文件塞爆,只能一个个目录排查,血泪教训。希望帮到你。
本文还有配套的精品资源,点击获取