ES数据迁移策略(ES系列二)

3,024 阅读11分钟

前言

在上一篇文章初识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的操作。

参考