简介:这是基于Hadoop Mapreduce的网站日志分析程序,面向计算机、大数据及相关专业的学生、教师或企业开发者,可用于毕业设计、课程设计与项目初期演示。包含完整的Java源代码、编译后的class文件及README文档说明,共15个文件,主要为7个Java源码、7个class文件与1个Markdown说明文档,压缩包仅17KB,结构简洁、便于直接导入IDE查看与运行。资源代码已经过完整测试,上传前均运行成功,在毕业设计答辩中平均分达到96分,可靠性较高。目前已有一百七十余人学习浏览。读者可借此掌握MapReduce处理网站日志的核心流程,包括日志解析、统计分析与结果输出等环节,也可基于源码进行二次修改,实现个性化功能。下载后可私聊咨询,支持远程教学,适合从零开始的初学者快速入门。
1. 从“日志躺在那儿”到“能查能算”:Hadoop 网站日志分析到底在解决什么问题
网站日志是少数几类“越攒越值钱却越放越烫手”的数据。单机日志分析工具在日请求量百万级以内还能凑合:grep、awk、Excel 透视表轮番上阵,最不济装个 ELK 把过去三天的数据倒腾进 Elasticsearch。可一旦流量涨到千万级、或者你忽然要回溯九十天内的用户访问路径,单机内存和磁盘 IO 就会同时见底——不是某条命令跑不动,而是整个分析链路从“等多久”退化成了“能不能跑完”。这个项目标题里的“基于 Hadoop 的网站日志分析程序”,本质上就是把这套“日志清洗 → 指标聚合 → 结果落库”的流程,从单机内存模型迁移到分布式计算模型上。
做这件事的人通常有三类:刚学完 Hadoop 想拿真实数据练手的后端工程师、公司日志量已经让 MySQL 查询慢到无法忍受的运维、以及要做用户行为分析但又没钱上商用数仓的小团队。所谓“源代码 + 文档说明”,一般指一个完整的可运行工程——包含日志清洗的 MapReduce 作业、按天/按 URL/按 IP 聚合的输出、以及配套的部署与调参文档。它不负责给你造数据,也不负责画炫酷大屏,它的核心价值是把“日志分析”这件事变成一条能稳定重放的流水线。下文我会顺着一条真实的实现路径来讲:数据从哪来、格式怎么定、MapReduce 怎么设计、跑完结果怎么查,最后再收在几个只有上线后才会踩到的参数坑上。
2. 把日志变成可计算的记录:输入格式、数据模型与 Hadoop 选型
2.1 为什么选 Hadoop 而不是 Spark 或 Flink:批处理场景的边界
做日志分析不一定非 Hadoop 不可,但选它有个非常实际的理由:日志分析的主场景是“回溯型计算”,不是“实时告警”。你要回答的是“昨天有多少人访问了 /checkout”“过去一周哪个落地页跳出率最高”,这些查询天然是批处理——数据已经完整落盘,计算时机在事后,结果容忍秒级甚至分钟级延迟。Hadoop 的 MapReduce 模型在这种场景下有天然优势:实现简单、磁盘廉价、对数据格式几乎零要求,而且生态里 Hive、Pig、Sqoop 这些工具都能直接吃它的输出。
Spark 和 Flink 当然也能做同样的事,在内存充足时跑得更快。但注意标题里说的是“程序 + 源代码”,不是“平台方案”。MapReduce 的代码结构足够直白——一个 Mapper、一个 Reducer、一个 Driver——非常适合作为教学和二次开发的骨架。如果你之前只接触过 Spark 的 DataFrame API,回来写一次原生的 MapReduce 反而能帮你把“Shuffle 阶段到底发生了什么”这件事想得更透。选型结论:数据量在 TB 级以下、集群规模 3~10 台、分析任务以 T+1 为主,Hadoop 是性价比最高的选择;如果实时性和迭代式计算占比超过四成,再考虑 Spark Streaming 或 Flink 也不迟。
2.2 日志格式先行:先定解析规则,再写代码
动手写 MapReduce 之前,第一件事是确认为你提供日志的访问来源。最常见的 Nginx 默认格式长这样:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent"';对应的一条真实日志行示例:
203.0.113.7 - - [12/May/2024:14:23:11 +0800] "GET /product/1024?from=home HTTP/1.1" 200 5321 "https://example.com/search?q=hadoop" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"把这条记录拆成字段,至少能拿到五个分析维度:客户端 IP、请求时间、请求方法 + 路径 + 查询参数、响应状态码与字节数、来源页面与 User-Agent。这里有一个新手最常见、也是必须在一开始就杜绝的错误:不要在 Mapper 里用空格 split 之后再按位置取字段,因为$http_user_agent内部自带空格,一旦位置偏移,整行解析立刻错乱。
提示:
$time_local字段里的日期带方括号、$request里的路径带查询串,这两处是做字段拆分时的天然边界,优先用正则或按中括号切分,而不是全局按空格切。
我一般会为这种情况准备一个日志样例文件,放在项目的sample-data/目录下,里面的每一行都按真实 Nginx 格式伪造,包含正常请求、404、带查询参数的 URL、以及被爬虫刷出来的异常 User-Agent。这个文件既是单元测试的输入,也是写 MapReduce 时的调试依据——不要等部署到集群上才发现解析器挂了。
2.3 明确分析输出的数据模型:MapReduce 的 Key 即维度
日志分析程序的分析结果需要提前定义好输出结构,否则写出来的 Reducer 容易变成一把梭。常见做法是把分析拆成多个独立作业,每个作业对应一张结果表。这里给出一个最常用的指标划分:
| 作业名 | 输入维度(Map 输出的 Key) | 输出指标(Reduce 累加/统计内容) | 典型用途 |
|---|---|---|---|
pv-uv-by-day | 日期(yyyy-MM-dd) | PV 总数、去重 UV 数 | 流量总览折线图 |
pv-by-url | 日期 + 请求路径 | 该路径下所有请求的总次数、平均响应字节 | 热点页面排行 |
status-code-count | 日期 + 状态码 | 每个状态码的计数 | 4xx/5xx 错误率监控 |
referer-top | 日期 + 来源域名 | 来自该域名的访问次数 | 外部引流分析 |
这里有个值得注意的设计决策:UV 的去重怎么在 MapReduce 里做。按天 + IP 做 Key 当然是对的,但要注意同一个用户用同一 IP 访问多个页面,产生的不是一条记录而是若干条。所以 UV 的正确做法是在 Reducer 内部维护一个HashSet,只对集合大小计数。如果数据量大到单个 Reducer 内存吃不消,再升级为二次 MapReduce:第一次作业输出(日期, IP)去重后的列表,第二次作业只数条数。很多博客把这称为“去重优化”,但本质上是把内存中的HashSet换成了磁盘上的排序归并,属于空间换时间的标准做法。
3. 用 Hadoop 伪分布式搭建快速获得可复现的调试环境
3.1 本地直接跑 MapReduce 的最小命令
标题下的相关搜索里,“hadoop伪分布式搭建”“hadoop开发环境搭建”出现的频率极高,说明大多数读者卡在环境上而不是代码上。如果你的目标是先把日志分析程序跑通,本地以单机模式验证是最快的路径——不需要启动任何守护进程,直接执行hadoop jar命令即可。
# Maven 构建出包含依赖的 jar 包 mvn clean package -DskipTests # 单机模式运行日志分析作业 hadoop jar target/log-analysis-1.0.jar \ com.example.loganalyzer.PvUvJob \ file:///home/dev/nginx-demo.log \ file:///tmp/log-analysis-output上述命令中的file:///前缀表示输入输出都在本地文件系统,Hadoop 会将 MapReduce 运行时环境与 HDFS 解耦,直接在 JVM 内模拟分布式执行。这对调试解析逻辑非常有用,因为出错时的堆栈信息直接完整地打在控制台上。但注意,单机模式不会启动 DataNode 和 NameNode,所以你在core-site.xml中配置的副本数、yarn-site.xml中的资源参数统统不生效——这个模式只验证业务逻辑,不验证集群配置。
3.2 伪分布式的配置清单与常见失败点
伪分布式是部署到真实集群前性价比最高的一步。它会在一台机器上同时启动 NameNode、DataNode 和 ResourceManager,让你可以用hdfs dfs -ls和yarn application -status这些命令完整体验分布式文件系统和资源调度的完整链路。核心配置分三个文件:core-site.xml指定 HDFS 的访问地址,hdfs-site.xml指定副本数与 NameNode 目录,yarn-site.xml指定资源调度模型。
<!-- core-site.xml --> <configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> </configuration><!-- hdfs-site.xml --> <configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/home/dev/hadoop-data/namenode</value> </property> </configuration><!-- yarn-site.xml --> <configuration> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>4096</value> </property> </configuration>这份配置中yarn.nodemanager.resource.memory-mb经常是新手翻车的起点:伪分布式默认会读取机器物理内存量,而yarn.scheduler.maximum-allocation-mb如果没同步调大,提交作业时极易报java.lang.OutOfMemoryError: unable to create new native thread或直接被 NodeManager 杀进程。另一个高频错误是dfs.namenode.name.dir指向了不存在或没有写权限的目录,导致start-dfs.sh后 NameNode 反复启动失败。验证配置是否正确,用一条命令就够了:
hdfs dfs -mkdir -p /user/test/input hdfs dfs -put sample-data/nginx-demo.log /user/test/input/ hadoop jar target/log-analysis-1.0.jar com.example.loganalyzer.PvUvJob \ /user/test/input/nginx-demo.log \ /user/test/output/pv-uv跑完看两处:第一是yarn application -list里的最终状态是否为SUCCEEDED,第二是hdfs dfs -cat /user/test/output/pv-uv/part-r-00000的输出是否与你单机模式跑出的结果一致。两边数字一样,说明你的代码和伪分布式环境都没有问题;不一样,优先回到第 2.2 节的解析逻辑上查字段错位。
4. 日志分析程序的核心实现:Mapper、Reducer 与 Driver 的完整骨架
4.1 Mapper 阶段:解析一行日志,输出可聚合的键值对
核心代码的逻辑可以抽象成一句话:每一行日志在 Mapper 中被解析成一个或若干个(key, value)对,MapReduce 框架负责把相同 key 的 value 合并到同一个 Reducer 实例中处理。下面是一段按天聚合 PV/UV 的 Mapper 实现,解析部分已经用正则封装好:
public class PvUvMapper extends Mapper<LongWritable, Text, Text, Text> { // 日志样例: 203.0.113.7 - - [12/May/2024:14:23:11 +0800] "GET /product/1024 HTTP/1.1" 200 5321 ... private static final Pattern LOG_PATTERN = Pattern.compile( "^([\\d.]+) \\S+ \\S+ \\[([^:]+):[\\d:]+ [^\\]]+\\] " + "\"\\S+ (\\S+) \\S+\" (\\d{3}) (\\d+) \"([^\"]*)\" \"([^\"]*)\"$" ); private Text outKey = new Text(); private Text outValue = new Text(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); Matcher matcher = LOG_PATTERN.matcher(line.trim()); if (!matcher.matches()) { // 记录脏数据条数,不要直接丢弃,否则数据质量无法追踪 context.getCounter("LogAnalysis", "MALFORMED_LINES").increment(1); return; } String ip = matcher.group(1); String date = matcher.group(2); // 形如 12/May/2024,需要再转成 2024-05-12 String url = matcher.group(3); String status = matcher.group(4); String bytes = matcher.group(5); // 转换日期格式 DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("dd/MMM/yyyy", Locale.ENGLISH); DateTimeFormatter outputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDate parsedDate = LocalDate.parse(date, inputFormatter); String day = parsedDate.format(outputFormatter); // 输出格式: (日期) -> (IP|URL|STATUS|BYTES) outKey.set(day); outValue.set(ip + "|" + url + "|" + status + "|" + bytes); context.write(outKey, outValue); } }这段代码里有三个设计细节值得展开。第一,正则匹配失败不是简单地跳过,而是打了一个 Counter——MapReduce 自带的计数器是监控脏数据比例的免费工具,MALFORMED_LINES会在作业结束后显示在控制台里。第二,(key, value)的结构设计刻意保持了宽松性:value 里的四个字段用|连接,便于同一个 Mapper 内做多种指标的初步提取,避免为每个指标写一整套解析代码。第三,日期格式化务必要指定Locale.ENGLISH,因为 Nginx 日志里的May、Oct是英文缩写,默认 locale 解析它是会抛异常的。
4.2 Reducer 阶段:按天聚合,用集合完成 UV 去重
Reducer 的输入来自 MapReduce 框架的 shuffle 阶段:同一个日期下的所有 value 会被组装成一个迭代器整体传给你的reduce方法。这里要注意的是,迭代器只能遍历一次,不能像List那样反复访问,所以在第一遍遍历时就要同时完成 PV 计数、UV 去重、状态码分类这几件事。
public class PvUvReducer extends Reducer<Text, Text, Text, Text> { @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { long pv = 0L; HashSet<String> uvSet = new HashSet<>(); HashMap<String, Long> statusCount = new HashMap<>(); for (Text value : values) { String[] fields = value.toString().split("\\|"); // 容错:如果字段数不对,该条不计入任何指标 if (fields.length < 4) { continue; } pv++; uvSet.add(fields[0]); String status = fields[2]; statusCount.put(status, statusCount.getOrDefault(status, 0L) + 1); } StringBuilder result = new StringBuilder(); result.append("pv=").append(pv); result.append(", uv=").append(uvSet.size()); for (Map.Entry<String, Long> entry : statusCount.entrySet()) { result.append(", status_").append(entry.getKey()).append("=").append(entry.getValue()); } context.write(key, new Text(result.toString())); } }这段代码有三点值得留意。第一是uvSet的数据结构选择:当单个日期的访问 IP 量在百万级别以内,HashSet完全够用;一旦超过千万,HashSet 的内存开销会导致 GC 频繁甚至 OOM,届时应改用二次 MapReduce,第一次只输出(日期, IP)并利用 MapReduce 自身的按键排序做去重,第二次再统计条数。第二是statusCount用getOrDefault做累加,这是 MapReduce 中非常典型的“在 Reducer 里做多维分组”的写法,避免为每个状态码单独写一个if分支。第三,最终的输出格式刻意做成了key后跟一串形如pv=..., uv=..., status_200=...的 KV 文本。这种格式的好处是后续不管接 Hive 外表还是直接导入 MySQL,都可以用简单的字符串分割完成字段映射。
4.3 可运行的 Driver 与作业参数
一个完整的 MapReduce 作业必须有一个静态main方法作为 Driver,它负责配置作业参数并提交到 YARN。参数要从命令行透传,而不是硬编码在代码里,这样同一套 jar 可以适应单机、伪分布式和真实集群三种运行模式。
public class PvUvJob { public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("Usage: PvUvJob <inputPath> <outputPath>"); System.exit(-1); } Configuration conf = new Configuration(); // 核心调优参数:控制每个 Reducer 拿到的数据量 conf.set("mapreduce.job.reduces", "4"); Job job = Job.getInstance(conf, "log-analysis pv/uv by day"); job.setJarByClass(PvUvJob.class); job.setMapperClass(PvUvMapper.class); job.setReducerClass(PvUvReducer.class); // Mapper 和 Reducer 的输出类型必须一致 job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); // 输入格式: 一行文本一条记录 job.setInputFormatClass(TextInputFormat.class); TextInputFormat.addInputPath(job, new Path(args[0])); // 输出目录不能预先存在,否则作业直接报错 FileOutputFormat.setOutputPath(job, new Path(args[1])); System.exit(job.waitForCompletion(true) ? 0 : 1); } }mapreduce.job.reduces这个参数是整段代码里最值得调的一个值。它的默认值是 1,意味着不管数据量多大,所有中间结果全部汇聚到一个节点上,既慢又容易内存溢出。常见的估算方式是拿输入文件的总字节数除以每台 DataNode 的可用内存,再乘以一个 0.5 到 1 的系数;比如 4GB 的输入日志、3 台 4GB 可用内存的节点,设置 6 到 8 个 Reducer 是一个合理的起点。同时注意,args[0]和args[1]的路径如果是集群模式,必须是 HDFS 上的路径而非本地路径,这个区别初学极易踩坑——最常见的表现是提交后报FileNotFoundException,日志里赫然写着file:///开头。
4.4 从作业到报表:把 HDFS 结果导入 MySQL 供查询
MapReduce 的输出是纯文本文件,直接给人看没问题,但要想做成可视化报表或供线上接口查询,最稳妥的方式是把结果表导入 MySQL。常规做法是用 Sqoop,但既然这个项目是“程序 + 源代码”,直接在 MapReduce 作业跑完后用一条 JDBC 批量更新命令来收尾,反而少引入一个依赖。下面是一条基于 SQL 脚本的落地方式:
# 承接上一步的作业输出,把 part-r-*. 文件合并后下载到本地 hdfs dfs -getmerge /user/test/output/pv-uv /tmp/pv-uv-result.tsv # 执行导入 SQL mysql -uanalytic -p -h 127.0.0.1 log_db <<EOF LOAD DATA LOCAL INFILE '/tmp/pv-uv-result.tsv' INTO TABLE daily_pv_uv FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n'; EOFgetmerge是 HDFS 上合并小文件的神器,它会按字典序把所有part-r-00000、part-r-00001合并成一个本地文件,省去在数据库侧写循环读取的麻烦。这里要留意的是FIELDS TERMINATED BY必须与 Reducer 里context.write输出的分隔符一致,MapReduce 默认用\t,所以上面 SQL 里指定了制表符;如果你在 Reducer 里改成了,,这里也要同步改。LOAD DATA 有一个隐蔽的坑:如果目标表已有重复日期的主键,导入会直接失败而不是跳过。稳妥的做法是先给daily_pv_uv表建好PRIMARY KEY (day),然后在导入前先执行一次DELETE FROM daily_pv_uv WHERE day IN (SELECT DISTINCT day FROM staging);,或者干脆把导入目标换成一张临时表,再用INSERT ... ON DUPLICATE KEY UPDATE刷进正式表。
5. 日志分析程序的进阶洞察:从统计指标到会话分析
5.1 用二次 MapReduce 做 Session 切分和路径还原
PV/UV 只是“看见了多少人”,真正让运营愿意打开报表的指标是“这些人到底干了什么”——这需要把同一用户 30 分钟内连续的访问归为一个会话(Session),再把这个会话内的访问路径串起来。会话划分的逻辑核心是一个时间阈值:两条相邻请求的间隔超过 30 分钟,就认为属于两个会话。
实现上有个取巧的 MapReduce 技巧:Mapper 阶段按(IP, 日期)作为 Key,Value 是时间戳;MapReduce 框架保证传给同一个 Reducer 实例的的记录按 Key 排好序,剩下的时间排序就是同一 Key 内部的局部操作。伪代码如下:
public class SessionReducer extends Reducer<Text, LongWritable, Text, Text> { private static final long SESSION_IDLE_THRESHOLD_MS = 30 * 60 * 1000L; @Override protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException { long sessionStart = -1; long lastTs = -1; int sessionSeq = 0; for (LongWritable ts : values) { long current = ts.get(); if (lastTs == -1) { sessionStart = current; } else if (current - lastTs > SESSION_IDLE_THRESHOLD_MS) { // 超过阈值,输出上一个会话,并开启新会话 context.write(key, new Text("session_" + sessionSeq + "|start=" + sessionStart + "|end=" + lastTs)); sessionSeq++; sessionStart = current; } lastTs = current; } // 输出最后一个会话 context.write(key, new Text("session_" + sessionSeq + "|start=" + sessionStart + "|end=" + lastTs)); } }这段代码有一个隐含的 MapReduce 知识:Iterable<LongWritable> values里的数据在同一个 Key 内是有序的,但顺序取决于 Map 输出的 value 排序规则,而不是输入行顺序。因此输入到 Mapper 的日志文件必须先按时间排好序,或者 Map 输出的 Key 用(IP, 时间戳)复合结构,让框架在 shuffle 阶段替你完成排序。这个细节是会话分析里最隐蔽也最致命的坑——如果忽略了它,你算出的会话时长会毫无意义。
5.2 验证结果与排查解析性能的 3 个实用技巧
跑完作业并不代表算对了,尤其是涉及时间跨度和 URL 去参数归一化时,肉眼几乎不可能发现错误。我每次验证作业结果都用以下三个方法,它们也适用于你拿到源代码后做二次开发时的自查:
用 Counter 做数据质量水位线。在 Mapper 里加context.getCounter("LogAnalysis", "MALFORMED_LINES").increment(1)作为“水位线计量器”。作业跑完后,对比 Counter 中显示的MALFORMED_LINES值与输入日志总行数。如果比例超过 1%,不要纠结业务指标,先回头查正则表达式是否漏掉了某些客户端异常协议——比如 HTTP/2.0 的请求行格式与 HTTP/1.1 不同、部分爬虫会在 User-Agent 里带无法解析的二进制的转义字符。
用-Dmapreduce.map.memory.mb调大 Mapper 内存并观察 GC 时间。如果作业卡在 50% 附近且日志里频繁出现GC overhead limit exceeded,多半是单条日志里的 User-Agent 过长、正则回溯过多。解法不是无脑加内存,而是把正则里对 User-Agent 的匹配从贪婪匹配改成正则的占有匹配或截断处理,例如.{1,200}限制长度。内存参数本身常用的调整方式是:在yarn-site.xml中调大yarn.nodemanager.resource.memory-mb,并在作业提交命令后加-Dmapreduce.map.memory.mb=2048。
用hdfs fsck检查输出文件健康度。作业SUCCEEDED后,执行hdfs fsck /user/test/output/pv-uv -files -blocks,重点看Total size和Missing blocks两项指标。日志分析作业的失败模式往往不是“挂了”,而是“跑完了但有些 block 副本损坏,Reducer 静默拿不到部分数据”。这种场景下作业状态依然是绿色,但结果与真实数据对不上。fsck是排查这类幽灵问题最快的工具。
5.3 从单个作业到分析流水线:把任务串成定时调度
日志分析程序的最终形态通常不是“手动跑一次”,而是“每天凌晨自动处理昨天的日志”。这一层建议直接复用系统 crontab 或写一个简单的 Shell 调度器,而不必为了一个小项目上 Apache Airflow。一个成熟的调度脚本至少包含四个阶段:
#!/bin/bash # 建议每天 01:30 执行,避开日志落盘的高峰期 YESTERDAY=$(date -d yesterday '+%Y-%m-%d') LOG_DIR="/data/nginx/logs/${YESTERDAY}" HDFS_INPUT="/user/logs/${YESTERDAY}" HDFS_OUTPUT="/user/analysis/${YESTERDAY}" # 1. 上传昨天的日志到 HDFS hdfs dfs -mkdir -p "${HDFS_INPUT}" hdfs dfs -put "${LOG_DIR}"/*.log "${HDFS_INPUT}"/ # 2. 执行分析作业 hadoop jar target/log-analysis-1.0.jar \ com.example.loganalyzer.PvUvJob \ "${HDFS_INPUT}" "${HDFS_OUTPUT}" # 3. 下载结果并将指标同步到 MySQL hdfs dfs -getmerge "${HDFS_OUTPUT}" "/tmp/pv-uv-${YESTERDAY}.tsv" mysql -uanalytic -p... -e \ "LOAD DATA LOCAL INFILE '/tmp/pv-uv-${YESTERDAY}.tsv' ..." # 4. 清理 HDFS 上 30 天前的中间结果,避免小文件堆积拖慢 NameNode hdfs dfs -rm -r -skipTrash "/user/analysis/$(date -d '30 days ago' '+%Y-%m-%d')"第 4 步常被忽略,但它恰恰是集群长期稳定运行的关键。MapReduce 作业每跑一次就会在 HDFS 上生成一批文件,如果输出目录名只精确到天,那么 90 天后的文件数会达到数千个。NameNode 将所有文件元数据保存在内存中,小文件堆积会直接拖慢整个集群的响应。日常运维建议每周检查一次hdfs dfs -count /user/analysis/,一旦文件总量超过 1 万就触发一次清理策略。至此,一个从日志采集、计算、落库到清理的全链路日志分析闭环已经完整闭环在了你手上。
本文还有配套的精品资源,点击获取