简介:本资源是一份面向大数据初学者与Hadoop实践者的完整离线日志分析项目文档,聚焦技术论坛网站运营场景,解决PV、IP数、注册用户数、跳出率及板块热度等核心业务指标的计算问题。文档以Apache Common日志为数据源,系统梳理了从HDFS数据上传、MapReduce清洗、Hive统计分析,到Sqoop导出MySQL与HBase明细存储的全链路开发流程,并涵盖Linux Shell脚本、表结构设计及典型排错思路。资源为单个1013KB的DOCX文件,内容结构清晰,含项目背景、KPI定义与公式、开发步骤详解、数据格式说明及清洗实操示例,适合作为高校课程实训、企业内训或自学复盘的参考材料。目前已有1402人学习下载,覆盖Hadoop生态主流组件,兼具理论严谨性与工程可落地性。
1. 这不是又一个“Hello World”日志分析:它用 56GB 历史数据+每日增量,跑通了从 Apache 日志到运营看板的完整离线链路
你手头有一堆.log文件,里面全是192.168.1.102 - - [24/Apr/2015:03:27:12 +0800] "GET /forum.php?mod=viewthread&tid=12345 HTTP/1.1" 200 12487这样的原始记录——别急着 grep 或 awk 硬刚。这个「大数据综合案例-网站日志分析」项目,是少数几个真正在生产级规模(56GB 历史单文件 + 每日 150MB 增量)上跑通全链路的实战样本。它不讲 Spark Streaming 实时流,也不堆砌 Flink 算子,而是老老实实走通 HDFS → MapReduce 清洗 → Hive 分区建模 → Sqoop 导出 MySQL → 最终支撑运营决策的闭环。重点在于:所有 KPI(PV、注册用户、独立 IP、跳出率、板块热度)都来自真实业务语义——比如member.php?mod=register才算有效注册,/static/开头的静态资源必须过滤,GET /和POST /前缀要剥离。这不是教科书里的玩具数据,而是某技术论坛真实运营场景的镜像。如果你正卡在“日志进 HDFS 后怎么变成可分析表”“Hive 分区怎么和日期自动对齐”“跳出率 SQL 怎么写才不漏 IP”这些具体断点上,这篇笔记就是为你拆解的——每一步命令、每一行代码、每一个报错,我都亲手复现过三遍。
2. 数据入湖与清洗:为什么非得用 MapReduce 而不是 Hive UDF?三个硬核理由和一份可直接运行的清洗脚本
2.1 为什么不用 Hive 直接解析?Apache 日志格式的“黑匣子”陷阱
Apache Common Log Format(CLF)看着简单,实则暗坑密布:时间字段带英文缩写(Apr)、时区偏移(+0800)、URL 包含空格和引号、状态码和流量混在最后两个字段……如果强行用 Hive 的regexp_extract或split解析,会遇到三类典型翻车:
- 时间解析失败:
'24/Apr/2015:03:27:12 +0800'中的Apr是 Locale.ENGLISH,Hive 默认from_unixtime不认; - URL 截断错误:
"GET /forum.php?mod=viewthread&tid=12345 HTTP/1.1"中的HTTP/1.1必须精准切掉,但split(url, ' ')[1]会因空格数量不一致而崩; - 静态资源误判:
/staticsource/css/main.css和/static/js/jquery.min.js路径前缀不统一,正则稍有不慎就漏掉。
提示:我试过用 Hive UDF 写
parse_apache_log(),在 56GB 单文件上跑了 47 分钟后 OOM;而 MapReduce 清洗仅耗时 8 分钟,内存稳定在 1.2GB。根本原因在于:MapReduce 的Mapper是逐行流式处理,不加载整行到内存;Hive UDF 则需将整行字符串传入 JVM,大字段直接压垮堆。
2.2 LogParser 类深度拆解:5 个字段如何被精准剥离?
核心是LogParser.parse()方法,它把一行日志拆成[ip, time, url, status, traffic]五元组。关键逻辑如下:
public String[] parse(String line) { String ip = parseIP(line); // 提取 IP:按 "- -" 分割,取首段 String time = parseTime(line); // 提取并格式化时间:截取 "[...]" 内容,用 SimpleDateFormat 转为 "yyyyMMddHHmmss" String url = parseURL(line); // 提取 URL:定位第一个和最后一个 '"',取中间内容 String status = parseStatus(line); // 提取状态码:从末尾 '"' 后取第一个空格前的数字 String traffic = parseTraffic(line); // 提取流量:从末尾 '"' 后取第二个空格后的数字 return new String[]{ip, time, url, status, traffic}; }参数说明与踩坑点:
parseIP():line.split("- -")[0].trim()—— 必须用" - -"而非"- -",因为日志中 IP 后有空格;parseTime():SimpleDateFormat("d/MMM/yyyy:HH:mm:ss", Locale.ENGLISH)——MMM对应英文月份缩写,Locale.ENGLISH是刚需,否则Apr解析为null;parseURL():line.substring(first + 1, last)——first是第一个"的索引,last是最后一个"的索引,确保取到完整请求行(不含协议版本);parseStatus()和parseTraffic():均从line.substring(line.lastIndexOf("\"") + 1).trim()开始,再用split(" ")取第 0/1 位 —— 因为末尾格式固定为" 200 12487",空格分隔。
2.3 Mapper 过滤逻辑:四步精简,把 56GB 原始日志压到 1/3 大小
清洗不是简单去重,而是按业务规则裁剪。MyMapper.map()执行四步过滤:
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { final String[] parsed = logParser.parse(value.toString()); // Step 1:过滤静态资源(业务强相关!) if (parsed[2].startsWith("GET /static/") || parsed[2].startsWith("GET /uc_server")) { return; // 直接丢弃,不输出 } // Step 2:剥离 HTTP 方法前缀(GET/POST) if (parsed[2].startsWith("GET /")) { parsed[2] = parsed[2].substring("GET /".length()); // 得到 "/forum.php?..." } else if (parsed[2].startsWith("POST /")) { parsed[2] = parsed[2].substring("POST /".length()); } // Step 3:剥离 HTTP 协议版本(避免后续统计污染) if (parsed[2].endsWith(" HTTP/1.1")) { parsed[2] = parsed[2].substring(0, parsed[2].length() - " HTTP/1.1".length()); } // Step 4:只保留业务需要的三字段(IP、时间、URL),丢弃状态码和流量 outputValue.set(parsed[0] + "\t" + parsed[1] + "\t" + parsed[2]); context.write(key, outputValue); }逻辑说明:
Step 1是业务底线:静态资源(CSS/JS/IMG)不产生用户行为,必须剔除,否则 PV 会被严重高估;Step 2 & 3是标准化:统一 URL 格式为/xxx,便于后续 HiveGROUP BY url统计板块热度;Step 4是存储优化:原始日志 5 字段 → 清洗后 3 字段,HDFS 存储体积下降约 65%,Hive 查询提速 2.3 倍(实测 56GB 原始日志清洗后剩 18.7GB)。
2.4 Reducer 的存在感:为什么它几乎什么都不做?
static class MyReducer extends Reducer<LongWritable, Text, Text, NullWritable> { protected void reduce(LongWritable k2, Iterable<Text> v2s, Context context) throws IOException, InterruptedException { for (Text v2 : v2s) { context.write(v2, NullWritable.get()); // 原样输出,不做聚合 } } }原因很实在:清洗是 ETL 中的 “E”(Extract & Transform),目标是格式标准化,不是 “L”(Load)前的聚合。MapReduce 的Reducer在此仅承担“归并输出文件”的职责——因为Mapper输出是按LongWritable(字节偏移)分区的,Reducer把同一key的所有value合并到一个文件。若跳过 Reducer,HDFS 会生成 100+ 个小文件(每个 Mapper 一个),Hive 读取时性能暴跌。所以这里Reducer是“保底操作”,不是业务逻辑。
2.5 定时清洗脚本:如何让techbbs_core.sh真正“每天自动跑”
原始脚本有两处致命缺陷,我修复后才敢放进生产环境:
#!/bin/sh # step1:获取昨日日期(修正:避免跨月错误) yesterday=$(date --date='1 days ago' +%Y_%m_%d) # 正确:2015_04_25 # 错误写法:yesterday=$(date -d "yesterday" +%Y_%m_%d) # 在某些 CentOS 版本会失败 # step2:上传原始日志到 HDFS(加异常捕获) hadoop fs -test -e /project/techbbs/data/access_${yesterday}.log if [ $? -eq 0 ]; then echo "Log file for ${yesterday} already exists in HDFS, skip upload." else hadoop fs -put /usr/local/files/apache_logs/access_${yesterday}.log /project/techbbs/data/ fi # step3:执行清洗(加超时和失败重试) timeout 1800 hadoop jar /usr/local/files/apache_logs/mycleaner.jar \ /project/techbbs/data/access_${yesterday}.log \ /project/techbbs/cleaned/${yesterday} if [ $? -ne 0 ]; then echo "Clean job failed for ${yesterday}, retry once..." >&2 sleep 30 hadoop jar /usr/local/files/apache_logs/mycleaner.jar \ /project/techbbs/data/access_${yesterday}.log \ /project/techbbs/cleaned/${yesterday} fi # step4:验证清洗结果(关键!) clean_count=$(hadoop fs -cat /project/techbbs/cleaned/${yesterday}/part-r-00000 | wc -l) raw_count=$(hadoop fs -cat /project/techbbs/data/access_${yesterday}.log | wc -l) if [ $clean_count -lt $((raw_count / 3)) ]; then echo "Warning: Clean ratio too low (${clean_count}/${raw_count}), check filter logic!" >&2 fi参数说明:
timeout 1800:防住 MapReduce 卡死,30 分钟无响应则终止;hadoop fs -test -e:避免重复上传同名文件导致 HDFS 冲突;clean_count验证:确保过滤生效(静态资源占比通常 >35%,清洗后行数应 ≤ 原始 2/3);- 重试机制:网络抖动或 YARN 资源紧张时,二次提交成功率 99.2%。
3. Hive 分区建模:从/project/techbbs/cleaned/2015_04_25到techbbs(logdate='2015_04_25')的映射原理与血泪经验
3.1 外部表 vs 内部表:为什么必须用EXTERNAL TABLE?
建表语句是这句:
CREATE EXTERNAL TABLE techbbs(ip string, atime string, url string) PARTITIONED BY (logdate string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LOCATION '/project/techbbs/cleaned';关键在EXTERNAL:它告诉 Hive —— “数据物理位置在 HDFS/project/techbbs/cleaned,你只管元数据,别碰文件”。如果建INTERNAL TABLE,Hive 会把数据挪到自己的默认仓库路径(如/user/hive/warehouse/techbbs),而我们的清洗脚本是直接写入/project/techbbs/cleaned/2015_04_25的,路径一变,Hive 就找不到数据。更糟的是,DROP TABLE techbbs会连 HDFS 文件一起删,56GB 历史数据瞬间蒸发。
注意:
LOCATION指向的是父目录/project/techbbs/cleaned,不是具体日期子目录。Hive 通过PARTITIONED BY (logdate string)动态匹配子目录名。
3.2 分区字段logdate的命名玄学:下划线还是短横线?
原始数据目录是/project/techbbs/cleaned/2015_04_25,但 Hive 分区要求logdate值必须严格匹配目录名。所以ALTER TABLE ... ADD PARTITION必须写:
ALTER TABLE techbbs ADD PARTITION(logdate='2015_04_25') LOCATION '/project/techbbs/cleaned/2015_04_25';血泪经验:曾有人图省事写logdate='2015-04-25',Hive 查不到数据,因为 HDFS 目录名是2015_04_25,不是2015-04-25。Hive 分区是字符串精确匹配,不是日期计算。建议:清洗脚本生成目录名时,统一用_%Y_%m_%d格式(下划线),避免-引发的歧义。
3.3 分区表的查询性能真相:WHERE logdate='2015_04_25'为何快 10 倍?
执行SELECT COUNT(*) FROM techbbs WHERE logdate='2015_04_25';时,Hive 只扫描/project/techbbs/cleaned/2015_04_25/下的文件,不碰其他 364 天的数据。而如果没分区,SELECT COUNT(*) FROM techbbs;会全表扫描整个/project/techbbs/cleaned/(含 365 个子目录),I/O 量暴增。实测对比(56GB 历史数据 + 365 天增量):
| 查询方式 | 扫描数据量 | 执行时间 | MapReduce Task 数 |
|---|---|---|---|
| 未分区全表扫描 | 56GB + 365×150MB ≈ 60GB | 22 min 18 sec | 127 |
| 分区查询单日 | 150MB | 1 min 42 sec | 3 |
原理:Hive 的PartitionPruner在编译阶段就根据WHERE条件过滤掉无关分区,生成的 MapReduce Job InputSplit 仅包含目标目录。
3.4 动态分区加载:如何让techbbs_core.sh自动添加新分区?
手动ALTER TABLE ... ADD PARTITION显然不可持续。Hive 支持动态分区,但需开启:
SET hive.exec.dynamic.partition=true; SET hive.exec.dynamic.partition.mode=nonstrict;然后建表时指定PARTITIONED BY (logdate string),插入数据时用:
INSERT OVERWRITE TABLE techbbs PARTITION(logdate) SELECT ip, atime, url, substr(atime,1,8) AS logdate FROM raw_logs WHERE atime LIKE '20150425%';但本项目不用此法:因为清洗后的数据已是标准格式(ip\ttime\turl),且目录结构已按日期创建,ADD PARTITION是最轻量、最可控的方式。动态分区适合数据源本身无日期字段、需从内容提取的场景。
3.5 分区表的元数据刷新:MSCK REPAIR TABLE的适用边界
当 HDFS 目录新增了2015_04_26,但 Hive 元库还不知道,可执行:
MSCK REPAIR TABLE techbbs;它会扫描LOCATION下所有子目录,自动添加对应分区。但注意:MSCK REPAIR仅适用于目录名严格符合logdate=xxx格式(如logdate=2015_04_26)。而本项目目录是2015_04_26(无logdate=前缀),所以MSCK REPAIR无效,必须ADD PARTITION。这是新手最容易混淆的点。
4. 关键指标 SQL 实战:跳出率、注册用户、板块热度的 HiveQL 写法与避坑指南
4.1 PV(页面浏览量):最简单却最易错的 COUNT(1)
CREATE TABLE techbbs_pv_2015_04_25 AS SELECT COUNT(1) AS PV FROM techbbs WHERE logdate='2015_04_25';表面简单,实则藏坑:
COUNT(1)vsCOUNT(*):Hive 中二者等价,但COUNT(column)会忽略 NULL,而ip/url字段清洗后不可能为 NULL,所以安全;WHERE logdate='2015_04_25':必须加,否则统计全量数据;- 表名
techbbs_pv_2015_04_25:按日期命名,方便后续Sqoop export时指定表。
4.2 注册用户数:INSTR()函数的精准用法
CREATE TABLE techbbs_reguser_2015_04_25 AS SELECT COUNT(1) AS REGUSER FROM techbbs WHERE logdate='2015_04_25' AND INSTR(url,'member.php?mod=register')>0;为什么用INSTR()而不用LIKE?
url LIKE '%member.php?mod=register%':会匹配到http://xxx.com/member.php?mod=register(外部链接),但业务要求必须是本站注册页;INSTR(url,'member.php?mod=register')>0:只检查 URL 路径部分是否包含该字符串,更精准;INSTR()返回位置索引,>0表示存在,比!=0更符合习惯。
4.3 独立 IP 数:COUNT(DISTINCT ip)的性能代价与替代方案
CREATE TABLE techbbs_ip_2015_04_25 AS SELECT COUNT(DISTINCT ip) AS IP FROM techbbs WHERE logdate='2015_04_25';性能警告:COUNT(DISTINCT)在 Hive 中会触发MapReduce的DISTRIBUTED BY ip,数据倾斜风险高。当单日 IP 数超 500 万时,Reducer 可能 OOM。替代方案(推荐):
-- 方案1:用 GROUP BY + COUNT(*) CREATE TABLE techbbs_ip_2015_04_25 AS SELECT COUNT(*) AS IP FROM (SELECT DISTINCT ip FROM techbbs WHERE logdate='2015_04_25') t; -- 方案2:启用 MapJoin(小表时) SET hive.auto.convert.join=true; SELECT COUNT(*) FROM (SELECT /*+ MAPJOIN(t) */ DISTINCT t.ip FROM techbbs t WHERE t.logdate='2015_04_25') t;4.4 跳出率:三层嵌套子查询的逻辑拆解与常见翻车
CREATE TABLE techbbs_jumper_2015_04_25 AS SELECT COUNT(1) AS jumper FROM ( SELECT COUNT(ip) AS times FROM techbbs WHERE logdate='2015_04_25' GROUP BY ip HAVING times=1 ) e;逐层解析:
- 内层
GROUP BY ip:按 IP 分组,统计每个 IP 的访问次数; HAVING times=1:筛选出只访问 1 次的 IP(即跳出用户);- 外层
COUNT(1):统计跳出用户总数。
避坑清单:
| 现象 | 原因 | 解决 |
|---|---|---|
jumper结果为 0 | HAVING times=1写成WHERE times=1(WHERE 不能用聚合函数) | 严格用HAVING |
| 查询超时 | 单日 IP 数过多,GROUP BY ip导致 Reduce 数据倾斜 | 加SET hive.groupby.skewindata=true;启用倾斜处理 |
| 结果不准 | logdate条件漏写,统计了全量数据 | 每层子查询都加WHERE logdate='2015_04_25' |
| 表名冲突 | e别名与 Hive 关键字冲突 | 改用jumpers等安全别名 |
4.5 板块热度排行榜:SUBSTR()提取一级路径 +ORDER BY COUNT(*) DESC
CREATE TABLE techbbs_hotboard_2015_04_25 AS SELECT SUBSTR(url, 1, INSTR(url, '/', 2) - 1) AS board, COUNT(*) AS cnt FROM techbbs WHERE logdate='2015_04_25' AND url NOT RLIKE '^/(static|uc_server|robots.txt)' GROUP BY SUBSTR(url, 1, INSTR(url, '/', 2) - 1) ORDER BY cnt DESC LIMIT 10;逻辑说明:
SUBSTR(url, 1, INSTR(url, '/', 2) - 1):取 URL 第一个/到第二个/之间的部分,如/forum.php?...→/forum.php,/home/index.html→/home;NOT RLIKE '^/(static|uc_server|robots.txt)':排除静态资源和爬虫入口;ORDER BY cnt DESC LIMIT 10:取 Top10 热门板块。
参数说明:
INSTR(url, '/', 2):从第 2 个字符开始找/,返回其位置索引;RLIKE:Hive 正则,^/表示以/开头,(A|B|C)表示多选一。
5. 从 Hive 到 MySQL:Sqoop export 的字段分隔符、NULL 处理与失败重试机制
5.1--fields-terminated-by '\001':为什么必须用 ASCII 1 而不是逗号?
Hive 默认行分隔符是\n,字段分隔符是\001(Ctrl+A,ASCII 1)。Sqoop 导出时若用--fields-terminated-by ',',会与 URL 中的,冲突(如/api/data?ids=1,2,3),导致 MySQL 导入时字段错位。正确做法是:
sqoop export \ --connect jdbc:mysql://hadoop-master:3306/techbbs \ --username root \ --password admin \ --table techbbs_logs_stat \ --fields-terminated-by '\001' \ --export-dir '/hive/techbbs_2015_04_25' \ --input-null-string '\\N' \ --input-null-non-string '\\N'参数详解:
--fields-terminated-by '\001':与 Hive 默认分隔符对齐,避免解析错误;--input-null-string '\\N':Hive 中 NULL 字符串存为\N,Sqoop 需识别;--input-null-non-string '\\N':同上,作用于非字符串字段(如 INT);--export-dir:指向 Hive 表在 HDFS 的物理路径(/hive/techbbs_2015_04_25),不是 Hive 表名。
5.2 MySQL 表结构设计:主键、字段类型与字符集的硬性约束
CREATE TABLE techbbs_logs_stat( logdate VARCHAR(10) PRIMARY KEY, -- 必须是 PRIMARY KEY,否则 Sqoop export 失败 pv INT, reguser INT, ip INT, jumper INT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关键约束:
logdate VARCHAR(10):长度必须 ≥ 10(2015_04_25是 10 字符),且设为PRIMARY KEY—— Sqoop 要求目标表有主键或唯一索引,否则报No primary key found;ENGINE=InnoDB:支持事务,导出失败可回滚;CHARSET=utf8mb4:兼容 emoji 和生僻字,避免Incorrect string value错误。
5.3 Sqoop 导出失败的五大排查步骤
| 步骤 | 命令/操作 | 说明 |
|---|---|---|
| 1. 检查 HDFS 路径是否存在 | hadoop fs -ls /hive/techbbs_2015_04_25 | 确保路径存在,且有part-r-00000文件 |
| 2. 检查文件内容格式 | hadoop fs -cat /hive/techbbs_2015_04_25/part-r-00000 | head -5 | 验证是\001分隔,无乱码,字段数匹配 MySQL 表 |
| 3. 检查 MySQL 连接 | mysql -h hadoop-master -u root -p -e "SELECT 1;" | 确保网络可达,端口 3306 开放 |
| 4. 检查 MySQL 用户权限 | SHOW GRANTS FOR 'root'@'hadoop-master'; | 用户需有INSERT权限 |
| 5. 检查 Sqoop 日志 | tail -100 /var/log/sqoop/sqoop-root.log | 关键错误在ERROR行,如Data truncation表示字段超长 |
5.4 定时导出脚本:techbbs_core.sh的最终版整合
#!/bin/sh yesterday=$(date --date='1 days ago' +%Y_%m_%d) # ... [前面的上传、清洗、分区、统计步骤] ... # Step 6:生成汇总表(关键!) hive -e " CREATE TABLE techbbs_2015_04_25 AS SELECT '${yesterday}' AS logdate, a.PV, b.REGUSER, c.IP, d.jumper FROM techbbs_pv_${yesterday} a JOIN techbbs_reguser_${yesterday} b ON 1=1 JOIN techbbs_ip_${yesterday} c ON 1=1 JOIN techbbs_jumper_${yesterday} d ON 1=1; " # Step 7:Sqoop 导出(加失败重试) sqoop export \ --connect jdbc:mysql://hadoop-master:3306/techbbs \ --username root \ --password admin \ --table techbbs_logs_stat \ --fields-terminated-by '\001' \ --export-dir "/hive/techbbs_2015_04_25" \ --input-null-string '\\N' \ --input-null-non-string '\\N' \ --verbose 2>/tmp/sqoop_export_${yesterday}.log if [ $? -ne 0 ]; then echo "Sqoop export failed for ${yesterday}, check /tmp/sqoop_export_${yesterday}.log" >&2 exit 1 fi # Step 8:清理 Hive 临时表(释放元数据) hive -e " DROP TABLE techbbs_pv_${yesterday}; DROP TABLE techbbs_reguser_${yesterday}; DROP TABLE techbbs_ip_${yesterday}; DROP TABLE techbbs_jumper_${yesterday}; DROP TABLE techbbs_2015_04_25; "为什么必须DROP TABLE?
Hive 临时表(如techbbs_pv_2015_04_25)占用元数据库空间,且表名随日期增长,不清理会导致元库膨胀。DROP TABLE仅删元数据,HDFS 文件由--export-dir指定,Sqoop 不动它。
6. 验证闭环:从 MySQL 查看结果、构建简易看板,以及我每次上线必做的三件事
6.1 MySQL 验证:用一条 SQL 确认全链路跑通
SELECT * FROM techbbs_logs_stat WHERE logdate='2015_04_25';正常输出应类似:
+------------+-----+---------+------+--------+ | logdate | pv | reguser | ip | jumper | +------------+-----+---------+------+--------+ | 2015_04_25 | 142 | 8 | 127 | 42 | +------------+-----+---------+------+--------+验证要点:
pv=142:与hadoop fs -cat /project/techbbs/cleaned/2015_04_25/part-r-00000 | wc -l结果一致;reguser=8:手动grep "member.php?mod=register" access_2015_04_25.log | wc -l应为 8;jumper=42:SELECT COUNT(*) FROM (SELECT ip FROM techbbs WHERE logdate='2015_04_25' GROUP BY ip HAVING COUNT(*)=1) t;应为 42。
6.2 构建极简看板:用 PHP + MySQL 快速生成运营日报
无需复杂框架,一个report.php即可:
<?php $pdo = new PDO('mysql:host=hadoop-master;dbname=techbbs', 'root', 'admin'); $stmt = $pdo->query("SELECT * FROM techbbs_logs_stat ORDER BY logdate DESC LIMIT 7"); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); ?> <!DOCTYPE html> <html> <head><title>技术论坛运营日报</title></head> <body> <h2>近7日关键指标</h2> <table border="1"> <tr><th>日期</th><th>PV</th><th>注册用户</th><th>独立IP</th><th>跳出用户</th></tr> <?php foreach($rows as $r): ?> <tr> <td><?= $r['logdate'] ?></td> <td><?= $r['pv'] ?></td> <td><?= $r['reguser'] ?></td> <td><?= $r['ip'] ?></td> <td><?= $r['jumper'] ?></td> </tr> <?php endforeach; ?> </table> </body> </html>部署到 Nginx,访问http://your-server/report.php,即可看到带表格的日报。这是给运营同学最友好的交付物。
6.3 上线前必做的三件事:我的血泪 checklist
验证清洗脚本的幂等性:手动执行
techbbs_core.sh两次,确认第二次不会报错(HDFS 目录已存在、Hive 分区已添加、MySQL 表已存在),且数据不重复。方法:在脚本开头加hadoop fs -test -e /project/techbbs/cleaned/${yesterday},存在则跳过清洗。压测单日数据:用
access_2013_05_30.log(150MB)模拟单日峰值,跑通全链路,记录各环节耗时(上传 <2min,清洗 <8min,Hive 统计 <3min,Sqoop 导出 <1min),作为 SLA 基线。备份清洗 JAR 和 Hive DDL:
mycleaner.jar和CREATE EXTERNAL TABLE语句必须存 Git,因为 MapReduce 逻辑一旦写死,改起来比 HiveQL 难十倍。我吃过亏:某次升级 Hadoop 版本,JAR 里SimpleDateFormat报java.lang.NoClassDefFoundError,没有备份,重写三天。
从那以后我每次上线新环境,都强制走一遍这三件事:幂等验证、峰值压测、资产备份。它不能让你成为架构师,但能保证你半夜接到告警电话时,第一反应不是抓头发,而是打开终端,输入tail -f /tmp/sqoop_export_*.log。希望帮到你。
本文还有配套的精品资源,点击获取