ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Hadoop与Spark的大数据实战:英雄联盟排位赛阵容分析平台搭建

基于Hadoop与Spark的大数据实战:英雄联盟排位赛阵容分析平台搭建 做这个hadoopSpark基于Python的英雄联盟排位赛阵容分析平台起因其实很简单某段时间我排位连跪复盘时翻了几十场对局记录发现每场只能看到零散数据完全看不出阵容层面到底输在哪。当时正好在啃大数据生态就想着不如把排位对局数据全量落盘用Hadoop做底层存储、Spark做批量分析、Python做清洗和可视化大屏把这套阵容为什么赢、为什么输真正量化出来。这个平台做完之后效果超出预期不仅能算单套阵容的综合胜率还能拆出经济曲线、控制链、视野、前期节奏等十几个维度把阵容画像直接投到屏幕上。整个项目从环境搭建到数据分析再到可视化展示链路完整非常适合拿来做大数据技术栈的课程设计、毕设或工程练手。下面我把整个搭建和调试过程拆开讲能直接照着复现。1. 先想清楚排位阵容分析到底要解决什么问题1.1 为什么以阵容作为核心分析对象英雄联盟是一个5v5对抗游戏单场的胜负往往被解读成某个选手操作好或某波团战失误但站在大数据视角单场是噪声阵容才是可聚合的样本。同一套阵容在100场对局里表现如何哪些英雄组合在一起会显著拉高或拉低胜率这些规律只有通过海量对局才能显现。这个平台的出发点就是不做上帝视角的赛后复盘而是做统计学视角的阵容体检。我最终确定的分析对象是每场排位赛中的两个阵营按位置拆出五个英雄再关联对局时长、击杀、经济、防御塔、视野得分、小龙/大龙/先锋等客观指标。这样每一场对局就变成了一条结构化记录几十场作为样本不够看但几千场、上万分比赛聚合起来之后阵容的强弱画像就非常清晰了。1.2 想从数据里回答的问题清单在设计分析逻辑之前我先列了一张问题清单后续所有报表维度都围绕这些问题展开当前版本里哪些阵容组合的胜率明显高于整体均值某两个英雄同时出场时化学反应是正向还是负向阵容在前中期0-15分钟、中后期15-25分钟、后期25分钟以后的优劣势如何分布赢下对局的阵容在控制、开团、消耗、分带等标签上有没有共性输出位英雄的装备走向与团队经济分配是否存在可量化的规律这些问题表面上是游戏理解落到工程上就是一组聚合指标阵容胜率、经济效率、击杀贡献、控制链覆盖率、资源控制率。每一类指标都能用Hadoop生态的批处理链路串起来不需要实时计算T1跑批就够用。2. 平台架构与数据流转从原始对局到可视化大屏的一条链路2.1 技术选型每个组件只干它最擅长的事整个平台的技术栈看起来重但真拆开之后其实很清晰没有哪个组件在抢别人的活全是按数据流分工。链路环节选型职责数据采集与清洗Python解析原始对局数据做字段抽取、类型转换、异常过滤分布式存储Hadoop HDFS存放清洗前后的半结构化数据按目录分区批量分析与聚合Spark读取清洗数据跑SQL/DataFrame作业生成统计结果结果存储MySQL存放Spark产出的聚合结果供可视化层查询可视化呈现Pyecharts Flask ECharts提供大屏页面与数据接口这里有一个容易被忽略的点为什么不在清洗阶段直接做完整分析非要引入Hadoop和Spark我的理由是采集端Python跑的单机脚本处理几百MB没问题但一旦数据量到了几十GB、上百GB单机Pandas会直接内存爆炸。HDFS负责把大文件分布到多个节点Spark则把计算任务切成小任务并行执行。换句话说这套架构不是为了炫技而是为了数据量涨上去之后不用推翻重来。2.2 数据流转与目录约定我这次的数据链路是这样设计的Python采集脚本拿到原始JSON经过第一层清洗后写入HDFS的/lol/raw目录按赛季和日期分区。清洗脚本再次读取raw目录做字段规范化、缺失值处理输出为Parquet格式落到/lol/clean目录。Spark作业读取clean目录执行阵容聚合、英雄组合分析把结果写入MySQL的几张结果表。Flask后端提供/api/comp_stats、/api/hero_pair等接口大屏通过Ajax轮询拉数据。这套链路最舒服的地方在于每一层的数据都是物化过的中途任何一步挂了只需要重跑那一层不用从采集端重新拉一遍原始数据。3. Hadoop 环境搭建伪分布式快速起步集群模式按需切换3.1 伪分布式起步一台机器也能跑通全流程很多初学者一上来就在VMware里开三台虚拟机搭Hadoop集群结果网络配置搞了两天SSH免密没配好还没开始分析就先放弃了。我的建议是纯学习阶段先用伪分布式把全链路跑通再考虑要不要扩成集群。伪分布式就是在一台Linux机器上同时启动NameNode、DataNode、ResourceManager、NodeManager等进程。我用的环境是Ubuntu 20.04 JDK 8 Hadoop 3.3.x。需要提前做三件事配置JAVA_HOME、配置SSH localhost免密登录、保证/etc/hosts里主机名解析正常。ssh-keygen -t rsa -P -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys免密登录的重要性在于Hadoop启动脚本需要通过SSH在节点上拉起进程如果每次都要输密码脚本会卡住。这个问题虽然不起眼但确实是我第一次搭建时卡得最久的地方。3.2 配置文件的坑与启动检查Hadoop的配置集中在$HADOOP_HOME/etc/hadoop/目录下核心是五个文件core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml和workers。伪分布式模式下我只改了前三个。core-site.xml最关键的是fs.defaultFS它决定了HDFS的访问地址。我使用hdfs://localhost:9000configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/home/hadoop/tmp/value /property /configurationhdfs-site.xml里伪分布式必须把副本数dfs.replication设为1否则三份副本会占满磁盘。同时指定NameNode和DataNode的本地目录configuration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/home/hadoop/hdfs/name/value /property property namedfs.datanode.data.dir/name value/home/hadoop/hdfs/data/value /property /configuration启动前记得格式化NameNode否则会出现NameNode not formatted错误。格式化命令是hdfs namenode -format。这里有个小坑如果之前启动过Hadoop或者目录路径换过格式化的目录必须清空否则DataNode进程可能反复退出。启动和检查命令如下start-dfs.sh start-yarn.sh jpsjps输出里应该能看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager这五个进程。如果少了任何一个优先查看$HADOOP_HOME/logs/下的日志不要瞎猜。3.3 从伪分布式到HA集群Zookeeper要管的事平台开发阶段单节点完全够用。但如果后面要接到真实大规模场景就绕不开高可用。Hadoop高可用集群里NameNode不能单点故障所以核心思路是让两个NameNode组成主备Zookeeper负责检测主节点心跳、触发故障转移。我最初理解Zookeeper时绕了一个弯以为ZK是给HDFS存数据的后来才理清楚ZK只是当一个协调者存的是集群元数据的锁和状态信息。要做HA必须引入JournalNode来同步两个NameNode的元数据Zookeeper则确保同时只有一个Active节点对外服务。这一步是hadoop和zookeeper整合实战中最容易混的部分建议对照官方文档做两遍第一遍照抄第二遍自己画一遍状态流转图。不过回到这个平台本身伪分布式模式下不需要部署ZK先把Spark分析跑通再考虑集群化是更务实的路径。4. 数据采集与预处理脏数据怎么变成可用特征4.1 数据来源与原始JSON结构数据是一切分析的基础这里要强调合规性。平台使用的数据来源包括官方赛事公开数据、个人排位对局记录导出以及公开数据集。所有数据都只用于本地学习研究不涉及非公开接口或绕过限制的采集方式。原始对局数据通常是JSON结构核心字段大致是这样的{ game_id: 202502280001, game_duration: 1820, teams: [ { team_id: 100, win: true, players: [ { champion_id: 64, position: top, kills: 3, deaths: 5, assists: 12, gold_earned: 12500, vision_score: 42, total_damage: 52100 } ] } ] }这类JSON有几个通病字段命名五花八门、英雄ID对应关系分散在不同文件里、部分场次数据缺失。如果不做清洗直接丢给Spark后面聚合时会出现大量脏数据所以这一步必须放在HDFS和Spark之前。4.2 清洗规则与特征提取清洗脚本我用Python写核心逻辑是三层第一层是结构展开。把嵌套的JSON拍平成一张宽表一行代表一个玩家在一场比赛中的表现同时把队伍胜负、对局时长等场次级字段冗余带过来。第二层是字段规范化。英雄ID要转换成英雄名称装备ID要映射成装备名位置字段统一成top/jungle/mid/bottom/support五种写法时间单位统一为秒或分钟。第三层是异常过滤。对局时长小于5分钟的局基本是挂机和秒退直接过滤KDA中死亡数为0的情况要特殊处理不能直接除0金币、伤害为负数的记录属于脏数据删掉。我还顺手做了一步期切分按对局时间把数据切到early、mid、late三个时期对应0-15分钟、15-25分钟、25分钟以后。这样后期做阵容强弱趋势分析时不用在Spark里再算时间窗口。清洗示例代码如下import json import pandas as pd def parse_match(raw: dict) - list: rows [] duration raw[game_duration] for team in raw[teams]: for player in team[players]: rows.append({ game_id: raw[game_id], duration: duration, win: 1 if team[win] else 0, champion: champion_map.get(player[champion_id], unknown), position: player[position], kills: player[kills], deaths: player[deaths], assists: player[assists], gold: player[gold_earned], vision: player[vision_score], damage: player[total_damage], phase: cut_phase(duration) }) return rows清洗后的结果直接以Parquet格式写入HDFS因为Parquet是列式存储对Spark后续按列过滤和聚合非常友好比CSV小很多读取速度也快。这一步是很多教程不会强调的细节我实际对比过同样一批数据CSV路径下Spark SQL跑一个聚合要40秒Parquet路径只要12秒左右。4.3 HDFS分区与存储估算HDFS目录我设计成三层分区/lol/raw/{season}/{date}/ /lol/clean/{season}/{date}/ /lol/result/{analysis_type}/raw和clean按赛季、日期分区是因为排位数据天然带有时间属性按天分区后面做增量处理非常方便。result目录按分析类型分区比如comp_stats存放阵容统计hero_pair存放英雄组合分析。存储量方面可以估算一下一场排位对局的JSON大约几十KB清洗后的Parquet每行玩家级记录大约几百字节。按一个赛季5000场来算clean目录大概不到1GB伪分布式完全扛得住。如果按日增量接入多赛季数据再到HDFS扩容的时机集群化才有真正的必要性。5. Spark 分析核心用 SQL 把阵容变成可比较的画像5.1 阵容画像从对局明细到可比较的数值数据清洗完之后摆在Spark面前的就是一张宽表每行是一个玩家在一场比赛中的数据。但阵容是个团队概念需要从单个玩家表现提升到5人组合表现。我在平台上定义了四类核心画像指标基础胜率某个5人组合或某个英雄组合的胜场数/总场数资源控制率小龙/大龙/峡谷先锋的获取比例经济效率每金币投入转化为伤害的能力用总伤害/总金币衡量节奏强度通过首塔、一血、前期经济差来判断阵容是前期阵容还是后期阵容这些指标的共性在于全部基于比率或均值而不是绝对值。因为英雄联盟每个版本的节奏完全不同只有把数据归一化成比率才能在不同版本之间做横向比较。5.2 Spark SQL 实现单场聚合与英雄联动Spark 分析我直接用PySpark SQL写开发体验很接近写普通SQL但底层是分布式执行。核心任务有两个阵容胜率统计和英雄组合关联分析。阵容胜率统计相对直接按5个英雄组合分组统计胜负。由于数据是玩家级别的需要先用窗口函数把同队的5个英雄拼起来from pyspark.sql import SparkSession from pyspark.sql.window import Window from pyspark.sql import functions as F spark SparkSession.builder \ .appName(lol_comp_analysis) \ .master(yarn) \ .getOrCreate() df spark.read.parquet(hdfs://localhost:9000/lol/clean/*.parquet) # 用窗口函数给每个队伍内的玩家按位置排序拼出阵容ID w Window.partitionBy(game_id, win).orderBy(position) comp_df df.withColumn( comp_id, F.concat_ws(-, F.collect_list(champion).over(w)) ) comp_stats comp_df.groupBy(comp_id, win).count() \ .groupBy(comp_id) \ .agg( F.sum(count).alias(total), F.sum(F.when(F.col(win) 1, F.col(count)).otherwise(0)).alias(wins) ) \ .withColumn(win_rate, F.col(wins) / F.col(total))英雄组合关联分析的逻辑更绕一点。要算英雄A和英雄B同时出场时的胜率就不能只按队伍聚合需要把同队英雄两两配对。这一步我用DataFrame自关联把同一场、同一队、不同位置的英雄拆成两两组合pair_df df.alias(a).join( df.alias(b), (F.col(a.game_id) F.col(b.game_id)) (F.col(a.win) F.col(b.win)) (F.col(a.position) F.col(b.position)), inner ).select( F.col(a.game_id), F.col(a.win), F.col(a.champion).alias(hero_a), F.col(b.champion).alias(hero_b) ) pair_stats pair_df.groupBy(hero_a, hero_b, win).count() \ .groupBy(hero_a, hero_b) \ .agg( F.sum(count).alias(total), F.sum(F.when(F.col(win) 1, F.col(count)).otherwise(0)).alias(wins) ) \ .withColumn(win_rate, F.col(wins) / F.col(total)) \ .filter(F.col(total) 30) # 样本量太少没有统计意义最后这一步total 30的过滤非常重要。如果组合只出场了3次胜率是100%也没有参考价值必须设置最低样本门槛。5.3 输出表结构与执行参数Spark作业产出的结果写入MySQL的三张核心表tbl_comp_stats5英雄组合的出场次数、胜率、平均对局时长tbl_hero_pair英雄两两组合的联动胜率与场次tbl_phase_stats不同时期early/mid/late的各阵容关键指标均值当Spark作业任务较重时Executors的资源配置也很关键。我第一次在YARN上跑聚合时默认参数直接把集群内存打爆了后来改成按数据量估算spark-submit \ --master yarn \ --deploy-mode cluster \ --num-executors 4 \ --executor-memory 4g \ --executor-cores 2 \ analysis_comp.py对于几GB的数据4个4G内存的Executor通常够用。如果数据量更大优先加Executor数量而不是单Executor内存因为单Executor内存过大会导致GC压力。6. 可视化大屏把分析结果摆上前台6.1 大屏布局业务指标怎么摆才有逻辑大数据项目最后如果没有一个看得见的呈现很容易被当成纯后台作业。可视化大屏的作用是把Spark跑出来的结果以最小理解成本展示给非技术背景的人。布局我采用了经典的三栏式顶部是一条KPI指标带展示总对局数、平均胜率、最具统治力阵容、当前版本登场英雄数。左侧是一个柱状图展示登场率Top10的英雄下面接一个表格展示阵容胜率Top10。中间核心区是一张雷达图画当前选中阵容在输出、控制、经济、视野、防御等维度上的得分再往下是英雄组合联动热力图。右侧放一个折线图展示不同时期前期/中期/后期阵容胜率的变化趋势。真正把大屏从好看变有用的关键是给图表之间加联动。比如点击左侧的英雄柱状图中间的雷达图就切换成包含该英雄的阵容画像。这个联动效果用的是ECharts的事件回调前端代码量不大但演示效果提升非常明显。6.2 Flask Pyecharts 的接法我用Pyecharts在Python侧直接生成图表配置然后由Flask提供数据接口前端负责请求和渲染。有一个常见的坑是Pyecharts生成的是HTML页面如果直接把它塞进大屏框架样式会很乱。我采用的方式是后端只返回ECharts需要的JSON配置前端拿到配置后自己初始化图表实例。后端接口示例from flask import Flask, jsonify from pyspark.sql import SparkSession app Flask(__name__) app.route(/api/hero_pair) def hero_pair(): spark SparkSession.builder.getOrCreate() df spark.read.jdbc( urljdbc:mysql://localhost:3306/lol_analysis, tabletbl_hero_pair, properties{user: root, password: ***} ) rows df.limit(50).toPandas().to_dict(orientrecords) return jsonify({code: 0, data: rows})前端每5秒轮询一次这个接口拉到新数据后更新图表。由于结果表是T1更新的轮询频率不需要太高否则反而是资源浪费。6.3 缓存、刷新与部署细节大屏上线后遇到一个比较影响体验的问题每次刷新页面所有图表要重新请求后端盯着屏幕看的人会看到一片白屏。解决办法很简单后端加一层缓存Spark结果写入MySQL后用Redis缓存接口响应缓存有效期设为10分钟。部署方面我用Nginx托管静态HTML页面Flask作为后端服务跑在5000端口通过Nginx反向代理把/api/转发到Flask。这样前端静态资源和API请求都是同一域名避免跨域问题。实际踩过的坑是如果直接用Flask托管静态页面大屏的图表资源加载会很慢用Nginx之后明显顺畅了。7. 调试复盘这套平台最容易踩的坑7.1 资源调度与Spark作业稳定性整个平台跑下来问题最集中的阶段就是Spark作业在YARN上的资源管理。我遇到过两类典型错误。第一类是ExecutorLostFailure日志里能看到某个Executor被NodeManager杀掉通常是单Executor内存超限。排查思路是先看Spark UI上的Event Timeline确认是哪一步触发了内存飙升。我在做英雄组合自关联时中间有一个巨大的Shuffle如果不做df.repartition(partitions)控制并行度很容易在Reduce阶段炸掉。第二类是Container ... running beyond virtual memory limits。YARN默认会把物理内存和虚拟内存都纳入限制Python的PySpark进程有时虚拟内存很高需要在yarn-site.xml里调整property nameyarn.nodemanager.vmem-check-enabled/name valuefalse/value /property不过这只是绕过限制根本解法还是减小单案并行任务的内存压力。7.2 数据质量与版本兼容问题对局数据里藏着很多版本陷阱。比如英雄联盟版本更新后某个英雄的重做会导致ID变化有些英雄会被禁用或者删除如果清洗脚本里维护的映射表不及时更新分析结果就会串。我的解决方案是把英雄映射表放在HDFS上的一个版本化目录里每天跑批前先检查映射表版本如果版本对不上就触发一次映射更新流程。还有一个很琐碎但很致命的坑个别字段可能是null在Spark里做concat_ws拼阵容ID时null值会被拼成空串导致两场完全不同的对局共用同一个comp_id。后来我统一在清洗阶段把所有null都设置为unknown才彻底解决这个问题。中文乱码也遇到过Pyecharts生成图表默认字体对中文支持没问题但Linux服务器本地缺中文字体时ECharts的标题会显示成方块。解决办法是往服务器装fonts-wqy-microhei然后重新生成缓存。7.3 大屏呈现的适配与交互细节大屏往往要在不同分辨率的显示器上投放如果写死像素宽度换台设备就乱套。我采用的办法是先按1920×1080设计再用CSS的transform: scale()根据屏幕实际宽高做等比缩放。这样字体和图表的相对位置能保持一致。另一个交互细节是轮询超时。如果Spark正在重跑结果表MySQL里的数据可能被锁后端接口响应变慢前端如果默认超时时间太短会频繁报错。我在前端把请求超时时间调到了15秒并且做了错误重试连续失败3次才提示数据加载失败避免用户看一眼大屏就刷出满屏报错。这套平台做完之后我个人最大的体会是把游戏数据和分布式计算放在一起恰恰是理解大数据实战链路成本最低的方式。因为游戏数据的规模和复杂性都很亲民不需要企业级数据量就能把HDFS、Spark、可视化大屏完整串起来。而在这个过程中踩过的每一个坑——从Hadoop的进程起不来到Spark的Executor爆炸再到ECharts的中文乱码——都变成了后续做其他项目时可以快速调用的经验。如果你也想做一个能跑通全链路的大数据项目不妨就从这个阵容分析平台开始走一遍比看十遍教程都有用。
返回列表