ods层如何保证和Mysql镜像一致

1.分层机制及分区字段

如何分层:

数仓在ods层之前有一层ods_binlog层,存放所有Mysql同步过来的binlog按天为分区放入表中,其中的数据和Ods层合并后以ctime的日期作为分区字段,动态分区发往指定分区的ods,保证每天的ods层数据始终和mysql截止到昨日24点数据始终一致

如何分区:

为什么要以ctime作为分区字段?因为对于大多数表而言,还是新增变化比较少的,而不经常改变历史数据,也就代表着以ctime作为分区,可以在合并逻辑时取的分区数据量就小,合并的速度就快

那什么时候可以以utime作为分区字段吗?当变化量大时,比如每天都会改变很多不同时间的历史数据,是不是以utime作为分区字段合适?其实并不是,因为昨天刚改了一部分历史,分到了昨天的分区,结果今天又改了,今天不仅改了昨天的还改了很多昨天以前的,其实还是基本上吧整个表的数据全拿了过来,我们要清楚我们分区的目的是为了什么,为了合并的时候尽量取少部分数据对吧,你不管按ctime还是utime,他都是取了基本全部数据,显然不能按这两种方式分区

那对于这类表应该如何分区?要看改变的是近一年新增的数据还是近几个月,假如改变的都是近几个月的数据,或者近几年的数据,那么我们是不是按照月的ctime或者按年作为分区更合适

那如果改变毫无规律怎么办?如果毫无规律而且有大量变化,那不妨我们干脆就不分区了,既然达不到分区的目的,就没必要死板的使用分区机制了对吧

2.合并sql

