☰
Hadoop安装与HDFS/HBase/MapReduce编程实践及实验统计避坑
2026/10/26 11:13:18 网站建设 项目流程

做大数据实验那几周,我印象最深的不是哪个API难写,而是第一台虚拟机的hdfs namenode -format我敲了四遍。第一遍忘了改core-site.xml里的主机名,第二遍 JDK 路径写到了 JRE 上,第三遍 SSH 免密只做了单向,第四遍才真正跑通。后来带别人做同样的四个实验,我发现大家踩的坑高度重合:安装阶段浪费一半时间,编程阶段又被依赖版本折腾掉另一半。这篇就把 Hadoop 安装及使用、HDFS 编程实践、HBase 编程实践、MapReduce 编程实践这四块串成一条线来讲,重点说清楚每一步为什么要这么配、出错时从哪查起、实验数据该怎么记才有对比价值。适合刚接触大数据组件、准备动手搭环境写代码的同学,也适合已经跑通过一遍但说不清原理的人对照复盘。

1. 四个实验模块其实是一条依赖链,不是四道独立题

很多人把这四个实验当成四个互不相干的任务,装完 Hadoop 就想跳过 HDFS 直接写 HBase,结果卡在Connection refused。先理清依赖关系,后面能省下大量返工。

1.1 从存储到计算的分层关系

HDFS 是底座,负责把文件切成块分散存到多台机器上;YARN 是资源调度层,负责把集群的 CPU 和内存切给各个任务;MapReduce 是跑在 YARN 上的计算框架;HBase 则是架在 HDFS 之上的列式存储,它的 RegionServer 把数据最终写进 HDFS 的目录里。

这条链决定了实验顺序不能乱:Hadoop 装不好,HDFS 的put/get就用不了;HDFS 起不来,HBase 的hbase-site.xml里配的根目录就没有落脚点;YARN 没起来,MapReduce 提交作业会一直停在ACCEPTED状态。我在实验记录里专门画过一张依赖表,每次卡住先对着表看是哪一层没通。

层级组件依赖谁起不来的典型表现
存储HDFS无(只需 JDK 和网络)put报无法连接 NameNode
调度YARNHDFS作业卡在 ACCEPTED,容器不分配
计算MapReduceYARN + HDFS任务反复重试后失败
列存HBaseHDFS + ZooKeeperRegionServer 起后秒退

1.2 虚拟机规格与角色分配的一个务实方案

实验环境我一般用三台虚拟机:一台 master,两台 slave。每台给 4 核、8G 内存、50G 磁盘,这个配置跑通全部四个实验足够了。内存再往下压到 4G 也能跑,但 HBase 的 RegionServer 默认堆就吃 1G,加上 DataNode、NodeManager,很容易 OOM 被系统杀掉,表现出来的现象就是"进程莫名其妙没了",特别难查。

角色分配上,master 跑 NameNode、ResourceManager、HMaster、ZooKeeper;slave1 和 slave2 跑 DataNode、NodeManager、RegionServer、ZooKeeper。ZooKeeper 必须凑成奇数个节点,三个正好,别为了省资源只起一个——HBase 强依赖 ZooKeeper 做协调,单节点虽然能跑,但一挂全挂,实验里想观察故障切换就没戏了。

提示:三台机器的主机名、IP 映射必须在安装前一次性定好并写进/etc/hosts,中途改名会让 NameNode 元数据里的路径全部对不上,只能重新格式化。

1.3 快照比什么都重要

我第一次做这套实验时没做快照,改坏一个配置文件就从头重装,四天装了六遍。后来养成习惯:系统装完打一个快照,Hadoop 配置完打一个,HBase 装完再打一个。出问题十秒回滚,比任何排查技巧都高效。这条经验看着朴素,但在实验场景里的性价比高得离谱。

2. Hadoop 安装:配置文件里真正要改的只有那几个键

网上教程动辄让你改十几个文件,其实核心就四个 XML 加一个hadoop-env.sh。改多了反而容易互相冲突。

