mysql binlog 記錄了所有可能涉及更新的操作,可以用來作為增量備份的一種選擇。為了管理binlog ,需要讀取每個binlog 文件的准確的開始和結束時間。用mysqlbinlog 工具可以解析binlog 文件,所以也可以通過分析輸出結果來獲取。但是mysqlbinlog 只能順序讀取記錄,如果只是分析開始時間還好,要分析結束時間,就必須等它把整個binlog 處理完。在binlog 文件體積大的時候,代價就大了些。好在mysql 對binlog 文件的格式是公開的,所以我們可以直接通過解析文件自己實現。
binlog 文件的格式在http://forge.mysql.com/wiki/MySQL_Internals_Binary_Log 可以找到。每個binlog 文件都有相同的開頭:0xfe 0×62 0×69 0x6e 。也就是0xfe 後面加上bin 。之後,就是一個個事件數據。binlog 的事件類型有很多種,但每個binlog 文件的第一個事件一定是格式描述事件(format description event),描述了binlog 文件格式版本信息;最後一個時間一定是輪轉事件(rotate event),記錄了下一個binlog 的文件名和事件開始偏移位置。每個事件都有一個一致的事件頭,其中就有事件的時間戳、事件類型等。讀取第一個事件和最後一個事件的信息就可以獲取binlog 文件的准確開始和結束時間了。
讀取第一個事件format description event 要容易一些,seek 跳過文件頭,讀取事件頭就行了。讀取最後一個事件的時間要稍麻煩些。因為事件的長度是不固定的。對於輪轉事件來說,除了事件頭以外,後面還有一個64位整數的開始位置偏移量以及下一個binlog 的文件名。長度不確定的部分就是最後的文件名部分。好在那個偏移量是一個固定的值:4(也就是跳過文件頭),所以可以從後往前讀取,用它來作為標記,檢查是否讀完了文件名。然後就可以跳過文件名和偏移量,讀取最後一個事件的事件頭了。
php 代碼如下:
<?php
/**
* read binlog info
*
* A mysql binlog file is begin with a head "\xfebin" and then log evnets. The
* first event is a format description event, the last event is a rotate event.
*
* For more infomation about mysql binlog format, see http://forge.mysql.com/wiki/MySQL_Internals_Binary_Log
*/
class BinlogInfo {
const EVENT_HEAD_SIZE = 19;
const FORMAT_DESCRIPTION_EVENT_DATA_SIZE = 59;
const BINLOG_HEAD = "\xfebin";
const FORMAT_DESCRIPTION_EVENT = 15;
const ROTATE_EVENT = 4;
private $eventHeadPackStr = '';
private $formatDescriptionEventDataPackStr = '';
function __construct() {
$this->eventHeadPackStr = $this->eventHeadPackStr();
$this->formatDescriptionEventDataPackStr = $this->formatDescriptionEventDataPackStr();
}
protected function eventHeadPackStr() {
$event_header_struct = array(
'timestamp' => 'l',
'type_code' => 'c',
'server_id' => 'l',
'event_length' => 'l',
'next_position' => 'l',
'flags' => 's',
);
return $this->toPackStr($event_header_struct);
}
protected function formatDescriptionEventDataPackStr() {
$format_description_event_data_struct = array(
'binlog_version' => 's',
'server_version' => 'a50',
'create_timestamp' => 'l',
'head_length' => 'c'
);
return $this->toPackStr($format_description_event_data_struct);
}
protected function toPackStr($arr) {
$ret = '';
foreach ($arr as $k=>$v) {
$ret.= '/'.$v.$k;
}
$ret = substr($ret, 1);
return $ret;
}
/**
* @param resource $file
*
* Mysql binlog file begin with a 4 bytes head: "\xfebin".
*/
protected function isBinlog($file) {
rewind($file);
$head = fread($file, strlen(self::BINLOG_HEAD));
return $head == self::BINLOG_HEAD;
}
/**
* @param resource $file
*
* Format description event is the first event of a binlog file
*/
protected function readFormatDescriptionEvent($file) {
fseek($file, strlen(self::BINLOG_HEAD), SEEK_SET);
$head_str = fread($file, self::EVENT_HEAD_SIZE);
$head = unpack($this->eventHeadPackStr, $head_str);
if ($head['type_code'] != self::FORMAT_DESCRIPTION_EVENT) {
return null;
}
$data_str= fread($file, self::FORMAT_DESCRIPTION_EVENT_DATA_SIZE);
$data = unpack($this->formatDescriptionEventDataPackStr, $data_str);
return array('head'=>$head, 'data'=>$data);
}
/**
* @param resource $file
*
* Rotate event is the last event of a binglog.
* After event header, there is a 64bit int indicate the first event
* position of next binlog file and next binlog file name without \0 at end.
* The position is always be 4 (hex: 0400000000000000).
*
*/
protected function readRotateEvent($file)
{
/**
* Rotate event size is 19(head size) + 8(pos) + len(filename).
* 100 bytes can contain a filename which length less than 73 bytes and
* it is short than the length of format description event so filesize -
* bufsize will never be negative.
*/
$bufsize = 100;
$size_pos = 8;
fseek($file, -$bufsize, SEEK_END);
$buf = fread($file, $bufsize);
$min_begin = strlen(self::BINLOG_HEAD) + self::EVENT_HEAD_SIZE + $size_pos;
$ok = false;
for ($i = $bufsize - 1; $i > $min_begin; $i--) {
if ($buf[$i] == "\0") {
$ok = true;
break;
}
}
if (!$ok) {
return null;
}
$next_filename = substr($buf, $i + 1);
$head_str = substr($buf, $i + 1 - $size_pos - self::EVENT_HEAD_SIZE, self::EVENT_HEAD_SIZE);
$head = unpack($this->eventHeadPackStr, $head_str);
if ($head['type_code'] != self::ROTATE_EVENT) {
return null;
}
return array('head'=>$head, 'nextFile'=>$next_filename);
}
/**
* @param string $path path to binlog file
*/
function read($path) {
$file = fopen($path, 'r');
if (!$file) {
return null;
}
if (!$this->isBinlog($file)) {
fclose($file);
return null;
}
$fde = $this->readFormatDescriptionEvent($file);
$re = $this->readRotateEvent($file);
fclose($file);
return array(
'beginAt' => $fde['head']['timestamp'],
'endAt' => $re['head']['timestamp'],
'nextFile' => $re['nextFile'],
'serverVersion' => $fde['data']['server_version'],
);
}
}