select 
id,userid,patientid,doctorid,diseaseid,commitidentity,type,deletetype,admincomment,realname,showattach,showrealname4doctor,agree,status,attitude,skill,tag,committime,ctime,utime,ver,dt 
from 
(
    select 
    binlog_type,id,userid,patientid,doctorid,diseaseid,commitidentity,type,deletetype,admincomment,realname,showattach,showrealname4doctor,agree,status,attitude,skill,tag,committime,ctime,utime,ver,dt,
    row_number() over(partition by id order by utime desc,binlog_type desc) rn
    --这里先按utime倒序,再按type倒序,为什么?
    --因为我首先是要取最新的那一份数据,对于新增数据,如果历史同步过来一份,新增同步也过来一份,此时我肯定只要一份,故我需要比对类型
    from 
    (
        select 
        case get_json_object(content,'$.type')
            when 'insert' then 1
            when 'bootstrap-insert' then 1
            --对于新增数据,且没有删除或修改,则不管取历史同步的那一份或insert的那一份都可以故都给值1
            when 'update' then 2
            --对于新增数据,且之后修改了,只看utime就拿到最新了?这里为什么我们还要给他一个值2呢?
            --因为如果刚插入就立马更新,可能utime并不会改变,所以此时我们要去类型为update的
            --注意这里其实埋了一个坑在里边,会不会有一种可能同一个utime发生了两次变化,那时候我们该怎么办?
            when 'delete' then 3
            --对于新增数据且插入后删除,我们则给值3,为什么是3而不是和上面的2位置替换一下呢?
            --因为考虑到了新增数据之后修改再删除的情况,所以给3就可以兼容这多种可能了
        end as binlog_type,
        cast(get_json_object(get_json_object(content,'$.data'),'$.id') as bigint) id,
        --强转为ods层需要的类型,get_json方法拿出来的数据都是string类型故需要强转
        cast(get_json_object(get_json_object(content,'$.data'),'$.userid') as bigint) userid,
        cast(get_json_object(get_json_object(content,'$.data'),'$.patientid') as bigint) patientid,
        cast(get_json_object(get_json_object(content,'$.data'),'$.doctorid') as bigint) doctorid,
        cast(get_json_object(get_json_object(content,'$.data'),'$.diseaseid') as bigint) diseaseid,
        cast(get_json_object(get_json_object(content,'$.data'),'$.commitidentity') as string) commitidentity,
        cast(get_json_object(get_json_object(content,'$.data'),'$.type') as string) type,
        cast(get_json_object(get_json_object(content,'$.data'),'$.deletetype') as tinyint) deletetype,
        cast(get_json_object(get_json_object(content,'$.data'),'$.admincomment') as string) admincomment,
        cast(get_json_object(get_json_object(content,'$.data'),'$.realname') as string) realname,
        cast(get_json_object(get_json_object(content,'$.data'),'$.showattach') as tinyint) showattach,
        cast(get_json_object(get_json_object(content,'$.data'),'$.showrealname4doctor') as tinyint) showrealname4doctor,
        cast(get_json_object(get_json_object(content,'$.data'),'$.agree') as int) agree,
        cast(get_json_object(get_json_object(content,'$.data'),'$.status') as tinyint) status,
        cast(get_json_object(get_json_object(content,'$.data'),'$.attitude') as tinyint) attitude,
        cast(get_json_object(get_json_object(content,'$.data'),'$.skill') as tinyint) skill,
        cast(get_json_object(get_json_object(content,'$.data'),'$.tag') as string) tag,
        case when get_json_object(content,'$.type') != 'bootstrap-insert' 
        then from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.committime'))+28800,'yyyy-MM-dd HH:mm:ss') 
        else get_json_object(get_json_object(content,'$.data'),'$.committime') 
        end as committime,
        --这里为什么要判断是否为boot类型,不是的时候加八小时呢?
        --因为在mysql中如果时间类型是timestamp,那么除了历史同步boot外,其余的时间都有问题,需要加八个小时
        --timestamp不是时间戳吗?在mysql中的时间戳和datetime标准日期格式一致
        --那Mysql那边如果是时间戳格式呢还需要这么做吗?mysql那边timestamp只会是日期格式,要知道我们平时所说的整型时间戳是unix时间戳
        --如果是时间戳格式无法保存在timestamp中,它会存储成int或bigint等类型,到时候不用加八小时
        --那kudu那边如何处理这个?只要是日期格式,utime_unix就置为0,只要是unix时间戳格式就置为1,不管它是int还是什么
        --总的来说只要源数据是timestamp,非Boot,这里就需要加8小时
        case when get_json_object(content,'$.type') != 'bootstrap-insert' 
        then from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.ctime'))+28800,'yyyy-MM-dd HH:mm:ss') 
        else get_json_object(get_json_object(content,'$.data'),'$.ctime') 
        end as ctime,
        case when get_json_object(content,'$.type') != 'bootstrap-insert' 
        then from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.utime'))+28800,'yyyy-MM-dd HH:mm:ss') 
        else get_json_object(get_json_object(content,'$.data'),'$.utime') 
        end as utime,
        cast(get_json_object(get_json_object(content,'$.data'),'$.ver') as int) ver,
        substr(case when get_json_object(content,'$.type') != 'bootstrap-insert'
                    then from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.ctime'))+28800,'yyyy-MM-dd HH:mm:ss')
                    else get_json_object(get_json_object(content,'$.data'),'$.ctime')
                    end,1,10) dt
        from ods_binlog.ods_binlog_basiccomment_avatar_comments_di
        where dt >= date_add(current_date,-1)
        --控制binlog分区>=昨天,只取昨天分区即可Impala测试时current_date需要替换成now
        and get_json_object(content,'$.type') != 'bootstrap-insert'
        and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd HH:mm:ss') >= date_add(current_date,-1)
        and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd HH:mm:ss') < date_add(current_date,0)
        --这三个and条件并不是一定要加的,什么时候可以不加呢?
        --1,如果今天刚历史同步过来的表第一次合并,没必要加and,为什么,这几个条件是控制时间漂移的,控制我只会取到昨日分区里和今日分区里,真正为昨日新增变化的那些binlog
        --而第一次同步,分区里只会有今天的binlog,直接全拿来合并就完事了,甚至连控制dt那行都不需要
        --但如果是第一次同步,非要加这几个条件的话,第一个and一定不要加,因为你直接吧boot过滤掉显然并不合适,
        --这个条件主要是为了方便历史同步后,当天晚上再做同步时,不再拿历史数据再做一次排序而已
        --2,如果历史同步后,当天晚上再做同步时,最好加上第一个and条件,但第二个and和where可以不用加,因为昨天刚历史同步的不会有前天的binlog,也不会有小于昨天的dt
        --但如果后天再做同步时,第一个and就可以省略了,因为今天就不可能再有boot数据了,这个条件可有可无,但其余两个and是一定要有的
        --其实总结一下最通用的方法,首次合并,不要这三个and,第二次合并加上这三个and

        union all
        --binlog和ods同一个分区dt的数据做合并,其实也就是同一个ctime取哪条留下来的问题
        --为什么不用union?因为union要比对,要去重,效率慢,而我通过排序123反而会快
        --但如果首次合并,其实union all下面这么多行都不需要,因为ods表里什么也没有

        select 1 binlog_type,
        --其实这里给小于3的任何一个整数都可以,因为这里肯定是旧数据,目的就是为了不让其在最外层被过滤掉
        id,userid,patientid,doctorid,diseaseid,commitidentity,type,deletetype,admincomment,realname,showattach,showrealname4doctor,agree,status,attitude,skill,tag,committime,ctime,utime,ver,dt 
        from ods.ods_basiccomment_avatar_comments_dic 
        where dt in (
            select 
            distinct substr(
                case when get_json_object(content,'$.type') != 'bootstrap-insert'
                        then from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.ctime'))+28800,'yyyy-MM-dd HH:mm:ss')
                        else get_json_object(get_json_object(content,'$.data'),'$.ctime')
                        end,1,10) dt
            --对查出来的有变化的dt分区进行去重方便比对效率
            from ods_binlog.ods_binlog_basiccomment_avatar_comments_di t1
            where dt >= date_add(current_date,-1)
            and get_json_object(content,'$.type') != 'bootstrap-insert'
            and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd HH:mm:ss') >= date_add(current_date,-1)
            and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd HH:mm:ss') < date_add(current_date,0)
            --这里的过滤条件原因同上,而这部分数据也刚好是有变化的数据,所以可以用来确定产生变化的分区
            --但由于如果首次合并,其实连union all下面这么多行都不需要,所以这里一旦需要用上,就一定不是第一次合并了,这些条件都要有
        )
        --这里不要手贱加表名,他不是表,只是where in()的右括号
    ) t
) tt
where tt.rn = 1 and tt.binlog_type != 3
--我们要的是最新的数据,也就是倒序后排名为1的数据,但如果老数据或新数据被删除我们是需要将其过滤掉的