2.1 系统层准备:三个容易偷懒但必须做的动作

第一是 JDK。Hadoop 3.x 要求 JDK 8 或 11,装完必须确认JAVA_HOME指向的是 JDK 目录而不是 JRE 目录。区分方法很简单:JDK 目录下有bin/javac,JRE 没有。我曾经把JAVA_HOME写成/usr/lib/jvm/java-8-openjdk-amd64/jre,hadoop version能正常输出,但一跑 MapReduce 就报JAVA_HOME is not set,因为编译阶段需要 javac。

第二是 SSH 免密。NameNode 启动时要通过 SSH 拉起各节点的 DataNode,必须做双向免密——master 能免密登录 slave,slave 也要能免密登录 master,同时本机ssh localhost也要免密。做法是各节点生成密钥后互相ssh-copy-id。只做单向是最常见的疏漏,表现是start-dfs.sh卡住不动或者提示输入密码。

第三是主机名解析。用hostname确认当前主机名,/etc/hosts里补上三台机器的映射,然后ping master验证。Ubuntu 系还要注意/etc/hostname和hosts里的名字必须完全一致,大小写敏感。

2.2 五个配置文件,逐个说清改了什么、为什么改

hadoop-env.sh里只需要加一行export JAVA_HOME=/你的JDK路径。为什么单独改这个而不是依赖系统环境变量?因为start-dfs.sh是通过 SSH 在远端执行的,非交互式 shell 不加载~/.bashrc,环境变量传不过去,必须在脚本里写死。

core-site.xml配两件事:fs.defaultFS指向hdfs://master:9000,以及hadoop.tmp.dir指定一个不会被系统清理的临时目录,比如/opt/module/hadoop-3.3.6/tmp。这个 tmp 目录非常关键——它默认在/tmp下,机器重启后可能被清空,NameNode 的元数据就丢了,下次启动直接报集群 ID 不一致。我吃过这个亏,后来一律显式指定。

hdfs-site.xml核心是dfs.replication,伪分布式写 1,完全分布式写 2 或 3。副本数不能超过 DataNode 数量,否则写入会一直报"只能复制到 0 个节点"。另外新版 Hadoop 不再依赖secondarynamenode节点配置,但dfs.namenode.secondary.http-address最好显式指定,避免 50090 端口冲突。

mapred-site.xml里把mapreduce.framework.name设为yarn,这一项决定了你的作业是跑在本地 JVM 里还是提交到集群。很多同学本地跑得好好的,一上集群就失败,就是因为忘了改这一项,或者改了但没同步到所有节点。

yarn-site.xml里必须配yarn.nodemanager.aux-services为mapreduce_shuffle,这个 shuffle 服务是 MapReduce 拉取中间结果的通道,漏配的典型症状是作业卡在 map 100% reduce 0% 不动。

2.3 启动流程与四项验证

配置分发到所有节点后,master 上依次执行hdfs namenode -format、start-dfs.sh、start-yarn.sh。格式化只做一次,第二次格式化会导致 DataNode 的 clusterID 与 NameNode 不匹配而无法启动——如果真需要重来,先把各节点的data和logs目录清干净。

启动后按顺序验证四件事,这个顺序不要跳:

  1. jps看进程。master 应该有 NameNode、ResourceManager、SecondaryNameNode;slave 应该有 DataNode、NodeManager。
  2. 浏览器打开master:9870看 HDFS 的 Web UI,master:8088看 YARN。两个页面都能打开,说明 RPC 和 HTTP 服务都正常。
  3. hdfs dfs -mkdir -p /test建目录,hdfs dfs -ls /能看到,说明写入链路通。
  4. hadoop jar自带的pi示例跑一次,验证 YARN 调度正常。这个示例很轻,几十秒就能出结果,是最快的端到端自检。

2.4 起不来时的排查次序

