简介:一套面向计算机专业毕业设计的基于Spark的地铁大数据客流分析系统项目资料,依托Apache Spark实现客流统计、预测与优化,覆盖数据采集、清洗、存储、分析和可视化全链路,适用于需要完成大数据类课题的学生及Spark入门开发者。压缩包共194个文件,约42.6MB,包含Java和Scala源码、Spark作业与配置脚本、SQL数据库脚本、XML/YAML/PROPERTIES等配置文件、CSV测试数据、Markdown技术文档、PPT报告以及大量PNG界面截图,目录结构清晰,从需求分析、系统设计、编码实现到测试部署均有对应文件。通过这套资料,可完整了解毕业设计流程,掌握Spark SQL、Spark Streaming、MLlib等核心组件的实际用法,同时获得可运行的代码框架与数据库设计思路,便于二次开发和论文撰写。目前已有259人学习,对准备毕业设计或地铁客流分析项目有较高的参考价值。
1. 为什么用 Spark 做地铁客流分析:一份 CSV 到可答辩的大数据毕设系统
地铁客流分析是计算机类毕业设计里最高频的选题之一,但高频意味着容易做泛:很多人拿到一份 CSV 就用 Pandas 画两张折线图交差,评委一问「数据量翻十倍你怎么处理」就卡住。这套方案走的是另一条路线——Spark 做分布式计算、HBase 做分布式存储、Logstash 做数据接入,把整个链路做成能真正跑起来的系统。压缩包里的 hbase.command、logstash-nginx.config、客流 CSV 样本、http 接口测试脚本,对应的是一条完整的「采集→存储→计算→输出」链。方案适合正在做大数据方向毕设或课设的学生,也适合刚接触 Spark 开发的从业者。要解决的核心问题有三个:客流数据怎么采集清洗、怎么用 Spark 做分布式统计分析、统计结果怎么通过 HTTP 接口输出。
2. 总体架构设计:采集、存储、计算、服务四层与五个核心模块的职责划分
2.1 技术选型:为什么是 Spark + HBase + Logstash 而不是 MySQL + Pandas
地铁客流分析系统的技术选型,首先要回答三个问题:数据存在哪里、谁来算、结果怎么对外提供。
先看存储。最常见的错误想法是「客流数据也就几十万条,MySQL 足够了」。这个思路在毕设演示阶段没有错,但如果需求文档里写了「系统需要支持海量客流数据的存储与高效查询」,MySQL 就撑不住两个场景:一是高并发写入,闸机数据在早晚高峰是持续写入的,MySQL 单机写入能力有限;二是按时间范围扫描,比如查某条线路一周的客流,MySQL 需要全表扫描或依赖索引,而 HBase 的 RowKey 范围扫描天生适合这种查询。HBase 的劣势是需要额外维护 HDFS 和 ZooKeeper 集群,但对毕设来说,这恰恰是加分项——架构复杂度本身就是展示学习成果的素材。
再看计算引擎。MapReduce 虽然也能做分布式计算,但每个任务都要经过 Map 和 Reduce 两个阶段,中间结果反复落盘,迭代式算法跑起来很慢。Spark 的优势在于基于内存的 DAG 计算引擎,同样的聚合作业速度快一个数量级,而且 Spark SQL 的编程体验接近传统 SQL,学习成本低。用一句话概括选型逻辑:MapReduce 是能用,Spark 是好用,Pandas 是只能在单机数据规模下用。
最后看数据接入。Logstash 在这个方案里承担的是「日志/文件→结构化记录」的转换,它内置了 file、http、jdbc 等多种输入插件,输出端可以直接对接 HBase 或 Kafka。相比自己写 Java 采集程序,Logstash 配置化的方式更适合毕设——配置文件的每一行都可以在文档里解释清楚,答辩时讲起来有据可查。
技术选型的对比可以用一张表快速说清:
| 对比维度 | MySQL + Pandas | Hadoop MapReduce + MySQL | Spark + HBase + Logstash |
|---|---|---|---|
| 数据规模 | 十万级 | 亿级 | 亿级 |
| 开发效率 | 高 | 低,需写大量 MR 代码 | 高,Spark SQL 接近 SQL |
| 实时性 | 弱 | 无 | 准实时(Spark Streaming) |
| 架构亮点 | 无 | 有分布式但老旧 | 完整大数据技术栈 |
| 答辩风险 | 被问「大数据在哪」 | 被问「为什么不用 Spark」 | 需要准备集群环境 |
这个方案在答辩时默认的提问路径就是沿着这张表展开的:为什么 HBase 不用 MySQL、为什么 Spark 不用 MapReduce、为什么加一层 Logstash 而不是直接读 CSV。把选型的理由讲透,比堆技术名词更有说服力。
2.2 四层架构与五个核心模块的职责边界
系统按数据流动方向划分为四层,每一层只依赖下一层提供的抽象接口,不跨层调用。
采集层是数据管道的入口,负责把三类数据源接入系统:历史客流 CSV 文件、nginx 网关日志、第三方 HTTP 接口返回的 JSON 数据。这一层在方案里对应 logstash-nginx.config 配置文件和 CSV 清洗脚本。存储层基于 HBase,管理原始客流表(metro_flow_raw)、小时聚合表(metro_flow_hour)和站点基础信息表(metro_station)三张核心表,同时依赖 HDFS 作为底层文件系统。计算层由 Spark 集群承载,运行离线聚合作业和准实时消费作业。服务层对外暴露 REST API,供前端展示或接口调试工具调用。
五个核心模块的职责划分如下:数据接入模块负责格式转换与初步校验,任何无法解析的记录要写入异常日志而不应该悄悄丢弃;数据存储模块负责建表、预分区与 RowKey 设计,这部分在 hbase.command 文件里可以看到完整的命令序列;离线分析模块实现按站点、线路、时段的聚合统计;准实时分析模块消费接口增量数据,更新最近一小时的热点站点排名;结果服务模块把聚合表的查询结果封装成统一 JSON 格式返回。模块之间通过 HBase 表或文件解耦,任何一个模块都可以独立替换——比如把 Logstash 换成 Flume,只要采集层输出的数据格式不变,下游完全无感。这个设计在答辩时可以作为「可维护性」的论据。
2.3 HBase 表结构设计与 RowKey 散列策略
HBase 表设计是这套方案的数据库部分核心内容,也是评委最容易追问的技术点。hbase.command 文件里给出了建表命令,设计上有三个关键决策。
第一个决策是原始表与聚合表分离。原始表 metro_flow_raw 保留每条闸机记录的明细数据,不做任何预聚合,用于回溯分析和数据校正;聚合表 metro_flow_hour 存储按小时聚合的结果,查询时直接读取而不需要实时计算。这种「明细存全量、查询走聚合」的模式,避免了每次查询都触发 Spark 作业的问题,是典型的空间换时间思路。
第二个决策是 RowKey 格式。原始表采用「反转站点ID + 时间戳」的组合:比如站点 ID 为 ST001,反转后是 100TS,再拼上小时时间戳形成 100TS_2025011514。反转的目的是避免热门站点成为读写热点——如果直接以站点ID开头,同一站点的数据会集中到同一个 Region,造成单节点过热;反转后数据分布更均匀,预分区的效果才能发挥出来。
# 创建客流原始数据表,预分区 6 个 region,减少写入热点 create 'metro_flow_raw', {NAME => 'cf', VERSIONS => 1}, {NUMREGIONS => 6, SPLITALGO => 'UniformSplit'} # 创建按小时聚合表,列族 c 存储进站/出站/换乘三个指标 create 'metro_flow_hour', {NAME => 'c', VERSIONS => 1} # 创建站点维度表,用于站点信息与线路关系维护 create 'metro_station', {NAME => 'info', VERSIONS => 1}第三个决策是预分区数量。建表时通过 NUMREGIONS 参数把表预分为 6 个 Region,避免数据量增长后频繁触发 Region 分裂。预分区数量需要根据集群规模和写入速率估算,可以参考「预期数据总量 / 每个 Region 建议 10GB 左右」的经验值,数据量不大时 6 个分区足够。
注意:HBase 的 RowKey 一旦确定,后期修改成本极高。建议在开发初期就用真实数据规模测试分布效果,不要等到答辩前发现热点问题再改——改 RowKey 意味着历史数据要重新导入。
2.4 数据字典与字段口径定义
数据的字段口径是整个系统的「黑匣子」所在,很多毕设翻车都是因为不同模块对「客流」二字的理解不一致。这个方案里需要明确定义三类指标:进站量(in_count)指某时段内通过闸机进站的乘客数;出站量(out_count)是出站闸机记录的乘客数;换乘量(transfer_count)指在同一站点从一条线路换到另一条线路的乘客数,这部分数据通常是估算值,来源是 AFC 系统的换乘标记。
三个指标的统计口径必须统一到小时粒度。也就是说,无论 CSV 原始数据里记录的是 15 分钟粒度还是单次闸机通行记录,统一在清洗阶段转换为小时总量。这样 Spark 聚合、HBase 存储和 API 输出三层的数据口径才一致,否则会出现「API 显示 8 点进站 5000 人,原始数据里对不上」的尴尬场景。
| 字段名 | 类型 | 说明 | 来源 |
|---|---|---|---|
| station_id | STRING | 站点编号,清洗后统一大写 | CSV / 接口 |
| line_id | STRING | 线路编号 | CSV / 接口 |
| event_time | TIMESTAMP | 业务发生时间,统一 yyyy-MM-dd HH:mm:ss | CSV / 接口 |
| in_count | INT | 小时进站量 | CSV 聚合 |
| out_count | INT | 小时出站量 | CSV 聚合 |
| transfer_count | INT | 小时换乘量,可为空 | CSV 聚合 |
| is_filled | BOOLEAN | 该记录是否存在缺失值填充 | 清洗程序生成 |
字段口径表建议写进设计文档的第三章,答辩时评委如果追问「换乘量是怎么来的」,可以直接翻到这一页解释。这个细节虽然小,但能体现你对数据治理的理解——很多学生答辩时被问倒,不是因为不会写代码,而是说不清数据是怎么被加工成他贴出来的图表的。
3. 数据管道实现:Logstash 接入、HBase 存储与 CSV 数据预处理
3.1 Logstash nginx 配置解析与接入参数说明
数据接入是整个链路里最容易翻车的一环,也是很多学生第一次接触 Logstash 时觉得「玄学」的地方——配置文件看着简单,跑起来却各种数据丢失或重复。
方案里的 logstash-nginx.config 解决的是「nginx 网关日志→结构化 JSON 记录」的转换。典型的地铁业务场景是:闸机数据先写入业务系统的数据库,业务系统通过 nginx 提供查询接口,外部系统调用接口获取客流数据。每次接口访问都会在 nginx 的 access 日志里留一条记录,Logstash 监听这个日志文件,把日志里的字段解析出来,再写入 HBase。
一个可用的 Logstash 配置如下:
input { file { path => "/var/log/nginx/metro_access.log" start_position => "beginning" sincedb_path => "/dev/null" codec => json } } filter { json { source => "message" } mutate { remove_field => ["message", "@version", "path"] } date { match => ["event_time", "yyyy-MM-dd HH:mm:ss"] target => "@timestamp" } } output { hbase { table => "metro_flow_raw" cf => "cf" rowkey => "%{station_id}_%{@timestamp}" } }这段配置里有几个关键参数需要说明。path 指定日志文件位置,测试环境通常把日志放在本地目录,生产环境则可能是远程文件系统或 Kafka。sincedb_path 是 Logstash 记录文件读取位置的地方,测试时设为 /dev/null 可以让每次启动都从文件头重新读,但生产环境必须改成持久化路径,否则重启后重复消费整份日志。codec 设为 json 表示日志本身就是 JSON 格式,如果 nginx 日志是普通文本格式,需要改用 grok 插件做正则解析。output 里的 rowkey 直接拼接 station_id 和 @timestamp,实际项目里建议加上反转逻辑,与 HBase 建表时的 RowKey 方案保持一致。
3.2 CSV 客流数据的清洗、去重与标准化实现
客流 CSV 样本是整条数据链路的起点,通常包含字段:日期、线路ID、站点ID、进站人数、出站人数、换乘人数、天气状况。这份数据规模不大,但脏数据问题一个不少:日期格式不统一、站点ID存在前后空格、部分记录出现负数或异常大值、换乘字段大量缺失。
清洗逻辑按三步实现。第一步是字段标准化,把日期统一成 yyyy-MM-dd HH:mm:ss,站点ID去除空格并统一大写。第二步是异常值处理,in_count 或 out_count 小于 0 的丢弃并写异常日志,大于阈值(比如单小时超过 5 万)的做标记,不直接改成均值——因为异常值本身有可能是真实数据,比如节假日某热门站点确实会爆量。第三步是缺失值填充与去重:换乘量缺失用同线路同时段的均值填充并标记 is_filled;去重则用「日期+站点ID+时段」作为唯一键,保留第一条记录。
用 Spark 做清洗的代码片段:
import org.apache.spark.sql.functions._ val raw = spark.read.option("header", "true").csv("hdfs:///data/metro/metro.csv") val cleaned = raw .withColumn("station_id", upper(trim(col("station_id")))) .withColumn("event_time", to_timestamp(col("date"), "yyyy-MM-dd HH:mm:ss")) .dropDuplicates("event_time", "station_id", "line_id") // 按业务主键去重 .filter(col("in_count") >= 0 && col("out_count") >= 0) .withColumn("is_filled", lit(false)) cleaned.write.mode(SaveMode.Overwrite).parquet("hdfs:///data/metro/cleaned")这段代码把清洗结果写成 Parquet 格式落盘,原因是 Parquet 列式存储比 CSV 更适合 Spark 后续的聚合读取,IO 开销小很多。清洗结果不要直接覆盖原始 CSV,保留原始文件是基本的工程素养——脏数据丢了想再复现问题就没有底了。
提示:清洗阶段最容易犯的错是「去重键设计不合理」。如果直接用全部字段做去重,同一站点同一时段的两条合法记录会被误删;用「时间+站点+线路」做业务主键,才能保证删掉的是真正重复的那条。
3.3 HTTP 接口增量数据接入与实时数据源补充
除了历史 CSV,系统还通过 HTTP 接口获取准实时客流数据。方案里的接口测试脚本(search.http、szt-api.http 对应的 REST Client 文件)可以在 IDE 里直接发送请求,响应结果就是 JSON 格式的客流数据。这种「接口即数据源」的方式有两个好处:一是数据获取逻辑清晰,答辩时可以直接演示;二是不需要模拟 Kafka 或其他消息中间件,降低环境搭建成本。
增量接入通常的做法是写一个定时任务,每 5 分钟调用一次接口,把返回的 JSON 解析为 DataFrame,追加写入 HBase。追加写入要走 HBase 的 put 操作而不是覆盖整表,保证历史数据不被破坏。接口调用有两个坑:一是限流,连续高频请求会被网关拒绝,需要在代码里控制调用间隔并加上重试逻辑,重试时用指数退避,第一次等 1 秒、第二次等 2 秒、最多重试 3 次;二是接口返回的时间字段可能与本地时区不一致,解析时显式指定时区,避免差 8 小时的经典翻车。
增量数据与历史数据的融合方式:历史 CSV 清洗后写入原始表作为基线数据,增量接口数据按时间戳追加。两份数据的字段结构相同,在 Spark 聚合时不需要区分来源。如果接口偶尔返回异常数据(比如某时段进站量为负),清洗阶段同样的异常过滤逻辑会兜住,不会污染聚合结果。
3.4 数据管道完整运行流程与验证方法
数据管道从文件到 HBase 的完整流程可以概括为七步:复制 CSV 到 HDFS → 启动 Spark 清洗作业 → 校验清洗结果行数和字段值 → 执行 hbase.command 建表 → 将清洗结果写入原始表 → 运行 Logstash 监听 nginx 日志 → 用 http 脚本触发接口返回并确认写入成功。
每一步都建议有对应的验证命令。比如第 3 步校验清洗结果,可以用 Spark Shell 执行 count() 对比清洗前后行数,看丢弃比例是否在合理范围(通常 1%-5% 之间,如果超过 10% 说明原始数据质量问题很大);第 7 步验证 Logstash 写入,可以用 HBase Shell 的 scan 'metro_flow_raw', {LIMIT => 10} 查看最近写入的记录,重点是确认 RowKey 格式和时间字段符合预期。
这套验证方法的价值在于,它能帮你快速定位链路中的故障模块。比如 HBase 里查不到数据,先看是 Logstash 没消费日志、还是消费了但没写入、还是写了但 RowKey 格式不对——按流程一步步排查,比对着日志瞎猜效率高得多。
4. Spark 核心分析实现:客流聚合、高峰时段识别与趋势预测
4.1 Spark SQL 实现按站点、线路、时段的客流聚合
Spark 是系统的计算核心,承担离线聚合、高峰识别和趋势预测三类任务。离线聚合是最基础的功能,用 Spark SQL 实现可读性最好,也最容易在答辩时演示。
val spark = SparkSession.builder() .appName("MetroFlowAgg") .config("spark.sql.shuffle.partitions", "20") // 控制 shuffle 分区数 .getOrCreate() val df = spark.read.parquet("hdfs:///data/metro/cleaned") // 从 event_time 提取小时字段,用于小时粒度聚合 val hourStat = df .withColumn("hour", date_format(col("event_time"), "yyyy-MM-dd HH:00")) .groupBy("station_id", "line_id", "hour") .agg( sum("in_count").as("total_in"), sum("out_count").as("total_out"), sum("transfer_count").as("total_transfer"), count("*").as("record_count") // 记录条数,用于数据质量校验 ) .orderBy("hour", "total_in") hourStat.coalesce(1).write.mode(SaveMode.Overwrite) .option("header", "true") .csv("hdfs:///data/metro/result/hour_stat")这段代码有三个参数值得说明。config("spark.sql.shuffle.partitions", "20") 控制聚合时的 shuffle 分区数,默认 200 对中小数据量来说过大,会拖慢作业执行;coalesce(1) 把结果合并到一个文件输出,方便直接查看,但生产环境不要这么做,会破坏并行性;record_count 字段是故意加的,聚合结果里保留记录条数,后续校验数据量时能对比「聚合后的行数乘以每小时」与原始行数是否吻合。
聚合结果同时写入 HBase 的 metro_flow_hour 表,供 API 层直接查询。写入时用 Overwrite 模式,因为聚合表可以从原始数据全量重算,属于可再生的中间结果。如果你在答辩时被问「为什么聚合表可以覆盖」,答案就是:原始表在,任何粒度的聚合都能随时重跑——这就是保留明细数据的意义。
4.2 通勤型与商圈型站点识别:基于高峰占比的分类算法
高峰时段识别是这套方案的加分功能,也是答辩时的亮点模块。思路如下:取出每个站点工作日按小时聚合的进站序列,计算早高峰(7:00-9:00)和晚高峰(17:00-19:00)的进站量占全天进站量的比例。比例超过 40% 的站点判定为通勤型站点,主要服务上班族;如果峰值出现在周末下午或午间,则判定为商圈型站点,主要服务购物休闲客流。
import org.apache.spark.sql.expressions.Window val stationDaily = hourStat .groupBy("station_id", "line_id", "date") .agg(sum("total_in").as("day_in")) val peakHour = hourStat .withColumn("is_peak", when(hour(col("hour")).between(7, 9).or(hour(col("hour")).between(17, 19)), 1).otherwise(0)) .groupBy("station_id", "date") .agg(sum(when(col("is_peak") === 1, col("total_in")).otherwise(0)).as("peak_in")) val stationType = stationDaily.join(peakHour, Seq("station_id", "date"), "inner") .withColumn("peak_ratio", col("peak_in") / col("day_in")) .withColumn("station_type", when(col("peak_ratio") >= 0.4, "commute").otherwise("commerce")) .select("station_id", "date", "peak_ratio", "station_type")分类结果存回 HBase 的站点信息表,API 输出站点客流统计时附带 station_type 字段。计算逻辑不复杂,但它体现了对业务场景的理解——同一个站点在工作日和周末呈现完全不同的客流画像,这是「数据分析」区别于「数据统计」的地方。答辩时主动讲出这层业务含义,比单纯报数字更有感染力。
4.3 基于时间序列的客流趋势预测与周期性处理
客流预测是毕设里最能展示机器学习能力的功能。直接对全量数据做线性回归的效果通常很差,因为客流数据的周期性极强——工作日的峰值出现在早晚通勤,周末的峰值出现在午后,节假日又是完全不同的形态。如果把所有天混在一起建模,模型会「混淆」这些模式,预测结果自然不准。
实践中的做法是把序列按「工作日 / 周末 / 节假日」三个类别拆开,分别训练模型。特征工程上,使用日期特征(月份、星期几、是否节假日)、前一日同时段客流、上周同一天同时段客流,这三个特征组合对客流预测的贡献最大。用 Spark MLlib 的线性回归或决策树都能得到可接受的结果,重点是训练集的时间切分——用前 70% 的时间段做训练,后 30% 做验证,而不是随机切分。
import org.apache.spark.ml.feature.VectorAssembler import org.apache.spark.ml.regression.LinearRegression val featureDF = df .withColumn("month", month(col("event_time"))) .withColumn("day_of_week", dayofweek(col("event_time"))) .withColumn("is_holiday", lit(0)) // 节假日标记,由外部数据源补充 .withColumn("prev_day_same_hour", lag("total_in", 24).over(Window.orderBy("event_time"))) .withColumn("prev_week_same_hour", lag("total_in", 168).over(Window.orderBy("event_time"))) val assembler = new VectorAssembler() .setInputCols(Array("month", "day_of_week", "is_holiday", "prev_day_same_hour", "prev_week_same_hour")) .setOutputCol("features") val lr = new LinearRegression() .setLabelCol("total_in") .setFeaturesCol("features") .setMaxIter(100)这里 lag 函数里 24 和 168 的含义分别是:按小时序列偏移 24 行代表前一日同时段,偏移 168 行代表前一周同一天同时段。窗口按 event_time 排序,所以这两个特征能捕捉日周期和周周期。评估指标用 RMSE 和 MAPE 两个。MAPE 控制在 15% 以内可以认为预测可接受,超过 20% 就要检查是否漏了重要特征或切分方式有问题。答辩时如果被问「预测在节假日表现如何」,如实说「节假日样本少,预测误差偏大」是加分回答——说明你测试过、知道模型的边界在哪里,而不是把训练集的指标拿出来硬说没有缺陷。
4.4 API 服务层实现与接口返回格式
服务层负责把 HBase 里的聚合结果通过 REST API 输出。接口测试脚本 search.http 和 szt-api.http 提供了三种常用查询模式:按站点查某天的客流统计、按线路查全天时段分布、查全站高峰时段排名。接口返回统一 JSON 格式,包含 code、msg、data 三层结构,data 里的字段与数据字典保持一致。
@GetMapping("/api/flow/station") public Result getStationFlow(@RequestParam String stationId, @RequestParam String date) { // 直接从 HBase 聚合表查询,避免实时计算 FlowStat stat = hbaseService.queryHourStat(stationId, date); if (stat == null) { return Result.error(404, "no data for station: " + stationId); } return Result.success(stat); }{ "code": 200, "msg": "success", "data": { "station": "ST001", "date": "2025-01-15", "total_in": 45231, "peak_hour": "08:00", "station_type": "commute" } }这个接口的关键设计是「查询不触发计算」。所有聚合结果在 Spark 作业里预先算好写入 HBase,API 层只做读操作,所以响应时间稳定在毫秒级。如果接口每次都实时跑 Spark 作业,且不说性能,连 SparkSession 的生命周期管理都是麻烦事。答辩时演示完接口秒回,可以顺势讲一句「这是预聚合架构带来的效果」,评委基本都会点头。
接口层的第二个细节是参数校验。stationId 为空、date 格式错误、站点不存在,都要返回明确的错误码而不是 500 异常。错误处理写得好不好,是评委判断代码质量的重要指标——很多毕设代码能跑通,但一输入非法参数就崩,这才是真正的扣分项。
5. 常见问题排查:集群环境、数据倾斜与 HBase 读写的避坑指南
这一章里列的六个问题,几乎都是我在实际调试这类 Spark + HBase + Logstash 项目时踩过的坑。每个问题按「现象→原因→解决」来写,希望你调试时能直接对号入座。
5.1 本地跑 Spark 频繁 OOM
现象:在 IDE 里直接运行 Spark 作业,处理稍大一点的数据就报 java.lang.OutOfMemoryError: Java heap space,程序卡死。
原因:本地模式的 executor 默认内存只有 1G,而代码里如果有 collect() 或对大数据集做 toLocalIterator,数据会全部拉到 Driver 端,内存瞬间被撑爆。很多时候不是集群的问题,而是「本地模式当集群模式用」导致的。
解决:在 SparkSession 初始化时显式设置内存参数,比如 config("spark.driver.memory", "2g")、config("spark.executor.memory", "2g");避免 collect(),需要预览结果用 take(100) 或写入文件后用 shell 查看。如果数据量确实大,可以先用 filter 缩小数据集再聚合,而不是把全量数据都加载进内存。这类问题在答辩演示时最容易发生,建议提前把作业打包成 jar 在集群上提交,而不是现场在 IDE 里跑。
5.2 HBase 查询热点,热门站点响应慢
现象:大部分站点查询几十毫秒返回,某个换乘大站的请求经常超过一秒,甚至超时。
原因:RowKey 以站点ID开头,热门站点的数据集中在同一个 Region,导致单节点的 CPU 和 IO 过载。这是 RowKey 没做散列导致的典型问题。
解决:RowKey 改为「反转站点ID + 时间戳」,让同一站点的数据分散到不同 Region;同时检查建表是否预分区,没有预分区的表在数据量上来后会频繁分裂,也会造成性能抖动。另外可以用 HBase 的 Region 信息工具查看每个 Region 的数据分布,如果某个 Region 的 StoreFile 大小明显高于其他 Region,基本可以确认热点问题。
5.3 Spark 读 HBase 报列映射错误
现象:作业提交后抛 NoSuchColumnException 或 NullPointerException,堆栈指向 hbase.columns.mapping。
原因:配置里的列族和列名与建表命令不一致。比如建表时列族是 cf,config 里写成 info;或者 RowKey 拼接格式与代码解析格式不匹配。这类问题通常出现在「你改过 HBase 表结构但忘了同步 Spark 配置」时,属于典型的配置漂移问题。
解决:建一张「表名-列族-列名-RowKey 格式」的对照表,每次改动 HBase 表结构,同步更新三处配置。调试时用 HBase Shell 的 desc 'metro_flow_raw' 确认实际列名,再回看 Spark 代码里的 mapping。经验之谈:Spark 和 HBase 版本不匹配也会报类似错误,建议按 hbase.command 文件里标注的版本组合来部署,不要自己随意升级。
5.4 Logstash 重复消费数据翻倍
现象:重启 Logstash 后,HBase 里数据量翻了一倍,统计结果明显偏高。
原因:测试时把 sincedb_path 设为 /dev/null,Logstash 认为文件没读过,从第一行重新消费全部日志。
解决:生产环境把 sincedb_path 指向持久化文件并保持不变;测试需要重放数据时,在重放前后记录 HBase 的数据量,重放完成后删除对应分区的增量数据。更稳妥的方案是在原始表加一个 batch_id 字段,每次接入用不同的 batch_id,统计时按最新 batch 过滤,这个设计同时解决了数据溯源的问题。
5.5 Spark 作业执行缓慢,聚合耗时数小时
现象:数据量不大(几千万条),但聚合作业跑了很久,Spark UI 显示大量时间落在 shuffle 阶段。
原因:shuffle 分区数设置不当。默认 spark.sql.shuffle.partitions=200,对中小数据量会生成大量小任务,每个任务的调度开销比计算本身还大;另一个原因是数据倾斜——某个热门站点的数据量是其他站点的几十倍,导致某个分区任务拖慢整个作业。
解决:根据数据量调整分区数,经验公式是「分区数约等于数据量除以 128MB」,数据量几个 GB 时设 20-50 就够了;数据倾斜可以用 salting 技巧,给热门站点的 Key 加随机前缀,聚合后再去掉前缀合并。在 Spark UI 的 Stage 页面查看每个 Task 的耗时分布,能快速定位是哪个分区拖了后腿。
5.6 HBase 数据目录误删导致整表丢失
现象:某天启动 HBase 后,scan 表提示 TableNotFoundException,打开 HDFS 发现数据目录空了。
原因:ZooKeeper 或 HDFS 的持久化目录被误删,常见于清理临时文件时手滑。这个问题的根源不是技术难度,而是「没有备份意识」。
解决:演示环境强制走「本地 CSV + HBase 导出快照」双保险。HBase 有快照功能可以将表快照导出到 HDFS 的另一份目录,恢复时直接 restore。这个教训值得写进项目文档的运维章节:数据备份不是可选项,是系统设计的一部分。
6. 验收与答辩:从功能测试到指标口径的六个准备动作
毕设验收和普通项目验收最大的区别在于:评委更关注「你做了什么、为什么这么做、怎么证明它是对的」,而不是功能的完整程度。基于这个判断,我把验收准备整理成六个动作,答辩前按顺序过一遍。
第一步,准备一份可复现的演示脚本。从 CSV 清洗、HBase 建表、Spark 作业提交到 API 查询,每一步都有对应的命令和预期输出。演示时不要现场敲命令,提前把结果截图或录屏存档。第二步,准备三张核心截图:HBase 里能查到聚合数据、Spark UI 上能看到作业执行成功、API 接口返回正确 JSON。这三张图能证明系统是真实可运行的,而不是纸面项目。
第三步,准备数量级和数据口径的说明。要能立刻说出系统处理了多少条数据、每条数据包含哪些字段、聚合精度是小时还是天、换乘量是实测还是估算。数据口径是答辩提问的重灾区,因为设计文档里最容易含糊其辞。第四步,准备两个备选问题答案。「为什么不用 Redis 缓存」「数据量翻十倍系统哪里先扛不住」这类问题考察架构理解深度,提前想好回答,能避免现场卡壳。
第五步,做一组对照实验证明 Spark 的价值。拿同一份数据分别用 Pandas 单机和 Spark 分布式跑聚合,记录耗时对比。哪怕数据量只有几十万条,Spark 在 shuffle 处理和容错性上的特性也足以讲出一段有说服力的对比。第六步,把 hbase.command、logstash-nginx.config、接口测试脚本整理成一份「环境准备清单」,放进文档附录。答辩时如果评委想看细节,直接翻到对应章节,这比临时找文件体面得多。
带过一届学生的血泪教训是:答辩前三天发现 HBase 表数据全部丢失,原因是清理临时目录时误删了持久化数据。从那以后我每次准备演示环境都强制走一遍「HBase 快照导出 + CSV 本地备份」的双保险流程,答辩前一晚还会完整跑一遍演示脚本,确认每个命令在干净环境里也能复现。这六个动作看起来琐碎,但能帮你避开答辩现场的大部分突发状况,希望帮到你。
本文还有配套的精品资源,点击获取