互联网体系架构具有可控性差、 数据量大、 架构复杂等特点,错综复杂的各业务模块需要解耦,各异构数据需要同步,双活/多活的容灾方案需要高实时性 等,在各种场合都需要一套可靠的数据实时推送方案。mysql已成为互联网项目存储的主力,围绕着它的各外围模块急需实时地获取它的数据,binlog监听是解决此实时同步问题的不二之选。
duckula可以做到mysql到ES的增量和全量的同步,但如果仅限制于做一张表对一个索引的同步,那它的使用场景就大大的限制了,有很多的场景都需要有2表关联及多表关联,这就需要duckula可以做到有关联关系时也能跟据规则进行增量和全量的同步。
在ES6.0以后,索引的type只能有一个,使得父子结构变的不那么清晰,毕竟对于java开发者来说,index->db,type->table的结构比较容易理解。而且明确不支持父子关系了,但是可以有2种方式达到我们所说的父子关系模型 ,nested类型和join类型,nested类型就是采用一个大json存放父表字段,里面可以含有许多的小json存放子表的数据,join类型则更像我们数据库存放的数据形态,在ES里面会有多种数据,其中每条记录会有一个附加的join类型字段,指示它是父表的数据还是子表的数据,它的值可以是一个json,其中有parent字段表示它中哪条记录的子表,这样一级级的指示父子关系,最后就是一棵父子关系树。
网上会有许多的文章来说明2者关系的特点,我从自身理解的方向来说明一下duckula为什么选择join类型:
1、join类型更像之前老版本ES用type来做父子关系,对于旧系统的改造会有优势
2、join类型所做查询是细粒度的,比如: 1父10000子,如果其中只有1个子记录命中查询,join类型只会查询这一条子记录,但nested类型是大的json,不管你如何查,只要一1条命中了,也会把整个大json给查出来。这对网络和ES也带来了不小的开销。
3、对于频繁做新增修改的场景,join类型也是细粒度的,只变更有变化的父或子数据就可以了,而nested类型不论父还是子数据有变更都需要修改整个大的记录,代价非常大。
4、使用ES来做查询都是千万级以上的数据,甚至亿级,对于上亿的主表,采用nested类型时,需主表每条记录又要到上亿甚至10亿数量的子表去查询,最后再组装为一个大的json,可想而知对于ES的全量dump导入会是什么样的性能。而对于join类型它可以快速的batch插入,它的全量导入的速度是nested类型全量导入的速度快几个数量级一点不为过。注意:全量的dump导入不是一次性的动作,在以后漫长的维护过程中,不管由于什么原因大批量的丢数据了,修改字段类型, 甚至es与mysql的数据不一致了,都需要再次做全量dump操作。如果不能快速的完成全量dump操作,将对业务造成很大的困扰。
由于不是项目形式的,做完能用就算了,duckula是以产品的模式来设计对join类型的支持,只要有类似的需求,运维只需要在duckula的ops上做相应的配置,duckula收集到配置后才能做全量与增量的导入。那么duckula必需要设计一套规则来满足可配置的需求。
duckula设计附加的join类型的mapping定义为(mapping片断):
"tams_relations": {
"type": "join",
"eager_global_ordinals": true,
"relations": {
"user_info": "user_addr:user_id"
}
}
字段“tams_relations”是固定的。
eager_global_ordinals:字段预先加载,全局序数会在一个新的段可进行搜索之前进行构建。
relations:定义父子关系。
“user_info”: “user_addr:user_id” 表示user_info为父表user_addr为子表关联字段是user_addr表的user_id字段
{
"_index": "demo_join",
"_type": "_doc",
"_id": "6",
"_score": 1,
"_source": {
"birthday": 1531238400000,
"update_time": 1552181688000,
"money": 6678000,
"name": "fff6644",
"id": 6,
"tams_relations": "user_info",
"age": 567666
}
},
{
"_index": "demo_join",
"_type": "_doc",
"_id": "user_addr:6",
"_score": 1,
"_routing": "6",
"_source": {
"postNo": "366528",
"user_id": 6,
"id": 2,
"tams_relations": {
"parent": "6",
"name": "user_addr:user_id"
},
"addr": "aaaa"
}
}
第一条记录是表示主表记录:它的tams_relations值 为join类型的前半部分 ,它的id是原样id不做任何处理。
第二条记录是子表记录,它的tams_relations的name值为mapping定义的join类型的后半部分:user_addr:user_id,注意,它的_id值是加了子表表名做前缀“user_addr:”的,目的为了避免父子表同一个id的记录互相覆盖。tams_relations字段还有一个部分是"parent",表示它的父记录的id,ES的routing也是跟据它来做路由的,这样就保证了存在父子关系的数据一定落到同一个分区中。
这个就是普通的binlog监听规则,注意点就是需要同时监听user_info和user_addr的2张表:
binlog_test_dbuser_info|user_addr
{‘topic’:‘demo_join’}
其它配置与普通的consumer没有什么变化 ,主要变化点在它的规则:binlog_test_dbuser_info|user_addr
{‘key’:‘id’,‘relakey’:‘user_id’ ,‘index’:‘demo_join’,‘middleware’:‘dev’}
其它部分没变,也是 库名表名
{规则item},在“规则item”里需要加一个配置:‘relakey’:‘user_id’,表示这个子表的哪个字段用来关系父表id。
与一般的索引创建的区别是它需要填写从表的配置:“库名-表名(从)-关联字段”。上图中的“内容”部分的json是duckula跟据数据库的字段和预行定制好的字段映射关系自动生成的。用户一定不要修改字段名,字段类型可以跟据需求做调整,duckula会试图匹配你调整的类型,但如果转换不成功会抛出异常,需要重新配置或是加强duckula的转换规则,以后可以考虑使用插件来解决此类类型转换问题。
由于历史原因,dump的配置与之前的配置没有变化,只能一张表一张表的导入,所以主表和子表需要分2次dump,如果资源允许,主表和子表的全量dump可以以2个进程的方式同时进行。