进程少了一个,先看logs目录下对应组件的日志后五十行,90% 的问题日志里直接写了原因。看不到明显报错,就按"网络→权限→配置→磁盘"的顺序查:ping通不通、端口telnet master 9000通不通、hosts有没有写错、磁盘是不是满了。磁盘满这个问题特别隐蔽,DataNode 会拒绝写入但进程还在,df -h一看才发现根分区只剩几百兆——虚拟机默认磁盘分配往往很紧。

3. HDFS 编程实践:先用命令行建立直觉,再上 API

直接抄一段 Java 代码跑通,其实学不到东西。我建议先花二十分钟用命令行把增删改查过一遍,对路径、权限、块的概念有了体感,再写代码会顺很多。

3.1 命令行阶段要建立的三点直觉

第一,HDFS 的路径是逻辑路径,/user/root/data.txt这种,跟你本地的文件系统完全隔离。hdfs dfs -put是从本地推到 HDFS,-get是反向拉回来,-cat是直接读内容。第二,目录要先建,-put到不存在的父目录会失败,不像本地cp会自动创建。第三,-ls显示的三个数字是权限位,默认 644,属于当前用户。

这一阶段多做一个动作:hdfs fsck / -files -blocks看看文件被切成了几个块、分布在哪些 DataNode 上。这个输出对理解后续的 MapReduce 分片特别有帮助——一个 128M 的块通常对应一个 Map 任务,数据倾斜问题从这一步就能看出端倪。

3.2 Java API 的最小可运行骨架

依赖管理是第一个坎。Maven 里引入hadoop-client就够了,版本必须和集群版本严格一致,3.3.6对3.3.6,差一个小版本都可能报NoSuchMethodError。我一般用provided作用域,打包时不带进去,运行时用集群的 jar。

<dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>3.3.6</version> <scope>provided</scope> </dependency>

最小骨架是这样:

Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://master:9000"); System.setProperty("HADOOP_USER_NAME", "root"); FileSystem fs = FileSystem.get(conf); // 创建目录 fs.mkdirs(new Path("/lab/hdfs")); // 上传本地文件 fs.copyFromLocalFile(new Path("/opt/data/words.txt"), new Path("/lab/hdfs/words.txt")); // 列目录 RemoteIterator<LocatedFileStatus> it = fs.listFiles(new Path("/lab/hdfs"), false); while (it.hasNext()) { LocatedFileStatus s = it.next(); System.out.println(s.getPath() + " block=" + s.getBlockSize() + " replicas=" + s.getReplication()); } fs.close();

System.setProperty("HADOOP_USER_NAME", "root")这行是 Windows 本地调试时的救命符。Windows 用户名和 HDFS 用户名不一致时,会报Permission denied: user=xxx, access=WRITE。在 Linux 集群上跑不需要这行,但加上也无害。

3.3 上传、下载、追加、删除的实操细节

copyFromLocalFile有个重载方法第二个参数是boolean delSrc,表示上传后是否删除本地源文件。默认是true,很多人没注意,上传完发现本地文件没了,还以为是被删库了。

追加内容用fs.append(),但要注意 HDFS 默认开启dfs.support.append,如果报错说明配置里被关了。追加操作只能由单个 writer 进行,写完必须close(),否则文件会处于"正在被写"的状态,别的程序读不到最后一段。

删除用fs.delete(path, true),第二个参数是是否递归。这里有个坑:delete返回false不代表失败,可能只是路径本来就不存在。要么先exists()判断,要么把返回值打日志,别默默忽略。

下载大文件时建议用fs.copyToLocalFile而不是open()流式读,前者内部做了优化。如果确实要流式处理,记得IOUtils.copyBytes(in, out, 4096, true)最后那个true是自动关闭流,漏了会文件句柄泄漏。

3.4 实验报告里该记什么数据

HDFS 这块的统计指标我一般记三组:不同大小文件(1M、64M、128M、256M)上传下载的耗时,对应的块数量,以及fsck输出的副本分布情况。128M 附近会出现明显的耗时台阶,因为刚好跨过默认块大小,这个现象记下来对照块大小参数就很好解释。表格形式最直观:

