目录

记一次MySQL删库的恢复

背景

最近正在做语音相关的工作,所有的语料数据都放在一个mysql数据库里面,同事一次误操作导致整个数据库被drop,里面包括了全部的文本语料和词库。 数据库放在一台linux机器上,文件系统是XFS。

从磁盘恢复数据

说实话,之前没有相关经验,只有先google一下看看有没有现成可以参考的解决方案。在stackexchange上看到有人提到TwinDB data recovery toolkit可以恢复,先需要停掉mysql,并停止一切写入,赶紧systemctl stop mysql,umount掉mysql database所在的disk。

MySQL 5.5之前是所有数据都放在ibdata1文件里面的,因此被删除的数据还在ibdata里面,而5.6之后是放在单独的目录里面,drop database/table是直接删除文件的。我们用的是8.0,不能直接扫描ibdata1,需要扫描整个磁盘,同时也意味着其他进程的写入也很可能导致数据被覆盖掉。不过既然是删除了文件,可以先试试能不能从xfs里面恢复删除的文件,找到一个工具,扫描了一遍磁盘,没能够找到对应的ibd文件。

TwinDB data recovery toolkit使用

  1. 先下载到本地编译

    1
    2
    3
    
    git clone https://github.com/twindb/undrop-for-innodb
    cd undrop-for-innodb
    make
    
  2. 使用stream_parser扫描整个磁盘

    1
    
    ./stream_parser /dev/sdc1
    

    会在当前目录下生成一个文件夹pages-XXX,XXX为扫描的文件名,我这里生成的是pages-sdc1,里面包含了FIL_PAGE_INDEX和FIL_PAGE_BLOB两个目录。

  3. 使用c_parser得到表corpora的ID,0000000000000001.page是存放SYS_TABLES的page文件

    1
    2
    3
    
    ./c_parser -4f pages-sdc1/FIL_PAGE_INDEX/0000000000000001.page -t dictionary/SYS_TABLES.sql | grep corpora
    
    000000000504    85000001320110  SYS_TABLES      "corpora"  54      4       1       0       0       ""      1
    

    因为InnoDB的数据是放在primary index上的,通过获取primary index的ID,就能够得到存放数据的page文件。0000000000000003.page是存放SYS_INDEXES的page文件

    1
    2
    3
    
    ./c_parser -4f pages-sdc1/FIL_PAGE_INDEX/0000000000000003.page -t dictionary/SYS_INDEXES.sql | grep 54
    
    000000000504    85000001320178  SYS_INDEXES     54      112      "PRIMARY"       1       3       1       3
    

    找到了corpora的数据是在0000000000000112.page文件里面的 在恢复数据之前需要先确定表结构,还好我们知道,创建一个包含corpora DDL的sql文件放到corpora.sql

  4. 恢复数据

    1
    2
    3
    
    mkdir -p  dumps/default
    ./c_parser -6f pages-sdc1/FIL_PAGE_INDEX/0000000000000112.page \
        -t corpora.sql > dumps/default/corpora 2> dumps/default/corpora.sql
    

    运行完成后,看看是否恢复成功

    1
    
    head -100 dumps/default/corpora
    

    很遗憾,数据不全,而且还有很多错误数据,看来已经被覆盖掉了不少page。尝试恢复另外两张表也没什么收获。难道只能从删库到跑路了?

从backup恢复

之前有做一个定时backup的脚本,每天半夜进行backup,赶紧到backup的机器上看看,结果发现最近一次的backup是3个月前的了。执行

1
crontab -l

是有对应的命令的,查了一下,有人说crontab命令后面必须有一行空行才能正确执行,还不确定是否是这个导致,顺手改掉,后面再定时检查。

从solr恢复

还好文字语料在solr中是有cache的,用于加速查询,赶紧连接到solr中,dump到文件中

1
curl "http://solr-server/solr/corpora/select?q=*:*&wt=csv&indent=true&rows=26159501" -o corpora.csv

有了csv文件,写一个python脚本直接导入即可。

从内存中恢复

但是还有词表没恢复,这张表是没有solr缓存的,怎么办? 还好,我们有一个在线分词查询程序,会将词表cache到内存中,这是一个python写的http server,分词是使用c++实现的DLL,运行在windows上。

词表保存在一个hash table里面,结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Word
{
    unsigned char   nbytes;   /* number of bytes */
    char            length;   /* number of characters */
    unsigned int    freq;
    char            text[word_embed_len];
};
struct Entry
{
    Word *word;
    Entry *next;
};
static Entry **bins = static_cast<Entry **>(std::calloc(init_size,
                                            sizeof(Entry *)));

