数据同步:搜索引擎实时化

数据同步:搜索引擎实时化

NodeJS的MySQL/MariaDB Binlog读取器

TL;DRGITHUB 直接读代码吧。

最近在构建公司里新的搜索引擎,统一界面搜索内部application的所有信息。这个过程还是比较漫长的事,只是开始就遇到了如何构建比较实时的搜索的问题。我们拿ElasticSearch来说好了,虽然它提供了方便的RESTful API,也有Logstash这样免费的工具帮助大家将关系型数据库的数据导入到ElasticSearch里,只是它是一次性导入,那么就是说我们需要每天刷一次ElasticSearch的索引,数据量小还好办,对于一大堆数据基本是不现实的。那么得想个办法实时和搜索引擎交互来更新数据。

首先数据量大,和搜索引擎交互的话,可以设计一个message queue,然后让搜索引擎慢慢消化实时更新的数据。所以用一个redis,再懒些就python的celery直接把实时buffer的中间件写好就可以了。剩下的问题就是如何把更新的数据往message queue里写了。

一开始是想从ORM层面入手,每个application都会有和数据库交互的地方,只要增删改的某数据,就异步发送一个HTTP请求给message queue。只是各种语言各种框架繁杂,比如python/django的话就ORM封装太完备,可能要使用signal去支持;bugzilla这样用perl写的古老app又得一行一行找sql运行的地方;于是从app的代码层面去做实时搜索引擎简直是天方夜谭。

接着就在数据库上动起脑筋了。现在常用的数据库其实并不多,最主要的当属mysql/mariadb和postgres了,当然oracle,sqlserver也还是有的,还有一些少用的neo4j,moongodb,以及hbase。所以比起application来,数据库的种类就少很多了。把它们和搜索引擎对接起来就可以实时更新数据了。

我们还以ElasticSearch为例,和它交互基本是用JSON格式的数据的。如果数据库都是MoongoDB,它本身就是document-based,所以op log也可以直接输出JSON,这样和ElasticSearch对接真的特别轻松。

到了MySQL/MariaDB,就有点懵。不过大家都能很快想到binary log的,于是就开始研究起来。原来也编译过很多次MariaDB了,可还真没有好好注意过里面的小工具,mysqlbinlog是可以很方便的打印binary log内容的。

这样就初步确立了可能的方案,写一个daemon,不停地刷binlog的position,有更新就读取binlog并提交到搜索引擎。如果是用Popenpopen(mysqlbinlog)去调用mysqlbinlog,然后把它的输出都抓出来,感觉很山寨:主要是mysqlbinlog对于row-based的binlog只能先输出base64编码的raw data。读取输出还得写个解析器解析,然后再写另一个解析器解析binlog内容。如果写插件的话也不错,在数据库里像HandlerSocket启动线程去listen一个端口,像端口外publish信息。

正准备写插件,忽然发现mysqlbinlog有一个参数-h可以指定远程host。那么就是说应该有什么sql语句对应读取binlog咯?确实show binlog events in 'xxx.xxxxxx'是可以得到binlog的,但是对应的log并没有raw data,就是如果是更新了数据,没办法通过这条命令知道哪个记录的哪些字段被更改了。

于是撬开mariadb的源代码,client/mysqlbinlog.cc里track一下。抓到了0x12号指令ComDumpBinlog,只是它没有对应的sql。好吧,还是很开心,可以不用写任何插件就可以直接通过数据库server读取binlog了,这样我们就不用过多侵入式地去改别人的app机器了。

前些天还在看node-mysql作者的github,他也蛮可爱,用pure javascript写了个mysql的connector,因为有人不断用native库超越他的pure javascript库的performance,他不断在改进,于是node-mysql的performance也和native的库性能相差不大了。