文件大小块数上传耗时(s)下载耗时(s)副本数
1 MB10.80.63
64 MB13.22.73
128 MB16.15.43
256 MB212.510.93

表里的数字是自己环境实测出来的才有意义,不同磁盘、不同网络差别很大,别抄别人的。重点是让读者看到"块数翻倍时耗时也大致翻倍"这个线性关系。

4. HBase 编程实践:rowkey 设计错了,代码写得再漂亮也白搭

HBase 的实验最容易走过场——照着教程建个表、插几行、查出来,就结束了。但真正有价值的思考在于:为什么这张表要这样设计。RowKey 一旦定错,后面所有查询都会变成全表扫描。

4.1 从访问模式倒推表结构

假设要存学生成绩,一条记录包含学号、姓名、班级、三门课成绩。最直接的 rowkey 就是学号,因为它唯一。但如果业务上最常见的查询是"按班级查所有学生",那学号做主键就要扫全表。

合理的做法是用班级_学号拼接作为 rowkey,比如class01_2023001。HBase 的 rowkey 是按字典序排列的,相同前缀的数据会物理上挨在一起,一次 scan 就能连续读出来。这就是所谓的"前缀聚簇",理解这一点比记住 API 重要得多。

列族设计上,把经常一起读的列放一个族里。成绩和姓名放info族,选课记录放course族,因为列族在存储层是分开的,跨族读会有额外开销。列族数量建议控制在 2 到 3 个,HBase 官方文档里也明确说过列族多了会拖慢 flush 和 compaction。

4.2 建表与 shell 验证

建表时预分区是个实用技巧。默认建表只有一个 Region,所有写入都打在一个 RegionServer 上,完全失去分布式的意义。用SPLITS指定切分点:

create 'student', {NAME => 'info', VERSIONS => 3}, {NAME => 'course'}, SPLITS => ['class01_', 'class02_', 'class03_', 'class04_']

VERSIONS => 3表示每列保留三个历史版本,默认是 1。做实验时如果同一个 rowkey 反复put,想看版本变化就必须调大这个值,否则只能看到最后一次。

shell 里几个高频命令记熟:list看所有表,describe 'student'看结构,scan 'student', {LIMIT => 5}看前五行,get 'student', 'class01_2023001'精确取一行,count 'student'数总数(大表慎用,这个操作会扫全表)。

4.3 Java API 写起来和关系型数据库差在哪

HBase 的 Java API 跟 JDBC 完全是两套思路。JDBC 拿一个连接直接执行 SQL,HBase 要依次拿 Connection、Table、然后构造 Put/Get/Scan 对象。

Configuration conf = HBaseConfiguration.create(); conf.set("hbase.zookeeper.quorum", "master,slave1,slave2"); conf.set("hbase.zookeeper.property.clientPort", "2181"); Connection conn = ConnectionFactory.createConnection(conf); Table table = conn.getTable(TableName.valueOf("student")); // 写入 Put put = new Put(Bytes.toBytes("class01_2023001")); put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("A同学")); put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("20")); table.put(put); // 精确读取 Get get = new Get(Bytes.toBytes("class01_2023001")); Result rs = table.get(get); String name = Bytes.toString(rs.getValue(Bytes.toBytes("info"), Bytes.toBytes("name"))); // 范围扫描 Scan scan = new Scan(); scan.withStartRow(Bytes.toBytes("class01_")); scan.withStopRow(Bytes.toBytes("class02_")); scan.setCaching(100); ResultScanner scanner = table.getScanner(scan); for (Result r : scanner) { System.out.println(Bytes.toString(r.getRow())); } scanner.close(); table.close(); conn.close();

几个必须注意的点:Connection是重量级对象,一个应用只创建一次并复用,千万不要每次操作都 new 一个,端口会被打满。Table相对轻量但也应该复用。ResultScanner用完必须close(),否则会占用 RegionServer 的资源,跑批量扫描时不关会导致后来的连接拿不到。

