离线数仓构建案例一

数据采集

日志数据(文件)到Kafka

自己写个程序模拟一些用户的行为数据,这些数据存在一个文件夹中。

接着使用flume监控采集这些文件,然后发送给kafka中待消费。

1、flume采集配置文件

监控文件将数据发给kafka的flume配置文件:


#定义组件
a1.sources = r1
a1.channels = c1

#配置source
a1.sources.r1.type = TAILDIR
a1.sources.r1.filegroups = f1
a1.sources.r1.filegroups.f1 = /opt/module/applog/log/app.*
a1.sources.r1.positionFile = /opt/module/flume/taildir_position.json
a1.sources.r1.interceptors =  i1
a1.sources.r1.interceptors.i1.type = com.atguigu.gmall.flume.interceptor.ETLInterceptor$Builder

#配置channel
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = 192.168.10.100:9092
a1.channels.c1.kafka.topic = topic_log
a1.channels.c1.parseAsFlumeEvent = false

#组装 
a1.sources.r1.channels = c1

a1.sources.r1.channels = c1

这边设置parseAsFlumeEvent = false后,数据就不会以flume的事件event的形式传递,就没有head了,只有body数据,head虽然对这个离线案例有用,但是如果要弄实时数仓,flink也会到kafka中取数据,这时head对于实时的就没用了。所以这边设置成false,也能减少数据传输的大小。

2、拦截器过滤数据

在source和channel之间设置拦截器,做一个轻度的清洗。

编写Flume拦截器

(1)创建Maven工程flume-interceptor

(2)创建包:com.atguigu.gmall.flume.interceptor

(3)在pom.xml文件中添加如下配置

<dependencies>

    <dependency>

        <groupId>org.apache.flume</groupId>

        <artifactId>flume-ng-core</artifactId>

        <version>1.9.0</version>

        <scope>provided</scope>

    </dependency>



    <dependency>

        <groupId>com.alibaba</groupId>

        <artifactId>fastjson</artifactId>

        <version>1.2.62</version>

    </dependency>

</dependencies>



<build>

    <plugins>

        <plugin>

            <artifactId>maven-compiler-plugin</artifactId>

            <version>2.3.2</version>

            <configuration>

                <source>1.8</source>

                <target>1.8</target>

            </configuration>

        </plugin>

        <plugin>

            <artifactId>maven-assembly-plugin</artifactId>

            <configuration>

                <descriptorRefs>

                    <descriptorRef>jar-with-dependencies</descriptorRef>

                </descriptorRefs>

            </configuration>

            <executions>

                <execution>

                    <id>make-assembly</id>

                    <phase>package</phase>

                    <goals>

                        <goal>single</goal>

                    </goals>

                </execution>

            </executions>

        </plugin>

    </plugins>

</build>

(4)在com.atguigu.gmall.flume.utils包下创建JSONUtil类

package com.atguigu.gmall.flume.utils;



import com.alibaba.fastjson.JSONObject;

import com.alibaba.fastjson.JSONException;



public class JSONUtil {

/*

* 通过异常判断是否是json字符串

* 是:返回true  不是:返回false

* */

    public static boolean isJSONValidate(String log){

        try {

            JSONObject.parseObject(log);

            return true;

        }catch (JSONException e){

            return false;

        }

    }

}

(5)在com.atguigu.gmall.flume.interceptor包下创建ETLInterceptor类

package com.atguigu.gmall.flume.interceptor;



import com.atguigu.gmall.flume.utils.JSONUtil;

import org.apache.flume.Context;

import org.apache.flume.Event;

import org.apache.flume.interceptor.Interceptor;





import java.nio.charset.StandardCharsets;

import java.util.Iterator;

import java.util.List;



public class ETLInterceptor implements Interceptor {



    @Override

    public void initialize() {



    }



    @Override

    public Event intercept(Event event) {



//1、获取body当中的数据并转成字符串

        byte[] body = event.getBody();

        String log = new String(body, StandardCharsets.UTF_8);

//2、判断字符串是否是一个合法的json,是:返回当前event;不是:返回null

        if (JSONUtil.isJSONValidate(log)) {

            return event;

        } else {

            return null;

        }

    }



    @Override

    public List<Event> intercept(List<Event> list) {



        Iterator<Event> iterator = list.iterator();



        while (iterator.hasNext()){

            Event next = iterator.next();

            if(intercept(next)==null){

                iterator.remove();

            }

        }



        return list;

    }