简单扫了一遍代码,很适合进行小改造,让node-mysql支持dump binlog。在packets里复制一下ComChangeUserPacket.js,然后改改就能得到ComDumpBinlogPacket.js。再学着sequences里的Query.js,把DumpBinlog.js写出来,基本上就可以跑出binlog了。整理整理,把修改的代码提取出来在外层给node-mysql打个patch,dumpBinlog(connection)就直接能得到binlog了。

下面就是binlog内容的解析器了。到这里已经不想弄了,太繁琐。心烦意燥的时候,Google一下nodejs binlog还真有人做过这样的事情。懊恼当时没有先好好搜索下。其中 https://github.com/nevill/zongji 好像就是我想要的东西。再看看代码,竟然也是在node-mysql上打patch。不过最终我是要把binlog变成JSON的,那么用他的还是得改,那还是把原来的写完吧,正好有了参考,会少走些弯路,尤其是在处理一个一个字节的时候。zongji会把更新后的数据该转化的就转化成直接可用的形式了,比如time,json,我在处理就全部保持buffer,真要用的时候再处理。这样保证代码也少一些。再参考mysql的dev文档(吐槽下mysql的dev文档,坑实在有点多),基本的dumpBinlog就成形了。

下一步就是细化了。比如在binlog里更新的数据是读不到field的名字的,只知道是第几个field有更新。那么desc table_name后和逐行匹配就能把field name补上了,后面就好监听是哪个schema的哪张table的哪条记录了(field name有了,就可以监听”id”,而不是监听第1个field了)。接着就是log的化简算法:比如当数据库操作有insert(id=1), update(id=1),在最终就是insert(id=1),搜索引擎会为这个记录建立索引;而insert(id=2), update(id=2), update(id=2), delete(id=2)化简后就是null,搜索引擎不为这条记录做任何操作。

目前发到https://github.com/dna2github/dna2poem/tree/master/sevord/binlog_publisher上的代码还仅仅是输出binlog,等到后续代码测试成熟了整理好了再搬上去吧。再example.js可以看到如何使用binlog dump:

/*
 @author: Seven Lju
 @date: 2016-05-16
 @desc: example to read binary log
 */
var mysql      = require('mysql');
var dumpBinlog = require('./DumpBinlog');

var connection = mysql.createConnection({
  host       : 'localhost',
  socketPath : '/tmp/mysql.sock',
  user       : 'root',
  password   : '',
  debug      : true
});


// assume executing below commands in order for local test
connection.query('SET @master_binlog_checksum=\'NONE\'');
connection.query('SET @mariadb_slave_capabilitym=4');
dumpBinlog(connection, {startPos: 4, logName: 'binlog.000001'});

connection.on("close", function (err) {
    console.log("SQL CONNECTION CLOSED.");
});
connection.on("error", function (err) {
    console.log("SQL CONNECTION ERROR: " + err);
});

运行在我local上测试得到的数据是(省略一些Packet):

...
--> ComDumpBinlogPacket
ComDumpBinlogPacket {
  command: 18,
  startPos: 4,
  flags: 512,
  serverId: 0,
  logName: 'binlog.000002' }

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 0,
  eventType: 4,
  eventName: 'ROTATE_EVENT',
  serverId: 10001,
  eventLength: 20,
  nextLogPos: 0,
  flags: 32,
  raw: <Buffer 04 00 00 00 00 00 00 00 62 69 6e 6c 6f 67 2e 30 30 30 30 30 32> }

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463364945,
  eventType: 15,
  eventName: 'FORMAT_DESCRIPTION_EVENT',
  serverId: 10001,
  eventLength: 224,
  nextLogPos: 248,
  flags: 0,
  eventData: 
   { binlogVersion: 4,
     serverVersion: '10.0.14-MariaDB-log',
     createTimestamp: 1463364945,
     headerLength: <Buffer 00 38 0d 00 08 00 12 00 04 04 04 04 12 00 00 dc 00 04 1a 08 00 00 00 08 08 08 02 00 00 00 0a 0a 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... > } }

