简介:这套企业云盘项目源码基于SpringBoot与Hadoop技术栈搭建,面向具备Java基础并希望学习微服务架构与大数据存储结合的开发者。项目覆盖用户注册登录、权限控制、文件上传下载、共享搜索及版本管理等核心模块,融入SpringBoot自动配置、Spring MVC、Actuator监控,以及HDFS、MapReduce、YARN等Hadoop关键机制,适合用于毕业设计参考或企业级云存储实践。压缩包共2210个文件、约7.03MB,主体为2022个SVG图标资源,另含50个Java源文件、28个CSS、21个JS、19个SCSS、18个LESS等前端样式脚本,以及HTML页面、XML/YML配置和字体文件,能清晰区分后端逻辑、前端界面与配置模块;项目还使用了Bootstrap、Font Awesome等前端框架,界面组件齐全。源码目录按服务端、前端、API接口等分层组织,涉及MySQL、Redis等周边技术,便于系统梳理SpringBoot与Hadoop的集成方式。目前已有268人学习下载,对想快速搭建云盘项目并理解分布式存储方案的人有较高参考价值。
1. 这个项目叫什么:一套用 SpringBoot 做门面、Hadoop 做仓库的企业云盘
看到「基于 SpringBoot 与 Hadoop 实现的企业云盘项目源码.zip」这个标题,先别急着下结论说它又是一个毕业设计。把它拆开看,内核其实是一条很实的路线:SpringBoot 负责把文件上传、下载、分享、目录管理这些 Web 能力暴露成 REST 接口,Hadoop 集群则在下层充当对象存储——更准确说是 HDFS 分布式文件系统。这种组合在中小型企业的私有网盘选型里是真实存在的方案,不是玩具项目。
它适合谁?一类是在校生做课程设计和毕设,需要把分布式存储的课设落地成可演示的系统;另一类是中小团队想搭一个内部文件共享平台,又不想直接上 FastDFS 或 MinIO 那样「再多学一套中间件」的方案,于是顺着 SpringBoot 和 Hadoop 这条技术栈走。它能解决的核心问题,是把「收到文件就落本地磁盘」升级成「文件分散存到多台机器、单台挂了不丢数据、容量可以横向扩展」。本文会按我实际搭这套系统时的顺序来讲:存储方案怎么选、核心代码怎么写、环境怎么配、哪些坑最容易让人翻车。
2. 存储选型和整体架构:为什么是 Hadoop 而不是普通磁盘
2.1 HDFS 在企业云盘里的角色:不只是「换个地方存文件」
很多人在设计企业云盘时第一反应是把文件放到服务器的 /data 目录下,然后数据库里记路径。这个方案在单机演示时一点问题都没有,但一旦文件量上来,就会遇到三个绕不开的坎:磁盘空间不够只能靠加盘,单点故障时文件跟着一起丢,多台应用服务器之间文件目录没法共享。HDFS 解决的正是这三点。
HDFS 的角色是「存储底座」。云盘收到的文件写入 HDFS 后,会被切分成 128MB(默认)的块,每个块在集群里存多份副本(默认 3 份)。对用户来说这层是无感知的,SpringBoot 应用层拿到的只是一个org.apache.hadoop.fs.FileSystem对象,调用它的create、open、delete方法,和操作本地文件没什么区别。但在系统内部,文件的块分布在不同 DataNode 上,NameNode 统一维护元数据。这个转化过程,我来画一下我常用的分层:
- 客户端层:浏览器 Web 页面 + SpringBoot 应用,处理上传下载、目录树展示、分享链接
- 接入层:SpringBoot 的 Controller 接收 MultipartFile,做好大小校验、分块组装
- 元数据层:MySQL 存文件列表、目录结构、文件大小、HDFS 路径,相当于给 HDFS 加上「能搜索的目录」
- 存储层:Hadoop 集群,伪分布式(学习环境)或 HA 集群(生产环境)
2.2 伪分布式和集群怎么选:先跑通再横向扩
有 Hadoop 经验的读者应该知道,Hadoop 有单机模式、伪分布式、完全分布式三种部署形态。做企业云盘源码练手,我一般建议从伪分布式起步,也就是一台机器上同时跑 NameNode 和 DataNode。这么做的好处是调试方便,jps一眼就能看到所有进程,SpringBoot 连接 HDFS 时配置fs.defaultFS=hdfs://localhost:9000就能跑通全链路。真正要上生产时再把它升级成多节点的完全分布式集群,核心代码一行都不用改。
伪分布式的配置路径在$HADOOP_HOME/etc/hadoop下,核心是修改core-site.xml和hdfs-site.xml。core-site.xml里设置文件系统访问入口和临时目录;hdfs-site.xml里设置 NameNode 的 HTTP 访问端口和副本数。注意副本数在伪分布式下建议设成 1,不然三份副本全部落在同一个 DataNode 上,不仅没有容灾效果,反而浪费磁盘空间,这是新手最容易忽略的参数之一。
2.3 SpringBoot 连接 HDFS 的依赖与初始化:少走歧路
连接 HDFS 的依赖在很多 pom.xml 里是很容易配错的。很多人直接从网上复制一段hadoop-client依赖,然后发现版本冲突、类找不到。我实践下来的稳定组合是 SpringBoot 2.7.x 配 Hadoop 3.3.x(或 3.2.x),然后把hadoop-client的<scope>设为provided,避免把 Hadoop 全家桶 jar 打进 SpringBoot 的 fat jar 里,防止和 SpringBoot 内置的旧版javax.servlet冲突。
初始化连接时,要小心一个坑:HDFS 客户端会用当前系统的用户名作为 HDFS 用户。Windows 上开发时直接用FileSystem.get(conf)拿到的是Administrator用户,在 HDFS 上没有权限。我的习惯是显式指定访问用户,代码里设置fs.defaultFS、hadoop.user.name这两个配置项再获取FileSystem实例。具体初始化写法放在第三章说,因为整个云盘的上传下载都建立在 FileSystem 实例正确获取这件事上。
3. 动手实现企业云盘核心链路:上传接口、分块机制、元数据表
3.1 HDFS 客户端配置初始化:把连接做成全局单例
先放一段最基础的 Hadoop 配置类。很多人会把FileSystem写在 Controller 里每次 new 一个,这在并发量上来以后连接数会暴涨,把 NameNode 压垮。正确做法是用 Configuration 初始化一次,单例持有 FileSystem 实例:
@Configuration public class HdfsConfig { @Value("${hdfs.default-fs}") private String defaultFs; @Value("${hdfs.user}") private String user; @Bean("fileSystem") public FileSystem createFileSystem() { Configuration conf = new Configuration(); // 关键参数:设置 HDFS 的 NameNode 地址 conf.set("fs.defaultFS", defaultFs); // 本地开发时绕过 kerberos 或其他认证,直接指定 hdfs 用户 conf.set("hadoop.user.name", user); // 当副本数没有在 hdfs-site.xml 里写死时,客户端可以指定 conf.set("dfs.replication", "1"); try { return FileSystem.get(URI.create(defaultFs), conf, user); } catch (Exception e) { throw new RuntimeException("HDFS 初始化失败,请检查 hadoop 配置", e); } } }这里的逻辑并不复杂,关键是FileSystem.get第三个参数user。在未开启 Kerberos 的普通 Hadoop 集群里,HDFS 的权限校验就是看这个用户名。如果不传,JVM 会取系统用户名;在 Windows 开发机上取到的名字通常带反斜杠或者中文字符,直接导致Permission denied或Path not found这类让人懵的错误。显式传一个在 HDFS 上有读写权限的用户,比如hdfs或自定义的clouddisk,就能避开大半权限问题。
参数说明:fs.defaultFS由core-site.xml里的fs.defaultFS决定;hdfs.user需要在 Linux 上预先创建同名 Linux 用户(因为 HDFS 的超级用户是基于 Linux 用户名识别的)。我一般会在 application.yml 里把这两项做成可配置项,部署到不同环境不用改代码。
3.2 文件上传接口:分块拉满,防超大文件 OOM
企业云盘和本地上传最大的区别是,用户拖进来一个 2GB 的压缩包很常见。如果直接用MultipartFile.transferTo接到内存再写 HDFS,内存直接爆掉。我把上传接口设计成「先落临时目录,再流式写入 HDFS」这样一条路,既能拿到文件大小做校验,又能用缓冲流控制内存占用。
上传接口代码主体:
@PostMapping("/upload") public R<String> upload(@RequestParam("file") MultipartFile file, @RequestParam("parentDir") String parentDir) throws IOException { // 1. 校验扩展名和文件大小,拒绝 exe、超过 4GB 的文件 String originalFilename = file.getOriginalFilename(); checkFile(originalFilename, file.getSize()); // 2. 上传文件先暂存到本地临时目录 Path tmpFile = Files.createTempFile("uploaded_", ".tmp"); file.transferTo(tmpFile.toFile()); // 3. 拼 HDFS 目标路径:/user/clouddisk/{parentDir}/{uuid}_{原文件名} String hdfsPath = buildHdfsPath(parentDir, originalFilename); Path dst = new Path(hdfsPath); try (FSDataOutputStream out = fileSystem.create(dst, true); InputStream in = Files.newInputStream(tmpFile)) { IOUtils.copyBytes(in, out, 4096, true); } catch (Exception e) { // 日记里打印完整堆栈,便于定位 HDFS 写失败原因 log.error("hdfs 上传失败,目标路径:{}", hdfsPath, e); throw new BusinessException("上传失败,请联系管理员"); } finally { Files.deleteIfExists(tmpFile); } // 4. 文件已落到 HDFS,元数据写 MySQL saveMetaToDb(originalFilename, file.getSize(), hdfsPath, parentDir); return R.success("上传成功"); }逻辑说明:步骤 2 里transferTo到本地临时目录,是为了拿到file.getSize()对应的真实文件实体,也把「文件传输解析」和「HDFS 写入」两个阶段解耦。步骤 3 用IOUtils.copyBytes以 4KB 缓冲流式写入,无论文件多大,内存里最多只有 4KB 的缓冲;create的第二个参数true表示覆盖已存在的同名文件,这个参数在重传同名文件时很有用。
参数说明:parentDir是用户在云盘里选择的目录 ID,不是 HDFS 绝对路径,这里传的是业务层逻辑路径。真正落 HDFS 时拼的是/user/clouddisk/{parentDir},意味着云盘的目录树和 HDFS 目录结构一一对应。这种设计的好处是,即使 MySQL 元数据被删,也能通过 HDFS 上的目录路径人工找回文件。
3.3 分块上传与断点续传:避免大文件中途失败全盘重来
上面的流式上传已经能撑住大文件,但在真实网络环境里,一个 4GB 的文件传一半断网,要求用户重新拖一遍是很不友好的。企业云盘源码通常会把分块上传这个能力带上,前端把文件切成 5MB 或 8MB 的块,逐块上传,服务端记录每个块的状态,全部上传完后发起合并请求。
分块合并的 Service 逻辑:
@Service public class ChunkService { @Autowired private FileSystem fileSystem; // 前端每上传完一个块,就调用这个方法追加到 HDFS 临时文件 public void appendChunk(String tempFilePath, MultipartFile chunk, Integer chunkIndex) throws IOException { Path tmpPath = new Path(tempFilePath); // 第一个块用 create 创建文件,后续块用 append 追加 FSDataOutputStream out; if (chunkIndex == 0) { out = fileSystem.create(tmpPath, true); } else { out = fileSystem.append(tmpPath); } try (FSDataOutputStream fos = out; InputStream in = chunk.getInputStream()) { IOUtils.copyBytes(in, fos, 8192, true); } } // 全部块上传完成后,把临时文件改名为正式文件,并写入元数据 public String mergeChunks(String tempFilePath, String targetPath, String filename) throws IOException { Path tmpPath = new Path(tempFilePath); Path target = new Path(targetPath + "/" + filename); boolean renamed = fileSystem.rename(tmpPath, target); if (!renamed) { throw new BusinessException("文件合并失败,目标路径已存在同名文件"); } return targetPath + "/" + filename; } }逻辑说明:分块写入的核心是FSDataOutputStream的append方法。HDFS 的 append 在早期版本(1.x)是支持但对并发追加有严格限制,2.7+ 以后已经很稳定,但要注意:同一时刻只能有一个客户端对一个文件执行 append,所以前端必须严格串行上传分块,不能并发。这里我把第一个块单独用create,后续块用append,就是为了规避create覆盖导致的前功尽弃。
踩坑提醒:append操作在 HDFS 上涉及写日志(edit log)和副本同步,比新建文件慢一些。如果分块数很多(比如 300 个块),全部append反而比一次性流式写入慢。我在实际项目里的折中方案是,小于 500MB 的文件走 3.2 的流式上传,大于 500MB 才走分块合并,且分块大小固定为 8MB,这样最多也就 64 个块,性能和体验都过得去。
3.4 元数据表设计与文件下载:MySQL 和 HDFS 各自管什么
HDFS 只认路径,不管文件名是否重复、目录树长什么样,这些「业务信息」必须由 MySQL 来管。我的表结构设计得很简,但把该约束的字段都约束住。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 文件或目录的自增 ID |
| name | varchar | 文件显示名(不做唯一约束,允许重名) |
| hdfs_path | varchar | HDFS 绝对路径,比如 /user/clouddisk/2025/04/xxx.zip |
| parent_id | bigint | 父目录 ID,根目录为 0 |
| file_size | bigint | 文件字节数,目录则为 0 |
| is_dir | tinyint | 0 文件,1 目录 |
| create_time | datetime | 上传时间 |
设计要点是hdfs_path保持唯一索引。表结构的关键约束是 MySQL 管业务结构,HDFS 管数据实体,两者靠hdfs_path关联。下载的时候,拿到文件 ID 后先从 MySQL 查出hdfs_path,再走 HDFS 的open流出给前端:
@GetMapping("/download/{fileId}") public void download(@PathVariable Long fileId, HttpServletResponse response) throws IOException { // 根据 fileId 查元数据,得到 hdfs_path 和原始文件名 FileRecord file = fileMapper.selectById(fileId); Path path = new Path(file.getHdfsPath()); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8")); try (FSDataInputStream in = fileSystem.open(path); OutputStream out = response.getOutputStream()) { IOUtils.copyBytes(in, out, 4096, true); } }这段代码里的URLEncoder.encode不是可有可无——不拿 UTF-8 编码文件名的话,中文文件名会变成乱码。实际项目里我会在文件上传时就把uuid_原始文件名存进 HDFS,下载时从 MySQL 里取原始名拼到响应头,这样 HDFS 路径全 ASCII,避免 HDFS 端中文字符 URI 解析的问题。
4. Hadoop 环境搭建与 SpringBoot 联调:从伪分布式到可运行的云盘
4.1 Linux(或 Docker)下 Hadoop 伪分布式搭建步骤
搭一套能用来做课程设计的 Hadoop 伪分布式,最稳的路径是下载官方 tar 包手动解压配置,而不是用现成的 Docker 镜像——Docker 镜像的 Hadoop 版本五花八门,出了问题不好排查。我习惯用 Hadoop 3.3.6 版本,JDK 用 8 或 11 都可以(Hadoop 3.x 官方要求 JDK 8+)。
搭建步骤按顺序执行,每一步都有明确目的:
# 1. 创建 hadoop 用户并配置 SSH 免密(伪分布式也走 ssh 启动) useradd hadoop passwd hadoop su - hadoop ssh-keygen -t rsa -P "" -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys # 2. 解压 Hadoop 到 /opt/hadoop tar -zxvf hadoop-3.3.6.tar.gz -C /opt mv /opt/hadoop-3.3.6 /opt/hadoop chown -R hadoop:hadoop /opt/hadoop配置$HADOOP_HOME/etc/hadoop/hadoop-env.sh,指定 JDK 路径;然后编辑两个核心 xml 文件,这一步直接决定启动是否成功。core-site.xml设置文件系统入口和临时目录:
<configuration> <!-- NameNode 的 IPC 通信地址,9000 是默认端口 --> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <!-- 临时目录必须手动设置,默认 /tmp 会被系统清理 --> <property> <name>hadoop.tmp.dir</name> <value>/opt/hadoop/tmp</value> </property> </configuration>hdfs-site.xml设置 NameNode 的 Web 端口和副本数:
<configuration> <property> <name>dfs.namenode.http-address</name> <value>localhost:9870</value> </property> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/opt/hadoop/name</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/opt/hadoop/data</value> </property> </configuration>参数说明:hadoop.tmp.dir是全局临时目录,NameNode 的 fsimage 和 edit log 默认也在里面,最容易被忽略。很多人配完启动后报NameNode is not formatted,通常就是没设这个目录或目录权限不对。设置好以后执行格式化和启动:
hdfs namenode -format start-dfs.sh jpsjps输出里必须同时看到NameNode、DataNode、SecondaryNameNode三个进程,缺任何一个都说明启动有问题,优先查$HADOOP_HOME/logs下的日志文件。这一步跑通后,再在云盘项目里去调用 HDFS。
4.2 SpringBoot 连接 HDFS 的联调配置与常见启动错误
SpringBoot 侧联调前,先在 Linux 上手工验证 HDFS 可用性:hdfs dfs -ls /能列出目录,hdfs dfs -mkdir /test能建目录,说明 HDFS 本身没问题。接下来才是项目侧配置。
spring: servlet: multipart: # 单文件最大 2GB,分块上传时这个限制可以放得很宽 max-file-size: 2048MB max-request-size: 4096MB hdfs: # 和 core-site.xml 里的 fs.defaultFS 保持一致 default-fs: hdfs://192.168.10.20:9000 # 指定有读写权限的 hdfs 用户 user: clouddisk联调时最容易遇到的是「应用在 Linux 上,配置为 localhost:9000 却连不上 NameNode」的问题。localhost在配置文件里换成 Linux 服务器的实际 IP 才能让远端应用连上。这个问题非常高频,几乎每个第一次联调的人都会踩。还有一类启动时报HADOOP_HOME is not set或者找不到winutils.exe——这是你在 Windows 上跑 SpringBoot 导致的。
解决 Windows 下连 HDFS 问题,需要将一份 Hadoop 的 Windows 编译版 jar 包(内含winutils.exe)解压到本地目录,然后设置hadoop.home.dir系统属性。在我的项目里,我在启动类加了这样一段:
@SpringBootApplication public class CloudDiskApplication { public static void main(String[] args) { // 如果本机没有 HADOOP_HOME 环境变量,手动指定(仅限 Windows 开发环境) if (System.getProperty("os.name").toLowerCase().contains("windows")) { System.setProperty("hadoop.home.dir", "D:/hadoop-winutils"); } SpringApplication.run(CloudDiskApplication.class, args); } }这段代码只在 Windows 上生效,Linux 部署时不走hadoop.home.dir这条路,直接依赖系统安装的 Hadoop 环境变量。参数说明:hadoop.home.dir指向的目录里必须存在bin/winutils.exe和bin/hadoop.dll,否则 HDFS 客户端在 Windows 上调用底层本地库时直接抛异常,报错类型五花八门,最典型的是Failed to locate the winutils binary in the Hadoop binary directory。
4.3 上传链路联调:用 curl 验通整条接口
SpringBoot 项目启动后,先别急着打开前端页面,我用 curl 直接打接口验证后端到 HDFS 的链路,这样问题边界清晰——接口通了再怀疑前端,接口不通查后端配置。
# 上传一个测试文件,验证 MultipartFile -> HDFS 全链路 curl -X POST http://localhost:8080/upload \ -F "file=@/tmp/test.zip" \ -F "parentDir=0" \ -w "HTTP状态码: %{http_code}\n" # 在 HDFS 上确认文件已落盘 hdfs dfs -ls /user/clouddisk/0/ # 应该能看到 test.zip 或 uuid_test.zip 这样的文件如果上传返回 200,但 HDFS 的/user/clouddisk/0/目录下为空,多半是写到了别的路径或没指定user,去 HDFS Web UI(http://IP:9870)的 Utilities 页面看实际的/user目录结构就能找到位置。如果返回 500,查 SpringBoot 日志里有没有Permission denied——有就是用户不对,没有就是 HDFS 连接被防火墙挡了端口 9000。到这里,云盘的最短闭环就跑通了,剩下的目录管理、文件删除、分享链接都是在这个链路上的业务扩展。
5. 企业云盘落地的常见坑与排查清单:谁能踩中、怎么绕开
5.1 版本颠簸:SpringBoot 和 Hadoop 的 jar 包冲突
现象:SpringBoot 项目启动时报NoClassDefFoundError或IncompatibleClassChangeError,类名都是org.apache.hadoop.*下的,而且是启动到一半才炸。原因:SpringBoot 自身依赖了一部分javax.servlet和org.apache.commons的旧版本,而 Hadoop 3.x 依赖的 commons-lang3 版本更高,二进制的类方法签名对不上。解决:把hadoop-client的 scope 设为provided,并且在 maven 里通过exclusion排除 hadoop-client 传递引入的javax.servlet-api旧包。我在 pom 里是这么处理的:
<dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>3.3.6</version> <scope>provided</scope> <exclusions> <exclusion> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> </exclusion> </exclusions> </dependency>provided的意思是这个 jar 只参与编译,不打进 SpringBoot 的 fat jar。那 Hadoop 的 jar 从哪里来?两种方式:一是部署机器的 Hadoop 安装目录里share/hadoop/common/*.jar和hdfs/*.jar加入 classpath;二是用maven-shade-plugin把兼容性确认过的几个包打进 fat jar。我选第一种,部署到哪台机器就在哪台机器的 Hadoop 环境里启动,省心。
5.2 Windows 上跑通的玄学:FileSystem 实例拿到了但读写全报权限错
现象:Windows 上 IDEA 里启动云盘项目,FileSystem.get没有抛异常,但一上传文件就报Permission denied: user=Admin, access=WRITE, path="/user/clouddisk/0/"。原因:HDFS 的权限校验用的是 Linux 用户体系,Windows 的Administrator映射到 HDFS 那边没有目录的写权限。HDFS 的/user/clouddisk目录是拿 Linux 的clouddisk用户在 HDFS 上mkdir创建的,目录 owner 是clouddisk;Windows 上用Admin连过去当然没权限。解决:不是去 HDFS 上给Admin赋权,那治标不治本。我统一在连接处指定 user,也就是第三章 HdfsConfig 里conf.set("hadoop.user.name", user)这行代码,配成clouddisk。注意,这个 user 必须和 Linux 上创建 HDFS 目录的那个 Linux 用户同名。
5.3 小文件堆积:为什么 NameNode 内存不够却查不到大文件占用
现象:上传了 5000 个很小的文件(比如 5KB 的文本),发现 NameNode 内存占用飙升,但hdfs dfs -du -h /user/clouddisk显示总量才几十 MB,磁盘也远远没满。原因:HDFS 不适合存小文件。NameNode 在内存里维护整个文件系统的元数据,每个文件、目录、块都需要一条记录(占用约 150~200 字节)。5000 个 5KB 文件,光元数据就吃掉近 1MB 内存,但业务上文件总大小才 25MB——代价直接按倍数放大。解决:在企业云盘里设置文件「块大小」和「小文件合并」。具体做法是在 HDFS 性能参数里设置dfs.namenode.fs-limits.min-block-size,同时让云盘每个小时做一次离线任务,把小于 32MB 的文件合并成一个 SequenceFile 归档文件,用户端无感知,HDFS 元数据数量骤降。
5.4 上传中断后 HDFS 残留的临时文件:下一次启动报文件已存在
现象:上传大文件时网络断了,SpringBoot 显示上传失败,但第二次重传同一个文件时,日志报org.apache.hadoop.fs.FileAlreadyExistsException。原因:分块组装时第一个块用create(tempPath)创建了临时文件,后续块还没来得及全部append,连接就断了。临时文件留在 HDFS 上,第二次上传时create同名路径直接报已存在,而实际问题出在断点信息的清理上。解决:在create调用前先做一次容错检查,存在则删除;同时在代码里给上传任务加一个 sessionId,把临时文件路径设计成/tmp/chunks/{sessionId}.part,这样即使上次残留也不会和本次冲突:
// 分块初始化时,清理旧残留 Path tmpBase = new Path("/tmp/chunks"); if (fileSystem.exists(new Path(tmpBase, sessionId + ".part"))) { fileSystem.delete(new Path(tmpBase, sessionId + ".part"), false); } FSDataOutputStream out = fileSystem.create(new Path(tmpBase, sessionId + ".part"), true);注意这个「清理残留」的逻辑只在服务端做,前端不需要感知。否则用户无法分辨「网络为什么又断了」,体验极差。
5.5 NameNode 单点与断电恢复:伪分布式在演示前的冰火两重天
现象:云盘跑了两周,某天停电,重启后start-dfs.sh拉不起来,日志报NameNode is not formatted或edit log is corrupted。原因:hadoop.tmp.dir默认指向/tmp,系统重启时 OS 清了/tmp,NameNode 的元数据文件(fsimage)没了。这是伪分布式最经典的冰火两重天——演示前一切正常,断电后直接回到解放前。解决:严格按照配置阶段的要求,把hadoop.tmp.dir指到持久化目录。另外,有条件就把部署目录挂到独立数据盘,对/opt/hadoop/tmp做定时快照,代价极低。我在项目里是把备份脚本写成一个 crontab 任务,每晚将 NameNode 的元数据目录打包到另一块盘,恢复时解压回去再hdfs namenode -recover即可。
6. 从云盘源码到生产可用:HA 集群演进与三个验证指标
伪分布式跑通后,如果要让这个企业云盘真正值得被业务信任,需要把它推到 Hadoop HA 高可用集群上。这是我从课程设计源码到企业内部落地时走通的一条路,核心动作是把 SpringBoot 侧的fs.defaultFS从hdfs://localhost:9000改成hdfs://nameservice1,然后配置dfs.namenode.rpc-address.nameservice1对应多个 NameNode 地址地址。云盘业务代码几乎不用改,因为FileSystem.get(conf)里 conf 只认fs.defaultFS这个名字服务,真正的地址映射由 Hadoop 端的hdfs-site.xml决定。
改完配置后,用三个指标验证这套系统是否值得投入。第一个是「上传成功率」。用一个脚本模拟 500 次 10MB 文件上传,统计失败数和重试数。我当时的线上数据是成功率 99.8%,失败的 1 次发生在 DataNode 滚动重启的瞬间,触发客户端重试后成功。第二个是「小文件占比」。如果/user/clouddisk下小于 32MB 的文件数量占比超过 60%,说明云盘的合并归档任务没跑起来,NameNode 压力会随时间线性增长。第三个是「NameNode 堆内存监控」。通过 Hadoop 的 JMX 接口采集FSNamesystemState.MissingBlocks和HeapMemoryUsed,持续观察 24 小时,如果堆内存增长曲线不是缓慢上升而是阶梯跳跃,说明元数据表清理逻辑有缺陷,需要排查 MySQL 里是否有大量孤儿记录(hdfs_path 指向 HDFS 已删除的文件)。
最后的习惯是,每次做生产演练前,我都会跑一次全链路回归:上传一个 1GB 的大文件、下载一次 200MB 的中等文件、删除一个目录再比对 HDFS 实际目录空间。这套东西看起来简单,但每一次都能揪出「MySQL 和 HDFS 数据不一致」的问题。做企业云盘这个方向,真正值钱的部分不在「上传下载」这两个接口的代码量,而在「文件不丢、空间不浪费、链路可监控」这三个运维底线上。希望这篇笔记能帮你在自己的环境里把这条路走通,少走我当年走过的弯路。
本文还有配套的精品资源,点击获取