ES 译文之如何使用 Logstash 实现关系型数据库与 ElasticSearch 之间的数据同

译者前言
近期的主要工作是在为公司的 APP 增加搜索功能。因为也遇到了需要把关系型数据库中的数据同步 ElasticSearch 中的问题,故抽了点时间翻译了这篇官方的博文。最近,在数据同步方面也有些思考。
本篇文章的重点不在 Logstash 的 JDBC 插件的使用方法,而是数据同步会遇到的一些细节问题如何处理。我觉得,这些设计思想是通用的,无论你使用的何种方式进行数据同步。
翻译正文

为了利用 ElasticSearch 强大的搜索能力,大部分的业务都会在关系型数据库的基础上部署 ElasticSearch。这类场景下,保持 ElasticSearch 和关系型数据库之间的数据同步是非常必要的。
本篇博文将会介绍如何通过 Logstash 实现在 MySQL 和 ElasticSearch 之间数据的高效复制与同步。
注:文中演示的代码和方法都经过在 MySQL 中的测试,理论上适应于所有的关系型数据库。
本文中,组件的相关信息如下:

MySQL:  8.0.16.
Elasticsearch: 7.1.1
Logstash: 7.1.1
Java: 1.8.0_162-b12
JDBC input plugin: v4.3.13
JDBC connector: Connector/J 8.0.16

数据同步概述
本文将会通过 Logstash 的 JDBC input 插件进行 ElasticSearch 和 MySQL 之间的数据同步。从概念上讲,JDBC 插件将通过周期性的轮询以发现上次迭代后的新增和更新的数据。为了正常工作,几个条件需要满足:
ElasticSearch 中 _id 设置必须来自 MySQL 中 id 字段。它提供了 MySQL 和 ElasticSearch 之间文档数据的映射关系。如果一条记录在 MySQL 更新,那么,ElasticSearch 所有关联文档都应该被重写。要说明的是,重写 ElasticSearch 中的文档和更新操作的效率相同。在内部实现上,一个更新操作由删除一个旧文档和创建一个新文档两部分组成。
当 MySQL 中插入或更新一条记录时,必须包含一个字段用于保存字段的插入或更新时间。如此一来, Logstash 就可以实现每次请求只获取上次轮询后更新或插入的记录。Logstash 每次轮询都会保存从 MySQL 中读取到的最新的插入或更新时间,该时间大于上次轮询最新时间。
如果满足了上述条件,我们就可以配置 Logstash 周期性的从 MySQL 中读取所有最新更新或插入的记录,然后写入到 Elasticsearch 中。
关于 Logstash 的配置代码,本文稍后会给出。
MySQL 设置
MySQL 库和表的配置如下:

CREATE DATABASE es_db

USE es_db

DROP TABLE IF EXISTS es_table

