
简介面向大数据相关专业学生与毕业设计者这份基于Hadoop的电信用户行为分析项目文档完整展示了从数据采集到可视化展示的流程。资源包共1个doc文件大小约6.38MB。文档先进行必要性与可行性分析再介绍Hadoop、Spark、MySQL、Spring MVC等技术选型并给出可视化界面、数据库及网页的总体设计。实验实现部分按步骤说明了Linux、JDK、Scala、Hadoop、Spark、MySQL、Tomcat等环境搭建以及将用户行为数据上传HDFS、用Spark分析并写入MySQL、最终通过Spring MVC进行Web可视化与Tomcat部署的完整过程。读者可据此复现整套电信用户行为分析实验也可直接参考其中的数据库表结构、功能模块划分和部署思路。已有304人学习适合需要完整案例支撑的大数据入门与课程设计场景。1. 基于 Hadoop 的电信用户行为分析最先要算清的是一张 7 亿行的表假如你手上是某个省份一个月的通话详单一天 7000 万行Excel 打开直接失去响应按用户汇总一次要等好几分钟——基于 Hadoop 的电信用户行为分析就是从这种场景里长出来的。这个标题在课程设计、毕业设计和数仓岗位面试里都很常见它的核心不是算法而是把 CDR 通话详单在 HDFS 上按天组织好再用 Hive 把活跃度、忙时、流量 TOP、离网倾向这些指标批量算出来。整件事的实现路径是固定的先定字段口径和分区规则再搭一套能跑的 Hadoop 环境把原始文件入到 ODS 层清洗后落到分析层最后用 Hive SQL 出指标。下面按建表口径 → 伪分布式环境 → 指标 SQL → 倾斜与验证四段展开。新手能照步骤搭起一套可复现的流程老手也能在内存参数、动态分区和数据倾斜这几个点上对照自己的踩坑经验。2. 建表口径先立住CDR 字段字典与 HDFS 目录设计电信用户行为分析的输入是每一通电话、每一条上网会话、每一条短信在核心网网关上打点生成的话单也就是 CDRCall Detail Record。分析用户行为而不是做一次性报表意味着后续会有多个指标从同一批数据取值通话次数、通话时长、流量、漫游、忙时分布、用户分群甚至离网预警。所以第一步不是写 SQL而是把字段语义和存储目录定死否则后面每个指标都要回源头重新对字段改一次表结构就要重刷一批分区。2.1 CDR 字段字典哪些字段会被后续分析反复引用我一般会按用户标识、时间、事件、用量、位置五个大类来梳理。下面是课程设计里可复现的最小字段集文件用制表符分隔字段Hive 类型说明在哪个指标里被用到msisdnSTRING手机号脱敏后所有用户级聚合的分组键imsiSTRINGSIM 卡标识换机行为识别call_timeSTRING事件时间 yyyy-MM-dd HH:mm:ss忙时分析、时间衰减call_durationINT通话时长秒MOU、费用估算call_typeSTRINGO 主叫 / T 被叫主被叫比例called_numberSTRING对端号码亲情号挖掘可选lac_ciSTRING位置区 小区编码常驻位置、通勤分析data_usageBIGINT该会话流量字节DOU、流量 TOP 榜sms_cntINT短信条数行为活跃度roam_flagTINYINT0 本地 / 1 漫游漫游用户分群plan_codeSTRING套餐编码连套餐表估算 ARPU这个字典里有两个细节值得注意。第一call_time 用 STRING 而不是 TIMESTAMP因为字符串截取在亿级数据上比类型转换省 CPU具体写法见第 4 章。第二真实话单里一定会有脏 msisdn——空串、非 1 开头、测试号段等。在字段注释里先标清原始号段未清洗比在每条 SQL 里到处写 RLIKE 过滤条件好维护得多。对应 ODS 层建表语句如下这是一张按天分区的外部表CREATE EXTERNAL TABLE IF NOT EXISTS ods_tel_cdr ( msisdn STRING COMMENT 手机号脱敏, imsi STRING COMMENT SIM卡标识, call_time STRING COMMENT 事件时间 yyyy-MM-dd HH:mm:ss, call_duration INT COMMENT 通话时长秒, call_type STRING COMMENT O主叫 T被叫, called_number STRING COMMENT 对端号码, lac_ci STRING COMMENT 位置区小区, data_usage BIGINT COMMENT 流量字节, sms_cnt INT COMMENT 短信条数, roam_flag TINYINT COMMENT 0本地 1漫游, plan_code STRING COMMENT 套餐编码 ) PARTITIONED BY (dt STRING COMMENT 天分区 yyyyMMdd) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t STORED AS TEXTFILE LOCATION /data/hive/warehouse/tel/ods_tel_cdr;分区列 dt 的格式用 yyyyMMdd 而不是带横杠的日期这样分区目录名可以直接当作字符串比较BETWEEN 20240101 AND 20240107 这类写法不需要任何转换函数。2.2 ODS 层为什么保持 TEXTFILE外部表 原始目录的配合ODS 层用外部表 TEXTFILE 是业界最常见的选择理由有三个方面。第一网关导出的原始话单就是文本TEXTFILE 让 Hive 直接读原始文件不做任何格式转换保留现场。第二外部表删除表结构时不会删 HDFS 上的原始文件元数据误删后数据还在重建表就能恢复这对还没上元数据备份的课程设计环境很重要。第三分区目录可以直接用 hdfs dfs -put 把文件扔进去再执行 MSCK REPAIR TABLE 注册分区完全绕开 LOAD DATA 的文件移动语义。对应的 HDFS 目录结构是这样组织的/data/hive/warehouse/tel/ods_tel_cdr/ └── dt20240101/ ├── cdr_part_001.txt ├── cdr_part_002.txt └── cdr_part_003.txt提示Hive 的 LOCATION 指向表级目录分区目录名必须是 dt20240101 这种列名值的形式MSCK REPAIR 才能识别。手动建目录时写成 /dt/20240101 是新手最常见的分区注册失败原因。2.3 从 ODS 清洗到 DWDORC 列式存储与 SNAPPY 压缩原始文件留底之后分析不能直接跑在 TEXTFILE 上。TEXTFILE 行式存储做 SELECT SUM(data_usage) 这类列聚合时要把整行读出来10 个字段只用到 2 个浪费大量磁盘 IO。我一般会建一张 DWD 层表换成 ORC 列式存储压缩选 SNAPPY——这是 Hadoop 生态里压缩速度和查询速度最平衡的组合比 Gzip 少耗 CPU适合分析型任务反复扫描。CREATE TABLE dwd_tel_cdr ( msisdn STRING COMMENT 手机号脱敏, imsi STRING COMMENT SIM卡标识, call_time STRING COMMENT 事件时间, call_duration INT COMMENT 通话时长秒, call_type STRING COMMENT O主叫 T被叫, called_number STRING COMMENT 对端号码, lac_ci STRING COMMENT 位置区小区, data_usage BIGINT COMMENT 流量字节, sms_cnt INT COMMENT 短信条数, roam_flag TINYINT COMMENT 0本地 1漫游, plan_code STRING COMMENT 套餐编码 ) PARTITIONED BY (dt STRING) STORED AS ORC TBLPROPERTIES (orc.compressSNAPPY);从 ODS 刷数据到 DWD 用动态分区插入清洗规则一并写在取数 SQL 里而不是回头改原始文件SET hive.exec.dynamic.partitiontrue; INSERT OVERWRITE TABLE dwd_tel_cdr PARTITION (dt) SELECT msisdn, imsi, call_time, IF(call_duration BETWEEN 0 AND 86400, call_duration, 0) AS call_duration, call_type, called_number, lac_ci, data_usage, sms_cnt, roam_flag, plan_code, dt FROM ods_tel_cdr WHERE dt 20240101 AND msisdn IS NOT NULL AND msisdn ;这里有个顺序要求动态分区列 dt 必须写在 SELECT 列表的最后Hive 靠位置而不是列名来对齐分区字段写错位置会直接报Column count doesnt match或把数据写进错误分区。清洗逻辑里 IF(call_duration BETWEEN 0 AND 86400, ...) 是防御负时长和超长误差数据一天最多 86400 秒超出必是脏数据。3. Hadoop 伪分布式搭建与 CDR 数据入库拿到一张分析表之后下一步是把 Hadoop 环境跑起来。单机课程设计用伪分布式模式就够了也就是一台机器同时跑 NameNode、DataNode、ResourceManager、NodeManager 四个进程。相比完全分布式它省掉了节点间免密的批量分发和机架感知配置但核心读写路径和分布式集群完全一致HDFS 命令、Hive SQL、YARN 调度全都能验证。嫌本机环境脏的话也可以直接拉 Hadoop 的 docker 镜像跑但容器方案在格式化 NameNode 的时机和数据目录挂载上容易出权限类问题下面按裸机方式讲。3.1 搭建前的三件事JDK、SSH 免密、解压路径很多教程一上来就改配置文件结果 start-dfs.sh 卡在要输入密码。伪分布式里 Master 和 Worker 是同一台机器先把 SSH 免密做好能省掉后面所有重启排错时的手动输入。java -version # 确认是 JDK 1.8而不是只装了 JRE ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys ssh localhost echo ok-P 表示生成空口令密钥-f 指定输出路径避免交互式确认。最后一条 ssh localhost 能直接回显 ok说明免密生效。紧接着把 Hadoop 解压到固定目录并在 hadoop-env.sh 里显式写明 JAVA_HOME。Hadoop 3.x 的 start-dfs.sh 不会继承 shell 里 export 的 JAVA_HOME必须写进 $HADOOP_HOME/etc/hadoop/hadoop-env.sh这是新装环境第一个隐蔽坑点。3.2 三份核心配置与 8GB 笔记本的内存预算伪分布式需要改三个配置文件core-site.xml 管文件系统入口hdfs-site.xml 管副本策略yarn-site.xml 管计算资源。课程设计环境的数据量一般在 GB 级副本数设 1 就够设 3 只会让 DataNode 在单机上多写两份垃圾。!-- core-site.xml -- configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/data/hadoop/tmp/value /property /configurationhadoop.tmp.dir 决定 NameNode 元数据和 DataNode 数据块的存放根目录。默认值指向系统 /tmp很多发行版会定时清理格式化之后重启发现元数据丢了报的就是 ClusterId 不一致或者 NameNode 起不来——这不是 Hadoop 的问题是目录选错了。单独建一个 /data/hadoop/tmp尽量和数据文件放同一个挂载盘。!-- hdfs-site.xml -- configuration property namedfs.replication/name value1/value /property /configurationyarn-site.xml 是伪分布式里最容易因为内存参数翻车的地方。默认配置按大集群估算单机 8GB 内存跑 MR 任务经常被 YARN 直接杀掉。三个必调参数如下参数默认值单机建议值作用yarn.nodemanager.resource.memory-mb81925120单节点可调度的物理内存总量yarn.scheduler.maximum-allocation-mb81922048单个 container 能申请的内存上限yarn.nodemanager.vmem-check-enabledtruefalse是否做虚拟内存超限检查!-- yarn-site.xml -- configuration property nameyarn.nodemanager.resource.memory-mb/name value5120/value /property property nameyarn.scheduler.maximum-allocation-mb/name value2048/value /property property nameyarn.nodemanager.vmem-check-enabled/name valuefalse/value /property /configuration解释一下这三个值的搭配逻辑。8GB 机器上 NM 拿到 5GB减去系统本身和 DataNode 的占用剩下的交给 MapReduce。每个 container 上限 2GBmap 任务堆内存 mapreduce.map.java.opts 默认继承 container 大小不会超售。vmem-check-enabled 设成 false 是关掉虚拟内存超限检查因为 JVM 申请虚拟地址空间经常超过物理内存值开着这个检查在伪分布式上会误杀任务。3.3 格式化、启停命令与样例数据入库配置完成后启动顺序是固定的先格式化 NameNode再启动 HDFS 层再启动 YARN 层。hdfs namenode -format start-dfs.sh start-yarn.sh jpsjps 输出里能看到 NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager 五个进程缺哪个就去查对应日志。启停命令这里有两个习惯一是 start-all.sh 和 stop-all.sh 虽然能用但会输出 deprecation 提示我一般分开用 start-dfs.sh 和 start-yarn.sh排错时一眼能看出是哪一层没起来二是 stop-dfs.sh 时 DataNode 退出要几十秒不要因为 jps 还看得到进程就急着重新格式化等日志输出SHUTDOWN_MSG再操作。注意伪分布式单节点不需要装 ZooKeeper。网上很多教程把 Hadoop 和 ZooKeeper 整合实战当作固定搭配但 ZK 只在 HA 高可用模式里承担 namenode 选主单机课程设计装了反而是多余进程还会占用本就不宽裕的内存。环境就绪后造一批模拟话单。我用 Python 生成 5 万条记录故意让 20% 的记录落在同一个号码上制造一个热点用户这个热点会在第 5 章说明数据倾斜时复现问题import random import datetime NUM_RECORDS 50000 HOT_USER 18800000001 def fake_msisdn(): if random.random() 0.2: return HOT_USER return 188 .join(random.choices(0123456789, k8)) with open(cdr_20240101.txt, w) as f: for _ in range(NUM_RECORDS): t datetime.datetime(2024, 1, 1, random.randint(0, 23), random.randint(0, 59), random.randint(0, 59)) fields [ fake_msisdn(), 46000 .join(random.choices(0123456789, k11)), t.strftime(%Y-%m-%d %H:%M:%S), str(random.randint(0, 3600)), random.choice([O, T]), 13 .join(random.choices(0123456789, k9)), random.choice([f9f1, a3a7]), str(random.randint(0, 100 * 1024 * 1024)), 0, 0, T1 ] f.write(\t.join(fields) \n)入库用 hdfs dfs -put 直接把文件放进分区目录然后执行 MSCK REPAIR 让 Hive 注册分区hdfs dfs -mkdir -p /data/hive/warehouse/tel/ods_tel_cdr/dt20240101 hdfs dfs -put cdr_20240101.txt /data/hive/warehouse/tel/ods_tel_cdr/dt20240101/ hive -e MSCK REPAIR TABLE ods_tel_cdr;MSCK REPAIR 会扫描表目录下的分区目录并批量写进 metastore比手写 ALTER TABLE ADD PARTITION 靠谱目录多的时候也快得多。入库后验证一下分区是否注册成功hive -e SHOW PARTITIONS ods_tel_cdr;能看到 dt20240101 就说明环境到入库链路全通了。提示如果 YARN 任务在提交阶段报 java.lang.NoClassDefFoundError: org/apache/hadoop/crypto先查两件事hadoop-env.sh 里的 JAVA_HOME 是否指向 JDK 而不是 JRE再执行 hadoop classpath 看输出是否为空。这条错误在新装环境里比真正缺 jar 常见得多。4. 用 Hive 算出电信用户行为指标活跃度、忙时与离网倾向数据进了 DWD 层接下来就是把行为翻译成可量化指标。电信行业常用的三个口径是 MOU月户均通话分钟、DOU月户均流量、ARPU月户均收入课程设计没有计费数据时至少要把前两个做出来再补一个离网倾向圈选。所有 SQL 都跑在 dwd_tel_cdr 上下面给出能直接运行的语句和对应的指标口径表。指标口径定义在哪里算日均通话次数按 msisdn 分组 COUNT(*)4.1户均通话时长 MOUSUM(call_duration) / 用户数4.1户均流量 DOUSUM(data_usage) / 用户数4.1忙时时段分布按小时聚合通话量与平均时长4.2离网倾向用户7 日内活跃天数低或话务骤降4.34.1 基础活跃度每人每天打了多少电话、用了多少流量第一张报表是用户日活跃度汇总按 msisdn 分组统计通话次数、通话总时长、流量总量和有流量天数SET hive.execution.enginetez; SET hive.exec.paralleltrue; SELECT msisdn, COUNT(*) AS call_cnt, SUM(call_duration) AS total_dur_s, SUM(data_usage) AS total_data_b, COUNT(IF(data_usage 0, 1, NULL)) AS data_cnt FROM dwd_tel_cdr WHERE dt 20240101 GROUP BY msisdn ORDER BY total_data_b DESC LIMIT 100;第一行把执行引擎切到 TezHive on Tez 比默认的 MapReduce 在 DAG 调度上更适合多阶段查询这也是标题相关热词里hive配置tez对应的实际配置点。第二行开启并行执行让没有依赖关系的 Stage 并行跑。SQL 里 COUNT(IF(data_usage 0, 1, NULL)) 的写法等价于有流量的天数用 IF 产出 1 或 NULL 再 COUNT比 COUNT(DISTINCT IF(...)) 省一轮去重。ORDER BY ... LIMIT 100 在 Tez 引擎下会做全局排序后取前 100直接得到流量 TOP 用户榜也就是 DOU 最高的那批人。4.2 忙时分布找到通勤早高峰和晚高峰忙时分析对电信网络规划很重要扩容基站、调整呼叫路由都依赖时段分布。按小时聚合通话量和平均时长SELECT substr(call_time, 12, 2) AS hour_bucket, COUNT(*) AS call_cnt, AVG(call_duration) AS avg_dur_s FROM dwd_tel_cdr WHERE dt 20240101 GROUP BY substr(call_time, 12, 2) ORDER BY hour_bucket;这里刻意用 substr(call_time, 12, 2) 而不是 hour(call_time)原因在于 call_time 在 ODS 阶段就是 STRINGsubstr 直接按位置截取第 12 到 13 位得到小时不触发任何类型转换。如果某几条脏数据时间格式不对hour() 会返回 NULL 导致该时段被吞掉substr 至少能保留现场。结果的典型形态是早上 8 点和晚上 19 点两个峰值上午 10 点到 11 点次高峰凌晨 2 点到 5 点低谷。如果你的是全国范围数据注意话单时间用的是哪个时区跨时区省份的手机在漫游场景下会有时区偏移课程设计里建议统一按服务省本地时间入库。4.3 离网倾向圈选7 天窗口规则而不是模型离网预测在真实业务里会用 XGBoost 之类的模型但在课程设计尺度上先用规则圈出明显不活跃的用户同样能讲清楚行为分析的落地价值。规则定为7 天内活跃天数不超过 2 天或者累计通话时长低于 10 分钟或者最后活跃距今超过 5 天满足其一就进入离网倾向名单WITH active7 AS ( SELECT msisdn, COUNT(DISTINCT dt) AS active_days, SUM(call_duration) AS dur7, SUM(data_usage) AS data7, MAX(dt) AS last_active_day FROM dwd_tel_cdr WHERE dt BETWEEN 20240101 AND 20240107 GROUP BY msisdn ) SELECT msisdn, active_days, dur7, data7, datediff(20240108, last_active_day) AS days_since_last FROM active7 WHERE active_days 2 OR dur7 600 OR datediff(20240108, last_active_day) 5;CTE 写法先算 7 天汇总再在外面套规则过滤好处是每个用户只聚合一次多个圈选条件复用同一份中间结果。规则阈值的选取要有业务解释10 分钟对应月通话 40 分钟的底线5 天不活跃对应流失前沉默期的行业经验。真实系统里这个结果还要 JOIN 投诉工单表、套餐变更表来降低误杀率但 d 表结构上就是 LEFT JOIN 维表补字段的问题本文不展开。4.4 没有 Hive 时同一指标用 MapReduce 怎么写Hive 执行 GROUP BY 时底层编译出来的就是 MapReduce 作业。理解这一点能帮你在 Hive 出问题时定位到 YARN 日志。以 4.2 的忙时统计为例Mapper 端做时段提取和局部计数Reducer 端做最终累加public class HourMapper extends MapperLongWritable, Text, Text, IntWritable { private static final IntWritable ONE new IntWritable(1); private final Text hour new Text(); Override protected void map(LongWritable key, Text value, Context context) { String[] fields value.toString().split(\t); if (fields.length 3) { return; // 脏行字段数不够直接跳过 } String callTime fields[2]; if (callTime.length() 13) { return; } hour.set(callTime.substring(11, 13)); context.write(hour, ONE); } }Reducer 只需要重写 reduce 方法把 values 累加。这段代码的逻辑和 4.2 的 substr(call_time, 12, 2) 完全等价区别在于 MapReduce 要自己处理脏行过滤Hive 把同样的判断内化成了 SQL 语义。日常开发用 Hive出诡异结果时打开 YARN 上对应作业的日志看 Map 输出条数和 Reduce 输入条数是否合理这一步排错能力在面试里比会写十个 SQL 更值钱。5. 数据倾斜、LOAD 语义与结果验证三个必踩的坑5.1 数据倾斜热点用户把单个 Reduce 拖到 40 分钟第 3 章造数据时故意让 20% 的话单落在同一个号码上这就是真实场景里的 VIP 用户效应——一个用户一天两万条话单按 msisdn 分组时这全部两万条会进同一个 Reduce其他 Reduce 早就跑完整个作业卡在最慢的那个分片上。先确认症状再动手YARN 作业页面上大部分 Reduce 显示 SUCCEEDED只有一个长期 RUNNING且该 Reduce 的输入记录数是其他 Reduce 的几十倍。课程设计级别最有效的解法是加盐两阶段聚合思路是把分组键拆成盐值 真实键先行打散聚合再去掉盐值做第二遍聚合SELECT split(salted_key, _)[1] AS msisdn, SUM(c) AS call_cnt FROM ( SELECT concat(cast(floor(rand() * 10) AS INT), _, msisdn) AS salted_key, COUNT(*) AS c FROM dwd_tel_cdr WHERE dt 20240101 GROUP BY concat(cast(floor(rand() * 10) AS INT), _, msisdn) ) t GROUP BY split(salted_key, _)[1];内层给每个用户随机拼一个 0 到 9 的前缀热点用户的记录就被拆到最多 10 个 Reduce 上每个 Reduce 的压力降到原来的十分之一外层再按真实 msisdn 汇总。这个方案只适用于 COUNT、SUM、AVG 这类可分解的聚合指标COUNT(DISTINCT) 这种去重指标不能直接这么拆因为同一盐值内的去重结果合并到外层时会互相污染。5.2 LOAD DATA 的移动语义原始文件到底去哪了把原始话单放进分区目录时如果用 LOAD DATA INPATH 而不是 hdfs dfs -put文件会被从源路径移动不是复制到表目录源路径会消失。这在 ODS 层是危险的一旦表被误删重建或者想回溯原始文件做重清洗只能回网关重新拉数。所以建表时用外部表、入库用 dfs -put 加 MSCK REPAIR等于把原始文件归 HDFS 管、元数据归 Hive 管这两件事分开任何一层出问题都不影响另一层。5.3 结果验证的三个核对点指标算完不能直接写进报告先做三件事。第一是条数核对原始文件 wc -l 的行数应该等于分区 COUNT(*)不一致就查分隔符是不是 \t文件里混入空格会导致整行被当成 null。第二是抽人核对从结果里随机挑一个用户在原始文件里 grep 出他当天的记录手工算一遍 SUM(call_duration)和 SQL 结果对不上就看是不是脏数据的 IF 过滤逻辑被误改了。第三是存储与统计信息核对hive -e ANALYZE TABLE dwd_tel_cdr PARTITION(dt20240101) COMPUTE STATISTICS; hive -e SHOW FORMATTED TABLE dwd_tel_cdr PARTITION(dt20240101);ANALYZE 之后 SHOW FORMATTED 能看到该分区的 numRows 和 totalSize这两个值会出现在后续执行计划的估算里也是判断 ORC 压缩是否生效的依据——原始 5 万条文本如果是 30MBORC SNAPPY 之后应该只有几 MB。核对完这三个点电信用户行为分析的链路从建表到出数才算真正闭环。本文还有配套的精品资源点击获取