<转>MapReduce实现的Join

本文介绍如何使用MapReduce实现大数据连接操作,包括Reduce Join和Map Join两种方式,并提供了一个具体的Map Join实例,展示了如何利用Hadoop的DistributedCache功能进行小表加载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MapReduce Join

对两份数据data1和data2进行关键词连接是一个很通用的问题,如果数据量比较小,可以在内存中完成连接。

如果数据量比较大,在内存进行连接操会发生OOM。mapreduce join可以用来解决大数据的连接。

1 思路

1.1 reduce join

在map阶段, 把关键字作为key输出,并在value中标记出数据是来自data1还是data2。因为在shuffle阶段已经自然按key分组,reduce阶段,判断每一个value是来自data1还是data2,在内部分成2组,做集合的乘积。

这种方法有2个问题:

1, map阶段没有对数据瘦身,shuffle的网络传输和排序性能很低。

2, reduce端对2个集合做乘积计算,很耗内存,容易导致OOM。

1.2 map join

两份数据中,如果有一份数据比较小,小数据全部加载到内存,按关键字建立索引。大数据文件作为map的输入文件,对map()函数每一对输入,都能够方便地和已加载到内存的小数据进行连接。把连接结果按key输出,经过shuffle阶段,reduce端得到的就是已经按key分组的,并且连接好了的数据。

这种方法,要使用hadoop中的DistributedCache把小数据分布到各个计算节点,每个map节点都要把小数据库加载到内存,按关键字建立索引。

这种方法有明显的局限性:有一份数据比较小,在map端,能够把它加载到内存,并进行join操作。

1.3 使用内存服务器,扩大节点的内存空间

针对map join,可以把一份数据存放到专门的内存服务器,在map()方法中,对每一个的输入对,根据key到内存服务器中取出数据,进行连接

1.4 使用BloomFilter过滤空连接的数据

对其中一份数据在内存中建立BloomFilter,另外一份数据在连接之前,用BloomFilter判断它的key是否存在,如果不存在,那这个记录是空连接,可以忽略。

1.5 使用mapreduce专为join设计的包

在mapreduce包里看到有专门为join设计的包,对这些包还没有学习,不知道怎么使用,只是在这里记录下来,作个提醒。

jar: mapreduce-client-core.jar

package: org.apache.hadoop.mapreduce.lib.join

2 实现map join

相对而言,map join更加普遍,下面的代码使用DistributedCache实现map join

2.1 背景

有客户数据customer和订单数据orders。

customer

客户编号 姓名 地址 电话
1 hanmeimei ShangHai 110
2 leilei BeiJing 112
3 lucy GuangZhou 119

** order**

订单编号 客户编号 其它字段被忽略
1 1 50
2 1 200
3 3 15
4 3 350
5 3 58
6 1 42
7 1 352
8 2 1135
9 2 400
10 2 2000
11 2 300

要求对customer和orders按照客户编号进行连接,结果要求对客户编号分组,对订单编号排序,对其它字段不作要求

客户编号 订单编号 订单金额 姓名 地址 电话
1 1 50 hanmeimei ShangHai 110
1 2 200 hanmeimei ShangHai 110
1 6 42 hanmeimei ShangHai 110
1 7 352 hanmeimei ShangHai 110
2 8 1135 leilei BeiJing 112
2 9 400 leilei BeiJing 112
2 10 2000 leilei BeiJing 112
2 11 300 leilei BeiJing 112
3 3 15 lucy GuangZhou 119
3 4 350 lucy GuangZhou 119
3 5 58 lucy GuangZhou 119
  1. 在提交job的时候,把小数据通过DistributedCache分发到各个节点。
  2. map端使用DistributedCache读到数据,在内存中构建映射关系--如果使用专门的内存服务器,就把数据加载到内存服务器,map()节点可以只保留一份小缓存;如果使用BloomFilter来加速,在这里就可以构建;
  3. map()函数中,对每一对,根据key到第2)步构建的映射里面中找出数据,进行连接,输出。