3.时间漂移问题

首先什么叫时间漂移?时间漂移通俗来讲,就是我昨天的数据结果到来的时候已经超过今天0点了,但他其实属于昨天的新增或变化

那为什么要处理时间漂移呢?因为如果放着不管,我们只合并昨天0点之前的数据,而不管这部分漂移,会导致,昨天该删的数据,我在数仓使用时,他依然存在,昨天变化的数据,我用的时候不是最新的状态

那么如何处理时间漂移呢?我们的合并sql其实内部就已经通过sql的逻辑解决了时间漂移这个问题

首次合并:

where dt >= date_add(current_date,-1)

确定分区大于等于昨天的Binlog分区,我可以拿出什么数据呢?昨天没有分区,那就是今天的分区,其中包括历史数据和合并开始那一刻产生的新增数据

为什么不做时间漂移呢?无所谓时间戳解决时间漂移,因为我首日就是为了检验数据量的准确性,和暂时勉强的使用,卡好昨天ctime即可,今天的数据没必要单独做漂移处理,我明天处理好就可以了

为什么叫暂时勉强的使用呢?因为要知道,昨天之前新增的数据结果今天在我今天合并前删除,那这部分数据我是无论如何都取不到的,所以叫勉强使用,因为正常情况下,我想要的肯定还是截止到昨日24点的全部数据

之后合并:

where dt >= date_add(current_date,-1)

确定分区大于等于昨天的Binlog分区,我可以拿出什么数据呢?我可以拿出昨天的数据和今天截止我调度时间的binlog

这部分数据都包含什么呢?包含:昨日分区:(1前天的漂移数据,2昨天的数据),今日分区:(3昨天漂移的数据,4今天的多余数据)

我要的是什么呢?2和3,所以我就要通过时间戳ts,过滤掉1和4

and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd

HH:mm:ss') >= date_add(current_date,-1)

该条件为了过滤1前天的漂移数据,虽然前天漂移的数据我今天再合并也无所谓,但没必要,反而会影响执行速度