    // a1.sources.r1.interceptors.i1.type 的值是这个的全类名
    public static class Builder implements Interceptor.Builder{



        @Override

        public Interceptor build() {

            return new ETLInterceptor();

        }

        @Override

        public void configure(Context context) {



        }



    }



    @Override

    public void close() {



    }

}

(6)打包

(7)需要先将打好的包放入到flume的lib目录下:/opt/module/flume/lib文件夹下面。

3、启动flume采集验证(f1.sh脚本)

使用上面的配置文件启动flume监控,,

bin/flume-ng agent -n a1 -c conf/ -f job/file_to_kafka.conf -Dflume.root.logger=info,console

接着创建一个Kafka消费者消费topic_log主题

bin/kafka-console-consumer.sh --bootstrap-server 192.168.10.100:9092 --topic topic_log

然后往文件中追加数据看能不能消费到。

看到完整的json被消费了,不完整的json被拦截器过滤了

f1.sh

#!/bin/bash

case $1 in
"start"){
        for i in hadoop102
        do
                echo " --------启动 $i 采集flume(日志到kafka)-------"
                ssh $i "nohup /opt/module/flume/bin/flume-ng agent -n a1 -c /opt/module/flume/conf/ -f /opt/module/flume/job/logfile_to_kf.conf >/dev/null 2>&1 &"
        done
};;
"stop"){
        for i in hadoop102
        do
                echo " --------停止 $i 采集flume(日志到kafka)-------"
                ssh $i "ps -ef | grep logfile_to_kf.conf | grep -v grep |awk  '{print \$2}' | xargs -n1 kill -9 "
        done

};;
esac

日志数据(文件)同步给Hadoop的hdfs

现在数据已经在Kafka了,下一步就是要将数据发给Hadoop存储,并且要按天进行分区。

按照规划,该Flume需将Kafka中topic_log的数据发往HDFS。并且对每天产生的用户行为日志进行区分,将不同天的数据发往HDFS不同天的路径。

1、创建flume消费者

创建flume消费者从Kafka中消费数据发给hdfs。

目前的数据位于kafka中,原本可以直接用下面的这种flume架构,但由于flume的上游将数据存到kafka的时候,只存了body,这边将数据发给hdfs中需要按照时间落盘,所以需要拦截器加上head,给每条数据在head中添加时间信息,但是拦截器需要有flume source才能生效。所以这种架构就不行。需要使用带有source的架构

带有source的架构模式 

拦截器:

    // 必须在在header中添加名为timestamp字段的时间戳
    @Override
    public Event intercept(Event event) {
        Map<String, String> headers = event.getHeaders();
        byte[] body = event.getBody();
        String log = new String(body, StandardCharsets.UTF_8);

        String ts = JSONObject.parseObject(log).getString("ts");
        headers.put("timestamp",ts);
        return event;
    }

flume配置文件:

#定义组件
a1.sources=r1
a1.channels=c1
a1.sinks=k1

#配置source1
a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
a1.sources.r1.batchSize = 2000
a1.sources.r1.batchDurationMillis = 2000
a1.sources.r1.kafka.bootstrap.servers = 192.168.10.100:9092
a1.sources.r1.kafka.topics=topic_log
a1.sources.r1.kafka.consumer.group.id = topic_log
a1.sources.r1.interceptors = i1
a1.sources.r1.interceptors.i1.type = com.atguigu.gmall.flume.interceptor.TimestampInterceptor$Builder

#配置channel
a1.channels.c1.type = file
a1.channels.c1.checkpointDir = /opt/module/flume/checkpoint/behavior1
a1.channels.c1.dataDirs = /opt/module/flume/data/behavior1
a1.channels.c1.maxFileSize = 2146435071
a1.channels.c1.capacity = 1000000
a1.channels.c1.keep-alive = 6

#配置sink
a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.path = /origin_data/gmall/log/topic_log/%Y-%m-%d
a1.sinks.k1.hdfs.filePrefix = log
a1.sinks.k1.hdfs.round = false


a1.sinks.k1.hdfs.rollInterval = 10
a1.sinks.k1.hdfs.rollSize = 134217728
a1.sinks.k1.hdfs.rollCount = 0

