简介:基于Hadoop的疾病信息统计平台是一份面向大数据学习者与Java开发者的完整项目源码,重点解决医疗疾病数据从采集、存储到分布式分析与可视化的工程实现问题。压缩包约10.87MB,共41个文件,以25个Java源文件为核心,配合6个XML配置、2个properties配置文件、2个jar包以及yml、arff等辅助文件,覆盖Maven工程结构、运行脚本、数据集样本与日志配置等典型模块。目前已有84人学习下载。项目展现出清晰的Hadoop生态整合思路:HDFS负责底层存储,MapReduce承担并行计算,同时可看到HBase实时查询、Hive数据仓库、YARN资源调度等组件的配置痕迹;arff文件提供可加载的疾病数据集,便于直接运行验证。源码中涉及Java面向对象、多线程、异常处理及MapReduce编程模型,适合正在做课程设计、毕业设计或希望上手Hadoop实战的读者参考,也可作为搭建类似统计平台的脚手架与排错蓝本。
1. 基于Hadoop的疾病信息统计平台:拆开压缩包之后,先想清楚这三件事
如果你拿到的是一份叫"基于hadoop的疾病信息统计平台.zip"的项目,第一反应通常是解压、找文档、看代码。但我建议你先别急着点开,先想清楚三件事:第一,这个平台的核心不是网站界面,而是Hadoop集群上的统计计算链路;第二,疾病信息统计的数据特征是条数多、维度杂、按时间持续累积,这正是HDFS加MapReduce/Hive的典型适用场景;第三,你要交付的不只是能跑的代码,而是一套从原始数据到统计报表的完整流水线。这篇文章就按这个思路,把环境搭建、表设计、ETL清洗、作业调优到结果落地的整条路走一遍,中间标注哪些地方最容易翻车。适合正在做Hadoop课程设计、或者想把大数据技术落到医疗数据统计场景的从业者。
2. 为什么疾病统计会选Hadoop:数据特征与平台架构拆解
2.1 疾病登记数据的四个特征,决定了技术选型
疾病信息统计跟普通业务系统不一样。普通业务系统是"一个用户一条记录",查询以点查为主;而疾病统计是"一段时间内全量记录聚合",查询以面查为主。具体拆开看有四个特征:
第一,量大。地市级疾控中心或三甲医院的信息科,一天新增的门诊、住院、传染病上报记录可以达到几十万条。放到一个区县级别,几年下来就是几亿条。MySQL单表在亿级数据上做 group by 不是不能跑,但跑一次全量周报要几分钟甚至更久,而且会把业务库的IO拖垮。
第二,维度多。一张疾病登记表至少涉及病种编码、患者年龄、性别、户籍地、发病地、就诊医院等级、报告日期、确诊日期、转归情况。任何一个维度都可能要单独出统计口径,组合起来就是几十组聚合SQL。
第三,脏数据比例不低。医院HIS系统导出的数据里,病种编码有空值、年龄字段出现负数、日期格式不统一、同一患者重复登记,这些在真实数据里几乎天天见。
第四,统计有周期性。日报、周报、月报、季度研判,每次都是全量重算。这种"批量计算"模型,跟MapReduce、Hive这种离线批处理引擎天然匹配。
基于这四点,HDFS做存储层、MapReduce或Hive做计算层、结果物化到MySQL供报表查询,是这条技术方向上最常见的成熟方案。不是说MySQL不能做,而是在数据量过亿、统计维度又多又杂的场景下,Hadoop的横向扩展能力和离线批量计算模型更省心。
2.2 平台模块划分:采集、存储、计算、服务四层
我一般会把一个Hadoop统计平台按职责拆成四层,这样不管是写文档还是后面维护,边界都很清楚。
采集层负责把医院HIS导出文件、上报系统的CSV、手工填报的Excel统一转换成带分隔符的文本文件,落到HDFS的原始目录。这个环节可以用Flume监听目录,也可以用shell脚本配合hdfs put命令,看现场环境而定。
存储层是HDFS,按日期和数据类型分目录,比如 /disease/raw/2025/01/ 下面放原始文件,/disease/clean/ 放清洗后的数据。注意HDFS不适合存大量小文件,原始文件如果都是一两MB的碎片,后面计算会很痛苦,这点到第5章避坑部分再细说。
计算层用MapReduce或Hive完成清洗去重、维度归并、指标聚合。课程设计阶段用Hive SQL最省时间,因为写一遍SQL就能出结果,不用像原生MapReduce那样写几百行Java。如果平台里已经接了Tez或者Spark,底层引擎可以换,但Hive SQL这层对外接口可以保持不变。
服务层把计算结果导出到MySQL或者直接生成CSV,给报表前端或者BI工具查询。Hadoop这边提供的是批量计算能力,不是实时查询,所以一定不要想着让前端直接查HDFS,延迟和并发都顶不住。
2.3 Hadoop组件选型:HDFS、YARN、MapReduce、Hive和ZooKeeper的取舍
一个完整的Hadoop生态,组件可以很多,但做疾病统计平台,真正离不开的就五个:HDFS、YARN、MapReduce、Hive(元数据库)、ZooKeeper。
HDFS负责存原始数据和中间结果;YARN负责调度计算资源;MapReduce是默认的计算引擎;Hive把SQL翻译成MapReduce作业,降低统计SQL的编写成本。ZooKeeper分两种情况:如果集群是单NameNode的伪分布式或完全分布式,ZooKeeper不是必须的;但如果要做NameNode高可用,也就是两个NameNode一主一备自动切换,那就一定要搭ZooKeeper。
这里有个常见的认知误区:以为Hadoop必须装ZooKeeper才能跑。实际上伪分布式单机环境里,不装ZooKeeper完全没问题,Hive的元数据默认存Derby或者换成MySQL也行。真正需要ZooKeeper的是YARN ResourceManager HA、HDFS NameNode HA这些场景。如果只是课程设计,用单NameNode加一个SecondaryNameNode做检查点备份就够了,别把架构搞复杂。
组件选型的取舍上,我建议是:Hive表的元数据库用MySQL,不要用Derby。Derby的问题是同一时间只允许一个Hive会话访问元数据,你用JDBC跑一次、再用beeline跑一次,很可能直接报锁冲突。把元数据库切到MySQL是一劳永逸的做法,在后面搭建章节会给出具体配置。
3. 在本地把Hadoop跑起来:伪分布式环境搭建与第一个统计任务
3.1 环境准备:JDK版本、免密登录和时钟同步
搭建之前先确认基础环境。JDK一定要用Hadoop官方支持版本,比如Hadoop 3.3.x配JDK 8或JDK 11。这里踩过坑的人不少:JDK版本太高,Hadoop的HDFS启动时直接报UnsupportedClassVersionError;版本太低,又可能遇到Crypto代码的兼容问题。建议装JDK 8,稳定省事。
第二件事是免密登录。伪分布式虽然只有一台机器,但启动脚本默认会用SSH连接localhost启动各个进程,不配免密的话,启动时会卡住要求输密码。执行:
# 生成密钥对,一路回车即可 ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa # 把公钥加到authorized_keys cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys # 验证免密是否生效 ssh localhost 'echo ok'输出 ok 说明免密成功了。这里 -P '' 表示空密码,省略以后SSH不用输密码。注意伪分布式和完全分布式都必须做这一步,很多Hadoop启动失败的案例就出在SSH这一步:start-dfs.sh 脚本明明已经执行,但DataNode一直没有启动,去日志里看全是Connection refused。
第三件事是时钟同步。虽然单机伪分布式不明显,但后面扩到多节点时,节点间时钟差超过阈值,HDFS会判定节点失联。可以用ntpdate同步一次,再配crontab定期同步。疾病统计里时间字段特别重要,如果各节点时间不一致,按天分区统计时数据可能落到前一天,这个隐患很隐蔽。
3.2 核心配置文件:core-site.xml、hdfs-site.xml、yarn-site.xml
Hadoop的配置集中在 etc/hadoop/ 目录下,伪分布式最少改三个文件。先看core-site.xml:
<configuration> <!-- 指定NameNode的RPC地址,伪分布式一般就用localhost --> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <!-- HDFS文件系统临时目录,需要手动创建并给权限 --> <property> <name>hadoop.tmp.dir</name> <value>/data/hadoop/tmp</value> </property> </configuration>fs.defaultFS 是客户端访问HDFS的入口地址,端口9000是NameNode的RPC端口,别改成8020还是什么的纠结,保持默认就行。hadoop.tmp.dir 是元数据存储的根目录,一定要手动新建并确保当前用户有写权限,Hadoop不会自动创建。
再看hdfs-site.xml:
<configuration> <!-- 伪分布式只有一台机器,副本数必须设为1,否则DataNode会一直尝试复制到第二个节点 --> <property> <name>dfs.replication</name> <value>1</value> </property> <!-- 关闭权限检查,开发环境省去chown的麻烦 --> <property> <name>dfs.permissions.enabled</name> <value>false</value> </property> </configuration>如果这里不把副本数改成1,伪分布式下文件状态会一直是 under replicated,虽然不影响读取,但每次运行hdfs dfsadmin -report都看到一堆警告。另外dfs.permissions.enabled关掉是给课程设计环境省事,生产环境别关。
然后是yarn-site.xml:
<configuration> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <!-- 给NodeManager分配可用内存,单位是MB,伪分布式按机器实际内存来 --> <property> <name>yarn.nodemanager.resource.memory-mb</name> <value>8192</value> </property> </configuration>aux-services配成mapreduce_shuffle是让YARN能跑MapReduce作业的必要条件,漏配的话作业提交后Container会一直处于INIT状态然后失败。memory-mb的值看机器内存,我本地机器是16G内存就给8G,给太多会把系统本身的内存挤爆。
3.3 HDFS命令集:把疾病登记文件传上去
配置改完后,第一次启动HDFS之前要格式化NameNode。这个操作要谨慎,它会在namenode目录下初始化元数据,重复执行会清掉旧的元数据信息,导致DataNode和NameNode的集群ID不一致,后面启动报错。
# 格式化NameNode,只有首次安装或元数据损坏时才执行 hdfs namenode -format # 启动HDFS和YARN start-dfs.sh start-yarn.sh # 用jps看进程,有NameNode、DataNode、ResourceManager、NodeManager就基本正常 jps格式化日志里会出现 Storage directory ... has been successfully formatted,看到这句再继续。不要迷信某些教程说"每次改完配置都要格式化",那是翻车的常见原因——格式化太多次会让DataNode的namespaceID对不上NameNode,日志里疯狂报Incompatible clusterIDs。
进程起来后,把疾病数据文件传上去:
# 创建HDFS原始数据目录,按日期分层管理 hdfs dfs -mkdir -p /disease/raw/2025/01 # 把本地的门诊登记CSV上传到对应日期目录 hdfs dfs -put ./outpatient_202501.csv /disease/raw/2025/01/ # 确认文件落位 hdfs dfs -ls -R /disease/raw上传完成后,可以顺手跑一个MapReduce自带的WordCount做冒烟测试,确认整个计算链路通不通。但上传的文件如果数据量太小,只有几十KB,Map任务可能只有1个,这是正常的,别急着调参数。
3.4 用Hive做疾病统计的第一条SQL
Hive装完以后,第一步是把元数据库从默认Derby切到MySQL。做法是在hive-site.xml里配置javax.jdo.option.ConnectionURL、ConnectionDriverName、ConnectionUserName、ConnectionPassword,然后在MySQL里建好metastore库和账号。
启动Hive初始化:
# 初始化元数据库结构,Derby换MySQL后必须执行一次 schematool -dbType mysql -initSchema # 启动Hive服务,然后进入beeline或hive命令行 hive --service metastore & hive在Hive命令行里建一张外部表指向HDFS上的疾病原始数据。外部表的意思是Hive只管表结构映射,不管数据文件生命周期,drop表不会删HDFS文件。这一点在原始数据管理上很重要。
CREATE EXTERNAL TABLE IF NOT EXISTS disease_raw ( patient_id STRING, disease_code STRING, disease_name STRING, province STRING, city STRING, age INT, gender STRING, report_date STRING ) ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE LOCATION '/disease/raw/2025/01';建表后跑一条最简单的统计——按省份统计病例数:
SELECT province, COUNT(*) AS cnt FROM disease_raw GROUP BY province ORDER BY cnt DESC;这条SQL会被翻译成MapReduce作业,Map端按province分组,Reduce端做计数汇总。如果表里数据量是几十万条,跑起来应该一到两分钟就结束。如果卡了很久,回去检查YARN的资源参数,大概率是内存给少了。
4. 疾病统计平台的表设计与ETL:从原始登记到可计算的维度表
4.1 源数据字段长什么样,目标表怎么设计
医院或疾控导出的原始登记表,常见字段大致是:流水号、患者姓名(部分脱敏)、身份证号、性别、出生日期、年龄、疾病ICD编码、疾病名称、诊断日期、报告日期、户籍省市区、就诊医院名称、医院等级、联系电话之类。真实场景里还会混入备注文本,里面什么都有。
设计Hive表时,我建议把字段分成三类。标识类:patient_id、report_no,用来去重;维度类:disease_code、province、city、gender、age_group、hospital_level,用来group by;事实类:report_date、confirm_date,用来做时间窗口。不要把备注、详细信息这些自由文本一股脑塞进统计表,既占存储又容易在序列化时引入解析错误。
年龄字段建议在ETL阶段就换算成年龄分组,比如 0-17、18-44、45-64、65及以上。因为在SQL里每次动态算年龄再分组,代码重复不说,口径分散在各个SQL里,后面业务方问一句"你们年龄分段标准是什么"还得去翻代码。提前物化成一个age_group字段,所有统计SQL直接 group by 它,口径统一。
4.2 建立Hive外部表:用日期分区管理增量数据
统计平台的数据是持续增长的,每天都有新文件上传,所以目标表一定要做分区。最常见的就是按天分区,分区字段叫dt,类型是STRING,值就是 yyyy-MM-dd。
CREATE EXTERNAL TABLE IF NOT EXISTS disease_clean ( patient_id STRING, disease_code STRING, disease_name STRING, province STRING, city STRING, gender STRING, age_group STRING, hospital_level STRING ) PARTITIONED BY (dt STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\001' STORED AS TEXTFILE;这里有个细节:字段分隔符我用了 \001,也就是ASCII的1号控制符。为什么不用逗号?因为疾病名称、医院名称这些字段里可能自带逗号,用逗号分隔极容易错位。\001在真实文本里几乎不会出现,是Hive社区常用的安全分隔符。数据加载时,原始CSV要先经过清洗脚本把逗号换成\001,再加载进表。
建表之后,要手动把分区的元数据注册进去:
# 为dt='2025-01-15'创建分区 hdfs dfs -mkdir -p /disease/clean/dt=2025-01-15 # 将清洗好的数据文件传入分区目录 hdfs dfs -put ./clean_20250115.csv /disease/clean/dt=2025-01-15/ # 在Hive中注册分区,让表能读到这个目录的数据 ALTER TABLE disease_clean ADD PARTITION (dt='2025-01-15');注意顺序:先建目录、放文件,再ADD PARTITION。如果先ADD PARTITION然后发现文件没传对,修复起来更麻烦。Hive不会自动扫描HDFS上新出现的目录,除非开msck repair table,但手动管理分区更直观,尤其在自动化脚本里,每一步都可控。
4.3 ETL清洗:去重、空值、非法日期
清洗逻辑是统计平台正确性的生命线。我基于真实落地经验,把清洗分成四步,每一步在Hive SQL里对应一段明确逻辑。
第一步去重。同一患者同一天因同一种疾病可能登记两次,按业务规则保留最早一条即可:
INSERT OVERWRITE TABLE disease_clean PARTITION (dt='2025-01-15') SELECT patient_id, disease_code, disease_name, province, city, gender, age_group, hospital_level FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY patient_id, disease_code, report_date ORDER BY report_no) AS rn FROM disease_raw ) t WHERE rn = 1;这里 ROW_NUMBER() 按 patient_id、disease_code、report_date 分组,组内按 report_no 排序,rn=1 保留的就是同一天同病种的第一条记录。这个写法在数据量几亿的时候会触发一次全量排序,但这是必要的,去重不做,后续所有指标都会虚高。
第二步处理空值。病种编码为空或者为NULL的,直接算无效数据,统计时排除;年龄为空且无法推算的,归到"未知"组,不要直接丢弃,因为业务方想知道到底有多少数据是缺失的。
-- 过滤严格无效的数据,保留年龄缺失但病种有效的记录 INSERT OVERWRITE TABLE disease_clean PARTITION (dt='2025-01-16') SELECT ... FROM disease_raw WHERE disease_code IS NOT NULL AND disease_code != '';第三步处理非法日期。report_date不是标准格式的,要么用正则截取,要么直接置NULL。日期错误的数据在按周/月统计时会落到错误的分区,这个错误非常隐蔽。第四步统一编码。疾病名称可能有"乙肝"和"乙型肝炎"两种写法,统计时按ICD编码归并,不能按中文名group by。
4.4 统计指标口径:发病率、地区分布、年龄分层
清洗完数据,统计SQL就相对简单了。按月统计各地区新发病例数:
SELECT substr(dt, 1, 7) AS month, province, COUNT(*) AS case_cnt FROM disease_clean WHERE dt >= '2025-01-01' AND dt <= '2025-01-31' GROUP BY substr(dt, 1, 7), province ORDER BY month, case_cnt DESC;如果要做发病率,需要一张人口基数维度表,按省、市、年存常住人口数。发病率 = 新发病例数 / 该地区人口数 × 100000,这是流行病学里常用的十万分率口径。注意人口表是按年更新的,Join时要按统计年份关联,不要用最新一年的人口数去算过去年份的发病率,口径会偏。
年龄分层统计也一样,直接group by age_group:
SELECT age_group, gender, COUNT(*) AS case_cnt FROM disease_clean WHERE dt = '2025-01-15' GROUP BY age_group, gender;到这一步,平台的统计能力已经打通了。但千万记住一点:Hive表里的数据是原始快照,不是实时更新的,统计结果是否可信,取决于ETL清洗规则和分区管理是否严格。这也是为什么下一章要把最常见的故障场景单独拿出来讲。
5. Hadoop平台排查与避坑:五个让新手翻车的点
5.1 NameNode启动失败:格式化两次导致的集群ID错乱
现象:start-dfs.sh 执行后,NameNode进程起来了,jps也能看到,但客户端执行 hdfs dfs -ls / 一直报 Connection refused,查看NameNode日志发现 Incompatible clusterIDs。
原因:安装过程中多次执行 hdfs namenode -format。每次格式化都会重新生成一个集群ID,但DataNode首次启动后就把旧集群ID存在自己的data目录里。下一次NameNode用了新集群ID,DataNode还用旧ID,两边对不上,NameNode拒绝让DataNode注册,整个集群实际处于半瘫痪状态。
解决:最稳妥的办法是清空NameNode和DataNode的数据目录,也就是hadoop.tmp.dir配置的目录,然后只格式化一次再重启。操作前确认数据已备份。
# 停掉所有Hadoop进程 stop-dfs.sh; stop-yarn.sh # 删掉元数据和数据目录,注意目录路径要和core-site.xml里配置一致 rm -rf /data/hadoop/tmp mkdir -p /data/hadoop/tmp # 重新格式化,注意:数据会被清空,只适合开发环境 hdfs namenode -format start-dfs.sh && start-yarn.sh血的教训:别一遇到启动失败就格式化。正确顺序永远是先看日志,日志在 $HADOOP_HOME/logs/ 目录下,hadoop-hdfs-namenode-主机名.log。绝大多数NameNode起不来的问题都是配置路径写错、端口被占或者权限不足,格式化是最后手段。
5.2 小文件太多导致Map任务数量爆炸
现象:往HDFS传了上千个几KB的CSV文件,跑一次统计SQL,Map任务启动了几百上千个,每个任务执行时间只有几秒,大部分时间浪费在任务调度和启动Container上,整个作业跑了很久还没结束。
原因:MapReduce默认一个文件或一个文件块对应一个Map任务。大量小文件意味着大量Map任务,而每个Map任务的启动开销远大于任务本身的执行时间。疾病数据从医院HIS导出时经常一个科室一个文件,攒一年就是几千个小文件。
解决:两个层面。入口层,上传前先合并文件,把一天的多个CSV合并成一个文本文件再put;计算层,在Hive里开小文件合并参数:
-- 控制Map输入合并,让多个小文件合并成一个分片 SET hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat; -- 控制Map端输出小文件合并 SET hive.merge.mapfiles=true; SET hive.merge.size.per.task=256000000;第一行参数最关键,CombineHiveInputFormat会把同一目录下多个小文件合并成一个逻辑分片,Map任务数从几百降到个位数。后面两个是Hive在作业结束时合并输出小文件的参数,target大小按256MB设置。这也是为什么我在前面强调原始目录按日期分层,因为EASILY合并时按目录处理最顺手。
5.3 中文乱码:Hive表字段和源文件编码不一致
现象:从CSV导入的数据,查询时中文全部显示为乱码,或者统计出来的省份名称是????。
原因:源CSV文件是GBK编码,而Hive表默认按UTF-8解析,两边不一致导致字节解析错误。
解决:第一步把源文件统一转码后再上传,这是最彻底的办法。用iconv命令:
# 把GBK编码的文件转成UTF-8,避免Hive读取时乱码 iconv -f GBK -t UTF-8 ./outpatient_20250115.csv > ./outpatient_20250115_utf8.csv第二步是建表时显式声明字符集。如果是外部表配合MySQL元数据库,还要确认MySQL的连接参数里 characterEncoding 配成 utf8,否则Hive的元数据里的注释、分区信息也会乱。第三步是如果数据已经进HDFS了,不要指望Hive能把脏数据变干净,得从源头清洗重跑。
这里有个很多人忽略的细节:用OpenCSVSerde还是LazySimpleSerDe。OpenCSVSerde天然处理CSV里的引号转义,但对编码的处理能力和普通SerDe一样,都要源文件编码正确。不要以为换了SerDe就能解决乱码,治本还是统一转UTF-8。
5.4 数据倾斜:按地区统计时单个Reduce卡死
现象:一条统计SQL,99%的Map任务都跑完了,但有一个Reduce任务长时间卡在99%,作业迟迟不结束。日志显示某个Reduce吸收了上千万条key。
原因:数据分布不均。比如按省份统计病例数时,某些人口大省的病例数可能是小省的几十倍,Hash分区把大量key分到同一个Reduce,这个Reduce成为瓶颈。疾病统计里,某些高发病种或特定地区的key倾斜尤其严重。
解决:第一招,给倾斜的key加随机前缀打散,统计完再去掉前缀。比如按省份统计时,对省份字段拼接一个随机数:
SELECT province, SUM(case_cnt) AS total_cnt FROM ( SELECT case when province = 'XX省' then concat(province, '_', floor(rand()*10)) else province end as province, COUNT(*) AS case_cnt FROM disease_clean WHERE dt >= '2025-01-01' GROUP BY province, CASE WHEN province = 'XX省' THEN concat(province, '_', floor(rand()*10)) ELSE province END ) t GROUP BY province;内层先把热点省份的数据随机拆成10份,分别聚合,外层再汇总一次。注意rand()乘以10的范围决定了拆分的份数,热点越严重,这个数值越大。第二招是调整Reduce个数,set mapreduce.job.reduces=200 把数据分散到更多Reduce上,缓解但不根治。第三招是用Hive的SKEW JOIN hint,但那是针对Join场景的,group by倾斜仍然得靠加盐手法。血泪经验:数据倾斜不会在测试环境暴露,因为测试数据量小且均匀,生产数据一上来就现原形。设计阶段就要评估团伙key的体量差异。
5.5 YARN容器被kill:内存参数配置不合理
现象:作业提交后,Container反复被NodeManager杀死,日志报 Container killed on request. Exit code is 143 或者物理内存超限。
原因:YARN的虚拟内存检测或物理内存检测把容器杀掉了。伪分布式下最常见的情况是,机器物理内存16G,给NodeManager的resource.memory-mb配了12G,同时预留的内存不足,系统内存吃紧后NodeManager开始杀Container。另一个原因是mapreduce.map.memory.mb和reduce内存参数没配,MapReduce默认的1G内存跑大文件时不够。
解决:先把总预算算清楚。NodeManager的可用内存不能超过物理内存减去系统运行所需,通常留25%-30%给系统。然后按比例配Map和Reduce内存:
# yarn-site.xml中,NodeManager可用内存给物理内存的70%左右 # mapreduce配置中,Map任务内存给1-2G,Reduce任务给2-4G在mapred-site.xml中:
<property> <name>mapreduce.map.memory.mb</name> <value>1536</value> </property> <property> <name>mapreduce.reduce.memory.mb</name> <value>3072</value> </property> <!-- 关闭虚拟内存检测,开发环境常用手段 --> <property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property>虚拟内存检测关掉能解决一部分莫名被kill的问题,因为JVM在Linux上占用的虚拟内存常常远超物理内存,检测一开就容易误杀。但关闭检测只是治标,Map和Reduce实际所需内存得根据数据量调。一个小技巧:一个Map任务默认处理128MB数据,如果每个Map都处理大量复杂计算,1536MB不够就提到2048MB或更大,同时保证NodeManager的总量能覆盖所有并发任务的和,否则内存会超卖。
6. 结果物化、调度与校验:把统计结果交到业务手上
6.1 把Hive统计结果导出到MySQL
Hadoop这边的统计结果在HDFS上,业务系统不会直接来查。常见的落地做法是用Sqoop把Hive表或者指定查询结果导出到MySQL:
sqoop export \ --connect jdbc:mysql://localhost:3306/disease_report \ --username root --password ****** \ --table stats_month_province \ --export-dir /disease/result/month_province \ --fields-terminated-by '\001' \ --batchexport-dir指向的是Hive结果表在HDFS上的目录,--fields-terminated-by必须跟Hive表的分隔符一致。如果环境里没有Sqoop,就退一步:把结果INSERT OVERWRITE到一个指定目录,然后用 hdfs dfs -getmerge 合并下载,再用mysql客户端load进去。Sqoop导出时最常见的问题就是MySQL表字段顺序跟结果文件列顺序不一致,导出前先用一条同名SQL查一下结果目录的文件头,确认列顺序再执行。
6.2 定时调度:crontab加shell脚本是零成本方案
疾病统计的日报月报是周期性任务。生产环境可以用Apache Airflow或者DolphinScheduler做可视化调度,但课程设计或小团队场景,crontab加一套shell脚本反而更实用,没有额外组件要维护。
# 每天凌晨2点跑前一天的数据ETL和统计 0 2 * * * /home/hadoop/bin/run_disease_report.sh脚本内部按四步走:上传原始文件到HDFS分区;调用Hive SQL跑清洗;调用统计SQL生成结果表;调用Sqoop导出到MySQL。每一步结束后检查退出码,失败就写日志并退出,避免出现"上游失败了,下游跑出空结果"的连锁问题。
6.3 结果校验:不要相信第一次跑出来的数字
平台上线前,一定要做结果校验。我的做法是选两个已知结果的月份,比如某地区官方发布的月度发病数,用平台重算一遍,对不上就查口径差异。另外随机抽某个分区数据,用原生SQL在MySQL里手工group by一遍,跟Hive结果做diff。Hadoop平台最容易出的问题不是算错,而是口径不一致:比如去重逻辑改了,但老分区的数据没有重跑,导致月度累加和日报对不上。
我个人的习惯是:每个统计结果表都带一个run_date字段,记录生成该结果的批次日期;任何时候重算,都写入新的分区,不覆盖旧结果。这样业务方看到哪个批次跑出来的数据,回溯也有据可查。这套习惯帮我挡过不止一次"数据对不上"的质疑。希望帮到你。
本文还有配套的精品资源,点击获取