and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd

HH:mm:ss') < date_add(current_date,0)

该条件为了过滤4今天的多余数据,如果没有这个过滤执行了漂移数据有什么危害呢?一是合并浪费时间,二是明天还要再合一次反而ods多了一部分数据来排序,但最重要的是T+1出问题

4.怎么对数?

首次同步检测时最后合并后数据量可能会多于mysql那边,原因是开始合并到查数这个过程中,有数据删除了,但是有时候差几条,你并不知道差这几条是否都是删除的,无法确定是不是合并出问题了

首先先对历史数据:

看maxwell拉取历史数据的条数,进度(process/ total),以Process的为准,和binlog中的bootstrap条数做对比

REFRESH ods_binlog.ods_binlog_healthpal_avatar_vip_healthservicerecords_di;
select count(*) from ods_binlog.ods_binlog_healthpal_avatar_vip_healthservicerecords_di 
where get_json_object(content,'$.type') = 'bootstrap-insert';

为什么process有时候要多于total?total是你开启历史同步时的条数,在同步的过程中可能有新增数据,他一起给拉过来了

那process会比total小吗?比如过程中删除了数据?不会,删除的binlog直接记录在maxwell新增同步里,所以我们才要先同步新增再同步历史

这样数据不是会重复一部分吗?重复无所谓可以合并,但我们的数据不可以丢失

其次再对合并后数据:

取ods中max(ctime), 求ods和mysql端小于等于这个时间的条数,然后取ods_binlog中type为delete且时间刚好大于这个时间的count数,去看这个delete数是否刚好为两者差值来判断数据量是否一致

原理:最大ctime之前插入修改或删除数据,两边一致,ctime之后插入数据,由于我统计的是ctime之前的,虽然此刻mysql肯定会比Ods的多,但我没有算上这部分数据,所以并没有影响,而之后修改数据,由于卡的是ctime,无论怎么改,ctime不会变,因此两边该是几条还是几条,所以这时候我统计的就只有最大ctime之后删除的数据差异了,因为最大ctime后删除,ods的数据条数还是那么多,因为合并已经结束,但mysql那边小于等于该ctime的count是没有计这条的

为什么取max?因为这样可以吧所有数都对上,如果大于这个值,有些数据还没合并过来,不准确,如果小于这个值,有些数据漏了,也不准确

为什么要以ctime而不是utime?因为我不知道合并开始时间是什么时候,只能做取舍,ctime有多少条,合并前就有多少条,除非之后delete,不然不会变;而如果我使用utime的话,如果在这之后更改了数据呢,ods中的条数就会多于mysql在此时间之前的,删除数据两边都没有了,倒是方便没有差异,但如果是修改呢,utime就会变化,一变化,ods这边就比mysql多了,虽然也可以去binlog里过滤update查条数,但是update数据量会比delete多,显然不好控制,而且数据只可能delete一次,但可以update无数次,你怎么控制呢?

REFRESH ods.ods_healthpal_avatar_vip_healthservicerecords_dic;
select max(ctime) from ods.ods_healthpal_avatar_vip_healthservicerecords_dic;
select count(*) from ods.ods_healthpal_avatar_vip_healthservicerecords_dic;
select count(*) from healthpal_avatar.vip_healthservicerecords where ctime <= '2022-08-26 14:54:58'; --mysql中
select count(*) from ods_binlog.ods_binlog_db_vip_healthservicerecords_di where get_json_object(content,'$.type') = 'delete' 
and case when get_json_object(content,'$.type') != 'bootstrap-insert' 
then from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.ctime'))+28800,'yyyy-MM-dd HH:mm:ss') 
else get_json_object(get_json_object(content,'$.data'),'$.ctime') 
end <= '2022-08-26 14:54:58'
and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd HH:mm:ss') > '2022-08-26 14:54:58

直接对比前两个count相减是否等于最后一个count即可

5. 加入调度需要注意的问题

第一天手动执行,binlog中只有当天分区,其中包括历史数据和截止那一刻的数据,会把这部分数据合并排序后放入ods,这样操作大多数时候只是为了检验数据合并情况,其实完全可以直接第二天手动执行,不过需要注意别过滤掉boot同时控制漂移

