前言
在上一篇文章初识ES中我们介绍了ES优势、使用场景以及相关的核心概念。这篇文章我们继续探讨如何将数据从Mysql迁移到ES中。
在本文中,Logstash和ES的版本均为7.3
一、全量数据迁移
在将业务迁移到ES的过程中首先就需要将数据全量迁移到ES中,这是一次性的,在调研的过程中主要考虑了如下几种方案。
1、go-mysql-elasticsearch
go-mysql-elasticsearch is a service syncing your MySQL data into Elasticsearch automatically. It uses mysqldump to fetch the origin data at first, then syncs data incrementally with binlog.
go-mysql-elasticsearch是一个开源的Mysql -> ES的同步工具,使用mysqldump的方式将数据拉下来,然后同步增加到binlog中来实现数据同步,但是使用这种方式的限制很多,而且基本不怎么维护,所以不建议使用这种方案。
2、ES BulkRequest
ES给我们提供了批量插入的API,我们可以采用在Mysql中分页查询,然后批量插入到ES中,类似下边的代码可以实现这样的功能。
BulkRequest bulkRequest = new BulkRequest("your index name");
for (Order item : orderList ){
Map<String, Object> itemMap = new HashMap<>(16);
itemMap.put(id, item.getId());
itemMap.put(orderCode, item.getOrderCode());
itemMap.put(orderName, item.getOrderName());
bulkRequest.add(new IndexRequest("your index name").id(item.getId().toString()).source(itemMap));
}
BulkResponse bulkResponse;
try {
bulkResponse = remoteHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
log.error("Insert batch execution failed: {}", e.getMessage(), e);
}
通过这种方式也是可以实现全量同步的,但是问题是执行批量插入的速度太慢,若我们的数据达到上千万级,同步的时间将会非常漫长。
3、阿里云DTS
若是生产环境均购买阿里云的ELK服务,可以考虑使用DTS服务来实现Mysql和ES的数据同步,DTS的全量同步原理架构图如下:
注:图片来自阿里云官方DTS实现了在全量同步的时候可以不停服,在全量数据迁移之前会启动增量数据拉取模块,增量数据拉取模块会拉取源实例的增量更新数据,并解析、封装、存储在本地存储中。当全量数据迁移完成后,DTS会启动增量日志回放模块,增量日志回放模块会从增量日志读取模块中获取增量数据,经过反解析、过滤、封装后迁移到目标实例,从而实现增量数据迁移。
但是DTS截止目前对于ES的支持仅限在<=6.7,而我们使用的是ES7.3
版本,所以这种方式也暂时不考虑。
4、Logstash
排除了上边三种方案之后,看似只剩下logstash能满足我们目前的需求了。Logstash是ELK技术栈的重要组成部分,Logstash是开源的、拥有实时管道处理能力的数据收集引擎。input模块支持多种输入源(kafka、mysql、filebeat等等),filter模块支持正则、ruby等等工具来实现强大的数据过滤,数据类型转换等等功能,最终将管道处理完成的数据用output模块输出到指定的目的地。在这里我们使用Jdbc input plugin来实现对Mysql的查询、ES作为最终的输出目的地来实现全量数据的迁移,先来看Logstash官方对它的描述:
"Description This plugin was created as a way to ingest data in any database with a JDBC interface into Logstash. You can periodically schedule ingestion using a cron syntax (see schedule setting) or run the query one time to load data into Logstash. Each row in the resultset becomes a single event. Columns in the resultset are converted into fields in the event."
大意就是这个插件会创建一种方式,用jdbc接口从任何数据库中拉取数据到logstash,我们可以使用cron
的方式定期拉取数据,或执行一次查询将数据加载到Logstash。
先来看一个Logstash的管道配置。
input {
jdbc {
jdbc_driver_library => "/opt/soft/mysql-connector-java-5.1.45.jar"
jdbc_driver_class => "com.mysql.jdbc.Driver"
jdbc_connection_string => "jdbc:mysql://localhost:3306/database_name"
jdbc_user => "root"
jdbc_password => "**************"
jdbc_paging_enabled => true
jdbc_page_size => 10000
clean_run => true
last_run_metadata_path => "/opt/soft/elk7.3/logstash/.log_last_run"
statement => "select id, order_code as orderCode, order_name as orderName, create_time as createTime, update_time as updateTime from es_order"
schedule => "* * * * *"
}
}
filter {
mutate {
copy => { "id" => "[@metadata][_id]"}
remove_field => ["@version", "_score", "_type"]
}
}
output {
elasticsearch {
user => admin
password => "********"
index => "es_order_index"
document_id => "%{[@metadata][_id]}"
hosts => ["http://localhost:9200"]
}
stdout{
codec => rubydebug
}
}
在上边的管道配置中,有下面几个地方需要注意:
input模块
- jdbc_driver_library:指定的Mysql驱动,一般来说是没有问题的,但是若版本采用8.0以上的版本,则可能会出现迁移全量数据遗漏的问题,导致总会有一部分数据同步失败,定位迁移失败原因非常麻烦。
- jdbc_paging_enabled:指定开启查询分页
- jdbc_page_size:指定每次分页查询的大小
- statement:我们指定查询语句,Logstash将根据这里的Sql发起查询,若sql较为复杂可以使用
statement_filepath
参数指定Sql文件的目录。 - schedule:通过设置Cron,Logstash周期性的执行任务,此参数没有默认值,无没有配置,则只执行一次;这里配置的
"* * * * *"
,代表着每分钟执行一次。
filter模块
这里的过滤模块较为简单,因为Mysql中的数据都是已经结构化的。
- copy:在ES中每一个文档(概念在上一篇已经讲过了),是由
index
、_type
、_id
三个参数共同标识的,所以每一个文档必须拥有一个_id作为metadata
,在这里我们使用Mysql
的主键复制到ES中的_id字段,方便我们在使用后边的增删改查。否则,则ES会自动生成一个Base64的字符串赋值给_id
。 - remove_field:这里可以指定我们要删除的的字段
output模块
- document_id => "%{[@metadata][_id]}":这一行即是将我们在filter模块中复制主键id的值提取出来设置到document_id中。
- codec => rubydebug:在测试阶段,我们将Logstash的执行日志打印出来。
在配置完成后,我们可以在启动的时候通过指定刚刚创建的管道来实现全量迁移。经过测试和生产环境的测试,迁移五百万数据大概在二十分钟左右,但是在生产数据迁移的过程中使用这种方式需要将迁移表写入、修改操作停止。为了避免停止整个服务,可以在配置中心配置开关来控制数据的写入,迁移完成之后打开,以减小对业务的影响。
二、增量数据同步
在上一部分,我们探讨了如何对数据的全量迁移,这里我们接着探讨如何实现对增量数据的同步。这里实质上就是想实现Mysql-ES的数据一致性,我们知道数据一致性大致分为两种,一种是强一致性,一种是最终一致性。
1、强一致性实现
在分布式系统中,若要实现强一致性,往往会牺牲较多的性能来实现,比如这里我们可以采用“同步双写”来实现事务。当有写入请求进来时,我们先写入到ES中,ES写入失败则此次插入失败,或者重新发起请求写入;待ES写入成功之后,再写Mysql,成功则事务完成,反之需要回滚ES已经写入的内容,否则双方数据将出现不一致。 从上边的流程来看,可能涉及到的远程调用较多、网络IO将拖慢接口的响应速度,使得系统吞吐量降低...所以想实现强一致性成本是比较高的,下面我们再来看看实现最终一致性的方案。
2、最终一致性
实现最终一致性的方案之间也有较大的区分。下边主要介绍两种方式来实现:异步MQ同步、储存层面实现最终一致性
a、异步MQ同步
方案描述:假设业务入口在微服务A中,对ES的封装在微服务B中,如下图:
- 1、业务请求到A服务中,若有对数据的写,先写入到Mysql中,然后将写的动作和data扔到MQ。
- 2、B服务监听对应的
Topic
,根据“相应的动作”对ES发起写请求 - 3、若写入失败,B服务将发起重试消费(可设置重试上限次数),直到成功。
- 4、若重试次数超过上限,则发起报警,我们可以触发业务补偿来实现数据最终一致性。
- 5、为了确保数据的一致性,建议在业务量较小的时间段执行一个“对账”的定时任务,来确认Mysql——ES数据是否一致。 上述的方案通过异步的方式将数据一致性的压力从A服务转移给B服务,从而最大程度降低对A服务性能的影响。
b、储存层面实现最终一致性
在分布式领域,数据一致性若是能够在存储层面来解决是相对理想的情况,业务不参与数据一致性的工作,对于业务方来说读Mysql和ES永远都能拿到一样的结果,当然这是一种理想状态。 现实中我们可以使用Logstash来实现数据的同步,从而数据一致性对于业务来说是透明的,但是由于资源的限制,我们无法做到完全Real-time。
c、Logstash增量同步
在前边全量数据迁移的部分我们已经使用Logstash来实现了,在增量同步同样可以使用这种方式,来来看增量同步的管道脚本。
input {
jdbc {
jdbc_driver_library => "/opt/soft/mysql-connector-java-5.1.47.jar"
jdbc_driver_class => "com.mysql.jdbc.Driver"
jdbc_connection_string => "jdbc:mysql://localhost:3306/database_name"
jdbc_user => "root"
jdbc_password => "********"
tracking_column => "unix_ts_in_secs"
use_column_value => true
tracking_column_type => "numeric"
clean_run => true
statement => "id, order_code as orderCode, order_name as orderName, create_time as createTime, update_time as updateTime, UNIX_TIMESTAMP(update_time) AS unix_ts_in_secs FROM es_order WHERE (UNIX_TIMESTAMP(update_time) > :sql_last_value AND update_time < NOW()) ORDER BY update_time ASC"
schedule => "*/30 * * * * *"
}
}
filter {
mutate {
copy => {"id" => "[@metadata][_id]"}
remove_field => ["@version", "unix_ts_in_secs", "myid"]
}
}
output {
elasticsearch {
index => "es_order_index"
document_id => "%{[@metadata][_id]}"
user => admin
password => "**********"
hosts => ["http://localhost:9200"]
}
}
在上一部分我们已经分析了一部分参数的含义,这里我们接着看新出现的属性值
-
tracking_column:用于跟踪Logstash从MySQL读取的最后一个文档,下面会进行描述),它存储在.logstash_jdbc_last_run中的磁盘上。该值将会用来确定Logstash在其轮询循环的下一次迭代中所请求文档的起始值。
-
unix_ts_in_secs:
unix_ts_in_secs
:这是一个由上述 SELECT 语句生成的字段,包含可作为标准Unix
时间戳(自Epoch起秒数)的 “modification_time”。我们刚讨论的 “tracking column” 会引用该字段。Unix 时间戳用于跟踪进度,而非作为简单的时间戳;如将其作为简单时间戳,可能会导致错误,因为在UMTs和本地时区之间正确地来回转换是一个十分复杂的过程。 -
sql_last_value:这是一个内置参数,包括Logstash轮询循环中当前迭代的起始点,上面 JDBC 输入配置中的 SELECT 语句便会引用这一参数。该字段会设置为“unix_ts_in_secs”(读取自 .logstash_jdbc_last_run)的最新值。在
Logstash
轮询循环内所执行的MySQL
查询中,其会用作所返回文档的起点。通过在查询中加入这一变量,能够确保不会将之前传播到Elasticsearch
的插入或更新内容重新发送到Elasticsearch
。 -
modification_time < NOW():查询语句中的这一部分是一个较难解释的概念。
注:引用自这里
对于SQL语句的正确性分析,在上边引用的文章已经有比较详细的描述,这里不再重述。
这种方案的问题在于——特别吃资源,特别是如果schedule
设置的定时执行时间间隔太短并且有多个表需要同步的情况下,由于这种Sql属于范围区间查询同时带有排序,无法让查询走索引,所以Mysql将会承受很大的压力。所以大家在选择这种方案的时候必须心中有数,虽然解放了应用层,但是数据存储层将会承受较大的压力。
d、Binlog增量同步
这种类型的方案更加适合数据的增量同步,它同时能够避免上述两种方案的问题:1、业务参与解决数数据一致性,2、对存储源压力过大。
Canal
canal是阿里开源的组件,它是基于MySQL数据库增量日志解析,提供增量数据订阅和消费,消费端其中就包括ES。如下图
这种方案暂时还没有使用过,感兴趣的小伙伴可以自己尝试地址在这儿Home
DTS
在上边的全量数据迁移也提到过这种方案,和Canal一样都是使用Binlog的方式来实现。
总结
我们先介绍了全量数据同步的方式,包括使用go-mysql-elasticsearch、ES BulkRequest、阿里云DTS和Logstash的方式,这里最推荐的还是Logstash来实现全量同步,当然有条件的可以使用DTS来实现。
后边我们探讨实现增量同步的集中方法,增量同步我们分为强一致性和最终一致性,强一致性实现成本相对较高;我们主要分析了最终一致性的几种方式,若是数据库资源较为充裕,可以考虑使用Logstash来实现增量同步,否则可以采用异步MQ的方式来实现,大家可以根据自己的实际情况来选择增量的同步方式。
下一部分我们继续分享如何使用Java High Level REST Client
来实现对ES的操作。