2.2 程序实现

public class Join extends Configured implements Tool {
    // customer文件在hdfs上的位置。
    // TODO: 改用参数传入
    private static final String CUSTOMER_CACHE_URL = "hdfs://hadoop1:9000/user/hadoop/mapreduce/cache/customer.txt";
    private static class CustomerBean {     
        private int custId;
        private String name;
        private String address;
        private String phone;
        
        public CustomerBean() {}
        
        public CustomerBean(int custId, String name, String address,
                String phone) {
            super();
            this.custId = custId;
            this.name = name;
            this.address = address;
            this.phone = phone;
        }

        
        
        public int getCustId() {
            return custId;
        }

        public String getName() {
            return name;
        }

        public String getAddress() {
            return address;
        }

        public String getPhone() {
            return phone;
        }
    }
    
    private static class CustOrderMapOutKey implements WritableComparable<CustOrderMapOutKey> {
        private int custId;
        private int orderId;

        public void set(int custId, int orderId) {
            this.custId = custId;
            this.orderId = orderId;
        }
        
        public int getCustId() {
            return custId;
        }
        
        public int getOrderId() {
            return orderId;
        }
        
        @Override
        public void write(DataOutput out) throws IOException {
            out.writeInt(custId);
            out.writeInt(orderId);
        }

        @Override
        public void readFields(DataInput in) throws IOException {
            custId = in.readInt();
            orderId = in.readInt();
        }

        @Override
        public int compareTo(CustOrderMapOutKey o) {
            int res = Integer.compare(custId, o.custId);
            return res == 0 ? Integer.compare(orderId, o.orderId) : res;
        }
        
        @Override
        public boolean equals(Object obj) {
            if (obj instanceof CustOrderMapOutKey) {
                CustOrderMapOutKey o = (CustOrderMapOutKey)obj;
                return custId == o.custId && orderId == o.orderId;
            } else {
                return false;
            }
        }
        
        @Override
        public String toString() {
            return custId + "\t" + orderId;
        }
    }
    
    private static class JoinMapper extends Mapper<LongWritable, Text, CustOrderMapOutKey, Text> {
        private final CustOrderMapOutKey outputKey = new CustOrderMapOutKey();
        private final Text outputValue = new Text();
        
        /**
         * 在内存中customer数据
         */
        private static final Map<Integer, CustomerBean> CUSTOMER_MAP = new HashMap<Integer, Join.CustomerBean>();
        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            
            // 格式: 订单编号 客户编号    订单金额
            String[] cols = value.toString().split("\t");           
            if (cols.length < 3) {
                return;
            }
            
            int custId = Integer.parseInt(cols[1]);     // 取出客户编号
            CustomerBean customerBean = CUSTOMER_MAP.get(custId);
            
            if (customerBean == null) {         // 没有对应的customer信息可以连接
                return;
            }
            
            StringBuffer sb = new StringBuffer();
            sb.append(cols[2])
                .append("\t")
                .append(customerBean.getName())
                .append("\t")
                .append(customerBean.getAddress())
                .append("\t")
                .append(customerBean.getPhone());
            
            outputValue.set(sb.toString());
            outputKey.set(custId, Integer.parseInt(cols[0]));
            
            context.write(outputKey, outputValue);
        }
        