#控制输出文件类型
a1.sinks.k1.hdfs.fileType = CompressedStream
a1.sinks.k1.hdfs.codeC = gzip

#组装 
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1

1、数据位于kafka中source用kafka source

kafka source其实就是一个kafka消费者,定义消费者组id,防止使用默认的组id导致消费不到数据,如果有两个消费者都消费toppic_id主题,同一个消费者组id一样的只有一个消费者能消费到。

a1.sources.r1.batchSize = 2000,一次批量写入channel通道的最大消息数。
a1.sources.r1.batchDurationMillis = 2000,若没达到一批次的消息数量,达到这个时间了也将消息都发给channel通道。这时间设置成产生2000条大概花费的时间。

2、Channel用file channel,我猜测:是由于要发送给hdfs,又因为hdfs是文件系统

通过配置dataDirs指向多个路径,每个路径对应不同的硬盘,flume就可以将来自Source的数据写到不同的目录硬盘,但是这边是单机,就设置了一个,可以增大Flume吞吐量。

a1.channels.c1.maxFileSize = 2146435071,file channel数据存储在文件中,                单个日志文件的最大大小(以字节计)。

a1.channels.c1.capacity = 1000000,file channel的最大容量 1000000条
a1.channels.c1.keep-alive = 6

回滚后,source要重新到文件或者kafka中取这2000条数据 

3、数据发给HDFS所以sink用hdfs sink

a1.sinks.k1.hdfs.path = /origin_data/gmall/log/topic_log/%Y-%m-%d

path中包含时间的转移序列,用于将不同时间的数据放到不同的路径。

基于以上hdfs.rollInterval=10:hdfs当达到10秒后滚动形成文件

hdfs.rollSize=134217728:hdfs数据当达到128M形成文件

hdfs.rollCount =0:event事件条数达到多少条形成文件

几个参数综合作用,效果如下:

(1)文件在达到128M时会滚动生成新文件

(2)文件创建超3600秒时会滚动生成新文件

还没达到形成新文件的时候,是以.tmp结尾存在的,这个时候是没用的。

2、启动flume消费者 f2.sh脚本

进入flume的家目录下执行:

bin/flume-ng agent -n a1 -c conf/ -f job/kafka_to_hdfs_log.conf -Dflume.root.logger=info,console

效果:

效果分析:文件一有新的日志数据写入,就会被flume采集到kafka的topic_log主题中,就会被flume消费者发到hdfs中的路径文件中。这样会有几个问题:

  1. 一有数据就发给hdfs中形成一个文件,就会产生大量的小文件,上面每个文件就几百B大小。

    元数据层面:每个小文件都有一份元数据,其中包括文件路径,文件名,所有者,所属组,权限,创建时间等,这些信息都保存在Namenode内存中。所以小文件过多,会占用Namenode服务器大量内存,影响Namenode性能和使用寿命

    计算层面:默认情况下MR会对每个小文件启用一个Map任务计算,非常影响计算性能。同时也影响磁盘寻址时间。

  2. 数据漂移问题:

 加入拦截器解决数据漂移、修改参数解决小文件问题后:

可以看到现在起码不是几十B了,因为现在时间10秒就形成新文件,到时候可以根据128M生成的时间设置。 

现在这条数据链路已经打通了。 

=====================

业务数据(MySQL)到HDFS

在离线数仓中,业务数据是很重要的一个来源,为后续的计算提供数据来源,离线数仓一般一天采集同步一次业务数据到离线数仓中,供后续使用(存储、计算、处理、分析)。

1、数据同步方案

同步的策略有增量同步(效率好、逻辑复杂)和全量同步(数据量大变化少时效率低、逻辑简单)。增量同步就是只将有变更的数据同步过来;而全量同步是每次都将全表同步过来,覆盖原有的数据。一般而言一个数据库中:大表变化多全量、大表变化少增量、小表都用全量。

同步策略

优点

缺点

全量同步

逻辑简单

在某些情况下效率较低。例如某张表数据量较大,但是每天数据的变化比例很低,若对其采用每日全量同步,则会重复同步和存储大量相同的数据。

增量同步

效率高,无需同步和存储重复数据

逻辑复杂,需要将每日的新增及变化数据同原来的数据进行整合,才能使用

全量同步通常使用DataX、Sqoop等基于查询的离线同步工具。而增量同步既可以使用DataX、Sqoop等工具,也可使用Maxwell、Canal等工具,下面对增量同步不同方案进行简要对比。