第二天如果手动执行,binlog中有昨天和今天的分区,其中包括历史数据和昨天的binlog,如果之前是kudu表可能还包括昨天漂移的binlog,虽然数据量大,但白天跑不怎么会占用资源

第二天如果不手动执行,选择凌晨调度执行和上述同理,数据量大,所以晚上可能跑不成功

第三天调度执行,binlog中有昨天分区和昨天漂移的数据,数据量很小,就没有问题

综上,第一天手动执行检验数据,第一天不执行直接第二天手动为了避免数据大晚上跑不动,第二天将其加入到调度任务,第三天正常调度执行即可

但如果数据量过小,我们只需要首天手动执行检测,然后就加入执行计划,第二天就开始调度执行即可

当然就算数据量大,我们也可以选择这么做,首天手动执行检测,然后就加入执行计划,但要过滤boot,第二天直接开始调度执行即可,不过此时最稳妥的方式还是,首日不要加入调度,因为就算首日检测通过了,然后你也过滤boot了,但直接加入调度,你心里没底,不知道给的内存够不够,还是最好第二天手动跑一下最合适,然后再加入调度

总结一句话:首日合并检测后第二天手动合并且过滤boot后没问题了再加入调度任务

6.合并数据时spark任务失败怎么办?

logs1:Caused by: java.net.SocketTimeoutException:Read timed out

logs2:Caused by: org.apache.hadoop.mapred.InvalidInputException: Input path

does not exist: hdfs://HDFSNS/Data/d1/hive/warehouse/ods.db/ods_db_Hdf_userlogintime_dic/dt=2018-06-16

第一次失败因为时间超时,第二次原因是找不到分区目录,这就很奇怪,分区和分区目录都是它自己创建的凭什么找不到?

超时原因是涉及到历史分区过多,需要创建多个分区,超过了大约1000的限制导致任务失败,不妨先去查一下最小的ctime也就是最小的分区是多少,最大的肯定就是今天了

select min(get_json_object(get_json_object(content, '$.data'), '$.ctime')) 
from ods_binlog.ods_binlog_db_hdf_userlogintime_di
where get_json_object(content,'$.type') = 'bootstrap-insert';

查询原因发现跨ctime太多需要动态创建并插入多个分区,验证了我们的猜想,当然直接show partition也是可以的

那为什么第二次尝试报错输入路径找不到呢?原因是第一次超时的时候,会回退,会把所有目录包括下面的文件都删除掉,但元数据里那个分区已经创建好了依然是有的,我们删除所有分区可以解决该问题,但分区太多太麻烦这里可以直接删表重建

由于该表是个外部表,我们不仅要drop删其元数据,还应该将整个表文件删除

如何解决?我把历史数据按ctime分开区间批次导入即可,例如:

通过该sql查询一下ctime的年份或月份分布情况,决定是两年还是三年甚至是某四年共用一个spark任务

select substr(get_json_object(get_json_object(content,'$.data'),'$.ctime'),1,4) as `year`, count(*) from ods_binlog.ods_binlog_baseflow_avatar_baseflows_di group by `year`;

7.批次合并少数据问题

我使用分任务批次合并出现了一个问题,ods的登录表数据量少,和mysql那边对不上,少了四万多

排查方法:和kudu表联查,也就是left join并过滤右表为空,并对日期截取分组求count

结果:刚好少2019和2021年1月1号这两天的数据,也就是我们分的区间边界值

暴力解决:暂时先将这两天也分批次合并进去ods

原因:其实还是非boot的8小时差异在搞鬼,在划分子任务的时候不能只简单的划分日期,因为会牵扯非boot加八小时的问题,会文件覆盖导致少数据,设想一种情况,小于2019年的任务,在执行任务的时候,是不是会对原本加8小时,也就是2019年1月1日8点之前的数据进行写分区文件,把这部分数据写入2019-01-01的分区内,但是大于2019年的任务,又将2019年1月1日8点之后的数据写了一次该分区,那么一个分区重复写了两次,两个任务会相互覆盖文件,最后的结果就是要么少8点之前的数据,要么少8点之后的数据

解决方法一:过滤出boot,最后再合并一次

