简介这份资源是面向大数据与人工智能方向学习者、开发者的一份Hadoop实战项目包聚焦网站日志的分布式处理与分析。项目以MapReduce编程模型为核心演示从日志解析、数据清洗到统计分析、用户行为挖掘的完整链路并延伸至用户画像、个性化推荐等AI应用场景适合具备Java基础、希望理解Hadoop生态的中级学习者。压缩包共14个文件以7个java源码与7个class编译文件为主源码便于阅读与二次修改class文件可直接运行验证整体约16KB结构紧凑。目前已有170人学习下载。通过该项目读者可掌握日志格式解析、MapReduce作业编写、HDFS数据存储与性能调优等关键技能并理解如何将分析结果用于异常检测与资源利用率优化为大数据与人工智能结合的实际开发打下基础。1. 网站日志分析为什么值得用 Hadoop 重做一遍单机awk加cron跑网站日志日 PV 十万以内还能撑住一旦到千万级、还要按小时出报表磁盘 IO 和内存就先扛不住了。基于 Hadoop 的网站日志分析程序本质是把「解析 Nginx/Apache 访问日志 → 清洗过滤 → 按维度聚合 → 出统计结果」这条链路搬到 HDFS 和 MapReduce 上让横向扩容替掉垂直堆配置。它适合两类人一类是手里有几十 GB 以上历史日志、想跑 UV/PV、状态码分布、Top URL、来源 IP 排行的后端或运维另一类是想拿一个完整可复现的离线批处理项目练手、把 HDFS、MapReduce、Hive 串起来的数据方向学习者。下面按「日志长什么样 → 怎么切字段 → 怎么写 MapReduce → 怎么用 Hive 提速 → 坑在哪」的顺序讲透。2. 先看清日志格式字段切分决定后面所有统计口径2.1 一条 Nginx combined 日志到底有哪些字段绝大多数网站访问日志用的是 combined 格式一行就是一条请求记录。典型长这样10.0.0.12 - - [12/Mar/2024:09:15:32 0800] GET /article/1024 HTTP/1.1 200 5321 https://example.com/list Mozilla/5.0 (Windows NT 10.0; Win64; x64)拆开看字段依次是客户端 IP、ident、认证用户、时间戳、请求行方法 路径 协议、状态码、响应字节数、Referer、User-Agent。做分析时真正高频用到的是 IP、时间戳、请求路径、状态码、字节数这五个。时间戳里的0800是时区偏移聚合按天或按小时时必须先统一到同一时区否则跨时区日志会错位。请求路径里常带查询串比如/search?qxxx统计 Top URL 前要先决定是保留还是截断查询串——这一步口径不定后面所有排行都对不上。2.2 用正则把一行日志切成结构化字段解析这步我一般放在 Map 阶段做用一条预编译正则把整行拆成命名分组。下面是最小可用的 Java 解析片段// 预编译正则避免每条记录都重新编译这是性能关键 private static final Pattern LOG_PATTERN Pattern.compile( ^(\\S) \\S \\S \\[([^\\]])\\] \(\\S) (\\S) [^\]*\ (\\d{3}) (\\d|-) \([^\]*)\ \([^\]*)\.*$ ); public LogEntry parse(String line) { Matcher m LOG_PATTERN.matcher(line); if (!m.find()) { return null; // 解析失败返回 null由调用方计入脏数据计数器 } LogEntry e new LogEntry(); e.ip m.group(1); e.time m.group(2); // 12/Mar/2024:09:15:32 0800 e.method m.group(3); e.path m.group(4); e.status Integer.parseInt(m.group(5)); // 字节数可能是 -表示响应体为空按 0 处理 e.bytes -.equals(m.group(6)) ? 0L : Long.parseLong(m.group(6)); e.referer m.group(7); e.ua m.group(8); return e; }逻辑说明正则用\S匹配非空白字段用[^\]]匹配方括号内的时间戳用[^]匹配引号内的请求行和 UA。参数上group(6)的-分支必须处理否则Long.parseLong(-)直接抛异常整个 Map 任务会因个别脏行挂掉。解析失败返回null而不是抛异常是为了让脏数据只影响单条记录不影响整个作业——这是血泪经验早期版本一遇脏行就崩重跑一次几十分钟。2.3 时间字段的两种处理策略时间戳12/Mar/2024:09:15:32 0800不能直接当字符串排序必须先转成标准时间。常见做法是用SimpleDateFormat配dd/MMM/yyyy:HH:mm:ss Z模式解析注意MMM是英文月份缩写locale 要设成Locale.ENGLISH否则中文环境下解析会失败。解析后统一转成yyyy-MM-dd HH这种小时粒度字符串作为聚合 key既省空间又方便按小时出报表。如果日志量大、时间格式固定也可以直接字符串截取拼 key省掉日期解析开销但前提是格式绝对稳定一旦上游改了日志格式就会静默出错。3. 用 MapReduce 跑通 PV、UV 与状态码分布3.1 作业拆分一个指标一个 Job 还是合并新手容易犯的错是把所有指标塞进一个 MapReduce 作业结果 Reducer 逻辑臃肿、输出格式混乱。我的做法是按指标拆 JobPV 一个、UV 一个、状态码分布一个、Top URL 一个。拆开的好处是每个 Job 的 key-value 类型清晰、可单独重跑、可单独调优。代价是多扫几遍 HDFS但离线批处理场景下这点 IO 换来的可维护性完全值得。如果日志量特别大再考虑用 MultipleOutputs 在一个 Job 里输出多路结果但那是优化阶段的事先把功能跑通。3.2 PV 统计Mapper 与 Reducer 的最小实现PV 就是总请求数Mapper 每条记录输出一个固定 keyReducer 累加即可// Mapper每条日志输出 PV, 1 public class PvMapper extends MapperLongWritable, Text, Text, IntWritable { private final Text key new Text(PV); private final IntWritable one new IntWritable(1); Override protected void map(LongWritable offset, Text value, Context ctx) throws IOException, InterruptedException { LogEntry e parser.parse(value.toString()); if (e ! null) { ctx.write(key, one); // 脏数据不计入 } } } // Reducer对同一个 key 的 1 求和 public class PvReducer extends ReducerText, IntWritable, Text, IntWritable { Override protected void reduce(Text key, IterableIntWritable values, Context ctx) throws IOException, InterruptedException { int sum 0; for (IntWritable v : values) { sum v.get(); } ctx.write(key, new IntWritable(sum)); } }逻辑说明Mapper 里parser是复用的解析器实例不要每条 new 一个。Reducer 的Iterable是复用对象不能把v直接存进集合要取get()的值。参数上mapreduce.job.reduces对 PV 这种单 key 聚合设 1 就够设多了反而每个 Reducer 都要拉全部数据。输出就是一行PV 1234567。3.3 UV 统计为什么不能简单去重UV 要按 IP 去重但 MapReduce 里没有全局去重只能靠 shuffle 把相同 IP 聚到一个 Reducer。做法是 Mapper 输出ip, 1Reducer 对每个 IP 只输出一次// UV Mapper输出 ip, 1 ctx.write(new Text(e.ip), one); // UV Reducer每个 ip 只写一条天然去重 protected void reduce(Text ip, IterableIntWritable values, Context ctx) { ctx.write(ip, NullWritable.get()); // 只输出 ip不输出计数 }这样输出文件的行数就是 UV。但要注意如果 IP 基数极大比如几千万独立 IP单个 Reducer 会成瓶颈这时要改用 HyperLogLog 之类的近似去重或者先按 IP 哈希分桶再两级聚合。精确 UV 和近似 UV 的取舍取决于业务能不能接受 1% 左右的误差。3.4 状态码分布与 Top URL 的排序技巧状态码分布Mapper 输出status, 1Reducer 求和输出200, 800000这种。Top URL 稍微麻烦因为要排序。常见做法是第一个 Job 统计每个 URL 的访问次数第二个 Job 用TotalOrderPartitioner或直接把结果拉到本地排序。日志量不大时我一般让第一个 Job 输出count, url利用 MapReduce 对 key 的默认排序再在输出里取前 N 行。注意 key 排序是字典序100会排在99前面所以 count 要格式化成定长字符串或者用IntWritable做 key 让数值排序生效。4. 用 Hive 建外部表把重复统计变成 SQL4.1 为什么解析完还要上 HiveMapReduce 写一次 PV 要几十行代码改个统计口径就得改代码重编译。上了 Hive 之后同样的统计变成一句 SQL改口径就是改 WHERE 条件。前提是先把日志解析成结构化数据落到 HDFS再建外部表指向它。这一步的投入产出比极高尤其是需要反复探索口径的阶段。4.2 建表与加载外部表 分区-- 建外部表指向 HDFS 上已解析好的结构化日志 CREATE EXTERNAL TABLE web_log ( ip STRING, log_time STRING, method STRING, path STRING, status INT, bytes BIGINT, referer STRING, ua STRING ) PARTITIONED BY (dt STRING, hour STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t STORED AS TEXTFILE LOCATION /warehouse/web_log; -- 加载某天某小时的数据 ALTER TABLE web_log ADD PARTITION (dt2024-03-12, hour09) LOCATION /warehouse/web_log/2024-03-12/09;逻辑说明用 EXTERNAL 表是因为数据是外部生成的删表不该删数据。分区字段dt和hour不写在列定义里而是由 PARTITIONED BY 声明这样查询时能分区裁剪只扫需要的目录。字段分隔符用\t因为日志里 UA 和 Referer 可能含逗号用逗号分隔会串列。参数上如果数据量大把STORED AS TEXTFILE换成ORC或PARQUET查询能快好几倍但需要先做一次格式转换。4.3 常用统计 SQL 与执行引擎选择-- 按小时 PV SELECT dt, hour, COUNT(*) AS pv FROM web_log WHERE dt 2024-03-12 GROUP BY dt, hour; -- 按小时 UV精确去重 SELECT dt, hour, COUNT(DISTINCT ip) AS uv FROM web_log WHERE dt 2024-03-12 GROUP BY dt, hour; -- 状态码分布 SELECT status, COUNT(*) AS cnt FROM web_log WHERE dt 2024-03-12 GROUP BY status ORDER BY cnt DESC;COUNT(DISTINCT ip)在数据量大时会退化成单 Reducer慢得让人怀疑人生。替代方案是用approx_distinct如果引擎支持或者先GROUP BY ip再COUNT。执行引擎上Hive 默认走 MapReduce慢换成 Tez 或 Spark 引擎同样的 SQL 能快数倍。这个切换在hive-site.xml里改hive.execution.engine即可不用改 SQL。5. 避坑与排查那些让作业跑不完的细节5.1 小文件过多导致 NameNode 压力大现象跑了几周后HDFS 上出现几十万个小文件每个几 KBNameNode 内存吃紧作业启动变慢。原因每个 Map 或 Reduce 任务输出一个文件分区粒度太细或 Reducer 数设太多。解决在写入前用CombineFileInputFormat合并输入输出端用mapreduce.job.reduces控制文件数或者定期跑一个合并作业把小文件拼成大文件。我一般按天分区每个分区控制在几个文件以内。5.2 数据倾斜某个 IP 或 URL 拖垮一个 Reducer现象作业卡在 99%某个 Reducer 跑了几个小时其他早就完成。原因某个热点 IP 或热门 URL 的 key 数据量远超其他全压到一个 Reducer。解决对 key 加随机前缀打散先局部聚合再全局聚合或者对热点 key 单独处理。UV 统计尤其容易撞上这个因为爬虫 IP 可能贡献几百万条记录。5.3 时间解析失败导致整批数据丢失现象某天开始所有记录解析失败输出为空。原因上游改了日志格式比如时间戳从0800变成08:00正则不匹配。解决解析失败时不要静默丢弃要计数并采样输出到单独目录方便排查。同时给正则加容错分支或者用更宽松的解析库。这个坑我踩过静默失败最可怕因为报表看起来「正常」只是数字全是零。5.4 编码问题让中文 UA 变乱码现象统计 UA 时中文全是问号或乱码。原因日志文件是 UTF-8但读取时用了平台默认编码。解决在FileInputFormat或读取流上显式指定UTF-8Hive 建表时也确认serialization.encoding是 UTF-8。这个坑在跨平台迁移时特别常见。5.5 时区不一致导致按天统计错位现象按天统计的 PV 和上游监控对不上差几个小时。原因日志时间戳带时区但解析后没统一转换或者 Hive 里log_time还是原始字符串。解决解析阶段就统一转成 UTC 或固定时区存成标准格式。跨时区业务尤其要注意别让0800和0000混在一起聚合。6. 进阶把批处理结果接上可视化与增量更新跑通离线统计只是第一步真正让这套东西有用的是「结果能被看见、能持续更新」。我一般把 Hive 统计结果导出到 MySQL 或直接写 Parquet再用轻量可视化工具出图。导出用INSERT OVERWRITE DIRECTORY或 Sqoop注意导出前先GROUP BY收敛数据量别把明细全导出去。增量更新是另一个关键点。全量重跑历史日志越来越慢常见做法是按天分区每天只跑增量分区历史分区不动。如果日志有迟到数据比如跨天补录就维护一个「可重跑窗口」比如最近三天可重跑更早的冻结。这样既保证时效又控制成本。验证统计对不对我有个笨办法但很管用拿一天日志用awk在单机上跑一遍 PV 和 UV和 Hadoop 结果对。对不上就查解析口径、时区、去重逻辑。这个交叉验证能揪出大部分静默错误。参数调优上mapreduce.map.memory.mb和mapreduce.reduce.memory.mb别设太大按数据量逐步加mapreduce.task.io.sort.mb影响 shuffle 性能数据量大时调到 200MB 以上有明显收益。最后说个习惯每次改解析逻辑或统计口径先拿 1% 的采样数据跑一遍确认输出符合预期再上全量。全量跑一次几十分钟采样跑一次几十秒这个时间差能省下大量重跑。希望帮到你。本文还有配套的精品资源点击获取