增量同步方案

DataX/Sqoop

Maxwell/Canal

对数据库的要求

原理是基于查询,故若想通过select查询获取新增及变化数据,就要求数据表中存在create_time、update_time字段,然后根据这些字段获取变更数据。

要求数据库记录变更操作,例如MySQL需开启binlog。

数据的中间状态

由于是离线批量同步,故若一条数据在一天中变化多次,该方案只能获取最后一个状态,中间状态无法获取。

由于是实时获取所有的数据变更操作,所以可以获取变更数据的所有中间状态。

2、各个表同步策略

一般而言一个数据库中:大表变化多全量、大表变化少增量、小表都用全量。

2.1、部署DataX全量同步数据

使用DataX全量同步数据给HDFS。

1、正常步骤需要为每个全量同步的表各自创建一个DataX任务的json文件,每个表都由公主和王子来写json文件,实在是有点麻烦,直接搞个脚本自动生成(如果报错把注释去掉):

# ecoding=utf-8
import json
import getopt
import os
import sys
import MySQLdb

#MySQL相关配置,需根据实际情况作出修改
mysql_host = "hadoop102"
mysql_port = "3306"
mysql_user = "root"
mysql_passwd = "000000"

#HDFS NameNode相关配置,需根据实际情况作出修改
hdfs_nn_host = "hadoop102"
hdfs_nn_port = "8020"

#生成DataX配置文件的目标路径,可根据实际情况作出修改
output_path = "/opt/module/datax/job/import"


def get_connection():
    return MySQLdb.connect(host=mysql_host, port=int(mysql_port), user=mysql_user, passwd=mysql_passwd)


def get_mysql_meta(database, table):
    connection = get_connection()
    cursor = connection.cursor()
    sql = "SELECT COLUMN_NAME,DATA_TYPE from information_schema.COLUMNS WHERE TABLE_SCHEMA=%s AND TABLE_NAME=%s ORDER BY ORDINAL_POSITION"
    cursor.execute(sql, [database, table])
    fetchall = cursor.fetchall()
    cursor.close()
    connection.close()
    return fetchall


def get_mysql_columns(database, table):
    return map(lambda x: x[0], get_mysql_meta(database, table))


def get_hive_columns(database, table):
    def type_mapping(mysql_type):
        mappings = {
            "bigint": "bigint",
            "int": "bigint",
            "smallint": "bigint",
            "tinyint": "bigint",
            "decimal": "string",
            "double": "double",
            "float": "float",
            "binary": "string",
            "char": "string",
            "varchar": "string",
            "datetime": "string",
            "time": "string",
            "timestamp": "string",
            "date": "string",
            "text": "string"
        }
        return mappings[mysql_type]

    meta = get_mysql_meta(database, table)
    return map(lambda x: {"name": x[0], "type": type_mapping(x[1].lower())}, meta)


def generate_json(source_database, source_table):
    job = {
        "job": {
            "setting": {
                "speed": {
                    "channel": 3
                },
                "errorLimit": {
                    "record": 0,
                    "percentage": 0.02
                }
            },
            "content": [{
                "reader": {
                    "name": "mysqlreader",
                    "parameter": {
                        "username": mysql_user,
                        "password": mysql_passwd,
                        "column": get_mysql_columns(source_database, source_table),
                        "splitPk": "",
                        "connection": [{
                            "table": [source_table],
                            "jdbcUrl": ["jdbc:mysql://" + mysql_host + ":" + mysql_port + "/" + source_database]
                        }]
                    }
                },
                "writer": {
                    "name": "hdfswriter",
                    "parameter": {
                        "defaultFS": "hdfs://" + hdfs_nn_host + ":" + hdfs_nn_port,
                        "fileType": "text",
                        "path": "${targetdir}",
                        "fileName": source_table,
                        "column": get_hive_columns(source_database, source_table),
                        "writeMode": "append",
                        "fieldDelimiter": "\t",
                        "compress": "gzip"
                    }
                }
            }]
        }
    }
    if not os.path.exists(output_path):
        os.makedirs(output_path)
    with open(os.path.join(output_path, ".".join([source_database, source_table, "json"])), "w") as f:
        json.dump(job, f)