        @Override
        protected void setup(Context context)
                throws IOException, InterruptedException {
            FileSystem fs = FileSystem.get(URI.create(CUSTOMER_CACHE_URL), context.getConfiguration());
            FSDataInputStream fdis = fs.open(new Path(CUSTOMER_CACHE_URL));
            
            BufferedReader reader = new BufferedReader(new InputStreamReader(fdis));
            String line = null;
            String[] cols = null;
            
            // 格式:客户编号  姓名  地址  电话
            while ((line = reader.readLine()) != null) {
                cols = line.split("\t");
                if (cols.length < 4) {              // 数据格式不匹配,忽略
                    continue;
                }
                
                CustomerBean bean = new CustomerBean(Integer.parseInt(cols[0]), cols[1], cols[2], cols[3]);
                CUSTOMER_MAP.put(bean.getCustId(), bean);
            }
        }
    }

    /**
     * reduce
     * @author Ivan
     *
     */
    private static class JoinReducer extends Reducer<CustOrderMapOutKey, Text, CustOrderMapOutKey, Text> {
        @Override
        protected void reduce(CustOrderMapOutKey key, Iterable<Text> values, Context context)
                throws IOException, InterruptedException {
            // 什么事都不用做,直接输出
            for (Text value : values) {
                context.write(key, value);
            }
        }
    }
    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            new IllegalArgumentException("Usage: <inpath> <outpath>");
            return;
        }
        
        ToolRunner.run(new Configuration(), new Join(), args);
    }

    @Override
    public int run(String[] args) throws Exception {
        Configuration conf = getConf();
        Job job = Job.getInstance(conf, Join.class.getSimpleName());
        job.setJarByClass(SecondarySortMapReduce.class);
        
        // 添加customer cache文件
        job.addCacheFile(URI.create(CUSTOMER_CACHE_URL));
        
        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        
        // map settings
        job.setMapperClass(JoinMapper.class);
        job.setMapOutputKeyClass(CustOrderMapOutKey.class);
        job.setMapOutputValueClass(Text.class);
        
        // reduce settings
        job.setReducerClass(JoinReducer.class);
        job.setOutputKeyClass(CustOrderMapOutKey.class);
        job.setOutputKeyClass(Text.class);
        
        boolean res = job.waitForCompletion(true);
        
        return res ? 0 : 1;
    }
}
运行环境
  • 操作系统: Centos 6.4
  • Hadoop: Apache Hadoop-2.5.0

==客户数据文件在hdfs上的位置硬编码为==
hdfs://hadoop1:9000/user/hadoop/mapreduce/cache/customer.txt, 运行程序之前先把客户数据上传到这个位置。

  • 程序运行结果

