大数据技术之_18_04
二十、数据分析20.1、统计表20.2、目标20.3、代码实现20.3.1、Mapper20.3.2、Reducer20.3.3、Runner20.3.4、自定义 OutPutFormat20.3.5、测试二十一、Hive 之 hourly 分析21.1、目标21.2、目标解析21.3、创建 Mysql 结果表21.4、Hive 分析21.4.1、创建 Hive 外部表,关联 HBase 数据表21.4.2、创建临时表用于存放 pageview 和 launch 事件的数据(即存放过滤数据)21.4.3、提取 e_pv 和 e_l 事件数据到临时表中21.4.4、创建分析结果临时保存表21.4.5、分析活跃访客数21.4.6、分析会话长度21.4.7、创建最终结果表21.4.8、向结果表中插入数据21.4.9、使用 Sqoop 导出 数据到 Mysql,观察数据21.5、定时任务流程二十二、常用 Maven 仓库地址
二十、数据分析
20.1、统计表

通过表结构可以发现,只要维度id确定了,那么 new_install_users 也就确定了。
20.2、目标
按照不同维度统计新增用户。比如:将 日、周、月 新增用户统计出来。传入的时间参数是: -date 2017-08-14
20.3、代码实现
20.3.1、Mapper
-
Step1、创建 NewInstallUsersMapper 类,outputKey 为 StatsUserDimension,outputValue 为 Text。定义全局变量,Key 和 Value 的对象。
-
Step2、覆写 map 方法,在该方法中读取 HBase 中待处理的数据,分别要包含维度的字段信息以及必有的字段信息。比如:serverTime、platformName、platformVersion、browserName、browserVersion、uuid。
-
Step3、数据过滤以及时间字符串转换。
-
Step4、构建维度信息:天维度,周维度,月维度,platform 维度[(name, version)(name, all)(all, all)],browser 维度[(browser, all) (browser, version)]。
-
Step5、设置 outputValue 的值为 uuid。
-
Step6、按照不同维度设置 outputKey。
-
Step7、将封装好的数据写入到 Mapper 的上下文对象中,输出给 Reducer。
示例代码如下:
NewInstallUsersMapper.java
package com.z.transformer.mr.statistics;
import java.io.IOException;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.TableMapper;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.log4j.Logger;
import com.z.transformer.common.DateEnum;
import com.z.transformer.common.EventLogConstants;
import com.z.transformer.common.GlobalConstants;
import com.z.transformer.common.KpiType;
import com.z.transformer.dimension.key.base.BrowserDimension;
import com.z.transformer.dimension.key.base.DateDimension;
import com.z.transformer.dimension.key.base.KpiDimension;
import com.z.transformer.dimension.key.base.PlatformDimension;
import com.z.transformer.dimension.key.stats.StatsCommonDimension;
import com.z.transformer.dimension.key.stats.StatsUserDimension;
import com.z.transformer.util.TimeUtil;
/**
* 思路:思路:HBase 读取数据 --> HBaseInputFormat --> Mapper --> Reducer --> DBOutPutFormat--> 这接写入到 MySql 中
*
* @author bruce
*/
public class NewInstallUserMapper extends TableMapper<StatsUserDimension, Text> {
// Mapper 的 OutPutKey 和 OutPutValue
// OutPutKey = StatsUserDimension 进行用户分析的组合维度(用户基本分析维度和浏览器分析维度)
// OutPutValue = Text uuid(字符串)
private static final Logger logger = Logger.getLogger(NewInstallUserMapper.class);
// 定义列族
private byte[] family = EventLogConstants.BYTES_EVENT_LOGS_FAMILY_NAME;
// 定义输出 key
private StatsUserDimension outputKey = new StatsUserDimension();
// 定义输出 value
private Text outputValue = new Text();
// 映射输出 key 中的 StatsCommonDimension(公用维度) 属性,方便后续封装操作
private StatsCommonDimension statsCommonDimension = this.outputKey.getStatsCommon();
private long date, endOfDate; // 定义运行天的起始时间戳和结束时间戳
private long firstThisWeekOfDate, endThisWeekOfDate; // 定义运行天所属周的起始时间戳和结束时间戳
private long firstThisMonthOfDate, firstDayOfNextMonth; // 定义运行天所属月的起始时间戳和结束时间戳
// 创建 kpi 维度对象
private KpiDimension newInstallUsersKpiDimension = new KpiDimension(KpiType.NEW_INSTALL_USER.name);
private KpiDimension browserNewInstallUsersKpiDimension = new KpiDimension(KpiType.BROWSER_NEW_INSTALL_USER.name);
// 定义一个特殊占位的浏览器维度对象
private BrowserDimension defaultBrowserDimension = new BrowserDimension("", "");
// 初始化操作
@Override
protected void setup(Mapper<ImmutableBytesWritable, Result, StatsUserDimension, Text>.Context context)
throws IOException, InterruptedException {
// 1、获取参数配置项的上下文
Configuration conf = context.getConfiguration();
// 2、获取我们给定的运行时间参数,获取运行的是哪一天的数据
String date = conf.get(GlobalConstants.RUNNING_DATE_PARAMES);
// 传入时间所属当前天开始的时间戳,即当前天的0点0分0秒的毫秒值
this.date = TimeUtil.parseString2Long(date);
// 传入时间所属当前天结束的时间戳
this.endOfDate = this.date + GlobalConstants.DAY_OF_MILLISECONDS;
// 传入时间所属当前周的第一天的时间戳
this.firstThisWeekOfDate = TimeUtil.getFirstDayOfThisWeek(this.date);
// 传入时间所属下一周的第一天的时间戳
this.endThisWeekOfDate = TimeUtil.getFirstDayOfNextWeek(this.date);
// 传入时间所属当前月的第一天的时间戳
this.firstThisMonthOfDate = TimeUtil.getFirstDayOfThisMonth(this.date);
// 传入时间所属下一月的第一天的时间戳
this.firstDayOfNextMonth = TimeUtil.getFirstDayOfNextMonth(this.date);
}
@Override
protected void map(ImmutableBytesWritable key, Result value, Context context)
throws IOException, InterruptedException {
// 1、获取属性,参数值,即读取 HBase 中的数据:serverTime、platformName、platformVersion、browserName、browserVersion、uuid
String serverTime = Bytes
.toString(value.getValue(family, Bytes.toBytes(EventLogConstants.LOG_COLUMN_NAME_SERVER_TIME)));
String platformName = Bytes
.toString(value.getValue(family, Bytes.toBytes(EventLogConstants.LOG_COLUMN_NAME_PLATFORM)));
String platformVersion = Bytes
.toString(value.getValue(family, Bytes.toBytes(EventLogConstants.LOG_COLUMN_NAME_VERSION)));
String browserName = Bytes
.toString(value.getValue(family, Bytes.toBytes(EventLogConstants.LOG_COLUMN_NAME_BROWSER_NAME)));
String browserVersion = Bytes
.toString(value.getValue(family, Bytes.toBytes(EventLogConstants.LOG_COLUMN_NAME_BROWSER_VERSION)));
String uuid = Bytes
.toString(value.getValue(family, Bytes.toBytes(EventLogConstants.LOG_COLUMN_NAME_UUID)));
// 2、针对数据进行简单过滤(实际开发中过滤条件更多)
if (StringUtils.isBlank(platformName) || StringUtils.isBlank(uuid)) {
logger.debug("数据格式异常,直接过滤掉数据:" + platformName);
return; // 过滤掉无效数据
}
// 属性处理
long longOfServerTime = -1;
try {
longOfServerTime = Long.valueOf(serverTime); // 将字符串转换为long类型
} catch (Exception e) {
logger.debug("服务器时间格式异常:" + serverTime);
return; // 服务器时间异常的数据直接过滤掉
}
// 3、构建维度信息
// 获取当前服务器时间对应的当天维度的对象
DateDimension dayOfDimension = DateDimension.buildDate(longOfServerTime, DateEnum.DAY);
// 获取当前服务器时间对应的当周维度的对象
DateDimension weekOfDimension = DateDimension.buildDate(longOfServerTime, DateEnum.WEEK);
// 获取当前服务器时间对应的当月维度的对象
DateDimension monthOfDimension = DateDimension.buildDate(longOfServerTime, DateEnum.MONTH);
// 还可以获取 当季维度、当年维度......
// 构建平台维度对象
List<PlatformDimension> platforms = PlatformDimension.buildList(platformName, platformVersion);
// 构建浏览器维度对象
List<BrowserDimension> browsers = BrowserDimension.buildList(browserName, browserVersion);
// 4、设置 outputValue
this.outputValue.set(uuid);
// 5、设置 outputKey
for (PlatformDimension pf : platforms) {
// 设置浏览器维度(是个空的)
this.outputKey.setBrowser(this.defaultBrowserDimension);
// 设置平台维度
this.statsCommonDimension.setPlatform(pf);
// 下面的代码是处理对应于 stats_user 表的统计数据
// 设置 kpi 维度
this.statsCommonDimension.setKpi(this.newInstallUsersKpiDimension);
// 处理不同时间维度的情况
// 处理天维度数据,要求服务器时间处于指定日期的范围:[today, endOfDate)
if (longOfServerTime >= date && longOfServerTime < endOfDate) {
// 设置时间维度为服务器时间当天的维度
this.statsCommonDimension.setDate(dayOfDimension);
// 输出数据
context.write(outputKey, outputValue);
}
// 处理周维度数据,范围:[firstThisWeekOfDate, endThisWeekOfDate)
if (longOfServerTime >= firstThisWeekOfDate && longOfServerTime < endThisWeekOfDate) {
// 设置时间维度为服务器时间所属周的维度
this.statsCommonDimension.setDate(weekOfDimension);
// 输出数据
context.write(outputKey, outputValue);
}
// 处理月维度数据,范围:[firstThisMonthOfDate, firstDayOfNextMonth)
if (longOfServerTime >= firstThisMonthOfDate && longOfServerTime < firstDayOfNextMonth) {
// 设置时间维度为服务器时间所属月的维度
this.statsCommonDimension.setDate(monthOfDimension);
// 输出数据
context.write(outputKey, outputValue);
}
// 下面的代码是处理对应于 stats_device_browser 表的统计数据
// 设置 kpi 维度
this.statsCommonDimension.setKpi(this.browserNewInstallUsersKpiDimension);
for (BrowserDimension br : browsers) {
// 设置浏览器维度
this.outputKey.setBrowser(br);
// 处理不同时间维度的情况
// 处理天维度数据,要求当前事件的服务器时间处于指定日期的范围内,[今天0点, 明天0点)
if (longOfServerTime >= date && longOfServerTime < endOfDate) {
// 设置时间维度为服务器时间当天的维度
this.statsCommonDimension.setDate(dayOfDimension);
// 输出数据
context.write(outputKey, outputValue);
}
// 处理周维度数据,范围:[firstThisWeekOfDate, endThisWeekOfDate)
if (longOfServerTime >= firstThisWeekOfDate && longOfServerTime < endThisWeekOfDate) {
// 设置时间维度为服务器时间所属周的维度
this.statsCommonDimension.setDate(weekOfDimension);
// 输出数据
context.write(outputKey, outputValue);
}
// 处理月维度数据,范围:[firstThisMonthOfDate, firstDayOfNextMonth)
if (longOfServerTime >= firstThisMonthOfDate && longOfServerTime < firstDayOfNextMonth) {
// 设置时间维度为服务器时间所属月的维度
this.statsCommonDimension.setDate(monthOfDimension);
// 输出数据
context.write(outputKey, outputValue);
}
}
}
}
}
20.3.2、Reducer
-
Step1、创建
NewInstallUserReducer<StatsUserDimension, Text, StatsUserDimension, MapWritableValue>类,覆写 reduce 方法。 -
Step2、统计 uuid 出现的次数,并且去重。
-
Step3、将数据拼装到 outputValue 中。
-
Step4、设置数据业务 KPI 类型,最终输出数据。
维度类结构图
我们再来回顾下大数据离线平台架构图:

