【尚硅谷】电商数仓V4.0丨大数据数据仓库项目实战【学习记录】
思考问题?
1. 为什么用hive on spark来处理数据?
2. 什么是零点漂移问题,怎么解决?
数据传输的过程中,是有延迟的。所以会存在将近零点的数据从采取日志的flume经过kafka缓冲,消费kafka数据的flume,再到HDFS中。HDFS会按照linux时间可能会判断出这是第二天的数据。
flume时间拦截器:能够按照日志的时间写入到HDFS中。
3. 事实表有哪几类?
事务型事实表:适用于不会发生变化的业务,同步策略:增量同步
周期型快照事实表:适用于不关心明细操作,只关心结果的业务。全量同步
累计型快照事实表: 适用于会发生周期性变化的业务。新增及变化同步
4. 维度建模的四个过程?
- 选择业务过程:确定有哪些事实表
- 声明粒度: 最小粒度,确认每张事实表中的每行数据所指代的内容
- 确认维度: 确定维度表
- 确认事实: 确定度量值
5.hive在装载dim后,会出现为null值的一行,为什么?
因为hive在insert数据时,会对map的小文件自动合并导致的。
insert会解析成计算任务,MR或者Spark计算任务,会去读取ODS层与商品相关的业务数据。
而这些业务数据是以lzo压缩+其索引存储的
所以hive在从这张表读取数据的时候,会误把这两个文件当成小文件进行合并。
也就是把索引文件误当成数据文件进行合并了。
从而会导致,索引文件没了,就不能切片了。
可以关闭map端的小文件合并选项。
6.为什么要做拉链表
能够更加高效的存储历史状态
1. 数据仓库概念
业务数据,用户行为数据,爬出数据
如何处理数据?
Flume采集用户行为数据(开源免费,使用广泛,简单效率高)
- 日志文件 > Hive
- java也可以实现
Sqoop(底层是hadoop,4个map)处理业务数据
- mysql > Hive
- 可以通过JDBC来实现,
- Hadoop也可以在重写Inputformat接口是使用JDBC的方式来实现
1.1 原始数据备份到ODS中
1.2 DWD完成数据的清洗
1.3 join形成大的分表(DWS,DWT)
join一次去查数据
DWS:按天聚合数据,按照主题聚合
DWT: 累加数据进行存储
1.4 ADS 结果报表
存储在Mysql中
1.5 输出
1.5.1 报表系统
便于展示,柱状图,饼状图,折线图
1.5.2 用户画像
统计类标签
规则类标签
机器学习类标签
1.5.3 推荐系统
构建推荐模型
1.5.4 机器学习
语言,图像,
2. 项目需求及架构设计
2.1 项目需求
产品经理提出的项目需求: 老板,客户,设计等人提出
项目技术如何选型?
框架版本如何选项(Apache,CDH,HDP)?
服务器使用物理机还是云主机?
如何确实集群规模?
- 用户行为数据采集平台搭建,输入是什么样?怎样处理?输出是什么样?需要用kafka做数据缓存吗?
- 业务数据采集平台搭建
- 数据仓库的维度建模 核心
- 分析,设备,会员,商品,地区,活动等电商核心主题,统计的报表指标近100个
- 采用即席查询工具,随时进行指标分析。(临时的指标,快速查询到结果)
- 对集群性能进行监控,发生异常需要报警。
- 元数据管理 hive的元数据, 当前任务挂掉后,会影响哪些后续的任务,任务的影响范围。
- 质量监控(指标差异?是否正常)
- 权限管理(表的级别,字段的级别)
2.2 项目框架
2.2.1 技术选型
数据量大小: 大的 hive, 小的数据量 sql
业务需求:实时?
技术成熟度: 数仓1.0 — 数据中台(大厂) — 数据湖
开发维护成本:物理机(专业运维,对应风扇,地方)和阿里云
总成本预算:
数据采集传输:Flume(文件日志),sqoop(业务数据),Kafka(数据量大的时候,先缓冲到kafka),logstash(也能处理文件日志,ELK框架,数据量小,负责程度不高),DataX(市场占有率和Sqoop55开)
数据存储:MySql(存储的是小量的数据,ADS中的数据,几M的数据),HDFS(能存储海量的数据),Hbase(存储的是KyLin,多维分析,快速查询,结果数据存到Hbase),Redis(实时计算会用到),MongoDB(尤其爬虫数据)
数据计算:Hive(底层走mr,计算慢),Tez(消耗内存大,查询速度快),Spark(部分数据在内存,部分数据在磁盘),Flink,Storm(用来处理实时运算)
数据查询 :Presto(快速查询,Apache用这个),Kylin,Impala(CDH版用这个),Druid和ClickHouse(快速实时查询,实时数仓的查询)
数据可视化: Echarts,Superset,前面两个开源免费,QuickBI(离线数据展示),DataV(快速实时数据展示)
任务调度: Azkaban(简单实用,上手快),Oozie(功能多),DolphinScheduler(国人开发),Airflow(python所写)
集群监控: Zabbix, Prometheus
元数据管理: Atlas(几千个,几万个任务)
权限管理: Ranger, Sentry
2.2.2 系统数据流程设计
Nginx 起负载均衡的作用,均匀分配到每台服务器中。
日志文件保存30天。
kafka 消峰
2.2.3 框架发行版本选型
1)如何选择Apache/CDH/HDP版本?
(1)Apache:运维麻烦,组件间兼容性需要自己调研。(一般大厂使用,技术实力雄厚,有专业的运维人员)(建议使用)
(2)CDH:国内使用最多的版本,但CM不开源,今年开始收费,一个节点1万美金/年。
(3)HDP:开源,可以进行二次开发,但是没有CDH稳定,国内使用较少
2)云服务选择
(1)阿里云的EMR、MaxCompute、Data Works
(2)亚马逊云EMR
(3)腾讯云EMR
(4)华为云EMR
2.2.4 服务器选型
服务器选择物理机还是云主机?
1)物理机:
以128G内存,20核物理CPU,40线程,8THDD和2TSSD硬盘,戴尔品牌单台报价4W出头。一般物理机寿命5年左右。
需要有专业的运维人员,平均一个月1万。电费也是不少的开销。
2)云主机:
云主机:以阿里云为例,差不多相同配置,每年5W。
很多运维工作都由阿里云完成,运维相对较轻松
3)企业选择
金融有钱公司和阿里没有直接冲突的公司选择阿里云
中小公司、为了融资上市,选择阿里云,拉倒融资后买物理机。
有长期打算,资金比较足,选择物理机。
2.2.5 集群规模
1)如何确认集群规模?(假设:每台服务器8T磁盘,128G内存)
(1)每天日活跃用户100万,每人一天平均100条:100万*100条=1亿条
(2)每条日志1K左右,每天1亿条:100000000/1024/1024=约100G
(3)半年内不扩容服务器来算:100G*180天=约18T
(4)保存3副本:18T*3=54T
(5)预留20%~30%Buf=54T/0.7=77T
(6)算到这:约8T*10台服务器
2)如果考虑数仓分层?数据采用压缩?需要重新再计算
2.2.6 集群资源规划设计
在企业中通常会搭建一套生产集群和一套测试集群。生产集群运行生产任务,测试集群用于上线前代码编写和测试。
1)生产集群
(1)消耗内存的分开(nn,rm)
(2)数据传输数据比较紧密的放在一起(Kafka 、Zookeeper)
(3)客户端(hive客户端,spark客户端)尽量放在一到两台服务器上,方便外部访问(风险高一点)
(4)有依赖关系的尽量放到同一台服务器(例如:Hive和Azkaban Executor)
2)测试集群服务器规划
3. 用户行为采集平台
3.2.2 埋点数据上报时机
3.3.6 环境变量配置说明
4.5 采集日志Flume
log->Flume->kafka
# 定义组件
a1.sources=r1
a1.channels=c1
# 配置source(taildirsource)
a1.sources.r1.type =TRILDIR
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
#配置拦截器(ETL数据清洗,判断数据json是否完整)
a1.sources.r1.interceptors=i1
a1.sources.r1.interceptors.i1.type = com.atguigu.flume.interceptor.ETLInterceptor$Builder
# 配置channel
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
# 读取的是在zookeeper中的topic元数据信息,多少个分区多少个副本
a1.channels.c1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092
a1.channels.c1.kafka.topic = topic_log
# 是否保持flume的数据格式 event
a1.channels.c1.parseAsFlumeEvent = false
# 配置sink
# 拼接组件
a1.sources.r1.channels=c1
Flume拦截器
- 继承Interceptor接口
- 实现initialize,intercept,close方法
- 继承Builder 接口
package com.atguigu.flume.interceptor;
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) {
byte[] body = event.getBody();
String log = new String(body, StandardCharsets.UTF_8);
if (JSONUtils.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;
}
public static class Builder implements Interceptor.Builder{
@Override
public Interceptor build() {
return new ETLInterceptor();
}
@Override
public void configure(Context context) {
}
}
@Override
public void close() {
}
}
JSONUtils 类
package com.atguigu.flume.interceptor;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
public class JSONUtils {
public static boolean isJSONValidate(String log){
try {
JSON.parse(log);
return true;
}catch (JSONException e){
return false;
}
}
}
结束进程
ps -ef | grep file-flume-kafka | grep -v grep |awk '{print \$2}' | xargs -n1 kill -9
4.6 消费kafka数据flume
可以 kafka channel + hdfs sink,效率高。
这次用filechannel
FileChannel底层原理
编写Flume时间戳拦截器
package com.atguigu.flume.interceptor;
import com.alibaba.fastjson.JSONObject;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class TimeStampInterceptor implements Interceptor {
@Override
public void initialize() {
}
@Override
public Event intercept(Event event) {
// 将日志拦下,取出header里面的key,取出body里面的对应的日志时间:将ts的值赋值给header的key
// 1. 获取keader里面的key
Map<String, String> headers = event.getHeaders();
// 2. 获取body中的ts
byte[] body = event.getBody();
String log = new String(body, StandardCharsets.UTF_8);
// string 转换成json对象
JSONObject jsonObject = JSONObject.parseObject(log);
String ts = jsonObject.getString("ts");
// 3. 将ts赋值给timestamp
headers.put("timestamp",ts);
return event;
}
@Override
public List<Event> intercept(List<Event> list) {
for (Event event : list) {
intercept(event);
}
return list;
}
public static class Builder implements Interceptor.Builder{
@Override
public Interceptor build() {
return new TimeStampInterceptor();
}
@Override
public void configure(Context context) {
}
}
@Override
public void close() {
}
}
配置conf
# 定义组件
a1.sources = r1
a1.channels = c1
a1.sinks = k1
# 配置source
a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
# 一次消费5000条
a1.sources.r1.batchSize = 5000
# 2秒消费一次
a1.sources.r1.batchDurationMillis = 2000
a1.sources.r1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.sources.r1.kafka.topics=topic_log
a1.sources.r1.interceptors = i1
a1.sources.r1.interceptors.i1.type = com.atguigu.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/
# 配置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
## 编码方式lzop,支持切片
a1.sinks.k1.hdfs.codeC = lzop
## 拼装
a1.sources.r1.channels = c1
a1.sinks.k1.channel= c1
启动命令
/opt/module/flume/bin/flume-ng agent --conf-file /opt/module/flume/conf/kafka-flume-hdfs.conf --name a1
2.电商业务数据采集平台
熟悉业务
SKU(库存量基本单位),
SPU商品信息聚合的最小单位
平台属性,
销售属性
流程:
- 查看各个表名,有啥关系
- 针对每个表,查看表名和行数据
- 了解表结构之后,熟悉了业务流程,其数据是如何发生变化的
- 在开发过程中通过sqoop导入数据时,不可以写*,因为*代表着hive中的表顺序要完全和mysql保持一致,如果mysql字段顺序改了,hive中的数据代表的意思就不一样了。
sql
启动sql
[wanghaha@hadoop102 software]$ sudo systemctl start mysqld
[wanghaha@hadoop102 software]$ sudo systemctl status mysqld
● mysqld.service - MySQL Server
Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
Active: active (running) since 六 2022-04-23 13:12:52 CST; 1 day 4h ago
Docs: man:mysqld(8)
http://dev.mysql.com/doc/refman/en/using-systemd.html
Process: 1477 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS (code=exited, status=0/SUCCESS)
Process: 1037 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS)
Main PID: 1480 (mysqld)
Tasks: 27
CGroup: /system.slice/mysqld.service
└─1480 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid
4月 23 13:12:50 hadoop102 systemd[1]: Starting MySQL Server...
4月 23 13:12:52 hadoop102 systemd[1]: Started MySQL Server.
同步策略
数据同步策略的类型包括:全量同步、增量同步、新增及变化同步、特殊情况
全量表:存储完整的数据。
增量表:存储新增加的数据。
新增及变化表:存储新增加的数据和变化的数据。
特殊表:只需要存储一次。
采用不同的同步策略
编写sqoop同步命令
将mysql中order_info 表数据导入到HDFS的/test路径
全量同步
bin/sqoop import \
--connect jdbc:mysql://hadoop102:3306/gmall \
--username root \
--password 000000 \
--query 'select * from order_info where $CONDITIONS '\
--target-dir /order_info/2020-06-15 \
--delete-target-dir \
--fields-terminated-by '\t' \
--num-mappers 2 \
--split-by id
增量同步
bin/sqoop import \
--connect jdbc:mysql://hadoop102:3306/gmall \
--username root \
--password 000000 \
--query 'select * from order_info where (date_format(create_time,'%Y-%m-%d')='2020-06-15' or date_format(operate_time,'%Y-%m-%d')='2020-06-15') and $CONDITIONS '\
--target-dir /order_info/2020-06-15 \
--delete-target-dir \
--fields-terminated-by '\t' \
--num-mappers 2 \
--split-by id
2.5.2 业务数据每日同步脚本
(1)在/home/atguigu/bin目录下创建
[atguigu@hadoop102 bin]$ vim mysql_to_hdfs_init.sh
添加如下内容:
注意点:
- sql存储的null值和hive中存储的null不一样,hive中存储的形式是/n。Hive中的Null在底层是以“\N”来存储,而MySQL中的Null在底层就是Null,为了保证数据两端的一致性。在导出数据时采用–input-null-string和–input-null-non-string两个参数。导入数据时采用–null-string和–null-non-string
- 要采用lzo压缩且支持压缩,必须提前创建索引
- 需要传入两个参数,第一个是表名,第二个是同步数据的第一天日期
- 在开发过程中通过sqoop导入数据时,不可以写*,因为*代表着hive中的表顺序要完全和mysql保持一致,如果mysql字段顺序改了,hive中的数据代表的意思就不一样了。
#! /bin/bash
APP=gmall
sqoop=/opt/module/sqoop/bin/sqoop
if [ -n "$2" ] ;then
do_date=$2
else
echo "请传入日期参数"
exit
fi
import_data(){
$sqoop import \
--connect jdbc:mysql://hadoop102:3306/$APP \
--username root \
--password 000000 \
--target-dir /origin_data/$APP/db/$1/$do_date \
--delete-target-dir \
--query "$2 where \$CONDITIONS" \
--num-mappers 1 \
--fields-terminated-by '\t' \
--compress \
--compression-codec lzop \
--null-string '\\N' \
--null-non-string '\\N'
hadoop jar /opt/module/hadoop-3.1.3/share/hadoop/common/hadoop-lzo-0.4.20.jar com.hadoop.compression.lzo.DistributedLzoIndexer /origin_data/$APP/db/$1/$do_date
}
import_order_info(){
import_data order_info "select
id,
total_amount,
order_status,
user_id,
payment_way,
delivery_address,
out_trade_no,
create_time,
operate_time,
expire_time,
tracking_no,
province_id,
activity_reduce_amount,
coupon_reduce_amount,
original_total_amount,
feight_fee,
feight_fee_reduce
from order_info"
}
import_coupon_use(){
import_data coupon_use "select
id,
coupon_id,
user_id,
order_id,
coupon_status,
get_time,
using_time,
used_time,
expire_time
from coupon_use"
}
import_order_status_log(){
import_data order_status_log "select
id,
order_id,
order_status,
operate_time
from order_status_log"
}
import_user_info(){
import_data "user_info" "select
id,
login_name,
nick_name,
name,
phone_num,
email,
user_level,
birthday,
gender,
create_time,
operate_time
from user_info"
}
import_order_detail(){
import_data order_detail "select
id,
order_id,
sku_id,
sku_name,
order_price,
sku_num,
create_time,
source_type,
source_id,
split_total_amount,