资讯动态

从Shell到Java API:HDFS文件系统操作与MapReduce提交实战

发布时间:2026/10/20 2:10:13 来源:尧图企业网站定制
前几篇学习笔记里我的日常操作基本被HDFS Shell命令承包了上传用hdfs dfs -put下载用hdfs dfs -get查目录用hdfs dfs -ls提交任务用hadoop jar。工具用顺手之后我开始琢磨一个很现实的问题如果要在Java服务里直接读写HDFS这些Shell命令就完全帮不上忙了。Hadoop的Java API承载的正是这件事它也是我Hadoop学习历程中的第9篇记录。这一篇不打算把官方文档从头到尾抄一遍那样没有意义。我更想记录的是自己真正用到的部分环境怎么搭、依赖怎么引、HDFS的增删改查怎么写、流怎么读怎么写、MapReduce任务怎么从客户端提交上去以及那些让初学者卡壳的报错到底该怎么排查。如果你也刚跨过Shell这道坎或者正准备把HDFS能力接进自己的Java项目这份记录应该能让你少绕几个弯。1. 从Shell命令到Java API编程视角的转变1.1 Shell命令力不从心的真实场景Shell命令适合运维人员手动操作但一旦进入“程序自动完成任务”的场景它的局限立刻暴露。我在学完Shell之后试着做了两个练习第一个是“把程序生成的日志实时写入HDFS”。模拟程序每隔几秒产出一条日志用Shell手工执行put显然不现实因为手动操作赶不上生成速度也不可能让程序去调用Shell进程。第二个练习是“批量清洗数据后写回HDFS”前置步骤需要先判断目标目录是否存在如果存在就清空重建再把清洗结果按日期分区写进去。这一套逻辑用Shell脚本也能写但纯脚本处理权限、超时、文件句柄、异常恢复都格外别扭。更常见的场景是产品化。业务系统把数据上传到某中间服务服务端再把数据落进HDFS或者读HDFS做后续计算。这种调用发生在Web服务内部后端代码只能通过编程接口去访问文件系统Shell命令在这个链路里没有位置。总结下来就是Shell负责“人指挥机器”Java API负责“程序指挥机器”两者解决的问题不一样但后者才是数据平台被业务系统调用的基础。1.2 FileSystem抽象理解整个API的钥匙刚开始学Java API时我花了不少时间找“HDFS自己的类”翻来翻去总觉得入口很散。后来才意识到整个Hadoop客户端API的核心不是某个叫“HDFSClient”的类而是一个抽象类org.apache.hadoop.fs.FileSystem。这个类把所有文件系统统一成一套操作模型。你在代码里调FileSystem.get()它会根据URI里的协议返回对应的实现hdfs://开头返回DistributedFileSystemfile://开头返回LocalFileSystems3a://开头返回S3对象存储的客户端。也就是说你不需要针对“HDFS”去学一套专属API学会FileSystem之后本地文件、HDFS、甚至部分对象存储都能用同一套增删改查逻辑操作。建立这个认知以后后续所有代码都变得好理解了一切都是Path一切操作都发生在FileSystem上。2. 环境准备依赖坐标、版本对应与FileSystem获取2.1 Maven依赖该怎么引入学习阶段强烈建议直接就引一个hadoop-client它把hadoop-common、hadoop-hdfs、hadoop-mapreduce-client-core等常用模块打包在一起一个依赖就能覆盖HDFS和MapReduce两部分的API省去很多挨个找坐标的麻烦。我在模拟项目X里用的是服务端一致的版本。因为Hadoop客户端跟服务端的版本存在兼容问题如果服务端是3.x客户端引用一个2.x的包运行时很容易出现ProtocolException或者某些类找不到。稳妥做法是去集群上执行hadoop version把输出的版本号填到pom里。我这边集群是伪分布式版本是3.3.x于是pom里就这样写dependency groupIdorg.apache.hadoop/groupId artifactIdhadoop-client/artifactId version3.3.4/version /dependency引入之后IDEA会下载一大批依赖这是正常的。唯一要注意的是依赖冲突如果项目里还引了别的Hadoop相关组件版本不一致时优先跑mvn dependency:tree捋一遍把重复的旧版本排除掉。2.2 获取FileSystem实例的三种方式获取FileSystem实例是每次操作的第一步方式不同行为和适用场景也不同我整理了一张表方式关键写法适用场景只传ConfigurationFileSystem.get(conf)本机已有core-site.xml能自动读取fs.defaultFS显式URIFileSystem.get(URI.create(hdfs://...), conf)生产中最常用不依赖本地配置文件指定用户FileSystem.get(uri, conf, hdfs)需要以特定用户身份访问HDFS我一开始图省事只写FileSystem.get(conf)结果因为本机core-site.xml没有配置它默认返回了本地文件系统后续所有路径都按file:///解析报了一堆奇怪的错误。后来改成显式传入URI问题立刻消失。最重要的代码大概是这样的Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfs://localhost:9000); conf.setInt(dfs.replication, 1); FileSystem fs FileSystem.get(URI.create(hdfs://localhost:9000), conf);注意这里的端口不是固定不变的。你less自己hdfs-site.xml里的dfs.namenode.rpc-address是多少就用多少。我这边安装时把它改成了9000所以示例里都按这个来。另外要养成一个习惯一个进程里尽量复用同一个FileSystem实例。每次调用都new一个Client会反复建立RPC连接浪费大量时间和资源。客户端本身是线程安全的一个应用初始化一次后面传给各个方法使用就够。2.3 本地开发连接集群的隐形门槛在Windows或Mac本机连Linux虚拟机上的HDFS环境问题往往比代码本身更磨人。我踩过三个一是主机名解析不了报连接超时需要在hosts文件里加一条虚拟机主机名和IP的映射二是虚拟机的防火墙挡掉了NameNode的RPC端口三是HDFS客户端日志默认是INFO级别一跑起来满屏滚动看着吓人其实都是正常信息。前两个属于网络问题用ping 主机名和telnet 主机IP 9000就能定位。第三个加一个log4j配置文件就能安静下来我用的配置是log4j.rootLoggerWARN, stdout log4j.appender.stdoutorg.apache.log4j.ConsoleAppender log4j.appender.stdout.TargetSystem.out log4j.appender.stdout.layoutorg.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern%d{yyyy-MM-dd HH:mm:ss} %p %m%n这样只输出WARN以上的警告控制台干净很多紧急错误也不会被淹没。3. 把Shell命令翻译成Java代码核心API实战3.1 目录与文件的生命周期操作有了FileSystem实例以后Shell里的常用命令基本都能找到对应的Java方法。我建议把这两个概念挂在脑子里Shell里的hdfs dfs -mkdir -p对应fs.mkdirs()hdfs dfs -rm -r对应fs.delete(path, true)第二个布尔参数表示是否递归删除。我在本地练习时写了一段常用的初始化和清理逻辑Path dir new Path(/user/mydata/input); if (!fs.exists(dir)) { fs.mkdirs(dir); } Path oldPath new Path(/user/mydata/old); if (fs.exists(oldPath)) { fs.rename(oldPath, new Path(/user/mydata/archive)); } Path trash new Path(/user/mydata/trash); if (fs.exists(trash)) { fs.delete(trash, true); }需要特别提醒的是Path用的是Hadoop自己的org.apache.hadoop.fs.Path不是JDK里的java.nio.file.Path写import的时候千万别搞混。我第一次就写错了包名IDE直接报“找不到符号”排查了一会儿才反应过来。如果想查文件的详细信息用fs.getFileStatus(path)返回的FileStatus里有文件长度、块大小、副本数、修改时间、权限等属性。做校验、做同步任务的时候这方法很常用。3.2 文件上传与下载的参数陷阱上传和下载看起来各自只要一个方法调用但参数形式有讲究。我练习时用的方案是Path localPath new Path(C:/data/test.txt); Path hdfsPath new Path(/user/mydata/test.txt); fs.copyFromLocalFile(false, true, localPath, hdfsPath); Path downloadDest new Path(D:/backup/); fs.copyToLocalFile(false, hdfsPath, downloadDest);copyFromLocalFile(false, true, src, dst)里第一个布尔参数表示是否删除本地源文件第二个表示HDFS上目标已存在时是否覆盖。我第一次用两参数的默认重载结果HDFS上已经存在同名文件时报了FileAlreadyExistsException研究之后才发现可以显式传overwrite参数。下载时同样要注意copyToLocalFile(false, src, dst)里的false表示不删除HDFS上的源文件。下载的目标路径在部分版本中不能是一个已经存在的文件如果本地文件已经在了先删掉或者换个路径再执行。Windows开发环境还有个不大不小的问题文件路径里的反斜杠。Java字符串里写C:/data/test.txt是最省事的方式统一用正斜杠避免转义出错。到了Linux服务器上编译执行路径风格本身没问题但需要注意文件分隔符生产环境我一般先new Path(file:///data/test.txt)这样的URI形式更严谨。3.3 文件遍历listFiles与listStatus怎么选遍历目录有两个方向一个是fs.listFiles(path, recursive)它返回RemoteIteratorLocatedFileStatus支持递归另一个是fs.listStatus(path)一次性返回FileStatus[]数组默认不递归。什么时候用哪个我用一次实际需求来举例模拟一个“按日期统计HDFS上文件总大小”的程序。RemoteIteratorLocatedFileStatus iterator fs.listFiles(new Path(/user/mydata), true); long totalSize 0; while (iterator.hasNext()) { LocatedFileStatus status iterator.next(); if (status.isFile()) { totalSize status.getLen(); System.out.println(status.getPath().toString() - status.getLen()); } } System.out.println(total size: totalSize);之所以用listFiles而不是listStatus是因为我需要递归扫描多层目录而且LocatedFileStatus额外携带了每个文件的数据块位置信息打印出来能看到当前Block所在的数据节点。这个Block位置在将来做数据本地化、判断数据倾斜、排查热点文件时都很有用。如果只是列一下某目录下有哪些文件和子目录不需要递归那listStatus更合适代码也更简单for (FileStatus st : fs.listStatus(new Path(/user/mydata))) { System.out.println((st.isDirectory() ? [dir] : [file] ) st.getPath()); }两个方法底层都有RPC开销但在合理范围内。要记住一点listFiles的递归如果层级特别深、文件特别多遍历时间会比较长生产环境建议分目录过滤一次不要全量扫。4. 绕不开的流式读写FSDataInputStream与FSDataOutputStream4.1 用OutputStream往HDFS写入数据FileSystem给文件读写提供了两个流类FSDataOutputStream用于写入FSDataInputStream用于读取。它们的用法跟普通Java IO流几乎一样所以只要会写本地文件迁移到HDFS的成本不高。写文件最常见的方式是fs.create(path)我强烈建议显式传第二个参数代表是否覆盖已存在文件Path outPath new Path(/user/mydata/out.txt); try (FSDataOutputStream out fs.create(outPath, true)) { out.writeBytes(line-01\n); out.writeBytes(line-02\n); out.writeBytes(line-03\n); }看到这里有人可能会有疑问写了之后数据是立刻落盘吗并不是。客户端的写操作先进入缓冲区由DFSClient分批发送到流水线上的DataNode最终close()时才会完整落地并关闭租约。所以写完必须关流不关流的情况下另一个进程去读可能读不全去追加写可能拿不到租约直接报错。如果只是想在文件末尾追加内容可以改用fs.append(path)。需要注意HDFS默认开启append特性但如果没开会报AppendNotSupportedException这种情况需要检查dfs.support.append配置。4.2 用InputStream读数据以及seek定位的妙用读取文件我先从最朴素的方式开始一行一行或者按字节读。不同的是FSDataInputStream继承自DataInputStream还额外实现了Seekable接口也就是说它天然支持“跳到某个偏移量再读”。我做过一个“只读取文件后半部分”的练习Path inPath new Path(/user/mydata/out.txt); try (FSDataInputStream in fs.open(inPath)) { long fileLen fs.getFileStatus(inPath).getLen(); long half fileLen / 2; in.seek(half); // 直接跳到中间位置 byte[] buffer new byte[512]; int len; while ((len in.read(buffer)) ! -1) { System.out.print(new String(buffer, 0, len, StandardCharsets.UTF_8)); } }这个能力初看不值钱但它在分布式计算里很关键。Hadoop的MapReduce拆分输入分片时每个Split需要知道自己的起始偏移量和长度Map任务就是靠seek定位到自己那块数据的。我之前看源码时一直疑惑“怎么做到从某个Block开始读”弄懂seek的作用后整个数据分片机制通了一半。此外FSDataInputStream还提供readFully(long position, byte[] buffer)这种“指定位置读取”的方法不需要先seek再read。在实现随机读取、抽样检查、校验数据时很好用。4.3 一个实用组合合并小文件文件流读写最典型的组合实践是“合并小文件”。HDFS上如果堆积了大量几KB到几十KB的文件NameNode内存会被海量元数据吃掉所以把零碎小文件合并成大块是常见的维护操作。我用Java API写过一个通用合并逻辑扫描指定目录下的所有文件逐个读入统一写到一个输出文件里。核心代码如下Path srcDir new Path(/user/mydata/small); Path mergedPath new Path(/user/mydata/merged.bin); try (FSDataOutputStream out fs.create(mergedPath, true)) { RemoteIteratorLocatedFileStatus iterator fs.listFiles(srcDir, true); while (iterator.hasNext()) { LocatedFileStatus st iterator.next(); if (!st.isFile()) { continue; } try (FSDataInputStream in fs.open(st.getPath())) { IOUtils.copyBytes(in, out, 4096, false); } } }这里有个细节必须解释一下IOUtils.copyBytes的最后一个参数是“是否自动关闭输出流”我传了false因为out需要在循环结束后统一关闭。如果把true传进去第一轮循环结束就会把out关掉后面的文件就全写不进去。这个坑是我实际运行时才发现的第一版代码在合并到第二个文件时就抛了IOException: Stream closed。IOUtils用的是org.apache.hadoop.io.IOUtils这个工具类在处理Hadoop流时会自动处理一些客户端细节比手动循环读字节数组省心。5. 把MapReduce任务也用Java跑起来从客户端到集群5.1 最小可运行WordCount的完整代码第9篇之前的记录里我用Shell跑过自带的WordCount示例但那只是“执行别人写好的程序”。想用好Java API必须能自己写一个MapReduce出来。我从最经典的WordCount开始Hadoop官方示例的骨架非常稳定理解之后可以套用到绝大多数统计逻辑。我练习时把Mapper、Reducer、Driver三个部分写在了一个类里方便在IDE里直接调试。正式项目建议拆成独立文件但学习阶段这样写更紧凑能看清三者关系。import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; import java.util.StringTokenizer; public class WordCountDriver { public static class TokenizerMapper extends MapperObject, Text, Text, IntWritable { private static final IntWritable one new IntWritable(1); private final Text word new Text(); Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { StringTokenizer itr new StringTokenizer(value.toString()); while (itr.hasMoreTokens()) { word.set(itr.nextToken()); context.write(word, one); } } } public static class IntSumReducer extends ReducerText, IntWritable, Text, IntWritable { private final IntWritable result new IntWritable(); Override protected void reduce(Text key, IterableIntWritable values, Context context) throws IOException, InterruptedException { int sum 0; for (IntWritable val : values) { sum val.get(); } result.set(sum); context.write(key, result); } } public static void main(String[] args) throws Exception { Configuration conf new Configuration(); Job job Job.getInstance(conf, word count); job.setJarByClass(WordCountDriver.class); job.setMapperClass(TokenizerMapper.class); job.setCombinerClass(IntSumReducer.class); job.setReducerClass(IntSumReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }这里有两个初学者容易忽略的点。一个是job.setJarByClass(WordCountDriver.class)少了这一行在集群上提交时YARN无法定位用户的jar包会报“找不到主类”或“ClassNotFound”。另一个是setMapOutputKeyClass和setMapOutputValueClass。这个例子里Mapper的输出类型和Reducer的最终输出类型恰好一致省略掉也能跑但一旦Map阶段和Reduce阶段类型不同省略就会导致类型不匹配所以建议每次都显式写上养成习惯。waitForCompletion(true)会阻塞客户端直到作业结束同时把Map任务和Reduce任务的进度百分比打印到控制台。学习阶段用这个最直观能清楚看到作业推进到哪一步。5.2 本地模式与集群模式的切换很多教程会让人一上来就把任务提交到YARN但我觉得先搞清楚运行模式更高效。MapReduce支持本地模式和集群模式切换开关是mapreduce.framework.name。在IDE里调试阶段我使用本地模式配置很简单conf.set(mapreduce.framework.name, local);本地模式并不会真的在“本地”把数据从HDFS拉下来输入输出路径仍然可以用hdfs://localhost:9000的HDFS地址。区别在于任务不会提交到YARN而是在当前JVM里用本地线程池模拟多个Map和Reduce任务执行。这样跑起来极快非常适合验证Map逻辑和Reduce逻辑是否正确。我发现控制台会打出类似Map task ...的进度但进程只有一个不会真的启动多容器。把这段配置去掉或者显式设置成yarn任务就会走正式提交流程。我在本机跑通WordCount后第二步是用Maven打成jar包传到服务器上用yarn jar命令提交。这里有一个我踩过的坑本地模式下waitForCompletion结束后输出目录可以重复利用但集群模式下如果输出目录已经存在任务会直接报Output directory ... already exists。可以理解为一个保护机制防止误覆盖但确实让第一次跑的人困惑。所以每次跑集群任务前记得先把上一次的输出目录删掉。把两种模式的区别整理成一张表方便对照对比项local模式yarn模式任务调度当前JVM内线程模拟YARN资源管理器调度容器部署环境IDE里直接跑需要jar包提交到集群执行速度快适合调试慢适合正式生产配置mapreduce.framework.namelocalmapreduce.framework.nameyarn输出目录判断较宽松存在即报错5.3 从提交到查看结果完整闭环包打成wcdemo-1.0.jar之后我在服务器上执行yarn jar wcdemo-1.0.jar WordCountDriver /user/mydata/input /user/mydata/wc-output执行期间控制台会持续输出Map和Reduce的进度条结束时能看到一堆Counter统计包括读取的行数、字节数、Spill次数、Shuffle字节数等。很多初学的人看到计数器只当作一堆数字但这些其实是最好的排障信息。最典型的是Map input records和自己预期的输入行数不一致时说明上游数据有问题或者文件没有按预期切分。任务跑完后去HDFS上看结果hdfs dfs -cat /user/mydata/wc-output/part-r-00000正常会看到类似word 2这样按字母排序的统计结果。如果想确认作业在YARN上的资源使用打开ResourceManager的Web界面任务结束也能在应用历史列表里找到对应记录。6. 学习Java API时踩过的坑与排查思路6.1 Permission denied并不是HDFS权限配错了第一次在Windows本机跑Java程序往HDFS写入就撞上一个典型的报错org.apache.hadoop.security.AccessControlException: Permission denied: userAdministrator, accessWRITE, inode/user/mydata:hdfs:supergroup:drwxr-xr-x我第一反应是HDFS权限配置有问题跑到Linux上检查了hdfs-site.xml发现权限完全正常。后来才明白HDFS的权限验证跟传统用户名密码不同它默认不校验密码而是“你声明你是谁你就是谁”。Java客户端默认会把当前操作系统的用户名作为HDFS用户身份上报Windows下这个用户名是Administrator在HDFS看来就是一个陌生的普通用户没有/user/mydata目录的写权限。解决思路不是去改HDFS全局权限而是让客户端以有权限的身份说话。我用了三种方法按推荐程度排列第一种在IDE或运行环境里设置环境变量HADOOP_USER_NAMEhdfsJava客户端启动时会读取这个变量作为用户身份。这是最快的方式适合本地调试。第二种用UserGroupInformation.createRemoteUser在代码里切换身份UserGroupInformation.createRemoteUser(hdfs).doAs((PrivilegedExceptionActionVoid) () - { // 在这里执行原有的文件系统操作 return null; });第三种最正规的是开启Kerberos之后做认证然后以票据身份访问HDFS。这一步涉及keytab和认证配置生产环境才需要学习阶段可以先不碰。这个报错的排查思路其实比解决本身更重要看到Permission denied不要先怀疑服务端权限配置先看报错里的user是谁再判断这个用户是否有对应目录权限。6.2 Wrong FS连接不上HDFS的“假故障”另一个高频报错长得很有迷惑性java.lang.IllegalArgumentException: Wrong FS: hdfs://localhost:9000/user/mydata, expected: file:///字面意思是要访问的HDFS路径被强制要求用本地文件系统去解析。我第一次看到直接懵了觉得“我明明写了hdfs协议怎么会期望file协议”。排查之后发现根因在获取FileSystem实例的那一步我当时用的是FileSystem.get(conf)而conf里没有fs.defaultFS这个配置于是底层按照默认值返回了本地文件系统的客户端。后面传入的路径带hdfs://前缀自然就跟当前客户端协议对不上。解决办法有两个方向。一个是在conf里显式设置fs.defaultFSconf.set(fs.defaultFS, hdfs://localhost:9000);另一个是获取时显式传URI这也是我更推荐的方式FileSystem fs FileSystem.get(URI.create(hdfs://localhost:9000), conf);除此之外还要注意保持路径风格一致。拿到HDFS客户端之后内部路径写不带协议的形式比如/user/mydata完全没问题但如果一会儿写hdfs://localhost:9000/user/mydata一会儿写/user/mydata某些内部调用仍然可能触发协议判断错误。我的习惯是获取Client时用带协议的URI后面所有业务路径统一用不带协议的Path。这个习惯坚持下来基本没有再遇到Wrong FS。6.3 Jar包冲突与依赖版本不一致学习阶段容易把各种新技术堆在同一个项目里一旦引入的Hadoop组件版本混杂就会出现一些非常难查的问题。我遇到过NoSuchMethodError: org.apache.hadoop.fs.FileSystem.get当时搜遍网络都没解决最后用mvn dependency:tree一看发现项目里同时存在2.7和3.3两个版本的hadoop-commonjar包一个类在高版本里改了签名另一个低版本类被加载进来倒是方法直接找不到了。Hadoop的jar包设计其实有点“重”里面夹带了大量依赖。如果你引入hadoop-client后又单独引了一个别的Hadoop生态组件比如某个解析库它内部可能带一份不同的Hadoop版本冲突概率会明显增加。遇到这种问题第一件事就是不要怀疑代码逻辑先做依赖树分析看看同一个类在classpath里出现了几次、来自哪个jar。找到之后用exclusions把低版本或多余的那个排除掉保留下与服务端一致的那个。日志轰炸也算一个变体的“环境坑”。不加log4j配置时即使一个简单的文件上传都会刷出几百行INFO让人误以为程序卡死。我建议所有学习项目从一开始就放一份log4j.properties直接把根级别设成WARN等真正需要排查问题时再临时打开INFO日志。6.4 流没有关闭导致租约无法释放前面写流式读写时提到过“必须关流”这里补充一个我印象很深的真实过程。有一次我写了一个程序循环写入文件第二遍运行时莫名报错提示目标文件“does not exist”可我明明刚创建过。排查了很久才发现是上一次运行里有一个FSDataOutputStream没有关闭。HDFS为了保证并发写安全会为正在写的文件建立租约没关流之前租约不会释放。第二次运行想重新创建同名文件NameNode认为这个文件还在被另一个客户端控制于是拒绝操作。程序里没有打印异常堆栈只显示“文件不存在”极具迷惑性。后来我给自己定了两条规矩。第一所有流都用try-with-resources方式写在括号里让JVM保证关闭try (FSDataOutputStream out fs.create(path, true)) { // 写入逻辑 }第二如果某些场景下确实没法用try-with-resources就在finally块里调用IOUtils.closeStream(stream)。另外还要注意一个连接对应着不少底层资源用完的FileSystem客户端如果长期不再使用也应调用fs.close()释放不过绝不能在每个方法里反复创建和关闭那会导致大量RPC连接建立和销毁性能很差。最后再分享一个小技巧调试Java API时我喜欢在代码里临时打印当前客户端的类名System.out.println(fs.getClass().getName()); System.out.println(fs.getScheme());看到DistributedFileSystem和hdfs说明连的是HDFS看到LocalFileSystem和file说明连的是本地文件系统。这个几秒钟的检查能避开大量路径协议、权限、连接类的低级问题也是我后续写更复杂的Hadoop客户端代码时一直保留的排查习惯。

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

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

免费获取报价 →
↑