这篇是《大数据实践笔记》系列的第二篇。上一篇聊的更多是宏观层面,比如大数据到底怎么入门、各个框架之间是什么关系、数据工程师和数据科学家的工作边界在哪里。这篇我打算换个节奏,直接带大家走一遍一个相对完整的实践项目——从零搭一套用户行为日志分析系统,涵盖集群规划、组件部署、数据采集、离线计算、可视化大屏,最后再把这段时间踩过的坑和面试中被反复问到的点一起整理出来。
为什么要做这么一套东西?因为我发现很多朋友,包括当初的我自己,学了一大堆理论,HDFS、MapReduce、Hive、Spark 都背得滚瓜烂熟,但真要独立搭一个环境、把一个需求从数据源到展示完整串起来,还是会卡住。这个笔记就是用来解决“知道”和“做到”之间那段距离的。如果你正在做大数据毕业设计、准备大数据开发岗位的面试,或者单纯想搞清楚一套大数据项目到底长什么样,这篇内容应该够你“抄作业”了。
先说清楚一点,我不会把每个组件的每个参数都贴出来,那会变成官方文档的复述。我重点讲的是“为什么这么做”和“踩过什么坑”,这些才是普通文档里查不到的东西。
1. 项目整体设计:先想清楚再动手
1.1 需求从哪里来:选一个能落地的场景
我选的场景是“电商平台用户行为日志分析”。原因很简单,这个场景足够经典,又不需要依赖真实业务数据。用户浏览、点击、加购、下单,这些行为日志字段设计起来容易理解,统计出来的 PV、UV、留存率、转化率这些指标,大家一看到就有概念,不需要额外解释业务背景。
需求拆成四块:
- 数据采集:模拟生成用户行为日志,通过 Flume 写入 Kafka。
- 数据存储:Kafka 消息被消费后落到 HDFS,Hive 建分区表管理。
- 数据计算:用 Spark SQL 做离线统计,产出指标结果表。
- 数据展示:把指标结果同步到 MySQL,前端大屏定时拉取渲染。
这个链路是非常典型的离线数仓架构,即使以后换到真实业务,骨架也不用大改。日志源换成真实埋点,表结构加几个业务字段,剩下的逻辑基本可以复用。
1.2 技术选型:不追新,只看合不合适
选型结果如下:HDFS + ZooKeeper + Yarn + Hive + Spark SQL + Flume + Kafka + MySQL + React + TypeScript + ECharts。可能有朋友会问,怎么不用 Flink?Flink 的实时处理能力确实强,但我当时这个项目核心是离线报表,实时只做简单的指标展示,Spark SQL 已经能覆盖绝大部分需求,而且 Spark 生态完善,之后想扩展 Structured Streaming 也非常顺。
选型核心原则是“够用就好,留好扩展空间”。我见过不少项目,一上来就堆 Flink、HBase、Druid、Kylin 全家桶,最后全都没跑起来。大数据项目最怕的不是框架旧,而是链路太长,过了两周连你自己都记不清数据流到了哪里。能用一条简单直接的链路解决,就不要人为制造复杂度。
2. 集群规划与部署细节:少了这一步后面全是泪
2.1 集群规模:3 台虚拟机够不够
我的环境是 3 台虚拟机,每台 4 核 8G 内存、100G 磁盘。节点角色划分如下:
| 节点 | 角色 | 核心组件 |
|---|---|---|
| node01 | Master | NameNode、ResourceManager、ZK(Follower)、Hive Metastore |
| node02 | Worker | DataNode、NodeManager、ZK(Leader)、Kafka |
| node03 | Worker | DataNode、NodeManager、ZK(Follower)、Kafka、Flume |
3 台机器跑生产肯定不够,但做学习和毕设完全够用。我的建议是:优先保证 Master 节点内存,NameNode 和 ResourceManager 都是吃内存的主,一旦内存不够,整个集群看起来还活着,但任务提交后一直卡住不动,特别折磨人。
2.2 部署顺序与关键配置项
部署顺序建议:基础环境 -> ZooKeeper -> HDFS -> Yarn -> Hive -> Kafka/Flume -> 计算与展示层。千万别一上来就装 Hive,依赖关系没理清,报错会让你怀疑人生。
基础环境初始化方面,以下几个动作容易忽略但非常重要:
- 配置
/etc/hosts,把 3 台节点的主机名和 IP 都写上。别用 IP 直连的方式,后续配置文件里都写主机名,迁移环境就知道好处了。 - Master 到所有节点做 SSH 免密登录。不然后面启动服务、分发脚本的时候,每次都要输密码,效率极低。
- 同步时间,用 chrony 就行。我有一次忘了同步,结果 HDFS 租约和 Kafka 的 offset 都出了诡异的问题,时区不一致会让时间戳相关的逻辑全部错乱。
- 关闭防火墙,或者放行必要端口。我图省事直接关了,虚拟机环境无所谓,生产环境请走安全组。
- 调整文件描述符限制,
ulimit -n改成 65535。Flume 连接一多,就会报too many open files,排查起来还挺隐蔽。
HDFS 核心配置里,我单独说一下几个容易踩坑的选项:
dfs.replication:3 台节点,副本数设 2 就够。设 3 的话,一半磁盘空间都用来存冗余了,学习环境没必要。dfs.namenode.handler.count:默认 10,小集群不用改。改高了反而增加内存开销。dfs.datanode.data.dir:强烈建议把数据目录挂到单独的磁盘分区,别和系统盘混在一起。系统盘一旦写满,整个节点直接瘫痪。- Yarn 的资源分配:4G 内存的机器,我给 NodeManager 分配 3G
yarn.nodemanager.resource.memory-mb,剩下的留给操作系统和组件进程。yarn.scheduler.maximum-allocation-mb默认 8G 不用调,但要注意别超过 NodeManager 的总资源。
2.3 部署翻车现场:DataNode 反复挂掉
分享一个我印象最深的翻车经历。DataNode 进程启动后没多久就自动退出,日志里报“磁盘空间不足”。我检查了一下,明明还有 50G 可用空间,怎么就不足了?
排查到最后发现,问题出在dfs.datanode.du.reserved这个参数上。它默认值是 0,但 DataNode 在判断剩余空间时,用的不是我们直觉上的“分区剩余空间”,而是经过 Hadoop 自身计算后的数据目录容量。当时我的数据目录和系统根目录在同一个分区,系统日志和临时文件一直在写,HDFS 判断可用容量时把其他文件占用也算进去了,导致明明还有空间却启动失败。
解决方法是把数据目录单独格式化、单独挂载。这个教训让我养成了一个习惯:分区规划和挂载点,一定要在部署前想清楚,不能等出问题再补救。
还有一次是 3 台机器 Java 版本不统一,一台 JDK8、两台 JDK11。结果 ZooKeeper 能启动,HDFS 也没报错,但 Hive Metastore 怎么都连不上,最后才发现是版本兼容问题。统一换成 JDK8 后一切正常。不是 JDK11 不好,而是 Hadoop 生态对 JDK8 的兼容性最成熟,别在这个环节给自己找坑。
3. 数据链路与离线计算:把日志变成指标
3.1 模拟数据:没有真实业务怎么练
没有真实业务数据,就用脚本模拟。我写了一个 Python 脚本,按一定的概率分布随机生成用户行为日志,字段包括:user_id、session_id、page_url、action、item_id、timestamp、device_type、province。输出格式用 JSON,每行一条,模拟用户的点击、浏览、加购、下单等行为。
数据量我控制在 300 万条左右。这个量级在 3 台小集群上跑 Spark SQL,几分钟能出结果,既能体现分布式计算相对于单机脚本的优势,又不会等得让人崩溃。量太大纯属自虐,量太小又看不出效果。
日志生成后,通过 Flume 的 spooldir 或 taildir source 监听日志目录,把数据写入 Kafka。Flume 这边有一个容易被忽略的点:channel建议用memory channel还是file channel?我用的是 memory,吞吐高,但要注意配置好capacity和transactionCapacity。如果日志量突然暴涨,memory channel 满了会直接丢数据。如果是生产环境,建议用 Kafka Channel 或者 file channel,防丢数据优先。
3.2 Hive 表结构和存储格式
Kafka 里的消息被消费后写入 HDFS,下一步就是建 Hive 表。我建了两层:ODS 层和 ADS 层。ODS 层表和原始日志字段保持一致,按天分区,存储格式用 ORC,压缩用 Snappy。有人喜欢用 TextFile,调试方便,但生产上 ORC 的扫描效率确实高很多,尤其做范围过滤时能省不少 IO。
建表语句大致如下:
CREATE TABLE ods_user_behavior ( user_id STRING, session_id STRING, page_url STRING, action STRING, item_id STRING, device_type STRING, province STRING, ts BIGINT ) PARTITIONED BY (dt STRING) STORED AS ORC TBLPROPERTIES ('orc.compress'='SNAPPY');这里有一个非常关键的习惯:所有查询都带上分区条件。如果忘了按dt过滤,在小集群上跑一个不带分区的全表扫描,那真是地狱体验。之前有一次我写临时 SQL 时漏了分区条件,一个查询跑了快 20 分钟,日志里全是 Container 被反复 kill 的记录。从那以后,我写任何 SQL 都会先确认分区过滤是否存在。
3.3 Spark SQL 算指标:别只跑 count
计算部分我写了几个经典指标。核心目的是让人理解底层的 Spark 任务到底怎么执行的,而不只是会跑count(*)。
比如算 UV(独立访客数):
SELECT dt, count(DISTINCT user_id) AS uv FROM ods_user_behavior WHERE dt = '2024-11-10' GROUP BY dt;这条 SQL 看起来简单,但count(DISTINCT)在数据量大时性能很差,因为它需要全量去重。我后来改成了先用GROUP BY user_id去重,再在外层套count,或者对精度要求不高的场景直接上approx_count_distinct,计算速度快非常多,误差通常在 1% 以内,展示大屏完全够用。
再比如留存率的计算,我的做法是:先算当日活跃用户集合,再和次日活跃用户集合做 JOIN,求交集用户数。这一步最容易遇到数据倾斜,因为少数热点用户可能占了绝大多数记录。定位办法是,先按user_id分组看每个组的记录条数,找到那个异常大的 key,然后用“加盐”的方式,给热点 key 加随机前缀,打散到不同的 reduce 端,最后再合并结果。
-- 两阶段聚合处理数据倾斜:先加盐打散,再聚合 SELECT user_id, sum(cnt) AS total_cnt FROM ( SELECT concat(cast(rand() * 10 as int), '_', user_id) AS salted_user_id, count(1) AS cnt FROM ods_user_behavior WHERE dt = '2024-11-10' GROUP BY concat(cast(rand() * 10 as int), '_', user_id) ) t GROUP BY user_id;这段代码是典型的两阶段聚合示例。第一阶段加盐把数据打散,第二阶段去掉盐再聚合,热点问题就迎刃而解了。
4. 可视化大屏:把算好的数据“晒”出来
4.1 前端技术栈:为什么是 React + TypeScript + ECharts
数据算完了,最后一步是展示。前端技术栈我选了 React 18 + TypeScript + Vite + ECharts。选 React 和 TS 的理由很简单:组件化开发思路清晰,TS 的类型系统能提前暴露很多数据字段错误,写起来并不比纯 JS 麻烦多少。ECharts 是免费的,图表类型全,社区案例多,大屏里常见的地图、柱状图、折线图、词云全都支持。
有朋友问我为什么不用现成的数据可视化平台。我的回答是:如果只是内部看数据,用免费的 Superset、Grafana 也能搭。但如果这个项目是要写在简历里给面试官看的,自研 React 大屏更能体现开发能力,而且完全不受平台限制。免费数据可视化大屏确实省事,但灵活度和定制化程度永远比不上自己写。
4.2 大屏布局、适配与数据刷新
大屏尺寸我按 1920x1080 设计,整个容器固定设计稿尺寸,用transform: scale动态计算缩放比来适配不同屏幕。相比 rem 适配,这种方案实现简单,不用把所有尺寸都改成相对单位,缺点是缩放后文字可能轻微模糊,但大屏展示场景完全够用。
布局上,顶部是标题和当前时间,左侧放实时 PV/UV 折线图,中间放用户地域分布地图,右侧放商品热销榜,底部放最新订单流水滚动表格。数据通过 HTTP 接口定时拉取,每 30 秒刷新一次。前端只负责展示,所有聚合计算都在后端 Spark 任务里提前算好了。
接口返回的数据结构用 TS 定义:
interface KpiResponse { pv: number; uv: number; avgDuration: number; trend: Array<{ time: string; pv: number; uv: number }>; rank: Array<{ item: string; sales: number }>; }ECharts 配置里有一个容易踩的坑:更新series.data时,如果直接替换整个数组,ECharts 内部 diff 时会触发动画错乱,甚至闪烁。我后来统一改用setOption(option, { notMerge: true }),避免新旧数据层的错误合并。数据刷新频率也别太激进,30 秒一次足够了,太频繁反而给后端 MySQL 制造不必要的压力。
还有一个细节:大屏上的滚动数字、轮播表格,最好用 CSS 动画或轻量库实现,不要为了这点功能引入重型组件,不然首屏加载会明显变慢。
5. 常见问题与面试复盘:这几个月攒下的坑
5.1 运行期问题排查实录
我把实际遇到过并且花了不止一个小时才解决的问题整理了一下,列成表格方便查阅:
| 问题 | 现场表现 | 根因 | 解决方案 |
|---|---|---|---|
| Executor OOM | 任务反复重试后失败,日志里有Container killed | Executor 内存分配不足或数据倾斜 | 增加spark.executor.memory,打开spark.memory.offHeap.enabled,同时检查数据倾斜 |
| Shuffle 溢写严重 | 任务运行极慢,磁盘 IO 飙升 | 并行度过低,单 task 数据量过大 | 调大spark.sql.shuffle.partitions,增加并行度 |
| 小文件过多 | NameNode 内存压力大,查询变慢 | Hive 写入时每个 reducer 都生成一个小文件 | 开启hive.merge.mapredfiles=true,或写入时用distribute by dt控制文件数量 |
| 表连接数据倾斜 | 部分 task 运行几十分钟,其他 task 秒完 | join key 分布不均 | 广播小表,或对热点 key 加盐拆散 |
这里重点说一下 N+1 问题。这本来是数据库领域的概念,但大数据场景同样存在。比如用循环逐条查 Hive 分区、或者在服务端循环调用外部接口拉数,都属于 N+1。我一开始写指标汇总服务时,就是逐个分区查 MySQL,结果 30 个分区就是 30 次交互,一次页面加载要等好几秒。后来改成一次 SQL 批量读取全部分区,在内存里做关联,性能提升了近一个量级。
5.2 大数据面试题:把项目讲成加分项
大数据面试题翻来覆去就是那几类:HDFS 读写流程、MapReduce 和 Spark 的区别、数据倾斜、Hive 优化、Kafka 消息可靠性等。单纯背八股没用,面试官更想听到的是你把原理结合到项目里讲。
我自己总结的回答模板是:先说结论,再讲原理,最后落到项目里的具体数据。比如问到 HDFS 写流程,我会说“客户端先向 NameNode 请求上传,NameNode 返回可用 DataNode 列表,客户端分块写入并复制到其他节点,全部完成后通知 NameNode 提交”,然后接一句项目里的场景:我为了应对大量小日志文件,专门调过名字节点的副本策略,并且在小文件合并上做了优化。
数据倾斜是绝对绕不开的考点。回答时要体现排查思路:先看日志里某个 stage 的 task 耗时差异,再看 key 的分布规律,最后说处理手段,比如两阶段聚合加盐、广播小表、拆热点 key。我上面给的加盐 SQL 片段,面试时直接背出来,比空谈概念有说服力得多。
还有一个高频考点是 Kafka 怎么保证消息不丢失。我的理解分三端:生产端,用acks=all加重试;Broker 端,replication.factor>=3,min.insync.replicas>=2;消费端,手动提交 offset,处理完业务逻辑再提交,避免重复消费或数据丢失。这些点都要结合项目实际来说,哪怕只是小集群模拟,也能体现你对可靠性的思考。
6. 方向扩展与学习建议:别让笔记停在纸面上
6.1 从毕设到竞赛:同一个骨架还能怎么用
如果你已经把这套项目跑通了,完全可以把它扩展成毕业设计,甚至参加竞赛。我后来参加过一数学建模方向的大数据赛题,题目涉及遥感卫星大数据的清洗、轨道计算与动态覆盖分析。我当时第一反应是,这个思路和用户行为分析惊人地相似:都是先做数据清洗和标准化,再设计存储结构,最后用可视化把结果展示出来。唯一不同的是数据源从日志变成了 TLE 星历,指标从 PV/UV 变成了经纬度和星下点轨迹。
所以不要觉得一个项目做完就完了,它的骨架是可以迁移的。数据采集、存储、计算、展示这套链路,在绝大多数数据类项目里都适用。区别只在于领域知识不同、指标口径不同,处理流程本质上是相通的。
6.2 数据科学与大数据技术专业的学习路线
经常有人问我“数据科学与大数据技术这个专业到底学什么、就业方向有哪些”。这个问题其实取决于你把自己定位在哪一层。如果偏数据开发,重点就是 Hadoop、Spark、Flink、数据仓库、实时计算;如果偏数据分析,重点是 SQL、统计学、Python、可视化。两者都需要具备完整的数据链路意识,哪怕你只做其中一环,也得知道上下游是谁。
我的学习路线建议很直接:先打牢 SQL 和 Python,再学 Linux 和 Hadoop,接着啃 Spark 和 Flink,最后补数据治理和面试题。不要一上来就啃源码,先会用,再问为什么。用起来之后,源码里那些概念会自然地变得容易理解。
还有一点我想特别强调,学习资料的数量永远代替不了实践量。资料看一百遍,不如自己启动一个集群、跑通一条数据流、再把它搞挂一次,然后从日志里把它捞回来。踩坑才是最快的成长方式。
最后聊一点个人的体会。大数据学习最痛苦的不是知识点本身难,而是知识点之间太散,串不起来。我自己也是从这个问题里走出来的,所以笔记第二篇特意用一条主线把部署、计算、展示串起来写。如果看完你能搭出一个小集群,跑通一条从日志到指标的链路,这篇笔记就没白写。下一篇我大概率会写实时计算和流批一体方向的内容,到时候咱们再接着聊。