def main(args):
    source_database = ""
    source_table = ""

    options, arguments = getopt.getopt(args, '-d:-t:', ['sourcedb=', 'sourcetbl='])
    for opt_name, opt_value in options:
        if opt_name in ('-d', '--sourcedb'):
            source_database = opt_value
        if opt_name in ('-t', '--sourcetbl'):
            source_table = opt_value

    generate_json(source_database, source_table)


if __name__ == '__main__':
    main(sys.argv[1:])

注:由于目标路径包含一层日期,用于对不同天的数据加以区分,故path参数并未写死,需在提交任务时通过参数动态传入,参数名称为targetdir

2、使用方式:

安装Python Mysql驱动由于需要使用Python访问Mysql数据库,故需安装驱动,命令如下:

sudo yum install -y MySQL-python

脚本使用说明

python gen_import_config.py -d database -t table

通过-d传入数据库名,-t传入表名,执行上述命令即可生成该表的DataX同步配置文件。

3、每个表的json文件都要这样执行也比较,直接再弄个脚本为每个表生成:

#!/bin/bash

python ~/bin/gen_import_config.py -d gmall -t activity_info
python ~/bin/gen_import_config.py -d gmall -t activity_rule
python ~/bin/gen_import_config.py -d gmall -t base_category1
python ~/bin/gen_import_config.py -d gmall -t base_category2
python ~/bin/gen_import_config.py -d gmall -t base_category3
python ~/bin/gen_import_config.py -d gmall -t base_dic
python ~/bin/gen_import_config.py -d gmall -t base_province
python ~/bin/gen_import_config.py -d gmall -t base_region
python ~/bin/gen_import_config.py -d gmall -t base_trademark
python ~/bin/gen_import_config.py -d gmall -t cart_info
python ~/bin/gen_import_config.py -d gmall -t coupon_info
python ~/bin/gen_import_config.py -d gmall -t sku_attr_value
python ~/bin/gen_import_config.py -d gmall -t sku_info
python ~/bin/gen_import_config.py -d gmall -t sku_sale_attr_value
python ~/bin/gen_import_config.py -d gmall -t spu_info

4、测试生产的配置文件是否可用

由于DataX同步任务要求目标路径提前存在,故需手动创建路径,当前activity_info表的目标路径应为/origin_data/gmall/db/activity_info_full/2020-06-14。命令不行可以手动创建。

hadoop fs -mkdir /origin_data/gmall/db/activity_info_full/2020-06-14

执行DataX同步命令

$ python /opt/module/datax/bin/datax.py -p"-Dtargetdir=/origin_data/gmall/db/activity_info_full/2020-06-14" /opt/module/datax/job/import/gmall.activity_info.json

python /opt/module/datax/bin/datax.py -p"-Dtargetdir=/origin_data/gmall/db/activity_info_full/2020-06-14" /opt/module/datax/job/import/gmall.activity_info.json

5、观察结果

观察HFDS目标路径是否出现数据。

6、全量同步脚本

#!/bin/bash

DATAX_HOME=/opt/module/datax

# 如果传入日期则do_date等于传入的日期,否则等于前一天日期
if [ -n "$2" ] ;then
    do_date=$2
else
    do_date=`date -d "-1 day" +%F`
fi

