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

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

NodeJS的MySQL/MariaDB Binlog读取器

TL;DR 去 GITHUB 直接读代码吧。

最近在构建公司里新的搜索引擎,统一界面搜索内部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: 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: 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: 03 00 01>,
        column_type_def: 03>,
        column_meta_def: ,
        null_bitmap: 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: 03 03 00 02>,
        column_type_def: 03 03>,
        column_meta_def: ,
        null_bitmap: 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: 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: 03 03 05 0f 05 04 0f 03 fe 03>,
        column_meta_def: 08 0f 00 08 04 4b 00 fe 3c>,
        null_bitmap: 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: 02 b1 00 00>,
        column_type_def: ,
        column_meta_def: 00>,
        null_bitmap: 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: 02 b4 00 01>,
        column_type_def: ,
        column_meta_def: 00>,
        null_bitmap: 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: 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: 03 03 05 0f 05 04 0f 03 fe 03>,
        column_meta_def: 08 0f 00 08 04 4b 00 fe 3c>,
        null_bitmap: 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': 74 65 73 74>,
  '4': null,
  '5': null,
  '6': 68 65 6c 6c 6f>,
  '7': null,
  '8': 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': 74 65 73 74>,
  '4': null,
  '5': null,
  '6': 68 65 6c 6c 6f>,
  '7': null,
  '8': 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被修改了。

你可能感兴趣的:(数据处理,javascript,nodejs,二进制日志,binlog,mysql,mariadb)