网站流量日志数据分析系统,听起来就是一个把 Nginx 日志接进来、算出 PV/UV、出报表的离线任务。可真动手做一轮,你会发现这套链路里藏了太多“看起来简单、做起来想骂人”的细节。我去年从头到尾搭建过一套这样的系统,今天把它拆开聊透,适合准备入门 Hadoop 离线数仓、或者想把公司日志分析体系补全的朋友做参考。整套东西不依赖实时计算,HDFS + Hive + 调度 + 导出就是全部家当,但怎么把每个环节做扎实,才是这套系统真正的价值所在。
1. 项目到底要做什么:需求拆解与技术选型
1.1 网站流量指标的业务口径从哪来
我们当时接的是一个内部业务平台的访问日志,日增量大概在 3 亿条左右,压缩落地后一百多 GB。业务方张嘴就要 PV、UV、热门页面、访问时长、跳出率、来源分析,后面还要按省份、按浏览器拆维度。听起来都是常见指标,但真要对齐口径就会发现,每个指标都需要业务方确认定义:
- PV 是“页面浏览量”,那静态资源算不算?jpg、css、js 必须滤掉。
- UV 是“独立访客”,按什么识别?IP、cookie、还是登录用户 ID?如果按 IP,局域网代理会导致 UV 被严重低估。
- 跳出率指的是“只访问了一个页面就离开的会话占比”,那就必须先定义什么是“一个会话”,30 分钟无新动作算一次会话过期?每次点击算新会话?
这些口径没定清楚,后面所有 HQL 都会白写。我的做法是拉上业务方一起列了一张指标口径清单,把每个指标的名称、定义、统计周期、维度字段写清楚,然后再开始设计表结构。
技术侧的需求其实不复杂:每天凌晨跑一次数据处理,上午 9 点前把结果推到报表库,给运营和产品看昨天的数据。它天然就是一个 T+1 的离线场景,对实时性几乎没有任何要求。数据量虽然到了百 GB 级,但离线批处理完全扛得住。
1.2 为什么是离线 Hadoop 而不是 Spark/Flink
这个项目选型时不是没有纠结过。团队里有人提出用 Spark 会不会更快,用 Flink 是不是能把时效升级到分钟级。我当时的判断是:不要为了一点点性能优势,把整套系统的复杂度抬上去。
- 时效要求:业务方只要“昨天全天数据”,晚上 12 点后开始跑完全来得及。离线 T+1 是性价比最高的方案。
- 技术栈匹配:团队对 Hive SQL 的熟悉程度远高于 Scala 和 Java,Spark 虽快但需要额外排查内存问题;Flink 更不必说,状态管理、精确一次语义、窗口机制,学习成本高。
- 基于 HDFS 的离线链路生态成熟:Flume 采集、Hive 计算、Sqoop 导出、调度平台跑任务,每一个环节都有大量公开实践可以抄作业。
数据量级也没到非用 Spark 不可的程度。3 亿条日访问日志,Hive 跑聚合任务,MapReduce 引擎下大概 40 到 60 分钟能跑完;如果用 Tez 引擎还能再快一些。为了快这半小时去迁移 Spark,完全不值得。
所以最终方案用了 Hadoop 离线全家桶:Flume 负责日志采集,HDFS 做存储,Hive 做清洗和指标计算,Oozie 负责任务编排,Sqoop 把结果导出到 MySQL,最后接报表平台。链路看起来长,但每个环节都有现成的轮子可以复用。
1.3 组件选型和链路设计
整体链路我用文字梳理一遍,流程如下:
- Nginx 将访问日志写到本地磁盘,日志格式配置成统一的 JSON 或 Key-Value 结构。
- 每台业务服务器部署 Flume Agent,用 taildir source 监控日志文件,将新写入的数据发送到采集集群的 Avro source。
- Flume 汇聚层将数据下沉到 HDFS,按日期和小时拆分目录,写入时直接压缩成 gzip 文件。
- 次日由调度平台触发 Hive 脚本:先加载原始日志到 ODS 层外部表,再清洗过滤得到 DWD 层明细表,然后按维度汇总到 DWS 层,最后生成 ADS 层业务指标表。
- Sqoop 将 ADS 层结果表导出到 MySQL。
- 报表平台或邮件报表直接查询 MySQL,给业务方展示。
这套链路里最容易被忽略的是 Flume 的 sink 端小文件控制。Flume 默认写 HDFS 会按你设置的滚动时间生成文件,如果设成 60 秒滚动一次,一晚上下来会出现几万个小文件,后面跑 Hive 时 NameNode 压力巨大。这个问题后面第 5 节细讲。
选型上还有一个细节:我们最初用 Hive 默认的 MapReduce 引擎,跑 3 亿条日志的 UV 去重要 50 分钟。后来把执行引擎切成 Tez,相同任务压到了 30 分钟左右,改动成本很低。所以如果你在用 Hive 3.x,强烈建议检查一下hive.execution.engine配置,别死守 MapReduce。
2. 日志采集与预处理:数据是地基
2.1 日志格式要注意的坑
Web 服务器日志最常见的坑就是“字段不完整”和“格式五花八门”。我们接手时发现 Nginx 日志里有几条明显缺字段:-代表空值、URL 里带着无法解码的乱码、user_agent 字段里混着换行符,直接切分会导致字段错位。
所以第一步是让 Nginx 输出为统一 JSON 格式。我们在 Nginx 配置里用log_format将remote_addr、time_local、request、status、body_bytes_sent、http_referer、http_user_agent、request_time等字段拼成 JSON。这样做比用默认的combined格式好在:解析时只需要 JSON 解析,不用做空格切分;字段缺失时也能保留空字符串而不是直接错位。
但 JSON 也有新坑:业务方在 URL 上带一些特殊字符,例如引号、反斜杠,会导致生成的 JSON 不合法,需要在 Nginx 侧把特殊字符做转义。我们线上就遇到过用户搜索词里带双引号,直接把整行日志干成脏数据的情况。
除了格式,字段补充也要在源头考虑。原始日志没有省份信息,只能拿到 IP,所以 ETL 阶段要用 IP 库把 IP 解析成省份、城市;user_agent 里有浏览器和系统信息,需要用解析库拆出browser、os、device_type字段。这些操作放在清洗阶段做,不放在采集端,因为 Flume 不擅长复杂计算。
2.2 Flume 采集配置实战
采集端用的是 Flume 1.9,核心诉求是:不丢数据、不产生大量小文件、能断点续采。这里我给出一份常用的采集 Agent 配置,大家可以直接参考:
# 业务服务器上的 Flume Agent:taildir + avro sink a1.sources = r1 a1.channels = c1 a1.sinks = k1 a1.sources.r1.type = TAILDIR a1.sources.r1.positionFile = /data/flume/taildir_position.json a1.sources.r1.filegroups = f1 a1.sources.r1.filegroups.f1 = /data/nginx/logs/access.log a1.sources.r1.fileHeader = true a1.channels.c1.type = file a1.channels.c1.checkpointDir = /data/flume/checkpoint a1.channels.c1.dataDirs = /data/flume/data a1.sinks.k1.type = avro a1.sinks.k1.hostname = flume-agg-01 a1.sinks.k1.port = 4141 a1.sources.r1.channels = c1 a1.sinks.k1.channel = c1汇聚层 Agent 的 sink 写 HDFS,关键配置如下:
# 汇聚层 Flume Agent:avro source + hdfs sink a2.sources = r1 a2.channels = c1 a2.sinks = k1 a2.sources.r1.type = avro a2.sources.r1.bind = 0.0.0.0 a2.sources.r1.port = 4141 a2.channels.c1.type = file a2.sinks.k1.type = hdfs a2.sinks.k1.hdfs.path = /warehouse/ods/raw_log/date=%Y-%m-%d/hour=%H a2.sinks.k1.hdfs.filePrefix = log a2.sinks.k1.hdfs.fileType = CompressedStream a2.sinks.k1.hdfs.codec = gzip a2.sinks.k1.hdfs.rollInterval = 300 a2.sinks.k1.hdfs.rollSize = 268435456 a2.sinks.k1.hdfs.rollCount = 0 a2.sinks.k1.hdfs.batchSize = 2000这段配置里有几个点很容易踩坑:
rollCount=0表示不按事件条数滚动文件,只按时间和大小。rollSize是每个文件达到 256MB 就滚动。这个值要配合后续 Hive 读取,不是越大越好,我们的经验是 128MB 到 512MB 都行。batchSize控制批量写 HDFS 的事务大小,太大会增加内存压力,2000 是比较稳妥的值。
Flume 埋点重启后为什么不会丢数据?依赖 taildir source 维护的 position 文件,它把每个文件读取的偏移量记录下来了,Agent 重启后从偏移量位置继续读。但要注意 positionFile 所在目录的权限,否则 Flume 起不来,数据会延迟采集,到早上的调度时点还没采完。
2.3 清洗与富化:ETL 该做哪些事
原始日志落 HDFS 后,接下来是以 Hive SQL 为主做清洗和富化。这个阶段目标很明确:
- 过滤无效数据:去掉状态码在 400 以上的请求,去掉以
.js、.css、.png、.jpg、.ico结尾的静态资源请求,去掉明显是爬虫的 user_agent。 - 字段标准化:把时间字段从字符串转成 timestamp,把 URL 解析出 path 和 query,把 IP 解析成省份。
- 会话标识生成:为每条访问记录生成一个可聚合的
session_id,后面算跳出率和平均访问时长就靠它。
清洗阶段的 HQL 是整套系统里最长最繁琐的,因为你要处理各种脏数据。比如 IP 字段出现-,时间字段出现null,user_agent 里出现半个中文字符。我的建议是:清洗分两层,第一层做硬过滤,直接丢脏数据;第二层做字段弥补,能解析的解析,不能解析的填unknown,不要因为一条脏数据让整个任务失败。
这一层 HQL 示例大致长这样:
INSERT OVERWRITE TABLE dwd_access_log PARTITION (dt = '${dt}') SELECT remote_addr, parse_url(request_url, 'PATH') AS request_path, parse_url(request_url, 'QUERY') AS request_query, status, body_bytes_sent, from_unixtime(unix_timestamp(time_local, 'yyyy-MM-dd HH:mm:ss')) AS access_time, ip_to_province(remote_addr) AS province, parse_user_agent(user_agent, 'browser') AS browser, parse_user_agent(user_agent, 'os') AS os FROM ods_raw_log WHERE dt = '${dt}' AND status < 400 AND request_path NOT RLIKE '\\.(js|css|png|jpg|ico|svg|woff|woff2)$' AND remote_addr != '-';有人会问,为什么不直接对 ODS 表做聚合,非要清洗一层明细?如果直接把原始日志全量保存,每天重复跑聚合时耗时会越来越长;而且明细层可以反复复用,后面加一个新指标,只需要从明细层重新聚合一次,不需要再解析一遍原始字符串。砍掉哪一层的干净明细,后面新增需求时哭的就是你。
3. Hive 数仓建模与指标计算:整套系统的重头
3.1 数仓分层:别一张表打天下
很多入门同学做日志分析时只建一张表,所有字段全塞进去,然后直接在原始表上跑count(distinct ...)。小数据量感觉没问题,一到亿级数据就原形毕露:跑一次要一小时,而且团队里每个人都写自己的临时 SQL,口径完全对不上。
这套系统我按标准数仓四层模型来搭建:
- ODS 层:
ods_raw_log,只做原始日志落地,与日志源字段一一对应,保留原始数据用于回溯和排查。 - DWD 层:
dwd_access_log,经过清洗、过滤、维度补全、会话标识生成的明细事实表,粒度是一条访问记录。 - DWS 层:
dws_flow_stats,按业务维度预先聚合,比如按日期 + 省份 + 浏览器统计 PV/UV/会话数。 - ADS 层:
ads_flow_report,面向业务方的最终结果表,按天存储核心指标,供报表查询。
这样分层最大的好处是:基础数据只清洗一次,所有人都从 DWD 层读取;指标口径收敛在 DWS 层,每次加指标只影响新增字段,不动老逻辑。我把“分层”理解为“把做饭和吃饭分开”:ODS 是菜市场买回来的菜,DWD 是洗好切好的菜,DWS 是半成品,ADS 是直接上桌的成品。
3.2 核心表结构与分区策略
ODS 表我建成了 Hive 外部表,数据由 Flume 直接写到表目录下,表本身没做复杂压缩转换:
CREATE EXTERNAL TABLE IF NOT EXISTS ods_raw_log ( remote_addr STRING, time_local STRING, request_url STRING, status INT, body_bytes_sent BIGINT, http_referer STRING, user_agent STRING, request_time FLOAT ) PARTITIONED BY (dt STRING, hour STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' STORED AS TEXTFILE LOCATION '/warehouse/ods/raw_log';注意外部表这个细节。如果 Hive 表是内部表,一个误操作DROP TABLE会把 HDFS 上的原始日志全删掉,找不回来;外部表删表只删元数据,数据文件还在 HDFS 上,这是日志数据最基本的保命手段。
DWD 层建议用列式存储加压缩:
CREATE TABLE IF NOT EXISTS dwd_access_log ( remote_addr STRING, province STRING, browser STRING, os STRING, request_path STRING, request_query STRING, status INT, body_bytes_sent BIGINT, access_time TIMESTAMP, session_id STRING ) PARTITIONED BY (dt STRING) STORED AS PARQUET TBLPROPERTIES ('parquet.compression'='SNAPPY');使用 Parquet 后的体积只有原始文本的 1/4 左右,而且列式存储在按省份、浏览器做聚合时更快。为什么分区只保留dt而不是dt + hour?因为清洗后是按天分析,小时分区只会导致大量小文件,没必要保留小时粒度。
3.3 PV/UV/跳出率等指标的计算口径与 SQL
指标计算是整个系统的核心,也是口径最容易打架的地方。我贴几个关键指标的 HQL 片段和边界条件说明。
PV是最简单的:
SELECT dt, COUNT(*) AS pv FROM dwd_access_log WHERE dt = '${dt}' GROUP BY dt;UV如果直接用COUNT(DISTINCT remote_addr),在亿级数据量下很容易因为数据倾斜变慢。优化思路是先做一层子查询去重再 count:
SELECT dt, COUNT(*) AS uv FROM ( SELECT remote_addr FROM dwd_access_log WHERE dt = '${dt}' GROUP BY remote_addr ) t;这里要注意“UV 按 IP 口径”和“UV 按设备口径”是有本质差别的。如果业务方要求按 cookie 或用户 ID 去重,DWD 层必须保留对应字段,HQL 改 group by 字段即可。
会话划分是跳出率和平均访问时长的基础。我们用的标准是:同一个remote_addr(或 cookie)下,相邻两条访问记录时间差超过 30 分钟,算作一个新的会话。在 Hive 里可以用窗口函数LAG实现:
SELECT remote_addr, access_time, session_id FROM ( SELECT remote_addr, access_time, CASE WHEN LAG(access_time) OVER ( PARTITION BY remote_addr ORDER BY access_time ) IS NULL OR UNIX_TIMESTAMP(access_time) - UNIX_TIMESTAMP( LAG(access_time) OVER ( PARTITION BY remote_addr ORDER BY access_time ) ) > 1800 THEN CONCAT(remote_addr, '_', ROW_NUMBER() OVER ( PARTITION BY remote_addr ORDER BY access_time )) ELSE NULL END AS session_id FROM dwd_access_log WHERE dt = '${dt}' ) t;这段 SQL 生成的session_id会在下一次 CASE 判断时通过上一个值带下去,所以必须再做一次LAST_VALUE或“向前填充”补齐 session_id。更标准的分会话方案是把数据导出到 Spark 或写 UDF 去做,但在不引新组件的前提下,Hive 窗口函数也能完成,只是脚本会稍长。
跳出率的定义是“跳出会话数 / 总会话数”。一个会话内如果只有一次页面浏览,就算跳出:
SELECT dt, SUM(IF(session_cnt = 1, 1, 0)) / COUNT(session_id) AS bounce_rate FROM ( SELECT session_id, COUNT(*) AS session_cnt FROM dwd_access_log WHERE dt = '${dt}' GROUP BY session_id ) t;平均访问时长同理:先算出每个会话的MAX(access_time) - MIN(access_time),再求均值。
需要特别说的是:指标 SQL 写出来不是终点,跑出来的数字要和业务方预期核对。我记得第一版跳出率脚本跑出来 68%,业务方说“不可能这么高”,后来一查是测试环境的一批爬虫只抓首页就离开,把跳出率拉高了。过滤爬虫口 HQL 加了一个白名单判断,再跑数值才回到 35% 左右。指标异常很多时候不是代码错,而是口径和现实没对齐。
4. 调度、导出与报表展示:把离线链路串起来
4.1 为什么不用 crontab 硬顶
离线链路有三个任务阶段:Flume 落 HDFS、Hive 清洗聚合、Sqoop 导出。如果只用 crontab,每个任务之间的依赖只能靠“时间估算”去硬等,例如设定 2 点跑清洗、6 点跑导出。集群一忙,计算延迟 20 分钟,清洗任务还没结束,导出任务已经把空结果推到 MySQL 了。
所以调度这么做:用一个工作流引擎把三个环节编排成 DAG,每个节点执行具体的脚本,节点之间做依赖检查,上游成功才触发下游,失败自动重试,最终任务全跑完可以发一条日报“数据已更新”。
我们用过 Oozie,后来切到了 Azkaban。简单对比一下:
| 能力 | Oozie | Azkaban |
|---|---|---|
| 配置方式 | XML,复杂 | properties,直观 |
| 任务依赖 | 支持 DAG | 支持 DAG |
| 失败重跑 | 支持且灵活 | 支持指定节点重跑 |
| 使用体验 | 学习成本高 | 上手快 |
如果你不是为了兼容 Hadoop 老版本生态,我更推荐 Azkaban。它用纯 properties 就能定义任务,还能在 Web UI 上直接点击失败节点重跑,省去了频繁登录服务器敲命令的麻烦。
调度脚本的核心是一个 Shell 包装,传dt参数给 HQL,并保证每个任务幂等。幂等是什么意思?同一份数据重跑多次,结果一致,不会因为重试导致数据翻倍。最简单的方式是每个 HQL 开头都执行INSERT OVERWRITE,指定dt分区,这样重跑就是覆盖写,不会留脏数据。
4.2 Sqoop 导出细节
指标算完后要推给业务看,Hive 表直接查询不现实,所以用 Sqoop 把 ADS 层表导出到 MySQL:
sqoop export \ --connect jdbc:mysql://mysql-host:3306/analytics \ --username reader --password xxx \ --table ads_flow_report \ --export-dir /warehouse/ads/ads_flow_report/dt=${dt} \ --input-fields-terminated-by '\001' \ --update-mode allowinsert \ --update-key dt \ --num-mappers 4这里有两个关键点:
- 并行度:
num-mappers控制并行导出线程数,不要盲目调到 30。MySQL 写入能力有限,并行太高会直接打爆数据库连接,我们一般设 4 到 8 个。 - 幂等:
update-mode allowinsert配合update-key dt,让 Sqoop 对主键冲突的行执行 update,不冲突则 insert,这样重跑同样日期的导出不会导致报表出现两行相同日期数据。
还有一个容易忽略的问题:Hive 导出到 MySQL 时字段分隔符要统一。Hive 表如果设置了自定义分隔符,比如\001,Sqoop 必须要加上--input-fields-terminated-by '\001',否则 sqoop 默认按逗号切分,所有字段都会错位,报表里数字全变成 0。
4.3 报表侧优化与数据校验
指标表导出到 MySQL 后,报表平台直接查询这张表。但 MySQL 单表数据量过大时查询会变慢,所以要在报表表上建索引:按dt + province + browser建联合索引,这样业务方按天筛选时能快速出数。
同时,报表侧不是只看数字,我们对每天的结果做数据质量校验:
- 当日 PV 和前一天对比,波动超过 30% 自动告警。
- 当日 UV 和前一天对比,波动超过 30% 自动告警。
- 检查 MySQL 报表表当天是否有数据、是否有重复行。
这些校验用 Shell 脚本 + 定时任务就能完成,不用引入复杂的监控系统。数据团队接到告警后第一时间检查 Nginx 日志是否有断流、Flume 是否有积压、Hive 任务是否有重试。
5. 离线系统常见问题与排查实录
5.1 小文件堆积:离线任务的第一杀手
我们系统刚上线时,跑了一周之后发现 Hive 任务越跑越慢,NameNode 的 RPC 请求经常超时。一查 HDFS 目录,Flume 按小时分区,每个分区下有几百个几 KB 到几十 MB 的小文件。因为配置里rollInterval=300,每 5 分钟就滚动一次,一个小时内 12 个文件,一天 288 个文件,一周 2000 多个。
小文件的危害不只是占 NameNode 内存,Hive 读取时要同时打开几千个文件,Map Task 数量暴涨,集群调度和计算成本跟着翻倍。
解决思路有两层:第一层在源头控制,把 Flume 的rollSize调大到 256MB,rollInterval调大到 30 分钟,让文件尽量大、数量尽量少。第二层在周期上兜底,每天调度一个文件合并任务,对当天的分区做INSERT OVERWRITE重新写成少量大文件:
INSERT OVERWRITE TABLE ods_raw_log PARTITION (dt = '${dt}') SELECT * FROM ods_raw_log WHERE dt = '${dt}';注意这一步也会触发一次全分区重读,所以只对时间超过一天的历史分区做定期合并,比如一周合并一次。千万不要对当前正在写入的分区执行,否则新采到的日志会被覆盖掉。
5.2 数据倾斜:按天聚合时最容易踩
COUNT(DISTINCT remote_addr)最大的问题是会把相同 IP 的数据分到同一个 Reduce,热门 IP 可能携带了上百万条记录,单 Reduce 跑到内存溢出。我们专门做过一次数据倾斜排查,发现某个爬虫 IP 在一天内产生了近 200 万条请求,导致 UV 任务卡了 40 分钟没跑完。
解决方式最常用的是“两阶段聚合”:先打随机前缀,分两次 group by。第一层把相同 key 打散到不同 Reduce,第二层去掉前缀再聚合。在 Hive 里可以写成嵌套子查询:
SELECT dt, COUNT(*) AS uv FROM ( SELECT tmp.remote_addr FROM ( SELECT CONCAT(CAST(RAND() * 10 AS INT), '_', remote_addr) AS diy_key, remote_addr FROM dwd_access_log WHERE dt = '${dt}' GROUP BY CONCAT(CAST(RAND() * 10 AS INT), '_', remote_addr), remote_addr ) tmp GROUP BY tmp.remote_addr ) t;另外,Hive 3.x 自带hive.groupby.skewindata=true配置,可以在不做手动加盐的前提下自动分桶聚合,但手动加盐对倾斜场景更可控。加了这层优化之后,UV 任务从卡死状态降到 15 分钟内跑完。
5.3 重复数据与回溯重跑
离线系统最怕的就是“跑错了,重跑一遍数据不对”。我们遇到过一次:调度平台因为网络闪断重跑了当天的清洗任务,但 Sqoop 导出节点没有一起重跑,导致 MySQL 里缺当天数据;后来又手动补跑导出,结果因为update-mode allowinsert配置没加,插入了一行重复数据,报表里出现两个相同 dt 的记录。
这个问题的根治思路是让整条链路的每个节点都具备幂等性。Hive 脚本一律用INSERT OVERWRITE,Sqoop 导出必须带--update-key。这样无论失败重跑多少遍,结果都不会叠加。
数据回溯时也要注意:如果业务方要求重新计算过去 7 天数据,调度脚本要把dt参数改成循环传入,逐天执行,不要一口气写一个跑 7 天数据的 SQL。逐天跑的好处是失败后只需要重跑失败那天的数据,而不是整个区间推倒重来。
5.4 任务超时与资源分配
Hive 离线任务在集群资源紧张时很容易出现“任务排队半小时,Map 阶段跑 40 分钟,Reduce 阶段再跑 20 分钟”的情况。尤其是多张业务表同时跑日任务时,资源队列抢占非常明显。
我们当时的处理是两个方面:
- 给不同优先级任务分配独立 YARN 队列:核心日报任务调度到高优先级队列,临时分析任务到低优先级队列,避免相互抢占。
- 设置合理的 Hive 容器参数,例如:
set hive.tez.container.size=4096; set hive.tez.java.opts=-Xmx3072m; set mapreduce.map.memory.mb=4096; set mapreduce.reduce.memory.mb=4096;注意容器内存不能超过 YARN 单节点可用内存,不然任务会直接失败。先跑一次小分区数据,用yarn logs查看实际使用的 Container 内存,再微调参数,比随便拍脑袋设一个大值靠谱得多。
5.5 数据质量校验:没有监控就不算完成
系统上线前,我花了不少时间写数据质量校验。最基础的三道检查:
- 数据完整性:ODS 和 DWD 层当天分区是否存在,记录数是否为 0。
- 波动监控:核心指标 PV/UV 的环比波动是否超过 30%。超过就触发告警,人工排查是业务活动、流量异常还是采集故障。
- 一致性校验:DWS 和 ADS 的 PV 相加是否等于 ODS 明细总量,避免聚合时漏数据。
这套脚本放在每日任务最后,如果校验失败,直接给相关人员发报警,并阻止报表导出。没有监控的离线系统就是走钢丝,数据错了业务方已经看了一上午报表,再回去解释就非常被动。
最后再分享一个小技巧
整个项目踩坑最多的地方其实集中在数据口径和幂等设计。很多同学搭离线系统时觉得“能跑就行”,但真正上线跑一段时间就会明白:离线链路的核心不是某个组件用得有多熟,而是把每个环节都做成可重跑、可校验、不重复。哪怕你只是给一个小站点搭日志分析,也建议从第一天就做好分层和幂等。
我个人实际操作中的习惯是:每个 Hive 脚本顶部都有一段统一的“参数校验”脚本,判断传入的dt是否合法、要计算的分区是否存在,出现问题直接失败退出,避免在错误参数下跑出全表脏数据。
这套系统后来还接入了更多业务表,加了主题宽表,也在尝试往实时方向扩展。但说起来,离线 Hadoop 这套 T+1 链路永远是数据分析的地基,把它吃透,后面学什么新框架都不慌。希望这篇复盘能帮你少踩几个坑。