...

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463365026,
  eventType: 19,
  eventName: 'TABLE_MAP_EVENT',
  serverId: 10001,
  eventLength: 27,
  nextLogPos: 408,
  flags: 0,
  eventData: 
   { tableId: 70,
     flags: 1,
     schema: 'hatch_stock',
     table: 't',
     columns: 
      { column_raw: <Buffer 03 00 01>,
        column_type_def: <Buffer 03>,
        column_meta_def: <Buffer >,
        null_bitmap: <Buffer 01>,
        count: 1,
        formated: [Object] } } }

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463365026,
  eventType: 23,
  eventName: 'WRITE_ROWS_EVENT_V1',
  serverId: 10001,
  eventLength: 14,
  nextLogPos: 442,
  flags: 0,
  eventData: { tableId: 70, flags: 1, rows: [ [Object] ] } }

...

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463365043,
  eventType: 19,
  eventName: 'TABLE_MAP_EVENT',
  serverId: 10001,
  eventLength: 29,
  nextLogPos: 556,
  flags: 0,
  eventData: 
   { tableId: 71,
     flags: 1,
     schema: 'hatch_stock',
     table: 't2',
     columns: 
      { column_raw: <Buffer 03 03 00 02>,
        column_type_def: <Buffer 03 03>,
        column_meta_def: <Buffer >,
        null_bitmap: <Buffer 02>,
        count: 2,
        formated: [Object] } } }

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463365043,
  eventType: 23,
  eventName: 'WRITE_ROWS_EVENT_V1',
  serverId: 10001,
  eventLength: 27,
  nextLogPos: 603,
  flags: 0,
  eventData: { tableId: 71, flags: 1, rows: [ [Object], [Object] ] } }

...

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463369981,
  eventType: 19,
  eventName: 'TABLE_MAP_EVENT',
  serverId: 10001,
  eventLength: 47,
  nextLogPos: 1030,
  flags: 0,
  eventData: 
   { tableId: 72,
     flags: 1,
     schema: 'hatch_stock',
     table: 't3',
     columns: 
      { column_raw: <Buffer 03 03 05 0f 05 04 0f 03 fe 03 09 08 0f 00 08 04 4b 00 fe 3c b6 00>,
        column_type_def: <Buffer 03 03 05 0f 05 04 0f 03 fe 03>,
        column_meta_def: <Buffer 08 0f 00 08 04 4b 00 fe 3c>,
        null_bitmap: <Buffer b6 00>,
        count: 10,
        formated: [Object] } } }

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463369981,
  eventType: 23,
  eventName: 'WRITE_ROWS_EVENT_V1',
  serverId: 10001,
  eventLength: 52,
  nextLogPos: 1102,
  flags: 0,
  eventData: { tableId: 72, flags: 1, rows: [ [Object] ] } }

...

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463391082,
  eventType: 19,
  eventName: 'TABLE_MAP_EVENT',
  serverId: 10001,
  eventLength: 30,
  nextLogPos: 1364,
  flags: 0,
  eventData: 
   { tableId: 73,
     flags: 1,
     schema: 'hatch_stock',
     table: 't4',
     columns: 
      { column_raw: <Buffer 0f 02 b1 00 00>,
        column_type_def: <Buffer 0f>,
        column_meta_def: <Buffer b1 00>,
        null_bitmap: <Buffer 00>,
        count: 1,
        formated: [Object] } } }

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463391082,
  eventType: 23,
  eventName: 'WRITE_ROWS_EVENT_V1',
  serverId: 10001,
  eventLength: 21,
  nextLogPos: 1405,
  flags: 0,
  eventData: { tableId: 73, flags: 1, rows: [ [Object] ] } }