Bytes.toBytes和Bytes.toString是固定搭配,所有字段都按字节数组存。这意味着数字和日期不会自动转换,存进去是什么字节就是什么字节,想要按数值排序得自己编码成定长字节串,这一点和关系库差别很大。

4.4 版本不匹配是 HBase 实验的头号杀手

HBase 和 Hadoop 的版本必须对应。HBase 2.4.x 一般配 Hadoop 3.2.x/3.3.x,具体对应关系在 HBase 官方的版本说明里查,每次我都是先查表再下载,从不凭感觉。版本差太大的典型症状是 RegionServer 启动后日志里刷NoSuchMethodError或者ClassNotFoundException,然后进程直接退出。

另一个高频问题是hbase-site.xml里hbase.rootdir写的是hdfs://master:9000/hbase,但 HDFS 根目录其实还处于安全模式或者根本没起。启动 HBase 前一定先确认hdfs dfs -ls /能正常返回,这一步花十秒钟,能省一小时。

还有 ZooKeeper 的dataDir必须是本地磁盘路径,不能是 HDFS 路径。ZooKeeper 需要在 HDFS 起来之前就工作,它要是依赖 HDFS 就成了循环依赖。这个设计逻辑想明白,配置就不会写错。

5. MapReduce 编程实践:把 WordCount 讲透胜过写十个作业

WordCount 是所有人入门 MapReduce 的第一个程序,但它其实包含了分布式计算的全部关键机制:数据分片、map 阶段局部计算、shuffle 阶段按 key 分组、reduce 阶段聚合。把它的每一步拆开看,后面的自定义作业都是照着这个模板改。

5.1 数据流里每个环节到底发生了什么

输入文件被InputFormat切成InputSplit,每个 split 交给一个 Map 任务。默认的TextInputFormat按行读,split的大小通常等于一个 HDFS 块(128M)。这就是所谓的"移动计算而不是移动数据"——任务被调度到数据所在的节点上跑。

Map 输出的<key, value>先进环形缓冲区,默认 100M,到 80% 阈值开始溢写到本地磁盘,溢写前会按 key 排序并可选合并。所有溢写文件最后合并成一个大文件,按分区归并。

Reduce 阶段通过 HTTP 从各个 Map 节点拉取属于自己的分区数据,这个拉取过程就是 shuffle。拉完再做一次归并排序,然后按 key 逐个调用reduce()。理解 shuffle 是理解 MapReduce 性能的关键——大部分调优参数都围绕这个阶段。

阶段输入输出落盘位置
Mapsplit 中的一行<key, value>内存缓冲区,溢写到本地
ShuffleMap 输出按 key 分组的数据拉取到 Reduce 节点
Reduce一组相同 key 的 value聚合结果输出到 HDFS

5.2 代码骨架与提交方式的区别

标准的 Mapper 和 Reducer 长这样:

public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> { private final IntWritable one = new IntWritable(1); private final Text word = new Text(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] tokens = value.toString().split("\\s+"); for (String t : tokens) { if (t.isEmpty()) continue; word.set(t); context.write(word, one); } } } public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> { private final IntWritable result = new IntWritable(); @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable v : values) { sum += v.get(); } result.set(sum); context.write(key, result); } }

Mapper 里的IntWritable对象要提到成员变量复用它,不要在循环里 new。这个细节在小数据量上看不出差别,数据量一大就是几百万次对象创建,GC 压力会很明显。这也是 Hadoop 提供IntWritable、LongWritable这些可变封装类型而不是直接用int的原因——它们可以在序列化框架里被复用。

提交作业时,Job对象里必须设置setJarByClass,否则集群找不到你的类。本地跑的时候可以省略,提交到集群就报ClassNotFoundException。这就是本地模式和集群模式最典型的差异之一。

Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "word count"); job.setJarByClass(WordCountDriver.class); job.setMapperClass(WordCountMapper.class); job.setReducerClass(WordCountReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); FileInputFormat.addInputPath(job, new Path("/lab/input")); FileOutputFormat.setOutputPath(job, new Path("/lab/output")); System.exit(job.waitForCompletion(true) ? 0 : 1);