#处理目标路径,此处的处理逻辑是,如果目标路径不存在,则创建;若存在,则清空,目的是保证同步任务可重复执行
handle_targetdir() {
  hadoop fs -test -e $1
  if [[ $? -eq 1 ]]; then
    echo "路径$1不存在,正在创建......"
    hadoop fs -mkdir -p $1
  else
    echo "路径$1已经存在"
    fs_count=$(hadoop fs -count $1)
    content_size=$(echo $fs_count | awk '{print $3}')
    if [[ $content_size -eq 0 ]]; then
      echo "路径$1为空"
    else
      echo "路径$1不为空,正在清空......"
      hadoop fs -rm -r -f $1/*
    fi
  fi
}

#数据同步
import_data() {
  datax_config=$1
  target_dir=$2

  # 先在HDFS中创建目录
  handle_targetdir $target_dir
  python $DATAX_HOME/bin/datax.py -p"-Dtargetdir=$target_dir" $datax_config
}

case $1 in
"activity_info")
  import_data /opt/module/datax/job/import/gmall.activity_info.json /origin_data/gmall/db/activity_info_full/$do_date
  ;;
"activity_rule")
  import_data /opt/module/datax/job/import/gmall.activity_rule.json /origin_data/gmall/db/activity_rule_full/$do_date
  ;;
"base_category1")
  import_data /opt/module/datax/job/import/gmall.base_category1.json /origin_data/gmall/db/base_category1_full/$do_date
  ;;
"base_category2")
  import_data /opt/module/datax/job/import/gmall.base_category2.json /origin_data/gmall/db/base_category2_full/$do_date
  ;;
"base_category3")
  import_data /opt/module/datax/job/import/gmall.base_category3.json /origin_data/gmall/db/base_category3_full/$do_date
  ;;
"base_dic")
  import_data /opt/module/datax/job/import/gmall.base_dic.json /origin_data/gmall/db/base_dic_full/$do_date
  ;;
"base_province")
  import_data /opt/module/datax/job/import/gmall.base_province.json /origin_data/gmall/db/base_province_full/$do_date
  ;;
"base_region")
  import_data /opt/module/datax/job/import/gmall.base_region.json /origin_data/gmall/db/base_region_full/$do_date
  ;;
"base_trademark")
  import_data /opt/module/datax/job/import/gmall.base_trademark.json /origin_data/gmall/db/base_trademark_full/$do_date
  ;;
"cart_info")
  import_data /opt/module/datax/job/import/gmall.cart_info.json /origin_data/gmall/db/cart_info_full/$do_date
  ;;
"coupon_info")
  import_data /opt/module/datax/job/import/gmall.coupon_info.json /origin_data/gmall/db/coupon_info_full/$do_date
  ;;
"sku_attr_value")
  import_data /opt/module/datax/job/import/gmall.sku_attr_value.json /origin_data/gmall/db/sku_attr_value_full/$do_date
  ;;
"sku_info")
  import_data /opt/module/datax/job/import/gmall.sku_info.json /origin_data/gmall/db/sku_info_full/$do_date
  ;;
"sku_sale_attr_value")
  import_data /opt/module/datax/job/import/gmall.sku_sale_attr_value.json /origin_data/gmall/db/sku_sale_attr_value_full/$do_date
  ;;
"spu_info")
  import_data /opt/module/datax/job/import/gmall.spu_info.json /origin_data/gmall/db/spu_info_full/$do_date
  ;;
"all")
  import_data /opt/module/datax/job/import/gmall.activity_info.json /origin_data/gmall/db/activity_info_full/$do_date
  import_data /opt/module/datax/job/import/gmall.activity_rule.json /origin_data/gmall/db/activity_rule_full/$do_date
  import_data /opt/module/datax/job/import/gmall.base_category1.json /origin_data/gmall/db/base_category1_full/$do_date
  import_data /opt/module/datax/job/import/gmall.base_category2.json /origin_data/gmall/db/base_category2_full/$do_date
  import_data /opt/module/datax/job/import/gmall.base_category3.json /origin_data/gmall/db/base_category3_full/$do_date
  import_data /opt/module/datax/job/import/gmall.base_dic.json /origin_data/gmall/db/base_dic_full/$do_date
  import_data /opt/module/datax/job/import/gmall.base_province.json /origin_data/gmall/db/base_province_full/$do_date
  import_data /opt/module/datax/job/import/gmall.base_region.json /origin_data/gmall/db/base_region_full/$do_date
  import_data /opt/module/datax/job/import/gmall.base_trademark.json /origin_data/gmall/db/base_trademark_full/$do_date
  import_data /opt/module/datax/job/import/gmall.cart_info.json /origin_data/gmall/db/cart_info_full/$do_date
  import_data /opt/module/datax/job/import/gmall.coupon_info.json /origin_data/gmall/db/coupon_info_full/$do_date
  import_data /opt/module/datax/job/import/gmall.sku_attr_value.json /origin_data/gmall/db/sku_attr_value_full/$do_date
  import_data /opt/module/datax/job/import/gmall.sku_info.json /origin_data/gmall/db/sku_info_full/$do_date
  import_data /opt/module/datax/job/import/gmall.sku_sale_attr_value.json /origin_data/gmall/db/sku_sale_attr_value_full/$do_date
  import_data /opt/module/datax/job/import/gmall.spu_info.json /origin_data/gmall/db/spu_info_full/$do_date
  ;;
esac

使用方式:

./mysql_to_hdfs_full.sh all 2023-10-25

all表示全量同步脚本设置的所有表,第二个参数是创建的文件夹时间,如果不传默认取前一天的时间,比如今天是2023年10月25日,则创建2023-10-24文件夹存放数据,生产环境中,都是凌晨1点多开始全量同步前面一天的数据。所以生产中第二个参数不传。

2.2、Maxwell增量同步数据

使用Maxwell增量同步业务数据到kafka,再由Flume采集到HDFS

1、创建一个Maxwell增量同步MySQL中需要增量同步的业务表,发送给kafka的topic_db主题

 

如果MySQL的端口不是3306,Maxwell的配置文件记得加上

mxw.sh start

2、由于有些表是全量同步,所以需要在MySQL的配置文件中将全量同步的表去掉bin_log

3、创建flume消费topic_db主题发送给hdfs

flume配置文件:

#定义组件
a1.sources=r1
a1.channels=c1
a1.sinks=k1

#配置source1
a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
a1.sources.r1.batchSize = 2000
a1.sources.r1.batchDurationMillis = 2000
a1.sources.r1.kafka.bootstrap.servers = 192.168.10.100:9092
a1.sources.r1.kafka.topics=topic_db
a1.sources.r1.kafka.consumer.group.id = topic_db
a1.sources.r1.interceptors = i1
a2.sources.r1.interceptors.i1.type = com.atguigu.gmall.flume.interceptor.TableNameTimestampInterceptor$Builder

#配置channel
a1.channels.c1.type = file
a1.channels.c1.checkpointDir = /opt/module/flume/checkpoint/behavior2
a1.channels.c1.dataDirs = /opt/module/flume/data/behavior2
a1.channels.c1.maxFileSize = 2146435071
a1.channels.c1.capacity = 1000000
a1.channels.c1.keep-alive = 6

#配置sink
a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.path = /origin_data/gmall/db/%{tableName}_inc/%Y-%m-%d
a1.sinks.k1.hdfs.filePrefix = db
a1.sinks.k1.hdfs.round = false


a1.sinks.k1.hdfs.rollInterval = 10
a1.sinks.k1.hdfs.rollSize = 134217728
a1.sinks.k1.hdfs.rollCount = 0

#控制输出文件类型
a1.sinks.k1.hdfs.fileType = CompressedStream
a1.sinks.k1.hdfs.codeC = gzip

#组装 
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1

a1.sinks.k1.hdfs.path = /origin_data/gmall/db/%{tableName}_inc/%Y-%m-%d

由于在hdfs中落盘的需要按照以上的格式,而%{tableName} 这种,hdfs会到event的头部中找tableName来解析,%Y-%m-%d会找timestamp的值解析。以下是一条event格式,但是如果不设置的话会按默认的时间。

timestamp^R^M1698288936059^R­^B{"database":"gmall","table":"comment_info","type":"insert","ts":1698288933,"xid":912,"commit":true,"data":{"id":1716761825009860616,"user_id":13,"nick_name":null,"head_img":null,"sku_id":12,"spu_id":25,"order_id":null,"appraise":null,"comment_txt":"æµ<8b>è¯<95>1219","create_time":null,"operate_time":null}}^Qib<8e>)^@^@^@^@^@^?^@^@^@^_^W^M^D^@^@^@^Q©^?äi<8b>^A^@^@^Yr^?äi<8b>^A^@^@^E^M^A^@^@^@^@^?^@^@^@$^W^M^B^@^@^@^Qª^?äi<8b>^A^@

以下是Maxwell增量同步MySQL的一条数据,其中ts字段需要个性化定制Maxwell才能生成,ts是当时监控到这条数据变更的时间,可以将这个时间设置给event的头部timestamp。

{
    "database": "gmall",
    "table": "comment_info",
    "type": "insert",
    "ts": 1698288933,
    "xid": 912,
    "commit": true,
    "data": {
        "id": 1716761825009860616,
        "user_id": 13,
        "nick_name": null,
        "head_img": null,
        "sku_id": 12,
        "spu_id": 25,
        "order_id": null,
        "appraise": null,
        "comment_txt": "æµ<8b>è¯<95>1219",
        "create_time": null,
        "operate_time": null
    }
}

拦截器

// flume采集的每条数据 event
    @Override
    public Event intercept(Event event) {
        Map<String, String> headers = event.getHeaders();
        byte[] body = event.getBody();
        String db = new String(body, StandardCharsets.UTF_8);

        // 获取Maxwell输出的时间戳 单位是秒
        Long ts = JSONObject.parseObject(db).getLong("ts");
        String table = JSONObject.parseObject(db).getString("table");

        // flume的hdfs sink解析需要 毫秒
        headers.put("timestamp", String.valueOf(ts * 1000));
        headers.put("tableName",table);
        return event;
    }

4、增量表首日全量

通常情况下,增量表需要在首日(首次)进行一次全量同步,后续每日再进行增量同步,首日全量同步可以使用Maxwell的bootstrap功能,方便起见,下面编写一个增量表首日全量同步脚本。

#!/bin/bash

# 该脚本的作用是初始化所有的增量表,只需执行一次

MAXWELL_HOME=/opt/module/maxwell

import_data() {
    $MAXWELL_HOME/bin/maxwell-bootstrap --database gmall --table $1 --config $MAXWELL_HOME/config.properties
}

case $1 in
"cart_info")
  import_data cart_info
  ;;
"comment_info")
  import_data comment_info
  ;;
"coupon_use")
  import_data coupon_use
  ;;
"favor_info")
  import_data favor_info
  ;;
"order_detail")
  import_data order_detail
  ;;
"order_detail_activity")
  import_data order_detail_activity
  ;;
"order_detail_coupon")
  import_data order_detail_coupon
  ;;
"order_info")
  import_data order_info
  ;;
"order_refund_info")
  import_data order_refund_info
  ;;
"order_status_log")
  import_data order_status_log
  ;;
"payment_info")
  import_data payment_info
  ;;
"refund_payment")
  import_data refund_payment
  ;;
"user_info")
  import_data user_info
  ;;
"all")
  import_data cart_info
  import_data comment_info
  import_data coupon_use
  import_data favor_info
  import_data order_detail
  import_data order_detail_activity
  import_data order_detail_coupon
  import_data order_info
  import_data order_refund_info
  import_data order_status_log
  import_data payment_info
  import_data refund_payment
  import_data user_info
  ;;
esac

离线数仓环境准备

现在日志数据和业务数据都采集过来了,是位于hdfs的文件中,需要把数据加入到我们数据仓库中,第一步先加入到hive中。

1、hive安装

大数据-hive-CSDN博客

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
一、项目简介 本项目教程以国内电商巨头实际业务应用场景为依托,同时以阿里云ECS服务器为技术支持,紧跟大数据主流场景,对接企业实际需求,对电商数的常见实战指标进行了详尽讲解,让你迅速成长,获取最前沿的技术经验。 二、项目架构 版本框架:Flume、DateHub、DataWorks、MaxCompute、MySql以及QuickBI等; Flume:大数据领域被广泛运用的日志采集框架; DateHub:类似于传统大数据解决方案中Kafka的角色,提供了一个数据队列功能。对于离线计算,DataHub除了供了一个缓冲的队列作用。同时由于DataHub提供了各种与其他阿里云上下游产品的对接功能,所以DataHub又扮演了一个数据的分发枢纽工作; 据上传和下载通道,提供SQL及MapReduce等多种计算分析服务,同时还提供完善的安全解决方案; DataWorks:是基于MaxCompute计算引擎,从工作室、车间到工具集都齐备的一站式大数据工厂,它能帮助你快速完成数据集成、开发、治理、服务、质量、安全等全套数据研发工作; QuickBI & DataV:专为云上用户量身打造的新一代智能BI服务平台。 三、项目场景 数项目广泛应用于大数据领域,该项目技术可以高度适配电商、金融、医疗、在线教育、传媒、电信、交通等各领域; 四、项目特色 本课程结合国内多家企业实际项目经验。从集群规模的确定到框架版本选型以及服务器选型,手把手教你从零开始搭建基于阿里云服务器的大数据集群。采用阿里云ECS服务器作为数据平台,搭建高可用的、高可靠的Flume数据采集通道,运用阿里云DateHub构建中间缓冲队列并担任数据分发枢纽将数据推送至阿里自主研发的DataWorks对数据进行分层处理,采用MaxCompute作为处理海量数据的方案,将计算结果保存至MySQL并结合阿里的QuickBI工作做最终数据展示。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

躺着听Jay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值