ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Hadoop的云盘系统实战:HDFS存储与秒传断点续传设计

基于Hadoop的云盘系统实战:HDFS存储与秒传断点续传设计 简介基于Hadoop的云盘系统.zip是一份面向大数据开发学习者与Hadoop实践者的项目资源系统展示如何借助HDFS分布式存储、MapReduce处理框架及云盘服务层构建可用的网络存储平台。压缩包共284个文件总大小1.16MB以105个Java源码文件为主辅以前端页面所需的32个JavaScript、30个HTML、13个CSS及多张GIF演示图并包含Maven配置、JSON配置、图标字体和JAR依赖等可帮助读者快速还原项目结构并理解前后端交互逻辑。资源覆盖了分布式文件系统管理、用户认证与权限控制、文件上传下载等关键模块适合作为课程设计或毕业设计的参考蓝本。已有98人学习该资源对于希望入门大数据云盘开发的读者可借助源码与界面素材直观掌握Hadoop生态在真实场景中的落地方式缩减从概念到实现的摸索时间。1. 基于Hadoop的云盘系统到底在解决什么问题做课程设计、毕业设计或者企业内部文档库时很多团队会把“基于Hadoop的云盘系统”当成一个传统CRUD项目来看待Spring Boot 管上传下载、MySQL 管用户和文件表最后把文件落到某个目录就完事。可一旦文件总量到了几百万个、单文件上 GB这台机器上的目录结构根本扛不住。HDFS 天然就是为海量大文件设计的分布式文件系统它的块存储、副本机制与流式读写接口恰好能补上单机文件系统在容量和可靠性上的缺口。这个标题背后真正要解决的是两件事第一怎么让 NameNode 的元数据与 DataNode 的数据块配合起来对外表现出一个“像网盘一样”的文件语义第二怎么把上传、下载、秒传、断点续传这些产品功能翻译成对 HDFS 的 Client API 调用而不是简单套一层 Shell 命令。下面按一条可复现的路径把从集群搭建到接口落地的关键点拆开讲。2. 选 HDFS 当云盘底座块、副本与容器化集群搭建2.1 云盘系统的数据层为什么落在 HDFS 上云盘和普通 Web 文件系统的最大区别在于“文件”不是传统意义的一个 inode而是一个逻辑映射。HDFS 里一个文件被切成一个或多个块块分散存在多个 DataNode 上NameNode 只保存文件名到块列表的映射以及块的物理位置。这样设计带来的直接好处是云盘的单文件上限不再受限于单块磁盘大小文件总量不再受限于单机磁盘容量这是选择 HDFS 作为底座的第一层理由。第二层理由与可靠性相关。副本机制意味着某个 DataNode 宕机后块还能从其他副本读出来云盘系统不需要自己做切片备份也不用在应用层写复杂的灾备脚本。第三层理由是在网络吞吐上客户端可以从多个 DataNode 并行读取一个文件的多个块大文件下载的带宽利用能力明显强于从单台服务器拉文件。但这里有个必须提前认清的边界HDFS 是“一次写入、多次读取”的模型随机写和文件末尾追加都受限。云盘的“编辑已上传文件”不能直接落在 HDFS 上常规做法是客户端先下载、修改后重新上传并覆盖业务层把旧版本放入回收站或版本列表。设计接口时要时刻记住这个限制否则后面做编辑功能会走弯路。2.2 块大小、副本数与机架感知HDFS 存储模型如何决定云盘接口HDFS 默认块大小 128MB默认副本数 3。对云盘系统来说这两个默认值通常要改。以块大小为例云盘里有大量小文件一个 4KB 的 TXT 文档也会独占一个块NameNode 内存里要维护每个块的位置信息小文件数量上去了NameNode 就是瓶颈。常见做法是把块调小到 64MB 或 32MB并且牺牲一部分磁盘预留空间来换元数据规模也可以在业务层做小文件合并。副本数则直接决定存储成本和上传速度。内网云盘保留 3 副本比较稳妥外网或测试环境建议 2 副本。上传数据时HDFS 客户端先联系 NameNode 获取块写入的管道列表然后按“第一个副本就近、第二个副本跨机架、第三个副本与第二个同机架不同节点”的策略落盘。这个流程意味着如果 DataNode 都部署在同一台物理机或同一个机架那么跨机架相关的配置项压根不生效但拓扑脚本在伪分布式环境下验不了得在至少两个节点的集群上才能看到完整效果。还有一项和下载接口强相关的参数是dfs.client.use.datanode.hostname默认值是 false客户端通过 IP 直连 DataNode。如果服务器有多个网卡或走的是 NAT 网络经常会出现 NameNode 返回给客户端的 DataNode 地址不可达、文件能列出却下载不下来的情况。调成 true 后客户端改用 DataNode 的主机名去连接并配合/etc/hosts里的内网解析问题就消失。2.3 用 Docker Compose 跑通最小 HDFS 集群常见的 Hadoop 环境有单机本地模式、伪分布式、完全分布式三种云盘系统至少得在伪分布式或完整集群上跑因为单机本地模式走的是本地文件系统扩展根本不上 HDFS。对开发机来说用 Docker Compose 拉起一个一主两从的最小集群最省事能直接模拟真正分布式环境。services: namenode: image: bde2020/hadoop-namenode:2.0.0-hadoop3.2.1-java8 container_name: nn environment: - CLUSTER_NAMEcloud-disk ports: - 9870:9870 - 9000:9000 volumes: - nn_data:/hadoop/dfs/name datanode1: image: bde2020/hadoop-datanode:2.0.0-hadoop3.2.1-java8 container_name: dn1 environment: - CLUSTER_NAMEcloud-disk ports: - 9864:9864 volumes: - dn1_data:/hadoop/dfs/data depends_on: - namenode datanode2: image: bde2020/hadoop-datanode:2.0.0-hadoop3.2.1-java8 container_name: dn2 environment: - CLUSTER_NAMEcloud-disk ports: - 9865:9864 volumes: - dn2_data:/hadoop/dfs/data depends_on: - namenode volumes: nn_data: dn1_data: dn2_data:启动命令是docker compose up -d等 NameNode 进入安全模式结束后访问http://localhost:9870DataNode 列表里能看到两个节点即成功。这套配置的关键在两处CLUSTER_NAME必须一致三个容器才能真正形成集群数据卷分别挂载避免容器删除丢数据。9870 是 NameNode 的 Web UI 端口9000 是 RPC 端口Java 客户端和命令行工具都走 9000。docker compose 里的depends_on只保证启动顺序不保证 NameNode 已就绪所以常出现 DataNode 启动时连不上 NameNode 的情况。解决方法是把初始化脚本改成docker compose restart datanode1 datanode2或者干脆删掉容器用同一卷重建。2.3.1 上传时的权限与临时目录坑伪分布式和 Docker 环境下HDFS 默认以当前 Linux 用户的身份访问如果客户端用 root 跑写/user/root/没问题但跨用户访问目录就要注意权限。常见做法是在代码里显式设置fs.defaultFS和hadoop.user.name或者把dfs.permissions.enabled设为 false后者仅限开发环境上线不建议关。3. 元数据与文件数据分离云盘系统的表和接口怎么设计3.1 把元数据放 MySQL把文件块放 HDFS很多人在设计云盘时习惯把文件路径直接挂到用户 ID 下比如/user/alice/docs/readme.md然后通过 HDFS 的 List 接口列目录。这个方案在文件量小时可用但 HDFS 对目录扫描支持弱列目录会产生大量 RPC而且删除目录是递归操作接口层面不好控制软删除和回收站。业界通用的做法是元数据与数据分离MySQL 管文件逻辑关系、目录树、分享链接、 MD5 这些业务字段HDFS 只当作一个按地址取数据的对象存储。建表时第一张表是文件记录表字段设计如下CREATE TABLE file_meta ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, file_name VARCHAR(255) NOT NULL, file_size BIGINT NOT NULL DEFAULT 0, hdfs_path VARCHAR(512) NOT NULL, md5 CHAR(32) NOT NULL, parent_id BIGINT NOT NULL DEFAULT 0, is_dir TINYINT NOT NULL DEFAULT 0, deleted TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_parent_name (user_id, parent_id, file_name), INDEX idx_md5 (md5) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;其中hdfs_path不是用户目录而是 HDFS 上的物理存储地址统一放/clouddisk/{user_id}/{uuid}文件名本身用业务名落库。这样设计的好处是 HDFS 路径永不重复用户重命名文件只改 MySQL不碰 HDFS。parent_id自引用形成目录树md5字段后面秒传要直接用必须建索引。deleted字段用来做软删除回收站展示的就是deleted1且未清理的数据。3.2 秒传、分片上传与断点续传的协议设计云盘最常见的三个增强功能是秒传、分片上传和断点续传三者不是独立功能而是同一套协议上的分工。3.2.1 秒传秒传的核心是文件内容寻址。客户端在上传前先计算整个文件的 MD5向后端发起一个预上传请求后端拿 MD5 去file_meta表里查。如果找到相同 MD5 且文件大小一致说明 HDFS 里已经有同样的文件此时不需要把文件体再写一遍只需要插入一条新记录把hdfs_path指向已有文件的路径上传接口直接返回成功。这个流程依赖一个前提HDFS 上同一个路径可以被多个元数据记录引用。由于hdfs_path用的是与业务无关的 UUID多个用户秒传同一份文件时他们各自的file_meta.hdfs_path会指向同一个 HDFS 路径互不冲突。有人会先入为主认为“删除时要判断引用计数”其实不用云盘允许用户对同一内容各持一份引用删除一个用户只是删掉那条 MySQL 记录物理文件等回收站清理时再做一次引用检查即可。3.2.2 分片上传与断点续传HDFS 不支持随机写所以不能像对象存储那样直接把分片并发写到 HDFS 的同一个文件上。实现分片上传的常规做法是客户端把文件切成固定大小分片按分片序号发给后端后端先写到本地临时目录或直接写入 HDFS 的临时目录全部传完后由后端将这些分片按顺序追加合并成目标文件。CREATE TABLE upload_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, upload_id VARCHAR(64) NOT NULL, file_name VARCHAR(255) NOT NULL, file_size BIGINT NOT NULL, chunk_size INT NOT NULL DEFAULT 1048576, total_chunks INT NOT NULL, uploaded_chunks VARCHAR(2048) NOT NULL DEFAULT [], hdfs_path VARCHAR(512) NOT NULL, status TINYINT NOT NULL DEFAULT 0 );upload_id是一次上传任务的唯一标识uploaded_chunks用 JSON 数组记录哪些分片已上报。每个分片上传完成后后端更新这个字段断点续传就是客户端再次发起时带着upload_id和本地已完成分片列表后端只接收缺失的分片。分片大小建议 1MB 到 8MB太小的分片上传请求数量过多1GB 文件传 1024 个 1MB 分片对服务端压力很大太大又失去了断点续传的粒度意义。实际生产环境常用 4MB。合并分片可以用 HDFS 的dfs.append接口或直接复制合并但在 Java 客户端上更稳妥的方式是把临时分片依次读出来用同一个FSDataOutputStream写入最终目标文件写完后删掉临时分片。整个过程集中在一个事务方法里执行确保中途失败时临时文件可清理。这里不建议用 HDFS 的 append因为 HDFS 追加写要求与上次写入的块长度对齐分片大小不一致时会抛异常坑多收益小。3.3 目录、分享与权限的路径设计云盘的目录树由parent_id表达上传文件时前端传parent_id后端生成 UUID 文件路径同时把file_meta.parent_id指向该目录的记录 ID。列出目录时执行一条WHERE parent_id ? AND deleted 0查询效果等同传统文件系统列表但语义上不受 HDFS 目录结构约束。这也是为什么不要把目录直接建在 HDFS 上的原因如果 HDFS 路径就是目录树那么删除非空目录会变成批量删除底层块操作业务上需要软删除时完全无法实现。分享功能单独建一张share_link表记录分享的文件记录 ID、分享 token、过期时间和提取码。网络层实现上分享链接打开后前端拿着 token 换取临时下载地址后端验证 token 后用 HDFS 客户端生成一个带过期时间的访问地址或者由后端流式转发文件数据。HDFS 原生不支持对象存储那种带签名的临时 URL所以更简单的方式是后端读 HDFS 流通过 HTTP 响应输出给浏览器文件名放在Content-Disposition头里。4. 用 HDFS Client API 把上传、下载、删除跑通4.1 引入依赖并初始化 FileSystem 客户端Hadoop 官方为 Java 提供了hadoop-client聚合依赖不需要每个模块单独引入Maven 里配置dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version3.3.6/version /dependency引入依赖后第一步不是写上传方法而是初始化FileSystem对象。HDFS 的客户端是通过FileSystem.get(Configuration)拿到的Configuration 里要指定的关键参数就一个fs.defaultFS指向 NameNode 的 RPC 地址import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfs://localhost:9000); conf.set(dfs.client.use.datanode.hostname, true); conf.set(dfs.replication, 2); FileSystem fs FileSystem.get(conf);dfs.replication在这里提醒一下客户端写入时NameNode 会优先使用集群全局配置但这个客户端参数可以覆盖全局设置。如果集群副本数是 3而某类临时文件希望只存 1 副本可以在客户端单独设置。这段代码里最容易被忽略的是dfs.client.use.datanode.hostname本地开发时如果通过 localhost 端口映射访问 Docker 里的 DataNode必须设置成 true否则客户端拿到的数据节点地址是容器内部 IP直接连接必失败。很多本地跑通的教程都没提这个参数导致换到 Docker 环境就报Connection refused。4.2 文件上传三种写法的取舍上传文件到 HDFS 的标准姿势是拿到FSDataOutputStream用IOUtils.copyBytes把输入流转进去import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.io.IOUtils; public void uploadToHdfs(InputStream localIn, String hdfsPath) throws IOException { Path outPath new Path(hdfsPath); try (FSDataOutputStream out fs.create(outPath, true)) { IOUtils.copyBytes(localIn, out, 4096, false); } }fs.create的第一个参数是 HDFS 路径第二个是overwrite这里要结合业务场景选择。云盘秒传场景下不同用户共享同一路径时不能直接覆盖否则会把已有引用对应的数据改掉而重新上传同一文件内容推荐改名后写入新路径而不是覆盖。IOUtils.copyBytes的第三个参数是缓冲区字节数默认 4096 对网络传输偏小大文件上传建议直接传 128 * 1024第四个参数是是否关闭输入流传 false 让流由外层业务逻辑控制避免在 HDFS 写入后把 HTTP 请求体也关掉。除了createHadoop 还提供了FileSystem.copyFromLocalFile它要求先把文件放到客户端本地适合冷数据迁移不适合 Web 上传因为 Web 上传拿到的是请求 InputStream不会有落盘过程。4.3 文件下载与断点续传的 IO 实现下载逻辑与上传对称从 HDFS 打开文件把数据流写入 HTTP 输出流。带断点续传的下载要做两点用seek定位到指定偏移量再精确读取请求的字节范围import org.apache.hadoop.fs.FSDataInputStream; public void downloadRange(String hdfsPath, OutputStream localOut, long start, long length) throws IOException { Path path new Path(hdfsPath); try (FSDataInputStream in fs.open(path)) { in.seek(start); byte[] buf new byte[1024 * 128]; long remaining length; int n; while (remaining 0 (n in.read(buf, 0, (int) Math.min(buf.length, remaining))) 0) { localOut.write(buf, 0, n); remaining - n; } } }这里seek是 HDFS 客户端最重要的定位方法它让服务端可以支持 HTTP Range 请求。当前端从断点位置恢复下载时请求头带Range: bytes1048576-后端解析出start1048576length为文件总大小减去起始位置调用downloadRange把剩余数据返回。读取循环里的remaining必须参与每次read的长度计算否则读多会返回超过请求范围的数据导致下载文件尾部多余。fs.open(path)也可以传入第二个参数即bufferSize范围下载场景建议调大到 256KB 以减少 RPC 交互次数。HDFS 的读操作本身不校验客户端读取的字节数是否与文件开头对齐这给了断点续传极大的灵活性。需要注意的边界是seek定位的偏移量如果超过文件长度read会返回 -1函数正常结束但 HTTP 层面应该先返回 416 状态码。因此调用这个下载方法前要从file_meta表查出file_size与请求的 start 比较后再决定走文件流还是报错。4.4 删除与目录删除的语义差异HDFS 删除文件的接口只有fs.delete(Path, boolean)第二个参数表示是否递归删除。云盘业务里的删除分两层先做逻辑删除把file_meta.deleted置 1用户界面不再展示此时不调用 HDFS 的delete保证秒传引用不失效当用户清空回收站或后台做物理清理时才真正调用public void deletePhysical(String hdfsPath) throws IOException { Path path new Path(hdfsPath); boolean deleted fs.delete(path, false); if (!deleted) { LOGGER.warn(HDFS file not deleted: {}, hdfsPath); } }物理删除之前必须确认没有其他活跃的file_meta记录指向该路径。有一条很容易踩的坑HDFS 删除文件是异步的吗实际上只要delete返回 trueNameNode 就把文件标记为已删除块会在随后的垃圾回收周期内被 DataNode 异步清理所以删除后立刻去查询该路径会得到false这不代表删除失败。回收站的物理清理逻辑里建议先做一次引用计数查询再执行delete并且不要对同一个路径并发调用删除。int refCount fileMetaService.countByHdfsPath(hdfsPath); if (refCount 0) { return; // 还有引用不能物理删除 }这段代码逻辑虽然简单却是云盘系统里最容易因为“忘了查引用”而把别人数据删穿的地方。常见的错误是后端只在逻辑删除时判断一次物理清理阶段就不再确认结果同一份秒传文件被两个用户引用时一个用户清空回收站把另一个用户的文件也删了。正确顺序始终是查引用、再删除。5. 从能跑到扛用四个关键参数与三条排查命令hadoop 集群搭好后云盘系统性能调优的第一步是改对四个参数它们直接影响上传下载的吞吐和 NameNode 的稳定性dfs.replication副本数控制在 2 到 3测试集群 2 够用减少一半存储开销dfs.blocksize块大小在文件平均不超过 16MB 时调成 32MB减少小文件对 NameNode 内存的占用dfs.namenode.handler.countNameNode 处理 RPC 的线程数小集群默认 10 不够用上传并发高时把它调到 50 到 100线程越多队列空出来的时间越短dfs.datanode.handler.count同样从默认 10 调到 30否则多客户端并发写同一个 DataNode 时 transfer 线程排队明显。这四个参数改完后重启 NameNode 和 DataNode 生效改动大但收益也最直接。上线后至少要会用三条命令验证集群状态和数据完整性。第一条是hdfs dfsadmin -report输出里能看到每个 DataNode 的状态、剩余容量和最后心跳时间节点挂了一个能从报告里直接看出来是在配置或者 bug 排查前必跑的命令。第二条是hdfs fsck /clouddisk -files -blocks -locations检查文件是否有损坏块这个命令的-files -locations参数会把每个块所在的 DataNode 显示出来用来验证副本数配置是否按要求生效以及某个文件是否因为磁盘故障降级成了单副本。第三条是hadoop fs -du -h /clouddisk看用户目录实际占用的物理空间这里的数值比ls -l显示的 logical size 准确得多因为差的就是副本放大倍率。最后的交付阶段像标题里那个.zip工程包一样项目往往被打包后在不能联网的内网环境部署。把 Hadoop 相关的配置外置到application.yml或独立的hdfs-site.xml比什么都重要客户端代码里写死hdfs://localhost:9000的版本拷到内网集群上必挂。把fs.defaultFS、dfs.client.use.datanode.hostname、副本数读到配置中心或环境变量里内网部署时只改配置不改代码整个云盘系统就能无缝切到生产集群上。本文还有配套的精品资源点击获取
返回列表