...

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463392197,
  eventType: 19,
  eventName: 'TABLE_MAP_EVENT',
  serverId: 10001,
  eventLength: 30,
  nextLogPos: 1658,
  flags: 0,
  eventData: 
   { tableId: 74,
     flags: 1,
     schema: 'hatch_stock',
     table: 't5',
     columns: 
      { column_raw: <Buffer 0f 02 b4 00 01>,
        column_type_def: <Buffer 0f>,
        column_meta_def: <Buffer b4 00>,
        null_bitmap: <Buffer 01>,
        count: 1,
        formated: [Object] } } }

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463392197,
  eventType: 23,
  eventName: 'WRITE_ROWS_EVENT_V1',
  serverId: 10001,
  eventLength: 21,
  nextLogPos: 1699,
  flags: 0,
  eventData: { tableId: 74, flags: 1, rows: [ [Object] ] } }

...

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463397516,
  eventType: 19,
  eventName: 'TABLE_MAP_EVENT',
  serverId: 10001,
  eventLength: 47,
  nextLogPos: 1831,
  flags: 0,
  eventData: 
   { tableId: 72,
     flags: 1,
     schema: 'hatch_stock',
     table: 't3',
     columns: 
      { column_raw: <Buffer 03 03 05 0f 05 04 0f 03 fe 03 09 08 0f 00 08 04 4b 00 fe 3c b6 00>,
        column_type_def: <Buffer 03 03 05 0f 05 04 0f 03 fe 03>,
        column_meta_def: <Buffer 08 0f 00 08 04 4b 00 fe 3c>,
        null_bitmap: <Buffer b6 00>,
        count: 10,
        formated: [Object] } } }

<-- BinlogPacket
BinlogPacket {
  protocol41: true,
  timestamp: 1463397516,
  eventType: 24,
  eventName: 'UPDATE_ROWS_EVENT_V1',
  serverId: 10001,
  eventLength: 100,
  nextLogPos: 1951,
  flags: 0,
  eventData: { tableId: 72, flags: 1, rows: [ [Object] ] } }

...

其中formated的table fields信息:

[ { type: 3, nullable: false, meta: [] },
  { type: 3, nullable: true, meta: [] },
  { type: 5, nullable: true, meta: [ 8 ] },
  { type: 15, nullable: false, meta: [ 15, 0 ] },
  { type: 5, nullable: true, meta: [ 8 ] },
  { type: 4, nullable: true, meta: [ 4 ] },
  { type: 15, nullable: false, meta: [ 75, 0 ] },
  { type: 3, nullable: true, meta: [] },
  { type: 254, nullable: false, meta: [ 254, 60 ] },
  { type: 3, nullable: false, meta: [] } ]

下面是一个update事件,从两个object可以比较得到第2个field从null更新成了4

{ '0': 1,
  '1': null,
  '2': null,
  '3': <Buffer 74 65 73 74>,
  '4': null,
  '5': null,
  '6': <Buffer 68 65 6c 6c 6f>,
  '7': null,
  '8': <Buffer 31 32 33 34 35 36 37 38 39 30 30 39 38 37 36 35 34 33 32 31>,
  '9': 12 }

{ '0': 1,
  '1': 4,
  '2': null,
  '3': <Buffer 74 65 73 74>,
  '4': null,
  '5': null,
  '6': <Buffer 68 65 6c 6c 6f>,
  '7': null,
  '8': <Buffer 31 32 33 34 35 36 37 38 39 30 30 39 38 37 36 35 34 33 32 31>,
  '9': 12 }

我们desc这张表有:

+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| a0    | int(11)     | NO   | PRI | NULL    | auto_increment |
| a1    | int(11)     | YES  |     | NULL    |                |
| a2    | double      | YES  |     | NULL    |                |
| a3    | varchar(5)  | NO   |     | NULL    |                |
| a4    | double      | YES  |     | NULL    |                |
| a5    | float       | YES  |     | NULL    |                |
| a6    | varchar(25) | NO   |     | NULL    |                |
| a7    | int(11)     | YES  |     | NULL    |                |
| a8    | char(20)    | NO   |     | NULL    |                |
| a9    | int(11)     | NO   |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+

也就是说a1这个field被修改了。

发布了51 篇原创文章 · 获赞 37 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/prog_6103/article/details/51428212
今日推荐