示例代码如下:
NewInstallUserReducer.java
package com.z.transformer.mr.statistics;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.MapWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import com.z.transformer.common.KpiType;
import com.z.transformer.dimension.key.stats.StatsUserDimension;
import com.z.transformer.dimension.value.MapWritableValue;
public class NewInstallUserReducer extends Reducer<StatsUserDimension, Text, StatsUserDimension, MapWritableValue> {
// 保存唯一 id 的集合 Set,用于计算新增的访客数量
private Set<String> uniqueSets = new HashSet<String>();
// 定义输出 value
private MapWritableValue outputValue = new MapWritableValue();
@Override
protected void reduce(StatsUserDimension key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
// 1、统计 uuid 出现的次数,去重
for (Text uuid : values) { // 增强 for 循环,遍历 values
this.uniqueSets.add(uuid.toString());
}
// 2、输出数据拼装
MapWritable map = new MapWritable();
map.put(new IntWritable(-1), new IntWritable(this.uniqueSets.size()));
this.outputValue.setValue(map);
// 3、设置 outputValue 数据对应描述的业务指标(kpi)
if (KpiType.BROWSER_NEW_INSTALL_USER.name.equals(key.getStatsCommon().getKpi().getKpiName())) {
// 表示处理的是 browser new install user kpi 的计算
this.outputValue.setKpi(KpiType.BROWSER_NEW_INSTALL_USER);
} else if (KpiType.NEW_INSTALL_USER.name.equals(key.getStatsCommon().getKpi().getKpiName())) {
// 表示处理的是 new install user kpi 的计算
this.outputValue.setKpi(KpiType.NEW_INSTALL_USER);
}
// 4、输出数据
context.write(key, outputValue);
}
}
20.3.3、Runner
-
Step1、创建 NewInstallUserRunner 类,实现 Tool 接口。
-
Step2、添加时间处理函数,用来截取参数。
-
Step3、组装 Job。
-
Step4、设置 HBase InputFormat(设置从 HBase 中读取的数据都有哪些)。
-
Step5、自定义 OutPutFormat 并设置。
示例代码如下:
NewInstallUserRunner.java
package com.z.transformer.mr.statistics;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
import org.apache.hadoop.hbase.filter.Filter;
import org.apache.hadoop.hbase.filter.FilterList;
import org.apache.hadoop.hbase.filter.MultipleColumnPrefixFilter;
import org.apache.hadoop.hbase.filter.SingleColumnValueFilter;
import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import com.z.transformer.common.EventLogConstants;
import com.z.transformer.common.EventLogConstants.EventEnum;
import com.z.transformer.common.GlobalConstants;
import com.z.transformer.dimension.key.stats.StatsUserDimension;
import com.z.transformer.dimension.value.MapWritableValue;
import com.z.transformer.mr.TransformerMySQLOutputFormat;
import com.z.transformer.util.TimeUtil;
public class NewInstallUserRunner implements Tool {
// 给定一个参数表示参数上下文
private Configuration conf = null;
public static void main(String[] args) {
try {
int exitCode = ToolRunner.run(new NewInstallUserRunner(), args);
if (exitCode == 0) {
System.out.println("运行成功");
} else {
System.out.println("运行失败");
}
System.exit(exitCode);
} catch (Exception e) {
System.err.println("执行异常:" + e.getMessage());
}
}
@Override
public void setConf(Configuration conf) {
// 添加自己开发环境所有需要的其他资源属性文件
conf.addResource("transformer-env.xml");
conf.addResource("output-collector.xml");
conf.addResource("query-mapping.xml");
// 创建 HBase 的 Configuration 对象
this.conf = HBaseConfiguration.create(conf);
}
@Override
public Configuration getConf() {
return this.conf;
}
@Override
public int run(String[] args) throws Exception {
// 1、获取参数上下文对象
Configuration conf = this.getConf();
// 2、处理传入的参数,将参数添加到上下文中
this.processArgs(conf, args);
// 3、创建 Job
Job job = Job.getInstance(conf, "new_install_users");
// 4、设置 Job 的 jar 相关信息
job.setJarByClass(NewInstallUserRunner.class);
// 5、设置 IntputFormat 相关配置参数
this.setHBaseInputConfig(job);
// 6、设置 Mapper 相关参数
// 在 setHBaseInputConfig 已经设置了
// 7、设置 Reducer 相关参数
job.setReducerClass(NewInstallUserReducer.class);
job.setOutputKeyClass(StatsUserDimension.class);
job.setOutputValueClass(MapWritableValue.class);
// 8、设置 OutputFormat 相关参数,使用一个自定义的 OutputFormat
job.setOutputFormatClass(TransformerMySQLOutputFormat.class);
// 9、Job 提交运行
boolean result = job.waitForCompletion(true);
// 10、运行成功返回 0,失败返回 -1
return result ? 0 : -1;
}
/**
* 处理时间参数,如果没有传递参数的话,则默认清洗前一天的。
*
* Job脚本如下: bin/yarn jar ETL.jar com.z.transformer.mr.etl.AnalysisDataRunner -date 2017-08-14
*
* @param args
*/
private void processArgs(Configuration conf, String[] args) {
String date = null;
for (int i = 0; i < args.length; i++) {
if ("-date".equals(args[i])) {
if (i + 1 < args.length) {
date = args[i + 1];
break;
}
}
}
// 查看是否需要默认参数
if (StringUtils.isBlank(date) || !TimeUtil.isValidateRunningDate(date)) {
date = TimeUtil.getYesterday(); // 默认时间是昨天
}
// 保存到上下文中间
conf.set(GlobalConstants.RUNNING_DATE_PARAMES, date);
}
/**
* 设置从 hbase 读取数据的相关配置信息
*
* @param job
* @throws IOException
*/
private void setHBaseInputConfig(Job job) throws IOException {
Configuration conf = job.getConfiguration();
// 获取已经执行ETL操作的那一天的数据
String dateStr = conf.get(GlobalConstants.RUNNING_DATE_PARAMES); // 2017-08-14
// 因为我们要访问 HBase 中的多张表,所以需要多个 Scan 对象,所以创建 Scan 集合
List<Scan> scans = new ArrayList<Scan>();
// 开始构建 Scan 集合
// 1、构建 Hbase Scan Filter 对象
FilterList filterList = new FilterList();
// 2、构建只获取 Launch 事件的 Filter
filterList.addFilter(new SingleColumnValueFilter(
EventLogConstants.BYTES_EVENT_LOGS_FAMILY_NAME, // 列族
Bytes.toBytes(EventLogConstants.LOG_COLUMN_NAME_EVENT_NAME), // 事件名称
CompareOp.EQUAL, // 等于判断
Bytes.toBytes(EventEnum.LAUNCH.alias))); // Launch 事件的别名
// 3、构建部分列的过滤器 Filter
String[] columns = new String[] {
EventLogConstants.LOG_COLUMN_NAME_PLATFORM, // 平台名称
EventLogConstants.LOG_COLUMN_NAME_VERSION, // 平台版本
EventLogConstants.LOG_COLUMN_NAME_BROWSER_NAME, // 浏览器名称
EventLogConstants.LOG_COLUMN_NAME_BROWSER_VERSION, // 浏览器版本
EventLogConstants.LOG_COLUMN_NAME_SERVER_TIME, // 服务器时间
EventLogConstants.LOG_COLUMN_NAME_UUID, // 访客唯一标识符 uuid
EventLogConstants.LOG_COLUMN_NAME_EVENT_NAME // 确保根据事件名称过滤数据有效,所以需要该列的值
};
// 创建 getColumnFilter 方法用于得到 Filter 对象
// 根据列名称过滤数据的 Filter
filterList.addFilter(this.getColumnFilter(columns));
// 4、数据来源表所属日期是哪些
long startDate, endDate; // Scan 的表区间属于[startDate, endDate)
long date = TimeUtil.parseString2Long(dateStr); // 传入时间所属当前天开始的时间戳,即当前天的0点0分0秒的毫秒值
long endOfDate = date + GlobalConstants.DAY_OF_MILLISECONDS; // 传入时间所属当前天结束的时间戳
long firstDayOfWeek = TimeUtil.getFirstDayOfThisWeek(date); // 传入时间所属当前周的第一天的时间戳
long lastDayOfWeek = TimeUtil.getFirstDayOfNextWeek(date); // 传入时间所属下一周的第一天的时间戳
long firstDayOfMonth = TimeUtil.getFirstDayOfThisMonth(date); // 传入时间所属当前月的第一天的时间戳
long lastDayOfMonth = TimeUtil.getFirstDayOfNextMonth(date); // 传入时间所属下一月的第一天的时间戳
// 选择最小的时间戳作为数据输入的起始时间,date 一定大于等于其他两个 first 时间戳值
// 获取起始时间
startDate = Math.min(firstDayOfMonth, firstDayOfWeek);
// 获取结束时间
endDate = TimeUtil.getTodayInMillis() + GlobalConstants.DAY_OF_MILLISECONDS;
if (endOfDate > lastDayOfWeek || endOfDate > lastDayOfMonth) {
endDate = Math.max(lastDayOfMonth, lastDayOfWeek);
} else {
endDate = endOfDate;
}
// 获取连接对象,执行,这里使用 HBase 的 新 API
Connection connection = ConnectionFactory.createConnection(conf);
Admin admin = null;
try {
admin = connection.getAdmin();
} catch (Exception e) {
throw new RuntimeException("创建 Admin 对象失败", e);
}
// 5、构建我们 scan 集合
try {
for (long begin = startDate; begin < endDate;) {
// 格式化 HBase 的后缀
String tableNameSuffix = TimeUtil.parseLong2String(begin, TimeUtil.HBASE_TABLE_NAME_SUFFIX_FORMAT); // 20170814
// 构建表名称:tableName = event_logs20170814
String tableName = EventLogConstants.HBASE_NAME_EVENT_LOGS + tableNameSuffix;
// 需要先判断表存在,然后当表存在的情况下,再构建 Scan 对象
if (admin.tableExists(TableName.valueOf(tableName))) {
// 表存在,进行 Scan 对象创建
Scan scan = new Scan();
// 需要扫描的 HBase 表名设置到 Scan 对象中
scan.setAttribute(Scan.SCAN_ATTRIBUTES_TABLE_NAME, Bytes.toBytes(tableName));
// 设置过滤对象
scan.setFilter(filterList);
// 添加到 Scan 集合中
scans.add(scan);
}
// begin 累加
begin += GlobalConstants.DAY_OF_MILLISECONDS;
}
} finally {
// 关闭 Admin 连接
try {
admin.close();
} catch (Exception e) {
// nothing
}
}
// 访问 HBase 表中的数据
if (scans.isEmpty()) {
// 没有表存在,那么 Job 运行失败
throw new RuntimeException("HBase 中没有对应表存在:" + dateStr);
}
// 指定 Mapper,注意导入的是 mapreduce 包下的,不是 mapred 包下的,后者是老版本
TableMapReduceUtil.initTableMapperJob(
scans, // Scan 扫描控制器集合
NewInstallUserMapper.class, // 设置 Mapper 类
StatsUserDimension.class, // 设置 Mapper 输出 key 类型
Text.class, // 设置 Mapper 输出 value 值类型
job, // 设置给哪个 Job
true); // 如果在 Windows 上本地运行,则 addDependencyJars 参数必须设置为 false,如果打成 jar 包提交 Linux 上运行设置为 true,默认为 true
}
/**
* 获取一个根据列名称过滤数据的 Filter
*
* @param columns
* @return
*/
private Filter getColumnFilter(String[] columns) {
byte[][] prefixes = new byte[columns.length][];
for (int i = 0; i < columns.length; i++) {
prefixes[i] = Bytes.toBytes(columns[i]);
}
return new MultipleColumnPrefixFilter(prefixes);
}
}
20.3.4、自定义 OutPutFormat
本案例是自定义将 Reducer 输出数据输出到 Mysql 表的OutPutFormat,我们先看一个 DemoOutputFormat 的代码:
DemoOutputFormat.java
import java.io.IOException;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.OutputCommitter;
import org.apache.hadoop.mapreduce.OutputFormat;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
/**
* 案例: 自定义outputformat
*/
public class DemoOutputFormat extends OutputFormat<WritableComparable<?>, Writable>{
@Override
public RecordWriter<WritableComparable<?>, Writable> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException {
// 获取一个记录写对象,其实就是创建一个写 reduce 输出的对象
return null;
}
@Override
public void checkOutputSpecs(JobContext context) throws IOException, InterruptedException {
// 检测输出空间, 比如检测 hdfs 文件夹是否存在之类的,这个方法运行在 job 提交到 yarn 之前
}
@Override
public OutputCommitter getOutputCommitter(TaskAttemptContext context) throws IOException, InterruptedException {
// 用于将结果保存到 HDFS 上的时候,进行 job 提交的
// 一般情况下,我们采用最简单的实现或者直接使用 FileOutputCommitter
return null;
}
/**
* 自定义的数据输出器
*/
static class DemoRecordWriter extends RecordWriter<WritableComparable<?>, Writable> {
@Override
public void write(WritableComparable<?> key, Writable value) throws IOException, InterruptedException {
// 定义具体如何将 Reduce 传入的 key/value 键值对进行输出的代码
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
// 关闭资源链接,清理一些内存之类东西
// 一定会调用,而且是在所有的数据处理完成后调用
}
}
}
具体的代码:
TransformerMySQLOutputFormat.java
package com.z.transformer.mr;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.OutputCommitter;
import org.apache.hadoop.mapreduce.OutputFormat;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import com.z.transformer.common.GlobalConstants;
import com.z.transformer.common.KpiType;
import com.z.transformer.converter.IDimensionConverter;
import com.z.transformer.converter.impl.DimensionConverterImpl;
import com.z.transformer.dimension.key.BaseDimension;
import com.z.transformer.dimension.value.BaseStatsValueWritable;
import com.z.transformer.util.JDBCManager;
public class TransformerMySQLOutputFormat extends OutputFormat<BaseDimension, BaseStatsValueWritable> {
@Override
public RecordWriter<BaseDimension, BaseStatsValueWritable> getRecordWriter(TaskAttemptContext context)
throws IOException, InterruptedException {
// 构建属于当前 OutPutForamt 的数据输出器
// 1、获取上下文
Configuration conf = context.getConfiguration();
// 2、创建 jdbc 连接
Connection conn = null;
try {
// 根据上下文中配置的信息获取数据库连接
// 需要在 hadoop 的 configuration 中配置 mysql 的驱动连接信息
conn = JDBCManager.getConnection(conf, GlobalConstants.WAREHOUSE_OF_REPORT);
conn.setAutoCommit(false); // 关闭自动提交机制,方便我们进行批量提交
} catch (SQLException e) {
throw new IOException(e);
}
// 3、构建对象并返回
return new TransformerRecordWriter(conf, conn);
}
@Override
public void checkOutputSpecs(JobContext context) throws IOException, InterruptedException {
// 该方法的主要作用是检测输出空间的相关属性,比如是否存在类的情况
// 如果说 Job 运行前提的必须条件不满足,直接抛出一个 Exception。
}
@Override
public OutputCommitter getOutputCommitter(TaskAttemptContext context) throws IOException, InterruptedException {
// 使用的是 FileOutputFormat 中默认的方式
String name = context.getConfiguration().get(FileOutputFormat.OUTDIR);
Path output = name == null ? null : new Path(name);
return new FileOutputCommitter(output, context);
}
/**
* 自定义的具体将 reducer 输出数据输出到 mysql 表的输出器
*/
static class TransformerRecordWriter extends RecordWriter<BaseDimension, BaseStatsValueWritable> {
private Connection conn = null; // 数据库连接
private Configuration conf = null; // 上下文保存成属性
private Map<KpiType, PreparedStatement> pstmtMap = new HashMap<KpiType, PreparedStatement>();
private int batchNumber = 0; // 批量提交数据大小
private Map<KpiType, Integer> batch = new HashMap<KpiType, Integer>();
private IDimensionConverter converter = null; // 维度转换对象
/**
* 构造方法
*
* @param conf
* @param conn
*/
public TransformerRecordWriter(Configuration conf, Connection conn) {
this.conf = conf;
this.conn = conn;
this.batchNumber = Integer.valueOf(conf.get(GlobalConstants.JDBC_BATCH_NUMBER, GlobalConstants.DEFAULT_JDBC_BATCH_NUMBER));
this.converter = new DimensionConverterImpl();
}
@Override
public void write(BaseDimension key, BaseStatsValueWritable value) throws IOException, InterruptedException {
try {
// 每个分析的 kpi 值是不一样的,一样的 kpi 有一样的插入 sql 语句
KpiType kpi = value.getKpi();
int count = 0; // count 表示当前 PreparedStatement 对象中需要提交的记录数量
// 1、获取数据库的 PreparedStatement 对象
// 从上下文中获取 kpi 对应的 sql 语句
String sql = this.conf.get(kpi.name);
PreparedStatement pstmt = null;
// 判断当前 kpi 对应的 preparedstatment 对象是否存在
if (this.pstmtMap.containsKey(kpi)) {
// 存在
pstmt = this.pstmtMap.get(kpi); // 获取对应的对象
if (batch.containsKey(kpi)) {
count = batch.get(kpi);
}
} else {
// 不存在, 第一次创建一个对象
pstmt = conn.prepareStatement(sql);
// 保存到 map 集合中
this.pstmtMap.put(kpi, pstmt);
}
// 2、获取 collector 类名称,如:collector_browser_new_install_user 或者 collector_new_install_user
String collectorClassName = this.conf.get(GlobalConstants.OUTPUT_COLLECTOR_KEY_PREFIX + kpi.name);
// 3、创建 class 对象
Class<?> clz = Class.forName(collectorClassName); // 获取类对象
// 调用 newInstance 方法进行构成对象,要求具体的实现子类必须有默认无参构造方法
ICollector collector = (ICollector) clz.newInstance();
// 4、设置参数
collector.collect(conf, key, value, pstmt, converter);
// 5、处理完成后,进行累计操作
count++;
this.batch.put(kpi, count);
// 6、执行,采用批量提交的方式
if (count > this.batchNumber) {
pstmt.executeBatch(); // 批量提交
conn.commit(); // 连接提交
this.batch.put(kpi, 0); // 恢复数据
}
} catch (Exception e) {
throw new IOException(e);
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
// 关闭资源
try {
// 1、进行 jdbc 提交操作
for (Map.Entry<KpiType, PreparedStatement> entry : this.pstmtMap.entrySet()) {
try {
entry.getValue().executeBatch(); // 批量提交
} catch (SQLException e) {
// nothing
}
}
this.conn.commit(); // 数据库提交
} catch (Exception e) {
throw new IOException(e);
} finally {
// 2、关闭资源
for (Map.Entry<KpiType, PreparedStatement> entry : this.pstmtMap.entrySet()) {
JDBCManager.closeConnection(null, entry.getValue(), null);
}
JDBCManager.closeConnection(conn, null, null);
}
}
}
}
20.3.5、测试
Step1、使用 maven 插件:maven-shade-plugin,将第三方依赖的 jar 全部打包进去,需要在 pom.xml 中配置依赖。参考【章节 十七、工具代码导入】中的 pom.xml 文件。
1、-P local clean package(不打包第三方jar)
2、-P dev clean package install(打包第三方jar)(推荐使用这种,本案例使用这种方式)
Step2、在 hadoop-env.sh 添加内容:
[atguigu@hadoop102 hadoop]$ pwd
/opt/module/hadoop-2.7.2/etc/hadoop
[atguigu@hadoop102 hadoop]$ vim hadoop-env.sh
export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:/opt/module/hbase/lib/*
尖叫提示:修改该配置后,需要配置分发,然后重启集群,方可生效!!!
Step3、打包成功后,将要运行的 transformer-0.0.1-SNAPSHOT.jar 拷贝至 /opt/module/hbase/lib 目录下,然后同步到其他机器或者配置分发:
同步到其他机器
[atguigu@hadoop102 ~]$ scp -r /opt/module/hbase/lib/transformer-0.0.1-SNAPSHOT.jar hadoop103:/opt/module/hbase/lib/
[atguigu@hadoop102 ~]$ scp -r /opt/module/hbase/lib/transformer-0.0.1-SNAPSHOT.jar hadoop104:/opt/module/hbase/lib/
或者配置分发
[atguigu@hadoop102 ~]$ xsync /opt/module/hbase/lib/transformer-0.0.1-SNAPSHOT.jar
尖叫提示:如果没有同步到其他机器或者配置分发,会出现类找不到异常,如下:
执行异常:java.lang.RuntimeException: java.lang.ClassNotFoundException: Class com.z.transformer.dimension.key.stats.StatsUserDimension not found
4、运行 jar 包,命令如下:
先进行数据清洗
$ /opt/module/hadoop-2.7.2/bin/yarn jar /opt/module/hbase/lib/transformer-0.0.1-SNAPSHOT.jar com.z.transformer.mr.etl.AnalysisDataRunner -date 2015-12-20
再进行统计运算
$ /opt/module/hadoop-2.7.2/bin/yarn jar /opt/module/hbase/lib/transformer-0.0.1-SNAPSHOT.jar com.z.transformer.mr.statistics.NewInstallUserRunner -date 2015-12-20
5、所遇到的 bug 小结
1、AnalysisDataRunner 中【判断输入的参数是否是一个有效的时间格式数据】前要加感叹号,否则会总是进入【默认清洗昨天的数据然后存储到 HBase 中】的判断结果中。
2、NewInstallUserRunner 中【判断输入的参数是否是一个有效的时间格式数据】前要加感叹号,否则会总是进入【默认清洗昨天的数据然后存储到 HBase 中】的判断结果中。
3、NewInstallUserRunner 中对于列的规则没有使用。
4、NewInstallUserMapper 中 outputValue 设置为 uuid 时没有放在判断该 uuid 是否为空之后。
5、NewInstallUserRunner 中 setConf 中没有添加自己开发环境所有需要的其他资源属性文件。
6、NewInstallUserReducer 中存放 new IntWritable(-1) 要与 BrowserNewInstallUserCollector、NewInstallUserCollector 中取出来的时候一致。
7、打包成功后,将要运行的 transformer-0.0.1-SNAPSHOT.jar 拷贝至 /opt/module/hbase/lib 目录下,然后同步到其他机器或者配置分发,否则会出现类找不到异常。
8、修改 hadoop-env.sh 配置后,需要配置分发,然后重启集群,方可生效!!!
9、由于 “-” 在 HBase 的表名中允许,在 Hive 的表名中不可以是 “-”,即在 Hive 中,“-” 是特殊字符,为了方便和统一,所以我们将 “-” 的地方替换为 “_”。这样就三者统一了。
即 HDFS 上存放数据的目录变为 /event_logs/2015/12/20,HBase 数据库中的表名变为 event_logs20151220,Hive 中的表名为 event_logsxxx。
二十一、Hive 之 hourly 分析
尖叫提示:由于 “-” 在 HBase 的表名中允许,在 Hive 的表名中不可以是 “-”,即在 Hive 中,“-” 是特殊字符,为了方便和统一,所以我们将 “-” 的地方替换为 “_”。这样就三者统一了。即 HDFS 上存放数据的目录变为 /event_logs/2015/12/20,HBase 数据库中的表名变为 event_logs20151220,Hive 中的表名为 event_logsxxx。
21.1、目标
分析一天 24 个时间段的新增用户、活跃用户、会话个数和会话长度四个指标,最终将结果保存到 HDFS 中,使用 sqoop 导出到 Mysql。
21.2、目标解析
- 新增用户:分析 launch 事件中各个不同时间段的 uuid 数量。
- 活跃用户:分析 pageview 事件中各个不同时间段的 uuid 数量。
- 会话个数:分析 pageview 事件中各个不同时间段的 会话id 数量。
- 会话长度:分析 pageview 事件中各个不同时间段内所有会话时长的总和。
21.3、创建 Mysql 结果表
stats_hourly.sql
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `stats_hourly`
-- ----------------------------
DROP TABLE IF EXISTS `stats_hourly`;
CREATE TABLE `stats_hourly` (
`platform_dimension_id` int(11) NOT NULL COMMENT '平台维度id',
`date_dimension_id` int(11) NOT NULL COMMENT '时间维度id',
`kpi_dimension_id` int(11) NOT NULL COMMENT 'kpi维度id',
`hour_00` int(11) DEFAULT '0' COMMENT '0-1点的计算值',
`hour_01` int(11) DEFAULT '0' COMMENT '1-2点的计算值',
`hour_02` int(11) DEFAULT '0' COMMENT '2-3点的计算值',
`hour_03` int(11) DEFAULT '0' COMMENT '3-4点的计算值',
`hour_04` int(11) DEFAULT '0' COMMENT '4-5点的计算值',
`hour_05` int(11) DEFAULT '0' COMMENT '5-6点的计算值',
`hour_06` int(11) DEFAULT '0' COMMENT '6-7点的计算值',
`hour_07` int(11) DEFAULT '0' COMMENT '7-8点的计算值',
`hour_08` int(11) DEFAULT '0' COMMENT '8-9点的计算值',
`hour_09` int(11) DEFAULT '0' COMMENT '9-10点的计算值',
`hour_10` int(11) DEFAULT '0' COMMENT '10-11点的计算值',
`hour_11` int(11) DEFAULT '0' COMMENT '11-12点的计算值',
`hour_12` int(11) DEFAULT '0' COMMENT '12-13点的计算值',
`hour_13` int(11) DEFAULT '0' COMMENT '13-14点的计算值',
`hour_14` int(11) DEFAULT '0' COMMENT '14-15点的计算值',
`hour_15` int(11) DEFAULT '0' COMMENT '15-16点的计算值',
`hour_16` int(11) DEFAULT '0' COMMENT '16-17点的计算值',
`hour_17` int(11) DEFAULT '0' COMMENT '17-18点的计算值',
`hour_18` int(11) DEFAULT '0' COMMENT '18-19点的计算值',
`hour_19` int(11) DEFAULT '0' COMMENT '19-20点的计算值',
`hour_20` int(11) DEFAULT '0' COMMENT '20-21点的计算值',
`hour_21` int(11) DEFAULT '0' COMMENT '21-22点的计算值',
`hour_22` int(11) DEFAULT '0' COMMENT '22-23点的计算值',
`hour_23` int(11) DEFAULT '0' COMMENT '23-00点的计算值',
PRIMARY KEY (`platform_dimension_id`,`date_dimension_id`,`kpi_dimension_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='按小时计算数值的分析结果保存表';
-- ----------------------------
-- Records of stats_hourly
-- ----------------------------
dimension_kpi.sql
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `dimension_kpi`
-- ----------------------------
DROP TABLE IF EXISTS `dimension_kpi`;
CREATE TABLE `dimension_kpi` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`kpi_name` varchar(45) DEFAULT NULL COMMENT 'kpi名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of dimension_kpi
-- ----------------------------
21.4、Hive 分析
Hive 的安装、配置及基本使用的参考链接:https://www.cnblogs.com/chenmingjun/p/10428809.html#_label1
21.4.1、创建 Hive 外部表,关联 HBase 数据表
create external table
event_logs20151220(
key string,
pl string,
ver string,
en string,
u_ud string,
u_sd string,
s_time bigint
)
stored by 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
with serdeproperties("hbase.columns.mapping" = ":key, info:pl,info:ver,info:en,info:u_ud,info:u_sd,info:s_time")
tblproperties("hbase.table.name" = "event_logs20151220");
21.4.2、创建临时表用于存放 pageview 和 launch 事件的数据(即存放过滤数据)
create table
stats_hourly_tmp1(
pl string,
ver string,
en string,
s_time bigint,
u_ud string,
u_sd string,
hour int,
date string
)
row format delimited
fields terminated by "\t";
上述 HQL 会报出一个异常
FAILED: ParseException line 5:2 Failed to recognize predicate 'date'. Failed rule: 'identifier' in column specification
失败:解析异常 10:2 无法识别谓词'date'。 失败的规则:列规范中的“标识符”
解决办法:直接弃用保留关键字 date,即不使用留关键字,或者我们可以将 date 加上反引号 date 。
参考链接:https://www.cnblogs.com/chenmingjun/p/10728644.html
21.4.3、提取 e_pv 和 e_l 事件数据到临时表中
分析:
FROM
event_logs20151220 INSERT overwrite TABLE stats_hourly_tmp1
SELECT
pl,
ver,
en,
s_time,
u_ud,
u_sd,
hour (from_unixtime(cast(s_time/1000 AS int), 'yyyy-MM-dd HH:mm:ss')),
from_unixtime(cast(s_time/1000 AS int), 'yyyy-MM-dd')
WHERE
en = 'e_pv'
OR en = 'e_l';
查看结果:
select * from stats_hourly_tmp1;
website 1 e_pv 1450572277569 E476E068-98E3-4615-9A35-504CE0A820EF 2F300B4C-779A-411A-BDCE-D388F1FF933B 8 2015-12-20
website 1 e_pv 1450572277710 129AC092-854C-466D-A432-A8C3B566CD3B 4ED722E2-C4DE-4CCC-A877-F1EEEC4FA1B1 8 2015-12-20
website 1 e_l 1450572277710 129AC092-854C-466D-A432-A8C3B566CD3B 4ED722E2-C4DE-4CCC-A877-F1EEEC4FA1B1 8 2015-12-20
website 1 e_l 1450572278043 780311A8-790F-47FE-84C2-DF8CF4D70024 E13A5C0E-5F95-4DD4-91A9-97A72866FF21 8 2015-12-20
website 1 e_pv 1450572278044 780311A8-790F-47FE-84C2-DF8CF4D70024 E13A5C0E-5F95-4DD4-91A9-97A72866FF21 8 2015-12-20
......
......
21.4.4、创建分析结果临时保存表
create table
stats_hourly_tmp2(
pl string,
ver string,
`date` string,
hour int,
kpi string,
value int
)
row format delimited
fields terminated by '\t';
21.4.5、分析活跃访客数
Step1、具体平台,具体平台版本(platform:name, version:version)
分析:
FROM
stats_hourly_tmp1 INSERT overwrite TABLE stats_hourly_tmp2
SELECT
pl,
ver,
`date`,
hour,
'active_users',
count(DISTINCT u_ud) AS active_users
WHERE
en = 'e_pv'
GROUP BY
pl,
ver,
`date`,
hour;
查看结果:
hive (default)> select * from stats_hourly_tmp2;
stats_hourly_tmp2.pl stats_hourly_tmp2.ver stats_hourly_tmp2.date stats_hourly_tmp2.hour stats_hourly_tmp2.kpi stats_hourly_tmp2.value
website 1 2015-12-20 8 active_users 3515
Step2、具体平台,所有版本(platform:name, version:all)
分析:
FROM
stats_hourly_tmp1 INSERT INTO TABLE stats_hourly_tmp2
SELECT
pl,
'all',
`date`,
hour,
'active_users',
count(DISTINCT u_ud) AS active_users
WHERE
en = 'e_pv'
GROUP BY
pl,
`date`,
hour;
查看结果:
hive (default)> select * from stats_hourly_tmp2;
stats_hourly_tmp2.pl stats_hourly_tmp2.ver stats_hourly_tmp2.date stats_hourly_tmp2.hour stats_hourly_tmp2.kpi stats_hourly_tmp2.value
website 1 2015-12-20 8 active_users 3515
website all 2015-12-20 8 active_users 3515
Step3、所有平台,所有版本(platform:all, version:all)
分析:
FROM
stats_hourly_tmp1 INSERT INTO TABLE stats_hourly_tmp2
SELECT
'all',
'all',
`date`,
hour,
'active_users',
count(DISTINCT u_ud) AS active_users
WHERE
en = 'e_pv'
GROUP BY
`date`,
hour;
查看结果:
hive (default)> select * from stats_hourly_tmp2;
stats_hourly_tmp2.pl stats_hourly_tmp2.ver stats_hourly_tmp2.date stats_hourly_tmp2.hour stats_hourly_tmp2.kpi stats_hourly_tmp2.value
website 1 2015-12-20 8 active_users 3515
website all 2015-12-20 8 active_users 3515
all all 2015-12-20 8 active_users 3515
21.4.6、分析会话长度
将每个会话的长度先要计算出来,然后统计一个时间段的各个会话的总和。
Step1、具体平台,具体平台版本(platform:name, version:version)
分析:
FROM (
SELECT
pl,
ver,
`date`,
hour,
u_sd,
(max(s_time) - min(s_time)) AS s_length
FROM
stats_hourly_tmp1
WHERE
en = 'e_pv'
GROUP BY
pl,
ver,
`date`,
hour,
u_sd
) AS tmp INSERT INTO TABLE stats_hourly_tmp2
SELECT
pl,
ver,
`date`,
hour,
'sessions_lengths',
cast(sum(s_length)/1000 AS int)
GROUP BY
pl,
ver,
`date`,
hour;
查看结果:
hive (default)> select * from stats_hourly_tmp2;
stats_hourly_tmp2.pl stats_hourly_tmp2.ver stats_hourly_tmp2.date stats_hourly_tmp2.hour stats_hourly_tmp2.kpi stats_hourly_tmp2.value
website 1 2015-12-20 8 active_users 3515
website all 2015-12-20 8 active_users 3515
all all 2015-12-20 8 active_users 3515
website 1 2015-12-20 8 sessions_lengths 131480
Step2、具体平台,所有版本(platform:name, version:all)
分析:
FROM (
SELECT
pl,
`date`,
hour,
u_sd,
(max(s_time) - min(s_time)) AS s_length
FROM
stats_hourly_tmp1
WHERE
en = 'e_pv'
GROUP BY
pl,
`date`,
hour,
u_sd
) AS tmp INSERT INTO TABLE stats_hourly_tmp2
SELECT
pl,
'all',
`date`,
hour,
'sessions_lengths',
cast(sum(s_length)/1000 AS int)
GROUP BY
pl,
`date`,
hour;
查看结果:
hive (default)> select * from stats_hourly_tmp2;
stats_hourly_tmp2.pl stats_hourly_tmp2.ver stats_hourly_tmp2.date stats_hourly_tmp2.hour stats_hourly_tmp2.kpi stats_hourly_tmp2.value
website 1 2015-12-20 8 active_users 3515
website all 2015-12-20 8 active_users 3515
all all 2015-12-20 8 active_users 3515
website 1 2015-12-20 8 sessions_lengths 131480
website all 2015-12-20 8 sessions_lengths 131480
Step3、所有平台,所有版本(platform:all, version:all)
FROM (
SELECT
`date`,
hour,
u_sd,
(max(s_time) - min(s_time)) AS s_length
FROM
stats_hourly_tmp1
WHERE
en = 'e_pv'
GROUP BY
`date`,
hour,
u_sd
) AS tmp INSERT INTO TABLE stats_hourly_tmp2
SELECT
'all',
'all',
`date`,
hour,
'sessions_lengths',
cast(sum(s_length)/1000 AS int)
GROUP BY
`date`,
hour;
查看结果:
hive (default)> select * from stats_hourly_tmp2;
stats_hourly_tmp2.pl stats_hourly_tmp2.ver stats_hourly_tmp2.date stats_hourly_tmp2.hour stats_hourly_tmp2.kpi stats_hourly_tmp2.value
website 1 2015-12-20 8 active_users 3515
website all 2015-12-20 8 active_users 3515
all all 2015-12-20 8 active_users 3515
website 1 2015-12-20 8 sessions_lengths 131480
website all 2015-12-20 8 sessions_lengths 131480
all all 2015-12-20 8 sessions_lengths 131480
21.4.7、创建最终结果表
我们在这里需要创建一个和 Mysql 表结构一致的 Hive 表,便于后期使用 Sqoop 导出数据到 Mysql 中。
create table
stats_hourly(
platform_dimension_id int,
date_dimension_id int,
kpi_dimension_id int,
hour_00 int,
hour_01 int,
hour_02 int,
hour_03 int,
hour_04 int,
hour_05 int,
hour_06 int,
hour_07 int,
hour_08 int,
hour_09 int,
hour_10 int,
hour_11 int,
hour_12 int,
hour_13 int,
hour_14 int,
hour_15 int,
hour_16 int,
hour_17 int,
hour_18 int,
hour_19 int,
hour_20 int,
hour_21 int,
hour_22 int,
hour_23 int
)
row format delimited fields
terminated by '\t';
21.4.8、向结果表中插入数据
我们需要 platform_dimension_id int, date_dimension_id int, kpi_dimension_id int 这三个字段,所以我们需要使用 UDF 函数生成对应的字段。
Step1、编写 UDF 函数,代码如下:
PlatformDimensionConverterUDF.java
package com.z.transformer.udf;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hive.ql.exec.UDF;
import com.z.transformer.common.GlobalConstants;
import com.z.transformer.converter.IDimensionConverter;
import com.z.transformer.converter.impl.DimensionConverterImpl;
import com.z.transformer.dimension.key.base.PlatformDimension;
/**
* 自定义根据平台维度信息获取维度 id 自定义 udf 的时候,如果使用到 FileSystem(HDFS 的 api),记住一定不要调用 close 方法
*/
public class PlatformDimensionConverterUDF extends UDF {
// 用于根据维度值获取维度 id 的对象
private IDimensionConverter converter = null;
/**
* 默认无参构造方法,必须给定的
*/
public PlatformDimensionConverterUDF() {
// 初始化操作
this.converter = new DimensionConverterImpl();
}
/**
* 根据给定的平台维度名称和平台维度版本获取对应的维度 id 值
*
* @param platformName
* 维度名称
* @param platformVersion
* 维度版本
* @return
* @throws IOException
* 获取 id 的时候产生的异常
*/
public int evaluate(String platformName, String platformVersion) throws IOException {
// 1、要求参数不能为空,当为空的时候,设置为 unknown 默认值
if (StringUtils.isBlank(platformName) || StringUtils.isBlank(platformVersion)) {
platformName = GlobalConstants.DEFAULT_VALUE;
platformVersion = GlobalConstants.DEFAULT_VALUE;
}
// 2、构建一个对象
PlatformDimension pf = new PlatformDimension(platformName, platformVersion);
// 3、获取维度 id 值,使用写好的 DimensionConverterImpl 类解析
return this.converter.getDimensionIdByValue(pf);
}
}
DateDimensionConverterUDF.java
package com.z.transformer.udf;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hive.ql.exec.UDF;
import com.z.transformer.common.DateEnum;
import com.z.transformer.converter.IDimensionConverter;
import com.z.transformer.converter.impl.DimensionConverterImpl;
import com.z.transformer.dimension.key.base.DateDimension;
import com.z.transformer.util.TimeUtil;
/**
* 根据给定的时间值返回对应的时间维度 id
*/
public class DateDimensionConverterUDF extends UDF {
// 用于根据维度值获取维度id的对象
private IDimensionConverter converter = null;
/**
* 默认无参构造方法,必须给定的
*/
public DateDimensionConverterUDF() {
// 初始化操作
this.converter = new DimensionConverterImpl();
}
/**
* 如果给定的参数不在处理范围,那么直接抛出异常
*
* @param date
* 字符串类型的时间值,例如:2015-12-20
* @param type
* 需要的时间维度所属类型,参数可选为:(year、month、week、day、season)
* @return 对应的维度 id 值
* @throws IOException
* 根据维度对象获取维度 id 值的时候产生的异常
*/
public int evaluate(String date, String type) throws IOException {
// 1、参数过滤,时间格式不正确的数据直接过滤
if (StringUtils.isBlank(date) || StringUtils.isBlank(type)) {
throw new IllegalArgumentException("参数异常,date 和 type 参数不能为空,date=" + date + ", type =" + type);
}
if (!TimeUtil.isValidateRunningDate(date)) {
// 不是一个有效的输入时间
throw new IllegalArgumentException("参数异常,date 参数格式要求为:yyyy-MM-dd,当前值为:" + date);
}
// 2、根据给定的 type 值来构建 DateEnum 对象
DateEnum dateEnum = DateEnum.valueOfName(type); // 根据名称获取对应的值,如果没有返回的是 null
if (dateEnum == null) {
// 给定的 type 值异常,无法转换为 DateEnum 枚举对象
throw new IllegalArgumentException("参数异常,type 参数只能是[year、month、week、day、season]其中的一个,当前值为:" + type);
}
// 3、创建 DateDimension 维度对象
DateDimension dateDimension = DateDimension.buildDate(TimeUtil.parseString2Long(date), dateEnum);
// 4、获取id的值
return this.converter.getDimensionIdByValue(dateDimension);
}
}
KpiDimensionConverterUDF.java
package com.z.transformer.udf;
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hive.ql.exec.UDF;
import com.z.transformer.converter.IDimensionConverter;
import com.z.transformer.converter.impl.DimensionConverterImpl;
import com.z.transformer.dimension.key.base.KpiDimension;
/**
* 用于根据 kpi 名称获取 kpi 维度 id
*/
public class KpiDimensionConverterUDF extends UDF {
// 用于根据维度值获取维度id的对象
private IDimensionConverter converter = null;
/**
* 默认无参构造方法,必须给定的
*/
public KpiDimensionConverterUDF() {
// 初始化操作
this.converter = new DimensionConverterImpl();
}
/**
* @param kpiName
* @return
* @throws IOException
*/
public int evaluate(String kpiName) throws IOException {
// 1、判断参数是否为空
if (StringUtils.isBlank(kpiName)) {
throw new IllegalArgumentException("参数异常,kpiName不能为空!!!");
}
// 2、构建 kpi 对象
KpiDimension kpi = new KpiDimension(kpiName);
// 3、获取 id 的值
return this.converter.getDimensionIdByValue(kpi);
}
}
Step2、编译打包 UDF 函数代码
编译参数:-P dev clean package install
导入 HBase 依赖:export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:/opt/module/hbase/lib/*
Step3、上传 UDF 函数代码 jar 包到 HDFS
如何使用 UDF 函数代码?
方式一:把自定义的 UDF 函数代码 jar 包直接集成到 Hive 的源码包中,但是需要我们重新编译 Hive 源码,很麻烦!
方式二:把自定义的 UDF 函数代码 jar 包上传至 Linux 本地,然后通过 add jar 的方式使用它们。
方式三:把自定义的 UDF 函数代码 jar 包上传至 HDFS 上,然后直接引用即可。(本案例使用该方式,具体操作如下:)
$ bin/hadoop fs -mkdir -p /event_logs/
$ bin/hadoop fs -mv /event-logs/* /event_logs/
尖叫提示:记得修改 Flume 的 HDFS SINK 路径以及手动上传脚本命令。
$ bin/hadoop fs -rm -r /event-logs/
上传脚本:
$ bin/hadoop fs -mkdir -p /udf_jar/transformer
$ bin/hadoop fs -put /opt/module/hbase/lib/transformer-0.0.1-SNAPSHOT.jar /udf_jar/transformer
Step4、使用 UDF 的 jar
hive(default)> create function date_converter as 'com.z.transformer.udf.DateDimensionConverterUDF' using jar 'hdfs://hadoop102:9000/udf_jar/transformer/transformer-0.0.1-SNAPSHOT.jar';
hive(default)> create function kpi_converter as 'com.z.transformer.udf.KpiDimensionConverterUDF' using jar 'hdfs://hadoop102:9000/udf_jar/transformer/transformer-0.0.1-SNAPSHOT.jar';
hive(default)> create function platform_converter as 'com.z.transformer.udf.PlatformDimensionConverterUDF' using jar 'hdfs://hadoop102:9000/udf_jar/transformer/transformer-0.0.1-SNAPSHOT.jar';
Step5、执行最终数据统计
insert overwrite table stats_hourly
select
default.platform_converter(pl,ver),
default.date_converter(`date`,'day'),
default.kpi_converter(kpi),
max(case when hour=0 then value else 0 end) as hour_00,
max(case when hour=1 then value else 0 end) as hour_01,
max(case when hour=2 then value else 0 end) as hour_02,
max(case when hour=3 then value else 0 end) as hour_03,
max(case when hour=4 then value else 0 end) as hour_04,
max(case when hour=5 then value else 0 end) as hour_05,
max(case when hour=6 then value else 0 end) as hour_06,
max(case when hour=7 then value else 0 end) as hour_07,
max(case when hour=8 then value else 0 end) as hour_08,
max(case when hour=9 then value else 0 end) as hour_09,
max(case when hour=10 then value else 0 end) as hour_10,
max(case when hour=11 then value else 0 end) as hour_11,
max(case when hour=12 then value else 0 end) as hour_12,
max(case when hour=13 then value else 0 end) as hour_13,
max(case when hour=14 then value else 0 end) as hour_14,
max(case when hour=15 then value else 0 end) as hour_15,
max(case when hour=16 then value else 0 end) as hour_16,
max(case when hour=17 then value else 0 end) as hour_17,
max(case when hour=18 then value else 0 end) as hour_18,
max(case when hour=19 then value else 0 end) as hour_19,
max(case when hour=20 then value else 0 end) as hour_20,
max(case when hour=21 then value else 0 end) as hour_21,
max(case when hour=22 then value else 0 end) as hour_22,
max(case when hour=23 then value else 0 end) as hour_23
from
stats_hourly_tmp2
group by
pl,
ver,
`date`,
kpi;
出现一个问题,截图如下:

原因:

解决办法:红框中的那句应该改为 Configuration conf = new Configuration();
查询结果命令:
hive (default)> select * from stats_hourly;
21.4.9、使用 Sqoop 导出 数据到 Mysql,观察数据
$ bin/sqoop export \
--connect jdbc:mysql://hadoop102:3306/report \
--username root \
--password 123456 \
--table stats_hourly \
--num-mappers 1 \
--export-dir /user/hive/warehouse/stats_hourly \
--input-fields-terminated-by "\t"
查看 Mysql 结果截图
dimension_kpi 表

stats_hourly 表
21.5、定时任务流程
1、定时每天00点00分00秒执行日期切割操作
2、Flume 实时监控日志文件并上传到指定 HDFS 目录(按天分割的目录)
3、ETL 的 jar 任务每天00点10分00秒执行
4、数据分析 的 jar 任务在上一个任务执行完毕后执行(推荐使用 oozie,根据执行任务的返回状态吗决定跳转到哪一个节点)
5、如果执行 Hive 脚本,则 HQL 语句需要全部放在 xxx.hql 文件中, 执行命令:bin/hive -f xxx.hql
6、如果上一步执行成功,则执行 sqoop 导出到 MySQL 中
二十二、常用 Maven 仓库地址
常用 Maven 仓库地址
中央库:http://repo.maven.apache.org/maven2/
CDN库:https://repository.cloudera.com/artifactory/cloudera-repos/
Maven 中央仓库最近更新的 Artifact:http://maven.outofmemory.cn/
Search/Browse/Explore Maven Repository:https://mvnrepository.com/
【转载文章务必保留出处和署名,谢谢!】

浙公网安备 33010602011771号