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被修改了。