解决方法二:不过滤boot,划分日期也区分出非boot加八小时

where dt >= date_add(current_date,-1) 
and case when get_json_object(content,'$.type') != 'bootstrap-insert' 
then from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.ctime'))+28800,'yyyy-MM-dd HH:mm:ss') 
else get_json_object(get_json_object(content,'$.data'),'$.ctime') 
end  > '2019' 
and case when get_json_object(content,'$.type') != 'bootstrap-insert' 
then from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.ctime'))+28800,'yyyy-MM-dd HH:mm:ss') 
else get_json_object(get_json_object(content,'$.data'),'$.ctime') 
end < '2021'

8.合并少数据问题(kafka分区相关)

问题:ods的servicecards数据量少,和mysql那边对不上,少了2

排查方法:利用二分法卡ctime将binlog端的boot和mysql端的对数,观察少了哪部分数据

结果:在临近max ctime的九秒内少了两条数据,且时间交错

原因分析:合并时有一部分binlog在kafka的其他分区,有一定延时,没有及时的拿到这部分数据

9.少数据一定是kafka拉取延迟吗?

问题:第二次合并对数据的时候发现数据量对不上,这时候可能存在两个原因

设想原因:

1.ts延迟,此时缺失数据的类型为insert的binlog中ts比ctime大

2.kafka分区拉取延迟,可能是拉取的时候有个别分区没有拉取到就开始合并操作,此时缺失数据靠近24点

先二分法确定少数据的区间,再找出binlog中该区间内的数据和Mysql对比看是少哪条

select * from ods_binlog_baseflow_avatar_baseflows_di
where get_json_object(content,'$.type') != 'bootstrap-insert' 
--控制非boot,因为boot加八小时会干扰结果,而且我们也用不上
and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd HH:mm:ss') >= '2022-09-13'        
and from_unixtime(cast(get_json_object(content,'$.ts') as bigint),'yyyy-MM-dd HH:mm:ss') < '2022-09-14'
--控制时间漂移日期,因为不想之后的binlog对其干扰
and from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.ctime'))+28800,'yyyy-MM-dd HH:mm:ss')  > '2022-09-13 23:59:00'         
and from_unixtime(unix_timestamp(get_json_object(get_json_object(content,'$.data'),'$.ctime'))+28800,'yyyy-MM-dd HH:mm:ss') <= '2022-09-14 00:00:00';
--控制少数据的ctime区间

观察这个数据最早的ts和其binlog中的ctime,判断是ts延迟了还是streaming拉取kafka分区没把他拉上去

select * from ods_binlog_baseflow_avatar_baseflows_di
where get_json_object(get_json_object(content,'$.data'),'$.id') = '6498536239'
order by get_json_object(content,'$.ts');

发现果然是ts竟然大于ctime一秒,也就是说ctime还是23:59:59但ts为第二天的0点

解决方法:合并时候让ts多漂移1秒,也就是取到0点

10.如果Kudu里已经有的表,想要在ods层再导入一份

首先我们每个表的新增数据是存在kafka里的一个主题内的,只要同步好schema后,maxwell就会采集到此刻开始的binlog,发往kafka的新增主题内

新增主题内的数据主要发往两个方向:1通过sparkstraming每五分钟一批次拉取存入ods_binlog下,2通过sparkstraimg每十分钟一批次拉取存入kudu表

所以kudu是不依赖于ods_binlog表的,他随时消费十分钟的数据给合并进dwb层了,由于没有建odsbinlog的表,就没有文件映射关系,所以每天的数据会在凌晨删除不会保留

所以我们如果要在同步一次,不仅需要建ods的表,还需要建ods_binlog层的表,完成这个映射,一天一次进行合并

由于ods不仅要消费历史主题还要消费新增Binlog主题,历史和新增谁先谁后的时间顺序无所谓,反正最后都要排序并取最新

但Kudu不是,kudu已经存在有一定的历史数据了,再消费一次历史主题的话可能会产生问题,因为在拉取历史数据的过程中,数据仍然在发生变化

只有在先拉了历史,然后发生了变化,但变化却比历史先到kudu的情况下,然后还没等历史过来,又立马把变化合并了,会发生这样的问题