一、存储 PB 级环境监测数据的性能优化方案 技术工具:Hadoop 3.x 功能:分布式存储与资源管理 1.数据存储架构优化 为高效存储 PB 级环境监测数据,在原有 Hadoop 集群架构基础上,采用分层存储策略。将高频访问的实时监测数据和近期预测数据存储于 HDFS 的高速存储层,选用 SSD 硬盘作为存储介质,利用其快速读写特性,满足实时监测与短期预测对数据读取速度的严格要求。对于历史监测数据等低频访问数据,则迁移至 HDFS 的大容量存储层,使用 HDD 硬盘降低存储成本。同时,通过Hadoop的异构存储管理功能,实现不同存储层之间的数据自动分层与迁移,在保证性能的前提下,优化存储资源利用率。 (1)异构存储配置实现 Hadoop 3.x 支持异构存储策略,通过配Storage Policy可自动管理数据分层。在hdfs-site.xml中添加以下配置: <property> <name>dfs.storage.policy.enabled</name> <value>true</value> </property> <property> <name>dfs.storage.policy.names</name> <value>LAZY_PERSIST, ALL_SSD, ONE_SSD, HOT, WARM, COLD, ALL_DISK</value> </property> 创建存储策略映射表,将不同类型数据分配到对应存储层: # 为实时数据设置SSD存储策略 hdfs storagepolicies -setStoragePolicy -path /air_quality/real_time -policy ALL_SSD # 为历史数据设置HDD存储策略 hdfs storagepolicies -setStoragePolicy -path /air_quality/historical -policy COLD (2)自动分层迁移脚本 编写定期执行的数据分层迁移脚本,将超过 30 天的历史数据自动迁移至冷存储层: #!/usr/bin/env python3 import subprocess import datetime import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') def migrate_old_data(days_threshold=30): cutoff_date = (datetime.datetime.now() - datetime.timedelta(days=days_threshold)).strftime('%Y-%m-%d') # 获取所有超过阈值的文件 cmd = f"hdfs dfs -find /air_quality/data -type f -mtime +{days_threshold}" files = subprocess.check_output(cmd, shell=True).decode().strip().split('\n') for file in files: # 检查文件当前存储策略 policy = subprocess.check_output(f"hdfs storagepolicies -getStoragePolicy -path {file}", shell=True).decode().strip() # 如果不是冷存储策略,则迁移 if "COLD" not in policy: logging.info(f"Migrating {file} to COLD storage") subprocess.run(f"hdfs storagepolicies -setStoragePolicy -path {file} -policy COLD", shell=True) if __name__ == "__main__": migrate_old_data() 2.数据压缩与编码 在数据写入 HDFS 前,应用高效的数据压缩算法与编码技术。针对空气质量监测数据的特点,采用 Snappy 压缩算法,该算法在提供较高压缩比的同时,具备极快的压缩和解压缩速度,能够显著减少数据存储空间,降低数据传输过程中的网络带宽消耗。此外,对数值型的污染物浓度等数据,运用列式存储与 Run - Length Encoding(行程长度编码)相结合的方式,进一步提升数据压缩效果,加速数据分析与查询操作,为 PB 级数据的高效处理提供支持。 (1)压缩算法配置 在 Hadoop 集群中配置 Snappy 压缩算法,修改mapred-site.xml: <property> <name>mapreduce.map.output.compress</name> <value>true</value> </property> <property> <name>mapreduce.map.output.compress.codec</name> <value>org.apache.hadoop.io.compress.SnappyCodec</value> </property> <property> <name>mapreduce.output.fileoutputformat.compress</name> <value>true</value> </property> <property> <name>mapreduce.output.fileoutputformat.compress.codec</name> <value>org.apache.hadoop.io.compress.SnappyCodec</value> </property> (2)列式存储与编码实现 使用 Parquet 列式存储格式处理数值型监测数据,配置示例: from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("AirQualityDataProcessing") \ .getOrCreate() # 读取原始数据 df = spark.read.csv("/air_quality/raw_data/*.csv", header=True) # 应用RLE编码并存储为Parquet格式 df.write \ .option("compression", "snappy") \ .option("parquet.enable.dictionary", "true") \ .option("parquet.page.size", 1048576) \ .parquet("/air_quality/processed_data") 3.集群扩展与负载均衡 随着环境监测数据量的持续增长,制定动态集群扩展策略。当集群存储利用率达到预设阈值(如 80%)时,自动添加新的 DataNode 节点。通过 Hadoop 的自动发现机制,新节点能够快速加入集群,并自动同步元数据与数据块,实现无缝扩展。同时,优化 YARN 资源管理器的负载均衡算法,根据各节点的 CPU、内存、磁盘 I/O 等资源使用情况,动态分配任务,避免部分节点负载过高而影响整体性能,确保集群在处理 PB 级数据时始终保持高效稳定运行。 (1)动态扩缩容脚本 实现基于存储利用率的自动扩缩容机制: #!/usr/bin/env python3 import subprocess import json import logging import requests logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') # 阈值配置 STORAGE_THRESHOLD = 0.8 # 80%使用率触发扩容 INSTANCE_TEMPLATE = "hadoop-datanode-template" MAX_NODES = 50 def get_cluster_usage(): # 获取HDFS集群使用情况 cmd = "hdfs dfsadmin -report" output = subprocess.check_output(cmd, shell=True).decode() # 解析输出获取使用百分比 for line in output.split('\n'): if "DFS Used%" in line: usage = float(line.split(':')[1].strip().replace('%', '')) / 100 return usage return 0.0 def scale_cluster(): usage = get_cluster_usage() logging.info(f"Current cluster usage: {usage:.2%}") # 获取当前节点数 cmd = "hdfs dfsadmin -report | grep 'Live datanodes' | awk '{print $3}'" current_nodes = int(subprocess.check_output(cmd, shell=True).decode().strip()) if usage > STORAGE_THRESHOLD and current_nodes < MAX_NODES: # 计算需要添加的节点数 nodes_to_add = min(5, MAX_NODES - current_nodes) logging.info(f"Adding {nodes_to_add} new datanodes...") # 使用GCP Compute Engine API添加节点 cmd = f"gcloud compute instance-groups managed resize hadoop-datanodes --size={current_nodes + nodes_to_add}" subprocess.run(cmd, shell=True) # 等待节点加入集群 logging.info("Waiting for new nodes to join the cluster...") # 实际应用中应添加等待节点就绪的逻辑 if __name__ == "__main__": scale_cluster() (2)资源调度优化配置 在yarn-site.xml中优化资源调度配置: <property> <name>yarn.resourcemanager.scheduler.class</name> <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value> </property> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>air_quality, batch, default</value> </property> <property> <name>yarn.scheduler.capacity.root.air_quality.capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.air_quality.maximum-capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.resource-calculator</name> <value>org.apache.hadoop.yarn.util.resource.DominantResourceCalculator</value> </property> 三、方案实施效果评估与总结 1.性能评估 通过模拟 PB 级环境监测数据的写入、存储和查询操作,对存储方案的性能进行全面评估。在数据写入方面,测试不同数据规模下的写入速度与吞吐量,验证数据压缩和集群扩展策略对写入性能的提升效果;在数据查询环节,针对常见的空气质量指标查询、历史数据统计分析等场景,测量查询响应时间,评估数据编码和负载均衡优化对查询性能的影响。将评估结果与预设的性能指标进行对比,分析方案在处理 PB 级数据时的优势与不足,为后续优化提供依据。 (1)基准测试脚本 使用 Terasort 基准测试评估 HDFS 性能: #!/bin/bash # 配置测试参数 SIZE=10000000000 # 10GB数据 NUM_MAPS=20 NUM_REDS=5 INPUT_DIR=/benchmarks/terasort/input OUTPUT_DIR=/benchmarks/terasort/output RESULT_FILE=~/terasort_results.txt # 生成测试数据 echo "Generating test data..." hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar teragen -Dmapred.map.tasks=$NUM_MAPS $SIZE $INPUT_DIR # 执行排序测试 echo "Running terasort benchmark..." START_TIME=$(date +%s) hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar terasort -Dmapred.map.tasks=$NUM_MAPS -Dmapred.reduce.tasks=$NUM_REDS $INPUT_DIR $OUTPUT_DIR END_TIME=$(date +%s) # 计算并保存结果 ELAPSED_TIME=$((END_TIME - START_TIME)) echo "Terasort completed in $ELAPSED_TIME seconds" > $RESULT_FILE # 验证结果 echo "Validating results..." hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar teravalidate $OUTPUT_DIR $OUTPUT_DIR/validate # 清理测试数据 echo "Cleaning up..." hdfs dfs -rm -r -f $INPUT_DIR $OUTPUT_DIR (2)性能监控配置 配置 Ganglia 监控 Hadoop 集群性能: # gmetad.conf配置 data_source "hadoop_cluster" namenode1:8651 datanode1:8651 datanode2:8651 # gmond.conf配置示例 cluster { name = "hadoop_cluster" owner = "AirQualitySystem" latlong = "unknown" url = "unknown"} udp_send_channel { host = namenode1 port = 8649 ttl = 1} udp_recv_channel { port = 8649} tcp_accept_channel { port = 8651 } 2.总结 性能优化成果:通过实施上述优化方案,系统性能提升显著 存储效率:通过分层存储和压缩技术,存储成本降低 40%,存储容量利用率提升至 85% 处理速度:实时数据查询响应时间从平均 2.5 秒降至 0.8 秒,提升 68% 扩展性:动态扩缩容机制使集群可在 15 分钟内完成 5 节点扩展,满足突发数据增长需求 生成关于以上内容的代码运行截图。
最新发布
06-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值