标签归档:logdb

基于Mysql的流水日志记录系统

什么是流水日志?说白了,就是用户的操作日志,包括 who, when, what, how, why 等等。以一个论坛的发表流水日志为例,可能需要记录发表者的帐号ID,发表时间,发表IP,发表内容,发表类型,所在的主贴ID等等。有了这些数据,事后很容易进行分析和统计。

流水日志的重要性是不言而喻的。再我负责的工作中,有很多种类的流水日志需要记录,以供日后的查询和建模分析,并且这些流水日志要记录的内容也经常需要扩展。我们一般使用 Mysql 来记录这些数据。

记录流水日志看似是一件很简单地事情,直接写入数据库就可以了。但当数据量大起来之后,在几千条每秒的量级下,很多原来不是问题的问题,就变成了问题。

首先业务程序直接写数据库是行不通的,就需要搭建专门的流水日志记录服务,通过网络进行通信。这个服务也不能直接写数据库,要有专门的进程(或线程)负责处理请求,另外有进程(或线程)负责写入数据库。或者简单一点的话,先写入文件,然后再定时的写入数据库,但这样会有延迟。每次一条的进行数据库插入速度会很慢,可以通过批量插入或Load in file的方式,以提高写入的效率。

其次,一旦涉及到每天几亿条数据的量级,对于 Mysql 来说,全部放在一个表里也是行不通的,这就涉及到分表的操作。最简单的,按自然时间分表,比如每天的数据写一张表里。分得太细的不方便查询,分的太粗数据量太大,查询也慢。另外,在按时间切分数据表的基础上,有时还需要按照某个具体字段再次分表,以减少单个表的数据量,提高查询效率。比如按用户ID取模,每天再分100张表。

另外,如果需要对字段进行扩展,也是一个麻烦事,如果系统设计的不好,很可能会在升级的过程中丢失部分数据。

一开始,我们是每个系统自己实现流水日志的记录,当然也就没做的很好,出过各种问题。并且,每次做一个新系统,都要重复解决上面的问题,很繁琐。

为了简化这个过程,我设计并实现了Logdb, 它提供一个基于Mysql的通用可配置化的流水日志记录系统。对于业务来说,只需要配置一下,然后调用自动生成的API就好了。会根据配置自动进行分表,如果是 MyISAM 引擎,会同时创建Merge表。并且支持自动删除过期不再需要的数据。扩展也很简单,能够自动识别新增的字段并且修改数据库,并且API是向后兼容的。代码已经开源,再这里,有兴趣的同学可以看一下。

Logdb使用UDP协议通讯,每次调用API就相当于向指定地址发送一个UDP请求。默认情况下Logdb是没有回包的。API也不会等待回包,这样调用API的开销是很小的,几乎不会对业务进程造成任何的延迟。UDP协议是不可靠的,不过在局域网内,UDP发生丢包的概率还是极低的。并且UDP在实现上相对比较简单,后续可能会添加对TCP协议的支持。

Logdb在实现上分为了3层,分别称为 Interface, Reciver 和 Worker,整体架构可以用下面这个简图表示(Windows 自带画图画的,比较挫)。

Logdb架构

其中最核心的部分是 Reciver 进程。Reciver 在收到请求后,根据配置和协议格式对消息进行解析,并生成对应的SQL语句。在默认情况下,为了提高入库的效率,不是每条日志都进行入库,而是缓存一定的时间,进行批量入库。因为对于 Mysql 来说,使用含多个VALUE的INSERT语句同时插入几行,比单行的 Insert 要快很多倍。在默认情况下,LogDB对每个表维护一个缓存,如果缓存时间超过 500ms,或者缓存长度达到 1MB,则进行入库操作。这对于人来说,可以说是实时入库的,查询上不会造成延迟。

因为入库是一个阻塞的操作,所以将入库的工作交给另一个独立的进程Worker 来做。Reciver 通过一个基于共享内存和文件的队列将 SQL 语句推送给一个 Worker。这个队列也是很有趣的:当Worker 没有即时处理队列中的数据导致队列内存满时,会自动把新的数据写入文件。这就保证即使在短时间内请求量超过Worker 进程能处理的量时,数据仍然不会丢失,而在正常情况下,又能避免大量的文件IO操作。

Worker 在入库时,如果检测到因为数据库连接断开而失败的情况,则把失败的数据放入另一个队列。当数据库连接恢复时,自动的重新入库这些数据。这就保证,即使在数据库发生异常或重启时,也不需要人为的干涉,更不会丢失数据。

那 Interface 进程做什么呢?其实 Interface 进程所做的工作非常简单:它将接受到的请求转发给 Reciver并等待回包。如果超过1s 没有收到 Reciver 的回包,那么同样把这个请求放入一个队列,等到 Reciver 有回包时重试。这就保证,即使在重启 Reciver时,新的数据也不会丢失。

Logdb 的配置使用 ini 格式,还是很简单易懂的,自带的配置文件是自说明的,在这里

扩展新的字段时,需要再原有字段的最后面添加。这是因为,Logdb 所用的协议是非常简单的:首先是简单地包头,然后是数据部分,数据是按照配置的简单地顺序排列再一起的。如果新增的字段放在了中间,那么在解析请求时就可能得到脏数据或者解析失败。为了在升级后兼容老的API,再解析请求时添加了简单地规则:如果请求包剩余长度为0,那么使用空值。

一开始是准备设计的更通用一点,希望能够支持多种数据库,但后来发现不同的数据库支持的字段差别还是挺大的,比较难实现的通用。我们用 Mysql 比较多,就只支持这一种好了。