例如update了一条数据到kudu了,但之后又bootstrap-insert了一条数据,本来这条数据应该更改了,但由于历史链路速度慢又重新insert一个旧的数据,所以Kudu里的数据并没有被更新

所以如果要导Ods,需要将kudu的历史主题链路关闭,将onlybinlog参数置为1,防止出现上述特殊情况造成数据不准确

但反过来先有ods表,再有kudu表,就不用关闭ods历史链路,因为kudu是十分钟就合一次,而ods是一天合一次,最后的排序不存在这种问题,他已经保证好了所有的数据都已经拿过来了

11.如何查看历史数据boot导入完毕?

首先看maxwell拉取历史数据的条数,进度(process /total),这里是maxwell历史数据作为生产者存进kafka历史主题的进度

也可以看kafka历史主题bi_table_init下的数据积压有没有继续增加,s_bi_ods_binlog是ods的消费者组,s_bi_table_init_new是kudu的消费者组

打开相应的消费者组查看待消费的积压数据Topic Summary还有多少,因为这里没有新增及变化,因为不是新增主题,所以如果还在增加说明maxwell还在拉取数据

然后可以看TableInitToOdsBinlog任务,它为sparkstreaming,不关闭,也就是Ods的消费者组进程,它通过每一分钟消费一次历史数据,将kafka历史主题下的数据分发到相应的binlog表目录下

如果在没有其他表历史同步的情况下,其records重新变为0(最靠谱),则代表streaming消费完毕数据,当kafka中待消费数据为0时也说明streming消费完毕

12.关于kafka在这里边的应用

生产者即部署在每个实例节点上的maxwell,收集不同表的Binlog一起发往相应的历史主题或新增主题

为什么历史主题和新增主题要分开?如果不分开,如何控制关闭kudu的历史链路?

为什么不每个表搞一个sparkstraimg作为消费者?没有必要,浪费资源,一个消费者组进行分发是更好地选择

为什么说他是一个消费者组?因为他的每个executor是一个消费者,通过hash取模后取相应数据进行消费,然后判断发往指定表的目录下,并将offset记录在一个特殊的topic中

为什么不每个表搞一个主题?如果每个表一个主题,首先管理维护起来麻烦,主题太多,其次每个主题都得有它的消费者组,还是需要很多个消费者组,就又回到了刚刚的问题

那就不可以一个消费者组消费不同主题吗?可以,但用在这里显然并不合适

13.是否要重启消费任务以及streaming的背压机制

streaming任务有背压机制,运行开始时会先拉取少部分数据进行消费,再依次增多,当拉取的数据量规定时间内消费不完时,会减少下批次拉取,是一个动态增长的方式

当多批次都卡在内存中,占满内存时则会报内存错误,此时需要去job里查看报错信息

对于streaming任务,rate参数代表每秒拉取每个分区的数据量,duration代表多久一批次,因此每次拉取的数据量用rate乘kafka主题分区数乘duration,

启动maxwelltokudu等任务时,可以观察一下前两批消费数据的速率,如果过慢要kill掉重新启动,因为可能分配的excutor在性能不太好的机器上,会影响后期的消费峰值

14.streming报错

org.apache.spark.scheduler.cluster.YarnClusterScheduler -Lost executor 2 on cdh7.bi.prod.idc1: Containermarked as failed:container e02 1660014269451 7532 01 000003 on host: cdh7.bi.prod.idc1. Exit status: 143.

说明spark执行在yarn上executor内存不足异常ERROR ,重启即可解决,它从断点开始继续消费

注:报错之后有可能出现一种情况就是,streming失败时已经写了文件,但还没有及时的更改offset,就会导致重复消费一部分数据,binlog的boot和mysql端对数对不上,但并不影响最后合并结果

15.如果binlog某天分区内数据有误怎么办?

删除binlog分区目录并不能导致元数据删除,还需删除分区,或者采用另一种方式,直接删除分区目录内的文件,而不是删除目录,相当于分区还存在,只是里面数据没有了

ALTER table ods_binlog.ods_binlog_db_hdf_userlogintime_di drop partition(dt = '2022-08-25');
show PARTITIONS ods_binlog.ods_binlog_db_hdf_userlogintime_di;

你可能感兴趣的:(数据仓库,hive)