输出目录必须不存在,存在就报FileAlreadyExistsException。这个设计是为了防止误覆盖,每次重跑前先hdfs dfs -rm -r /lab/output。

5.3 从 Counter 里读出作业的真实情况

作业跑完控制台会打印一堆 Counter,这是 MapReduce 实验里最有价值的统计数据来源。Map input records是读入多少行,Map output records是 map 输出了多少条,Reduce input groups是有多少个不同的 key,Reduce output records是最终结果条数。

对比这几个数字能看出很多问题。比如 map 输出 10 万条而 reduce 输入组只有 3 个,说明数据高度集中到少数 key 上,这就是数据倾斜的典型特征,某个 reduce 任务会跑得特别慢。我做过一个词频统计的实验,用一份日志数据,Reduce input groups分到三个 reduce 上分别是 8 万、1.5 万、200,那台跑 8 万的任务耗时是其他两台的十几倍,这个现象记进实验报告比单纯写"作业成功"有意义得多。

另外Combine input records和Combine output records能看出 Combiner 起了多大作用。加了 Combiner 之后 map 输出条数应该明显下降,下降比例就是本地聚合的效果。

5.4 Combiner 和 Partitioner 什么时候必须自己写

Combiner 本质上是跑在 Map 端的 reduce,它要求操作满足结合律。求和、求最大值可以用,求平均值绝对不行——因为平均值的平均不等于总平均值。这个坑很经典,很多人在实验里用 Combiner 做了个平均值统计,结果和不用 Combiner 的结果对不上,还不明白为什么。

Partitioner 决定 key 分到哪个 reduce。默认的HashPartitioner用key.hashCode() % numReduceTasks,遇到某些 key 集中的数据就会倾斜。解决办法是自定义 Partitioner,把热 key 加随机后缀打散,或者干脆按业务维度重新切分。做实验时用两三行数据看不出效果,至少造个几万条、故意让某个 key 占大头,才能观察到差异。

6. 实验统计怎么做才不是流水账

标题里带"实验统计",这部分其实是整个报告含金量最高的地方。只写"安装成功""程序运行成功",那不叫统计,叫日志。真正有价值的记录是:在什么条件下、测了什么、得到了什么数字、这些数字说明什么。

6.1 我固定采集的几类指标

环境类:机器配置、组件版本、节点角色,这些是实验的前提,不记下来数据就失去参照意义。时间类:每个操作的耗时,安装阶段记"从开始配置到jps全部进程正常"的总时长,编程阶段记单个任务的执行时间。规模类:数据量大小、文件块数、记录条数。资源类:从 YARN 的 8088 页面拿到每个作业的容器数、分配内存、CPU 时间。

这四类数据凑在一起才能形成对比。比如"处理 1G 数据用了 40 秒",单独看没意义;加上"3 个容器、每容器 1G 内存、共 6 个 Map 任务",就能算出大致吞吐,也能和另一个配置做比较。

6.2 一份可复用的实验记录表

我把四个实验的数据放在一张总表里,纵向是实验模块,横向是观测维度。这样一眼能看出哪个环节耗时最长、哪次改动带来的收益最大。

实验关键配置数据规模主指标观测值备注
Hadoop 安装3 节点 / 8G-环境就绪耗时95 min含两次回滚
HDFS 编程副本 3 / 块 128M256 MB上传耗时12.5 s块数 2
HBase 编程4 预分区 / 2 列族10 万行批量写入耗时48 s单线程 put
MapReduce3 reduce / 带 Combiner500 MB作业总耗时63 s倾斜明显

写这张表的时候有一个原则:所有数字都要能追溯来源。耗时是从提交到COMPLETED的日志时间戳,还是从 Web UI 里看到的 elapsed time,要统一口径,否则横向比较就是自欺欺人。

6.3 让数据说话的两个小实验

