如何分层:
数仓在ods层之前有一层ods_binlog层,存放所有Mysql同步过来的binlog按天为分区放入表中,其中的数据和Ods层合并后以ctime的日期作为分区字段,动态分区发往指定分区的ods,保证每天的ods层数据始终和mysql截止到昨日24点数据始终一致
如何分区:
为什么要以ctime作为分区字段?因为对于大多数表而言,还是新增变化比较少的,而不经常改变历史数据,也就代表着以ctime作为分区,可以在合并逻辑时取的分区数据量就小,合并的速度就快
那什么时候可以以utime作为分区字段吗?当变化量大时,比如每天都会改变很多不同时间的历史数据,是不是以utime作为分区字段合适?其实并不是,因为昨天刚改了一部分历史,分到了昨天的分区,结果今天又改了,今天不仅改了昨天的还改了很多昨天以前的,其实还是基本上吧整个表的数据全拿了过来,我们要清楚我们分区的目的是为了什么,为了合并的时候尽量取少部分数据对吧,你不管按ctime还是utime,他都是取了基本全部数据,显然不能按这两种方式分区
那对于这类表应该如何分区?要看改变的是近一年新增的数据还是近几个月,假如改变的都是近几个月的数据,或者近几年的数据,那么我们是不是按照月的ctime或者按年作为分区更合适
那如果改变毫无规律怎么办?如果毫无规律而且有大量变化,那不妨我们干脆就不分区了,既然达不到分区的目的,就没必要死板的使用分区机制了对吧
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的数据,但如果老数据或新数据被删除我们是需要将其过滤掉的
首先什么叫时间漂移?时间漂移通俗来讲,就是我昨天的数据结果到来的时候已经超过今天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出问题
首次同步检测时最后合并后数据量可能会多于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即可
第一天手动执行,binlog中只有当天分区,其中包括历史数据和截止那一刻的数据,会把这部分数据合并排序后放入ods,这样操作大多数时候只是为了检验数据合并情况,其实完全可以直接第二天手动执行,不过需要注意别过滤掉boot同时控制漂移
第二天如果手动执行,binlog中有昨天和今天的分区,其中包括历史数据和昨天的binlog,如果之前是kudu表可能还包括昨天漂移的binlog,虽然数据量大,但白天跑不怎么会占用资源
第二天如果不手动执行,选择凌晨调度执行和上述同理,数据量大,所以晚上可能跑不成功
第三天调度执行,binlog中有昨天分区和昨天漂移的数据,数据量很小,就没有问题
综上,第一天手动执行检验数据,第一天不执行直接第二天手动为了避免数据大晚上跑不动,第二天将其加入到调度任务,第三天正常调度执行即可
但如果数据量过小,我们只需要首天手动执行检测,然后就加入执行计划,第二天就开始调度执行即可
当然就算数据量大,我们也可以选择这么做,首天手动执行检测,然后就加入执行计划,但要过滤boot,第二天直接开始调度执行即可,不过此时最稳妥的方式还是,首日不要加入调度,因为就算首日检测通过了,然后你也过滤boot了,但直接加入调度,你心里没底,不知道给的内存够不够,还是最好第二天手动跑一下最合适,然后再加入调度
总结一句话:首日合并检测后第二天手动合并且过滤boot后没问题了再加入调度任务
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`;
我使用分任务批次合并出现了一个问题,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'
问题:ods的servicecards数据量少,和mysql那边对不上,少了2
排查方法:利用二分法卡ctime将binlog端的boot和mysql端的对数,观察少了哪部分数据
结果:在临近max ctime的九秒内少了两条数据,且时间交错
原因分析:合并时有一部分binlog在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点
首先我们每个表的新增数据是存在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是一天合一次,最后的排序不存在这种问题,他已经保证好了所有的数据都已经拿过来了
首先看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消费完毕
生产者即部署在每个实例节点上的maxwell,收集不同表的Binlog一起发往相应的历史主题或新增主题
为什么历史主题和新增主题要分开?如果不分开,如何控制关闭kudu的历史链路?
为什么不每个表搞一个sparkstraimg作为消费者?没有必要,浪费资源,一个消费者组进行分发是更好地选择
为什么说他是一个消费者组?因为他的每个executor是一个消费者,通过hash取模后取相应数据进行消费,然后判断发往指定表的目录下,并将offset记录在一个特殊的topic中
为什么不每个表搞一个主题?如果每个表一个主题,首先管理维护起来麻烦,主题太多,其次每个主题都得有它的消费者组,还是需要很多个消费者组,就又回到了刚刚的问题
那就不可以一个消费者组消费不同主题吗?可以,但用在这里显然并不合适
streaming任务有背压机制,运行开始时会先拉取少部分数据进行消费,再依次增多,当拉取的数据量规定时间内消费不完时,会减少下批次拉取,是一个动态增长的方式
当多批次都卡在内存中,占满内存时则会报内存错误,此时需要去job里查看报错信息
对于streaming任务,rate参数代表每秒拉取每个分区的数据量,duration代表多久一批次,因此每次拉取的数据量用rate乘kafka主题分区数乘duration,
启动maxwelltokudu等任务时,可以观察一下前两批消费数据的速率,如果过慢要kill掉重新启动,因为可能分配的excutor在性能不太好的机器上,会影响后期的消费峰值
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端对数对不上,但并不影响最后合并结果
删除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;