
简介基于Hadoop的游戏数据分析系统项目源码包是一套适合大数据方向学习与实战的完整参考面向具备一定Java基础、希望深入理解Hadoop应用场景的开发者、学生及竞赛参与者。压缩包共20个文件主要包含JSP动态页面、JavaScript/CSS前端资源、Java核心代码、SQL建表脚本及JAR依赖包整体仅2.1MB结构紧凑清晰便于导入开发工具直接运行。系统实现了玩家活跃度分析、付费行为分析、游戏习惯分析、新用户分析等典型模块覆盖数据采集、清洗、MapReduce计算与结果展示全流程配套数据库初始化脚本和Web可视化界面可帮助学习者快速掌握Hadoop分布式文件系统与并行计算在真实业务中的集成方式。已有220人学习下载适合用作课程设计、毕业设计或实习项目也能为从事游戏数据分析的工程师提供可复用的工程参考。1. 游戏日报表为什么最后都落到 Hadoop 上做过游戏数据分析的人基本都经历过这个阶段运营每天早上要一份日报包含新增、活跃、留存、付费金额数据量从几十万涨到几千万行的时候MySQL 的单表查询开始把数据库拖到报警Excel 打开一个月的日志要转半小时圈。这时候把日志往 HDFS 里一扔用 Hive 写几条 SQL 让 MapReduce 去跑反而是最稳的路子。基于 Hadoop 的游戏数据分析系统解决的正是这种数据量大、计算逻辑偏批处理、对实时性要求不高的离线分析场景涵盖了从环境搭建、日志采集、Hive 建模到指标计算和结果导出的完整链路。适合正在做课程设计或毕业设计的在校生也适合准备给团队搭第一套离线数仓的初级工程师照着复现。2. 搭一套能跑起来的最小 Hadoop伪分布式装法与三个必调参数2.1 为什么我们先不上集群伪分布式的取舍很多人在从零安装 hadoop 这一步就被劝退了因为一上来就照着三台机器的集群教程去配每台机器都要配免密、配 Zookeeper、配高可用折腾两天可能连格式化都没过。我一般建议先跑伪分布式它在单机上用不同进程模拟出 NameNode、DataNode、ResourceManager、NodeManager你写的 HDFS 命令和 Hive SQL 跟集群上没有任何区别只是性能差一些。伪分布式的核心价值在于把分布式系统的概念复杂度和运维复杂度分开。你可以在这一层把 HDFS 的块存储、副本机制、MapReduce 的调度流程先跑通等真上了集群只是把配置里的 IP 从 localhost 换成多台机器再补上 Zookeeper 做自动故障转移。这也是为什么 Hadoop 课程设计和面试题里问得最多的不是集群搭建而是伪分布式搭建过程。硬件上 8G 内存的机器就能跑Ubuntu、CentOS 都可以Windows 下要么用 WSL2要么按 IDEA 连远程 HDFS 的方式做开发套路一样。2.2 Hadoop 安装与核心配置从解压到格式化环境以 Ubuntu 为例先装好 JDK。Hadoop 3.x 用 JDK 8 或 JDK 11 都可以我习惯用 JDK 8兼容性最稳。下载 Hadoop 二进制包后解压到/opt/hadoop然后配置环境变量。这一步网上教程很多但真正决定能不能起来的是etc/hadoop/下五个配置文件的参数。# 解压并配置环境变量 tar -zxvf hadoop-3.3.6.tar.gz -C /opt/ mv /opt/hadoop-3.3.6 /opt/hadoop # 编辑 /etc/profile 追加 export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export HADOOP_HOME/opt/hadoop export PATH$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin source /etc/profile接下来是core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml、hadoop-env.sh这五个文件。core-site 里指定 NameNode 的地址和临时数据目录hdfs-site 里必须把副本数改成 1因为伪分布式只有一个 DataNode默认 3 个副本会导致大量块处于 under-replicated 状态日志里全是警告。hadoop-env.sh 里要把JAVA_HOME写死。下面是核心的最小配置。!-- core-site.xml -- configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/opt/hadoop/data/tmp/value /property /configuration !-- hdfs-site.xml -- configuration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/opt/hadoop/data/namenode/value /property property namedfs.datanode.data.dir/name value/opt/hadoop/data/datanode/value /property /configuration !-- yarn-site.xml -- configuration property nameyarn.nodemanager.resource.memory-mb/name value4096/value /property property nameyarn.scheduler.maximum-allocation-mb/name value4096/value /property /configurationfs.defaultFS决定了你所有 HDFS 路径的前缀后面写/game_logs其实就是hdfs://localhost:9000/game_logs。hadoop.tmp.dir这个参数是很多人的坑默认指向/tmp/hadoop-xxxLinux 重启后/tmp会被清空NameNode 的数据目录没了进程起不来。我吃过这个亏所以把数据目录单独指到了/opt/hadoop/data下。dfs.replication设成 1 是伪分布式的铁律。yarn.nodemanager.resource.memory-mb要跟机器实际内存匹配如果你只有 8G 内存留给 YARN 4G 是比较保守的值。2.3 服务起没起对用 jps 和 HDFS 命令自检配置完成后首次启动前必须先格式化 NameNode这个动作是生成文件系统的元数据类似于给一块新硬盘做格式化。很多人启动报错就是因为漏了这一步或者重复格式化导致 clusterID 不一致。# 配置 ssh 免密本地也需要 ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys # 格式化 NameNode只有第一次需要 hdfs namenode -format # 启动 HDFS 和 YARN start-dfs.sh start-yarn.sh # 验证五个进程都在 jpsjps能看到五个进程才算起对了NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager。少一个都说明有问题这时候去/opt/hadoop/logs/下找对应进程的日志最后十行基本就是原因。我自己一般会再跑两条命令确认 HDFS 本身是通的单看进程在并不能说明文件系统可用。# 建一个游戏日志目录 hdfs dfs -mkdir -p /game_logs hdfs dfs -ls /如果mkdir报FileAlreadyExistsException或者Connection refused先去logs/hadoop-hadoop-namenode-xxx.log看错误。Hadoop 3.x 的 NameNode Web 界面在http://localhost:9870YARN 的 ResourceManager 界面在http://localhost:8088能看到 DataNode 的容量和节点状态。到了这一步你的 hadoop 伪分布式搭建全过程就算真正跑通了后面的分析任务全部建立在进程在、文件系统能读写这个基础上。3. 游戏日志进 HDFS埋点格式、上传命令与分区表设计3.1 一份能落地的游戏埋点日志长什么样游戏数据分析系统的数据源头是客户端上报的埋点日志。常见做法是客户端把行为上报到 Nginx由日志采集服务把数据按天落盘最终传到 HDFS。如果你手头没有真实日志做练习我一般建议先用脚本生成模拟数据把格式跑通再换真数据。埋点日志的格式不需要很花哨竖线分隔比 JSON 省空间也方便 Hive 用正则切分。下面是一个能直接用的模拟数据生成脚本。import random import datetime # 生成一天的模拟游戏日志格式 # 日志时间|玩家ID|服务器ID|事件类型|附加参数 events [login, battle_start, battle_end, pay, level_up] servers [s1, s2, s3] result [] base_time datetime.datetime(2024, 6, 1) for i in range(10000): event random.choice(events) player_id fp{random.randint(1000, 9999)} server random.choice(servers) # 时间随机偏移模拟一天内的分布 offset random.randint(0, 86400) ts base_time datetime.timedelta(secondsoffset) params str(random.randint(1, 100)) if event pay else str(random.randint(1, 1000)) result.append(f{ts.strftime(%Y-%m-%d %H:%M:%S)}|{player_id}|{server}|{event}|{params}) with open(/tmp/game_log_20240601.txt, w) as f: f.write(\n.join(result))这个脚本生成的字段顺序要记住时间、玩家 ID、服务器、事件、附加参数。其中pay事件的附加参数存的是充值金额单位是分这是游戏行业的通用规范——用整数分而不是浮点元避免精度问题。如果你在真实公司里还会看到事件里带上渠道 ID、设备型号、版本号但做系统原型时五个字段已经够用了。3.2 日志落地 HDFS从 put 到 Flume 的取舍有了日志文件下一步是上传到 HDFS。最简单的做法是hdfs dfs -put但生产上很少有人手动传文件一般用 Flume 监听目录或者通过采集服务直接写入。手动的上传命令要按日期组织目录这样 Hive 分区表才能对上。# 手动上传目录按日期分区 hdfs dfs -mkdir -p /game_logs/dt2024-06-01 hdfs dfs -put /tmp/game_log_20240601.txt /game_logs/dt2024-06-01/ # 用 cron 实现每天自动上传前一天的日志 0 2 * * * hdfs dfs -put /data/game_logs/$(date -d yesterday %Y%m%d).txt /game_logs/dt$(date -d yesterday %Y-%m-%d)/这里有一个很多新手会翻车的点/game_logs/dt2024-06-01/这个目录下必须直接放文件不能嵌套子目录。假如你把整个/data/game_logs/目录用put -r传上去HDFS 里会变成/game_logs/dt2024-06-01/game_log_20240601.txt还算好但如果多套了一层2024-06-01/game_log_20240601.txtHive 读分区时会去扫描整个目录树的文件内容很可能因为找不到字段分隔符而返回 NULL。HDFS 没有传统文件系统的覆盖提示put到已有同名的路径会直接覆盖所以脚本里的日期变量要小心拼错一旦日志被覆盖就没有后悔药了。如果你要用 Flume 做定时采集核心配置是监听一个 spill 目录Flume 收到文件后自动按前缀和大小滚动写入 HDFS这种方式比 crontab 更可靠因为 Flume 自带断点续传和失败重试。不过伪分布式做课设的话crontab 加put足矣少一个组件少一份排查负担。3.3 Hive 外表与分区别把分区字段塞进数据表日志进了 HDFS要用 Hive 把它变成能跑 SQL 的表。这里强烈建议建外部表而不是内部表。差异在于删除外部表只删元数据HDFS 里的文件还在内部表删除会连同数据文件一起删。做数据分析的人都有过误删表的经历外部表就是那粒后悔药。建表语句如下。CREATE EXTERNAL TABLE game_log ( log_time STRING, player_id STRING, server_id STRING, event_type STRING, params STRING ) PARTITIONED BY (dt STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY | LOCATION /game_logs; -- 把 HDFS 上已有的分区目录挂载到表上 MSCK REPAIR TABLE game_log; -- 确认分区识别成功 SHOW PARTITIONS game_log;建表时的字段顺序、分隔符必须和数据文件完全一致多一个空格都会让整列数据变成 NULL。分区字段dt是虚拟列它的值来自目录名/game_logs/dt2024-06-01不需要写进数据文件里。如果你在数据文件里也加了日期列查询时用WHERE dt2024-06-01会过滤掉一部分但SELECT dt会把目录名和字段值混在一起逻辑就乱了。MSCK REPAIR TABLE的作用是扫描LOCATION目录下的分区目录并注册到元数据上传了新分区后要重新执行一次。如果你用的是 Hive 3.x 并且开启了 metastore 的自动发现有时不需要手动REPAIR但我习惯执行一下因为集群的hive.metastore.auto-discovery.enabled不一定开着。4. 用 Hive SQL 跑游戏核心指标DAU、留存与付费分析4.1 为什么不能直接 count去重、时间窗口与数据倾斜游戏数据分析系统最常见的指标是 DAU日活跃用户数。很多新手上来就写SELECT COUNT(*) FROM game_log WHERE dt2024-06-01算出来的数翻了好几倍因为同一玩家一天里会产生几十条行为日志。DAU 的定义是当天至少有一次行为的独立玩家数所以必须去重。SELECT COUNT(DISTINCT player_id) AS dau FROM game_log WHERE dt 2024-06-01 AND event_type IN (login, battle_start, pay);这里把event_type做了限制是因为有些事件是后台自动触发的比如心跳包、拉取排行榜、装备检查这些不是玩家的主动行为混进去会让 DAU 虚高。另一个常见问题是只统计login事件结果会偏低因为有一部分玩家跨天挂着没退出次日直接打了一把就下线没有产生新的 login 事件。所以我习惯把login、battle_start、pay都算进活跃口径这套逻辑你要在前言里给运营讲清楚避免他们拿你的数字跟渠道后台对不上来质疑你。COUNT(DISTINCT ...)在数据量大时会把相同 player_id 都 shuffle 到同一个 reducer 上造成数据倾斜。如果一天的日志过亿建议用子查询先去重再 count或者开set hive.map.aggrtrue;默认就是开着的触发 map 端聚合减少 shuffle 的数据量。我在课设里这么写没问题但如果你把这个逻辑用在千万级日活的游戏上会发现跑得很慢因为COUNT(DISTINCT)本质上是单 reducer 作业所有玩家的 ID 都要汇到一个节点上去重。4.2 留存分析用 SQL 做日期差与首登表留存是游戏分析里最核心的指标。新增玩家次日留存、7 日留存的算法逻辑完全一样。留存的难点在于需要一张基准表——记录每个玩家首次进入游戏的日期然后拿这个表和每天的活跃玩家做关联算日期差。-- 先取每个玩家的首登日作为新增基准 CREATE TABLE player_first_login AS SELECT player_id, MIN(dt) AS first_login_dt FROM game_log WHERE event_type IN (login, battle_start) GROUP BY player_id; -- 计算次日留存首登后第2天仍然活跃的玩家占比 SELECT COUNT(DISTINCT b.player_id) / COUNT(DISTINCT a.player_id) AS day1_retention FROM player_first_login a LEFT JOIN ( SELECT DISTINCT player_id, dt FROM game_log WHERE event_type IN (login, battle_start) ) b ON a.player_id b.player_id WHERE b.dt IN (SELECT date_add(a.first_login_dt, 1));这个 SQL 有三个关键点。第一首登日的口径是MIN(dt)如果玩家第一天只产生了 pay 事件他是不会被算进新增的所以event_type的过滤条件要和 DAU 保持一致性。第二date_add(first_login_dt, 1)得到的日期要和活跃表b.dt精确匹配这里要求日期格式严格是yyyy-MM-dd如果你存了带时分秒的字符串匹配就会失效。第三我用了LEFT JOIN加WHERE b.dt IN (...)等价于INNER JOIN但比JOIN再过滤更清晰因为你会发现有些玩家的登录时间在首登之前——这通常是因为测试环境的时间戳错乱数据质量有问题而不是 SQL 写错了。如果你要算 7 日留存或 30 日留存把date_add(..., 1)改成date_add(..., 6)、date_add(..., 29)就行。生产环境里我一般会把留存逻辑封装成一个存储过程或者定时调度的 HiveQL 脚本传入N作为参数一次性输出从 1 到 30 的留存矩阵。4.3 付费分析从流水明细到 ARPU/ARPPU付费指标里ARPU每活跃用户平均收入和 ARPPU每付费用户平均收入是两个容易搞混的概念。ARPU 的分母是全量活跃用户ARPPU 的分母只是付过费的用户。对运营来说ARPPU 衡量的是付费玩家的消费力ARPU 衡量的是整体变现效率。下面这条 SQL 可以把两个指标一起算出来。SELECT dt, SUM(CASE WHEN event_type pay THEN CAST(params AS INT) ELSE 0 END) / 100 AS revenue, COUNT(DISTINCT CASE WHEN event_type pay THEN player_id END) AS paying_users, COUNT(DISTINCT player_id) AS active_users, SUM(CASE WHEN event_type pay THEN CAST(params AS INT) ELSE 0 END) / 100 / COUNT(DISTINCT player_id) AS arpu FROM game_log WHERE dt 2024-06-01 GROUP BY dt;params字段在 pay 事件里存的是以分为单位的金额所以这里先CAST(params AS INT)转成整数再除以 100 得到元。很多新手直接拿字符串做计算Hive 会静默转成 0结果就是收入全是 0。COUNT(DISTINCT CASE WHEN ... THEN ... END)是条件去重计数的标准写法它统计的是付费玩家数。ARPPU 就是revenue / paying_users把上面两个数相除即可。付费分析还有一个维度是首充玩家这是游戏运营非常关注的指标。逻辑和留存类似先找每个玩家第一条 pay 记录的日期再算它和首登日的间隔天数。如果间隔是 0说明当天就付费了这是最优质的玩家。这类分析的 SQL 不复杂核心是把event_type pay和player_id组合起来做窗口函数。Hive 支持ROW_NUMBER() OVER (PARTITION BY player_id ORDER BY log_time)你可以用它取第一条 pay 记录但要小心log_time是字符串格式时排序不一定是时间序建议存的时候就用yyyy-MM-dd HH:mm:ss这种零填充格式字典序就等于时间序。5. Hadoop 游戏项目常见问题排查从启动失败到结果对不上5.1 NameNode 格式化后起不来数据目录冲突启动start-dfs.sh后jps里连 NameNode 的进程都看不到或者进程存在但 Web 界面无法访问。去logs/hadoop-hadoop-namenode-xxx.log里看最常见的报错是NameNode is not formatted或者Directory /opt/hadoop/data/namenode is in an inconsistent state。原因是/tmp被系统自动清理或者你换了hadoop.tmp.dir路径后没有重新格式化。还有一种情况你格式化了一次启动成功后面因为改配置重新执行了hdfs namenode -format导致 NameNode 的 clusterID 和 DataNode 的不一致DataNode 拒绝注册。解决方法是删掉数据目录重新初始化。注意先把服务停掉然后把dfs.namenode.name.dir和dfs.datanode.data.dir两个目录下的内容清空最后重新hdfs namenode -format。重复格式化不是不行但每次都要清空两个目录只清一个就会出现 clusterID 不匹配。这也是我为什么建议把数据目录从/tmp挪到/opt/hadoop/data的原因路径更直观清理时不容易漏。5.2 跑 Hive 任务报内存不足YARN 容器参数没调Hive 跑查询时MapReduce 任务执行到一半报Java heap space或者Container killed by the ApplicationMaster日志里还会出现Container [pidxxx] is running beyond physical memory limits。新手看到内存溢出第一时间会去调 Hive 的hive.heapsize但这通常治标不治本。真正的瓶颈是 YARN 给每个容器分配的物理内存默认只有 1GB而你的日志分析和 join 操作很容易超过这个值。我一般这样调整把yarn.nodemanager.resource.memory-mb设成机器内存的 50% 到 60%同时把单个容器能拿到的上限yarn.scheduler.maximum-allocation-mb设成同样的值这样 map 和 reduce 各有 2G 到 4G 可用。如果你的机器只有 4G 内存就不要强行给 YARN 分配 4G系统本身要留出 1.5G 以上否则节点会直接 OOM整个 Hadoop 集群都崩了。!-- yarn-site.xml 中追加 -- property namemapreduce.map.memory.mb/name value2048/value /property property namemapreduce.reduce.memory.mb/name value2048/value /property调完参数要重启 YARNstop-yarn.sh再start-yarn.sh。如果还报内存不足就要用EXPLAIN看执行计划里是不是某个 reducer 拉取了过多数据这就是数据倾斜的问题了光加内存解决不了。5.3 留存算出来是空的时区和字符串格式化在搞鬼留存的 SQL 跑完结果是 0 或者 NULL。先确认分区目录里有数据SELECT COUNT(*) FROM game_log WHERE dt2024-06-01正常返回但 join 出来的表里第一个玩家都没有匹配上。我遇到过的情况是日志里的时间戳用了 UTC 时间。如果游戏客户端在海外或者服务器用了 UTC北京时间凌晨 0 点到 8 点产生的日志在 UTC 里还是前一天dt分区和MIN(dt)的日期就会错位。玩家在北京时间 6 月 1 日 00:30 登录日志里记的是 5 月 31 日 16:30这个玩家的首登日变成了 5 月 31 日6 月 1 日的活跃表里自然找不到他留存率就偏低甚至为 0。解决方法是清洗数据时统一时区Hive 里有现成的函数-- 如果 log_time 存的是 UTC先转成北京时间 SELECT from_utc_timestamp(log_time, Asia/Shanghai) AS beijing_time FROM game_log;转完之后用to_date()截取日期保证dt字段永远是东八区的日期。另一个坑是date_add(2024-06-01, 1)的结果虽然是字符串 2024-06-02但 Hive 里日期计算要求传入的标准格式是yyyy-MM-dd如果你前面存的是2024/06/01或者20240601全部对不上。5.4 上传的日志文件 Hive 读不出来分隔符和压缩格式文件到 HDFS 了SHOW PARTITIONS也正常但SELECT查出来的字段全是 NULL。用hdfs dfs -cat看文件内容分隔符明明是|建表语句里也写了FIELDS TERMINATED BY |问题出在文件的换行符或者隐藏字符上。Windows 下编辑的日志文件换行符是\r\nHive 默认按\n切行所以每行末尾会带着\r。如果最后一个字段是params它的值就变成了123\rCAST 成数值时直接失败返回 NULL。这个问题很难一眼看出来因为 HDFS 的-cat不会显示\r字符。解决方法是上传前用sed -i s/\r$//清理或者在建表语句里指定ROW FORMAT SERDE时用正则把\r也当成分隔符。还有一个问题是日志文件被压缩成了 gz 或者 zipHive 虽然能自动识别 gzip 解压但 zip 格式不一定支持。我一般建议日志统一用 gzip 或者不压缩压缩格式和切片大小直接相关zip 会导致一个文件只能被一个 map 任务处理跑得极慢。6. 把分析结果接出去用 Sqoop 导出到 MySQL 并出图表Hive 算完的结果存在 HDFS 上运营和产品经理不会用命令行看数所以要导出到 MySQL再让报表平台去读。常见的做法是用 Sqoop 把 Hive 表的目录导出到 MySQL 的表中每天定时执行。先在 MySQL 里建好目标表字段类型要和 Hive 里的对应上再写导出命令。# 建 MySQL 表提前执行 # CREATE TABLE game_dau_report ( # dt DATE, dau INT, revenue DECIMAL(10,2), arpu DECIMAL(10,2), PRIMARY KEY (dt) # ); sqoop export \ --connect jdbc:mysql://localhost:3306/game_analysis \ --username root --password 123456 \ --table game_dau_report \ --export-dir /user/hive/warehouse/game_dau_report \ --input-fields-terminated-by \001 \ --update-mode allowinsert \ --update-key dt注意 Hive 内部表的默认字段分隔符是\001也就是^A字符不是|。如果你用--input-fields-terminated-by ,或者|Sqoop 解析出来的字段全错。--update-mode allowinsert的意思是如果主键存在就更新不存在就插入这样每天的定时任务重复执行也不会产生重复数据。导出完成后去 MySQLSELECT * FROM game_dau_report验证一下行数和 Hive 里SELECT COUNT(*)是否一致偶尔会因为字段里带了换行符或者特殊字符导致行数对不上。如果你嫌 Sqoop 太重还有一个轻量方案直接用 Hive 的INSERT OVERWRITE DIRECTORY把查询结果导出成 CSV 再走 Python 写库。哪种方式都行关键是把每天的调度串起来。我一般用 crontab 或者 Azkaban顺序是先跑 HiveQL 生成报表表再跑 Sqoop 导出到 MySQL最后报表平台比如 Metabase 或者自写的 ECharts 页面直接连 MySQL 拉数据。这样整条链路从日志到图表都是自动化的早上九点前运营就能看到昨天的数据。做这套系统的过程中我最大的教训是别急着优化性能先保证数据口径一致。伪分布式跑得慢不是问题但如果你 DAU 口径每天变、时区不统一再快的集群产出的数字也没人敢信。把日志格式约定好、日期统一成东八区、字段分隔符全链路保持一致这套流程哪怕数据量再翻十倍也只是把机器换大一点逻辑不用动。希望这个项目方向能帮你在 Hadoop 离线分析这条路上少踩几个坑顺利跑通自己的第一版游戏数据分析系统。本文还有配套的精品资源点击获取