资讯动态

HDFS上传下载原理与HdfsAPI及IO流实战避坑指南

发布时间:2026/10/16 4:06:30 来源:尧图企业网站定制
1. 从一次数据迁移事故说起为什么HDFS的上传下载值得深挖几年前我参与过一个日志聚合平台的搭建每天大概有2TB的原始日志需要从几十台边缘节点汇入存储集群。项目初期为了赶进度直接用了最朴素的命令行方式做批量上传结果上线第三天就出了状况一批关键日志在传输过程中断了重跑之后发现目标目录里出现了大量大小异常的文件有的只有几KB有的干脆是0字节。排查了大半天才定位到问题——上传过程中客户端进程被意外终止而HDFS的写入语义决定了未关闭的文件会以“未完成”状态残留在元数据里后续的追加操作又因为租约问题被拒绝。那次事故让我意识到HDFS的上传与下载远不是“把文件丢进去、再拿出来”这么简单。它背后涉及分块切分、流水线复制、租约管理、校验和验证一整套机制而HdfsAPI和IO流操作则是我们日常开发中绕不开的两条路径。这篇文章就围绕这三个核心点展开HDFS上传下载的底层原理、HdfsAPI的实战用法、以及基于IO流的灵活操作方式。适合已经了解HDFS基本概念、但在实际开发中经常被各种异常卡住的工程师也适合想从“会用命令”进阶到“理解机制”的运维同学。我会尽量把每个环节的“为什么”讲清楚比如为什么块大小默认是128MB而不是更大或更小为什么上传时要先写副本再确认为什么IO流方式在某些场景下反而比API更可靠。同时附上可直接复现的代码和参数配置以及我在实际项目中踩过的坑和总结的排查技巧。2. HDFS上传与下载的底层原理拆解2.1 一次上传到底经历了什么从客户端到DataNode的完整链路很多人用hdfs dfs -put用得很顺手但问到“这个文件是怎么落到磁盘上的”就说不清楚了。我试着用一次典型的上传过程来还原整条链路。客户端发起上传请求时首先联系的是NameNode而不是直接找DataNode。NameNode收到请求后做三件事检查目标路径是否存在、检查客户端是否有写权限、然后在元数据中创建一条新记录但此时记录的状态是“未完成”的。接着NameNode会根据机架感知策略和副本因子默认3返回一组DataNode地址给客户端。这里有个细节返回的DataNode是按网络拓扑距离排序的客户端会优先选择最近的节点建立连接。客户端拿到地址列表后开始分块写入。假设文件是300MB块大小128MB那么会被切成3个块128MB、128MB、44MB。每个块独立走一遍“建立流水线—写入—确认”的流程。流水线的建立方式是客户端只和第一个DataNode建立TCP连接第一个DataNode再和第二个建立连接第二个和第三个建立连接形成一条链。数据包沿着这条链依次传递每个节点在收到数据后先写入本地磁盘同时转发给下游节点。下游节点写完后再逐级回传ACK确认。这里有个容易被忽略的点客户端并不是等一个块完全写完才写下一个块。实际上当第一个块的数据包还在流水线上传输时客户端已经开始发送第二个块的数据了。这种流水线并行机制是HDFS高吞吐的关键。但这也意味着如果某个DataNode响应变慢整个流水线都会被拖累客户端会触发超时重试或节点替换。注意上传过程中如果客户端进程崩溃NameNode上那条“未完成”的记录不会立即清除需要等租约超时默认60分钟后才会被回收。这就是为什么有时候你重新上传同名文件会报“文件已存在”或“租约冲突”。2.2 下载为什么比上传快短路读与本地化策略下载的链路和上传方向相反但机制上有几个显著差异。客户端向NameNode请求读取某个文件时NameNode返回的是该文件所有块的DataNode位置列表按距离排序。客户端会优先选择最近的DataNode读取如果客户端本身就在某个DataNode所在的物理机上还可以启用短路读——直接读本地磁盘文件跳过TCP传输。这个优化在大数据计算场景下非常关键比如MapReduce任务调度到数据所在节点时读取几乎不消耗网络带宽。另一个差异是下载不需要租约。上传时客户端要持有写租约才能写入但读取是无状态的多个客户端可以同时读同一个文件。这也解释了为什么HDFS适合“一次写入、多次读取”的场景——写入路径复杂且串行化程度高读取路径简单且可以高度并行。校验和机制在下载时也会生效。每个块在写入时都会计算校验和并单独存储客户端读取时会重新计算并比对。如果发现不匹配客户端会向NameNode报告坏块并从其他副本重新读取。这个机制保证了数据完整性但也带来一个副作用如果某个DataNode的磁盘出现静默损坏读取时才会暴露所以定期跑fsck是必要的运维动作。2.3 块大小为什么是128MB一个需要算账的决策很多人问过为什么HDFS的块大小默认是128MB而不是像传统文件系统那样几KB。这个问题需要从寻址开销和传输时间两个维度来算账。假设块大小设为1MB一个1GB的文件需要1000个块NameNode要维护1000条元数据记录每次读取都要多次寻址。而如果块大小设为1GB一个1GB的文件只占1个块但会导致数据分布不均——一个小文件占用一个完整块浪费存储空间同时并行度下降因为一个块只能由一个任务处理。128MB这个值是怎么来的考虑典型机械硬盘的顺序读取速度约100MB/s网络带宽约1Gb/s约125MB/s。如果块大小是128MB那么传输一个块的时间大约是1秒左右而寻址时间定位到块起始位置大约是几十毫秒。寻址时间与传输时间的比值控制在1%左右这是一个比较理想的平衡点。如果块太小寻址开销占比上升如果块太大并行度下降且故障恢复时间变长。当然这个值不是固定的。在SSD普及和万兆网络环境下很多集群会把块大小调到256MB甚至512MB。调整的依据是块大小 ≈ 网络带宽 × 传输时间目标。你可以根据自己集群的实际带宽和磁盘性能来重新计算。3. HdfsAPI实战从环境准备到增删改查3.1 环境准备与依赖配置的避坑指南用Java操作HDFS第一步是把依赖搞对。我见过太多人卡在NoClassDefFoundError或Connection refused上其实大部分是依赖版本和配置的问题。先说依赖。如果你用的是Maven核心依赖是hadoop-client但不要直接引这个聚合包它会带入大量你不需要的依赖导致冲突。我的做法是按需引入dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-common/artifactId version3.3.4/version /dependency dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-hdfs-client/artifactId version3.3.4/version /dependency版本选择上客户端版本最好和服务端大版本一致小版本可以不同。比如服务端是3.3.x客户端用3.3.4没问题但用2.7.x就可能出现RPC协议不兼容。配置文件方面把core-site.xml和hdfs-site.xml放到classpath下是最省事的做法。但如果你像我一样经常需要在多个集群之间切换硬编码配置文件就很痛苦。更好的方式是用代码动态设置Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfs://namenode-host:8020); conf.set(dfs.replication, 3); conf.set(dfs.client.socket-timeout, 60000); FileSystem fs FileSystem.get(conf);这里有个坑fs.defaultFS的端口不一定是8020。不同发行版和配置下可能是9000、8020、9001等。最可靠的方式是去NameNode的Web UI上看或者查core-site.xml里的实际配置。实操心得在IDE里跑HDFS客户端代码时如果报UnknownHostException大概率是hosts文件没配。把NameNode和DataNode的主机名映射加到本机hosts里比改代码用IP地址更稳妥因为HDFS的块位置信息里返回的是主机名。3.2 上传文件的三种姿势与性能对比用HdfsAPI上传文件常见的有三种方式各有适用场景。第一种fs.copyFromLocalFile()。这是最省事的方法一行代码搞定fs.copyFromLocalFile( new Path(/local/path/file.log), new Path(/hdfs/path/file.log) );它内部封装了IO流的读写和关闭逻辑适合快速脚本和小文件。但缺点是灵活性差你没法控制块大小、副本因子、缓冲区大小这些参数。而且如果本地文件很大它会一次性占用较多内存。第二种FSDataOutputStream手动写入。这种方式给了你完全的控制权FSDataOutputStream out fs.create( new Path(/hdfs/path/file.log), true, // overwrite 4096, // buffer size (short) 3, // replication 128 * 1024 * 1024 // block size ); FileInputStream in new FileInputStream(/local/path/file.log); byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead in.read(buffer)) 0) { out.write(buffer, 0, bytesRead); } out.close(); in.close();这里的关键参数是buffer size。默认是4096字节对于大文件来说太小了会导致频繁的系统调用。我一般设成64KB到128KB实测吞吐量能提升20%以上。但也不要设太大超过1MB反而可能因为内存拷贝开销导致性能下降。第三种FileSystem.create()配合Progressable。如果你需要监控上传进度可以传入一个回调FSDataOutputStream out fs.create( new Path(/hdfs/path/file.log), new Progressable() { Override public void progress() { System.out.println(Uploading...); } } );这个回调在每个数据包默认64KB写入后被调用一次可以用来做进度条或心跳保活。三种方式的性能对比我做过一个简单的测试上传一个1GB的文件copyFromLocalFile耗时约12秒手动流式写入128KB缓冲区耗时约10秒带进度回调的流式写入耗时约11秒。差异主要来自缓冲区大小和是否有多余的封装开销。3.3 下载文件的正确打开方式下载文件同样有几种方式但坑比上传少一些。最直接的是fs.copyToLocalFile()fs.copyToLocalFile( new Path(/hdfs/path/file.log), new Path(/local/path/file.log) );它会把HDFS文件完整下载到本地。如果本地已存在同名文件会直接覆盖不会报错。如果你需要边读边处理比如逐行解析日志那就用FSDataInputStreamFSDataInputStream in fs.open(new Path(/hdfs/path/file.log)); BufferedReader reader new BufferedReader(new InputStreamReader(in)); String line; while ((line reader.readLine()) ! null) { // 处理每一行 } reader.close();这里有个性能陷阱readLine()在遇到超大文件时可能因为单行过长导致内存溢出。更安全的做法是用固定大小的缓冲区读取byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead in.read(buffer)) 0) { // 处理buffer中的bytesRead个字节 }还有一个高级用法是随机读取。FSDataInputStream支持seek()方法可以跳到文件的任意位置FSDataInputStream in fs.open(new Path(/hdfs/path/file.log)); in.seek(1024 * 1024); // 跳到1MB位置 byte[] buffer new byte[4096]; in.read(buffer);这个功能在读取大文件的特定片段时非常有用比如你想看一个10GB日志文件的最后1MB不需要把整个文件读一遍。注意seek()之后如果跨了块边界客户端需要重新向NameNode请求下一个块的位置信息会有一次额外的RPC开销。所以频繁的小范围seek不如一次性顺序读取效率高。3.4 元数据操作列出、删除、重命名与权限除了读写文件内容日常开发中大量操作其实是针对元数据的。列出目录内容用listStatus()FileStatus[] statuses fs.listStatus(new Path(/hdfs/path)); for (FileStatus status : statuses) { System.out.println(status.getPath().getName() status.getLen() status.getModificationTime()); }FileStatus对象包含了文件大小、修改时间、权限、副本数等信息。如果你只需要文件名用listFiles()会更高效它返回的是RemoteIterator支持懒加载适合目录下文件很多的场景。删除文件用delete()boolean deleted fs.delete(new Path(/hdfs/path/file.log), false);第二个参数是recursive删除目录时必须设为true否则会抛异常。这里有个坑delete()返回true只表示删除操作被接受不保证文件立即从磁盘上消失。NameNode会先把文件标记为删除实际的数据块回收是异步的。重命名用rename()boolean renamed fs.rename( new Path(/hdfs/path/old.log), new Path(/hdfs/path/new.log) );rename()在HDFS上是原子操作但仅限于同一个文件系统内。跨文件系统重命名会失败。权限操作和Linux类似用setPermission()和setOwner()fs.setPermission(new Path(/hdfs/path/file.log), new FsPermission(644)); fs.setOwner(new Path(/hdfs/path/file.log), user, group);但要注意HDFS的权限模型比Linux简化很多没有setuid/setgid这些高级特性。而且如果集群没开启权限检查dfs.permissions.enabled为false这些设置只是记录在元数据里不会真正生效。4. IO流操作HDFS更灵活但也更容易踩坑的路径4.1 为什么有时候必须用IO流而不是APIHdfsAPI虽然方便但在某些场景下会显得力不从心。我遇到过几个典型情况一是需要和已有的IO框架集成。比如你有一个现成的数据处理管道输入是InputStream输出是OutputStream这时候用FSDataInputStream和FSDataOutputStream直接对接最自然不需要先把数据落到本地临时文件再上传。二是需要精细控制读写过程。比如你想在上传过程中对数据进行加密或压缩用API的copyFromLocalFile就没法插入处理逻辑而用IO流可以在read和write之间做任意变换。三是需要处理非标准格式的数据。比如从Kafka消费到的消息直接写入HDFS或者从HDFS读取数据后直接推送到另一个消息队列这些场景用IO流比API更直接。但IO流也意味着你要自己管理资源关闭、异常处理、缓冲区大小这些细节稍不注意就会导致连接泄漏或数据损坏。4.2 用IO流实现一个带压缩的上传管道我拿一个实际需求来演示把本地日志文件压缩后上传到HDFS减少存储空间和网络传输量。Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfs://namenode-host:8020); FileSystem fs FileSystem.get(conf); // 输入本地文件 InputStream localIn new FileInputStream(/local/path/app.log); // 压缩层 GZIPOutputStream gzipOut null; // 输出HDFS FSDataOutputStream hdfsOut fs.create( new Path(/hdfs/path/app.log.gz), true ); gzipOut new GZIPOutputStream(hdfsOut); byte[] buffer new byte[65536]; int bytesRead; try { while ((bytesRead localIn.read(buffer)) 0) { gzipOut.write(buffer, 0, bytesRead); } } finally { // 关闭顺序很重要先关压缩流再关HDFS流 if (gzipOut ! null) { gzipOut.close(); // 会flush压缩数据并关闭底层流 } localIn.close(); }这段代码有几个关键点。第一关闭顺序必须是先关GZIPOutputStream再关FSDataOutputStream因为GZIP流在关闭时会写入压缩尾部信息如果先关HDFS流这些尾部信息就丢了导致压缩文件损坏。第二缓冲区大小设成64KB这是压缩场景下的经验值太小会导致压缩率下降太大则内存占用高。第三异常处理里要确保两个流都被关闭否则HDFS上的文件会处于“未完成”状态。实测下来一个1GB的文本日志压缩后大约200MB上传时间从12秒降到4秒左右效果很明显。4.3 用IO流做断点续传的思路与实现断点续传是IO流操作HDFS的一个高级用法。HDFS本身不直接支持断点续传但可以通过append()方法来实现。思路是这样的上传前先检查目标文件是否存在如果存在则获取其当前大小然后从本地文件的对应位置继续读取并追加。Path hdfsPath new Path(/hdfs/path/large-file.dat); Path localPath new Path(/local/path/large-file.dat); long localSize new File(localPath.toUri()).length(); long hdfsSize 0; if (fs.exists(hdfsPath)) { hdfsSize fs.getFileStatus(hdfsPath).getLen(); } if (hdfsSize localSize) { System.out.println(File already fully uploaded); return; } FSDataOutputStream out; if (hdfsSize 0) { // 追加模式 out fs.append(hdfsPath); } else { out fs.create(hdfsPath); } // 跳过已上传的部分 RandomAccessFile localFile new RandomAccessFile( localPath.toUri().getPath(), r); localFile.seek(hdfsSize); byte[] buffer new byte[65536]; int bytesRead; while ((bytesRead localFile.read(buffer)) 0) { out.write(buffer, 0, bytesRead); } out.close(); localFile.close();这里有几个坑要特别注意。第一append()操作需要持有租约如果同一个文件有多个客户端同时追加会抛AlreadyBeingCreatedException。第二追加操作只能由创建文件的客户端执行其他客户端追加会失败。第三如果追加过程中客户端崩溃文件会处于不一致状态需要手动干预。所以断点续传更适合“单客户端、低频次”的场景。如果是多客户端并发写入建议用临时文件加最终重命名的方式而不是直接追加。4.4 IO流操作中的缓冲区与刷新策略缓冲区大小和刷新策略直接决定了IO流操作的性能和可靠性。先说缓冲区。FSDataOutputStream内部有一个默认64KB的缓冲区数据先写入这个缓冲区满了之后才打包成数据包发送到DataNode。如果你在应用层再加一层缓冲区比如用BufferedOutputStream包装就变成了双重缓冲反而增加内存拷贝开销。我的建议是应用层缓冲区大小设为HDFS内部缓冲区的一半到一倍即32KB到64KB这样既能减少系统调用又不会和内部缓冲冲突。刷新策略方面FSDataOutputStream提供了hflush()和hsync()两个方法。hflush()确保当前缓冲区的数据被发送到所有DataNode并收到ACK但不保证DataNode把数据刷到磁盘。hsync()则进一步要求DataNode执行fsync确保数据落盘。这两个方法的代价差异很大。hflush()的延迟在毫秒级hsync()可能达到几十毫秒甚至上百毫秒。所以如果你的场景对数据可靠性要求极高比如金融交易日志每次写入后调hsync()如果只是普通日志定期调hflush()就够了或者干脆依赖关闭流时的自动刷新。实操心得在写入大量小文件时频繁调用hflush()会成为性能瓶颈。我一般会攒够一定数量的记录比如1000条或者达到一定大小比如1MB再调一次在可靠性和吞吐量之间取平衡。5. 常见问题与排查技巧实录5.1 上传下载中的典型异常与速查表下面这张表整理了我这些年遇到的高频异常和对应的排查方向基本覆盖了90%的场景。异常信息可能原因排查方向解决方案Connection refusedNameNode或DataNode未启动或端口不对检查服务进程和端口配置启动服务核对fs.defaultFS端口UnknownHostException主机名无法解析检查本机hosts文件和DNS添加主机名映射AlreadyBeingCreatedException文件正在被其他客户端写入检查是否有并发写入等待租约超时或联系持有者释放Could not obtain block所有副本所在DataNode都不可用检查DataNode状态和网络等待节点恢复或调整副本因子ChecksumException数据块校验和不匹配检查磁盘是否损坏删除坏块并从其他副本恢复SafeModeExceptionNameNode处于安全模式检查NameNode日志等待退出安全模式或手动触发DiskOutOfSpaceExceptionDataNode磁盘空间不足检查各节点磁盘使用率清理空间或扩容LeaseExpiredException写租约超时检查客户端是否长时间无响应重新发起写入这张表里的每一条我都在实际环境中遇到过。其中AlreadyBeingCreatedException和LeaseExpiredException是最让人头疼的因为它们往往不是代码问题而是环境或流程问题。我的经验是写HDFS的代码一定要加超时和重试逻辑不要假设一次就能成功。5.2 小文件问题的根源与合并策略HDFS最被诟病的就是小文件问题。每个文件无论多小NameNode都要维护一条元数据记录大约150字节。如果有一亿个小文件光元数据就要占用15GB内存这对NameNode来说是巨大的压力。小文件问题的根源往往在写入端。比如日志采集场景如果每个日志事件都写一个文件那必然产生海量小文件。解决办法是在写入前做合并。一种方式是在客户端做缓冲攒够一定大小再写入HDFS。比如用BufferedOutputStream包装FSDataOutputStream设置缓冲区为128MB这样只有缓冲区满了才会真正创建一个HDFS文件。但这种方式的问题是如果客户端崩溃缓冲区里的数据就丢了。更可靠的方式是两阶段写入先写到本地临时文件达到阈值后再上传到HDFS并删除本地文件。这种方式牺牲了一些实时性但保证了数据不丢。还有一种方式是用HDFS的归档功能HAR或存储策略把多个小文件打包成一个大文件。但HAR文件读取时需要解包对上层应用有侵入性所以用得不多。我的建议是在架构设计阶段就把小文件问题考虑进去尽量在数据源头做聚合。如果实在避免不了定期跑合并任务把历史小文件合并成大文件。5.3 权限与用户切换的坑HDFS的权限模型和Linux类似但有一个很大的差异HDFS客户端默认使用当前操作系统用户作为HDFS用户。这意味着如果你在Linux上用root用户跑程序写入HDFS的文件owner就是root如果用hadoop用户跑owner就是hadoop。这个机制在单机环境下没问题但在跨平台场景下会出问题。比如你在Windows上开发用IDE跑程序HDFS客户端会尝试用Windows用户名去认证而HDFS集群里根本没有这个用户就会报权限错误。解决办法有两种。一是在代码里显式设置用户System.setProperty(HADOOP_USER_NAME, hadoop);二是在Configuration里设置conf.set(hadoop.user.name, hadoop);但要注意这种方式只是“声称”自己是某个用户并没有真正的认证。如果集群开启了Kerberos认证这种方式就行不通了必须走正规的认证流程。注意在生产环境中不要用System.setProperty这种方式硬编码用户名应该通过配置文件或环境变量来管理避免安全风险。5.4 网络抖动与超时参数调优HDFS客户端有一系列超时参数默认值在稳定网络环境下没问题但在跨机房或网络质量差的环境下经常触发超时。关键参数有这几个dfs.client.socket-timeout客户端与DataNode的socket超时默认60秒。dfs.datanode.socket.write.timeoutDataNode写数据的超时默认480秒。dfs.client.block.write.retries写块失败后的重试次数默认3次。ipc.client.connect.timeoutRPC连接超时默认20秒。ipc.client.connect.max.retriesRPC连接重试次数默认10次。在网络不稳定的环境下我会把socket-timeout调到120秒block.write.retries调到5次。但也不要调得太大否则一个故障节点会拖慢整个写入过程。更好的做法是配合节点排除机制当某个DataNode连续失败多次后客户端会把它加入黑名单后续写入不再选择这个节点。conf.set(dfs.client.socket-timeout, 120000); conf.set(dfs.client.block.write.retries, 5); conf.set(dfs.client.block.write.replace-datanode-on-failure.enable, true); conf.set(dfs.client.block.write.replace-datanode-on-failure.policy, DEFAULT);最后这个replace-datanode-on-failure策略很关键。设为DEFAULT时如果流水线中的某个DataNode失败客户端会申请一个新的DataNode替换它而不是整个块重写。这大大提高了写入的容错能力。5.5 监控与日志出问题时看什么HDFS客户端出问题时光看应用日志往往不够还需要结合服务端日志和监控指标。客户端侧可以开启DEBUG日志Logger.getLogger(org.apache.hadoop).setLevel(Level.DEBUG);这会打印出每次RPC调用、每个数据包的发送和ACK情况对定位问题非常有帮助。但DEBUG日志量很大只建议在排查问题时临时开启。服务端侧NameNode的日志在logs/hadoop-namenode-*.logDataNode的日志在logs/hadoop-datanode-*.log。重点看BlockReceiver和DataXceiver相关的记录它们会记录每个块的接收和发送情况。监控方面NameNode的Web UI默认50070端口可以看到集群的整体状态、文件系统容量、活跃节点数等。如果发现Missing blocks或Under-replicated blocks数量上升说明有DataNode出了问题需要尽快处理。我个人的习惯是在客户端代码里加一个简单的统计记录每次上传下载的耗时、字节数、重试次数定期输出到日志。这样即使没有专业的监控系统也能快速发现性能退化或异常趋势。6. 一些实战中的经验碎片写HDFS客户端代码这些年积累了一些不太成体系但很实用的经验放在这里供参考。关于副本因子不要盲目用默认的3。如果数据重要性不高且存储成本敏感可以设为2如果是临时中间数据设为1也可以。但要注意副本因子是在文件创建时确定的后续修改只能通过setReplication()调整而且调整是异步的不会立即生效。关于块大小如果你的文件普遍小于128MB可以考虑调小块大小减少存储浪费。但块大小一旦设定已有文件不会自动调整只影响新写入的文件。关于并发写入HDFS不支持多客户端同时写同一个文件。如果业务上确实需要并发写要么在应用层做分片每个客户端写不同的文件要么用其他存储系统。我见过有人试图用append()实现并发写结果就是不断遇到租约冲突最后不得不重构。关于测试建议在开发环境用MiniDFSCluster做单元测试它可以在本地JVM里启动一个完整的HDFS集群不需要依赖外部环境。虽然启动慢一点但能覆盖大部分API和IO流场景比连真实集群测试更可控。Configuration conf new Configuration(); conf.set(dfs.replication, 1); MiniDFSCluster cluster new MiniDFSCluster.Builder(conf) .numDataNodes(1) .build(); FileSystem fs cluster.getFileSystem(); // 在这里跑测试 cluster.shutdown();最后说一个数据一致性的细节。HDFS保证的是“写入关闭后可见”也就是说在out.close()返回之前其他客户端读不到这个文件的内容。如果你需要“写入即可见”那HDFS不适合这个场景需要考虑其他方案。这个特性在大多数批处理场景下没问题但在实时性要求高的场景下需要特别注意。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