CREATE TABLE es_table (
  id BIGINT(20) UNSIGNED NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY unique_id (id),
  client_name VARCHAR(32) NOT NULL,
  modification_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  insertion_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

配置中有几点需要说明,如下:

es_table,MySQL 的数据表,我们将把它的数据同步到 ElasticSearch 中;
id,记录的唯一标识。注意,id 定义为主键的同时,也定义为唯一建,可以保证每个 id 在表中只出现一次。同步 ElasticSearch 时,将会转化为文档的 _id;
client_name,表示用户定义用来保存数据的字段,为使博文保持简洁,我们只定义了一个字段,更多字段也很容易加入。接下来的演示,我们会更新该字段,用以说明不仅仅新插入记录会同步到 MySQL,更新记录同样会同步到 MySQL;
modification_time,用于保存记录的更新或插入时间,它使得 Logstash 可以在每次轮询时只请求上次轮询后新增更新的记录;
insertion_time,该字段用于一条记录插入时间,主要是为演示方便,对同步而言,并非必须;

MySQL 操作
前面设置完成,我们可以通过如下命令插入记录:

INSERT INTO es_table (id, client_name) VALUES (<id>, <client name>);`

使用如下命令更新记录:

UPDATE es_table SET client_name = <new client name> WHERE id=<id>;

使用如下命令更新插入记录:

INSERT INTO es_table (id, client_name) VALUES (<id>, <client_name when created>) ON DUPLICATE KEY UPDATE client_name=<client name when updated>

同步代码
Logstash 的 pipeline 配置代码如下,它实现了前面描述的功能,从 MySQL 到 ElasticSearch 的数据同步。

input {
  jdbc {
    jdbc_driver_library => "<path>/mysql-connector-java-8.0.16.jar"
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://<MySQL host>:3306/es_db"
    jdbc_user => "<my username>"
    jdbc_password => "<my password>"
    jdbc_paging_enabled => true
    tracking_column => "unix_ts_in_secs"
    use_column_value => true
    tracking_column_type => "numeric"
    schedule => "*/5 * * * * *",
    statement => "SELECT *, UNIX_TIMESTAMP(modification_time) AS unix_ts_in_secs FROM es_table WHERE (UNIX_TIMESTAMP(modification_time)) > :sql_last_value AND modification_time < NOW() ORDER BY modification_time desc"
  }
}
filter {
  mutate {
    copy => { "id" => "[@metadata][_id]"}
    remove_field => ["id", "@version", "unix_ts_in_secs"]
  }
}

output {
  # stdout { codec => "rubydebug" }
  elasticsearch {
    index => "rdbms_sync_idx"
    document_id => "%{[%metedata][_id]}"
  }
}

关于 Pipeline 配置的几点说明,如下:

tracking_column

此处配置为 "unix_ts_in_secs"。它被用于追踪最新的记录,并被保存在 .logstash_jdbc_last_run 文件中,下一次轮询将以这个边界位置为准进行记录获取。SELECT 语句中,可通过 :sql_last_value 访问该配置字段的值。

unix_ts_in_secs

由 SELECT 语句生成,是 "modification_time" 的 UNIX TIMESTAMP。它被前面讨论的 "track_column" 引用。使用 UNIX TIMESTAMP,而非其他时间形式,可以减少复杂性,防止时区导致的时间不一致问题。

sql_last_value

内建的配置参数,指定每次轮询的开始位置。在 input 配置中,可被 SELECT 语句引用。在每次轮询开始前,从 .logstash_jdbc_last_run 中读取,此案例中,即为 "unix_ts_in_secs" 的最近值。如此便可保证每次轮询只获取最新插入和更新的记录。

schedule

通过 cron 语法指定轮询的执行周期,例子中,"/5 " 表示每 5 秒轮询一次。

modification_time < NOW()

SELECT 语句查询条件的一部分,当前解释不清,具体情况待下面的章节再作介绍。

filter

该配置指定将 MySQL 中的 id 复制到 metadata 字段 _id 中,用以确保 ElasticSearch 中的文档写入正确的 _id。而之所以使用 metadata,因为它是临时的,不会使文档中产生新的字段。同时,我们也会把不希望写入 Elasticsearch 的字段 id 和 @version 移除。

output

在 output 输出段的配置,我们指定了文档应该被输出到 ElasticSearch,并且设置输出文档 _id 为 filter 段创建的 metadata 的 _id。如果需要调试,注释部分的 rubydebug 可以实现。
SELECT 语句的正确性分析
接下来,我们将开始解释为什么 SELECT 语句中包含 modification_time < NOW() 是非常重要的。为了解释这个问题,我们将引入两个反例演示说明,为什么下面介绍的两种最直观的方法是错误的。还有,为什么 modification_time < Now() 可以克服这些问题。
直观场景一
当 where 子句中仅仅包含 UNIX_TIMESTAMP(modification_time) > :sql_last_value,而没有 modification < Now() 的情况下,工作是否正常。这个场景下,SELECT 语句是如下形式:

statement => "SELECT *, UNIX_TIMESTAMP(modification_time)
AS unix_ts_in_secs FROM es_table WHERE (UNIX_TIMESTAMP(modification_time) >
:sql_last_value) ORDER BY modification_time ASC"

粗略一看,似乎没发现什么问题,应该可以正常工作。但其实,这里有一些边界情况,可能导致一些文档的丢失。举个例子,假设 MySQL 每秒插入两个文档,Logstash 每 5 秒执行一次。如下图所示,时间范围 T0 至 T10,数据记录 R1 至 R22。

Logstash 的第一次轮询发生在 T5 时刻,读取记录 R1 至 R11,即图中青色区域。此时,sql_last_value 即为 T5,这个时间是从 R11 中获取到的。
如果,当 Logstash 完成从 MySQL 读取数据后,同样在 T5 时刻,又有一条记录插入到 MySQL 中。 而下一次的轮询只会拉取到大于 T5 的记录,这意味着 R12 将会丢失。如图所示,青色和灰色区域分别表示当次和上次轮询获取到的记录。

注意,这类场景下的 R12 将永远不会再被写入到 ElasticSearch。
直观场景二
为了解决这个问题,或许有人会想,如果把 where 子句中的大于(>)改为大于等于(>=)是否可行。SELECT 语句如下

statement => "SELECT *, UNIX_TIMESTAMP(modification_time) AS unix_ts_in_secs FROM es_table WHERE (UNIX_TIMESTAMP(modification_time) >= :sql_last_value) ORDER BY modification_time ASC"

这种方式其实也不理想。这种情况下,某些文档可能会被两次读取,重复写入到 ElasticSearch 中。虽然这不影响结果的正确性,但却做了多余的工作。如下图所示,Logstash 的首次轮询和场景一相同,青色区域表示已经读取的记录。

Logstash 的第二次轮询将会读取所有大于等于 T5 的记录。如下图所示,注意 R11,即紫色区域,将会被再次发送到 ElasticSearch 中。

这两种场景的实现效果都不理想。场景一会导致数据丢失,这是无法容忍的。场景二,存在重复读取写入的问题,虽然对数据正确性没有影响,但执行了多余的 IO。
终极方案
前面的两场方案都不可行,我们需要继续寻找其他解决方案。其实也很简单,通过指定 (UNIX_TIMESTAMP(modification_time) > :sql_last_value AND modification_time < NOW()),我们就可以保证每条记录有且只发送一次。
如下图所示,Logstash 轮询发生在 T5 时刻。因为指定了 modification_time < NOW(),文档只会读取到 T4 时刻,并且 sql_last_value 的值也将会被设置为 T4。

开始下一次的轮询,当前时间 T10。
由于设置了 UNIX_TIMESTAMP(modification_time) > :sql_last_value,并且当前 sql_last_value 为 T4,因此,本次的轮询将从 T5 开始。而 modification_time < NOW() 决定了只有时间小于等于 T9 的记录才会被读取。最后,sql_last_value 也将被设置为 T9。

如此,MySQL 中的每个记录就可以做到都能被精确读取了一次,如此就可以避免每次轮询可能导致的当前时间间隔内数据丢失或重复读取的问题。
系统测试
简单的测试可以帮助我们验证配置是否如我们所愿。我们可以写入一些数据至数据库,如下:

INSERT INTO es_table (id, client_name) VALUES (1, ‘Jim Carrey‘);
INSERT INTO es_table (id, client_name) VALUES (2, ‘Mike Myers‘);
INSERT INTO es_table (id, client_name) VALUES (3, ‘Bryan Adams‘);

一旦 JDBC 输入插件触发执行,将会从 MySQL 中读取所有记录,并写入到 ElasticSearch 中。我们可以通过查询语句查看 ElasticSearch 中的文档。

`GET rdbms_sync_idx/_search`

执行结果如下:

"hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "rdbms_sync_idx",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "insertion_time" : "2019-06-18T12:58:56.000Z",
          "@timestamp" : "2019-06-18T13:04:27.436Z",
          "modification_time" : "2019-06-18T12:58:56.000Z",
          "client_name" : "Jim Carrey"
        }
      },
Etc …

更新 id=1 的文档,如下:

UPDATE es_table SET client_name = ‘Jimbo Kerry‘ WHERE id=1;

通过 _id = 1,可以实现文档的正确更新。通过执行如下命令查看文档:

GET rdbms_sync_idx/_doc/1

结果如下:

{
  "_index" : "rdbms_sync_idx",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "_seq_no" : 3,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "insertion_time" : "2019-06-18T12:58:56.000Z",
    "@timestamp" : "2019-06-18T13:09:30.300Z",
    "modification_time" : "2019-06-18T13:09:28.000Z",
    "client_name" : "Jimbo Kerry"
  }
}

文档 _version 被设置为 2,并且 modification_time 和 insertion_time 已经不一样了,client_name 已经正确更新。而 @timestamp,不是我们需要关注的,它是 Logstash 默认添加的。
更新添加 upsert 执行语句如下:

INSERT INTO es_table (id, client_name) VALUES (4, ‘Bob is new‘) ON DUPLICATE KEY UPDATE client_name=‘Bob exists already‘;

复制代码和之前一样,我们可以通过查看 ElasticSearch 中相应文档,便可验证同步的正确性。
文档删除
不知道你是否已经发现,如果一个文档从 MySQL 中删除,并不会同步到 ElasticSearch 。关于这个问题,列举一些可供我们考虑的方案,如下:
MySQL 中的记录可通过包含 is_deleted 字段用以表明该条记录是否有效。一旦发生更新,is_deleted 也会同步更新到 ElasticSearch 中。如果通过这种方式,在执行 MySQL 或 ElasticSearch 查询时,我们需要重写查询语句来过滤掉 is_deleted 为 true 的记录。同时,需要一些后台进程将 MySQL 和 ElasticSearch 中的这些文档删除。
另一个可选方案,应用系统负责 MySQL 和 ElasticSearch 中数据的删除,即应用系统在删除 MySQL 中数据的同时,也要负责将 ElasticSearch 中相应的文档删除。
总结
本文介绍了如何通过 Logstash 进行关系型数据库和 ElasticSearch 之间的数据同步。文中以 MySQL 为例,但理论上,演示的方法和代码也应该同样适应于其他的关系型数据库。

原文地址:https://blog.51cto.com/14207399/2419450

时间: 2024-08-29 08:28:46

ES 译文之如何使用 Logstash 实现关系型数据库与 ElasticSearch 之间的数据同的相关文章

使用sqoop 在关系型数据库和Hadoop之间实现数据的抽取

(一)从关系型数据库导入至HDFS 1.将下面的参数保持为 import.script import --connectjdbc:mysql://192.168.1.14:3306/test--username root--password 1234 -m1--null-string''--table user--columns "id,username,age"--target-dir/user/root/sqoop_test  -- 此目录不能存在 2. 执行sqoop --opt

sqoop实现关系型数据库与hadoop之间的数据传递-import篇

由于业务数据量日益增长,计算量非常庞大,传统的数仓已经无法满足计算需求了,所以现在基本上都是将数据放到hadoop平台去实现逻辑计算,那么就涉及到如何将oracle数仓的数据迁移到hadoop平台的问题. 这里就不得不提到一个很实用的工具--sqoop,它是一款开源的工具,主要用于实现关系型数据库与hadoop中hdfs之间的数据传递,其中用的最多的就是import,export了. sqoop的安装配置也是非常简单的,这里就不说明了,本文主要针对如何使用sqoop实现oracle到hive(h

mysql基本认识【关系型数据库和nosql、mysql操作流程和体系,库操作,表操作,数据的操作,字符集的操作,以及php作为client操作数据库】对连接本身没有疑问

1.关系型数据库永久性保存数据的仓库php的变量只是php脚本执行期间,临时性保存变量的空间[使用内存空间临时保存] 关系型数据库:利用二者的关系来描述实体的信息.[利用二维表字段名和字段值来进行描述][关系型数据库根本不是可以使用外键将两个表构建成关联的意思,而是实现描述实体的二维表的形式] nosql:not only sql[sql表示操作关系型数据的语言]所以nosql指的就是非关系型数据库[典型的是键值对型的数据(redis.memcache)][nosql可以视情况添加信息,不需要对

sqoop导入关系型数据库-解密Sqoop

Sqoop作为Hadoop与传统数据库之间的桥梁,对于数据的导入导出有着重要作用.通过对Sqoop基本语法以及功能的阐述,深刻解密Sqoop的作用和价值.  一.什么是Apache Sqoop? Cloudera开发的Apache开源项目,是SQL-to-Hadoop的缩写.主要用于在Hadoop(Hive)与传统的数据库(mysql.postgresql...)间进行数据的传递,可以将一个关系型数据库(例如: MySQL ,Oracle ,Postgres等)中的数据导进到Hadoop的HDF

转载:关系型数据库与面向对象

几乎所有的企业应用都需要持久化数据,没有数据持久化需求的企业应用在现在的市场环境下几乎是不可能出现的.由于关系型数据的普及,通常我们提到数据持久化时,一般指的是将数据持久化到关系型数据库中.关系型数据是一种结构化的数据管理方式,开发者只能通过 SQL 来操作数据库. Java 语言天生就是一门面向对象的编程语言,在 Java 的世界中,被处理的内容都被组织成一个一个的对象,对象和对象之间存在着继承.引用关系,这样的关系无法通过简单的方式直接反应到关系型数据库中.因此在关系型数据库与面向对象之间便

关系型数据库和NoSQL数据库

关系型数据库和NoSQL数据库 什么是NoSQL 大家有没有听说过 “NoSQL”呢?近年,这个词极受关注.看到“NoSQL”这个词,大家可能会误以为是“No!SQL”的缩写,并深感愤怒:“SQL怎么会没有必要了 呢?”但实际上,它是“Not Only SQL”的缩写.它的意义是:适用关系型数据库的时候就使用关系型数据库,不适用的时候也没有必要非使用关系型数据库不可,可以考虑使用更加合适的数据存 储. 为弥补关系型数据库的不足,各种各样的NoSQL数据库应运而生. 为了更好地了解本书所介绍的No

关系和非关系型数据库

什么是SQL SQL指结构化查询语言,是一门ANSI(美国国家标准学会)标准的计算机语言,主要用来访问和操作数据库系统.某些关系型数据库要求在每个SQL命令的末端使用分号,如MySQL(若不在命令末尾使用分号则报错),如果使用的关系型数据库是MS SQL Server或者SQL Server ,则不需要在每个SQL命令末端使用分号. RDBMS RDBMS指的是关系型数据库管理系统,RDBMS是SQL的基础,同样也是很多现在关系型数据库的基础,RDBMS的数据是存储在被称为表的数据库对象中,表是

关系型数据库表结构的两个设计技巧

By良少http://blog.csdn.net/shendl 关系型数据库表结构的设计,有下面两个设计技巧: 物理主键作为关联的外键 关系型数据库,由多个数据表构成.每一个数据表的结构是相同的,不同表之间可能存在关联关系.表之间的关联关系,正是关系型数据库得名的原因. 一个表由多个字段构成.其中可能有多个字段适合作为主键.主键字段,就是表中每一行都不会有重复数据的字段. 主键,可以分为两种:物理主键和逻辑主键. 每一张数据库的表,都使用自增长的id字段作为物理主键. 多表之间的外键关联,都关联

redis(jedis)相关API ,实现与关系型数据库相似的功能

本文简单介绍了在使用jedis操作redis这个nosql数据库过程中,总结的一些问题,例如使用jedis实现形如关系型数据库的数据关联关系处理. 分三个层面: 1:单表数据处理,新增一行数据到数据库中,如果存在主键id是自增情况的条件下,如何新增数据集合到数据库行数据: 2:一对多关联关系,本文举例形如---学生&&成绩: 一个学生包含多个学科的成绩,某一个学科的的成绩属于某个学生: 3:多对多关联关系,本文举例例如---文章&&标签: 一篇文章可以添加多个标签,某一个标