大数据技术之_18_大数据离线平台_04_数据分析 + Hive 之 hourly 分析 + 常用 Maven 仓库地址

二十、数据分析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<StatsUserDimensionText{
    // 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<StatsUserDimensionTextStatsUserDimensionMapWritableValue{

    // 保存唯一 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<BaseDimensionBaseStatsValueWritable{

    @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<BaseDimensionBaseStatsValueWritable{
        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, nullnull);
            }
        }

    }
}

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.RuntimeExceptionjava.lang.ClassNotFoundExceptionClass 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(11NOT NULL COMMENT '平台维度id',
  `date_dimension_id` int(11NOT NULL COMMENT '时间维度id',
  `kpi_dimension_id` int(11NOT NULL COMMENT 'kpi维度id',
  `hour_00` int(11DEFAULT '0' COMMENT '0-1点的计算值',
  `hour_01` int(11DEFAULT '0' COMMENT '1-2点的计算值',
  `hour_02` int(11DEFAULT '0' COMMENT '2-3点的计算值',
  `hour_03` int(11DEFAULT '0' COMMENT '3-4点的计算值',
  `hour_04` int(11DEFAULT '0' COMMENT '4-5点的计算值',
  `hour_05` int(11DEFAULT '0' COMMENT '5-6点的计算值',
  `hour_06` int(11DEFAULT '0' COMMENT '6-7点的计算值',
  `hour_07` int(11DEFAULT '0' COMMENT '7-8点的计算值',
  `hour_08` int(11DEFAULT '0' COMMENT '8-9点的计算值',
  `hour_09` int(11DEFAULT '0' COMMENT '9-10点的计算值',
  `hour_10` int(11DEFAULT '0' COMMENT '10-11点的计算值',
  `hour_11` int(11DEFAULT '0' COMMENT '11-12点的计算值',
  `hour_12` int(11DEFAULT '0' COMMENT '12-13点的计算值',
  `hour_13` int(11DEFAULT '0' COMMENT '13-14点的计算值',
  `hour_14` int(11DEFAULT '0' COMMENT '14-15点的计算值',
  `hour_15` int(11DEFAULT '0' COMMENT '15-16点的计算值',
  `hour_16` int(11DEFAULT '0' COMMENT '16-17点的计算值',
  `hour_17` int(11DEFAULT '0' COMMENT '17-18点的计算值',
  `hour_18` int(11DEFAULT '0' COMMENT '18-19点的计算值',
  `hour_19` int(11DEFAULT '0' COMMENT '19-20点的计算值',
  `hour_20` int(11DEFAULT '0' COMMENT '20-21点的计算值',
  `hour_21` int(11DEFAULT '0' COMMENT '21-22点的计算值',
  `hour_22` int(11DEFAULT '0' COMMENT '22-23点的计算值',
  `hour_23` int(11DEFAULT '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(11NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `kpi_name` varchar(45DEFAULT 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

失败:解析异常 102 无法识别谓词'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 endas hour_00,
  max(case when hour=1 then value else 0 endas hour_01,
  max(case when hour=2 then value else 0 endas hour_02,
  max(case when hour=3 then value else 0 endas hour_03,
  max(case when hour=4 then value else 0 endas hour_04,
  max(case when hour=5 then value else 0 endas hour_05,
  max(case when hour=6 then value else 0 endas hour_06,
  max(case when hour=7 then value else 0 endas hour_07,
  max(case when hour=8 then value else 0 endas hour_08,
  max(case when hour=9 then value else 0 endas hour_09,
  max(case when hour=10 then value else 0 endas hour_10,
  max(case when hour=11 then value else 0 endas hour_11,
  max(case when hour=12 then value else 0 endas hour_12,
  max(case when hour=13 then value else 0 endas hour_13,
  max(case when hour=14 then value else 0 endas hour_14,
  max(case when hour=15 then value else 0 endas hour_15,
  max(case when hour=16 then value else 0 endas hour_16,
  max(case when hour=17 then value else 0 endas hour_17,
  max(case when hour=18 then value else 0 endas hour_18,
  max(case when hour=19 then value else 0 endas hour_19,
  max(case when hour=20 then value else 0 endas hour_20,
  max(case when hour=21 then value else 0 endas hour_21,
  max(case when hour=22 then value else 0 endas hour_22,
  max(case when hour=23 then value else 0 endas 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/

posted @ 2019-04-17 23:02 黑泽君 阅读(...) 评论(...) 编辑 收藏