第一个是副本数对比。同一份 128M 文件,dfs.replication分别设 1、2、3,测上传和下载耗时。写入耗时会随副本数上升,读取耗时基本不变甚至略有下降,因为可以就近读。这个结果记录下来,副本数为什么默认是 3 就很好解释了——在可靠性和存储成本之间取的平衡点。

第二个是 reduce 数量对比。同一份数据,numReduceTasks分别设 1、3、6,看总耗时和单个 reduce 的耗时分布。通常从 1 到 3 有明显提速,3 到 6 收益递减,因为容器启动和 shuffle 开销吃掉了并行带来的好处。这条曲线画出来,比背十遍"要根据数据量设置 reduce 数"都管用。

注意:统计实验一定要控制变量。改了 reduce 数就别同时改内存配置,否则说不清是哪个因素带来的变化。我第一版报告就是因为同时调了两个参数,结论完全站不住脚,只好重做。

7. 那些反复出现的坑,以及我现在的第一反应

最后把四个实验里踩得最多的几个坑集中说一下,每个都给出我现在遇到时的第一步动作。这些是教程里基本不会写的东西,但真做实验时十有八九会碰上。

7.1 进程在但服务不可用

jps显示进程都在,但客户端连不上,这种最迷惑。本质通常是进程处于"假活"状态——比如 DataNode 启动后因为磁盘问题进入只读模式,进程还在但拒绝写入。第一反应去翻对应组件的日志,搜ERROR和WARN,而不是急着重启。重启会掩盖问题,下次还会犯。

还有一种情况是端口冲突。比如之前的进程没退干净,start-dfs.sh又起了一遍,新进程绑不上端口就静默退出。用netstat -tlnp | grep 9000确认端口占用,kill -9掉旧进程再重来。

7.2 中文路径和编码问题

HDFS 上的路径带中文,Java API 里用new Path("中文")有时正常有时报错,取决于 JVM 的默认字符集。稳妥做法是全程用英文路径,确实要用中文就显式指定 UTF-8。Windows 本地跑的时候更要注意,控制台默认 GBK,输出中文乱码会让人误以为是数据错了,其实只是显示问题。

7.3 本地模式和集群模式的切换混淆

Configuration对象每次 new 出来,读的是core-site.xml的默认配置。如果你的 classpath 里没有集群的配置文件,它会退回到本地文件系统模式,fs.defaultFS变成file:///。这时候所有操作都在本地磁盘上执行,看起来"能跑通",其实完全没用上 HDFS。

判断方法很简单:fs.getUri()打印出来看,是hdfs://master:9000还是file:///。养成每次调试先打印这一行的习惯,能省掉大量莫名其妙的困惑。我一般还会同时打印fs.getWorkingDirectory(),确认工作目录也在集群上。

7.4 内存不够时最先崩的是谁

三台 8G 的机器跑全套组件,最容易 OOM 的是 RegionServer。它的默认堆是 1G,批量写入几十万行时很容易撑爆。解决办法是在hbase-env.sh里调大HBASE_HEAPSIZE,同时把 RegionServer 的hbase.hregion.memstore.flush.size适当调小,让它更早地把内存数据刷到磁盘。

YARN 容器内存同样要注意。yarn.nodemanager.resource.memory-mb默认是 8G,但虚拟机上实际可用没那么多,配置超过物理内存会导致容器分配成功但启动失败,日志里写的是Container exited with a non-zero exit code 1,看不出真正原因。把这一项设成物理内存的 70% 左右比较稳。

7.5 关于时间安排的一点个人体会

这四个实验如果连着做,安装环节留一整天,编程三个环节各留半天,是比较合理的分配。急着一天全做完的结果通常是环境反复重装、报告全在凑字。我第二次做的时候先把 Hadoop 和 HBase 环境彻底跑稳、打好快照,再集中写代码,反而比第一次省了将近一半时间。做大数据实验这件事,前期在环境上多花的每一分钟,后面都会以更少的返工回报你。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询