从代码可以看出来,用的是Seperated chaining方式存放的,如下图:

/img/Hash_table.png

怎么从内存中还原词表呢?有两种方法:

我选择第二个,使用Process Explorer保存一个dump文件。这里需要注意,必须保存full dump,否则Heap不会保存,后续无法获取heap中的数据

minidump文件格式

不像其他文件格式,MS对minidump文件的格式描述还是很清楚的,在这里可以找到。有人已经给我们写好了python解析minidump的库,我们只需要找到全局变量数组bins,然后遍历这个数组找出所有的Word即可。

定位变量地址

在windows中,一个进程运行时是有自己独立进程空间的,这个进程依赖的DLL都会作为module被加载到这个进程的地址空间中来,我们在minidump中可以找到这个DLL所在的base address,然后在这个DLL中定位到这个变量所在offset,两者相加即可得到这个变量在整个进程空间中的地址。

minidump中包含了所有module的相关信息,而变量的偏移地址,就需要借助一些其他方法来获取了。如果有DLL对应的PDB文件,则比较容易,但是这个DLL的PDB文件也没有保存下来,只能想其他办法。

使用IDA Pro,打开这个DLL,通过exported function,很容易定位到这个操作这个变量的函数,然后在这个函数中找到这个全局变量的位置,然后用这个地址减去IDA加载DLL时的base address,就得到了这个变量的offset。

有了offset,接下来就是遍历hash table了。这里我使用了python的struct库来进行数据的数据的读取。

读取minidump

minidump这个库给我们提供了方便进行内存位置读取的方法,简化后的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from minidump.minidumpfile import MinidumpFile
from minidump.minidumpreader import MinidumpFileReader
import struct

INT_SIZE = 4
PTR_SIZE = 8

class Word:
    def __init__(self, reader, ptr):
        self.reader = reader
        self.ptr = ptr
        buf = reader.read(ptr, 8)
        # ignore padding between length and freq
        self.nbytes, self.length, padding, self.freq = struct.unpack('<BBHL', buf)
        if self.nbytes > 0:
            self.text = reader.read(ptr + 8, self.nbytes).decode('utf8')
        else:
            self.text = ''

    def __str__(self):
        if self.nbytes > 0:
            return 'text: {}, freq: {}'.format(self.text, self.freq)
        else:
            return '<EMPTY>'

class Entry:
    def __init__(self, reader, ptr):
        self.reader = reader
        self.ptr = ptr
        self.word = None

        buf = reader.read(ptr, 2 * PTR_SIZE)
        self.pword, self.pnext = struct.unpack('<QQ', buf)
        if self.pword != 0:
            self.word = Word(reader, self.pword)

    def __str__(self):
        if self.pword != 0:
            return '{}, pnext: {:x}'.format(self.word, self.pnext)
        else:
            return 'pnext:{:x}'.format(self.pnext)

def process_entry(entry):
    pass

def main():
    mp = MinidumpFile.parse('python.dmp')
    reader = MinidumpFileReader(mp)

    dll = reader.get_module_by_name('XXX.dll')
    baseaddr = dll.baseaddress

    # get offset from IDA Pro
    p_n_bins = baseaddr + 0x1EA58
    pp_bins = baseaddr + 0x1FD60

    p_bins = struct.unpack('<Q', reader.read(pp_bins, PTR_SIZE))[0]
    n_bins = struct.unpack('<L', reader.read(p_n_bins, INT_SIZE))[0]


    print('p_n_bins: {:x}, n_bins: {:x}'.format(p_n_bins, n_bins))
    for i in range(n_bins):
        pentry = struct.unpack('<Q', reader.read( p_bins + i * PTR_SIZE, PTR_SIZE))[0]
        if pentry != 0:
            entry = Entry(reader, pentry)
            process_word(entry)

            while entry.pnext != 0:
                entry = Entry(reader, entry.pnext)
                process_entry(entry)


if __name__ == '__main__':
    main()

总结

  1. 出现误删,一定要马上停止写入,umount磁盘,如果有可能,尽量保留磁盘镜像
  2. 数据库的权限如果没有设置好,会导致很容易就能删库跑路
  3. 数据库的备份没有起作用,一定要定时检查备份脚本是否正常工作
  4. 后面有时间对InnoDB的结构进行一下分析