/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.aliyun.odps.mapred; import java.io.IOException; import com.aliyun.odps.Column; import com.aliyun.odps.data.Record; import com.aliyun.odps.data.TableInfo; import com.aliyun.odps.mapred.conf.JobConf; /** * Mapper 处理输入表的记录对象 {@link Record},加工处理成键值对集合(若 {@link Reducer} 个数不为0)输出到 * {@link Reducer} ,或者直接输出结果记录. * * <p> * Mapper 由{@link JobConf#setMapperClass(Class)}进行指定. * </p> * <p> * 对于 Mapper 存在两种情况: * <ul> * <li>若 {@link Reducer} 数为0,这种作业也称为 Map-only 作业。 * <li>若 {@link Reducer} 数不为0,Mapper 生成键值对输出到 Reducer,这些键值对从 Mapper 输出到 Reducer * 的中间过程也称为 shuffle。 * </ul> * Reducer 数通过 {@link JobConf#setNumReduceTasks(int)} 指定,默认 Reducer 数为 Map 数的 * 1/4,如果是 Map-only 作业,需要显式指定 Reducer 数目为零:job.setNumReduceTasks(0); * </p> * * <p> * Mapper 包括三个方法: * <ul> * <li>{@link #setup(TaskContext)} 在任务开始, map 方法之前调用,每个map调用且仅调用一次。 * <li>{@link #cleanup(TaskContext)} 在任务结束,map 方法之后调用,每个map调用且仅调用一次。 * <li>{@link #map(long, Record, TaskContext)} map 方法,每次调用传入一条记录,见 run 方法的默认实现 * </ul> * 上述方法都会传入一个上下文对象,Mapper 实现时可以调用上下文对象的方法,编写代码时最好能多参考下 {@link TaskContext} * 及其父类的接口<br/> * <br/> * 自定义 Mapper 可以重载上述方法。 * * <br/> * 对于某些应用,重载 run 方法更合适一些,例如 SleepJob 重载了 run 方法。<br/> * <br/> * <strong><font color="red"> 注意:传给 map 方法的 Record 对象会被上下文对象复用!</font></strong> * <br/> * </p> * * <p> * Mapper 可以通过以下几个方法输出计算结果: * <ul> * <li>{@link TaskContext#write(Record)} 直接输出计算结果到默认输出表 * <li>{@link TaskContext#write(Record, String)} 直接输出计算结果到指定 label 的输出表 * <li>{@link TaskContext#write(Record, Record)} 输出中间计算结果(键值对)到 Reducer,此方法要求 * Reducer 数大于0,否则抛出OperationUnsupportedException。 * </ul> * </p> * * <p> * 键值对通过下面两个方法进行设置。有关Column的定义,见{@link Column}: * <ul> * <li>{@link JobConf#setMapOutputKeySchema(Column[])} * <li>{@link JobConf#setMapOutputValueSchema(Column[])} * </ul> * </p> * <p> * 在运行时 Mapper 和 Reducer 可能会起多个任务并发执行,中间结果键值对输出到哪个 Reducer 进行处理由 Partition * Columns 决定,用户可以通过 {@link JobConf#setPartitionColumns(String[])} * 指定。注意Partition Columns 必须包含在中间结果的键(key)中。 * </p> * * <p> * 在客户端定义的作业配置({@link JobConf})可以通过 {@link TaskContext#getJobConf()} 方法取得。 * </p> * * <p> * 代码示例,摘自 WordCount: * * <pre> * public static class TokenizerMapper extends MapperBase { * * Record word; * Record one; * * @Override * public void setup(TaskContext context) throws IOException { * word = context.createMapOutputKeyRecord(); * one = context.createMapOutputValueRecord(); * one.setBigint(0, 1L); * } * * @Override * public void map(long recordNum, Record record, TaskContext context) throws IOException { * for (int i = 0; i < record.getColumnCount(); i++) { * String[] words = record.get(i).toString().split("\\s+"); * for (String w : words) { * word.setString(0, w); // word.set(new Object[]{w}) * context.write(word, one); * } * } * } * } * </pre> * * </p> * * @see TaskContext * @see Reducer */ public interface Mapper { /** * 传给{@link Mapper}的上下文对象. */ public interface TaskContext extends com.aliyun.odps.mapred.TaskContext { /** * 获取当前输入记录序号,记录序号从1开始. * * @return 当前Map输入记录序号 */ public long getCurrentRecordNum(); /** * 获取当前输入记录. * * 注意:此方法返回的Record对象在{@link #nextRecord()}方法里会被复用 * * @return 当前输入记录 */ public Record getCurrentRecord(); /** * 向前读取下一条记录,如果还有未读取的记录,返回true,否则返回false. * * 读取的记录可以通过{@link #getCurrentRecord()}取得 * * @return 如果还有未读取的记录,返回true,否则返回false */ public boolean nextRecord(); /** * 获取Map的输入表或分区的信息. * * <p> * ODPS M/R 程序支持多路输入,输入可以是表或分区,本方法返回的是当前 {@link Mapper} 所负责处理的表或分区的信息. * </p> * <p> * 例如,输入是一个分区表 t,该表有两级四个分区: * <ul> * <li>pt=1/ds=1 * <li>pt=1/ds=2 * <li>pt=2/ds=1 * <li>pt=2/ds=2 * </ul> * </p> * 如果每个分区都有数据,即使数据量非常小,也至少会划分成 4 个 {@link Mapper} 来跑,每个 {@link Mapper} * 可以通过本方法获取当前分区的信息({@link TableInfo}) */ public TableInfo getInputTableInfo(); } /** * 在任务开始, map 方法之前调用. * * @param context * Map 上下文对象 * @throws IOException */ void setup(TaskContext context) throws IOException; /** * map 方法,处理输入表的记录. * * <p> * <strong><font color="red"> 注意:传给 map 方法的 Record 对象会被上下文对象复用, * 即每次map方法调用都传入相同的Record对象。若需要保存上一条记录的数据,使用{@link Record#toArray()} * </font></strong> * * @param key * 当前的记录序号 * @param record * 当前记录对象 * @param context * Map 上下文对象 * @throws IOException */ void map(long key, Record record, TaskContext context) throws IOException; /** * 在任务结束,map 方法之后调用. * * @param context * Map 上下文对象 * @throws IOException */ void cleanup(TaskContext context) throws IOException; }