hive离线数仓数据采集——基于canal的binlog数据同步方案

1.简介

在离线数仓中,我们常常会把DB数据以及日志数据抽取到数仓的ODS层。关于DB数据抽取我们一般采用DataX直连mysql select * 的方式,这种方法在业务初期数据量较小的时候不会对业务DB造成什么影响,但是随着数据量的增加,问题就逐渐暴露出来。
缺点为:
1)容易产生慢查询,会影响线上业务
2)抽取时间过长,无法满足数仓生产的时效性
为了解决这些痛点,我们可以采用 history_data + binlog_data (‘binlog实时采集变化数据’ merge ‘历史数据’)的方案,通过阿里的开源项目Canal,从MySQL实时拉取Binlog并完成解析合并。

2.方案架构

方案架构
整体的架构如上图所示。在Binlog实时采集方面,采用了阿里巴巴的开源项目Canal,负责从MySQL实时拉取Binlog并完成适当解析。之后由数据平台组的同学负责将数据放到hdfs对应路径下,接着再由数仓同学做merge操作。详细canal操作网上教程已经很多了,本文就不做过多解释啦!

3.离线还原数据

3.1.数据落盘至hdfs

由于Canal采集的时候订阅的是整个mysql库的binlog,因此每个数据库的binlog日志会存储在同一个文件中 。我们以天为单位,每天产生一个文件。通过hadoop fs -ls 我们可以看到数据已经产生了。
在这里插入图片描述

3.2 Merge操作

mysql中支持增、删、改,我们的binlog会将这3种操作记录下来。但是需要知道的是hive不支持删、改操作,因此对于这种操作我们需要进行特殊的处理。
我们的思路是
1)将当天产生的binlog数据 (INSERT、UPDATE)与历史数据合并
2)通过对比历史数据,找出每个id最后一条更新的记录
3)将binlog中DELETE的数据过滤删除

3.3 Merge sql 代码

3.3.1 首先创建一个快照表来存放test库的binlog日志


CREATE EXTERNAL TABLE IF NOT EXISTS tmp_ods_zhidao_binlog_test_df
(
      content   string   COMMENT '日志内容'
)
COMMENT 'binlog同步测试表'
PARTITIONED BY (dt STRING)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
STORED AS INPUTFORMAT 'com.hadoop.mapred.DeprecatedLzoTextInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION '/user/hive/warehouse/zhidao.db/tmp_ods_zhidao_binlog_test_df';

3.3.2 创建一个待还原的ods层hive表

CREATE EXTERNAL TABLE IF NOT EXISTS temp_zhidao_test_car_poi
(
      id                                    STRING COMMENT 'id'
     ,name                                  STRING COMMENT '姓名'
)
COMMENT '测试test_car_poi'
PARTITIONED BY (dt STRING )
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
STORED AS orc
LOCATION '/user/hive/warehouse/zhidao.db/temp_zhidao_test_car_poi';

3.3.3 在hive中还原出与mysql相同的数据(binlog+历史数据)

3.3.3.1 binlog demo

# insert
{
    "data":[
        {
            "id":"5",
            "name":"张三"
        }
    ],
    "database":"bigdata_test",
    "es":1574668868000,
    "id":4,
    "isDdl":false,
    "mysqlType":{
        "id":"bigint(20)",
        "name":"char(20)"
    },
    "old":null,
    "pkNames":[
        "id"
    ],
    "sql":"",
    "sqlType":{
        "id":-5,
        "name":"张三"
    },
    "table":"test_car_poi",
    "ts":1574668868389,
    "type":"INSERT"
}
 
 
# update
{
    "data":[
        {
            "name":"李四"
        }
    ],
    "database":"bigdata_test",
    "es":1574668972000,
    "id":5,
    "isDdl":false,
    "mysqlType":{
        "name":"char(20)"
    },
    "old":null,
    "pkNames":null,
    "sql":"",
    "sqlType":{
        "name":"李四"
    },
    "table":"test_car_poi",
    "ts":1574668972601,
    "type":"UPDATE"
}
 
 
# delete
{
    "data":[
        {
            "id":"4"
        }
    ],
    "database":"bigdata_test",
    "es":1574669054000,
    "id":7,
    "isDdl":false,
    "mysqlType":{
        "id":"bigint(20)"
    },
    "old":null,
    "pkNames":[
        "id"
    ],
    "sql":"",
    "sqlType":{
        "id":-5
    },
    "table":"test_car_poi",
    "ts":1574669054657,
    "type":"DELETE"
}

通过demo我们可以看出每种binlog操作都是以json格式存储的。知道了格式我们就有办法处理了。

3.3.3.2 全量数据合并

注意:
1) 这里我们用es字段来判断更新时间
2) 同一个id在每天可能update多次

DROP TABLE IF exists zhidao.temp_zhidao_test_car_poi_20200520_001;
CREATE TABLE IF NOT EXISTS zhidao.temp_zhidao_test_car_poi_20200520_001 AS
-- INSERT UPDATE
select 
       get_json_object(regexp_replace( regexp_replace((get_json_object(content,'$.data')),'\\[',''),'\\]',''),'$.id') as id,
       get_json_object(regexp_replace( regexp_replace((get_json_object(content,'$.data')),'\\[',''),'\\]',''),'$.name') as name
       from_unixtime(floor((get_json_object(content,'$.es'))/1000),'yyyy-MM-dd HH:mm:ss' ) as es
from 	
zhidao.tmp_ods_zhidao_binlog_test_df a    -- 此表存放的是binlog日志
where dt='2020-05-20'  
    and get_json_object(content,'$.table')='test_car_poi'
    and get_json_object(content,'$.type') in ('INSERT' ,'UPDATE') 

union all
-- 前一日旧数据
select 
       id,name,
       concat(date_sub('2020-05-20',1),' 00:00:01') as es    -- 给前一天数据标记一个es
from 	
zhidao.temp_zhidao_test_car_poi a 
where dt=date_sub('2020-05-20',1)  ;

3.3.3.3 写入数据(同时过滤掉mysql中已删除的记录)

INSERT OVERWRITE TABLE zhidao.temp_zhidao_test_car_poi PARTITION(dt='2020-05-20')
SELECT   a.id,a.name  
FROM
  (SELECT *         -- 找出最新的一条记录
   FROM (SELECT  id,name,
                 ROW_NUMBER() OVER (PARTITION BY id  ORDER BY es DESC ) as rn
         FROM zhidao.temp_zhidao_test_car_poi_20200520_001
         ) t
   WHERE rn=1) a
LEFT JOIN   -- 删除delete的数据
  (select 
       get_json_object(regexp_replace( regexp_replace((get_json_object(content,'$.data')),'\\[',''),'\\]',''),'$.id') as id
   from 	
        zhidao.tmp_ods_zhidao_binlog_test_df a 
   where dt='2020-05-20'  
    and get_json_object(content,'$.table')='test_car_poi'
    and get_json_object(content,'$.type')='DELETE'
  ) b on a.id=b.id
WHERE b.id is null    -- 删除delete的数据
;

因为此方法存在ROW_NUMBER()排序,运行时间成本会比较大。如果大家有什么更好的看法,欢迎各位大神一起交流探讨。

猜你喜欢

转载自blog.csdn.net/Lyx_____h/article/details/106335040