Quantcast
Channel: 数据库内核月报
Viewing all 692 articles
Browse latest View live

MySQL · 引擎特性 · InnoDB Buffer Pool

$
0
0

前言

用户对数据库的最基本要求就是能高效的读取和存储数据,但是读写数据都涉及到与低速的设备交互,为了弥补两者之间的速度差异,所有数据库都有缓存池,用来管理相应的数据页,提高数据库的效率,当然也因为引入了这一中间层,数据库对内存的管理变得相对比较复杂。本文主要分析MySQL Buffer Pool的相关技术以及实现原理,源码基于阿里云RDS MySQL 5.6分支,其中部分特性已经开源到AliSQL。Buffer Pool相关的源代码在buf目录下,主要包括LRU List,Flu List,Double write buffer, 预读预写,Buffer Pool预热,压缩页内存管理等模块,包括头文件和IC文件,一共两万行代码。

基础知识

Buffer Pool Instance:

大小等于innodb_buffer_pool_size/innodb_buffer_pool_instances,每个instance都有自己的锁,信号量,物理块(Buffer chunks)以及逻辑链表(下面的各种List),即各个instance之间没有竞争关系,可以并发读取与写入。所有instance的物理块(Buffer chunks)在数据库启动的时候被分配,直到数据库关闭内存才予以释放。当innodb_buffer_pool_size小于1GB时候,innodb_buffer_pool_instances被重置为1,主要是防止有太多小的instance从而导致性能问题。每个Buffer Pool Instance有一个page hash链表,通过它,使用space_id和page_no就能快速找到已经被读入内存的数据页,而不用线性遍历LRU List去查找。注意这个hash表不是InnoDB的自适应哈希,自适应哈希是为了减少Btree的扫描,而page hash是为了避免扫描LRU List。

数据页:

InnoDB中,数据管理的最小单位为页,默认是16KB,页中除了存储用户数据,还可以存储控制信息的数据。InnoDB IO子系统的读写最小单位也是页。如果对表进行了压缩,则对应的数据页称为压缩页,如果需要从压缩页中读取数据,则压缩页需要先解压,形成解压页,解压页为16KB。压缩页的大小是在建表的时候指定,目前支持16K,8K,4K,2K,1K。即使压缩页大小设为16K,在blob/varchar/text的类型中也有一定好处。假设指定的压缩页大小为4K,如果有个数据页无法被压缩到4K以下,则需要做B-tree分裂操作,这是一个比较耗时的操作。正常情况下,Buffer Pool中会把压缩和解压页都缓存起来,当Free List不够时,按照系统当前的实际负载来决定淘汰策略。如果系统瓶颈在IO上,则只驱逐解压页,压缩页依然在Buffer Pool中,否则解压页和压缩页都被驱逐。

Buffer Chunks:

包括两部分:数据页和数据页对应的控制体,控制体中有指针指向数据页。Buffer Chunks是最低层的物理块,在启动阶段从操作系统申请,直到数据库关闭才释放。通过遍历chunks可以访问几乎所有的数据页,有两种状态的数据页除外:没有被解压的压缩页(BUF_BLOCK_ZIP_PAGE)以及被修改过且解压页已经被驱逐的压缩页(BUF_BLOCK_ZIP_DIRTY)。此外数据页里面不一定都存的是用户数据,开始是控制信息,比如行锁,自适应哈希等。

逻辑链表:

链表节点是数据页的控制体(控制体中有指针指向真正的数据页),链表中的所有节点都有同一的属性,引入其的目的是方便管理。下面其中链表都是逻辑链表。

Free List:

其上的节点都是未被使用的节点,如果需要从数据库中分配新的数据页,直接从上获取即可。InnoDB需要保证Free List有足够的节点,提供给用户线程用,否则需要从FLU List或者LRU List淘汰一定的节点。InnoDB初始化后,Buffer Chunks中的所有数据页都被加入到Free List,表示所有节点都可用。

LRU List:

这个是InnoDB中最重要的链表。所有新读取进来的数据页都被放在上面。链表按照最近最少使用算法排序,最近最少使用的节点被放在链表末尾,如果Free List里面没有节点了,就会从中淘汰末尾的节点。LRU List还包含没有被解压的压缩页,这些压缩页刚从磁盘读取出来,还没来的及被解压。LRU List被分为两部分,默认前5/8为young list,存储经常被使用的热点page,后3/8为old list。新读入的page默认被加在old list头,只有满足一定条件后,才被移到young list上,主要是为了预读的数据页和全表扫描污染buffer pool。

FLU List:

这个链表中的所有节点都是脏页,也就是说这些数据页都被修改过,但是还没来得及被刷新到磁盘上。在FLU List上的页面一定在LRU List上,但是反之则不成立。一个数据页可能会在不同的时刻被修改多次,在数据页上记录了最老(也就是第一次)的一次修改的lsn,即oldest_modification。不同数据页有不同的oldest_modification,FLU List中的节点按照oldest_modification排序,链表尾是最小的,也就是最早被修改的数据页,当需要从FLU List中淘汰页面时候,从链表尾部开始淘汰。加入FLU List,需要使用flush_list_mutex保护,所以能保证FLU List中节点的顺序。

Quick List:

这个链表是阿里云RDS MySQL 5.6加入的,使用带Hint的SQL查询语句,可以把所有这个查询的用到的数据页加入到Quick List中,一旦这个语句结束,就把这个数据页淘汰,主要作用是避免LRU List被全表扫描污染。

Unzip LRU List:

这个链表中存储的数据页都是解压页,也就是说,这个数据页是从一个压缩页通过解压而来的。

Zip Clean List:

这个链表只在Debug模式下有,主要是存储没有被解压的压缩页。这些压缩页刚刚从磁盘读取出来,还没来的及被解压,一旦被解压后,就从此链表中删除,然后加入到Unzip LRU List中。

Zip Free:

压缩页有不同的大小,比如8K,4K,InnoDB使用了类似内存管理的伙伴系统来管理压缩页。Zip Free可以理解为由5个链表构成的一个二维数组,每个链表分别存储了对应大小的内存碎片,例如8K的链表里存储的都是8K的碎片,如果新读入一个8K的页面,首先从这个链表中查找,如果有则直接返回,如果没有则从16K的链表中分裂出两个8K的块,一个被使用,另外一个放入8K链表中。

核心数据结构

InnoDB Buffer Pool有三种核心的数据结构:buf_pool_t,buf_block_t,buf_page_t。

but_pool_t:

存储Buffer Pool Instance级别的控制信息,例如整个Buffer Pool Instance的mutex,instance_no, page_hash,old_list_pointer等。还存储了各种逻辑链表的链表根节点。Zip Free这个二维数组也在其中。

buf_block_t:

这个就是数据页的控制体,用来描述数据页部分的信息(大部分信息在buf_page_t中)。buf_block_t中第一字段就是buf_page_t,这个不是随意放的,是必须放在第一字段,因为只有这样buf_block_t和buf_page_t两种类型的指针可以相互转换。第二个字段是frame字段,指向真正存数据的数据页。buf_block_t还存储了Unzip LRU List链表的根节点。另外一个比较重要的字段就是block级别的mutex。

buf_page_t:

这个可以理解为另外一个数据页的控制体,大部分的数据页信息存在其中,例如space_id, page_no, page state, newest_modification,oldest_modification,access_time以及压缩页的所有信息等。压缩页的信息包括压缩页的大小,压缩页的数据指针(真正的压缩页数据是存储在由伙伴系统分配的数据页上)。这里需要注意一点,如果某个压缩页被解压了,解压页的数据指针是存储在buf_block_t的frame字段里。

这里介绍一下buf_page_t中的state字段,这个字段主要用来表示当前页的状态。一共有八种状态。这八种状态对初学者可能比较难理解,尤其是前三种,如果看不懂可以先跳过。

BUF_BLOCK_POOL_WATCH:

这种类型的page是提供给purge线程用的。InnoDB为了实现多版本,需要把之前的数据记录在undo log中,如果没有读请求再需要它,就可以通过purge线程删除。换句话说,purge线程需要知道某些数据页是否被读取,现在解法就是首先查看page hash,看看这个数据页是否已经被读入,如果没有读入,则获取(启动时候通过malloc分配,不在Buffer Chunks中)一个BUF_BLOCK_POOL_WATCH类型的哨兵数据页控制体,同时加入page_hash但是没有真正的数据(buf_blokc_t::frame为空)并把其类型置为BUF_BLOCK_ZIP_PAGE(表示已经被使用了,其他purge线程就不会用到这个控制体了),相关函数buf_pool_watch_set,如果查看page hash后发现有这个数据页,只需要判断控制体在内存中的地址是否属于Buffer Chunks即可,如果是表示对应数据页已经被其他线程读入了,相关函数buf_pool_watch_occurred。另一方面,如果用户线程需要这个数据页,先查看page hash看看是否是BUF_BLOCK_POOL_WATCH类型的数据页,如果是则回收这个BUF_BLOCK_POOL_WATCH类型的数据页,从Free List中(即在Buffer Chunks中)分配一个空闲的控制体,填入数据。这里的核心思想就是通过控制体在内存中的地址来确定数据页是否还在被使用。

BUF_BLOCK_ZIP_PAGE:

当压缩页从磁盘读取出来的时候,先通过malloc分配一个临时的buf_page_t,然后从伙伴系统中分配出压缩页存储的空间,把磁盘中读取的压缩数据存入,然后把这个临时的buf_page_t标记为BUF_BLOCK_ZIP_PAGE状态(buf_page_init_for_read),只有当这个压缩页被解压了,state字段才会被修改为BUF_BLOCK_FILE_PAGE,并加入LRU List和Unzip LRU List(buf_page_get_gen)。如果一个压缩页对应的解压页被驱逐了,但是需要保留这个压缩页且压缩页不是脏页,则这个压缩页被标记为BUF_BLOCK_ZIP_PAGE(buf_LRU_free_page)。所以正常情况下,处于BUF_BLOCK_ZIP_PAGE状态的不会很多。前述两种被标记为BUF_BLOCK_ZIP_PAGE的压缩页都在LRU List中。另外一个用法是,从BUF_BLOCK_POOL_WATCH类型节点中,如果被某个purge线程使用了,也会被标记为BUF_BLOCK_ZIP_PAGE。

BUF_BLOCK_ZIP_DIRTY:

如果一个压缩页对应的解压页被驱逐了,但是需要保留这个压缩页且压缩页是脏页,则被标记为BUF_BLOCK_ZIP_DIRTY(buf_LRU_free_page),如果该压缩页又被解压了,则状态会变为BUF_BLOCK_FILE_PAGE。因此BUF_BLOCK_ZIP_DIRTY也是一个比较短暂的状态。这种类型的数据页都在Flush List中。

BUF_BLOCK_NOT_USED:

当链表处于Free List中,状态就为此状态。是一个能长期存在的状态。

BUF_BLOCK_READY_FOR_USE:

当从Free List中,获取一个空闲的数据页时,状态会从BUF_BLOCK_NOT_USED变为BUF_BLOCK_READY_FOR_USE(buf_LRU_get_free_block),也是一个比较短暂的状态。处于这个状态的数据页不处于任何逻辑链表中。

BUF_BLOCK_FILE_PAGE:

正常被使用的数据页都是这种状态。LRU List中,大部分数据页都是这种状态。压缩页被解压后,状态也会变成BUF_BLOCK_FILE_PAGE。

BUF_BLOCK_MEMORY:

Buffer Pool中的数据页不仅可以存储用户数据,也可以存储一些系统信息,例如InnoDB行锁,自适应哈希索引以及压缩页的数据等,这些数据页被标记为BUF_BLOCK_MEMORY。处于这个状态的数据页不处于任何逻辑链表中

BUF_BLOCK_REMOVE_HASH:

当加入Free List之前,需要先把page hash移除。因此这种状态就表示此页面page hash已经被移除,但是还没被加入到Free List中,是一个比较短暂的状态。
总体来说,大部分数据页都处于BUF_BLOCK_NOT_USED(全部在Free List中)和BUF_BLOCK_FILE_PAGE(大部分处于LRU List中,LRU List中还包含除被purge线程标记的BUF_BLOCK_ZIP_PAGE状态的数据页)状态,少部分处于BUF_BLOCK_MEMORY状态,极少处于其他状态。前三种状态的数据页都不在Buffer Chunks上,对应的控制体都是临时分配的,InnoDB把他们列为invalid state(buf_block_state_valid)。
如果理解了这八种状态以及其之间的转换关系,那么阅读Buffer pool的代码细节就会更加游刃有余。

接下来,简单介绍一下buf_page_t中buf_fix_count和io_fix两个变量,这两个变量主要用来做并发控制,减少mutex加锁的范围。当从buffer pool读取一个数据页时候,会其加读锁,然后递增buf_page_t::buf_fix_count,同时设置buf_page_t::io_fix为BUF_IO_READ,然后即可以释放读锁。后续如果其他线程在驱逐数据页(或者刷脏)的时候,需要先检查一下这两个变量,如果buf_page_t::buf_fix_count不为零且buf_page_t::io_fix不为BUF_IO_NONE,则不允许驱逐(buf_page_can_relocate)。这里的技巧主要是为了减少数据页控制体上mutex的争抢,而对数据页的内容,读取的时候依然要加读锁,修改时加写锁。

Buffer Pool内存初始化

Buffer Pool的内存初始化,主要是Buffer Chunks的内存初始化,buffer pool instance一个一个轮流初始化。核心函数为buf_chunk_initos_mem_alloc_large
。阅读代码可以发现,目前从操作系统分配内存有两种方式,一种是通过HugeTLB的方式来分配,另外一种使用传统的mmap来分配。

HugeTLB:

这是一种大内存块的分配管理技术。类似数据库对数据的管理,内存也按照页来管理,默认的页大小为4KB,HugeTLB就是把页大小提高到2M或者更加多。程序传送给cpu都是虚拟内存地址,cpu必须通过快表来映射到真正的物理内存地址。快表的全集放在内存中,部分热点内存页可以放在cpu cache中,从而提高内存访问效率。假设cpu cache为100KB,每条快表占用1KB,页大小为4KB,则热点内存页为100KB/1KB=100条,覆盖1004KB=400KB的内存数据,但是如果也默认页大小为2M,则同样大小的cpu cache,可以覆盖1002M=200MB的内存数据,也就是说,访问200MB的数据只需要一次读取内存即可(如果映射关系没有在cache中找到,则需要先把映射关系从内存中读到cache,然后查找,最后再去读内存中需要的数据,会造成两次访问物理内存)。也就是说,使用HugeTLB这种大内存技术,可以提高快表的命中率,从而提高访问内存的性能。当然这个技术也不是银弹,内存页变大了也必定会导致更多的页内的碎片。如果需要从swap分区中加载虚拟内存,也会变慢。当然最终要的理由是,4KB大小的内存页已经被业界稳定使用很多年了,如果没有特殊的需求不需要冒这个风险。在InnoDB中,如果需要用到这项技术可以使用super-large-pages参数启动MySQL。

mmap分配:

在Linux下,多个进程需要共享一片内存,可以使用mmap来分配和绑定,所以只提供给一个MySQL进程使用也是可以的。用mmap分配的内存都是虚存,在top命令中占用VIRT这一列,而不是RES这一列,只有相应的内存被真正使用到了,才会被统计到RES中,提高内存使用率。这样是为什么常常看到MySQL一启动就被分配了很多的VIRT,而RES却是慢慢涨上来的原因。这里大家可能有个疑问,为啥不用malloc。其实查阅malloc文档,可以发现,当请求的内存数量大于MMAP_THRESHOLD(默认为128KB)时候,malloc底层就是调用了mmap。在InnoDB中,默认使用mmap来分配。
分配完了内存,buf_chunk_init函数中,把这片内存划分为两个部分,前一部分是数据页控制体(buf_block_t),在阿里云RDS MySQL 5.6 release版本中,每个buf_block_t是424字节,一共有innodb_buffer_pool_size/UNIV_PAGE_SIZE个。后一部分是真正的数据页,按照UNIV_PAGE_SIZE分隔。假设page大小为16KB,则数据页控制体占的内存:数据页约等于1:38.6,也就是说如果innodb_buffer_pool_size被配置为40G,则需要额外的1G多空间来存数据页的控制体。
划分完空间后,遍历数据页控制体,设置buf_block_t::frame指针,指向真正的数据页,然后把这些数据页加入到Free List中即可。初始化完Buffer Chunks的内存,还需要初始化BUF_BLOCK_POOL_WATCH类型的数据页控制块,page hash的结构体,zip hash的结构体(所有被压缩页的伙伴系统分配走的数据页面会加入到这个哈希表中)。注意这些内存是额外分配的,不包含在Buffer Chunks中。
除了buf_pool_init外,建议读者参考一下but_pool_free这个内存释放函数,加深对Buffer Pool相关内存的理解。

Buf_page_get函数解析

这个函数极其重要,是其他模块获取数据页的外部接口函数。如果请求的数据页已经在Buffer Pool中了,修改相应信息后,就直接返回对应数据页指针,如果Buffer Pool中没有相关数据页,则从磁盘中读取。Buf_page_get是一个宏定义,真正的函数为buf_page_get_gen,参数主要为space_id, page_no, lock_type, mode以及mtr。这里主要介绍一个mode这个参数,其表示读取的方式,目前支持六种,前三种用的比较多。

BUF_GET:

默认获取数据页的方式,如果数据页不在Buffer Pool中,则从磁盘读取,如果已经在Buffer Pool中,需要判断是否要把他加入到young list中以及判断是否需要进行线性预读。如果是读取则加读锁,修改则加写锁。

BUF_GET_IF_IN_POOL:

只在Buffer Pool中查找这个数据页,如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则直接返回空。加锁方式与BUF_GET类似。

BUF_PEEK_IF_IN_POOL:

与BUF_GET_IF_IN_POOL类似,只是即使条件满足也不把它加入到young list中也不进行线性预读。加锁方式与BUF_GET类似。

BUF_GET_NO_LATCH:

不管对数据页是读取还是修改,都不加锁。其他方面与BUF_GET类似。

BUF_GET_IF_IN_POOL_OR_WATCH:

只在Buffer Pool中查找这个数据页,如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则设置watch。加锁方式与BUF_GET类似。这个是要是给purge线程用。

BUF_GET_POSSIBLY_FREED:

这个mode与BUF_GET类似,只是允许相应的数据页在函数执行过程中被释放,主要用在估算Btree两个slot之前的数据行数。
接下来,我们简要分析一下这个函数的主要逻辑。

  • 首先通过buf_pool_get函数依据space_id和page_no查找指定的数据页在那个Buffer Pool Instance里面。算法很简单instance_no = (space_id << 20 + space_id + page_no >> 6) % instance_num,也就是说先通过space_id和page_no算出一个fold value然后按照instance的个数取余数即可。这里有个小细节,page_no的第六位被砍掉,这是为了保证一个extent的数据能被缓存到同一个Buffer Pool Instance中,便于后面的预读操作。

  • 接着,调用buf_page_hash_get_low函数在page hash中查找这个数据页是否已经被加载到对应的Buffer Pool Instance中,如果没有找到这个数据页且mode为BUF_GET_IF_IN_POOL_OR_WATCH则设置watch数据页(buf_pool_watch_set),接下来,如果没有找到数据页且mode为BUF_GET_IF_IN_POOL、BUF_PEEK_IF_IN_POOL或者BUF_GET_IF_IN_POOL_OR_WATCH函数直接返回空,表示没有找到数据页。如果没有找到数据但是mode为其他,就从磁盘中同步读取(buf_read_page)。在读取磁盘数据之前,我们如果发现需要读取的是非压缩页,则先从Free List中获取空闲的数据页,如果Free List中已经没有了,则需要通过刷脏来释放数据页,这里的一些细节我们后续在LRU模块再分析,获取到空闲的数据页后,加入到LRU List中(buf_page_init_for_read)。在读取磁盘数据之前,我们如果发现需要读取的是压缩页,则临时分配一个buf_page_t用来做控制体,通过伙伴系统分配到压缩页存数据的空间,最后同样加入到LRU List中(buf_page_init_for_read)。做完这些后,我们就调用IO子系统的接口同步读取页面数据,如果读取数据失败,我们重试100次(BUF_PAGE_READ_MAX_RETRIES)然后触发断言,如果成功则判断是否要进行随机预读(随机预读相关的细节我们也在预读预写模块分析)。

  • 接着,读取数据成功后,我们需要判断读取的数据页是不是压缩页,如果是的话,因为从磁盘中读取的压缩页的控制体是临时分配的,所以需要重新分配block(buf_LRU_get_free_block),把临时分配的buf_page_t给释放掉,用buf_relocate函数替换掉,接着进行解压,解压成功后,设置state为BUF_BLOCK_FILE_PAGE,最后加入Unzip LRU List中。

  • 接着,我们判断这个页是否是第一次访问,如果是则设置buf_page_t::access_time,如果不是,我们则判断其是不是在Quick List中,如果在Quick List中且当前事务不是加过Hint语句的事务,则需要把这个数据页从Quick List删除,因为这个页面被其他的语句访问到了,不应该在Quick List中了。

  • 接着,如果mode不为BUF_PEEK_IF_IN_POOL,我们需要判断是否把这个数据页移到young list中,具体细节在后面LRU模块中分析。

  • 接着,如果mode不为BUF_GET_NO_LATCH,我们给数据页加上读写锁。

  • 最后,如果mode不为BUF_PEEK_IF_IN_POOL且这个数据页是第一次访问,则判断是否需要进行线性预读(线性预读相关的细节我们也在预读预写模块分析)。

LRU List中young list和old list的维护

当LRU List链表大于512(BUF_LRU_OLD_MIN_LEN)时,在逻辑上被分为两部分,前面部分存储最热的数据页,这部分链表称作young list,后面部分则存储冷数据页,这部分称作old list,一旦Free List中没有页面了,就会从冷页面中驱逐。两部分的长度由参数innodb_old_blocks_pct控制。每次加入或者驱逐一个数据页后,都要调整young list和old list的长度(buf_LRU_old_adjust_len),同时引入BUF_LRU_OLD_TOLERANCE来防止链表调整过频繁。当LRU List链表小于512,则只有old list。
新读取进来的页面默认被放在old list头,在经过innodb_old_blocks_time后,如果再次被访问了,就挪到young list头上。一个数据页被读入Buffer Pool后,在小于innodb_old_blocks_time的时间内被访问了很多次,之后就不再被访问了,这样的数据页也很快被驱逐。这个设计认为这种数据页是不健康的,应该被驱逐。
此外,如果一个数据页已经处于young list,当它再次被访问的时候,不会无条件的移动到young list头上,只有当其处于young list长度的1/4(大约值)之后,才会被移动到young list头部,这样做的目的是减少对LRU List的修改,否则每访问一个数据页就要修改链表一次,效率会很低,因为LRU List的根本目的是保证经常被访问的数据页不会被驱逐出去,因此只需要保证这些热点数据页在头部一个可控的范围内即可。相关逻辑可以参考函数buf_page_peek_if_too_old

buf_LRU_get_free_block函数解析

这个函数以及其调用的函数可以说是整个LRU模块最重要的函数,在整个Buffer Pool模块中也有举足轻重的作用。如果能把这几个函数吃透,相信其他函数很容易就能读懂。

  • 首先,如果是使用ENGINE_NO_CACHE发送过来的SQL需要读取数据,则优先从Quick List中获取(buf_quick_lru_get_free)。

  • 接着,统计Free List和LRU List的长度,如果发现他们再Buffer Chunks占用太少的空间,则表示太多的空间被行锁,自使用哈希等内部结构给占用了,一般这些都是大事务导致的。这时候会给出报警。

  • 接着,查看Free List中是否还有空闲的数据页(buf_LRU_get_free_only),如果有则直接返回,否则进入下一步。大多数情况下,这一步都能找到空闲的数据页。

  • 如果Free List中已经没有空闲的数据页了,则会尝试驱逐LRU List末尾的数据页。如果系统有压缩页,情况就有点复杂,InnoDB会调用buf_LRU_evict_from_unzip_LRU来决定是否驱逐压缩页,如果Unzip LRU List大于LRU List的十分之一或者当前InnoDB IO压力比较大,则会优先从Unzip LRU List中把解压页给驱逐,否则会从LRU List中把解压页和压缩页同时驱逐。不管走哪条路径,最后都调用了函数buf_LRU_free_page来执行驱逐操作,这个函数由于要处理压缩页解压页各种情况,极其复杂。大致的流程:首先判断是否是脏页,如果是则不驱逐,否则从LRU List中把链表删除,必要的话还从Unzip LRU List移走这个数据页(buf_LRU_block_remove_hashed),接着如果我们选择保留压缩页,则需要重新创建一个压缩页控制体,插入LRU List中,如果是脏的压缩页还要插入到Flush List中,最后才把删除的数据页插入到Free List中(buf_LRU_block_free_hashed_page)。

  • 如果在上一步中没有找到空闲的数据页,则需要刷脏了(buf_flush_single_page_from_LRU),由于buf_LRU_get_free_block这个函数是在用户线程中调用的,所以即使要刷脏,这里也是刷一个脏页,防止刷过多的脏页阻塞用户线程。

  • 如果上一步的刷脏因为数据页被其他线程读取而不能刷脏,则重新跳转到上述第二步。进行第二轮迭代,与第一轮迭代的区别是,第一轮迭代在扫描LRU List时,最多只扫描innodb_lru_scan_depth个,而在第二轮迭代开始,扫描整个LRU List。如果很不幸,这一轮还是没有找到空闲的数据页,从三轮迭代开始,在刷脏前等待10ms。

  • 最终找到一个空闲页后,page的state为BUF_BLOCK_READY_FOR_USE。

控制全表扫描不增加cache数据到Buffer Pool

全表扫描对Buffer Pool的影响比较大,即使有old list作用,但是old list默认也占Buffer Pool的3/8。因此,阿里云RDS引入新的语法ENGINE_NO_CACHE(例如:SELECT ENGINE_NO_CACHE count(*) FROM t1)。如果一个SQL语句中带了ENGINE_NO_CACHE这个关键字,则由它读入内存的数据页都放入Quick List中,当这个语句结束时,会删除它独占的数据页。同时引入两个参数。innodb_rds_trx_own_block_max这个参数控制使用Hint的每个事物最多能拥有多少个数据页,如果超过这个数据就开始驱逐自己已有的数据页,防止大事务占用过多的数据页。innodb_rds_quick_lru_limit_per_instance这个参数控制每个Buffer Pool Instance中Quick List的长度,如果超过这个长度,后续的请求都从Quick List中驱逐数据页,进而获取空闲数据页。

删除指定表空间所有的数据页

函数(buf_LRU_remove_pages)提供了三种模式,第一种(BUF_REMOVE_ALL_NO_WRITE),删除Buffer Pool中所有这个类型的数据页(LRU List和Flush List)同时Flush List中的数据页也不写回数据文件,这种适合rename table和5.6表空间传输新特性,因为space_id可能会被复用,所以需要清除内存中的一切,防止后续读取到错误的数据。第二种(BUF_REMOVE_FLUSH_NO_WRITE),仅仅删除Flush List中的数据页同时Flush List中的数据页也不写回数据文件,这种适合drop table,即使LRU List中还有数据页,但由于不会被访问到,所以会随着时间的推移而被驱逐出去。第三种(BUF_REMOVE_FLUSH_WRITE),不删除任何链表中的数据仅仅把Flush List中的脏页都刷回磁盘,这种适合表空间关闭,例如数据库正常关闭的时候调用。这里还有一点值得一提的是,由于对逻辑链表的变动需要加锁且删除指定表空间数据页这个操作是一个大操作,容易造成其他请求被饿死,所以InnoDB做了一个小小的优化,每删除BUF_LRU_DROP_SEARCH_SIZE个数据页(默认为1024)就会释放一下Buffer Pool Instance的mutex,便于其他线程执行。

LRU_Manager_Thread

这是一个系统线程,随着InnoDB启动而启动,作用是定期清理出空闲的数据页(数量为innodb_LRU_scan_depth)并加入到Free List中,防止用户线程去做同步刷脏影响效率。线程每隔一定时间去做BUF_FLUSH_LRU,即首先尝试从LRU中驱逐部分数据页,如果不够则进行刷脏,从Flush List中驱逐(buf_flush_LRU_tail)。线程执行的频率通过以下策略计算:我们设定max_free_len = innodb_LRU_scan_depth * innodb_buf_pool_instances,如果Free List中的数量小于max_free_len的1%,则sleep time为零,表示这个时候空闲页太少了,需要一直执行buf_flush_LRU_tail从而腾出空闲的数据页。如果Free List中的数量介于max_free_len的1%-5%,则sleep time减少50ms(默认为1000ms),如果Free List中的数量介于max_free_len的5%-20%,则sleep time不变,如果Free List中的数量大于max_free_len的20%,则sleep time增加50ms,但是最大值不超过rds_cleaner_max_lru_time。这是一个自适应的算法,保证在大压力下有足够用的空闲数据页(lru_manager_adapt_sleep_time)。

Hazard Pointer

在学术上,Hazard Pointer是一个指针,如果这个指针被一个线程所占有,在它释放之前,其他线程不能对他进行修改,但是在InnoDB里面,概念刚好相反,一个线程可以随时访问Hazard Pointer,但是在访问后,他需要调整指针到一个有效的值,便于其他线程使用。我们用Hazard Pointer来加速逆向的逻辑链表遍历。
先来说一下这个问题的背景,我们知道InnoDB中可能有多个线程同时作用在Flush List上进行刷脏,例如LRU_Manager_Thread和Page_Cleaner_Thread。同时,为了减少锁占用的时间,InnoDB在进行写盘的时候都会把之前占用的锁给释放掉。这两个因素叠加在一起导致同一个刷脏线程刷完一个数据页A,就需要回到Flush List末尾(因为A之前的脏页可能被其他线程给刷走了,之前的脏页可能已经不在Flush list中了),重新扫描新的可刷盘的脏页。另一方面,数据页刷盘是异步操作,在刷盘的过程中,我们会把对应的数据页IO_FIX住,防止其他线程对这个数据页进行操作。我们假设某台机器使用了非常缓慢的机械硬盘,当前Flush List中所有页面都可以被刷盘(buf_flush_ready_for_replace返回true)。我们的某一个刷脏线程拿到队尾最后一个数据页,IO fixed,发送给IO线程,最后再从队尾扫描寻找可刷盘的脏页。在这次扫描中,它发现最后一个数据页(也就是刚刚发送到IO线程中的数据页)状态为IO fixed(磁盘很慢,还没处理完)所以不能刷,跳过,开始刷倒数第二个数据页,同样IO fixed,发送给IO线程,然后再次重新扫描Flush List。它又发现尾部的两个数据页都不能刷新(因为磁盘很慢,可能还没刷完),直到扫描到倒数第三个数据页。所以,存在一种极端的情况,如果磁盘比较缓慢,刷脏算法性能会从O(N)退化成O(N*N)。
要解决这个问题,最本质的方法就是当刷完一个脏页的时候不要每次都从队尾重新扫描。我们可以使用Hazard Pointer来解决,方法如下:遍历找到一个可刷盘的数据页,在锁释放之前,调整Hazard Pointer使之指向Flush List中下一个节点,注意一定要在持有锁的情况下修改。然后释放锁,进行刷盘,刷完盘后,重新获取锁,读取Hazard Pointer并设置下一个节点,然后释放锁,进行刷盘,如此重复。当这个线程在刷盘的时候,另外一个线程需要刷盘,也是通过Hazard Pointer来获取可靠的节点,并重置下一个有效的节点。通过这种机制,保证每次读到的Hazard Pointer是一个有效的Flush List节点,即使磁盘再慢,刷脏算法效率依然是O(N)。
这个解法同样可以用到LRU List驱逐算法上,提高驱逐的效率。相应的Patch是在MySQL 5.7上首次提出的,阿里云RDS把其Port到了我们5.6的版本上,保证在大并发情况下刷脏算法的效率。

Page_Cleaner_Thread

这也是一个InnoDB的后台线程,主要负责Flush List的刷脏,避免用户线程同步刷脏页。与LRU_Manager_Thread线程相似,其也是每隔一定时间去刷一次脏页。其sleep time也是自适应的(page_cleaner_adapt_sleep_time),主要由三个因素影响:当前的lsn,Flush list中的oldest_modification以及当前的同步刷脏点(log_sys->max_modified_age_sync,有redo log的大小和数量决定)。简单的来说,lsn - oldest_modification的差值与同步刷脏点差距越大,sleep time就越长,反之sleep time越短。此外,可以通过rds_page_cleaner_adaptive_sleep变量关闭自适应sleep time,这是sleep time固定为1秒。
与LRU_Manager_Thread每次固定执行清理innodb_LRU_scan_depth个数据页不同,Page_Cleaner_Thread每次执行刷的脏页数量也是自适应的,计算过程有点复杂(page_cleaner_flush_pages_if_needed)。其依赖当前系统中脏页的比率,日志产生的速度以及几个参数。innodb_io_capacity和innodb_max_io_capacity控制每秒刷脏页的数量,前者可以理解为一个soft limit,后者则为hard limit。innodb_max_dirty_pages_pct_lwm和innodb_max_dirty_pages_pct_lwm控制脏页比率,即InnoDB什么脏页到达多少才算多了,需要加快刷脏频率了。innodb_adaptive_flushing_lwm控制需要刷新到哪个lsn。innodb_flushing_avg_loops控制系统的反应效率,如果这个变量配置的比较大,则系统刷脏速度反应比较迟钝,表现为系统中来了很多脏页,但是刷脏依然很慢,如果这个变量配置很小,当系统中来了很多脏页后,刷脏速度在很短的时间内就可以提升上去。这个变量是为了让系统运行更加平稳,起到削峰填谷的作用。相关函数,af_get_pct_for_dirtyaf_get_pct_for_lsn

预读和预写

如果一个数据页被读入Buffer Pool,其周围的数据页也有很大的概率被读入内存,与其分开多次读取,还不如一次都读入内存,从而减少磁盘寻道时间。在官方的InnoDB中,预读分两种,随机预读和线性预读。

随机预读:

这种预读发生在一个数据页成功读入Buffer Pool的时候(buf_read_ahead_random)。在一个Extent范围(1M,如果数据页大小为16KB,则为连续的64个数据页)内,如果热点数据页大于一定数量,就把整个Extend的其他所有数据页(依据page_no从低到高遍历读入)读入Buffer Pool。这里有两个问题,首先数量是多少,默认情况下,是13个数据页。接着,怎么样的页面算是热点数据页,阅读代码发现,只有在young list前1/4的数据页才算是热点数据页。读取数据时候,使用了异步IO,结合使用OS_AIO_SIMULATED_WAKE_LATERos_aio_simulated_wake_handler_threads便于IO合并。随机预读可以通过参数innodb_random_read_ahead来控制开关。此外,buf_page_get_gen函数的mode参数不影响随机预读。

线性预读:

这中预读只发生在一个边界的数据页(Extend中第一个数据页或者最后一个数据页)上(buf_read_ahead_linear)。在一个Extend范围内,如果大于一定数量(通过参数innodb_read_ahead_threshold控制,默认为56)的数据页是被顺序访问(通过判断数据页access time是否为升序或者逆序来确定)的,则把下一个Extend的所有数据页都读入Buffer Pool。读取的时候依然采用异步IO和IO合并策略。线性预读触发的条件比较苛刻,触发操作的是边界数据页同时要求其他数据页严格按照顺序访问,主要是为了解决全表扫描时的性能问题。线性预读可以通过参数innodb_read_ahead_threshold来控制开关。此外,当buf_page_get_gen函数的mode为BUF_PEEK_IF_IN_POOL时,不触发线性预读。
InnoDB中除了有预读功能,在刷脏页的时候,也能进行预写(buf_flush_try_neighbors)。当一个数据页需要被写入磁盘的时候,查找其前面或者后面邻居数据页是否也是脏页且可以被刷盘(没有被IOFix且在old list中),如果可以的话,一起刷入磁盘,减少磁盘寻道时间。预写功能可以通过innodb_flush_neighbors参数来控制。不过在现在的SSD磁盘下,这个功能可以关闭。

Double Write Buffer(dblwr)

服务器突然断电,这个时候如果数据页被写坏了(例如数据页中的目录信息被损坏),由于InnoDB的redolog日志不是完全的物理日志,有部分是逻辑日志,因此即使奔溃恢复也无法恢复到一致的状态,只能依靠Double Write Buffer先恢复完整的数据页。Double Write Buffer主要是解决数据页半写的问题,如果文件系统能保证写数据页是一个原子操作,那么可以把这个功能关闭,这个时候每个写请求直接写到对应的表空间中。
Double Write Buffer大小默认为2M,即128个数据页。其中分为两部分,一部分留给batch write,另一部分是single page write。前者主要提供给批量刷脏的操作,后者留给用户线程发起的单页刷脏操作。batch write的大小可以由参数innodb_doublewrite_batch_size控制,例如假设innodb_doublewrite_batch_size配置为120,则剩下8个数据页留给single page write。
假设我们要进行批量刷脏操作,我们会首先写到内存中的Double Write Buffer(也是2M,在系统初始化中分配,不使用Buffer Chunks空间),如果dblwr写满了,一次将其中的数据刷盘到系统表空间指定位置,注意这里是同步IO操作,在确保写入成功后,然后使用异步IO把各个数据页写回自己的表空间,由于是异步操作,所有请求下发后,函数就返回,表示写成功了(buf_dblwr_add_to_batch)。不过这个时候后续的写请求依然会阻塞,知道这些异步操作都成功,才清空系统表空间上的内容,后续请求才能被继续执行。这样做的目的就是,如果在异步写回数据页的时候,系统断电,发生了数据页半写,这个时候由于系统表空间中的数据页是完整的,只要从中拷贝过来就行(buf_dblwr_init_or_load_pages)。
异步IO请求完成后,会检查数据页的完整性以及完成change buffer相关操作,接着IO helper线程会调用buf_flush_write_complete函数,把数据页从Flush List删除,如果发现batch write中所有的数据页都写成了,则释放dblwr的空间。

Buddy伙伴系统

与内存分配管理算法类似,InnoDB中的伙伴系统也是用来管理不规则大小内存分配的,主要用在压缩页的数据上。前文提到过,InnoDB中的压缩页可以有16K,8K,4K,2K,1K这五种大小,压缩页大小的单位是表,也就是说系统中可能存在很多压缩页大小不同的表。使用伙伴体统来分配和回收,能提高系统的效率。
申请空间的函数是buf_buddy_alloc,其首先在zip free链表中查看指定大小的块是否还存在,如果不存在则从更大的链表中分配,这回导致一些列的分裂操作。例如需要一块4K大小的内存,则先从4K链表中查找,如果有则直接返回,没有则从8K链表中查找,如果8K中还有空闲的,则把8K分成两部分,低地址的4K提供给用户,高地址的4K插入到4K的链表中,便与后续使用。如果8K中也没有空闲的了,就从16K中分配,16K首先分裂成2个8K,高地址的插入到8K链表中,低地址的8K继续分裂成2个4K,低地址的4K返回给用户,高地址的4K插入到4K的链表中。假设16K的链表中也没有空闲的了,则调用buf_LRU_get_free_block获取新的数据页,然后把这个数据页加入到zip hash中,同时设置state状态为BUF_BLOCK_MEMORY,表示这个数据页存储了压缩页的数据。
释放空间的函数是buf_buddy_free,相比于分配空间的函数,有点复杂。假设释放一个4K大小的数据块,其先把4K放回4K对应的链表,接着会查看其伙伴(释放块是低地址,则伙伴是高地址,释放块是高地址,则伙伴是低地址)是否也被释放了,如果也被释放了则合并成8K的数据块,然后继续寻找这个8K数据块的伙伴,试图合并成16K的数据块。如果发现伙伴没有被释放,函数并不会直接退出而是把这个伙伴给挪走(buf_buddy_relocate),例如8K数据块的伙伴没有被释放,系统会查看8K的链表,如果有空闲的8K块,则把这个伙伴挪到这个空闲的8K上,这样就能合并成16K的数据块了,如果没有,函数才放弃合并并返回。通过这种relocate操作,内存碎片会比较少,但是涉及到内存拷贝,效率会比较低。

Buffer Pool预热

这个也是官方5.6提供的新功能,可以把当前Buffer Pool中的数据页按照space_id和page_no dump到外部文件,当数据库重启的时候,Buffer Pool就可以直接恢复到关闭前的状态。

Buffer Pool Dump:

遍历所有Buffer Pool Instance的LRU List,对于其中的每个数据页,按照space_id和page_no组成一个64位的数字,写到外部文件中即可(buf_dump)。

Buffer Pool Load:

读取指定的外部文件,把所有的数据读入内存后,使用归并排序对数据排序,以64个数据页为单位进行IO合并,然后发起一次真正的读取操作。排序的作用就是便于IO合并(buf_load)。

总结

InnoDB的Buffer Pool可以认为很简单,就是LRU List和Flush List,但是InnoDB对其做了很多性能上的优化,例如减少加锁范围,page hash加速查找等,导致具体的实现细节相对比较复杂,尤其是引入压缩页这个特性后,有些核心代码变得晦涩难懂,需要读者细细琢磨。


AliSQL · 特性介绍 · 动态加字段

$
0
0

背景

加字段作为业务需求变更中最常见的需求,InnoDB引擎表的加字段功能一直以来被运维人员所诟病,
虽然支持了online方式,但随着表空间越来越大,copy整张表的代价也越来越大。
AliSQL版本在InnoDB的compact记录格式的基础上,设计了新的记录格式comfort,支持动态加字段。

使用方法

使用的实例如下:

CREATE TABLE test(
id int primary key,
name varchar(100),
key(name)
)ENGINE=InnoDB  ROW_FORMAT=comfort;

ALTER TABLE test ADD col1 INT;

这里没有增加新的语法,只是增加了新的InnoDB的记录格式,alter语句保持一致。
可以通过SHOW CREATE TABLE或者查询information_schema.tables查看ROW_FORMAT。

mysql> show create table test\G;
*************************** 1. row ***************************
       Table: test
Create Table: CREATE TABLE `test` (
  `id` int(11) NOT NULL,
  `name` varchar(100) DEFAULT NULL,
  `col1` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=COMFORT
1 row in set (0.00 sec)

实现方法

AliSQL设计了一种新的记录格式,命名为comfort,其格式从compact演化而来:

Compact行记录的格式:

compact.png

  • 变长字段长度列表:如果列的长度小于255字节,用1字节表示;如果大于255个字节,用2字节表示。
  • NULL标志位:表明该行数据是否有NULL值。占一个字节。
  • 记录头信息:固定占用5字节,每位的含义见下表:
名称大小(bit)描述
()1未知
()1未知
delete_flag1该行是否已被删除
min_rec_flag1为1,如果该记录是预先被定义为最小的记录
n_owned4该记录拥有的记录数
heap_no13索引堆中该记录的排序记录
record_type3记录类型,000表示普通,001表示B+树节点指针,010表示infimum,011表示supermum,1xx表示保留
next_record16页中下一条记录的相对位置

新的Comfort记录格式如下:

[Lens | N_nulls | N_fields | Extra_bytes | columns...]

其中:
1. Extra_bytes中info_bits占用一个bit来标识comfort记录,即记录头中未使用的2个bit中的其中一个。
2. 新增N_fields占用1或者2个Bytes来标识当前记录的column数量:
当记录数小于128个时,占用1个Bytes
当大于等于128时,使用2个Bytes。

实现逻辑

假设变更的case如下:

CREATE TABLE `test` (
  `id` int(11) NOT NULL,
  `name` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=COMFORT;

alter table test add col1 int;

1. alter变更

1. 变更数据字典SYS_TABLES中的n_cols字段,即更新column数量
InnoDB的变更语句如下:

     trx->op_info = "Updating column in SYS_TABLES";
     /* N_COLS include compact format bit.*/
     error = que_eval_sql(
             info,
             "PROCEDURE UPDATE_SYS_TABLES_PROC () IS\n""BEGIN\n""UPDATE SYS_TABLES SET N_COLS=N_COLS+1\n""WHERE ID=:table_id;\n""END;\n",
             FALSE, trx);

2.变更数据字典SYS_COLUMNS,新增一条记录,即新增的column
InnoDB的变更语句如下:

       trx->op_info = "inserting column in SYS_COLUMNS";
       error = que_eval_sql(
               info,
               "PROCEDURE INSERT_SYS_COLUMNS_PROC () IS\n""BEGIN\n""INSERT INTO SYS_COLUMNS VALUES\n""(:table_id, :pos, :name, :mtype, :prtype, :len, :prec);\n""END;\n",
               FALSE, trx);

3. 变更dictionary cache中的dict_table_t对象
新的column需要追加到dict_table_t定义的column数组中,

变更前:
table->columns:
(id, name, row_id, trx_id, undo_ptr)

变更后:
table->columns:
(id, name, col1, row_id, trx_id, undo_ptr)

其代码如下:

      /* The new column will be added into after user_def cols,
      before SYS_COLS(ROW_ID, TRX_ID, ROLL_PTR) in dict_table_t */
      for (ulint i= 0; i < n_cols; i++) {
              col = (dict_col_t*)save_cols + i;
              if (i == n_cols - DATA_N_SYS_COLS) {
                      dict_mem_table_add_col(user_table, user_table->heap,
                                      field->field_name,
                                      mtype, prtype, len);
              }
              dict_mem_table_add_col(user_table, user_table->heap,
                                      col_name,
                                      col->mtype, col->prtype, col->len);
              new_col = dict_table_get_nth_col(user_table, user_table->n_def - 1);
              dict_col_copy_ord_prefix(new_col, col);
      }

4. 变更Dictionary Cache中的dict_index_t对象(Cluster index)

变更前:
Primary key的field数组如下:
(id, trx_id, undo_ptr, name)

变更后:
Primary key的field数组如下:
(id, trx_id, undo_ptr, name, col1)

其代码如下:

       /*The new column will added into after last field in dict_index_t */
       for (ulint i = 0; i < n_fields; i++) {
               dfield = (dict_field_t*)(save_fields) + i;
               if (dfield->col->ind < n_cols - DATA_N_SYS_COLS) {
                       col = dict_table_get_nth_col(user_table, dfield->col->ind);
               } else {
                       col = dict_table_get_nth_col(user_table, dfield->col->ind + 1);
               }
               dict_index_add_col(clust_index, user_table, col, dfield->prefix_len);
       }
       col = dict_table_get_nth_col(user_table, n_cols - DATA_N_SYS_COLS);

5. 变更Dictionary Cache中的dict_index_t对象(Secondary index)

变更前:
secondary index的field数组:(name, id)

变更后:
secondary index的field数组:(name, id)

在变更前后,二级索引所对应的fields没有发生变化,fields所对应的column的位置也没有变更,只是因为dict_table_t对象的columns对象重建了,所以需要变更一下field做引用的culumn,这里需要reload一下即可。

对比Online和Dynamic方式

InnoDB原生的Online方式的步骤大致是:
1. 持有exclusive MDL lock,
2. 根据变更后的表结构新建临时表,
3. 新建log表,记录原表的变更
4. MDL降级为shared 锁,原表允许DML,
5. copy数据到新的临时表,并持续copy log表中的记录
6. MDL升级为exclusive
7. apply完log表中所有的记录,并rename表
8. 删除老表,完成变更

InnoDB新的Dynamic方式的步骤大致是:
1. 持有exclusive MDL lock,
2. 降级为shared的锁,允许DML
3. 升级为exclusive锁
4. 变更数据字典(SYS_TABLES, SYS_COLUMNS)
5. 变更数据字典缓存(dict_table_t, dict_index_t)
6. 释放MDL锁

测试情况:

Compact格式的表加字段,共计20W多条记录的情况下,耗时25.98s。
y.png

Comfort格式的表加字段,共计20W多条记录的情况下,耗时0.01s。
x.png

总结

动态加字段能够在不copy记录的情况下,秒级完成结构的变更,大大方便了运维DBA人员的日常变更,这个功能patch已经开源在AliSQL版本。
如果有兴趣,可以关注AliSQL的开源项目:https://github.com/alibaba/AliSQL

PgSQL · 特性分析 · 数据库崩溃恢复(上)

$
0
0

背景

为了合并I/O提高性能,PostgreSQL数据库引入了共享缓冲区,当数据库非正常关闭,比如服务器断电时,共享缓冲区即内存中的数据就会丢失,这个时候数据库操作系统重启时就需要从非正常状态中恢复过来,继续提供服务。本文将具体分析在这种情况下,PostgreSQL数据库如何从崩溃状态中恢复。

上期月报PgSQL · 特性分析 · checkpoint机制浅析中介绍了PostgreSQL中的checkpoint机制。其中提到,当PostgreSQL数据库崩溃恢复时,会以最近的checkpoint为基础,不断应用这之后的XLOG日志。为了更好地理解PostgreSQL数据库从崩溃中恢复的过程,我们需要弄清楚以下几个问题:

  • 数据库操作系统如何识别到自己是非正常状态(崩溃状态)
  • 数据库如何找到合适的checkpoint作为基础
  • 为什么应用XLOG日志可以恢复数据库数据
  • 数据库如何应用XLOG日志

数据库状态

在PostgreSQL中,把数据库分为以下几种状态:

typedef enum DBState
{
	DB_STARTUP = 0,/*数据库启动*/
	DB_SHUTDOWNED,/*数据库正常关闭*/
	DB_SHUTDOWNED_IN_RECOVERY,/*数据库在恢复时关闭*/
	DB_SHUTDOWNING,/*数据库启动到正常关闭过程中崩溃*/
	DB_IN_CRASH_RECOVERY,/*数据库在恢复过程中崩溃*/
	DB_IN_ARCHIVE_RECOVERY,/*数据库处于归档恢复*/
	DB_IN_PRODUCTION/*数据库处于正常工作状态,等待接受事务处理*/
} DBState;

PostgreSQL的数据库状态被存储在pg_control文件中,可以执行pg_controldata命令,查看当前的数据库状态,返回结果如下:

pg_control version number:            942
Catalog version number:               201409291
Database system identifier:           6403125794625722170
Database cluster state:               shut down
...

其中 Database cluster state: shut down指明当前数据库的状态为DB_SHUTDOWNED,即正常关闭状态。

pg_control文件由对应的结构体ControlFileData存储,ControlFileData数据结构如下:

 typedef struct ControlFileData
{
	uint64		system_identifier; /*唯一系统标识符——保证控制文件和产生XLOG文件的数据库一致*/
	uint32		pg_control_version; /* 标识pg_control的版本*/
	uint32		catalog_version_no; /*标识catalog的版本 */
	DBState		state;			/*最后一次操作后的数据库状态 */
	pg_time_t	time;			/*pg_control最近一次更新的时间时*/
	...
	pg_crc32	crc;
} ControlFileData;

每次PostgreSQL数据库启动时,会读取pg_control文件获取最后一次操作后的数据库状态,如果为非正常关闭状态(DB_SHUTDOWNED),则会执行崩溃恢复逻辑。

checkpoint相关结构

ControlFileData结构

当数据库意识到自己处于崩溃状态后,会去选择一个合适的checkpoint作为基础,不断应用在这之后的XLOG日志。在PostgreSQL中,最近一次检查点的信息会被存储在pg_control文件中,pg_control由对应的结构体ControlFileData存储,ControlFileData数据结构如下:

 typedef struct ControlFileData
{
        ...
	XLogRecPtr	checkPoint;		/*指向最近一次的检查点位置*/
	XLogRecPtr	prevCheckPoint;  /*指向最近一次检查点的前一次检查点的位置*/
	CheckPoint	checkPointCopy; /*最近一次检查点控制信息的副本*/
	XLogRecPtr	minRecoveryPoint; /*归档恢复时必须恢复到的最小LSN*/
	XLogRecPtr	backupStartPoint; /*在线备份时进行的检查点开始LSN*/
	XLogRecPtr	backupEndPoint; /*在线备份时进行的检查点结束LSN*/
        bool		backupEndRequired; /* 用于判断是否基于正确的在线备份集恢复*/
        TimeLineID	minRecoveryPointTLI; /* 必须恢复到的最小时间线 */
	...
	pg_crc32	crc;
} ControlFileData;

在数据库崩溃恢复过程中,一般会选取最近一次的检查点作为恢复的基础,但是因为一个检查点的时间比较长,所以有可能数据库系统在检查点做完之前崩溃,这样磁盘上的检查点可能是不完全的,所以PostgreSQL数据库会多存储一个检查点的位置,即prevCheckPoint。

在数据库崩溃恢复过程中,PostgreSQL规定了三个在启动之前必须恢复到的最小位点:

  • minRecoveryPoint
    • 数据库在归档恢复过程中,minRecoveryPoint被更新为最新被刷新到磁盘的LSN。每次数据库启动时必须已经replay该位置的XLOG日志记录。
  • backupStartPoint
    • 数据库在线备份开始时,会调用pg_start_backup函数执行一次checkpoint,并生成backup_label文件。当使用在线备份集进行恢复时,backupStartPoint就是上述checkpoint记录对应的LSN,当达到了该LSN,该值置为0,在置为0之前,数据库不能启动。该值被记录在backup_label文件中如下,直到在线备份结束,pg_stop_backup将该文件删除。这样就保证了在备份过程中,数据库崩溃了,可以默认从备份开始时的日志检查点开始恢复。

      ``` START WAL LOCATION: 0/6000020 (file 000000040000000000000006) CHECKPOINT LOCATION: 0/6000020 BACKUP METHOD: pg_start_backup BACKUP FROM: master START TIME: 2017-05-15 10:18:55 HKT LABEL: zhuodao
      ```
      
  • backupEndPoint
    • 当数据库从一个备库做的在线备份集进行恢复时,backupEndPoint表示备份结束的LSN,当达到该LSN,该值置为0,在置为0之前,数据库不能启动。

recovery.conf文件

在恢复过程中,用户可以通过使用recovery.conf文件来指定恢复的各个参数,如下:

  • 归档恢复设置
    • restore_command:用于获取一个已归档段的XLOG日志文件的命令
    • archive_cleanup_command:清除不在需要的XLOG日志文件的命令
    • recovery_end_command:归档恢复结束后执行的命令
  • 恢复目标设置(默认情况下,数据库将会一直恢复到 WAL 日志的末尾)
    • recovery_target = ’immediate’:在从一个在线备 份中恢复时,这意味着备份结束的那个点
    • recovery_target_name (string):这个参数指定(pg_create_restore_point()所创建)的已命名的恢复点,将恢复到该恢复点
    • recovery_target_time (timestamp):这个参数指定恢复到的时间戳
    • recovery_target_xid (string):这个参数指定恢复到的事务 ID
    • recovery_target_inclusive (boolean):指定是否在指定的恢复目标之后停止(true),或者在恢复目标之前停止 (false);适用于recovery_target_time或者recovery_target_xid被指定的情况;这个设置分别控制事务是否有准确的目标提交时间或 ID 是否将被包括在该恢复中;默认值为 true
    • recovery_target_timeline (string):指定恢复到一个特定的时间线
    • recovery_target_action (enum):指定在达到恢复目标时服务器应该立刻采取的动作,包括pause(暂停)、promote(接受连接)、shutdown(停止服务器),其中pause为默认动作
  • 备库参数设置
    • standby_mode(boolean):为on表示作为一个备库,否则不为备库
    • primary_conninfo (string):指定备库连接主库的连接字符串
    • primary_slot_name (string):通过流复制指定主库的一个复制槽来复制主库数据,如果没有设置primary_conninfo,则此参数无效
    • trigger_file (string):指定一个触发器文件,该文件存在可以结束备库的恢复,即升级备库为一个独立的主库
    • recovery_min_apply_delay (integer):这个参数允许将恢复延迟一段固定的时间,如果没有指定单位则以毫秒为单位。

如果recovery.conf中同时指定了recoveryTargetXid、recoveryTargetName、recoveryTargetTime时,PostgreSQL会按照RECOVERY_TARGET_XID> RECOVERY_TARGET_NAME > RECOVERY_TARGET_TIME的优先级来获取最终的目标恢复位点。

如果在recovery.conf指定recovery_targetTimeLine为latest,则可以基于当前TimeLineID为起点寻找最新时间线:

  • 寻找当前TimeLineID的时间线历史文件“XXX.history”,如果存在则继续寻找,否则错误退出
  • TimeLineID是线性增长的,将当前TimeLineID自增1寻找是否存在时间线历史文件,直到不存在对应的时间线历史文件为止,即可找到最新的时间线。

XLOG日志结构

XLOG日志中详细地记录了服务进程对数据库的操作过程。之前的月报PgSQL · 特性分析 · Write-Ahead Logging机制浅析介绍过PostgreSQL WAL机制的实现,下面将具体介绍XLOG日志的组织结构。

概括起来,XLOG日志分为多个XLOG逻辑日志文件,每个逻辑日志文件包含多个XLOG段文件,每个XLOG段文件包含多个XLOG日志页:

  • 每个XLOG逻辑日志文件都有一个ID
  • 实际XLOG被分为pg_xlog目录下多个大小为16MB的段文件
    • 文件名由时间线TimeLineID(8位16进制)、逻辑日志文件号(8位16进制)和段文件ID(8位16进制)组成
  • 每个段文件分为多个8KB的页(块)
    • 每个页包含一个头部,头部信息之后才是真正的XLOG日志记录

其中,值得注意的是,每个XLOG段文件大小可以在编译时使用–with-wal-segsize参数来指定,每页的大小可以在编译的时使用–with-wal-blocksize参数来指定,接下来主要介绍XLOG日志每页的组织形式。

XLOG日志页的组织形式

在PostgreSQL中,XLOG日志页可以分为以下几部分:

组成部分具体含义
PageHeaderDataXLOG日志页面头部信息
XLogRecordXLog日志记录的头部信息
Data of RMGR资源管理器的数据,长度xl_len
Backup Block 0备份数据块头部BkpBlock + 块大小的备份数据
Backup Block 1备份数据块头部BkpBlock + 块大小的备份数据
Backup Block 2备份数据块头部BkpBlock + 块大小的备份数据
Backup Block 3备份数据块头部BkpBlock + 块大小的备份数据

XLOG日志页头部信息

每个XLOG日志页分为页面头部信息和日志记录,其头部信息XLogPageHeaderData结构如下:

typedef struct XLogPageHeaderData
{
	uint16		xlp_magic;		/* 校验位,用于识别不同的XLOG版本 */
	uint16		xlp_info;		/* flag bits, see below */
	TimeLineID	xlp_tli;		/* 页面第一条记录的时间线 */
	XLogRecPtr	xlp_pageaddr;	/* XLOG页面的首地址 */
	uint32		xlp_rem_len;	/* 前XLOG页面最后一条记录剩余的长度 */
} XLogPageHeaderData;

其中,xlp_info是标志位:

  • 0x0001表示该页面包含一个跨页面的记录(上个页面的最后一条记录)
  • 0x0002表示该页面为段文件的首个页面,头部是一个长头部
  • 0x0004表示该页面备份数据块是可选的

如果当前的页面没有足够的空间来存储一个XLOG日志记录,系统允许将剩余的数据存储到下一个页面,但是XLog日志记录的头部信息,即后文中的XLogRecord是不允许分开存储到两个不同的页面的。

如果该页面为段文件的首个页面,除了上面的标准页面头部信息外,还增加一个长头部用来更精确地定位文件,即XLogLongPageHeaderData:

typedef struct XLogLongPageHeaderData
{
	XLogPageHeaderData std;		/* 标准页面头部信息 */
	uint64		xlp_sysid;		/* pg_control 中的系统标识符*/
	uint32		xlp_seg_size;	/* 段的尺寸 */
	uint32		xlp_xlog_blcksz;	/* 页(块)的尺寸*/
} XLogLongPageHeaderData;

#### XLOG日志记录的头部信息

每个XLOG日志页面头部之后才是真正的XLOG日志记录,XLogRecord记录了XLOG的相关数据信息,具体结构如下:

typedef struct XLogRecord
{
	uint32		xl_tot_len;		/* 整条记录的总长度*/
	TransactionId xl_xid;		/* 事务ID */
	XLogRecPtr	xl_prev;		/* 上条XLOG日志记录的位置(LSN) */
	uint8		xl_info;		/* flag bits, see below */
	RmgrId		xl_rmid;		/* 资源管理器ID */
	/* 2 bytes of padding here, initialize to zero */
	pg_crc32c	xl_crc;			/* 本记录的CRC校验码 */
	/* XLogRecordBlockHeaders and XLogRecordDataHeader follow, no padding */
} XLogRecord;

其中,xl_rmid表示资源管理器ID,在PostgreSQL中,资源管理器根据资源种类,可以分为17类,其分别的ID按照以下顺序分别为0-16:

PG_RMGR(RM_XLOG_ID, "XLOG", xlog_redo, xlog_desc, NULL, NULL)
PG_RMGR(RM_XACT_ID, "Transaction", xact_redo, xact_desc, NULL, NULL)
PG_RMGR(RM_SMGR_ID, "Storage", smgr_redo, smgr_desc, NULL, NULL)
PG_RMGR(RM_CLOG_ID, "CLOG", clog_redo, clog_desc, NULL, NULL)
PG_RMGR(RM_DBASE_ID, "Database", dbase_redo, dbase_desc, NULL, NULL)
PG_RMGR(RM_TBLSPC_ID, "Tablespace", tblspc_redo, tblspc_desc, NULL, NULL)
PG_RMGR(RM_MULTIXACT_ID, "MultiXact", multixact_redo, multixact_desc, NULL, NULL)
PG_RMGR(RM_RELMAP_ID, "RelMap", relmap_redo, relmap_desc, NULL, NULL)
PG_RMGR(RM_STANDBY_ID, "Standby", standby_redo, standby_desc, NULL, NULL)
PG_RMGR(RM_HEAP2_ID, "Heap2", heap2_redo, heap2_desc, NULL, NULL)
PG_RMGR(RM_HEAP_ID, "Heap", heap_redo, heap_desc, NULL, NULL)
PG_RMGR(RM_BTREE_ID, "Btree", btree_redo, btree_desc, NULL, NULL)
PG_RMGR(RM_HASH_ID, "Hash", hash_redo, hash_desc, NULL, NULL)
PG_RMGR(RM_GIN_ID, "Gin", gin_redo, gin_desc, gin_xlog_startup, gin_xlog_cleanup)
PG_RMGR(RM_GIST_ID, "Gist", gist_redo, gist_desc, gist_xlog_startup, gist_xlog_cleanup)
PG_RMGR(RM_SEQ_ID, "Sequence", seq_redo, seq_desc, NULL, NULL)
PG_RMGR(RM_SPGIST_ID, "SPGist", spg_redo, spg_desc, spg_xlog_startup, spg_xlog_cleanup)

其中,上述引用代码中PG_RMGR函数的参数依次为:

参数名称具体含义
symname资源管理器ID
name资源名称
redoredo恢复函数
desc描述函数
startup启动函数
cleanup清理函数

在PostgreSQL中,用xl_rmid和xl_info高4位来唯一地标示该XLOG日志记录对应的数据库操作,例如事务资源管理器(RM_XACT_ID),对应XLogRecord中xl_info字段高4位:

#define XLOG_XACT_COMMIT			0x00
#define XLOG_XACT_PREPARE			0x10 
#define XLOG_XACT_ABORT				0x20
#define XLOG_XACT_COMMIT_PREPARED	0x30
#define XLOG_XACT_ABORT_PREPARED	0x40
#define XLOG_XACT_ASSIGNMENT		0x50
#define XLOG_XACT_COMMIT_COMPACT	0x60

例如元组管理器(RM_HEAP_ID),对应xl_info的高4位:

#define XLOG_HEAP_INSERT		0x00
#define XLOG_HEAP_DELETE		0x10
#define XLOG_HEAP_UPDATE		0x20
/* 0x030 is free, was XLOG_HEAP_MOVE */
#define XLOG_HEAP_HOT_UPDATE	0x40
#define XLOG_HEAP_NEWPAGE		0x50
#define XLOG_HEAP_LOCK			0x60
#define XLOG_HEAP_INPLACE		0x70

xl_info字段是个xl_info低4位表示当前XLOG记录数据块备份的情况:

#define XLR_BKP_BLOCK_MASK		0x0F	/* all info bits used for bkp blocks */
#define XLR_MAX_BKP_BLOCKS		4
#define XLR_BKP_BLOCK(iblk)		(0x08 >> (iblk))		/* iblk in 0..3 */

当日志记录涉及到的缓冲区Buffer从上个checkpoint后第一次被修改,则将该Buffer备份附加到XLOG日志的备份块iblk中,对应修改xl_info的XLR_BKP_BLOCK(iblk)位。这是为了保证每个写入到磁盘的数据都是完整的页,当写入某个整页的过程中出现崩溃,即写入的页面不是完整的,则可以从XLOG日志中知直接将备份块恢复过来。

除此之外,XLogRecord的xl_crc记录XLOG日志记录的CRC校验,保证写入到磁盘的XLOG记录都是完整的,如果应用不完整的日志记录,PostgreSQL会报错。

XLOG日志记录的资源管理器数据

XLOG日志记录的资源管理器数据由一系列XLogRecData结构体链表组成,之所以要用XLogRecData链,是因为在所要处理的日志记录实体数据在内存空间可能不是连续存储的,而且数据可能分布在多个缓冲区内,需要用XlogRecData链表将它们组织起来。XlogRecData数据结构如下:

typedef struct XLogRecData
{
	char	   *data;	/*资源管理器包含数据的开始*/
	uint32		len;		/*资源管理器包含的数据大小*/
	Buffer		buffer;	/*如果有buffer指明第几个缓冲区*/
	bool		buffer_std;	/*是否含有标准的pd_lower/pd_upper结构*/
	struct XLogRecData *next;	/*指向下一个结构体*/
} XLogRecData;

其中,buffer_std该值为true,则容许XLOG释放备份页的空闲空间,空闲空间由pd_lower和pd_upper限定:

  • pd_lower表示页面起始位置与未分配空间开头的字节偏移
  • pd_upper表示页面末尾位置与未分配空间末尾的字节偏移

XLogRecData中data保存每条XLOG日志记录中的数据信息,以INSERT、UPDATE、DELETE为例,XLogRecData中data的大体内容如下(该图引自《Internals Of PostgreSQL Wal》):

screenshot.png

可以看出,根据XLogRecData的信息,我们很容易恢复出对应的数据。

备份数据块

备份数据块包含一个头部信息BkpBlock和一块大小的备份数据,其中BkpBlock结构如下:

typedef struct BkpBlock
{
        RelFileNode  node;	/* 用于唯一标示该块所属的关系表,包括表空间OID,数据库OID,关系表OID等*/
	ForkNumber	 fork;		/*一个关系表在存储上可能由多个分支组成,每个分支以文件单独存储,RelFileNode对应关系表的分支号*/
	BlockNumber  block;		/*对应块的块号*/
	uint16		hole_offset;	/*空洞偏移量*/
	uint16		hole_length;	/* 空洞长度*/
} BkpBlock;

如果需要备份的块存在空洞,则备份的时候只记录这个空洞的偏移量和长度,但没有实际备份它,从而提高备份效率。

备份数据块头部后紧跟一个块大小的备份数据,该块可以在数据库崩溃恢复时直接恢复。

Redo恢复的具体步骤

每次postmaster进程启动时,都会调用StartupXLOG函数对数据库崩溃进行恢复,由于该过程非常繁琐,为了更好的理解,本文把Redo恢复分为三个阶段:
- Redo恢复前
- Redo恢复中
- Redo恢复后

Redo恢复前

该阶段主要是根据数据库当前状态判断是否需要恢复,如果需要则获取恢复的起始位点以及目标恢复时间线(recoveryTargetTLI);如不需要则正常启动系统。该阶段具体操作如下:

  1. 读取控制文件pg_control,根据文件中的信息设置恢复参数
  2. 检查pg_xlog和pg_xlog/archive_status文件夹是否存在
  3. 读取配置文件recovery.conf,根据文件中的信息设置恢复参数
  4. 读出时间线历史记录中的时间线列表expectedTLIs,如果recoveryTargetTLI不在时间线列表expectedTLIs中,则系统报错
  5. 检测是否存在backup_label文件,如果存在,则从备份标记定义的检查点(CHECKPOINT LOCATION)读取检查点的记录到record中
    a. 若record不空,则从record中的检查点记录为恢复起始位置,参数InRecovery参数设置为true
    b. 若record为空,则系统报错
  6. 如果不存在backup_label文件,读取pg_control文件中的最近一次检查点,并把它的记录读到record中。
    a. 若record不空,则从record中的检查点记录为恢复起始位置
    b. 若record为空,则读取最近一次检查点的前面一次检查点(prevCheckPoint),并把它的记录读到record中
    c. 如果新record不为空,把参数InRecovery参数设置为true,否则系统报错
  7. 把record记录中的值赋给一个检查点结构体变量checkPoint,checkPoint的nextXid和nextOid赋给共享缓冲区中的变量缓冲区ShmemVariableCache的nextXid和nextOid。把checkPoint的时间线ID赋给ThisTimeLineID
  8. 在checkPoint的redo指针和undo指针有效的情况下,把参数InRecovery参数设置为true。
  9. pg_control中数据库状态不是DB_SHUTDOWNED(系统正常关闭)时,把参数InRecovery参数设置为true
  10. pg_control中参数InArchiveRecovery为真,把参数InRecovery设置为true
  11. 当参数InRecovery的值为true时,执行恢复

总结起来,在PostgreSQL中,如果启动时遇到以下情况,需要进行恢复操作:

  • pg_control中的数据库状态不正常(非DB_SHUTDOWNED)
  • pg_control中记录的最新检查点读取不到XLOG日志文件
  • 通过指定recovery.conf文件,指定归档恢复

其中第三种情况是用户通过配置文件recovery.conf手动控制恢复过程。

Redo恢复中

上个阶段主要是做Redo恢复之前的准备工作,确定恢复起始的位置,而本阶段主要是基于上个阶段,进行真正的恢复操作:

  1. 初始化恢复环境,启动各种需要恢复的资源,即调用对应资源管理器的启动函数:

    RmgrTable[rmid].rm_startup();
    
  2. 设置需要Redo的日志记录的起始位置(离上个阶段checkPoint最近的一条日志记录),把起始位置处的日志记录读入record,进入循环,不断地进行redo操作

    a. 如果record不空,从record开始循环执行redo操作,处理完一条需要redo的记录,即调用对应资源管理器的redo操作:

        RmgrTable[record->xl_rmid].rm_redo(EndRecPtr, record);
    

    b. 如果record为空,不需要进行redo操作

  3. 读取下一条记录到record中,不断进行redo操作,直到执行到了我们所要求的时间线的位置,或者已经把所有的日志记录中需要redo的record执行完毕

Redo恢复后

这个阶段主要是对Redo恢复的环境进行清理,并启动需要的辅助进程。

  1. 本次恢复结束之际,确定是否需要再设置一个新的时间线。如果是恢复到某个指定的时间点上而不是全部恢复,则生成一个新的时间线
  2. 更新XlogCtl控制结构体中的recoveryLastRecPtr
  3. 当参数InRecovery的值为true时,执行初始环境的清理工作,调用:

    RmgrTable[rmid].rm_cleanup();
    
  4. 执行CreateCheckPoint,强迫恢复的内容刷到磁盘
  5. 调用PreaaalocXlogFiles为新日志记录重新分配日志段文件,同时释放日志恢复时申请的内存。调用ShutdownRecoveryTransactionEnvironment关闭恢复环境。再次更新控制结构体ControlFile、Xlogctl等。
  6. 开始启动clog、prepared transactions需要的资源或环境等内容,为恢复结束后、系统正常运行做准备工作
  7. startupXlOG结束,系统正常启动

总结

至此,我们分析了PostgreSQL数据库在崩溃时恢复的具体过程,其中具体的Redo恢复过程,实际上是通过资源管理器获取对应的redo函数接口来执行恢复操作,每种资源管理器其处理过程不尽相同,这里我们不再一一介绍,后面的月报我们会去分析各种资源的redo函数具体操作。

MySQL · 答疑解惑 · MySQL 的那些网络超时错误

$
0
0

前言

我们在使用/运维 MySQL 过程中,经常会遇到一些网络相关的错误,比如:

Aborted connection 134328328 to db: 'test' user: 'root' host: '127.0.0.1' (Got timeout reading communication packets)

MySQL 的网络超时相关参数有好几个,这个超时到底是对应哪个参数呢?

在之前的月报中,我们介绍过 MySQL 的 网络通信模块,包括各模块间的关系,数据网络包是如何发送接受的,以及结果集的数据格式,大家可以先回顾下。

这里我们对 mysqld 处理网络包时,遇到的超时异常情况进行分析,希望大家在遇到网络相关的报错时,能更好理解和排查问题。

问题分析

MySQL 是平等网络协议,就是说 client 和 server 之间的网络交互是一来一回的,client 发送完请求后,必须等待 server 响应包回来,才能发下一个请求。
对 mysqld 来说,就是接收网络请求,然后内部处理,将结果集返回给客户端,然后等待下一个请求:

先看下 mysqld server 和网络超时相关的参数有哪些:

  • interactive_timeout
  • wait_timeout
  • net_read_timeout
  • net_write_timeout
  • connect_timeout

在底层实现上,不管是读还是写操作,超时都是通过 poll(&pfd, 1, timeout)做的,参数之间的区别是针对连接的不同状态。

读超时

wait_timeout 是给读请求用的,在 do_command开始就做设置:

my_net_set_read_timeout(net, thd->variables.net_wait_timeout);

这个时候,连接是空闲的,等待用户的请求。

等读完用户的请求包后,连接就变成 active 的,在调用 dispatch_command执行 SQL 前,通过

my_net_set_read_timeout(net, thd->variables.net_read_timeout);

把超时设置回 net_read_timeout,之后在执行 SQL 请求过程中,server 和 client 基本不会有网络交互,所以这个超时基本用不上。

有一个特殊的情况是 LOAD DATA LOCAL FILE命令,server 在执行过程中,需要和 client 再做网络交互。

interactive_timeout 是给交互模式的客户端使用的,比如我们常用的 mysql client 工具,这个是在认证过程中设置的,逻辑如下:

static void
server_mpvio_update_thd(THD *thd, MPVIO_EXT *mpvio)
{
  thd->client_capabilities= mpvio->client_capabilities;
  thd->max_client_packet_length= mpvio->max_client_packet_length;
  if (mpvio->client_capabilities & CLIENT_INTERACTIVE)
    thd->variables.net_wait_timeout= thd->variables.net_interactive_timeout;
  thd->security_ctx->user= mpvio->auth_info.user_name;
  if (thd->client_capabilities & CLIENT_IGNORE_SPACE)
    thd->variables.sql_mode|= MODE_IGNORE_SPACE;
}

如果客户端的能力位上设置了 CLIENT_INTERACTIVE,会用 interactive_timeout的值覆盖 wait_timeout的值。
而一般情况下,我们应用在建立连接时,是不会设置这个能力位的。

写超时
net_write_timeout对应写超时,在连接认证完成后,server 和 client 交互过程中写超时一真是不变的。

认证超时

connect_timeout是给连接认证过程用的,读和写都用这个值,认证完成后,读和写分别设置为 net_read_timeoutnet_write_timeout

总结

可以看到和读相关的超时参数是最多的,也比较容易搞混乱。

  1. 如果是认证过程中超时,不管是读还是,都是 connect_timeout;
  2. 对于读网络超时,一般是 wait_timeout/interactive_timeout,基本不会是 net_read_timeout(特例是业务用到 LOAD DATA LOCAL FILE);
  3. 对于写网络超时,都是 net_write_timeout。

在遇到超时情况下,可以根据这些原则判断对那个参数做调整。

比如下面这种情况:

2017-05-15 19:32:41 47930 [Warning] Aborted connection 6 to db: 'unconnected' user: 'root' host: 'localhost' (Got timeout reading communication packets)

很可能需要调整的 wait_timeout/interactive_timeout。

2017-05-15 20:06:27 5063 [Warning] Aborted connection 12 to db: 'test' user: 'root' host: 'localhost' (Got timeout writing communication packets)

需要调整 net_write_timeout

需要注意的是,MySQL 的关于网络的错误,除了超时以外都认为是 error,没有做进一步的细分,比如可能会看到下面这种日志,有可能是客户端异常退出了,也有可能是网络链路异常。

2017-05-15 19:34:57 47930 [Warning] Aborted connection 8 to db: 'unconnected' user: 'root' host: 'localhost' (Got an error reading communication packets)

2017-05-15 20:07:39 5063 [Warning] Aborted connection 13 to db: 'test' user: 'root' host: 'localhost' (Got an error writing communication packets)

HybridDB · 最佳实践 · HybridDB 数据合并的方法与原理

$
0
0

引言

刚开始使用HybridDB的用户,有个问的比较多的问题:如何快速做数据“合并”(Merge)?所谓“合并”,就是把数据新版本更新到HybridDB中。如果数据已经存在,则将它们替换为新版本;如果不存在,将它们插入数据库中。一般是离线的做这种数据合并,例如每天一次批量把数据更新到HybridDB中。也有客户需要实时的更新,即做到分钟级甚至秒级延迟。这里我们介绍一下HybridDB中数据合并的方法和背后原理。

简单更新过程

无论怎么做数据合并,都是对数据的修改,即Update、Delete、Insert、Copy等操作。我们先要了解一下HybridDB中的数据更新过程。我们以用户发起一次Update操作为例(对列存表单行记录的更新),整个流程如下图所示。

pic

其中的步骤说明如下:

  1. 用户把Update的SQL请求发送到主节点;

  2. 主节点发起分布式事务,并对被Update的表加锁(HybridDB不允许并行的Update同一张表),然后把更新请求分发到对应的子节点。

  3. 子节点通过索引扫描,定位到要更新的数据,并更新数据。对于列存表,更新逻辑其实就是删除旧的数据行,并在表的尾端写入新的数据行。(列存表)被更新的数据页面会写入内存缓存区,对应的表文件长度的变化(因为尾端写入了数据,所以数据表对应的文件长度增大了)会写入日志(xlog文件)。

  4. 在Update命令结束前,内存中的被更新的数据页面和xlog日志,都要同步到Mirror节点。同步完成后,主节点结束分布式事务,返回用户执行成功的消息。

可以看出,整个过程的链条很长,SQL语句解析、分布式事务、锁,主节点子节点之间的连接建立、子节点与Mirror数据和日志同步等操作,都会耗费CPU或IO资源,同时拖慢整个请求的响应时间。因此,对于HybridDB来说,应该尽量避免单行数据的更新,而是尽量批量的更新数据,也就是尽量做到:

  • 尽量把更新放到一个SQL语句,减少语句解析、节点通信、数据同步等开销;

  • 尽量把更新放到一个事务,避免不必要的事务开销。

简而言之,就是数据的合并和更新,尽量以”成批“的形式进行。下面我们看看,如何批量的做数据更新。

批量Update

假如我们要Update很多独立数据行,怎么才能用一个SQL来实现呢?

我们假设有张表target_table需要做更新(称为目标表),这张表的定义如下。一般目标表都非常大,这里我们往target_table里面插入1千万数据。为了能快速更新,target_table上要有索引。这里我们定义了primary key,会隐含的创建一个唯一值索引(unique index)。

create table target_table(c1 int, c2 int, primary key (c1));

insert into target_table select generate_series(1, 10000000);

为了做批量的Update,需要用到中间表(Stage Table),其实就是为了更新数据临时创建的表。为了更新target_table的数据,可以先把新数据插入到中间表source_table中。然后,把新数据通过COPY命令OSS外部表等方式导入到source_table。这里为简单起见,我们直接插入一些数据。

create table source_table(c1 int, c2 int);

insert into source_table select generate_series(1, 100), generate_series(1,100);

source_table数据准备好后,执行下面的update set … from … where ..语句,即可实现批量的Update。注意,为了最大限度的使用到索引,在执行Update前,要使用set opitimzer=on启用ORCA优化器(如果不启用ORCA优化器,则需要执行set enable_nestloop = on才能使用到索引)。


set optimizer=on;

update target_table set c2 = source_table.c2 from source_table where target_table.c1= source_table.c1;

这种Update的执行计划如下:

=> explain update target_table set c2 = source_table.c2 from source_table where target_table.c1= source_table.c1;
                                                         QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
 Update  (cost=0.00..586.10 rows=25 width=1)
   ->  Result  (cost=0.00..581.02 rows=50 width=26)
         ->  Redistribute Motion 4:4  (slice1; segments: 4)  (cost=0.00..581.02 rows=50 width=22)
               Hash Key: public.target_table.c1
               ->  Assert  (cost=0.00..581.01 rows=50 width=22)
                     Assert Cond: NOT public.target_table.c1 IS NULL
                     ->  Split  (cost=0.00..581.01 rows=50 width=22)
                           ->  Nested Loop  (cost=0.00..581.01 rows=25 width=18)
                                 Join Filter: true
                                 ->  Table Scan on source_table  (cost=0.00..431.00 rows=25 width=8)
                                 ->  Index Scan using target_table_pkey on target_table  (cost=0.00..150.01 rows=1 width=14)
                                       Index Cond: public.target_table.c1 = source_table.c1

可以看到,HybridDB“聪明”的选择了索引。但是,如果往source_table里面加入更多数据,优化器会认为使用Nest Loop关联方法+索引扫描,不如不使用索引高效,而是会选取Hash关联方法+表扫描方式执行。例如:

postgres=> insert into source_table select generate_series(1, 1000), generate_series(1,1000);
INSERT 0 1000
postgres=> analyze source_table;
ANALYZE
postgres=> explain update target_table set c2 = source_table.c2 from source_table where target_table.c1= source_table.c1;
                                              QUERY PLAN
------------------------------------------------------------------------------------------------------
 Update  (cost=0.00..1485.82 rows=275 width=1)
   ->  Result  (cost=0.00..1429.96 rows=550 width=26)
         ->  Assert  (cost=0.00..1429.94 rows=550 width=22)
               Assert Cond: NOT public.target_table.c1 IS NULL
               ->  Split  (cost=0.00..1429.93 rows=550 width=22)
                     ->  Hash Join  (cost=0.00..1429.92 rows=275 width=18)
                           Hash Cond: public.target_table.c1 = source_table.c1
                           ->  Table Scan on target_table  (cost=0.00..477.76 rows=2500659 width=14)
                           ->  Hash  (cost=431.01..431.01 rows=275 width=8)
                                 ->  Table Scan on source_table  (cost=0.00..431.01 rows=275 width=8)

上述批量的Update方式,减少了SQL编译、节点间通信、事务等开销,可以大大提升数据更新性能并减少对资源的消耗。

批量Delete

对于Delete操作,采用和上述批量Update类似的中间表,然后使用下面的带有“Using”子句的Delete来实现批量删除:

delete from target_table using source_table where target_table.c1 = source_table.c1;

可以看到,这种批量的Delete同样使用了索引。

explain delete from target_table using source_table where target_table.c1 = source_table.c1;
                                             QUERY PLAN
-----------------------------------------------------------------------------------------------------
 Delete (slice0; segments: 4)  (rows=50 width=10)
   ->  Nested Loop  (cost=0.00..41124.40 rows=50 width=10)
         ->  Seq Scan on source_table  (cost=0.00..6.00 rows=50 width=4)
         ->  Index Scan using target_table_pkey on target_table  (cost=0.00..205.58 rows=1 width=14)
               Index Cond: target_table.c1 = source_table.c1

利用Delete + Insert做数据合并

回到本文刚开始的问题,如何实现批量的数据合并?做数据合并时,我们先把待合入的数据放入中间表中。如果我们预先知道待合入的数据,在目标表中都已经有对应的数据行,即我们通过Update语句即可实现数据合入。但多数情况下,待合入的数据中,一部分是在目标表中已存在记录的数据,还有一部分是新增的,目标表中没有对应记录。这时候,使用一次批量的Delete + 一次批量的Insert即可:

set optimizer=on;

delete from target_table using source_table where target_table.c1 = source_table.c1;

insert into target_table select * from source_table;

利用Values()表达式做实时更新

使用中间表,需要维护中间表生命周期。有的用户想实时的批量更新数据到HybridDB,即持续性的同步数据或合并数据到HybridDB。如果采用上面的方法,需要反复的创建、删除(或Truncate)中间表。其实,可以利用Values表达式,达到类似中间表的效果,但不用维护表。方法是先将待更新的数据拼成一个Values表达式,然后按如下方式执行Update或Delete:

update target_table set c2 = t.c2 from (values(1,1),(2,2),(3,3),…(2000,2000)) as t(c1,c2) where target_table.c1=t.c1

delete from target_table using (values(1,1),(2,2),(3,3),…(2000,2000)) as t(c1,c2) where target_table.c1 = t.c1

注意,使用set optimizer=on;set enable_nestloop=on;都可以生成使用索引的查询计划。比较复杂的情形,比如索引字段有多个、涉及分区表等,必须要使用ORCA优化器才能匹配上索引。

总结

上面我们简单介绍了HybridDB的批量数据合并和更新的最佳实践。利用这些方法,无论是在每天一次或多次的ETL操作,还是实时更新数据的场景,都可以把HybridDB的数据更新效率充分发挥出来。

MSSQL · 应用案例 · 构建死锁自动收集系统

$
0
0

摘要

这篇文章介绍SQL Server的一个典型的应用案例,即如何利用Event Notification与Service Broker技术相结合来实现死锁信息自动收集系统。通过这个系统,我们可以全面把控SQL Server数据库环境中所有实例上发生的死锁详细信息,供我们后期分析和解决死锁场景。

死锁自动收集系统需求分析

当 SQL Server 中某组资源的两个或多个线程或进程之间存在循环的依赖关系时,但因互相申请被其他进程所占用,而不会释放的资源处于的一种永久等待状态,将会发生死锁。SQL Server服务自动死锁检查进程默认每5分钟跑一次,当死锁发生时,会选择一个代价较小的进程做为死锁牺牲品,以此来避免死锁导致更大范围的影响。被选择做为死锁牺牲品的进程会报告如下错误:

Msg 1205, Level 13, State 51, Line 8
Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

如果进程间发生了死锁,对于用户业务系统,乃至整个SQL Server服务健康状况影响很大,轻者系统反应缓慢,服务假死;重者服务挂起,拒绝请求。那么,我们有没有一种方法可以完全自动、无人工干预的方式异步收集SQL Server系统死锁信息并远程保留死锁相关信息呢?这些信息包括但不仅限于:

  • 死锁发生在哪些进程之间

  • 各个进程执行的语句块是什么?死锁时,各个进程在执行哪条语句?

  • 死锁的资源是什么?死锁发生在哪个数据库?哪张表?哪个数据页?哪个索引上?

  • 死锁发生的具体时间点,包含语句块开始时间、语句执行时间等

  • 用户进程使用的登录用户是什么?客户端驱动是什么?
    ……
    如此的无人值守的自动死锁收集系统,就是我们今天要介绍的应用案例分享:利用SQL Server的Event Notification与Service Broker建立自动死锁信息收集系统。

Service Broker和Event Notification简介

在死锁自动收集系统介绍开始之前,先简要介绍下SQL Server Service Broker和Event Notification技术。

Service Broker简介

Service Broker是微软至SQL Server 2005开始集成到数据库引擎中的消息通讯组件,为 SQL Server提供队列和可靠的消息传递的能力,可以用来构建基于异步消息通讯为基础的应用程序。Service Broker既可用于单个 SQL Server 实例的应用程序,也可用于在多个实例间进行消息分发工作的应用程序。Service Broker使用TCP/IP端口在实例间交换消息,所包含的功能有助于防止未经授权的网络访问,并可以对通过网络发送的消息进行加密,以此来保证数据安全性。多实例之间使用Service Broker进行异步消息通讯的结构图如下所示(图片来自微软的官方文档):

01.png

Event Notification简介

Event Notification的中文名称叫事件通知,执行事件通知可对各种Transact-SQL数据定义语言(DDL)语句和SQL跟踪事件做出响应,采取的响应方式是将这些事件的相关信息发送到 Service Broker 服务。事件通知可以用来执行以下操作:

  • 记录和检索发生在数据库上的更改或活动。

  • 执行操作以异步方式而不是同步方式响应事件。

可以将事件通知用作替代DDL 触发器和SQL跟踪的编程方法。事件通知的信息媒介是以xml数据类型的信息传递给Service Broker服务,它提供了有关事件的发生时间、受影响的数据库对象、涉及的 Transact-SQL 批处理语句等详细信息。对于SQL Server死锁而言,可以使用Event Notification来跟踪死锁事件,来获取DEADLOCK_GRAPH XML信息,然后通过异步消息组件Service Broker发送到远端的Deadlock Center上的Service Broker队列,完成死锁信息收集到死锁中央服务。

死锁收集系统架构图

在介绍完Service Broker和Event Notification以后,我们来看看死锁手机系统的整体架构图。在这个系统中,存在两种类型角色:我们定义为死锁客户端(Deadlock Client)和死锁中央服务(Deadlock Center)。死锁客户端发生死锁后,首先会将Deadlock Graph XML通过Service Broker发送给死锁中央服务,死锁中央服务获取到Service Broker消息以后,解析这个XML就可以拿到客户端的死锁相关信息,最后存放到本地日志表中,供终端客户查询和分析使用。最终的死锁收集系统架构图如下所示:
02.png

详细的死锁信息收集过程介绍如下:死锁客户端通过本地SQL Server的Event Notification捕获发生在该实例上的Deadlock事件,并在死锁发生以后将Deadlock Graph XML数据存放到Event Notification绑定的队列中,然后通过绑定在该队列上的存储过程自动触发将Deadlock Graph XML通过Service Broker异步消息通讯的方式发送到死锁中央服务。中央服务在接收到Service Broker消息以后,首先放入Deadlock Center Service Broker队列中,该队列绑定了消息自动处理存储过程,用来解析Deadlock Graph XML信息,并将死锁相关的详细信息存入到Deadlock Center的Log Table中。最后,终端用户可以直接对Log Table来查询和分析所有Deadlock Client上发生的死锁信息。通过这系列的过程,最终达到了死锁信息的自动远程存储、收集,以提供后期死锁场景还原和复盘,达到死锁信息可追溯,及时监控,及时发现的目的。

Service Broker配置

系统架构设计完毕后,接下来是系统的配置和搭建过程,首先看看Service Broker的配置。这个配置还是相对比较繁琐的,包含了以下步骤:

  • 创建Service Broker数据库(假设数据库名为DDLCenter)并开启Service Broker选项

  • 创建Service Broker队列的激活存储过程和相关表对象

  • 创建Master数据库下的Master Key

  • 创建传输层本地和远程证书

  • 创建基于证书的用户登录

  • 创建Service Broker端口并授权用户连接

  • 创建DDLCenter数据库下的Master Key

  • 创建会话层本地及远程证书

  • 创建Service Broker组件所需要的对象,包括:Message Type、Contact、Queue、Service、Remote Service Binding、Route

Deadlock Client Server

以下的配置请在Deadlock Client SQL Server实例上操作。

  • 创建DDLCenter数据库并开启Service Broker选项
-- Run script on client server to gather deadlock graph xml
USE master
GO
-- Create Database
IF DB_ID('DDLCenter') IS NULL
	CREATE DATABASE [DDLCenter];
GO
-- Change datbase to simple recovery model
ALTER DATABASE [DDLCenter] SET RECOVERY SIMPLE WITH NO_WAIT
GO
-- Enable Service Broker
ALTER DATABASE [DDLCenter] SET ENABLE_BROKER,TRUSTWORTHY ON
GO
-- Change database Owner to sa
ALTER AUTHORIZATION ON DATABASE::DDLCenter TO [sa]
GO
  • 三个表和两个存储过程

表[DDLCollector].[Deadlock_Traced_Records]:从Event Notification队里接收的消息会记录到该表中。
表[DDLCollector].[Send_Records]:Deadlock Client成功发送Service Broker消息记录
表[DDLCollector].[Error_Records]:记录发生异常情况时的信息。
存储过程[DDLCollector].[UP_ProcessDeadlockEventMsg]:Deadlock Client绑定到队里的激活存储过程,一旦队列中有消息进入,这个存储过程会被自动调用。
存储过程[DDLCollector].[UP_SendDeadlockMsg]:Deadlock Client发送异步消息给Deadlock Center,这个存储过程会被上面的激活存储过程调用。

-- Run on Client Instance
USE [DDLCenter]
GO
-- Create Schema
IF NOT EXISTS(
	SELECT TOP 1 *
	FROM sys.schemas
	WHERE name = 'DDLCollector'
)
BEGIN
	EXEC('CREATE SCHEMA DDLCollector');
END
GO

-- Create table to log Traced Deadlock Records
IF OBJECT_ID('DDLCollector.Deadlock_Traced_Records', 'U') IS NOT NULL
	DROP TABLE [DDLCollector].[Deadlock_Traced_Records]
GO

CREATE TABLE [DDLCollector].[Deadlock_Traced_Records](
	[RowId] [BIGINT] IDENTITY(1,1) NOT NULL,
	[Processed_Msg] [xml] NULL,
	[Processed_Msg_CheckSum] INT,
	[Record_Time] [datetime] NOT NULL 
		CONSTRAINT DF_Deadlock_Traced_Records_Record_Time DEFAULT(GETDATE()),
	CONSTRAINT PK_Deadlock_Traced_Records_RowId PRIMARY KEY
	(RowId ASC)
) ON [PRIMARY]
GO

-- Create table to record deadlock graph xml sent successfully log
IF OBJECT_ID('DDLCollector.Send_Records', 'U') IS NOT NULL
	DROP TABLE [DDLCollector].[Send_Records]
GO

CREATE TABLE [DDLCollector].[Send_Records](
	[RowId] [BIGINT] IDENTITY(1,1) NOT NULL,
	[Send_Msg] [xml] NULL,
	[Send_Msg_CheckSum] INT,
	[Record_Time] [datetime] NOT NULL 
		CONSTRAINT DF_Send_Records_Record_Time DEFAULT(GETDATE()),
	CONSTRAINT PK_Send_Records_RowId PRIMARY KEY
	(RowId ASC)
) ON [PRIMARY]
GO

-- Create table to record error info when exception occurs
IF OBJECT_ID('DDLCollector.Error_Records', 'U') IS NOT NULL
	DROP TABLE [DDLCollector].[Error_Records]
GO

CREATE TABLE [DDLCollector].[Error_Records](
	[RowId] [int] IDENTITY(1,1) NOT NULL,
	[Msg_Body] [xml] NULL,
	[Conversation_handle] [uniqueidentifier] NULL,
	[Message_Type] SYSNAME NULL,
	[Service_Name] SYSNAME NULL,
	[Contact_Name] SYSNAME NULL,
	[Record_Time] [datetime] NOT NULL
		CONSTRAINT DF_Error_Records_Record_Time DEFAULT(GETDATE()),
	[Error_Details] [nvarchar](4000) NULL,
	CONSTRAINT PK_Error_Records_RowId PRIMARY KEY
	(RowId ASC)
) ON [PRIMARY]
GO


USE [DDLCenter]
GO

-- Create Store Procedure to Send Deadlock Graph xml to Center Server
IF OBJECT_ID('DDLCollector.UP_SendDeadlockMsg', 'P') IS NOT NULL
	DROP PROC [DDLCollector].[UP_SendDeadlockMsg]
GO

CREATE PROCEDURE [DDLCollector].[UP_SendDeadlockMsg](
	@DeadlockMsg XML
)
AS  
BEGIN      
	SET NOCOUNT ON; 

	DECLARE 
		@handle UNIQUEIDENTIFIER
		,@Proc_Name SYSNAME
		,@Error_Details VARCHAR(2000)
	;

	-- get the store procedure name
	SELECT 
        @Proc_Name = ISNULL(QUOTENAME(SCHEMA_NAME(SCHEMA_ID)) 
        + '.' 
        + QUOTENAME(OBJECT_NAME(@@PROCID)),'')
    FROM sys.procedures
    WHERE OBJECT_ID = @@PROCID
	;
	
	BEGIN TRY
		
		-- Begin Dialog
		BEGIN DIALOG CONVERSATION @handle
		FROM SERVICE [http://soa/deadlock/service/ClientService]
		TO Service 'http://soa/deadlock/service/CenterService'
		ON CONTRACT [http://soa/deadlock/contract/CheckContract]
		;

		-- Send deadlock graph xml as the message to Center Server
		SEND ON CONVERSATION @handle
		MESSAGE TYPE [http://soa/deadlock/MsgType/Request] (@DeadlockMsg);

		-- Log it successfully
		INSERT INTO [DDLCollector].[Send_Records]([Send_Msg], [Send_Msg_CheckSum])   
		VALUES( @DeadlockMsg, CHECKSUM(CAST(@DeadlockMsg as NVARCHAR(MAX))))
	END TRY
	BEGIN CATCH
		
		-- Record the error info when exception occurs
		SET   @Error_Details=
				' Error Number: ' + CAST(ERROR_NUMBER() AS VARCHAR(10)) +
				' Error Message : ' + ERROR_MESSAGE() +
				' Error Severity: ' + CAST(ERROR_SEVERITY() AS VARCHAR(10)) +
				' Error State: ' + CAST(ERROR_STATE() AS VARCHAR(10)) +
				' Error Line: ' + CAST(ERROR_LINE() AS VARCHAR(10)) +
				' Exception Proc: ' + @Proc_Name
		;    
        
		-- record into table
		INSERT INTO [DDLCollector].[Error_Records]([Msg_Body], [Conversation_handle], [Message_Type], [Service_Name], [Contact_Name], [Error_Details])         
		VALUES(@DeadlockMsg, @handle, 'http://soa/deadlock/MsgType/Request', 'http://soa/deadlock/service/ClientService', 'http://soa/deadlock/contract/CheckContract', @Error_Details); 

	END CATCH
END
GO

-- Create Store Procedure for Queue: when extend event notification queue message
-- this store procedure will be called.
IF OBJECT_ID('DDLCollector.UP_ProcessDeadlockEventMsg', 'P') IS NOT NULL
	DROP PROC [DDLCollector].[UP_ProcessDeadlockEventMsg]
GO

CREATE PROCEDURE [DDLCollector].[UP_ProcessDeadlockEventMsg]
AS
/*

SELECT * FROM [DDLCollector].[Deadlock_Traced_Records]
SELECT * FROM [DDLCollector].[Send_Records]

SELECT * FROM [DDLCollector].[Error_Records]

*/
BEGIN      
	SET NOCOUNT ON;   
	DECLARE 
		@handle UNIQUEIDENTIFIER
		, @Message_Type SYSNAME
		, @Service_Name SYSNAME
		, @Contact_Name SYSNAME
		, @Error_Details VARCHAR(2000)
		, @Message_Body XML
		, @Proc_Name SYSNAME
	;

	-- Store Procedure Name
	SELECT 
        @Proc_Name = ISNULL(QUOTENAME(SCHEMA_NAME(SCHEMA_ID)) 
        + '.' 
        + QUOTENAME(OBJECT_NAME(@@PROCID)),'')
    FROM sys.procedures
    WHERE OBJECT_ID = @@PROCID
	;

	BEGIN TRY
    
	-- Receive message from queue
	WAITFOR(RECEIVE TOP(1)        
			@handle = conversation_handle
			, @Message_Type = message_type_name
			, @Service_Name = service_name
			, @Contact_Name = service_contract_name
			, @Message_Body = message_body        
			FROM dbo.[http://soa/deadlock/queue/ClientQueue]),Timeout 500
	;
	
	-- just return if there is no message needed to process        
	IF(@@Rowcount=0)      
		BEGIN   
			RETURN        
		END
	-- Get data from message queue
	ELSE IF @Message_Type = 'http://schemas.microsoft.com/SQL/Notifications/EventNotification'      
		BEGIN               
			-- Record message log first
			INSERT INTO  [DDLCollector].[Deadlock_Traced_Records](Processed_Msg, [Processed_Msg_CheckSum])         
			VALUES(@Message_Body, CHECKSUM(CAST(@Message_Body as NVARCHAR(MAX))))
		
			-- BE NOTED HERE: PLEASE DO'T END CONVERSATION, OR ELSE EXCEPTION WILL BE THROWN OUTPUT
			/*
			Error: 17001, Severity: 16, State: 1.
			Failure to send an event notification instance of type 'DEADLOCK_GRAPH' on conversation handle '{67419386-7C34-E711-A709-001C42099969}'. Error Code = '8429'.
			Error: 17005, Severity: 16, State: 1.
			Event notification 'DeadLockNotificationEvent' in database 'master' dropped due to send time service broker errors. Check to ensure the conversation handle, service broker contract, and service specified in the event notification are active.  
			*/
			--END CONVERSATION @handle

			--Here call another Store Procedure to send deadlock graph info to center server
			EXEC [DDLCollector].[UP_SendDeadlockMsg] @Message_Body;
		END
	--End Diaglog Message Type, that means we should end this conversation      
	ELSE IF @Message_Type = N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'        
		BEGIN         
			END CONVERSATION @handle;     
		END
	-- Konwn Service Broker Errors by System.   
	ELSE IF @Message_Type = N'http://schemas.microsoft.com/SQL/ServiceBroker/Error'        
		BEGIN         
			END CONVERSATION @handle       
		
			INSERT INTO [DDLCollector].[Error_Records]([Msg_Body], [Conversation_handle], [Message_Type], [Service_Name], [Contact_Name], [Error_Details])         
			VALUES(@Message_Body, @handle, @Message_Type, @Service_Name, @Contact_Name, ' Exception Store Procedure: ' + @Proc_Name);               
		END       
	ELSE
		-- unknown Message Types.        
		BEGIN         
			END CONVERSATION @handle

			INSERT INTO [DDLCollector].[Error_Records]([Msg_Body], [Conversation_handle], [Message_Type], [Service_Name], [Contact_Name], [Error_Details])          
			VALUES(@Message_Body, @handle, @Message_Type, @Service_Name, @Contact_Name, ' Received unexpected message type when executing Store Procedure: ' + @Proc_Name);

			-- unexpected message type         
			RAISERROR (N' Received unknown message type: %s', 16, 1, @Message_Type) WITH LOG;        
		END      
	END TRY      
	BEGIN CATCH       
	BEGIN        
		SET   @Error_Details=
				' Error Number: ' + CAST(ERROR_NUMBER() AS VARCHAR(10)) +
				' Error Details : ' + ERROR_MESSAGE() +
				' Error Severity: ' + CAST(ERROR_SEVERITY() AS VARCHAR(10)) +
				' Error State: ' + CAST(ERROR_STATE() AS VARCHAR(10)) +
				' Error Line: ' + CAST(ERROR_LINE() AS VARCHAR(10)) + 
				' Exception Proc: ' + @Proc_Name
		;    
        
		INSERT INTO [DDLCollector].[Error_Records]([Msg_Body], [Conversation_handle], [Message_Type], [Service_Name], [Contact_Name], [Error_Details])         
		VALUES(@Message_Body, @handle, @Message_Type, @Service_Name, @Contact_Name, @Error_Details); 
	END      
	END CATCH  
END  
GO
  • 创建Master库下Master Key
USE master
GO
-- If the master key is not available, create it. 
IF NOT EXISTS (SELECT * 
				FROM sys.symmetric_keys
				WHERE name LIKE '%MS_DatabaseMasterKey%') 
BEGIN
	CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'ClientMasterKey*'; 
END 
GO
  • 创建传输层本地证书并备份到本地文件系统

这里请注意证书的开始生效时间要略微早于当前时间,并设置合适的证书过期日期,我这里是设置的过期日期为9999年12月30号。

USE master
GO
-- Crete Transport Layer Certification
CREATE CERTIFICATE TrpCert_ClientLocal
AUTHORIZATION dbo
WITH SUBJECT = 'TrpCert_ClientLocal',
START_DATE = '05/07/2017',
EXPIRY_DATE = '12/30/9999'
GO

-- then backup it up to local path
-- and after that copy it to Center server
BACKUP CERTIFICATE TrpCert_ClientLocal
TO FILE = 'C:\Temp\TrpCert_ClientLocal.cer';
GO
  • 创建传输层远程证书

这里的证书是通过证书文件来创建的,这个证书文件来自于远程通讯的另一端Deadlock Center SQL Server的证书文件的一份拷贝。

USE master
GO
-- Create certification came from Center Server.
CREATE	CERTIFICATE TrpCert_RemoteCenter 
FROM FILE = 'C:\Temp\TrpCert_RemoteCenter.cer'
GO
  • 创建基于证书文件的用户登录

这里也可以创建带密码的常规用户登录,但是为了规避安全风险,这里最好创建基于证书文件的用户登录。

USE master
GO
-- Create user login
IF NOT EXISTS(SELECT * 
			FROM sys.syslogins 
			WHERE name='SSBDbo')
BEGIN
	CREATE LOGIN SSBDbo FROM CERTIFICATE TrpCert_ClientLocal;
END
GO
  • 创建Service Broker TCP/IP通讯端口并授权用户连接权限

这里需要注意的是,端口授权的证书一定本地实例创建的证书,而不是来自于远程服务器的那个证书。比如代码中的AUTHENTICATION = CERTIFICATE TrpCert_ClientLocal部分。

USE master
GO 		 
--Creaet Tcp endpoint for SSB comunication and grant connect to users. 	 
CREATE ENDPOINT EP_SSB_ClientLocal
STATE = STARTED 
AS TCP 
( 
 	LISTENER_PORT = 4022 
) 
FOR SERVICE_BROKER (AUTHENTICATION = CERTIFICATE TrpCert_ClientLocal,  ENCRYPTION = REQUIRED 
) 
GO 

-- Grant Connect on Endpoint to User SSBDbo
GRANT CONNECT ON ENDPOINT::EP_SSB_ClientLocal TO SSBDbo 
GO
  • 创建DDLCenter数据库Master Key
-- Now, let's go inside to conversation database
USE DDLCenter
GO

-- Create Master Key
IF NOT EXISTS (SELECT * 
				FROM sys.symmetric_keys 
				WHERE name LIKE '%MS_DatabaseMasterKey%')
BEGIN		
	CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'DDLCenterMasterKey*';
END
GO
  • 创建会话层本地证书
USE DDLCenter
GO
-- Create conversation layer certification
CREATE CERTIFICATE DlgCert_ClientLocal
AUTHORIZATION dbo
WITH SUBJECT = 'DlgCert_ClientLocal',
START_DATE = '05/07/2017',
EXPIRY_DATE = '12/30/9999'
GO

-- backup it up to local path
-- and then copy it to remote Center server
BACKUP CERTIFICATE DlgCert_ClientLocal
TO FILE = 'C:\Temp\DlgCert_ClientLocal.cer';
GO
  • 创建DDLCenter用户,不需要和任何用户登录匹配
USE DDLCenter
GO
-- Create User for login under conversation database
IF NOT EXISTS(
	SELECT TOP 1 *
	FROM sys.database_principals
	WHERE name = 'SSBDbo'
)
BEGIN
	CREATE USER SSBDbo WITHOUT LOGIN;
END
GO
  • 创建会话层远程证书,这个证书文件来自Deadlock Center SQL Server备份
USE DDLCenter
GO
-- Create converstaion layer certification came from remote Center server.
CREATE	CERTIFICATE DlgCert_RemoteCenter
AUTHORIZATION SSBDbo
FROM FILE='C:\Temp\DlgCert_RemoteCenter.cer'
GO

GRANT CONNECT TO SSBDbo;
  • 创建Service Broker组件对象

Deadlock Client与Deadlock Center在创建Service Broker组件对象时存在差异:第一个差异是创建Service的时候,需要包含Event Notification的Contract,名称为
http://schemas.microsoft.com/SQL/Notifications/PostEventNotification;第二个差异是需要多创建一个指向本地服务的路由http://soa/deadlock/route/LocalRoute。

USE DDLCenter
GO

-- Create Message Type
CREATE MESSAGE TYPE [http://soa/deadlock/MsgType/Request]
       VALIDATION = WELL_FORMED_XML;
CREATE MESSAGE TYPE [http://soa/deadlock/MsgType/Response]
       VALIDATION = WELL_FORMED_XML;
GO

-- Create Contact
CREATE CONTRACT [http://soa/deadlock/contract/CheckContract](
	[http://soa/deadlock/MsgType/Request] SENT BY INITIATOR,
	[http://soa/deadlock/MsgType/Response] SENT BY TARGET
);
GO

-- Create Queue
CREATE QUEUE dbo.[http://soa/deadlock/queue/ClientQueue] 
WITH STATUS = ON, RETENTION = OFF
, ACTIVATION (STATUS = ON , 
				PROCEDURE_NAME = [DDLCollector].[UP_ProcessDeadlockEventMsg] , 
				MAX_QUEUE_READERS = 2 , 
				EXECUTE AS N'dbo') 
GO

-- Create Service
-- Here is very import, we have to create service for both contacts
-- to get extend event notification and SSB work.
CREATE SERVICE [http://soa/deadlock/service/ClientService]
ON QUEUE [http://soa/deadlock/queue/ClientQueue]
(
  [http://soa/deadlock/contract/CheckContract],
  [http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]
);
GO

-- Grant Send on service
GRANT SEND ON SERVICE::[http://soa/deadlock/service/ClientService] to SSBDbo;
GO

-- Create Remote Service Bingding
CREATE REMOTE SERVICE BINDING [http://soa/deadlock/RSB/CenterRSB]
TO SERVICE 'http://soa/deadlock/service/CenterService' 
WITH  USER = [SSBDbo],
ANONYMOUS=Off
GO

-- Create Route
CREATE ROUTE [http://soa/deadlock/route/CenterRoute]
WITH SERVICE_NAME = 'http://soa/deadlock/service/CenterService',
ADDRESS = 'TCP://10.211.55.3:4024';
GO

-- Create route for the DeadlockNotificationSvc
CREATE ROUTE [http://soa/deadlock/route/LocalRoute]
WITH SERVICE_NAME = 'http://soa/deadlock/service/ClientService',
ADDRESS = 'LOCAL';
GO

Deadlock Center Server

  • 创建DDLCenter数据库并开启Service Broker选项
-- Run script on center server to receive client deadlock xml
USE master
GO
-- Create Database
IF DB_ID('DDLCenter') IS NULL
	CREATE DATABASE [DDLCenter];
GO
-- Change datbase to simple recovery model
ALTER DATABASE [DDLCenter] SET RECOVERY SIMPLE WITH NO_WAIT
GO
-- Enable Service Broker
ALTER DATABASE [DDLCenter] SET ENABLE_BROKER,TRUSTWORTHY ON
GO
-- Change database Owner to sa
ALTER AUTHORIZATION ON DATABASE::DDLCenter TO [sa]
GO
  • 三张表和两个存储过程

表[DDLCollector].[Collect_Records]:Deadlock Center成功接收到的Service Broker消息。
表[DDLCollector].[Error_Records]:记录发生异常情况的详细信息。
表[DDLCollector].[Deadlock_Info]:记录所有Deadlock Client端发生的Deadlock详细信息。
存储过程[DDLCollector].[UP_ProcessDeadlockGraphEventMsg]:Deadlock Center上绑定到队列的激活存储过程,一旦队列中有消息进入,这个存储过程会被自动调用。
存储过程[DDLCollector].[UP_ParseDeadlockGraphEventMsg]:Deadlock Center上解析Deadlock Graph XML的存储过程对象,这个存储过程会被上面的激活存储过程调用来解析XML,然后放入表[DDLCollector].[Deadlock_Info]中。

USE [DDLCenter]
GO

-- Create Schema
IF NOT EXISTS(
	SELECT TOP 1 *
	FROM sys.schemas
	WHERE name = 'DDLCollector'
)
BEGIN
	EXEC('CREATE SCHEMA DDLCollector');
END
GO

-- Create table to log the received message
IF OBJECT_ID('DDLCollector.Collect_Records', 'U') IS NOT NULL
	DROP TABLE [DDLCollector].[Collect_Records]
GO

CREATE TABLE [DDLCollector].[Collect_Records](
	[RowId] [BIGINT] IDENTITY(1,1) NOT NULL,
	[Deadlock_Graph_Msg] [xml] NULL,
	[Deadlock_Graph_Msg_CheckSum] INT,
	[Record_Time] [datetime] NOT NULL 
		CONSTRAINT DF_Collect_Records_Record_Time DEFAULT(GETDATE()),
	CONSTRAINT PK_Collect_Records_RowId PRIMARY KEY
	(RowId ASC)
) ON [PRIMARY]
GO

-- create table to record the exception when error occurs
IF OBJECT_ID('DDLCollector.Error_Records', 'U') IS NOT NULL
	DROP TABLE [DDLCollector].[Error_Records]
GO

CREATE TABLE [DDLCollector].[Error_Records](
	[RowId] [int] IDENTITY(1,1) NOT NULL,
	[Msg_Body] [xml] NULL,
	[Conversation_handle] [uniqueidentifier] NULL,
	[Message_Type] SYSNAME NULL,
	[Service_Name] SYSNAME NULL,
	[Contact_Name] SYSNAME NULL,
	[Record_Time] [datetime] NOT NULL
		CONSTRAINT DF_Error_Records_Record_Time DEFAULT(GETDATE()),
	[Error_Details] [nvarchar](4000) NULL,
	CONSTRAINT PK_Error_Records_RowId PRIMARY KEY
	(RowId ASC)
) ON [PRIMARY]
GO

-- create business table to record deadlock analysised info
IF OBJECT_ID('DDLCollector.Deadlock_Info', 'U') IS NOT NULL
	DROP TABLE [DDLCollector].[Deadlock_Info]
GO
CREATE TABLE [DDLCollector].[Deadlock_Info](
	RowId INT IDENTITY(1,1) NOT NULL
	,SQLInstance sysname NULL
	,SPid INT NULL
	,is_Vitim BIT NULL
	,DeadlockGraph XML NULL
	,DeadlockGraphCheckSum INT NULL
	,lasttranstarted DATETIME NULL
	,lastbatchstarted DATETIME NULL
	,lastbatchcompleted DATETIME NULL
	,procname SYSNAME NULL 
	,Code NVARCHAR(max) NULL
	,LockMode sysname NULL
	,Indexname sysname NULL
	,KeylockObject sysname NULL
	,IndexLockMode sysname NULL
	,Inputbuf NVARCHAR(max) NULL
	,LoginName sysname NULL
	,Clientapp sysname NULL
	,Action varchar(1000) NULL
	,status varchar(10) NULL
	,[Record_Time] [datetime] NOT NULL
		CONSTRAINT DF_Deadlock_Info_Record_Time DEFAULT(GETDATE()),
	CONSTRAINT PK_Deadlock_Info_RowId PRIMARY KEY
	(RowId ASC)
)
GO



USE [DDLCenter]
GO

-- Create store procedure to analysis deadlock graph xml
-- and log into business table
IF OBJECT_ID('DDLCollector.UP_ParseDeadlockGraphEventMsg', 'P') IS NOT NULL
	DROP PROC [DDLCollector].[UP_ParseDeadlockGraphEventMsg]
GO

CREATE PROCEDURE [DDLCollector].[UP_ParseDeadlockGraphEventMsg](
	@DeadlockGraph_Msg XML
)
AS  
BEGIN      
	SET NOCOUNT ON; 

	;WITH deadlock
	AS
	(
		SELECT
			OwnerID = T.C.value('@id', 'varchar(50)')
			,SPid = T.C.value('(./@spid)[1]','int')
			,status = T.C.value('(./@status)[1]','varchar(10)')
			,Victim = case 
						when T.C.value('@id', 'varchar(50)') = T.C.value('./../../@victim','varchar(50)') then 1 
						else 0 end
			,LockMode = T.C.value('@lockMode', 'sysname')
			,Inputbuf = T.C.value('(./inputbuf/text())[1]','nvarchar(max)')
			,Code = T.C.value('(./executionStack/frame/text())[1]','nvarchar(max)')
			,SPName = T.C.value('(./executionStack/frame/@procname)[1]','sysname')
			,Hostname = T.C.value('(./@hostname)[1]','sysname')
			,Clientapp = T.C.value('(./@clientapp)[1]','varchar(1000)')
			,lasttranstarted = T.C.value('(./@lasttranstarted)[1]','datetime')
			,lastbatchstarted = T.C.value('(./@lastbatchstarted)[1]','datetime')
			,lastbatchcompleted = T.C.value('(./@lastbatchcompleted)[1]','datetime')
			,LoginName = T.C.value('@loginname', 'sysname')
			,Action = T.C.value('(./@transactionname)[1]','varchar(1000)')
		FROM @DeadlockGraph_Msg.nodes('EVENT_INSTANCE/TextData/deadlock-list/deadlock/process-list/process') AS T(C)
	)
	,
	keylock
	AS
	(
		SELECT
			OwnerID = T.C.value('./owner[1]/@id', 'varchar(50)')
			,KeylockObject = T.C.value('./../@objectname', 'sysname')
			,Indexname = T.C.value('./../@indexname', 'sysname')
			,IndexLockMode = T.C.value('./../@mode', 'sysname')
		FROM @DeadlockGraph_Msg.nodes('EVENT_INSTANCE/TextData/deadlock-list/deadlock/resource-list/keylock/owner-list') AS T(C)
	)
	SELECT
		SQLInstance = A.Hostname 
		,A.SPid
		,is_Vitim = A.Victim
		,DeadlockGraph = @DeadlockGraph_Msg.query('EVENT_INSTANCE/TextData/deadlock-list')
		,DeadlockGraphCheckSum = CHECKSUM(CAST(@DeadlockGraph_Msg AS NVARCHAR(MAX)))
		,A.lasttranstarted
		,A.lastbatchstarted
		,A.lastbatchcompleted
		,A.SPName
		,A.Code
		,A.LockMode
		,B.Indexname
		,B.KeylockObject
		,B.IndexLockMode
		,A.Inputbuf
		,A.LoginName
		,A.Clientapp
		,A.Action
		,status
		,[Record_Time] = GETDATE()
	FROM deadlock AS A
			LEFT JOIN keylock AS B
			ON A.OwnerID = B.OwnerID
	ORDER BY A.SPid, A.Victim
	;
END
GO

-- Create store Procedure for Center server service queue to process deadlock xml
-- when message sending from client server.
IF OBJECT_ID('DDLCollector.UP_ProcessDeadlockGraphEventMsg', 'P') IS NOT NULL
	DROP PROC [DDLCollector].[UP_ProcessDeadlockGraphEventMsg]
GO

CREATE PROCEDURE [DDLCollector].[UP_ProcessDeadlockGraphEventMsg]
AS
/*
EXEC [DDLCollector].[UP_ProcessDeadlockGraphEventMsg]

SELECT * FROM [DDLCollector].[Collect_Records]

SELECT * FROM [DDLCollector].[Error_Records]

SELECT * FROM [DDLCollector].[Deadlock_Info]
*/
BEGIN      
	SET NOCOUNT ON;   
	DECLARE 
		@handle UNIQUEIDENTIFIER
		, @Message_Type SYSNAME
		, @Service_Name SYSNAME
		, @Contact_Name SYSNAME
		, @Error_Details VARCHAR(2000)
		, @Message_Body XML
		, @Proc_Name SYSNAME
	;

	-- Store Procedure name
	SELECT 
        @Proc_Name = ISNULL(QUOTENAME(SCHEMA_NAME(SCHEMA_ID)) 
        + '.' 
        + QUOTENAME(OBJECT_NAME(@@PROCID)),'')
    FROM sys.procedures
    WHERE OBJECT_ID = @@PROCID
	;

	BEGIN TRY
        
	-- Receive deadlock message from service queue
	WAITFOR(RECEIVE TOP(1)        
			@handle = conversation_handle
			, @Message_Type = message_type_name
			, @Service_Name = service_name
			, @Contact_Name = service_contract_name
			, @Message_Body = message_body        
			FROM dbo.[http://soa/deadlock/queue/CenterQueue]),Timeout 500
	;
	        
	IF(@@Rowcount=0)      
		BEGIN   
			RETURN        
		END
	-- Message type is the very correct one
	ELSE IF @Message_Type = N'http://soa/deadlock/MsgType/Request'        
		BEGIN               
			-- Record message log first
			INSERT INTO  [DDLCollector].[Collect_Records](Deadlock_Graph_Msg, [Deadlock_Graph_Msg_CheckSum])          
			VALUES(@Message_Body, CHECKSUM(cast(@Message_Body as NVARCHAR(MAX))))
		
			END CONVERSATION @handle

			--Here call another Store Procedure to process our message to record deadlock relation info
			INSERT INTO [DDLCollector].[Deadlock_Info]
			EXEC [DDLCollector].[UP_ParseDeadlockGraphEventMsg] @Message_Body;
		END
	--End Diaglog Message Type, that means we should end this conversation      
	ELSE IF @Message_Type = N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'        
		BEGIN         
			END CONVERSATION @handle;        
		END
	-- Konwn Service Broker Errors by System.   
	ELSE IF @Message_Type = N'http://schemas.microsoft.com/SQL/ServiceBroker/Error'        
		BEGIN         
			END CONVERSATION @handle       
		
			INSERT INTO [DDLCollector].[Error_Records]([Msg_Body], [Conversation_handle], [Message_Type], [Service_Name], [Contact_Name], [Error_Details])          
			VALUES(@Message_Body, @handle, @Message_Type, @Service_Name, @Contact_Name, ' Exception Store Procedure: ' + @Proc_Name);               
		END       
	ELSE
		-- unknown Message Types.        
		BEGIN         
			END CONVERSATION @handle

			INSERT INTO [DDLCollector].[Error_Records]([Msg_Body], [Conversation_handle], [Message_Type], [Service_Name], [Contact_Name], [Error_Details])          
			VALUES(@Message_Body, @handle, @Message_Type, @Service_Name, @Contact_Name, ' Received unexpected message type when executing Store Procedure: ' + @Proc_Name);

			-- unexpected message type         
			RAISERROR (N' Received unexpected message type: %s', 16, 1, @Message_Type) WITH LOG;        
		END      
	END TRY      
	BEGIN CATCH       
	BEGIN
		-- record exception record       
		SET   @Error_Details=
				' Error Number: ' + CAST(ERROR_NUMBER() AS VARCHAR(10)) +
				' Error Message : ' + ERROR_MESSAGE() +
				' Error Severity: ' + CAST(ERROR_SEVERITY() AS VARCHAR(10)) +
				' Error State: ' + CAST(ERROR_STATE() AS VARCHAR(10)) +
				' Error Line: ' + CAST(ERROR_LINE() AS VARCHAR(10)) + 
				' Exception Proc: ' + @Proc_Name
		;    
        
		INSERT INTO [DDLCollector].[Error_Records]([Msg_Body], [Conversation_handle], [Message_Type], [Service_Name], [Contact_Name], [Error_Details])          
		VALUES(@Message_Body, @handle, @Message_Type, @Service_Name, @Contact_Name, @Error_Details); 
	END      
	END CATCH  
END  
GO
  • 创建Master库下Master Key
USE master
GO
-- If the master key is not available, create it. 
IF NOT EXISTS (SELECT * 
				FROM sys.symmetric_keys
				WHERE name LIKE '%MS_DatabaseMasterKey%') 
BEGIN
	CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'CenterMasterKey*'; 
END 
GO 
  • 创建传输层本地证书并备份到本地文件系统
USE master
GO
-- Crete Transport Layer Certification
CREATE CERTIFICATE TrpCert_RemoteCenter
AUTHORIZATION dbo
WITH SUBJECT = 'TrpCert_RemoteCenter',
START_DATE = '05/07/2017',
EXPIRY_DATE = '12/30/9999'
GO

-- then backup it up to local path
-- and after that copy it to Client server
BACKUP CERTIFICATE TrpCert_RemoteCenter
TO FILE = 'C:\Temp\TrpCert_RemoteCenter.cer';
GO
  • 创建传输层远程证书,这个证书文件来至于Deadlock Client SQL Server
USE master
GO
-- Create certification came from client Server.
CREATE	CERTIFICATE TrpCert_ClientLocal 
FROM FILE = 'C:\Temp\TrpCert_ClientLocal.cer'
GO
  • 创建基于证书文件的用户登录
USE master
GO
-- Create user login
IF NOT EXISTS(SELECT * 
			FROM sys.syslogins 
			WHERE name='SSBDbo')
BEGIN
	CREATE LOGIN SSBDbo FROM CERTIFICATE TrpCert_RemoteCenter;
END
GO
  • 创建Service Broker TCP/IP通讯端口并授权用户连接权限
USE master
GO
-- Creaet Tcp endpoint for SSB comunication and grant connect to users. 	 
CREATE ENDPOINT EP_SSB_RemoteCenter
STATE = STARTED 
AS TCP 
( 
 	LISTENER_PORT = 4024
) 
FOR SERVICE_BROKER (AUTHENTICATION = CERTIFICATE TrpCert_RemoteCenter,  ENCRYPTION = REQUIRED 
) 
GO 

-- Grant Connect on Endpoint to User SSBDbo
GRANT CONNECT ON ENDPOINT::EP_SSB_RemoteCenter TO SSBDbo 
GO
  • 创建DDLCenter数据库Master Key
-- Now, let's go inside to conversation database
USE DDLCenter
GO

-- Create Master Key
IF NOT EXISTS (SELECT * 
				FROM sys.symmetric_keys 
				WHERE name LIKE '%MS_DatabaseMasterKey%')
BEGIN		
	CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'DDLCenterMasterKey*';
END
GO
  • 创建会话层本地证书
USE DDLCenter
GO
-- Create conversation layer certification
CREATE CERTIFICATE DlgCert_RemoteCenter
AUTHORIZATION dbo
WITH SUBJECT = 'DlgCert_RemoteCenter',
START_DATE = '05/07/2017',
EXPIRY_DATE = '12/30/9999'
GO

-- backup it up to local path
-- and then copy it to remote client server
BACKUP CERTIFICATE DlgCert_RemoteCenter
TO FILE = 'C:\Temp\DlgCert_RemoteCenter.cer';
GO
  • 创建DDLCenter用户,不需要和任何用户登录匹配
USE DDLCenter
GO
-- Create User for login under conversation database
IF NOT EXISTS(
	SELECT TOP 1 *
	FROM sys.database_principals
	WHERE name = 'SSBDbo'
)
BEGIN
	--CREATE USER SSBDbo FOR LOGIN SSBDbo;
	CREATE USER SSBDbo WITHOUT LOGIN;
END
GO
  • 创建会话层远程证书,这个证书文件来自Deadlock Center SQL Server备份
USE DDLCenter
GO
-- Create converstaion layer certification came from remote client server.
CREATE	CERTIFICATE DlgCert_ClientLocal
AUTHORIZATION SSBDbo
FROM FILE='C:\Temp\DlgCert_ClientLocal.cer'
GO

GRANT CONNECT TO SSBDbo;
  • 创建Service Broker组件对象
USE DDLCenter
GO

-- Create Message Type
CREATE MESSAGE TYPE [http://soa/deadlock/MsgType/Request]
       VALIDATION = WELL_FORMED_XML;
CREATE MESSAGE TYPE [http://soa/deadlock/MsgType/Response]
       VALIDATION = WELL_FORMED_XML;
GO

-- Create Contact
CREATE CONTRACT [http://soa/deadlock/contract/CheckContract](
	[http://soa/deadlock/MsgType/Request] SENT BY INITIATOR,
	[http://soa/deadlock/MsgType/Response] SENT BY TARGET
);
GO

-- Create Queue
CREATE QUEUE [dbo].[http://soa/deadlock/queue/CenterQueue] 
WITH STATUS = ON , RETENTION = OFF
, ACTIVATION (STATUS = ON , 
				PROCEDURE_NAME = [DDLCollector].[UP_ProcessDeadlockGraphEventMsg] , 
				MAX_QUEUE_READERS = 3 , 
				EXECUTE AS N'dbo') 
GO

-- Create Service
CREATE SERVICE [http://soa/deadlock/service/CenterService]
ON QUEUE [http://soa/deadlock/queue/CenterQueue]
(
  [http://soa/deadlock/contract/CheckContract]
);
GO

-- Grant Send on service to User SSBDbo
GRANT SEND ON SERVICE::[http://soa/deadlock/service/CenterService] to SSBDbo;
GO

-- Create Remote Service Bingding
CREATE REMOTE SERVICE BINDING [http://soa/deadlock/RSB/ClientRSB]
TO SERVICE 'http://soa/deadlock/service/ClientService' 
WITH  USER = SSBDbo,
ANONYMOUS=Off
GO

-- Create Route
CREATE ROUTE [http://soa/deadlock/route/ClientRoute]
WITH SERVICE_NAME = 'http://soa/deadlock/service/ClientService',
ADDRESS = 'TCP://10.211.55.3:4022';
GO

Event Notification配置

Event Notification只需要在Deadlock Client Server创建即可,因为只需要在Deadlock Client上跟踪死锁事件。在为Deadlock Client 配置Service Broker章节,我们已经为Event Notification创建了队列、服务和路由。因此,在这里我们只需要创建Event Notification对象即可。方法参见如下的代码:

USE DDLCenter
GO

-- Create Event Notification for the deadlock_graph event.
IF EXISTS(
	SELECT * FROM sys.server_event_notifications  
	WHERE name = 'DeadLockNotificationEvent'
)
BEGIN
	DROP EVENT NOTIFICATION DeadLockNotificationEvent
	ON SERVER;
END
GO


CREATE EVENT NOTIFICATION DeadLockNotificationEvent
ON SERVER
WITH FAN_IN
FOR DEADLOCK_GRAPH
TO SERVICE 
'http://soa/deadlock/service/ClientService', 
'current database'
GO

模拟死锁

至此为止,所有对象和准备工作已经准备完成,万事俱备只欠东风,让我们在Deadlock Client实例上模拟死锁场景。首先,我们在Test数据库下创建两个测试表,表名分别为:dbo.test_deadlock1和dbo.test_deadlock2,代码如下:

IF DB_ID('Test') IS NULL
	CREATE DATABASE Test;
GO

USE Test
GO

-- create two test tables
IF OBJECT_ID('dbo.test_deadlock1','u') IS NOT NULL
    DROP TABLE dbo.test_deadlock1
GO

CREATE TABLE dbo.test_deadlock1(
id INT IDENTITY(1,1) not null PRIMARY KEY
,name VARCHAR(20) null
);

IF OBJECT_ID('dbo.test_deadlock2','u') IS NOT NULL
    DROP TABLE dbo.test_deadlock2
GO

CREATE TABLE dbo.test_deadlock2(
id INT IDENTITY(1,1) not null PRIMARY KEY
,name VARCHAR(20) null
);

INSERT INTO dbo.test_deadlock1
SELECT 'AA'
UNION ALL
SELECT 'BB';


INSERT INTO dbo.test_deadlock2
SELECT 'AA'
UNION ALL
SELECT 'BB';
GO

接下来,我们使用SSMS打开一个新的连接,我们假设叫session 1,执行如下语句:

--session 1
USE Test
GO

BEGIN TRAN 
UPDATE dbo.test_deadlock1
SET name = 'CC'
WHERE id = 1
;
WAITFOR DELAY '00:00:05'

UPDATE dbo.test_deadlock2
SET name = 'CC'
WHERE id = 1
;
ROLLBACK

紧接着,我们使用SSMS打开第二个连接,假设叫Session 2,执行下面的语句:

--session 2
USE Test
GO

BEGIN TRAN 
UPDATE dbo.test_deadlock2
SET name = 'CC'
WHERE id = 1
;

UPDATE dbo.test_deadlock1
SET name = 'CC'
WHERE id = 1
;
COMMIT

等待一会儿功夫以后,死锁发生,并且Session 2做为了死锁的牺牲品,我们会在Session 2的SSMS信息窗口中看到如下的死锁信息:

Msg 1205, Level 13, State 51, Line 8
Transaction (Process ID 60) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

用户查询死锁信息

根据上面的模拟死锁小节,说明死锁已经真真切切的发生了,那么,死锁信息到底有没有被捕获到呢?如果终端用户想要查看和分析所有客户端的死锁信息,只需要连接Deadlock Center SQL Server,执行下面的语句:

-- Run on Deadlock Center Server
USE DDLCenter
GO

SELECT * FROM [DDLCollector].[Deadlock_Info]

由于结果集宽度太宽,人为将查询结果分两段截图,第一段结果集展示如下:
03.png

第二段结果集截图如下:
04.png

从这个结果集,我们可以清楚的看到Deadlock Client发生死锁的详细信息,包含:

  • 死锁发生的Deadlock Client实例名称:CHERISH-PC

  • 被死锁进程号60,死锁进程57号

  • 死锁相关进程的事务开始时间,最后一个Batch开始执行时间和完成时间

  • 死锁进程执行的代码和Batch语句

  • 死锁发生时锁的类型

  • 表和索引名称

  • 死锁相关进程的登录用户

……
等等。

踩过的坑

当Deadlock Client 上SQL Server发生两次或者两次以上的Deadlock事件以后,自建的Event Notification对象(名为:DeadLockNotificationEvent)会被SQL Server系统自动删除,从而导致整个死锁收集系统无法工作。

表象

SQL Server在错误日志中会抛出如下4个错误信息:两个错误编号为17004,一个编号为17001的错误,最后是一个编号为17005错误,其中17005明确说明了,Event Notification对象被删除了。如下:

Error: 17004, Severity: 16, State: 1.
Event notification conversation on dialog handle '{4A6A0FBD-7A34-E711-A709-001C42099969}' closed without an error.
Error: 17004, Severity: 16, State: 1.
Event notification conversation on dialog handle '{476A0FBD-7A34-E711-A709-001C42099969}' closed without an error.
Error: 17001, Severity: 16, State: 1.
Failure to send an event notification instance of type 'DEADLOCK_GRAPH' on conversation handle '{F711A404-7934-E711-A709-001C42099969}'. Error Code = '8429'.
Error: 17005, Severity: 16, State: 1.
Event notification 'DeadLockNotificationEvent' in database 'master' dropped due to send time service broker errors. Check to ensure the conversation handle, service broker contract, and service specified in the event notification are active.

错误日志截图如下:
05.png

问题分析

从错误提示信息due to send time service broker errors来看,最开始花了很长时间来排查Service Broker方面的问题,在长达数小时的问题排查无果后,静下心来仔细想想:如果是Service Broker有问题的话,我们不可能完成第一、第二条死锁信息的收集,所以问题应该与Service Broker没有直接关系。于是,注意到了错误提示信息的后半部分Check to ensure the conversation handle, service broker contract, and service specified in the event notification are active,再次以可以成功收集两条deadlock错误信息为由,排除Contact和Service的问题可能性,所以最有可能出问题的地方猜测应该是conversation handle,继续排查与conversation handle相关操作的地方,发现存储过程[DDLCollector].[UP_ProcessDeadlockEventMsg]的中的代码:

...
ELSE IF @Message_Type = 'http://schemas.microsoft.com/SQL/Notifications/EventNotification'      
		BEGIN               
			-- Record message log first
			INSERT INTO  [DDLCollector].[Deadlock_Traced_Records](Processed_Msg, [Processed_Msg_CheckSum])         
			VALUES(@Message_Body, CHECKSUM(CAST(@Message_Body as NVARCHAR(MAX))))
		
			END CONVERSATION @handle

			--Here call another Store Procedure to send deadlock graph info to center server
			EXEC [DDLCollector].[UP_SendDeadlockMsg] @Message_Body;
		END
...

这个逻辑分支不应该有End Conversation的操作,因为这里是与Event Notification相关的Message Type操作,而不是Service Broker相关的Message Type操作。

解决问题

问题分析清楚了,解决方法就非常简单了,注释掉这条语句END CONVERSATION @handle后,重新创建存储过程。再多次模拟死锁操作,再也没有出现Event Notification被系统自动删除的情况了,说明这个问题已经被彻底解决,坑已经被填上了。
解决问题的代码修改和注释如下截图,以此纪念下踩过的这个坑:
06.png

福利发放

以下是关于SQL Server死锁相关的系列文章,可以帮助我们全面了解、分析和解决死锁问题,其中第一个是这篇文章的视频演示。

最后总结

这篇文章是一个完整的SQL Server死锁收集系统典型案例介绍,你甚至可以很轻松简单的将这个方案应用到你的产品环境,来收集产品环境所有SQL Server实例发生死锁的详细信息,并根据该系统收集到的场景来改进和改善死锁发生的概率,从而降低死应用发生异常错误的可能性。因此这篇文章有着非常重要的现实价值和意义。

PostgreSQL · 实现分析 · PostgreSQL 10.0 并行查询和外部表的结合

$
0
0

前言

大家都知道,PostgreSQL 近几大版本中加入了很多 OLAP 相关特性。9.6 的并行扫描应该算最大的相关特性。在今年发布的 10.0 中,并行扫描也在不断加强,新增了并行的索引扫描。

我们知道并行扫描是支持外部数据源的。在云上,有很多存储存储产品可以以外部数据源的形式做数据库的外部存储。例如,阿里云的 OSS 和 AWS 的 S3 都是绝佳的外部数据源。云上的 PostgreSQL 和他们的结合可以给用户提供既廉价又高性能数存储的方案。

另人欣喜的是,PostgreSQL 的外部表对外提供了可编程接口,并且支持并行扫描框架。利用它可以使 PostgreSQL 的外部数据源访问效率得到质的提升。

技术铺垫

并行查询

并行查询是 PostgreSQL 引入的一个大特性,它可以优化 SQL 语句的执行方式,从传统的单一进程,最多使用单个 CPU 运算的模式,提升到多进程,协同完成工作的模式。

并行查询消耗更多的硬件资源,大大提高了任务的执行效率。
在 PostgreSQL 中,一个 SQL 任务是否可以被并行化,可以通过查看 SQL 的执行计划(Plan)的方式看到。

例如:

EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%';
                                     QUERY PLAN                                      
-------------------------------------------------------------------------------------
 Gather  (cost=1000.00..217018.43 rows=1 width=97)
   Workers Planned: 2
   ->  Parallel Seq Scan on pgbench_accounts  (cost=0.00..216018.33 rows=1 width=97)
         Filter: (filler ~~ '%x%'::text)
(4 rows)

可以看到,上面的 SQL 采用了并行的方式执行,它使用了2个额外的并行工作进程(共3个进程)完成工作。

  1. 并行 worker 主要完成顺序扫描数据的和过滤数据的工作,符合条件的数据被发送给主进程。
  2. 主进程的 Gather 节点接受来自子进程的数据,再发给客户端。

并行查询的参数配置

合理的配置下列参数能让 PostgreSQL 成功开启并行查询特性。

  1. max_worker_processes 整个实例允许的最大并行工作进程,它的值建议和实例所在主机的逻辑 CPU 相关
  2. max_parallel_workers_per_gather 单个 Gather 节点的并行度,让单个 SQL 更快的执行,可以增大该参数的设置。
  3. force_parallel_mode 是否让查询优化器尽可能的选择并行的执行方式。

详细的参数描述在这

外部表

外部表是 PostgreSQL 引入外部数据的入口,任何的外部数据源都可以使用该接口把数据引入到数据库中。用户可以像访问表一样读写外部数据源上的数据。
目前 PostgreSQL 支持的常见外部数据源有 MySQL Oracle PostgreSQL OSS S3 等。
PostgreSQL 在引入并行查询时也支持了外部表的并行查询,并扩展了之前的编程接口。

并行的外部表扫描实现分析

SQL 语句执行一般过程

一条 SQL 语句的执行通常经历下面的过程:

1) 语法分析和语意分析
2) 查询优化
3) 查询执行

外部表的扫描在阶段 2 和 3 都有相应的操作

  1. 查询优化阶段,需要提供对应外部数据源的数据大小(行数和行宽度)等信息,用于优化器计算最优的查询路径
  2. 查询执行阶段,需要实现几个回调函数,用于向执行器(executor)输送以行(slot)为单位的数据,直到外部数据读取完成。

并行查询在传统模式上的变化

并行查询模式的引入,是 PostgreSQL 在传统的 Pipeline 模式上的较大改动。

大致的改进点如下,这部分也是外部表的并行查询模式实现所要注意的

一 查询优化阶段

提供给优化器并行模型的各类代价信息,参与优化器进行整体的代价评估。
当并行模式最终被确认为最优方案后,优化器会给出并行模式的执行计划。

二 查询执行器阶段

执行器得到一个带有并行执行节点的计划,还要进行如下工作

  1. 启动并行工作进程。
  2. 开辟相关共享内存结构,准备交换数据。
  3. 构造并行协作相关内存结构。
  4. 给工作进程下发并行相关的执行任务。
  5. 并行执行,并行工作进程拿到数据做相应的处理后发送给主进程的 Gather 节点,主进程的 Gather 节点拿到数据后返回给上层节点。直到所有数据处理完毕。
  6. 释放资源,处理事物信息。

上诉工作中框架相关的通用工作 PostgreSQL 已经完成,我们需要在并行框架下实现各阶段的部分逻辑。下面将会重点说明这部分细节。

外表的并行查询的实现分析

实现外部表的并行扫描需要注意上述环节中每个环节,下面描述概要设计

一: 查询优化阶段

  1. 实现 IsForeignScanParallelSafe ,返回 true。 标志该数据源可以并行化。

  2. 补充函数 GetForeignPaths 根据外数据的规模和可提供的并行工作进程数等信息提供给优化器可以行并行 Path。
    • 调用 create_foreignscan_path 创建可并行的外部表扫描节点 Path。
    • 调用 add_partial_path 把生成好的 Path 加入优化器 Path 备选队列。
  3. 补充函数 GetForeignPlan 创建可并行的外部表扫描节点 Plan。
    • 函数内部调用 make_foreignscan 根据输入的 Path 生成 Plan, 并向上返回。

二: 查询执行阶段

并行任务关键当然是把一个大的任务拆分成多个尽可能不相关的子任务,让这些子任务被并行的完成。

例如:

  • 1 对外部 MySQL 一张表 t 的读取,可以按照表 t 主键的值域把数据拆分成 N 部分,让并行 worker 分别读取其中一部分。

  • 2 对外部数据源 oss 一个目录 dir1 中多个文件中数据的读取,可以把这批文件均匀的分成 N 份,让并行 worker 分别完成其中的一部分。

如何合理的切分子任务,往往决定了最终的并行效果。合理的切分数据会使并行任务间尽量少的交互,最终任务完成耗时和并行工作进程数线性相关。

执行器的具体工作:

  1. 实现 EstimateDSMForeignScan 计算需求的共享内存大小。这部分内存将用户存放整个并行任务的相关信息。
    这部分流程主进程完成,即 Gather 节点完成。

  2. 实现 InitializeDSMForeignScan 分配共享内存,放入相关信息。
    我们把整个大任务拆分成一个子任务队列,并存入到共享内存中,初始化锁等信息。
    这部分流程也主进程完成,即 Gather 节点完成。

  3. 实现 InitializeWorkerForeignScan 并行 Worker 读取共享内存上的信息,获取子任务,准备正式开始工作。

  4. 数据的读写操作。
    这部分的实现尽量兼容传统模式的数据读取,或小幅调整。

  5. 实现 ShutdownForeignScan 数据扫描完成的后清理工作。

详细的 Foreign Data Wrapper 接口实现说明在这

并行外部表查询的应用

并行查询能大大提高数据的访问效率,他把外部数据源深度整合到 PostgreSQL 中。可以轻松的和本地数据一起做复杂的运算。同时,我们也能利用这套机制,实现高效的外部数据导入工作。

RocksDB · 特性介绍 · HashLinkList 内存表

$
0
0

RocksDB 内存表简介

RocksDB 是一个基于 LSM 树(Log-Structured Merge-tree)结构的单机数据库引擎,内存表是它最重要的数据结构之一。除了默认的跳表(SkipList)之外,它还增加了各种其他的内存表,例如:HashSkipList、HashLinkList、Vector 等。HashSkipList 是在跳表外套了一层 Hash,每个桶对应了一个跳表。类似的,HashLinkList 是在链表外套了一层 Hash,每个桶对应了一个链表。这两种内存表都需要配置 prefix_extractor,以计算 hash 值。RocksDB 支持的内存表类型见下图:

RocksDB 内存表

为了方便使用,内存表有对应的工厂类 MemTableRepFactory。与内存表类型对应,完整的内存表工厂类的继承关系如下图:

RocksDB 内存表工厂类

从内存表的基类 MemTableRep 中可以看到,它支持的 API 有 Get、Insert 等,但是没有 Delete。这是因为 Delete 被上层转换成了一个 Insert,真正的删除是在数据合并过程中做的。

HashLinkList 内存表

应用示例

相比 HashSkipList 而言,HashLinkList 要更简洁/简单一些,也可以节约内存的占用。它的缺点是不支持并发的写入。为了测试各种内存表的表现,RocksDB 提供了一个简单的测试工具,参看:memtablerep_bench.cc。我们也可以在示例代码 examples/simple_example.cc 中修改一下来验证 HashLinkList 的使用。例如:

$ git diff examples/simple_example.cc
diff --git a/examples/simple_example.cc b/examples/simple_example.cc
index 57d1b25..7b67ff0 100644
--- a/examples/simple_example.cc
+++ b/examples/simple_example.cc
@@ -9,6 +9,7 @@
 #include "rocksdb/db.h"
 #include "rocksdb/slice.h"
 #include "rocksdb/options.h"
+#include "rocksdb/slice_transform.h"
 
 using namespace rocksdb;
 
@@ -17,6 +18,9 @@ std::string kDBPath = "/tmp/rocksdb_simple_example";
 int main() {
   DB* db;
   Options options;
+  options.memtable_factory.reset(NewHashLinkListRepFactory(4, 0, 1, true, 4));
+  options.prefix_extractor.reset(NewFixedPrefixTransform(1));
+  options.allow_concurrent_memtable_write = false;
   // Optimize RocksDB. This is the easiest way to get RocksDB to perform well
   options.IncreaseParallelism();
   options.OptimizeLevelStyleCompaction();

需要注意的几个地方:
1. options.memtable_factory.reset() 是将默认的 SkipList 替换为我们要测试的 HashLinkList。
2. options.prefix_extractor.reset() 是设置 Hash 需要的 prefix_extractor,如果不设置,默认用的还会是 SkipList。
3. options.allow_concurrent_memtable_write = false 是因为 HashLinkList 不支持并发写入,不设置运行时会报错。
4. 数据存放在 /tmp/rocksdb_simple_example,如果数据目录已经存在,则会打开已经存在的数据库。

实现代码

HashLinkList 的代码存在以下两个文件中:memtable/hash_linklist_rep.h 以及 memtable/hash_linklist_rep.cc。可以看到它做了一些细节的优化,从简单的 Hash 链表逐步演化到 Hash 跳表。代码注释得非常清楚,参看下图:

HashLinkListDataStructure

由于存在上述的一些优化,Hash 表在不同时刻会存在不同的形式。

  1. 第一种情况最简单,也就是桶为空。不占用额外内存空间。
  2. 第二种情况下,桶里头只有一条记录,存在链表第一个节点中。Next 指针占用额外空间。Next 指针取值为 NULL。后续其他情况下 Next 指针都不是 NULL,可以依赖这一点来区分不同的场景。
  3. 第三种情况下,桶里头的记录多余一条,链表的第一个节点是个表头,记录链表中的记录数。记录数是为了判断链表是否应该转换为跳表而设的。额外空间包括这个表头节点以及每个节点中的 Next 指针。
  4. 第四种情况下,桶里头的记录数超过了设定的阈值(threshold_use_skiplist),链表被转换为一个跳表。链表的第一个节点的 Next 指针指向自己。Count 继续保留,可以用来做测试等。

如果 HashLinkList 要支持并发写,就需要对数据结构做适当的控制。不过当前它并不支持并发写,而是单写多读。它实现时采用了 C++ 11 的 Atomic 做一些特殊的处理避免加锁。另外需要注意的是,这个 Iterator 的实现 MemTableRep::Iterator* HashLinkListRep::GetIterator() 比较费资源,它会 new 一个 MemtableSkipList,把记录都遍历出来并插入进去。

采用修改后的 simple_example.cc,可以看到插入、查找、删除所对应的执行路径,用来理解代码的主要执行流程。

Put

(gdb) bt
#0  rocksdb::(anonymous namespace)::HashLinkListRep::Insert (this=0xcd2af0, handle=0xcf3250) at memtable/hash_linklist_rep.cc:580
#1  0x000000000044af7f in rocksdb::MemTable::Add (this=0xcf3000, s=1, type=rocksdb::kTypeValue, key=..., value=..., allow_concurrent=false, post_process_info=0x0) at db/memtable.cc:452
#2  0x00000000004a9850 in rocksdb::MemTableInserter::PutCF (this=0x7fffffffb550, column_family_id=0, key=..., value=...) at db/write_batch.cc:944
#3  0x00000000004a422e in rocksdb::WriteBatch::Iterate (this=0x7fffffffbb60, handler=0x7fffffffb550) at db/write_batch.cc:386
#4  0x00000000004a61f1 in rocksdb::WriteBatchInternal::InsertInto (writers=..., sequence=1, memtables=0xceb780, flush_scheduler=0xcf0780, ignore_missing_column_families=false, recovery_log_number=0, 
    db=0xcf0000, concurrent_memtable_writes=false) at db/write_batch.cc:1294
#5  0x0000000000609f2e in rocksdb::DBImpl::WriteImpl (this=0xcf0000, write_options=..., my_batch=0x7fffffffbb60, callback=0x0, log_used=0x0, log_ref=0, disable_memtable=false)
    at db/db_impl_write.cc:215
#6  0x00000000006093c2 in rocksdb::DBImpl::Write (this=0xcf0000, write_options=..., my_batch=0x7fffffffbb60) at db/db_impl_write.cc:48
#7  0x000000000060ca99 in rocksdb::DB::Put (this=0xcf0000, opt=..., column_family=0xcce9e0, key=..., value=...) at db/db_impl_write.cc:794
#8  0x0000000000609240 in rocksdb::DBImpl::Put (this=0xcf0000, o=..., column_family=0xcce9e0, key=..., val=...) at db/db_impl_write.cc:23
#9  0x00000000005f7ee7 in rocksdb::DB::Put (this=0xcf0000, options=..., key=..., value=...) at ./include/rocksdb/db.h:201
#10 0x00000000004082a0 in main () at simple_example.cc:40

Get

(gdb) bt
#0  rocksdb::(anonymous namespace)::HashLinkListRep::Get (this=0xcd2af0, k=..., callback_args=0x7fffffffb750, callback_func=0x44b82c <rocksdb::SaveValue(void*, char const*)>)
    at memtable/hash_linklist_rep.cc:727
#1  0x000000000044c1fc in rocksdb::MemTable::Get (this=0xcf3000, key=..., value=0x7fffffffc020, s=0x7fffffffc090, merge_context=0x7fffffffba60, range_del_agg=0x7fffffffb910, seq=0x7fffffffb898, 
    read_opts=...) at db/memtable.cc:678
#2  0x00000000005f9800 in rocksdb::MemTable::Get (this=0xcf3000, key=..., value=0x7fffffffc020, s=0x7fffffffc090, merge_context=0x7fffffffba60, range_del_agg=0x7fffffffb910, read_opts=...)
    at ./db/memtable.h:196
#3  0x00000000005e667a in rocksdb::DBImpl::GetImpl (this=0xcf0000, read_options=..., column_family=0xcce9e0, key=..., pinnable_val=0x7fffffffbba0, value_found=0x0) at db/db_impl.cc:959
#4  0x00000000005e6296 in rocksdb::DBImpl::Get (this=0xcf0000, read_options=..., column_family=0xcce9e0, key=..., value=0x7fffffffbba0) at db/db_impl.cc:905
#5  0x00000000005f811f in rocksdb::DB::Get (this=0xcf0000, options=..., column_family=0xcce9e0, key=..., value=0x7fffffffc020) at ./include/rocksdb/db.h:289
#6  0x00000000005f8233 in rocksdb::DB::Get (this=0xcf0000, options=..., key=..., value=0x7fffffffc020) at ./include/rocksdb/db.h:299
#7  0x0000000000408364 in main () at simple_example.cc:44

Delete

(gdb) bt
#0  rocksdb::(anonymous namespace)::HashLinkListRep::Insert (this=0xcd2af0, handle=0xcf3278) at memtable/hash_linklist_rep.cc:580
#1  0x000000000044af7f in rocksdb::MemTable::Add (this=0xcf3000, s=2, type=rocksdb::kTypeDeletion, key=..., value=..., allow_concurrent=false, post_process_info=0x0) at db/memtable.cc:452
#2  0x00000000004a9d9e in rocksdb::MemTableInserter::DeleteImpl (this=0x7fffffffb680, column_family_id=0, key=..., value=..., delete_type=rocksdb::kTypeDeletion) at db/write_batch.cc:999
#3  0x00000000004a9eb8 in rocksdb::MemTableInserter::DeleteCF (this=0x7fffffffb680, column_family_id=0, key=...) at db/write_batch.cc:1018
#4  0x00000000004a42db in rocksdb::WriteBatch::Iterate (this=0x7fffffffbc60, handler=0x7fffffffb680) at db/write_batch.cc:393
#5  0x00000000004a61f1 in rocksdb::WriteBatchInternal::InsertInto (writers=..., sequence=2, memtables=0xceb780, flush_scheduler=0xcf0780, ignore_missing_column_families=false, recovery_log_number=0, 
    db=0xcf0000, concurrent_memtable_writes=false) at db/write_batch.cc:1294
#6  0x0000000000609f2e in rocksdb::DBImpl::WriteImpl (this=0xcf0000, write_options=..., my_batch=0x7fffffffbc60, callback=0x0, log_used=0x0, log_ref=0, disable_memtable=false)
    at db/db_impl_write.cc:215
#7  0x00000000006093c2 in rocksdb::DBImpl::Write (this=0xcf0000, write_options=..., my_batch=0x7fffffffbc60) at db/db_impl_write.cc:48
#8  0x0000000000408480 in main () at simple_example.cc:53

MySQL · myrocks · fast data load

$
0
0

Fast data load

Load data相比普通insert效率更高,Load data批量插入数据有效减少了解析SQL的开销。MyRocks 同其他MySQL 引擎一样也支持Load data语法,同时MyRocks对data load也做了特殊优化。RocksDB引擎有一个规律是,数据最终会存储在最底层SST文件中,MyRocks通过参数rocksdb_bulk_load控制是否直接将数据存储在最底层SST文件中,而不走普通的insert流程。

先来看下普通insert流程(图片来自yoshinorim)

screenshot.png

优化后的bulk load流程(图片来自yoshinorim)

screenshot.png

由于SST文件中的数据必须是有序的,所以 bulk load特性有一个限制是插入的数据必须是按主键有序的。

Insert和Load data都支持bulk load特性,Load data文件中的数据容易保证有序,但对于非自增insert来说,要保证有序插入比较困难,因此bulk load特性对普通insert意义不大。

rocksdb_bulk_load设为1后,开启bulk load特性。值得注意的是,在 bulk load特性下,会默认忽略唯一性检查,同时rocksdb_commit_in_the_middle自动开启。

Bulk load 源码实现

  • step 1 第一次插入时会新建SST临时文件, 参见myrocks::Rdb_sst_info::open_new_sst_file
    文件形如:test.t1_PRIMARY_0_0.bulk_load.tmp
    db.tablename_indexname_count1_count2_.bulk_load.tmp
    其中count1每次都会原子自增,防止并发load时出现重名的情况。
    其中count2表示当前是第几个SST临时文件

  • step 2 随后插入都会直接插入到SST临时文件中,参见myrocks::Rdb_sst_info::put

  • step 3 SST临时文件写满或load结束,将SST临时文件copy或hard link为正式的SST文件,同时更新SST元数据信息,参考rocksdb::ExternalSstFileIngestionJob::Prepare/ExternalSstFileIngestionJob::Run

  • step 4 删除临时SST文件,参考ExternalSstFileIngestionJob::Cleanup

如果bulk load中途mysqld crash有可能残留SST临时文件,mysqld重启时会自动清理SST临时文件。参考Rdb_sst_info::init

Bulk load 相关测试

load data 测试

Bulk load下rocksdb load data比innodb快近3倍。
Bulk load下rocksdb load data比rocksdb 普通load data快近6倍。

screenshot.png

perf top

可以看出bulk load模式下,插入流程要简洁很多。

  • rocksdb without bulk load
    屏幕快照 2017-05-16 上午1.14.05.png

  • rocksdb with bulk load
    屏幕快照 2017-05-16 上午1.08.27.png

insert 测试

由于SQL解析占比重较大,bulk load模式下的insert优势并不明细。

screenshot.png

perf top

可以看出普通insert相比load data有更多的SQL解析操作(MySQLparse),同时非bulk load下的insert比bulk load下insert有更多的排序操作(KeyComparator)。

  • insert without bulk load
    屏幕快照 2017-05-16 上午12.45.54.png

  • insert with bulk load
    屏幕快照 2017-05-16 上午12.51.43.png

PgSQL · 应用案例 · "写入、共享、存储、计算"最佳实践

$
0
0

背景

数据是为业务服务的,业务方为了更加透彻的掌握业务本身或者使用该业务的群体,往往会收集,或者让应用埋点,收集更多的日志。

随着用户量、用户活跃度的增长,时间的积累等,数据产生的速度越来越快,数据堆积的量越来越大,数据的维度越来越多,数据类型越来越多,数据孤岛也越来越多。

日积月累,给企业IT带来诸多负担,IT成本不断增加,收益确不见得有多高。

pic

上图描绘了企业中可能存在的问题:

1. 数据孤岛问题严重(如果没有大数据平台时)。

2. 对成本预估不足,计算能力扩容麻烦,又或者铺张浪费严重。

3. 数据冗余问题突出。

4. 存储成本昂贵。

5. 业务萎缩后硬件成为固定资产,IT负担严重,几乎没有硬件伸缩能力。

6. 数据量太大,几乎无法备份。

7. 业务需求多,数据种类多,分析成本、开发成本高昂。

本文将针对这个场景,给出一个比较合理的方案,灵活使用,可以减轻企业IT成本,陪伴企业高速成长。

行业场景

### 1. 物流

一个包裹,从揽件、发货、运输、中转、配送到签收整个流程中会产生非常多的跟踪数据,每到一个节点,都会扫描一次记录包裹的状态信息。

运输过程中,车辆与包裹关联,车辆本身采集的轨迹、油耗、车辆状态、司机状态等信息。

配送过程,快递员的位置信息、包裹的配送信息都会被跟踪,也会产生大量的记录。

一个包裹在后台可能会产生上百条跟踪记录。

运输的车辆,一天可能产生上万的轨迹记录。

配送小哥,一天也可能产生上万条轨迹记录。

我曾经分享过一个物流配送动态规划的话题。有兴趣的童鞋也可以参考

《聊一聊双十一背后的技术 - 物流、动态路径规划》

物流行业产生的行为数据量已经达到了海量级别。

怎样才能有效的对这些数据进行处理呢?

比如:

实时按位置获取附近的快递员。

实时统计包裹的流量,快递员的调度,车辆的调度,仓库的选址等等一系列的需求。

2. 金融

金融行业也是数据的生产大户,用户的交易,企业的交易,证券数据等等。

数据量大,要求实时计算,要求有比较丰富的统计学分析函数等。

我曾经分享过一个关于模拟证券交易的系统需求分析。有兴趣的童鞋也可以参考

《PostgreSQL 证券行业数据库需求分析与应用》

3. 物联网

物联网产生的数据有时序属性,有流计算需求(例如到达阈值触发),有事后分析需求。

数据量庞大,有数据压缩需求。

我刚好也写过一些物联网应用的数据库特性分析,这些特性可以帮助物联网实现数据的压缩、流计算等需求。

《流计算风云再起 - PostgreSQL携PipelineDB力挺IoT》

《旋转门数据压缩算法在PostgreSQL中的实现 - 流式压缩在物联网、监控、传感器等场景的应用》

《PostgreSQL 物联网黑科技 - 瘦身几百倍的索引(BRIN index)》

《一个简单算法可以帮助物联网,金融 用户 节约98%的数据存储成本》

《”物联网”流式处理应用 - 用PostgreSQL实时处理(万亿每天)》

《PostgreSQL 黑科技 range 类型及 gist index 助力物联网(IoT)》

物联网还有一个特性,传感器上报的数据往往包括数字范围(例如温度范围)、地理位置、图片等信息,如何高效的存储,查询这些类型的数据呢?

4. 监控

监控行业,例如对业务状态的监控,对服务器状态的监控,对网络、存储等硬件状态的监控等。

监控行业具有比较强的业务背景,不同的垂直行业,对监控的需求也不一样,处理的数据类型也不一样。

例如某些行业可能需要对位置进行监控,如公车的轨迹,出了位置电子围栏,发出告警。换了司机驾驶,发出警告。等等。

pic

5. 公安

公安的数据来自多个领域,例如 通讯记录、出行记录、消费记录、摄像头拍摄、社交、购物记录 等等。

公安的数据量更加庞大,一个比较典型的场景是风险控制、抓捕嫌犯。涉及基于地理位置、时间维度的人物关系分析(图式搜索)。

如何才能满足这样的需求呢?

6. 其他行业

其他不再列举。

行业痛点

如何解决数据孤岛,打通数据共享渠道?

如何高效率的写入日志、行为轨迹、金融数据、轨迹数据等?

如何高效的实时处理数据,根据阈值告警通知,实时分析等?

如何解决大数据的容灾、备份问题?

如何解决大数据的压缩和效率问题?

如何解决数据多维度、类型多,计算复杂的问题?

如何解决企业IT架构弹性伸缩的问题?

总结起来几个关键字:

写入、共享、存储、计算。

方案

用到三个组件:

pic

1. RDS PostgreSQL

支持时序数据、块级索引、倒排索引、多核并行、JSON、数组存储、OSS_FDW外部读写等特性。

解决OLTP,GIS应用、复杂查询、时空数据处理、多维分析、冷热数据分离的问题。

2. HybridDB PostgreSQL

支持列存储、水平扩展、块级压缩、丰富的数据类型、机器学习库、PLPYTHON、PLJAVA、PLR编程、OSS_FDW外部读写等特性。

解决海量数据的计算问题。

3. OSS 对象存储

多个RDS实例之间,可以通过OSS_FDW共享数据。

OSS多副本、跨域复制。

解决数据孤岛、海量数据存储、跨机房容灾、海量数据备份等问题。

1 写入

pic

数据写入分为3条路径:

1. 在线实时写入,可以走RDS SQL接口,单个实例能达到 百万行/s 以上的写入速度。

2. 批量准实时写入,可以走HybridDB SQL接口,单个实例能达到 百万行/s 以上的写入速度。

3. 批量准实时写入,比如写文件,可以走OSS写入接口,带宽弹性伸缩。

2 共享

多个RDS实例之间,可以通过OSS_FDW共享数据。

例如A业务和B业务,使用了两个RDS数据库实例,但是它们有部分需求需要共享数据,传统的方法需要用到ETL,而现在,使用OSS_FDW就可以实现多实例的数据共享,而且效率非常高。

pic

通过RDS PostgreSQL OSS_FDW的并行读写功能(同一张表的文件,可以开多个worker process进程并行读写),共享数据的读写效率非常高。

pic

并行体现三个方面:OSS读写并行、RDS PostgreSQL多核计算并行、RDS PG或HybridDB的多机并行。

pic

3 存储

对于实时数据,使用RDS PostgreSQL, HybridDB的本地数据存储。对于需要分析、需要共享的数据,使用OSS进行存储。

OSS相比计算资源的存储更加的廉价,在确保灵活性的同时,降低了企业的IT成本。

通过OSS对象存储,解决了企业的数据冗余、成本高等问题,满足了数据的备份、容灾等需求。

4 计算

pic

通过RDS PostgreSQL, HybridDB, OSS的三个基本组件,实现了计算资源、存储资源的分离。

因为计算节点的数据量少了(大部分数据都存在OSS了),计算节点的扩容、缩容、容灾、备份都更加方便。

计算本身分为以下几种

1. 流式计算

流式计算分为两种,一种是实时统计,另一种是设置阈值进行实时的告警。

通过pipelinedb(base on postgresql)可以实现这两类流计算。

好处:

SQL标准接口,丰富的内置函数支持复杂的流计算需求,丰富的数据类型(包括GIS,JSON等)支持更多的流计算业务场景,异步消息通知机制支持第二类流计算需求。

pipelinedb正在进行插件化改造,以后可以作为PostgreSQL的插件使用。

https://github.com/pipelinedb/pipelinedb/issues?q=is%3Aissue+is%3Aopen+label%3A%22extension+refactor%22

pic

例如在监控领域,使用流计算的异步消息机制,可以避免传统主动问询监控的无用功问题。

pic

2. 实时交互业务

传统的OLTP需求,使用RDS PostgreSQL可以满足。

PostgreSQL的特性包括:GIS、JSON、数组、冷热分离、水平分库、K-V类型、多核并行、块级索引、倒排索引等。

PostgreSQL支持的场景包括:流计算、图式搜索、时序数据、路径规划、模糊查询、全文检索、相似查询、秒杀、基因、金融、化学、GIS应用、复杂查询、BI、多维分析、时空数据搜索等。

覆盖银行、保险、证券、物联网、互联网、游戏、天文、出行、电商、传统企业等行业。

3. 准实时分析

结合OSS对象存储,RDS PostgreSQL和HybridDB都可以实现准实时的分析。

同一份OSS的数据,也可以在多个实例之间进行共享,同时访问。

4. 离线分析、挖掘

结合OSS对象存储,RDS PostgreSQL和HybridDB都可以实现对离线数据的分析和挖掘。

RDS PostgreSQL 支持单机多核并行,HybridDB for PostgreSQL支持多机并行。用户可以根据计算量进行选择。

计算需要具备的能力

计算的灵魂是类型的支持、以及类型的处理。

1. PostgreSQL内置了丰富的类型支持,包括(数字、字符串、时间、布尔、枚举、数组、范围、GIS、全文检索、bytea、大对象、几何、比特、XML、UUID、JSON、复合类型等),同时支持用户自定义的类型。可以支持几乎所有的业务场景

2. 操作符,为了满足对数据的处理需求,PG对每一种支持的类型,都支持非常丰富的操作,

3. 内置函数,PG内置了丰富的统计学函数、三角函数、GIS处理函数,MADlib机器学习函数等。

4. 自定义计算逻辑,用户可以通过C, python, java, R等语言,定义数据的处理函数。扩展PostgreSQL, HybridDB for PostgreSQL的数据处理能力。

5. 聚合函数,内置了丰富的聚合函数,支持数据的统计。

6. 窗口查询功能的支持。

7. 递归查询的支持。

8. 多维分析语法的支持。

方案小结

1 RDS PostgreSQL 优势

主要体现在这几个方面

1. 性能

RDS PostgreSQL主要处理在线事务以及少量的准实时分析。

PG OLTP的性能可以参考这篇文档,性能区间属于商业数据库水准。

《数据库界的华山论剑 tpc.org》

PG 的OLAP分析能力,可以参考这篇文档,其多核并行,JIT,算子复用等特性,使得PG的OLAP能力相比其他RDBMS数据库有质的提升。

《分析加速引擎黑科技 - LLVM、列存、多核并行、算子复用 大联姻 - 一起来开启PostgreSQL的百宝箱》

PostgreSQL 10 在HTAP(OLTP与OLAP混合应用场景)方面还有更多的增强,预计社区5月份会发布BETA版本。

2. 功能

功能也是PostgreSQL的强项,在上一章《计算需要具备的能力》有详细介绍。

3. 扩展能力

计算能力扩展,RDS PostgreSQL的主要场景是OLTP在线事务,通过增加CPU,可以同时扩展OLTP的能力,以及扩展复杂计算的性能。

存储能力扩展,通过OSS存储以及oss_fdw插件,可以扩展RDS PG的存储能力,打破存储极限。

4. 成本

存储成本:由于大部分需要分离的数据都存储到OSS了,用户不再需要考虑这部分的容灾、备份问题。相比存储在数据库中,存储成本大幅降低。

开发成本:RDS PG, HybridDB PG都支持丰富的SQL标准接口,访问OSS中的数据(通过TABLE接口),使用的也是SQL标准接口。节省了大量的开发成本,

维护成本:使用云服务,运维成本几乎为0。

5. 覆盖行业

覆盖了银行、保险、证券、物联网、互联网、游戏、天文、出行、电商、传统企业等行业。

2 HybridDB PostgreSQL 优势

1. 性能

HybridDB PostgreSQL为MPP架构,计算能力出众。

2. 功能

在上一章《计算需要具备的能力》有详细介绍。

3. 扩展能力

计算能力扩展,通过增加计算节点数,可以扩展复杂计算的性能。

存储能力扩展,通过OSS存储以及oss_fdw插件,可以扩展RDS PG的存储能力,打破存储极限。

4. 成本

存储成本:由于大部分需要分离的数据都存储到OSS了,用户不再需要考虑这部分的容灾、备份问题。相比存储在数据库中,存储成本大幅降低。

开发成本:RDS PG, HybridDB PG都支持丰富的SQL标准接口,访问OSS中的数据(通过TABLE接口),使用的也是SQL标准接口。节省了大量的开发成本,

维护成本:使用云服务,运维成本几乎为0。

5. 覆盖行业

覆盖了银行、保险、证券、物联网、互联网、游戏、天文、出行、电商、传统企业等行业。

典型用法

pic

pic

云端数据库如何与海量存储结合?

《RDS PostgreSQL : 使用 oss_fdw 读写OSS对象存储》

《HybridDB PostgreSQL : 使用 oss_fdw 读写OSS对象存储》

MySQL · 源码分析 · Tokudb序列化和反序列化过程

$
0
0

序列化和写盘

Tokudb数据节点写盘主要是由后台线程异步完成的:

  • checkpoint线程:把cachetable(innodb术语buffer pool)中所有脏页写回
  • evictor线程:释放内存,如果victim节点是dirty的,需要先将数据写回。

数据在磁盘上是序列化过的,序列化的过程就是把一个数据结构转换成字节流。

写数据包括两个阶段:

  • 序列化:把结构化数据转成字节流
  • 压缩:对序列化好的数据进行压缩

tokudb序列化和压缩单位是partition,对于internal节点,就是把msg buffer序列化并压缩;对于leaf节点,就是把basement node序列化并压缩。

一个节点(node)在磁盘上是如何存储的呢?
节点数据在写盘时会被写到某个offset开始的位置,这个offset是从blocktable里面分配的一个空闲的空间。我们后面会专门写一篇有关btt(Block Translation Table)和block table的文章。
一个node的数据包含:header,pivot key和partition三部分:

  • header:节点meta信息
  • pivot key:记录了每个partition的key区间
  • partition:排序数据;一个node如果包含多个partition,这些partition是依次顺序存放的

有趣的是,压缩算法的信息是存放在partition压缩buffer的第一个字节。所以,tokudb支持FT索引内部同时使用多种压缩算法。

反序列化和读盘

Tokudb读盘的过程是在cachetable里通过调用get_and_pin系列函数实现

  • 前景线程调用get_and_pin系列函数
  • cleaner线程调用bring_node_fully_into_memory,这个函数调用pf_callback把不在内存中的那些partition读到内存。

数据从磁盘读到内存之前需要进行解压缩,然后对解压缩好的buffer进行反序列化,转换成内存数据结构。反序列化是使用序列化相反的方法把数据解析出来。

前面提过序列化和压缩的单位是partition,反序列化和解压缩的单位也是partition。

酱,节点数据就可以被FT层访问了。

序列化和压缩过程详解

这里顺便提一下BTT (Block Translation Table),这个表记录了节点(blocknum)在FT文件存储位置(offset)的映射关系。

为什么要引入这个表?Tokudb刷脏时,数据被写到一个新的空闲位置,避免了in-place update,简化recovery过程。

toku_ftnode_flush_callback是调用get_and_pin系列函数提供的flush_callback回调,checkpoint线程(也包含checkpoint thread pool的线程,在checkpoint过程中帮助前景线程做节点数据的回写)或evictor线程在这个函数里面会调用toku_serialize_ftnode_to做序列化和压缩工作。

toku_serialize_ftnode_to比较简单,首先调用toku_serialize_ftnode_to_memory执行序列化和压缩,然后调用blocktable.realloc_on_disk,为blocknum分配一个新的offset,最后调用pwrite把压缩的buffer写到盘上,回写完成清node->dirty标记。

这里单独说一下toku_serialize_ftnode_to_memory的第6个参数in_parallel,true表示并行处理序列化和压缩过程,false表示串行处理。

toku_ftnode_flush_callback通常是在evictor或者checkpoint线程上下文调用的,不影响前景线程服务客户端,这个参数一般是false,只有在loader场景下是true。

toku_serialize_ftnode_to (int fd, BLOCKNUM blocknum, FTNODE node, FTNODE_DISK_DATA* ndd, bool do_rebalancing, FT ft, bool for_checkpoint) {

    size_t n_to_write;
    size_t n_uncompressed_bytes;
    char *compressed_buf = nullptr;

    // because toku_serialize_ftnode_to is only called for
    // in toku_ftnode_flush_callback, we pass false
    // for in_parallel. The reasoning is that when we write
    // nodes to disk via toku_ftnode_flush_callback, we
    // assume that it is being done on a non-critical
    // background thread (probably for checkpointing), and therefore
    // should not hog CPU,
    //
    // Should the above facts change, we may want to revisit
    // passing false for in_parallel here
    //
    // alternatively, we could have made in_parallel a parameter
    // for toku_serialize_ftnode_to, but instead we did this.
    int r = toku_serialize_ftnode_to_memory(
        node,
        ndd,
        ft->h->basementnodesize,
        ft->h->compression_method,
        do_rebalancing,
        toku_drd_unsafe_fetch(&toku_serialize_in_parallel),
        &n_to_write,
        &n_uncompressed_bytes,
        &compressed_buf
        );
    if (r != 0) {
        return r;
    }

    // If the node has never been written, then write the whole buffer, including the zeros
    invariant(blocknum.b>=0);
    DISKOFF offset;

    // Dirties the ft
    ft->blocktable.realloc_on_disk(blocknum, n_to_write, &offset,
                                   ft, fd, for_checkpoint);

    tokutime_t t0 = toku_time_now();
    toku_os_full_pwrite(fd, compressed_buf, n_to_write, offset);
    tokutime_t t1 = toku_time_now();

    tokutime_t io_time = t1 - t0;
    toku_ft_status_update_flush_reason(node, n_uncompressed_bytes, n_to_write, io_time, for_checkpoint);

    toku_free(compressed_buf);
    node->dirty = 0;  // See #1957.   Must set the node to be clean after serializing it so that it doesn't get written again on the next checkpoint or eviction.
    return 0;
}

序列化和压缩过程是在toku_serialize_ftnode_to_memory实现,这个函数比较长,我们分成3段来看。

  • partition序列化和压缩
  • pivot key序列化和压缩
  • header序列化

partition序列化和压缩

toku_serialize_ftnode_to_memory的第5个参数do_rebalancing表示leaf节点在写回之前是否要做rebalance,这个参数是在toku_ftnode_flush_callback指定的,如果写回的是数据节点本身,那么是需要做rebalance的。

toku_serialize_ftnode_to_memory首先确保整个数据节点都在内存中,这么做是因为节点的partition数据是依次顺序存放的;然后根据do_rebalancing决定是否要对leaf节点做rebalance;接着是一大段内存分配:

  • sb包含节点partition压缩数据的数组,每个元素包含partition的uncompressed的buffer和compressed的buffer
  • ndd是指针数组,记录了每个partition压缩后数据的offset和size

这里有个小的优化,并没有为每个partition申请compressed的buffer,而是申请了一个足够大的buffer,每个partition使用其中的一段。uncompressed的buffer也是一样处理的。

足够大的buffer是什么意思呢?

  • uncompressed的buffer:各个partition的size总和。
  • compressed的buffer:压缩后的最大可能长度加上8个字节的overhead(每个partition压缩前的size和压缩后的size)

使用不同压缩算法,压缩之后的最大可能长度是不同的。

分配好buffer之后,调用serialize_and_compress_in_parallel或者serialize_and_compress_serially进行序列化和压缩。

int toku_serialize_ftnode_to_memory(FTNODE node,
                                    FTNODE_DISK_DATA* ndd,
                                    unsigned int basementnodesize,
                                    enum toku_compression_method compression_method,
                                    bool do_rebalancing,
                                    bool in_parallel, // for loader is true, for toku_ftnode_flush_callback, is false
                            /*out*/ size_t *n_bytes_to_write,
                            /*out*/ size_t *n_uncompressed_bytes,
                            /*out*/ char  **bytes_to_write)
// Effect: Writes out each child to a separate malloc'd buffer, then compresses
//   all of them, and writes the uncompressed header, to bytes_to_write,
//   which is malloc'd.
//
//   The resulting buffer is guaranteed to be 512-byte aligned and the total length is a multiple of 512 (so we pad with zeros at the end if needed).
//   512-byte padding is for O_DIRECT to work.
{
    toku_ftnode_assert_fully_in_memory(node);

    if (do_rebalancing && node->height == 0) {
        toku_ftnode_leaf_rebalance(node, basementnodesize);
    }
    const int npartitions = node->n_children;

    // Each partition represents a compressed sub block
    // For internal nodes, a sub block is a message buffer
    // For leaf nodes, a sub block is a basement node
    toku::scoped_calloc sb_buf(sizeof(struct sub_block) * npartitions);
    struct sub_block *sb = reinterpret_cast<struct sub_block *>(sb_buf.get());
    XREALLOC_N(npartitions, *ndd);

    //
    // First, let's serialize and compress the individual sub blocks
    //

    // determine how large our serialization and compression buffers need to be.
    size_t serialize_buf_size = 0, compression_buf_size = 0;
    for (int i = 0; i < node->n_children; i++) {
        sb[i].uncompressed_size = serialize_ftnode_partition_size(node, i);
        sb[i].compressed_size_bound = toku_compress_bound(compression_method, sb[i].uncompressed_size);
        serialize_buf_size += sb[i].uncompressed_size;
        compression_buf_size += sb[i].compressed_size_bound + 8; // add 8 extra bytes, 4 for compressed size, 4 for decompressed size
    }

    // give each sub block a base pointer to enough buffer space for serialization and compression
    toku::scoped_malloc serialize_buf(serialize_buf_size);
    toku::scoped_malloc compression_buf(compression_buf_size);
    for (size_t i = 0, uncompressed_offset = 0, compressed_offset = 0; i < (size_t) node->n_children; i++) {
        sb[i].uncompressed_ptr = reinterpret_cast<char *>(serialize_buf.get()) + uncompressed_offset;
        sb[i].compressed_ptr = reinterpret_cast<char *>(compression_buf.get()) + compressed_offset;
        uncompressed_offset += sb[i].uncompressed_size;
        compressed_offset += sb[i].compressed_size_bound + 8; // add 8 extra bytes, 4 for compressed size, 4 for decompressed size
        invariant(uncompressed_offset <= serialize_buf_size);
        invariant(compressed_offset <= compression_buf_size);
    }

    // do the actual serialization now that we have buffer space
    struct serialize_times st = { 0, 0 };
    if (in_parallel) {
        serialize_and_compress_in_parallel(node, npartitions, compression_method, sb, &st);
    } else {
        serialize_and_compress_serially(node, npartitions, compression_method, sb, &st);
    }

serialize_and_compress_serially就是串行调用serialize_and_compress_partition进行序列化和压缩。

static void
serialize_and_compress_serially(FTNODE node,
                                int npartitions,
                                enum toku_compression_method compression_method,
                                struct sub_block sb[],
                                struct serialize_times *st) {
    for (int i = 0; i < npartitions; i++) {
        serialize_and_compress_partition(node, i, compression_method, &sb[i], st);
    }
}

serialize_and_compress_in_parallel使用了threadpool来并行执行序列化和压缩,每个partition由一个专门的线程来处理。当前上下文也可以执行序列化和压缩,所以threadpool只创建了(npartitions-1)个线程。

threadpool线程执行的函数也是serialize_and_compress_partition;threadpool线程和当前上下文之间是使用work进行同步的。

static void *
serialize_and_compress_worker(void *arg) {
    struct workset *ws = (struct workset *) arg;
    while (1) {
        struct serialize_compress_work *w = (struct serialize_compress_work *) workset_get(ws);
        if (w == NULL)
            break;
        int i = w->i;
        serialize_and_compress_partition(w->node, i, w->compression_method, &w->sb[i], &w->st);
    }
    workset_release_ref(ws);
    return arg;
}

static void
serialize_and_compress_in_parallel(FTNODE node,
                                   int npartitions,
                                   enum toku_compression_method compression_method,
                                   struct sub_block sb[],
                                   struct serialize_times *st) {
    if (npartitions == 1) {
        serialize_and_compress_partition(node, 0, compression_method, &sb[0], st);
    } else {
        int T = num_cores;
        if (T > npartitions)
            T = npartitions;
        if (T > 0)
            T = T - 1;
        struct workset ws;
        ZERO_STRUCT(ws);
        workset_init(&ws);
        struct serialize_compress_work work[npartitions];
        workset_lock(&ws);
        for (int i = 0; i < npartitions; i++) {
            work[i] = (struct serialize_compress_work) { .base = ,
                                                         .node = node,
                                                         .i = i,
                                                         .compression_method = compression_method,
                                                         .sb = sb,
                                                         .st = { .serialize_time = 0, .compress_time = 0} };
            workset_put_locked(&ws, &work[i].base);
        }
        workset_unlock(&ws);
        toku_thread_pool_run(ft_pool, 0, &T, serialize_and_compress_worker, &ws);
        workset_add_ref(&ws, T);
        serialize_and_compress_worker(&ws);
        workset_join(&ws);
        workset_destroy(&ws);

        // gather up the statistics from each thread's work item
        for (int i = 0; i < npartitions; i++) {
            st->serialize_time += work[i].st.serialize_time;
            st->compress_time += work[i].st.compress_time;
        }
    }
}

pivot key序列化和压缩

回到toku_serialize_ftnode_to_memory,序列化partition之后就是序列化pivot key的过程。
sb_node_info存放pivot key压缩数据的信息:

  • uncompressed_ptr和uncompressed_size是未压缩数据的buffer和size
  • compressed_ptr和compressed_size_bound是压缩后数据的buffer和压缩后最大可能的size+8个字节的overhead(未压缩数据size和压缩后数据的size)

前面提到,压缩后的size是由压缩算法决定,不同的压缩算法压缩之后最大可能的size是不同的。

toku_serialize_ftnode_to_memory调用serialize_and_compress_sb_node_info把pivot key信息序列化并压缩。

pivot key的compressed buffer头8个字节分别存储pivot key的compressed size和uncompressed size,从第9个字节开始才是压缩的字节流;而checksum是针对整个compressed buffer做的。

    //
    // Now lets create a sub-block that has the common node information,
    // This does NOT include the header
    //
    // determine how large our serialization and copmression buffers need to be
    struct sub_block sb_node_info;
    sub_block_init(&sb_node_info);
    size_t sb_node_info_uncompressed_size = serialize_ftnode_info_size(node);
    size_t sb_node_info_compressed_size_bound = toku_compress_bound(compression_method, sb_node_info_uncompressed_size);
    toku::scoped_malloc sb_node_info_uncompressed_buf(sb_node_info_uncompressed_size);
    toku::scoped_malloc sb_node_info_compressed_buf(sb_node_info_compressed_size_bound + 8); // add 8 extra bytes, 4 for compressed size, 4 for decompressed size
    sb_node_info.uncompressed_size = sb_node_info_uncompressed_size;
    sb_node_info.uncompressed_ptr = sb_node_info_uncompressed_buf.get();
    sb_node_info.compressed_size_bound = sb_node_info_compressed_size_bound;
    sb_node_info.compressed_ptr = sb_node_info_compressed_buf.get();

    // do the actual serialization now that we have buffer space
    serialize_and_compress_sb_node_info(node, &sb_node_info, compression_method, &st);

    //
    // At this point, we have compressed each of our pieces into individual sub_blocks,
    // we can put the header and all the subblocks into a single buffer and return it.
    //

    // update the serialize times, ignore the header for simplicity. we captured all
    // of the partitions' serialize times so that's probably good enough.
    toku_ft_status_update_serialize_times(node, st.serialize_time, st.compress_time);

header序列化

序列化pivot key之后,toku_serialize_ftnode_to_memory计算节点node压缩前size和压缩后的size。
计算方法很简单:partition的size总和 + pivot key的size + header的size + 4个字节的overhead(pivot key的checksum)。

节点node压缩之后的size是为分配压缩后的数据buffer,为了支持direct I/O,分配的buffer和buffer size必须是512对齐的。

分配的buffer size记在n_bytes_to_write返回给调用函数;压缩之后的数据存储在bytes_to_write指向的buffer中。

节点node压缩之前的size,就是为了返回给调用函数,记在n_uncompressed_bytes参数中。

    // The total size of the node is:
    // size of header + disk size of the n+1 sub_block's created above
    uint32_t total_node_size = (serialize_node_header_size(node) // uncompressed header
                                 + sb_node_info.compressed_size   // compressed nodeinfo (without its checksum)
                                 + 4);                            // nodeinfo's checksum
    uint32_t total_uncompressed_size = (serialize_node_header_size(node) // uncompressed header
                                 + sb_node_info.uncompressed_size   // uncompressed nodeinfo (without its checksum)
                                 + 4);                            // nodeinfo's checksum
    // store the BP_SIZESs
    for (int i = 0; i < node->n_children; i++) {
        uint32_t len         = sb[i].compressed_size + 4; // data and checksum
        BP_SIZE (*ndd,i) = len;
        BP_START(*ndd,i) = total_node_size;
        total_node_size += sb[i].compressed_size + 4;
        total_uncompressed_size += sb[i].uncompressed_size + 4;
    }

    // now create the final serialized node
    uint32_t total_buffer_size = roundup_to_multiple(512, total_node_size); // make the buffer be 512 bytes.
    char *XMALLOC_N_ALIGNED(512, total_buffer_size, data);
    char *curr_ptr = data;

前面提到节点node序列化的过程分为3个阶段:

  • partition序列化和压缩
  • pivot key序列化和压缩
  • header序列化

前2个阶段都讨论过了,header的部分是调用serialize_node_header实现的。

到这里其他部分的序列化和压缩工作都做好了,header的序列化直接在前面分配好的压缩后数据buffer上进行,不需要压缩,也不必分配sub_block数据结构。

header处理完,直接把pivot key的sub_block的compressed_ptr数据和checksum拷贝过来。

pivot key处理完,直接把每个partition的compressed_ptr和checksum依次拷贝过来。

pad的部分写0。

    // write the header
    struct wbuf wb;
    wbuf_init(&wb, curr_ptr, serialize_node_header_size(node));
    serialize_node_header(node, *ndd, &wb);
    assert(wb.ndone == wb.size);
    curr_ptr += serialize_node_header_size(node);

    // now write sb_node_info
    memcpy(curr_ptr, sb_node_info.compressed_ptr, sb_node_info.compressed_size);
    curr_ptr += sb_node_info.compressed_size;
    // write the checksum
    *(uint32_t *)curr_ptr = toku_htod32(sb_node_info.xsum);
    curr_ptr += sizeof(sb_node_info.xsum);

    for (int i = 0; i < npartitions; i++) {
        memcpy(curr_ptr, sb[i].compressed_ptr, sb[i].compressed_size);
        curr_ptr += sb[i].compressed_size;
        // write the checksum
        *(uint32_t *)curr_ptr = toku_htod32(sb[i].xsum);
        curr_ptr += sizeof(sb[i].xsum);
    }
    // Zero the rest of the buffer
    memset(data + total_node_size, 0, total_buffer_size - total_node_size);

    assert(curr_ptr - data == total_node_size);
    *bytes_to_write = data;
    *n_bytes_to_write = total_buffer_size;
    *n_uncompressed_bytes = total_uncompressed_size;

    invariant(*n_bytes_to_write % 512 == 0);
    invariant(reinterpret_cast<unsigned long long>(*bytes_to_write) % 512 == 0);
    return 0;
}

假若一个node包含2个partition,它的序列化结构如下所示:

image.png

反序列化和解压缩过程详解

由于tokudb支持partial fetch(只读某几个partition)和partial evict(即把clean节点的部分partition释放掉),反序列化过程相比序列化过程略复杂一些。

fetch callback通过bfe这个hint告诉toku_deserialize_ftnode_from需要读那些partition。

bfe有五种类型:

  • ftnode_fetch_none:只需要读header和pivot key,不需要读任何partition。只用于optimizer计算cost
  • ftnode_fetch_keymatch:只需要读match某个key的partition,ydb层提供的一个接口,一般不用
  • ftnode_fetch_prefetch:prefetch时使用
  • ftnode_fetch_all:需要把所有partition读上来;写节点时使用(msg inject或者msg apply的子节点)
  • ftnode_fetch_subset:需要读若干个partition,FT search路径上使用。

只有在ft search高度>1以上的中间节点时,read_all_partitions会被设置成true,走老的代码路径deserialize_ftnode_from_fd,一次性把所有partition都读到内存中。

其他情况会调用read_ftnode_header_from_fd_into_rbuf_if_small_enough,把节点的header读到内存中,然后反序列化header并设置ndd(每个partition的offset和size);解压缩和反序列化pivot key设置pivot信息;根据bfe读取需要的partition。

节点的header,pivot key和partition都有自己的checksum信息,解析每个部分时都要确认checksum是匹配的。

enum ftnode_fetch_type {
    ftnode_fetch_none = 1, // no partitions needed.
    ftnode_fetch_subset, // some subset of partitions needed
    ftnode_fetch_prefetch, // this is part of a prefetch call
    ftnode_fetch_all, // every partition is needed
    ftnode_fetch_keymatch, // one child is needed if it holds both keys
};

int
toku_deserialize_ftnode_from (int fd,
                               BLOCKNUM blocknum,
                               uint32_t fullhash,
                               FTNODE *ftnode,
                               FTNODE_DISK_DATA* ndd,
                               ftnode_fetch_extra *bfe
    )
// Effect: Read a node in.  If possible, read just the header.
{
    int r = 0;
    struct rbuf rb = RBUF_INITIALIZER;

    // each function below takes the appropriate io/decompression/deserialize statistics

    if (!bfe->read_all_partitions) {
        read_ftnode_header_from_fd_into_rbuf_if_small_enough(fd, blocknum, bfe->ft, &rb, bfe);
        r = deserialize_ftnode_header_from_rbuf_if_small_enough(ftnode, ndd, blocknum, fullhash, bfe, &rb, fd);
    } else {
        // force us to do it the old way
        r = -1;
    }
    if (r != 0) {
        // Something went wrong, go back to doing it the old way.
        r = deserialize_ftnode_from_fd(fd, blocknum, fullhash, ftnode, ndd, bfe, NULL);
    }

    toku_free(rb.buf);
    return r;
}

deserialize_ftnode_header_from_rbuf_if_small_enough比较长,基本是toku_serialize_ftnode_to_memory的相反过程。

header部分是不压缩的,直接解析,比较magic number,解析node->n_children和ndd等。

然后比较header的checksum

    node->n_children = rbuf_int(rb);
    // Guaranteed to be have been able to read up to here.  If n_children
    // is too big, we may have a problem, so check that we won't overflow
    // while reading the partition locations.
    unsigned int nhsize;
    nhsize =  serialize_node_header_size(node); // we can do this because n_children is filled in.
    unsigned int needed_size;
    needed_size = nhsize + 12; // we need 12 more so that we can read the compressed block size information that follows for the nodeinfo.
    if (needed_size > rb->size) {
        r = toku_db_badformat();
        goto cleanup;
    }

    XMALLOC_N(node->n_children, node->bp);
    XMALLOC_N(node->n_children, *ndd);
    // read the partition locations
    for (int i=0; i<node->n_children; i++) {
        BP_START(*ndd,i) = rbuf_int(rb);
        BP_SIZE (*ndd,i) = rbuf_int(rb);
    }

    uint32_t checksum;
    checksum = toku_x1764_memory(rb->buf, rb->ndone);
    uint32_t stored_checksum;
    stored_checksum = rbuf_int(rb);
    if (stored_checksum != checksum) {
        dump_bad_block(rb->buf, rb->size);
        r = TOKUDB_BAD_CHECKSUM;
        goto cleanup;
    }

接着处理pivot key,比较pivot key部分的checksum,解压缩,反序列化,设置pivot信息。

    // Finish reading compressed the sub_block
    const void **cp;
    cp = (const void **) &sb_node_info.compressed_ptr;
    rbuf_literal_bytes(rb, cp, sb_node_info.compressed_size);
    sb_node_info.xsum = rbuf_int(rb);
    // let's check the checksum
    uint32_t actual_xsum;
    actual_xsum = toku_x1764_memory((char *)sb_node_info.compressed_ptr-8, 8+sb_node_info.compressed_size);
    if (sb_node_info.xsum != actual_xsum) {
        r = TOKUDB_BAD_CHECKSUM;
        goto cleanup;
    }

    // Now decompress the subblock
    {
        toku::scoped_malloc sb_node_info_buf(sb_node_info.uncompressed_size);
        sb_node_info.uncompressed_ptr = sb_node_info_buf.get();
        tokutime_t decompress_t0 = toku_time_now();
        toku_decompress(
            (Bytef *) sb_node_info.uncompressed_ptr,
            sb_node_info.uncompressed_size,
            (Bytef *) sb_node_info.compressed_ptr,
            sb_node_info.compressed_size
            );
        tokutime_t decompress_t1 = toku_time_now();
        decompress_time = decompress_t1 - decompress_t0;

        // at this point sb->uncompressed_ptr stores the serialized node info.
        r = deserialize_ftnode_info(&sb_node_info, node);
        if (r != 0) {
            goto cleanup;
        }
    }

最后是根据bfe读取需要的partition,读partition是通过调用pf_callback实现的。

    // Now we have the ftnode_info.  We have a bunch more stuff in the
    // rbuf, so we might be able to store the compressed data for some
    // objects.
    // We can proceed to deserialize the individual subblocks.

    // setup the memory of the partitions
    // for partitions being decompressed, create either message buffer or basement node
    // for partitions staying compressed, create sub_block
    setup_ftnode_partitions(node, bfe, false);

    // We must capture deserialize and decompression time before
    // the pf_callback, otherwise we would double-count.
    t1 = toku_time_now();
    deserialize_time = (t1 - t0) - decompress_time;

    // do partial fetch if necessary
    if (bfe->type != ftnode_fetch_none) {
        PAIR_ATTR attr;
        r = toku_ftnode_pf_callback(node, *ndd, bfe, fd, &attr, NULL);
        if (r != 0) {
            goto cleanup;
        }
    }

deserialize_ftnode_from_fd的部分留给读者自行分析。

PgSQL · 应用案例 · HTAP视角,数据与计算的生态融合

$
0
0

背景

随着技术的普及,越来越多以前需要很高的成本才能获取的数据,现在触手可及。

1. 点云(点的位置坐标+RGB+其他属性),以前只有军用领域在使用,比如《普罗米修斯》这部电影,通过一些小的飞行器(点云传感器设备)飞入未知的通道后,传回获取的点云数据,从而构建通道的全系影像。

pic

现在民用领域,也有很多点云的类似应用。例如:扫地机器人,无人车,消防(探测房屋结构),VR(通过点云数据构建全息影像)等等。

pic

2. 气象数据 (位置、日照、温度、雨量、风量等),气象数据往往是栅格类型的数据,一个栅格包含了一片区域的日照、温度、雨量、风量等数据,栅格可以切分和聚合。

气象数据的有非常多的用途,例如:

光伏电厂的选址,需要分析某区域某个时间段,日照数据统计。

多个栅格的数据聚合,或者一个栅格数据的部分截取等。比如一个包含了浙江省的栅格数据,如果只需要杭州市区的数据,那么可以在读取时将杭州的区域切分出来。

在时间维度上分析,在地理位置维度上分析,在其他属性维度分析,多个维度的分析。

生成时序动态图等。

历史栅格数据不断的积累,不停的上传新的数据使得历史数据越来越多。

pic

3. 地震数据(高频波,傅立叶变换),地震数据是一些包含了地理位置属性的XYZ三个方向的高频波形数据,收到数据后,需要对其进行快速的数据转换,预测和告警。

同时还需要对历史的数据进行挖掘。

pic

4. 天文数据(寻天,星系,轨迹比对),从古至今,人类一直没有停止对外太空的探索,天文台就是一个最为直接的探索外太空的设备。

有一个项目叫“寻天”,每天这些望远镜需要对天球坐标进行全方位的拍摄,拍摄的数据以栅格类型存入数据库,以备后续的分析。比如寻址超新星,寻找类太阳系等。其中寻找类太阳系就需要对单个栅格的多个历史数据进行比对,通过行星运行轨迹对光线造成的细微影响找出类太阳系的星体。

涉及到大量的时间、空间维度的运算。

pic

5. 室内定位(孤立坐标系、相对坐标系),实际上现在室内定位也非常的成熟了,例如你站在某个商场中,商场有若干个WIFI热点,只要你的手机开启了WIFI,那么通过3个WIFI热点与你的手机之间的信号强弱,就可以定位到你的位置。除了通过WIFI进行定位,还有磁场、声波、视觉等定位方法。定位后,数据以坐标+误差范围的形式存入数据库,这个坐标是相对坐标。

室内定位有什么商业用途呢?例如可以获取某个时间点的人群分布,哪个商场或者站台附近聚集了人群,进行营销效果的挖掘。

又比如,在时间+空间维度上,统计分析人流量,平均的驻留时间等。

pic

6. 室外定位(定位方法:GPS、基站信号强弱等),人群踩踏事件预测,非法聚众预测,事件预测,某个位置的人群驻足时间(广告效应报告)等。

pic

7. 其他,民用,军用

还有那些喜闻乐见的应用,o2o, 地图, 导航, 位置交友, 都带有很强的时间、空间、业务数据属性。

面向这么多的军用转民用技术,民用的软件技术有没有准备好?数据库有没有准备好接招呢?

一、时间、空间、业务数据 - 存储与计算的挑战

1. 数据类型越来越丰富,例如大多数业务基本上都会包含空间数据。

2. 大多数的数据具备时序属性,例如金融数据、物联网传感数据、气象数据、天文数据、地震监测数据等。

3. 数据查询维度(筛选条件)越来越多,(时间、空间、UID等),例如

在2017-01-01 ~ 2017-02-01这个月,某个点附近方圆30公里发生的事件。

在某个时间段,所有区域发生的事件。

在某个时间段,某个区域,某些用户发生的事件。

4. 数据的计算需求越来越复杂,参与计算的数据量越来越庞大,计算离数据太远导致传输效率浪费。

越来越多计算下推的需求。

5. 业务对数据计算的时效性越来越高,越来越多的计算被前置(如流计算,数据清洗等)。

6. 业务对数据深度学习的需求越来越多,而计算与数据的距离使得效率低下。

传统的存储与计算分离,使得整体的计算效率偏低。越来越多的计算前置、计算下推需求,来提升存储计算分离这种架构下的效率。

二、数据库如何对接行业Lib生态 - 提升开发、执行效率,降低成本

每个行业都有各自的特点,每个行业都有对行业理解深厚的ISV(地头蛇),每个行业都有各自的积累(开发框架、Lib库等)。

例如

在科学计算这个领域,有很多的python, R, go, julia语言相关的第三方库。这些行业第三方库是开发人员、科研人员对行业的理解与积累。(这些科学计算Lib库可能被广泛应用于气象预测、地震预测、金融等众多行业。)

如果这些Lib库可以与数据紧密的结合,大大的拉近了计算与数据的距离,直接提升计算效率并且降低了成本,开发人员一定会很高兴。

<Python常用科学计算相关外部库>

pic

以往是这样算(数据从数据库拉取到应用程序,应用程序再对其进行计算):

pic

现在是这样算(使用科学计算相关的Lib库,就在数据库里面算):

pic

数据库与程序开发语言、以及对应的LIB库打通,是一件很美妙的事情。

PostgreSQL的PL框架实现了这一点,目前已支持plcuda, plpython, plr, pljava, plperl, pltcl, C等非常多的内置编程语言,(通过接口,还可以支持更多的地球编程语言)。

PLpython用法举例

这个UDF用于获取文件系统的使用情况    
  
create or replace function get_fs_info() returns void as $$  
import os    
import statvfs  
phydevs = []    
f = open("/proc/filesystems", "r")    
for line in f:    
  if not line.startswith("nodev"):    
    phydevs.append(line.strip())    
  retlist = []    
f = open('/etc/mtab', "r")    
for line in f:    
  if line.startswith('none'):    
    continue    
  fields = line.split()    
  device = fields[0]    
  mountpoint = fields[1]    
  fstype = fields[2]    
  if fstype not in phydevs:    
    continue    
  if device == 'none':    
    device = ''    
  vfs=os.statvfs(mountpoint)  
  available=vfs[statvfs.F_BAVAIL]*vfs[statvfs.F_BSIZE]/(1024*1024*1024)  
  capacity=vfs[statvfs.F_BLOCKS]*vfs[statvfs.F_BSIZE]/(1024*1024*1024)  
  used=capacity-available  
  plpy.notice('mountpoint',mountpoint,'capacityGB',capacity,'usedGB',used,'availableGB',available)  
$$ language plpythonu;  

使用pl编程后,数据与计算水乳交融,效率大增。

pic

pic

pic

再通过 CPU多核并行、向量计算、JIT、GPU、FPGA等手段扩展单体计算能力。通过sharding、MPP等手段横向扩展。消灭瓶颈。

三、数据库如何搭乘硬件发展的快车

通常我们理解的计算单元就是CPU,然而随着技术的发展,越来越多专业的硬件,例如显卡计算单元GPU,例如可烧录,可编程的FPGA,还有随着AI火起来的面向机器学习的定制芯片TPU。

谷歌硬件工程师揭秘,TPU为何会比CPU、GPU快30倍?

老黄呕心之作,英伟达能凭借Tesla V100技压群雄吗?

深入理解 CPU 和异构计算芯片 GPU/FPGA/ASIC 1

深入理解 CPU 和异构计算芯片 GPU/FPGA/ASIC 2

那么数据库能否跟上这波硬件发展的浪潮呢,或者说如何抓住硬件发展的红利呢?

1 PostgreSQL如何与硬件整合

1. CPU

CPU的发展趋于缓慢,主要包括 :

扩展指令集,(如向量计算指令,已被PostgreSQL利用来加速OLAP数据分析场景,约有10倍的性能提升),例如

《PostgreSQL 向量化执行插件(瓦片式实现) 10x提速OLAP》

增加CPU计算单元,(例如PostgreSQL已支持多核并行计算,提升OLAP数据分析场景的性能,多核并行,一条SQL可以充分利用多个CPU核,缩短单条SQL的响应时间,特别适合OLAP业务),例如

《分析加速引擎黑科技 - LLVM、列存、多核并行、算子复用 大联姻 - 一起来开启PostgreSQL的百宝箱》

2. GPU

GPU与CPU的对比如下,GPU在核心数、FFLOPS、内存带宽方面,相比CPU有非常明显的优势。

pic

PostgreSQL通过pl/cuda语言接口,用户可以在数据库中直接使用GPU的计算能力。

pic

pl/cuda用法参考:

https://github.com/pg-strom/devel

3. FPGA

FPGA作为一种高性能、低功耗的可编程芯片,可以根据客户定制来做针对性的算法设计。所以在处理海量数据的时候,FPGA 相比于CPU 和GPU,优势在于:FPGA计算效率更高,FPGA更接近IO。

FPGA不采用指令和软件,是软硬件合一的器件。对FPGA进行编程要使用硬件描述语言,硬件描述语言描述的逻辑可以直接被编译为晶体管电路的组合。所以FPGA实际上直接用晶体管电路实现用户的算法,没有通过指令系统的翻译。

FPGA的英文缩写名翻译过来,全称是现场可编程逻辑门阵列,这个名称已经揭示了FPGA的功能,它就是一堆逻辑门电路的组合,可以编程,还可以重复编程。

PostgreSQL 社区,xilinx都有这方面的结合产品。

https://www.pgcon.org/2015/schedule/track/Hacking/799.en.html

4. TPU

在Google I/O 2016的主题演讲进入尾声时,Google的CEO皮采提到了一项他们这段时间在AI和机器学习上取得的成果,一款叫做Tensor Processing Unit(张量处理单元)的处理器,简称TPU。在大会上皮采只是介绍了这款TPU的一些性能指标,并在随后的博客中公布了一些使用场景:

Google一直坚信伟大的软件将在伟大的硬件的帮助下更加大放异彩,所以Google便在想,我们可不可以做出一款专用机机器学习算法的专用芯片,TPU便诞生了。

TPU的灵感来源于Google开源深度学习框架TensorFlow,所以目前TPU还是只在Google内部使用的一种芯片。

https://www.leiphone.com/news/201605/xAiOZEWgoTn7MxEx.html

2 小结

PostgreSQL以其扩展接口(pl/language, customscan, operator, type, index扩展),可以非常方便的对接以上各种硬件计算单元,让数据和计算紧密的结合,提高能效比。

通过利用指令集、多核计算对接CPU,通过PL/CUDA,customscan对接GPU,通过customscan对接FPGA,等等,一切都是为了提升计算能力。

四、数据库的发展

关系数据库发展了几十年,最核心的功能,依旧是支持可靠的数据存取、支持SQL接口。

随着社会的进步,数据库正在添加越来越多的功能,比如GIS就是其中之一。

为什么要将GIS功能添加到数据库中呢?在应用层实现不好吗?

这个问题很有意思,在应用层实现当然是可以的,但不是最好的。

举个例子,我们存储了一批用户、商铺的位置数据,要求某个用户周边的其他商铺,如果要在应用层实现这个功能,需要将位置数据都下载到程序端,然后计算距离,并输出周边的商铺。而用户请求的并发可能较高,请求的位置可能都不一样。在应用层实现这个功能,效率非常低下,因为每一次请求,都需要将数据载入应用层,同时需要计算每条记录的距离。印证了一句古话“远水解不了近渴”。

pic

在数据库层实现GIS这个功能遵循了两个宗旨:

1. 数据和计算在一起,每次请求不再需要move data,提升了整体效率。

2. 让数据库支持GIS类型和GIS索引,让每一次距离查询都可以通过索引检索,提升查询效率。

pic

可以看出,数据库的发展实际上也是遵循了以上原则,在保证数据库不会成为瓶颈的前提下,让整体的效率得以提升。

1 PostgreSQL 哪些手段解决瓶颈问题?

1. 提升计算能力

充分利用硬件的能力提升计算能力。例如结合 CPU指令、CPU多核协作、GPU、FPGA。。。

2. 提升开发效率

SQL标准的兼容性越好,开发用起来越爽。

支持的类型、function、索引越丰富,开发用起来越爽。

支持的编程接口越丰富,开发人员越爽,例如通过plpython对接PyPI,通过plR对接CRAN,通过plcuda对接GPU开发生态。

支持的开发框架越多,开发人员越爽。

3. 提升扩展能力

分为两个部分的扩展,一部分是计算能力的扩展,另一部分是开发能力的扩展。

扩展计算能力:

通过sharding,水平扩展节点,扩展整体性能。

通过MPP插件,扩展跨库计算能力。

扩展开发能力:

通过扩展接口(类型、索引、PL语言、UDF、解析器、执行器),支持更多的数据类型、索引类型、编程语言等。GIS就是其中一个例子,扩展了GIS类型、索引、UDF等等。

3.1 如何扩展数据类型?

https://www.postgresql.org/docs/10/static/xtypes.html

3.2 如何扩展索引?

https://www.postgresql.org/docs/10/static/xindex.html

https://www.postgresql.org/docs/10/static/gist.html

https://www.postgresql.org/docs/10/static/spgist.html

https://www.postgresql.org/docs/10/static/gin.html

https://www.postgresql.org/docs/10/static/brin.html

3.3 如何嫁接编程语言?

https://www.postgresql.org/docs/10/static/plhandler.html

3.4 如何扩展操作符?

https://www.postgresql.org/docs/10/static/xoper.html

3.5 如何扩展UDF?

https://www.postgresql.org/docs/10/static/xfunc.html

3.6 如何扩展外部数据接口?

https://www.postgresql.org/docs/10/static/fdwhandler.html

3.7 如何扩展聚合UDF?

https://www.postgresql.org/docs/10/static/xaggr.html

2 PostgreSQL 如何提升业务整体效率?

1. 计算与数据在一起,减少move data。

前面举的GIS的例子说明了一个问题,频繁的移动数据使得程序的效率低下,如果将计算与数据结合起来,可以大幅的提升效率。

3 PostgreSQL 如何融合行业Lib生态

1. 计算与数据在一起,减少move data。

PostgreSQL内置了许多函数、数据类型、索引类型(已超越ORACLE支持的范畴),可以满足大多数的业务场景需求。

如果内存的数据类型不能满足业务需求,可以通过类型扩展接口,扩展数据类型以及类型配套的操作符、函数、索引等。

如果内置的函数、操作符无法满足业务对数据处理的需求时,用户可以通过plpython, plr, plcuda, pljava, plperl, pltcl等数据库过程语言,不仅扩展了编程能力,同时还对接了编程语言生态。

例如PyPI, CRAN等库,在数据库中完成对数据的一站式处理。

这个章节描写了如何扩展PostgreSQL:类型、函数、操作符、索引、聚合等。

https://www.postgresql.org/docs/10/static/extend.html

2. SQL接口流计算

pipelinedb是基于PostgreSQL的一个流计算数据库,1.0版本将支持插件化,PostgreSQL用户可以通过安装插件的方式,支持流计算的功能。

SQL流计算有诸多好处,数据库的SQL接口非常成熟,支持非常成熟的统计分析函数,统计分析语法。建立流的过程非常简单。

《(流式、lambda、触发器)实时处理大比拼 - 物联网(IoT)\金融,时序处理最佳实践》

《流计算风云再起 - PostgreSQL携PipelineDB力挺IoT》

SQL接口的流计算,使用便捷,开发成本低,启动成本低,扩展能力强,效率高。

除此之外,PostgreSQL还整合了CPU\GPU\FPGA等计算能力,整合了PL编程接口,流式处理的能力更加的强大。

比如气象类应用,大量的用到了GIS + 科学计算(plpython)+ 流式计算 + GPU (pl cuda)的处理能力。使用PostgreSQL就非常的恰当。

《PostgreSQL 支持CUDA编程 pl/cuda》

《PostgreSQL 点云应用》

五、小结

对企业来说,数据和计算是两个不可分割的部分。

经历了几十年的发展,数据库在数据的可靠存取、业务连续性方面成就卓越,企业也非常相信数据库这方面的能力,通常会将数据都存入数据库中。

同时企业对数据的计算需求也在膨胀,从最初的简单计算,到现在越来越复杂的计算需求。计算的需求分为两个部分,1、运算能力,2、编程能力。

1. 数据库在运算方面的能力也在逐渐提高,但是在兼顾数据可靠性的前提下,弹性提升运算能力没有想象中容易,大多数的关系数据库仅仅依赖 CPU\硬盘 等本地硬件能力的提升,运算能力提升非常有限,企业也不能等待数据库在这方面的提升。

2. 数据库在编程能力方面,有几种提升手段,一种是扩展SQL语法,支持更多的数据类型、函数、索引等。另一种是语言的支持,通常数据库会内置存储过程语言,例如Oracle的PL/SQL,PostgreSQL的plpgsql,但是这些语言的编程能力有限。

所以市场中衍生出适合各种场景的数据库或框架,以牺牲”并发能力、数据可靠性、一致性、易用性、事务、功能等”的某些部分为代价。例如 时序数据库、流计算数据库、NOSQL、大数据框架、分布式数据库 等等。

那么关系数据库到底还能不能提升计算能力呢?

实际上还是和数据库本身的框架有关,PostgreSQL的框架特别有意思,开放了众多的接口,在保证数据库核心功能不妥协的前提下,允许对其进行扩展。包括:

数据库服务端编程语言(PLpython, java, perl, R, …)、类型、函数、操作符、索引、聚合、外部存储、customScan等。

数据库的未来 - HTAP

Hybrid Transactional/Analytical Processing (HTAP)是gartner提出的一个新名词,代表一种既能处理在线事务,又能处理分析型请求的混合数据库。

https://en.wikipedia.org/wiki/Hybrid_Transactional/Analytical_Processing_(HTAP)

pic

比如在物联网的边缘计算场景,就非常的适合,成本低,效率高,一体成型。

pic

要实现HTAP,必须打通数据、计算的任督二脉。PostgreSQL在这方面具有天然的优势,从这几年的发展也能看出端倪。

1. 通过PL(数据库内置编程语言(PLpython, java, perl, R, …))对接行业生态,让开发者积累的Lib得以传承。

2. 通过扩展接口对接硬件生态,让CPU,GPU,FPGA,TPU,ASIC等参与垂直的专业计算,提升效率,打破传统的CPU ONLY的模式。

3. 通过流实现计算前置,解决数据的实时计算需求。

4. 通过FDW接口,存储接口将计算下推,让更多具备运算能力的单元参与运算,避免集中式运算的局面。提升大数据量的处理能力。

其中的代表包括postgres_fdw, 阿里云的oss_fdw。

5. 通过sharding技术实现数据库的水平扩展。

6. 通过MPP提升大规模计算协作能力。

7. BSD-like许可,已经有非常多的企业以PostgreSQL为基础打造了更多的衍生产生,免去重复造轮子的过程。

8. 扩展类型、函数、操作符、索引接口,对接垂直行业生态。

PostGIS, 基因类型, 化学类型, 图像特征类型, 全文检索等插件,就是非常典型的例子。支持更多的垂直行业应用。

9. 当数据库可以无限扩展,具备强大的计算能力时,它已然不是一个传统的只能存取数据的数据库,而是一个提供了编程能力、计算能力、扩展能力的数据平台(或数据工厂),提升数据的使用效率、节约成本。

10. 即使数据库可以无限扩展,还有一点需要注意,资源的控制。特别是开放了pl之后,用户写的代码可能把资源用尽。一个比较有效的资源调度:当系统有足够的空闲资源时放开用,当系统资源不足时,按权重调度分配资源的使用。

PostgreSQL是进程模型,这方面可以结合docker, cgroup等手段实现资源的控制。

pic

六、参考

http://postgis.net/docs/manual-dev/

https://2016.foss4g-na.org/sites/default/files/slides/gbroccolo_FOSS4GNA2016_pointcloud_0.pdf

https://www.slideshare.net/kaigai/pgconfsv2016-plcuda/

https://github.com/pg-strom/devel

http://www.pgconfsv.com/program/schedule

http://kaigai.hatenablog.com/entry/2016/11/17/070708

http://www.pgconfsv.com/plcuda-fusion-hpc-grade-power-database-analytics-0

http://www.pgconf.asia/JP/wp-content/uploads/2016/12/20161203_PGconf.ASIA_PLCUDA.pdf

http://gohom.win/2015/08/10/python-good-lib/

《PostgreSQL 数据库扩展语言编程 之 plpgsql - 1》

http://it.sohu.com/20170525/n494441009.shtml

https://www.leiphone.com/news/201704/55UjF0lafhIZVGJR.html

MySQL · 引擎特性 · 从节点可更新机制

$
0
0

背景

主从集群,指由一个主数据库实例和多个从数据库实例组成,其中主数据库实例提供读写功能支持,而从数据库不提供对外服务或只提供只读功能支持,但也有从数据库提供读写功能支持,下面就这几种集群架构做详细的解读,并就如何实现从节点可更新机制进行探讨。

主从集群概述

主从集群的实现方式主要有以下几种:

  • 基于磁盘镜像的主备集群
  • 基于Proxy中间件的主从(多主)集群
  • 基于共享磁盘的主从集群
  • 基于日志重放(物理日志或逻辑日志)的主从集群

一、基于磁盘镜像的主备集群

基于磁盘镜像的主备集群是最早出现的一种高可用解决方案,对数据库系统没有特殊要求,不需要额外的功能支持,利用原有单机数据库系统即可搭建,主要利用磁盘镜像来实现主备之间数据的同步。

原理是利用磁盘镜像的机制,当主机数据更新后,在写入磁盘的同时将数据同步到备机的磁盘上,主机缓冲区中的数据并不能同步到备机。当主机down机后,除了缓冲区数据未同步之外,磁盘上的数据还可能因为部分写而导致数据错误。所谓部分写是由于IO的特性导致的。

通常磁盘IO以扇区为单位一次性读写,但真正的数据页通常大于一个扇区(512字节),因此在down机时有可能只写了数据页的一部分,如一个数据页面大小为16K,down机时只写了此数据页的2K。后面会专门介绍数据库解决部分写的方法。

它的缺点也比较明显,主备机实例不能同时启动,只有当主机down机后,备机启动,并执行恢复操作,然后提供对外服务。因此,切换时间较长,而且备机的资源无法充分利用,适用于主机硬件损坏导致无法恢复的场景,也可用于计划检修等场景。

二、基于Proxy中间件的主从(多主)集群

基于proxy中间件可以搭建多主或主从集群,主要原理是利用proxy代理对应用请求进行分类、转发,当是写请求时,将写请求转发到集群中的每一台服务器;当是读请求时,将读请求转发到任意一台服务器即可。

基于proxy中间件搭建的集群,在数据库层面并不保证多机之间的数据一致性,而是由proxy来间接实现一致性,proxy通过检查发给所有服务器的写请求的执行结果,当任一服务器执行失败,就反向执行写请求,以保证多节点之间数据的一致性,这种一致性只是弱一致性,在数据库中也称为事务补偿机制。此外,也可以利用数据库本身提供的XA协议(如果数据库支持的话),但为了提高一致性,会损失性能和可用性。

它的主要缺点是无法保证事务的强一致性,当一个节点down机后,其它节点后续的写请求无法在down机服务器上执行,会造成down机服务器的数据丢失。

三、基于共享磁盘的主从集群

基于共享磁盘的主从集群是目前比较常见的集群架构之一,原理是通过主从服务器读取共享磁盘的数据来进行数据同步。因为数据库服务器的缓存,所以除了从共享磁盘读取数据之外,还要进行日志的内存重放,以更新缓存的数据。

共享磁盘架构的主要问题是缓存的同步,当主机提交事务时,同时将日志发到从机,或将日志刷写到磁盘后通知从机读取,然后从机重放日志,更新缓冲区的数据,但从机禁止将缓冲区数据写回到磁盘,只有主机可以将数据写回到磁盘。另外一个问题是部分写的问题,后面会专门讨论。

根据主机提交事务的时机,可以实现异步、近同步、实时同步等不同模式。异步指主机提交事务时,将日志发给从机,但不关心是否发送成功即完成事务提交。因为有网络延迟、重放日志延迟等,主从之间的数据并不是实时同步的。通常日志发送采用专门的进(线)程来实现,从机的数据有可能delay主机很久,这要实际的生产环境有很大关系。

近同步指从机收到日志即向主机返回成功,主机即可完成事务提交,但此时从机尚未日志重放,会有稍许日志重放的延迟。

共享磁盘架构的主要的问题在于必须有集群文件系统的支持,依赖于集群文件系统来保证节点间的数据一致性,并且在数据库层面也要有分布式锁来防止写写冲突和读写冲突。

四、基于日志重放(物理日志或逻辑日志)的主从集群

基于日志重放的主从集群是最常见的集群构架,并且也有很多独立于数据库的产品。日志大体有两种,一种是逻辑日志,一种是物理日志。物理日志通常都比较小,易于保存和传输,但重放的代价比较大,对从机资源的消耗也比较大,mysql的主从集群就是典型的以逻辑日志为基础的主从集群。与之相反的是物理日志,通常物理日志都比较大,但物理日志重放的代价都比较小,对从机资源的消耗也比较小,但对网络资源的消耗比较大。

逻辑日志重放代价上最有利的场景一个应用场景就是全表更新;而物理日志重放代价上最有利的场景更新全表扫描中的一条记录。因此,不能简单的说,哪种日志更好,必须以实际的应用场景来具体分析。

数据库系统中解决部分写的常见方法

在数据库系统中,数据通常以固定大小的页面来保存,页面大小也通常是512字节的整数倍,512字节是通常磁盘一个扇区的大小,扇区是一次磁盘IO的最小单位。SSD盘通常扇区大小为4K。在数据库系统中,一般情况下,数据页面的大小都会远大于512字节,随着大数据的到来,8K,16K页面已经很常见。当数据页面比较大时,要将一个数据页面刷写到磁盘上就需要多次IO,但这多次IO之间并不是原子操作,在执行过程中很可能因为各种原因导致中断,如断电,磁盘损坏等,造成的后果就是磁盘上的数据页面前面部分已经更新,后半部分仍是旧的数据,也就是部分写。出现部分写后的数据页是无法使用的,典型的牛唇不对马嘴。使用raid条带化也可能会导致同样的问题。

在数据库系统实现过程中,必须如何发现并解决这个问题。首先我们来说一说单机系统中的解决方法。

首先数据库要能检测到这个问题,实现的方法也比较简单,在页面头上记录一个标志,如时间戳,然后在页面尾上也记录一个相同的标志,当读取数据页面后,只要检查一下标志是否相同即可证明页面是否存在部分写。需要注意的是每次页面更新后,标志也必须同时进行修改,不能使用原有页面的标志。还有一些代价更大的方法,如生成整个页面的摘要,然后记录到页头中,除了可以检查页面的部分写之外,还能防止页面被恶意篡改,但这对系统资源的消耗比较大。一种热衷的办法是计算部分页面数据的摘要,但要包含页面头和页面尾,这样就可以能检测页面的部分写,也能部分达到防止篡改的目的。

在实现页面部分写检测的功能之后,我们还要解决如何这个问题,我们的目的是能得到正确的数据,而不只是发现错误。当数据库发现页面错误之后,可以采用的办法是通过日志重放来恢复数据。如果要从系统最初的状态重头开始进行日志重放,那么代价太大,或者有些场景下就是不可能实现的。为了加速这个过程,数据库引用checkpoint来解决这个问题。当checkpoint时,当前所有缓冲区中被修改的页面都要被刷写到磁盘上,执行成功后,记录checkpoint日志,以后就可以从这个点进行恢复。这与部分写有什么关系呢?数据库在更新每个缓冲区的数据后,都会记录日志,日志内容记录了当前修改的内容,最重要的一点是在记录日志时,如果发现当前的缓冲区是在checkpoint之后的首次修改时,会做一个当前页面的快照,并将其记录到磁盘上,在postgresql中是将其记录到redo日志中,另外一些数据库是记录到一个专门的日志中,如mysql的double write。当这个缓冲区被多次修改后,若在将其刷写到磁盘的过程中出现的断机等异常情况,造成部分写后,系统重新启动读取页面时发现页面有部分写,那么会将之前记录到磁盘的快照覆盖出现部分写的数据页面,然后在此基础上进行日志重放,从而达到恢复部分写的结果。

在基于共享磁盘的主从集群中也会存在相同的问题,有可能是主要写了一半,而从机此时来读取数据,就会读取到错误的数据。利用前面时间戳或摘要的方法,可以轻易解决检测部分写的问题。如何读到正确的数据呢?一种简单易用的方法就是重读,当检测到数据页面错误时,重试几次就好了,就能解决绝大多数的问题了,如果总是读取不正确,就要考虑是不是磁盘坏掉了。报错给DBA也是一个不错的选择。如果非要数据库来解决了,也是可以的,同样采用在快照上重放日志就可以实现数据的恢复,但代价比较大,运行过程中进行数据页恢复的设计也比较复杂。

从节点可更新机制

通常在主从集群中,主节点是可以读写的,而从节点通常只能提供只读功能,能不能实现在从节点也可以读写支持呢?当然可以,常见的方法有如下几种:

  • SQL转发
  • 全局锁
  • 主机延迟裁定

1.SQL转发

从节点接受应用请求后,经过词法语法分析后,若发现是更新类的操作,如DML,DDL等,将其转发给主节点,再将主节点执行后的返回结果转发回给应用程序。从节点本身实际并不真正执行更新操作,它依赖于主从之间的同步机制来更新本地缓存的数据。若不是实时同步更新,可能会导致应用程序在从节点更新,又在从节点读取到更新前的脏数据(历史数据)。若是更新较多的应用场景,从节点并不能承担分流的任务,主节点的负载不会有明显降低。

2.全局锁

如果集群实现了全局锁,从节点就可以真正在本地更新数据,但需要有额外的同步机制将从节点的数据同步到主节点。全局锁的实现可以使用集中式锁管理,也可以使用分布式锁管理,可以根据需要和应用场景来选择实现。
全局锁虽然解决了从节点的更新问题,但会对整个系统的性能造成较大影响,因为每次对数据页面的访问都要加全局锁,增加了大量额外的网络开销。并且还要实现从节点到主节点的数据同步机制。

3.主机延迟裁定

从节点收到应用程序的更新请求后,直接在备机执行syntax解析、SQL优化、执行等,在提交之前,向主节点发送裁定请求,以判定此写操作是否可以提交。向主节点发送的内容至少应包括更新前的数据快照、修改后的数据等,主节点收到裁定请求,比较更新前的数据快照是否与当前的数据相同,若相同则说明在从机修改数据之前此份数据没有被修改过,是在最新数据版本上进行的更新,则将更新的内容应用到主节点,如果需要记录日志,同时将日志刷写到磁盘。如果更新前的数据快照与当前的数据不相同,则说明在从机修改数据之前,主机已经修改过数据,但尚未同步到从机,因此必须回滚事务。

PgSQL · 特性分析 · 数据库崩溃恢复(下)

$
0
0

背景

在上期月报PgSQL · 特性分析 · 数据库崩溃恢复(上),我们分析了PostgreSQL在数据库非正常退出后(包括通过recovery.conf用户主动恢复)的处理,概括起来分为以下几步:

  1. 如果满足以下条件之一,则进行非正常退出恢复
    • pg_control文件中的数据库状态不正常(非DB_SHUTDOWNED)
    • pg_control文件中记录的最新检查点读取不到XLOG日志文件
    • 用户指定recovery.conf文件主动恢复
  2. 根据pg_control、backup_label确定恢复的XLOG日志记录起点
  3. 不断读取每个XLOG record,根据所属的资源管理器去调用各自:
    • rm_startup()
    • rm_redo(EndRecPtr, record)
    • rm_cleanup();
  4. 不断恢复XLOG日志记录,直到满足以下条件之一,则停止恢复正常启动
    • 达到recovery.conf文件中规定的最终的目标恢复位点
    • 当前XLOG日志全部应用完成
  5. 清理环境,并启动需要的辅助进程

其中,PostgreSQL会根据每个XLOG record所属的资源管理器操作来执行对应的函数。PostgreSQL中有以下的资源管理器:

RM_XLOG_ID
RM_XACT_ID
RM_SMGR_ID
RM_CLOG_ID
RM_DBASE_ID
RM_TBLSPC_ID
RM_MULTIXACT_ID
RM_RELMAP_ID
RM_STANDBY_ID
RM_HEAP2_ID
RM_HEAP_ID
RM_BTREE_ID
RM_HASH_ID
RM_GIN_ID
RM_GIST_ID
RM_SEQ_ID
RM_SPGIST_ID

而每种资源管理器,都存在几种具体的操作,例如RM_HEAP_ID包括以下操作(后面的数字代表对应XLogRecord中xl_info字段高4位,详情参考上期月报):

#define XLOG_HEAP_INSERT		0x00
#define XLOG_HEAP_DELETE		0x10
#define XLOG_HEAP_UPDATE		0x20
/* 0x030 is free, was XLOG_HEAP_MOVE */
#define XLOG_HEAP_HOT_UPDATE	0x40
#define XLOG_HEAP_NEWPAGE		0x50
#define XLOG_HEAP_LOCK			0x60
#define XLOG_HEAP_INPLACE		0x70

下面我们将以堆表的INSERT操作为例,具体分析对应的XLOG record内容和资源管理器对应的处理函数。

XLOG record

XLOG record 结构

PgSQL · 特性分析 · 数据库崩溃恢复(上)中,分析了WAL日志页的基本结构。其中,一条日志记录的的组织形式如下所示(以下分析基于RDS for PostgreSQL,即9.4版本):

 *		Fixed-size header (XLogRecord struct) /*日志记录头*/
 *		rmgr-specific data /*资源管理器数据,对应操作的对象,譬如元组的id等内容*/
 *		BkpBlock/*备份块块头*/
 *		backup block data/*操作的一些数据,如更新元组时,要更新的新值存储在这个区域*/
 *		BkpBlock
 *		backup block data
 *		...

为了更好地探究堆表INSERT操作对应XLOG record 的内容,我们创建一个简单的TABLE,并执行INSERT操作:

create table test(id int);
insert into test values(3);

XLogInsert函数

在执行INSERT操作的时候,PostgreSQL会调用heap_insert函数,其中会调用XLogInsert去插入对应的XLOG record:

recptr = XLogInsert(RM_HEAP_ID, info, rdata);
PageSetLSN(page, recptr);

注意:实际上是调用两次XLogInsert,除了HEAP INSERT操作的XLOG record,还有事务提交的XLOG record。

函数XLogInsert的返回值是XLogRecPtr结构类型,即LSN(log secquence number)。heap_insert函数在执行XLogInsert()后,把其返回值XLogRecPtr记录赋值给对应的page的PageHeaderData结构中,以实现WAL机制(参考PgSQL · 特性分析 · Write-Ahead Logging机制浅析)。

XLogInsert函数中会去包装一个XLOG record,并把它刷写到磁盘,我们接下来分析一下XLogInsert函数。

XLogInsert函数定义:

XLogRecPtr XLogInsert(RmgrId rmid, uint8 info, XLogRecData *rdata)

XLogInsert的三个函数参数分别是:

  • rmid
    • RmgrId类型
    • 代表本条XLOG record所属的资源管理器类型,例如我们上面的例子中INSERT操作属于RM_HEAP_ID,即堆表资源管理器
  • info
    • uint8类型
    • 代表资源管理器对应的操作,例如堆表中INSERT操作为0x00
  • rdata
    • XLogRecData指针类型(链表)
    • 每个XLogRecData结构体存储对应的资源管理器数据rmgr-specific data

之所以要用XLogRecData链,是因为在所要处理的日志记录实体数据在内存空间可能不是连续存储的,而且数据可能分布在多个缓冲区内,需要用XlogRecData链表将它们组织起来。XlogRecData数据结构如下:

typedef struct XLogRecData
{
	char	   *data;	/*包含实体数据的起始位置*/
	uint32		len;		/*包含实体数据大小*/
	Buffer		buffer;	/*如果有buffer指明第几个缓冲区*/
	bool		buffer_std;	/*是否含有标准的pd_lower/pd_upper结构*/
	struct XLogRecData *next;	/*指向下一个XLogRecData*/
} XLogRecData;

其中,buffer_std该值为true,则容许XLOG释放备份页的空闲空间,空闲空间由pd_lower和pd_upper限定:

  • pd_lower表示页面起始位置与未分配空间开头的字节偏移
  • pd_upper表示页面末尾位置与未分配空间末尾的字节偏移

通过分析三个XLogInsert函数参数,可以看出XLogInsert主要是将rdata封装成一个XLOG record。接下来我们将分析heap_insert函数内如何对rdata进行赋值。

heap_insert函数

heap_insert函数主要操作HeapTupleData结构体,对应在每个数据页中存储的每个tuple,结构如下图所示:
image.png

tuple分为头部信息和数据信息,这里不再展开,我们将在分析PostgreSQL的MVCC机制时,将其中的结构详细分析。

heap_insert函数的主要操作如下:

  • 调用RelationGetBufferForTuple方法找到shmem里缓存数据块buffer
  • 调用RelationPutHeapTuple方法,把组装好的元组tuple放到对应buffer中合适的位置
  • 赋值XLogRecData类型变量rdata,通过代码分析可以看出rdata实际上是对tuple的内容摘要
  • XLogRecData rdata[4]; 堆表的INSERT操作有4个XLogRecData结构体组成的链表
  • rdata[0].data 存储一个xl_heap_insert结构,用于标示一些基本信息:
/* This is what we need to know about insert */
typedef struct xl_heap_insert
{
	xl_heaptid	target;			/* inserted tuple id */
	uint8		flags;
	/* xl_heap_header & TUPLE DATA FOLLOWS AT END OF STRUCT */
} xl_heap_insert;
  • rdata[0].buffer = InvalidBuffer
  • rdata[1].data存储一个xl_heap_header结构,存储tuple头部的简化信息:
typedef struct xl_heap_header
{
	uint16		t_infomask2;
	uint16		t_infomask;
	uint8		t_hoff;
} xl_heap_header;
  • rdata[1].buffer = need_tuple_data ? InvalidBuffer : buffer;如果需要存储整个数据块,则把buffer赋值给rdata
  • rdata[2].data存储tuple头部后面的数据,比如INSERT操作的插入元组的每列的数值
  • rdata[2].buffer = need_tuple_data ? InvalidBuffer : buffer;同rdata[1]
  • 如果需要存储整个数据块,则rdata[3].buffer=buffer
  • 调用XLogInsert,将rdata封装成XLOG record写入WAL缓冲区,如果需要切换日志段文件,调用XLogWrite刷写到磁盘

经过以上分析,我们可以知道,XLOG record的核心部分是资源管理器数据(XLogRecData)和备份数据块(backup block data),这两个数据包含了我们恢复时候需要的数据。在各个资源管理器的具体操作调用XLogInsert之前,需要对这两个部分进行填充。

资源管理器对应处理函数

资源管理器结构

在恢复过程中,每个资源管理器对应的处理函数是不同的,为了更好的抽象资源管理器,在PostgreSQL中定义了RmgrData结构体和一个RmgrData类型数组RmgrTable。

typedef struct RmgrData
{
	const char *rm_name;
	void		(*rm_redo) (XLogRecPtr lsn, struct XLogRecord *rptr);
	void		(*rm_desc) (StringInfo buf, uint8 xl_info, char *rec);
	void		(*rm_startup) (void);
	void		(*rm_cleanup) (void);
} RmgrData;
extern const RmgrData RmgrTable[];
PG_RMGR(RM_XLOG_ID, "XLOG", xlog_redo, xlog_desc, NULL, NULL)
PG_RMGR(RM_XACT_ID, "Transaction", xact_redo, xact_desc, NULL, NULL)
PG_RMGR(RM_SMGR_ID, "Storage", smgr_redo, smgr_desc, NULL, NULL)
PG_RMGR(RM_CLOG_ID, "CLOG", clog_redo, clog_desc, NULL, NULL)
PG_RMGR(RM_DBASE_ID, "Database", dbase_redo, dbase_desc, NULL, NULL)
PG_RMGR(RM_TBLSPC_ID, "Tablespace", tblspc_redo, tblspc_desc, NULL, NULL)
PG_RMGR(RM_MULTIXACT_ID, "MultiXact", multixact_redo, multixact_desc, NULL, NULL)
PG_RMGR(RM_RELMAP_ID, "RelMap", relmap_redo, relmap_desc, NULL, NULL)
PG_RMGR(RM_STANDBY_ID, "Standby", standby_redo, standby_desc, NULL, NULL)
PG_RMGR(RM_HEAP2_ID, "Heap2", heap2_redo, heap2_desc, NULL, NULL)
PG_RMGR(RM_HEAP_ID, "Heap", heap_redo, heap_desc, NULL, NULL)
PG_RMGR(RM_BTREE_ID, "Btree", btree_redo, btree_desc, NULL, NULL)
PG_RMGR(RM_HASH_ID, "Hash", hash_redo, hash_desc, NULL, NULL)
PG_RMGR(RM_GIN_ID, "Gin", gin_redo, gin_desc, gin_xlog_startup, gin_xlog_cleanup)
PG_RMGR(RM_GIST_ID, "Gist", gist_redo, gist_desc, gist_xlog_startup, gist_xlog_cleanup)
PG_RMGR(RM_SEQ_ID, "Sequence", seq_redo, seq_desc, NULL, NULL)
PG_RMGR(RM_SPGIST_ID, "SPGist", spg_redo, spg_desc, spg_xlog_startup, spg_xlog_cleanup)

RmgrTable的下标对应上文提到的资源管理器ID,比如HEAP表对应的RmgrData是RmgrTable[RM_HEAP_ID]。

RmgrData中定义了rm_redo、rm_desc、rm_startup、rm_cleanup四个函数指针,分别对应每个资源管理器具体的redo、desc、startup、cleanup函数,我们主要分析下其中的rm_redo恢复函数

REDO函数

不同资源管理器的REDO函数参数都为(XLogRecPtr lsn, XLogRecord *record),这两个类型的参数我们在前面两期月报中均有涉及。其中:

  • XLogRecPtr类型代表着该log record在日志序列中的位置
  • XLogRecord类型代表着该log record的头部信息

REDO函数主要是根据该log record的位置取到对应的log record进行相应的恢复操作,下面我们以堆表INSERT操作为例,分析下对应的REDO函数heap_xlog_insert:

  • 调用XLogRecGetData(record)方法,取出上文中的xl_heap_insert结构xlrec
  • 如果该XLOG record存在full-page image,则恢复该数据块
  • 将上文提到的rdata恢复成tuple,写入到缓存数据块中
  • 标记缓存数据块为脏页,等待刷出

通过以上分析,我们可以大体知道XLOG record满足了以下几点才能实现崩溃恢复甚至是任意时间点恢复:

  • 可靠性
    • full-page image,每次checkpoint第一次更新时都需要将整页复制
    • CRC32校验码
    • 插入XLOG record时不接受其他信号
  • 可重复性
    • 多次重放一个log record,得到的结果相同
  • 可恢复性
    • 大多数资源管理器操作对应的log record中存储的都是对应数据在磁盘存储的一个摘要(例如INSERT操作的rdata是tuple的一个摘要)

XLOG日志在PostgreSQL运维中占据了非常重要的地位,从WAL机制到备份恢复以及主备复制,许多功能都离不开XLOG日志。推荐大家看下《PostgreSQL Replication》这本书,加深对PostgreSQL中XLOG以及Replication的理解。

另外,PostgreSQL XLOG目前也存在一些问题,最明显的问题就是写放大,因为full-page image导致XLOG record体积太大,如果设置不合理,可能日志的体积是数据体积的20倍左右。但是经过参数调优,可以尽可能地避免这种情况,可参考文章如何遏制PostgreSQL WAL的疯狂增长

MySQL · 捉虫动态 · InnoDB crash

$
0
0

问题描述

在 MySQL 官方最新的版本 MySQL 5.6.36 版本上,我们遇到了一个非常有意思的bug,实例几乎每个小时crash一次,查看其产生的 core file,发现如下的backtrace:

#3  <signal handler called>
#4  0x00002b65596248a5 in raise (sig=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:64
#5  0x00002b6559626085 in abort () at abort.c:92
#6  0x00000000010deabe in dict_index_is_clust (index=0x0) at storage/innobase/include/dict0dict.ic:269
#7  0x00000000010f1efb in row_merge_drop_indexes (trx=0x2b656c027b28, table=0x2b65840323e8, locked=1) at storage/innobase/row/row0merge.cc:2880
#8  0x00000000012f41ea in dict_table_remove_from_cache_low (table=0x2b65840323e8, lru_evict=1) at storage/innobase/dict/dict0dict.cc:2109
#9  0x00000000012efbdd in dict_make_room_in_cache (max_tables=400, pct_check=100) at storage/innobase/dict/dict0dict.cc:1446
#10 0x0000000001197cad in srv_master_evict_from_table_cache (pct_check=100) at storage/innobase/srv/srv0srv.cc:2012
#11 0x00000000011988ff in srv_master_do_idle_tasks () at storage/innobase/srv/srv0srv.cc:2207
#12 0x000000000119930f in srv_master_thread (arg=0x0) at storage/innobase/srv/srv0srv.cc:2355
#13 0x00002b65583f5851 in start_thread (arg=0x2b6560895700) at pthread_create.c:301
#14 0x00002b65596d967d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:115

从core来看,现象也比较明确:

  1. InnoDB master thread 正在淘汰 InnoDB 表数据对象 dict_table_t
  2. 淘汰的 dict_table_t 对象 table->drop_abort= true,所以需要删除未完成的index
  3. 当在row_merge_drop_indexes() 函数中删除索引时, 发现 table->indexes= 0, 随后就crash了

由于是master thread 后台线程触发的crash,所以并不能知道用户现场做了什么操作,以及什么时候做了什么操作而对此产生了影响。

所以,只能根据当前 core 文件中的对象 dict_table_t 的属性进行排查,来查找线索。

InnoDB背景

1. Master Thread

InnoDB有一个常驻后台 master 线程,主要做以下工作:

  1. 前台用户线程lazy drop 的 table,master thread负责清理
  2. merge insert buffer
  3. 淘汰dict table cache
  4. flush log buffer
  5. make checkpoint

其中,evict table cache 的过程,会根据 server 层的一个变量 table_definition_cache 来进行淘汰,
因为 server 层会根据这个变量的设置来缓存从FRM文件中得到的数据字典定义 即table_share 对象,所以引擎层缓存超过这个设置的意义也不大。

Master线程会根据 LRU 链表即 dict_sys->table_LRU 进行淘汰,但淘汰的过程,需要保证 dict_table_t 对象不能被 handler 引用,也就是当前没有 statement 语句在操作这个表,在 dict_table_t 中,使用 table->n_ref_count 来表示有多少个handler对象在引用。

2. dict_table_t的生命周期

1. 装载
当操纵这个表的时候,InnoDB 的 handler 对象需要引用这个 dict_table_t 对象,首先会在 dict_sys->table_hash 进行hash查找:
1. 如果存在,说明已经存在 dictionary cache 中,
2. 如果不存在,需要读取InnoDB的数据字典SYS_TABLES, SYS_INDEXES, SYS_COLUMNS等来装载dict_table_t对象

2. 引用
当 statement 执行的时候,会先创建 handler,然后 handler 会引用 dict_table_t 对象对象,即增加 table->n_ref_count++,因为增加了引用,会调整 dict_sys->table_LRU 的位置,保持热度。
当语句结束的时候,如果 handler close 的话,会解除 dict_table_t 对象的引用,即递减 table->n_ref_count--。

3. 缓存
因为 server 层存在 table open cache,受 table_open_cache 参数设置影响,所以,当 statement 结束的时候,并不会立即 close opened table,相应的 InnoDB 的 handler 也不会立即关闭,这样就保持了 table->n_ref_count 引用数。

4. 淘汰
Master thread 每一秒钟都会轮询 dict_sys->table_LRU, 当 table->n_ref_count == 0, 进行淘汰dict_table_t 对象, 保留的数量受参数table_definition_cache控制。

3. table->drop_aborted

按照 InnoDB online DDL 的定义,在 DDL 的过程中,如果任务失败,会把 table->drop_aborted 设置成 true,随后,会回滚掉当前的操作,因为是online操作,在中间时刻不阻塞 DML, 所以这里会产生两种情况:
1. 如果当前没有 statement 操作这个表,那当前在回滚的时候,就把这个 DDL 给直接回滚掉了
2. 如果当前有 statement 在操作这个表,那就会把 table->drop_aborted 设置成TRUE,进行 lazy drop 回滚。

根据代码的路径,lazy drop的会在以下场景发生:
1. dict_table_close()
也就是当最后一个 statement 引用 dict_table_t 使用完了之后,即 table->n_ref_count == 0 时,这个线程负责清理掉未完成的 DDL
2. 下一个 DDL
也就是当下一个 DDL 操作的时候,如果发现 table->drop_aborted 为 true,那么也会负责清理这个未完成的 DDL

复现过程

从上面的 InnoDB 背景介绍来看,我们已经 cover 了这个 crash 相关的概念和内容,下面我们就来看复现过程:

1. 从core文件看 table->drop_aborted= true
所以我们断定一定存在失败的 DDL 语句,随后通过审计日志,我们发现:

   alter table t add unique key(col1);
   ERROR 1062 (23000): Duplicate entry '2' for key 'col1'

2.回滚
因为用户操作的时候,没有回滚掉这个 online add unique key 操作,所以我们断定在 alter 操作的时候,同时有 DML 语句在。

根据这两点我们构思了如下的case:

环境准备:

create table t(id int primary key, col1 int) engine=innodb;
insert into t values(1, 2);
insert into t values(2, 2);

Session 1:

 // 需要再执行rollback之前,session 2进行insert,递增table->n_ref_count
 alter table t add unique key(col1);

Session 2:

// 需要等待alter操作完成之后,insert才去完成,继而递减table->n_ref_count
insert into t values(3, 2);

Session 3:

// close所有打开的表,使 table->n_ref_count == 0;
flush tables
// 创建1000张表,这样t表就会率先淘汰出去
let $loop=1000;
while($loop)
{
  eval create table t_$loop(id int)engine=innodb;
  dec $loop;
}

为了复现这个case,我们添加了两个sleep函数在代码中,参考如下:

diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc
index 41c767a..bfd7102 100644
--- a/storage/innobase/handler/ha_innodb.cc
+++ b/storage/innobase/handler/ha_innodb.cc
@@ -6779,6 +6779,7 @@ no_commit:

 		build_template(true);
 	}
+	os_thread_sleep(5000000);

 	innobase_srv_conc_enter_innodb(prebuilt->trx);

diff --git a/storage/innobase/handler/handler0alter.cc b/storage/innobase/handler/handler0alter.cc
index e772208..dea7696 100644
--- a/storage/innobase/handler/handler0alter.cc
+++ b/storage/innobase/handler/handler0alter.cc
@@ -4138,6 +4138,7 @@ rollback_inplace_alter_table(
 		(almost) nothing has been or needs to be done. */
 		goto func_exit;
 	}
+	os_thread_sleep(2000000);

 	row_mysql_lock_data_dictionary(ctx->trx);

这样我们就复现出来这个 crash, 同样,我们在 MySQL 5.7.18,以及还没有 release 的8.0版本上发现了存在相同的问题。

问题原因和修复方法

1. 问题原因
从代码的设计上,table->drop_aborted设置成TRUE,会在两种场景下进行lazy drop,即上面提到的:
1. dict_table_close 即 dict_table_t 对象 n_ref_count 引用数降成0
2. 下一个 DDL 的时候

而这个 lazy drop 却是在 master thread 要淘汰 dict_table_t 的时候。 因为淘汰的条件需要 n_ref_count == 0, 所以一定发生过dict_table_close() 了。

那问题的原因就明确了: 在 dict_table_close 把 n_ref_count 降成0的时候,没有完成 lazy drop 回滚。

2. 修复方法
知道了问题的原因,修复方法很简单,我们发现 dict_table_close() 函数存在一些逻辑错误,
我们将会在Aliyun RDS版本和我们的开源版本AliSQL上进行修复,敬请关注。
同时也可以关注,我们提交给官方和MariaDB的进度:
https://bugs.mysql.com/bug.php?id=86607
https://jira.mariadb.org/browse/MDEV-13051


MSSQL · 实现分析 · SQL Server实现审计日志的方案探索

$
0
0

摘要

这篇文章介绍四种实现MSSQL Server审计日志功能的方法探索,即解析数据库事务日志、SQL Profiler、SQL Audit以及Extended Event。详细介绍了这四种方法的具体实现,以及通过优缺点的对比和总结,最终得出结论,使用Extended Event实现审计日志是最好的选择,为产品化选型提供参考。

审计日志需求分析

对于关系型数据库来而言,在生产环境SQL Server数据库实例中,审计日志是一个非常重要且必须的强需求功能,主要体现在以下几个方面。

  • 安全审计

  • 问题排查

  • 性能调优

安全审计

在一些存取敏感信息的产品环境数据库SQL Server实例中(比如:财务系统、设计到国家安全层面的数据库系统),对数据操作要求十分谨慎,安全要求等级十分严密,需要对每一条数据操作语句进行审计,以便做到每次数据变动或查看均可追溯。在这个场景中,对敏感信息操作的审计是基于数据安全性的要求。

问题排查

在日常生产系统管理维护过程中,我们经常会遇到类似的场景和疑问:能否找到是谁在哪个时间点执行了什么语句把数据XXX给删除(更新)了呢?笔者在从事DBA行业的几年工作经历过程中,无数次被问及到类似的问题。要解决这个场景中的问题,审计日志功能是不二选择。

性能调优

利用审计日志对数据库系统进行性能调优是审计日志非常重要的功能和用途。比如,以下是几个审计日志典型的应用场景:

  • 找出某段时间内哪些语句导致了系统性能消耗严重(比如:CPU、IOPS等)

  • 找出某段时间内的TOP CPU SQL语句

  • 找出某段时间内的TOP IO SQL语句

  • 找出某段时间内的TOP Time Cost SQL语句

  • 找出某段时间内哪个用户使用的数据库系统资源最多

  • 找出某段时间内哪个应用使用的数据库系统资源最多

……

实现审计日志的方法

基于以上对审计日志的需求分析,我们了解到审计日志的功能是关系型数据至关重要的强需求,让我们来看看SQL Server数据库系统有哪些实现审计日志功能的方法和具体实现,以及这些方法的优缺点对比。

数据库日志分析

在SQL Server数据库事务日志中,记录了每个事务的数据变更操作的详细信息,包含谁在哪个时间点做了什么操作。所以,我们可以基于SQL Server数据库事务日志的分析,来获取数据变更的详细审计日志信息。使用这个方法来实现审计日志功能的,有一家叫着ApexSQL的公司产品做的很不错,产品ApexSQL Log就是通过数据库事务日志来实现审计日志功能的产品,详情参见:ApexSQL Log。附一张来自ApexSQL官网的截图:
01.png

但是,由于SQL Server本身是微软的闭源产品,对于事务日志格式外界很难知道,所以这个方法的实现门槛很高,实现难度极大。加之,有可能不同版本的SQL Server事务日志格式存在差异,必须要对每个版本的事务日志解析做相应的适配,导致维护成本极高,产品功能延续性存在极大风险和挑战。

SQL Profiler

SQL Profiler是微软从SQL Server 2000开始引入的数据库引擎跟踪工具,具有使用界面操作的接口、使用SQL语句创建接口以及使用SMO编程创建接口。使用SQL Profiler,可以实现非常多的功能,比如:

  • 图形化监控数据库引擎执行的SQL语句(也可以将执行语句保存到表中)

  • 查看执行语句实时的执行计划

  • 数据库引擎错误信息排查

  • 数据库性能分析

  • 阻塞,锁等待、锁升级及死锁跟踪

  • 后台收集查询语句信息

……

所以,从功能完整性角度来说,我们完全可以使用SQL Profiler来实现就数据库实例级别的审计日志的功能。那么接下来让我们看看如何使用SQL Profiler实现审计日志的功能。

图形化创建

开始 => 运行 => 键入“Profiler” => 回车,打开Profiler工具后,点击“New Trace” => Server Name => Authentication => Connect,如下图所示:
02.png

然后,选择General => Save to table => 选择要保留到的实例名、数据库名、架构名和表名 => OK
03.png

接下来选择要跟踪的事件,Events Selection => SQL:StmtCompleted => Column Filters => LoginName => Not Like %sa% => OK => Run
04.png

使用SQL语句创建

使用图形化界面创建SQL Profiler实现审计日志功能,简单易用,很容易上手。但是,过程繁琐、效率不高,难于自动化。这个时候,就需要使用SQL语句来创建SQL Profiler功能,实现一键创建的方法了。

use master
GO

set nocount on

declare 
	@trace_folder nvarchar(256)
	,@trace_file nvarchar(256) 
	,@max_files_size bigint
	
	,@stop_time datetime
	,@file_count int

	,@int_filter_cpu int
	,@int_filter_duration bigint
	,@int_filter_spid int
	,@set_trace_status int
;

select 
	@trace_folder=N'C:\Temp\perfmon'
	
	,@max_files_size = 50			--max file size for each trace file
	,@file_count = 10				--max file count
	
	,@stop_time = '6/13/2017 10:50'	--null: stop trace manully; specify time (stop at the specify time)
	,@int_filter_cpu = NULL				-- >= @int_filter_cpu ms will be traced. or else, skipped.
										--NULL: ignore this filter
	,@int_filter_duration = NULL		--execution duration filter: millisecond
										--NULL: ignore this filter
	--,@int_filter_spid = 151			--integer: specify a spid to trace
										--				
										
	,@set_trace_status = 1	--0: Stops the specified trace.; 
							--1: Starts the specified trace.;
							--2: Closes the specified trace and deletes its definition from the server.;
;

/*

select * from sys.traces

*/
--private variables
declare
	@trace_id int
	,@do int
	,@loop int
	,@trace_event_id int
	,@trace_column_id int
	,@return_code tinyint
	,@return_decription varchar(200)
	,@field_separator char(1)

;	
select
	@field_separator = ','			--trace columns list separator
;

IF right(ltrim(rtrim(@trace_folder)), 1 ) <> '\'
BEGIN
	SELECT 
		@trace_folder = ltrim(rtrim(@trace_folder)) + N'\' 
	;
	exec sys.xp_create_subdir @trace_folder
END
;

select
	@trace_file = @trace_folder + REPLACE(@@SERVERNAME, N'\', N'')
;

IF @int_filter_spid IS NOT NULL
BEGIN
	select
		@trace_file = @trace_file + cast(@int_filter_spid as varchar)
	;
END

--select @trace_file

select top 1
	@trace_id = id
from sys.traces
where path like @trace_file + N'%'

if @trace_id is not null
begin
	
	-- Start Trace (status 1 = start)
	EXEC sys.sp_trace_setstatus @trace_id, @set_trace_status

	return
end

if OBJECT_ID('tempdb..#trace_event','u') is not null
	drop table #trace_event
create table #trace_event
(
	id int identity(1,1) not null primary key
	,trace_event_id int not null
	,trace_column_id int not null
	,event_name sysname null
	,trace_column_name sysname null
)

;with trace_event
as
(		--select * from sys.trace_events order by trace_event_id
	select 
		is_trace = 1 , event_name = 'SQL:StmtCompleted'
		,trace_column_list = 'NestLevel,ClientProcessID,EndTime,DatabaseID,GroupID,ServerName,SPID,DatabaseName,NTUserName,IntegerData2,RequestID,EventClass,SessionLoginName,NTDomainName,TextData,XactSequence,CPU,ApplicationName,Offset,LoginSid,TransactionID,IntegerData,Duration,SourceDatabaseID,LineNumber,ObjectID,Reads,RowCounts,Writes,IsSystem,ObjectName,LoginName,ObjectType,StartTime,HostName,EventSequence,'
),
trace_column
as(
	select 
		*
		,trace_column_list_xml = 
								CAST(
										'<V><![CDATA[' 
													+ REPLACE(
														REPLACE(
																REPLACE(
																			trace_column_list,CHAR(10),']]></V><V><![CDATA['
																		),@field_separator,']]></V><V><![CDATA['
																),CHAR(13),']]></V><V><![CDATA['
															) 
										+ ']]></V>'
									as xml
								)
	from trace_event
	where is_trace = 1
)
,data
as(
	select 
		trace_column = T.C.value('(./text())[1]','sysname')
		,event_name
	from trace_column AS a
		CROSS APPLY trace_column_list_xml.nodes('./V') AS T(C)
)
INSERT INTO #trace_event
select 
	trace_event_id = ev.trace_event_id
	,trace_column_id = col.trace_column_id
	,a.event_name
	,trace_column_name = a.trace_column
from data as a
	inner join sys.trace_columns as col
	on a.trace_column = col.name
	inner join sys.trace_events as ev
	on a.event_name = ev.name
where col.trace_column_id is not null
order by ev.trace_event_id
;

--select * from #trace_event

---private variables
select 
	@trace_id = 0
	,@do = 1
	,@loop = @@ROWCOUNT
	,@trace_event_id = 0
	,@trace_column_id = 0
	,@return_code = 0
	,@return_decription = ''
;

--create trace
exec @return_code = sys.sp_trace_create @traceid = @trace_id OUTPUT 
										, @options = 2  
										, @tracefile =  @trace_file
										, @maxfilesize = @max_files_size
										, @stoptime = @stop_time
										, @filecount =  @file_count
;

select 
	trace_id = @trace_id
	,[current_time] = getdate()
	,[stop_time] = @stop_time
;

set
	@return_decription = case @return_code
								when 0 then 'No error.'
								when 1 then 'Unknown error.'
								when 10 then 'Invalid options. Returned when options specified are incompatible.'
								when 12 then 'File not created.'
								when 13 then 'Out of memory. Returned when there is not enough memory to perform the specified action.'
								when 14 then 'Invalid stop time. Returned when the stop time specified has already happened.'
								when 15 then 'Invalid parameters. Returned when the user supplied incompatible parameters.'
							else ''
							end
;

raiserror('Trace create with:
%s',10,1,@return_decription) with nowait

--loop set trace event & event column
while @do <= @loop
begin
	select top 1
		@trace_event_id = trace_event_id
		,@trace_column_id = trace_column_id
	from #trace_event
	where id = @do
	;
	
	--set trace event
	exec sys.sp_trace_setevent @trace_id, @trace_event_id, @trace_column_id, 1
	raiserror('exec sys.sp_trace_setevent @trace_id, %d, %d, 1',10,1,@trace_event_id,@trace_column_id) with nowait
	
	set @do = @do + 1;
end

--CPU >= 500/ cpu columnid = 18
IF @int_filter_cpu IS NOT NULL
	EXEC sys.sp_trace_setfilter @trace_id, 18, 0, 4, @int_filter_cpu

--duration filter/ duration columnid=13
IF @int_filter_duration IS NOT NULL
	EXEC sys.sp_trace_setfilter @trace_id, 13, 0, 4, @int_filter_duration

--spid filter/ spid columnid=12
IF @int_filter_spid IS NOT NULL
	exec sys.sp_trace_setfilter @trace_id, 12, 0, 0, @int_filter_spid


--applicationName not like 'SQL Server Profiler%'
EXEC sys.sp_trace_setfilter @trace_id, 10, 0, 7, N'SQL Server Profiler%'

-- Start Trace (status 1 = start)
EXEC sys.sp_trace_setstatus @trace_id, @set_trace_status
GO

其中输入参数表达的含义解释如下:

@trace_folder:Trace文件存放的位置

@max_files_size:每一个Trace文件大小

@file_count:Trace滚动最多的文件数量

@stop_time:Trace停止的时间

@int_filter_cpu:CPU过滤阈值,CPU使用率超过这个值会被记录下来,单位毫秒

@int_filter_duration:执行时间过滤阈值,执行时间超过这个值会被记录,单位毫秒

@set_trace_status:Trace的状态:0停止;1启动;2删除

SMO编程创建

SQL Profiler除了使用图形化界面创建,使用系统存储过程创建两种方法以外,还可以使用SMO编程方法来创建。

SQL Audit

使用SQL Audit实现SQL Server审计日志功能需要以下三个步骤来完成:

  • 创建实例级别的Audit并启动

  • 创建数据库级别的Audit Specification

  • 读取审计日志文件

创建实例级别Audit

使用Create Server Audit语句创建实例级别的Audit,方法如下:

USE [master]
GO

CREATE SERVER AUDIT [Audit_Svr_User_Defined_for_Testing]
TO FILE 
( FILEPATH = N'C:\Temp\Audit'
 ,MAXSIZE = 10 MB
 ,MAX_ROLLOVER_FILES = 10
 ,RESERVE_DISK_SPACE = OFF
)
WITH
( QUEUE_DELAY = 1000
 ,ON_FAILURE = CONTINUE
)
GO

启动实例级别的Audit,代码如下

USE [master]
GO
ALTER SERVER AUDIT [Audit_Svr_User_Defined_for_Testing] WITH(STATE=ON)
;
GO

创建数据库级别Audit Specification

实例级别Audit创建完毕后,接下来是对需要审计的数据库建立对于的Audit Specification,方法如下:

USE [testdb]
GO
CREATE DATABASE AUDIT SPECIFICATION [Audit_Spec_for_TestDB]
FOR SERVER AUDIT [Audit_Svr_User_Defined_for_Testing] 
ADD (SELECT, INSERT, UPDATE, DELETE, EXECUTE ON DATABASE::[testdb] BY [public])
WITH (STATE = ON);
GO

由于SQL Audit Specification是基于数据库级别的,所以存在以下场景的维护性复杂度增加:

  • 用户需要审计实例中某些或者所有数据库,必须在每个需要审计的数据库下创建对象

  • 用户实例有新数据库创建,并需要审计日志功能时,必须在新的数据库下创建对象

读取审计日志文件

最后,我们需要将审计日志文件中存放的内容读取出来,使用SQL Server提供的系统函数sys.fn_get_audit_file,方法如下:

DECLARE 
	@AuditFilePath sysname
;
 
SELECT 
   @AuditFilePath = audit_file_path
FROM sys.dm_server_audit_status
WHERE name = 'Audit_Svr_User_Defined_for_Testing'
 
SELECT statement,* 
FROM sys.fn_get_audit_file(@AuditFilePath,default,default)
;

Extended Event

微软SQL Server产品长期的规划是逐渐使用Extended Event来替换SQL Profiler工具,因为Extended Event更加轻量级,性能消耗比SQL Profiler大幅降低,因此对用户系统性能影响也大幅减轻。在审计日志的应用场景中,只需要在实例级别创建一个Extended Event Session对象,然后启用即可。既满足了功能性的需求,又能够做到很好后期维护,不需要为某一个数据库创建相应对象,对实例的性能消耗大幅降低到5%左右。

创建Extended Event Session

使用Create Event Session On Server语句创建基于实例级别的Extended Event。语句如下:

USE master
GO

CREATE EVENT SESSION [svrXEvent_User_Define_Testing] ON SERVER 
ADD EVENT sqlserver.sql_statement_completed
( 
	ACTION 
	( 
		sqlserver.database_id,
		sqlserver.database_name,
		sqlserver.session_id, 
		sqlserver.username, 
		sqlserver.client_hostname,
		sqlserver.client_app_name,
		sqlserver.sql_text, 
		sqlserver.query_hash,
		sqlserver.query_plan_hash,
		sqlserver.plan_handle,
		sqlserver.tsql_stack,
		sqlserver.is_system,
		package0.collect_system_time
	) 
	WHERE sqlserver.username <> N'NT AUTHORITY\SYSTEM'
		AND sqlserver.username <> 'sa'
		AND (NOT sqlserver.like_i_sql_unicode_string(sqlserver.client_app_name, '%IntelliSense'))
		AND sqlserver.is_system = 0
		
)
ADD TARGET package0.asynchronous_file_target
( 
	SET 
		FILENAME = N'C:\Temp\svrXEvent_User_Define_Testing.xel', 
		MAX_FILE_SIZE = 10,
		MAX_ROLLOVER_FILES = 100
)
WITH (
	EVENT_RETENTION_MODE = NO_EVENT_LOSS,
	MAX_DISPATCH_LATENCY = 5 SECONDS
);
GO

启用Extended Event Session

Extended Event Session对象创建完毕后,需要启动这个session对象,方法如下:

USE master
GO

-- We need to enable event session to capture event and event data 
ALTER EVENT SESSION [svrXEvent_User_Define_Testing]
ON SERVER STATE = START;
GO

读取审计日志文件

Extend Event生成审计日志文件以后,我们可以使用sys.fn_xe_file_target_read_file系统函数来读取,然后分析event_data列所记录的详细信息。

USE master
GO
SELECT *
FROM sys.fn_xe_file_target_read_file('C:\Temp\svrXEvent_User_Define_Testing*.xel', null, null, null)

方案对比

根据前面章节“实现审计日志的方法”部分的介绍,我们从可靠性、对象级别、可维护性、开销和对数据库系统的影响五个方面来总结这四种技术的优缺点。

  • 可靠性:这四种实现审计日志的方法可靠性都有保障,如果使用数字化衡量可维护性,得满分100分

  • 对象级别:SQL Profiler和Extended Event是基于实例级别的技术方案;解析事务日志解析和SQL Audit方法是基于数据库级别的技术,一旦有数据库创建或者删除操作,需要做相应的适配,所以维护成本也相对高。基于数据库级别的方案得分为0,基于实例级别得分为100

  • 维护性:基于实例级别的实现方法可维护性(得分100)显然优于基于数据库级别(得分为0)的实现方式

  • 开销:SQL Profiler对数据库系统开销很大,大概20%左右(得分100 - 20 = 80),其他三种开销较小5%左右(得分100 - 5 = 95)

  • 影响:开销大的技术方案自然影响就大,反之亦然。得分与开销部分类似。

四种技术方案优缺点汇总如下表所示:
05.png

以下是对四种实现审计日志方法五个维度打分,得分统计汇总如下表所示:
06.png

将汇总得分做成雷达图,如下图所示:
07.png

从雷达图我们可以很清楚的看到,综合考虑可靠性、可维护性、系统开销和影响来看,使用Extended Event实现审计日志的方法是最优的选择。

最后总结

本期分享了SQL Server实现审计日志功能的四种技术方案和详细实现,并从可靠性、可维护性、对象级别、系统开销和影响五个维度分析了四种方案各自的优缺点,最后的结论是使用Extended Event实现审计日志方法是最优选择,以此来为我们的产品化做出正确的选择。

MySQL · 源码分析 · InnoDB Repeatable Read隔离级别之大不同

$
0
0

开始介绍之前,先让我们了解一些基本概念。ANSI SQL STANDARD定义了4类隔离级别(READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE),包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级别一般支持更高的并发处理,并拥有更低的系统开销。

  • Read Uncommitted(读未提交)
    在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
  • Read Committed(读已提交)
    一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
  • Repeatable Read(可重读)
    这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
  • Serializable(可串行化)
    这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
    这四种隔离级别采取不同的锁类型来实现。并发控制中读取同一个表的数据,可能出现如下问题:

脏读(Drity Read):事务T1修改了一行数据,事务T2在事务T1提交之前读到了该行数据。

不可重复读(Non-repeatable read): 事务T1读取了一行数据。 事务T2接着修改或者删除了改行数据,当T1再次读取同一行数据的时候,读到的数据时修改之后的或者发现已经被删除。

幻读(Phantom Read): 事务T1读取了满足某条件的一个数据集,事务T2插入了一行或者多行数据满足了T1的选择条件,导致事务T1再次使用同样的选择条件读取的时候,得到了比第一次读取更多的数据集。

MySQL/INNODB支持ANSI SQL STANDARD规定的四种隔离级别(READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE).本篇文章重点关注一下MySQL REPEATABLE READ隔离级别与其他数据实现方式上的不同之处。

下面看一下MySQL在REPEATABLE READ 隔离级别下的工作方式:

开启两个session。

rr.png

接下来看一下另外一个开源数据库PostgreSQL在REPEATABLE READ 隔离级别下的工作方式:

rr-pg.png

同样测试了SQL SERVER,得到的结果与PostgreSQL是一致的。

从上面的执行情况我们可以看到MySQL与PostgreSQL两者工作方式上有所不同。MySQL在执行UPDATE语句的时候对于session2的INSERT语句是可以看到的,也就是说发生了幻读。那么MySQL在隔离级别为REPEATABLE READ的情况下,表现出来的幻读现象是否属于一个BUG呢?曾经有人在2013年给官方提过一个关于该现象的BUG,请参考https://bugs.mysql.com/bug.php?id=63870。 从BUG页面的注释可以了解到,该现象是与MySQL对REPATABLE READ隔离级别的实现方式有关。而这种幻读现象对于REPATABLE READ隔离级别也是正确的方式。请看wikipedia上对于REPEATABLE READ的描述:

Repeatable reads
In this isolation level, a lock-based concurrency control DBMS implementation keeps read and
write locks (acquired on selected data) until the end of the transaction. However, range-locks are not managed, so phantom reads can occur.

另外我们接着看一下ANSI SQL STANDARD对于各种隔离级别发生幻读的规定:
iso-trx.png

我们从wikipedia以及ANSI SQL STANDARD可以看到对于REPEATABLE READ隔离级别下是允许出现幻读现象的。

接下来我们从源码的角度分析一下Innodb对于REPEATABLE READ隔离级别的执行过程(代码只覆盖重要执行部分)。
以上面的例子为依据进行剖析:
对于第一条SELECT语句,InnoDB将调用row_search_for_mysql函数来返回扫描行。函数row_search_for_mysql调用相关代码如下:

UNIV_INTERN
dberr_t
row_search_for_mysql(
/*=================*/
  byte*   buf,    /* 用来存放记录的空间地址 */
  ulint   mode,   /* InnoDB页扫描顺序 */
  row_prebuilt_t* prebuilt, /* InnoDB扫描需要的所有信息都包含在这个结构体,比如表以及Index等信息 */
  ulint   match_mode, /* 对于Index的匹配模式,是精确匹配还是前缀索引匹配 */
  ulint   direction)  /* 指定扫描顺序,正序还是倒叙扫描 */
{
	...
	/* 从这里我们看出开始一个新事务,并非是从执行BEGIN语句位置开始,而是从其后开始执行的第一条语句开始分配事务ID */
	trx_start_if_not_started(trx, ((trx->mysql_thd
          && thd_is_select(trx->mysql_thd)
          ) || srv_read_only_mode) ? FALSE : TRUE); 

	...
	// 如果是SQL语句第一次开始执行,需要考虑对TABLE增加意向所

	 if (!prebuilt->sql_stat_start) {
	 // 这里标记SQL语句已经开始执行,处理一条SQL语句循环扫描记录的过程
    /* No need to set an intention lock or assign a read view */

    if (UNIV_UNLIKELY
        (trx->read_view == NULL 
         && prebuilt->select_lock_type == LOCK_NONE)) {
      fputs("InnoDB: Error: MySQL is trying to"" perform a consistent read\n""InnoDB: but the read view is not assigned!\n",            stderr);
      trx_print(stderr, trx, 600);
      fputc('\n', stderr);
      ut_error;
	    }
  } else if (prebuilt->select_lock_type == LOCK_NONE) {
    /* This is a consistent read */
    /* Assign a read view for the query */
	// 如果是第一次执行SELECT语句,构建READ_VIEW. 该READ_VIEW 用来判断记录的可见性
    trx_assign_read_view(trx);
    prebuilt->sql_stat_start = FALSE;
  } else {
    ...
  }

	...

	 /* We are ready to look at a possible new index entry in the result
  set: the cursor is now placed on a user record */
	/* 从这里我们看一下InnoDB如何获取一条新纪录。由于上面例子中SESSION1的第一条语句是SELECT语句,InnoDB在REPEATABLE READ 隔离级别下,不对SELECT 语句加锁,所以这里执行SELECT语句的时候prebuilt->select_lock_type为LOCK_NONE。下面我们直接看一下prebuilt->select_lock_type为LOCK_NONE的情况下,InnoDB如何扫描行? */
  if (prebuilt->select_lock_type != LOCK_NONE) {
	... //稍后会对prebuilt->select_lock_type != LOCK_NONE的情况进行分析
	}
  else
	{
		/* This is a non-locking consistent read: if necessary, fetch
    a previous version of the record */

    if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) {

      /* 对于READ UNCOMMITTED隔离级别,我们什么都不需要,只要让他读取最新的记录版本即可 */

    } else if (index == clust_index) {

      /* Fetch a previous version of the row if the current
      one is not visible in the snapshot; if we have a very
      high force recovery level set, we try to avoid crashes
 by skipping this lookup */
	  // 如果是全表扫描或主键扫描,这里需要看看当前记录是否对当前事务可见
      if (UNIV_LIKELY(srv_force_recovery < 5)
          && !lock_clust_rec_cons_read_sees(
            rec, index, offsets, trx->read_view)) {
		// 如果不可见,这里需要查找历史版本
        rec_t*  old_vers;
        /* The following call returns 'offsets'
        associated with 'old_vers' */
        err = row_sel_build_prev_vers_for_mysql(
          trx->read_view, clust_index,
          prebuilt, rec, &offsets, &heap,
          &old_vers, &mtr);
		        if (err != DB_SUCCESS) {

          goto lock_wait_or_error;
        }

        if (old_vers == NULL) {
          /* The row did not exist yet in
          the read view */
		  // 如果当前记录对当前事务不可见,也没有历史版本,直接查找下一条记录
          goto next_rec;
        }

        rec = old_vers;
		   } else {
      /* We are looking into a non-clustered index,
      and to get the right version of the record we
      have to look also into the clustered index: this
      is necessary, because we can only get the undo
      information via the clustered index record. */

      ut_ad(!dict_index_is_clust(index));
	  // 这里处理是Secondary index扫描的情况
      if (!lock_sec_rec_cons_read_sees(
            rec, trx->read_view)) {
        /* We should look at the clustered index.
		        However, as this is a non-locking read,
        we can skip the clustered index lookup if
        the condition does not match the secondary
        index entry. */
		// 这里InnoDB做了一下优化,如果当前记录不满足ICP,直接查找下一条记录;如果满足ICP则需要继续根据聚集索引寻找历史版本
        switch (row_search_idx_cond_check(
            buf, prebuilt, rec, offsets)) {
        case ICP_NO_MATCH:
          goto next_rec;
        case ICP_OUT_OF_RANGE:
          err = DB_RECORD_NOT_FOUND;
          goto idx_cond_failed;
        case ICP_MATCH:
          goto requires_clust_rec;
        }
        ut_error;
      }
    }
  }
...
	}

}

接下来我们看一下UPDATE的执行过程。对于UPDATE操作执行流程的简单描述如下:

根据WHERE条件扫描一条记录(row_search_for_mysql)

更新当前获取的记录(ha_innobase::update_row)

重新将更新后的记录写入InnoDB存储引擎(row_upd_step)

那么我们按照上面的这个流程看一下源码方面的执行过程:

UNIV_INTERN
dberr_t
row_search_for_mysql(
/*=================*/
  byte*   buf,    /* 用来存放记录的空间地址 */
  ulint   mode,   /* InnoDB页扫描顺序 */
  row_prebuilt_t* prebuilt, /* InnoDB扫描需要的所有信息都包含在这个结构体,比如表以及Index等信息 */
  ulint   match_mode, /* 对于Index的匹配模式,是精确匹配还是前缀索引匹配 */
  ulint   direction)  /* 指定扫描顺序,正序还是倒叙扫描 */
{
	...
	/* 从这里我们看出开始一个新事务,并非是从执行BEGIN语句位置开始,而是从其后开始执行的第一条语句开始分配事务ID */
	trx_start_if_not_started(trx, ((trx->mysql_thd
          && thd_is_select(trx->mysql_thd)
          ) || srv_read_only_mode) ? FALSE : TRUE); 

	...
	// 如果是SQL语句第一次开始执行,需要考虑对TABLE增加意向所

	 if (!prebuilt->sql_stat_start) {
	 // 这里标记SQL语句已经开始执行,处理一条SQL语句循环扫描记录的过程
    /* No need to set an intention lock or assign a read view */

    if (UNIV_UNLIKELY
        (trx->read_view == NULL 
         && prebuilt->select_lock_type == LOCK_NONE)) {
      ...
	    }
  } else if (prebuilt->select_lock_type == LOCK_NONE) {
	...
  } else {
	// 这里开始非INSERT的DML操作,因为DML会对记录增加记录排他锁。具体需要增加什么类型的锁,可以参考https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
 wait_table_again:
	// 这里要对TABLE加意向锁
    err = lock_table(0, index->table,
         prebuilt->select_lock_type == LOCK_S
         ? LOCK_IS : LOCK_IX, thr);

    if (err != DB_SUCCESS) {

      table_lock_waited = TRUE;
      goto lock_table_wait;
    }    
    prebuilt->sql_stat_start = FALSE;
  }

	...
  if (prebuilt->select_lock_type != LOCK_NONE) {
	 ulint lock_type;

    if (!set_also_gap_locks
        || srv_locks_unsafe_for_binlog
        || trx->isolation_level <= TRX_ISO_READ_COMMITTED
        || (unique_search && !rec_get_deleted_flag(rec, comp))) {
	  // 这里对于READ_UNCOMMITTED以及READ_COMMITTED,或者唯一键扫描不需要使用gap锁
      goto no_gap_lock;
    } else {
      lock_type = LOCK_ORDINARY;
    }
	
	/* If we are doing a 'greater or equal than a primary key
    value' search from a clustered index, and we find a record
    that has that exact primary key value, then there is no need
    to lock the gap before the record, because no insert in the
    gap can be in our search range. That is, no phantom row can
    appear that way.

    An example: if col1 is the primary key, the search is WHERE
    col1 >= 100, and we find a record where col1 = 100, then no
    need to lock the gap before that record. */

    if (index == clust_index
        && mode == PAGE_CUR_GE
        && direction == 0
        && dtuple_get_n_fields_cmp(search_tuple)
        == dict_index_get_n_unique(index)
        && 0 == cmp_dtuple_rec(search_tuple, rec, offsets)) {
no_gap_lock:
      lock_type = LOCK_REC_NOT_GAP;
    }

	    err = sel_set_rec_lock(btr_pcur_get_block(pcur),
               rec, index, offsets,
               prebuilt->select_lock_type,
               lock_type, thr);

    switch (err) {
      const rec_t*  old_vers;
    case DB_SUCCESS_LOCKED_REC:
      if (srv_locks_unsafe_for_binlog
          || trx->isolation_level
          <= TRX_ISO_READ_COMMITTED) {
        /* Note that a record of
        prebuilt->index was locked. */
        prebuilt->new_rec_locks = 1;
      }
      err = DB_SUCCESS;
    case DB_SUCCESS:
	 // 加锁成功后就认为记录可见了,并未像SELECT语句一样根据事务开始的READ_VIEW进行可见性判断。所以对于DML来说,所有提交的事务都是可见的。
      break;
    case DB_LOCK_WAIT:
	      /* Never unlock rows that were part of a conflict. */
	  // 如果存在锁冲突,也就是其他事务正在更新同一行
      prebuilt->new_rec_locks = 0;

      if (UNIV_LIKELY(prebuilt->row_read_type
          != ROW_READ_TRY_SEMI_CONSISTENT)
          || unique_search
          || index != clust_index) {

        goto lock_wait_or_error;
      }

      /* The following call returns 'offsets'
      associated with 'old_vers' */
	  // 这里需要查看是否有别的事务提交了,以便获取最新版本的记录
      row_sel_build_committed_vers_for_mysql(
        clust_index, prebuilt, rec,
        &offsets, &heap, &old_vers, &mtr);

      /* Check whether it was a deadlock or not, if not
      a deadlock and the transaction had to wait then
      release the lock it is waiting on. */
	        err = lock_trx_handle_wait(trx);

      switch (err) {
      case DB_SUCCESS:
        /* The lock was granted while we were
        searching for the last committed version.
        Do a normal locking read. */

        offsets = rec_get_offsets(
          rec, index, offsets, ULINT_UNDEFINED,
          &heap);
        goto locks_ok;
      case DB_DEADLOCK:
        goto lock_wait_or_error;
      case DB_LOCK_WAIT:
        err = DB_SUCCESS;
        break;
      default:
        ut_error;
      }
	        if (old_vers == NULL) {
        /* The row was not yet committed */

        goto next_rec;
      }
	  did_semi_consistent_read = TRUE;
      rec = old_vers;
      break;
    default:

      goto lock_wait_or_error;
    }

	}

从上面的代码我们可以看到,对于UPDATE操作更新的记录包含幻读读取到的已提交事务的最新记录。那么接下来看为什么UPDATE之后的SELECT语句对于UPDATE之后的所有语句都可见了? 原因是前面的UPDATE语句执行之后,会将当前记录上存储的事务信息更新为当前的事务,而当前事务所做的任何更新,对本事务所有SELECT查询都变的可见,因此最后输出的结果是UPDATE执行后更新的所有记录。

当前各种数据库对于隔离级别的支持不尽相同,比如ORACLE,它只实现了READ COMMITTED和SERIALIZABLE两种ANSI SQL STANDARD规定的隔离级别(这里ORACLE还实现了一种自定义的READ ONLY隔离级别,具体请参考https://docs.oracle.com/cd/B28359_01/server.111/b28318/consist.htm#CNCPT621) , 而没有实现REPEATABLE READ。对于相同的隔离级别,不同的数据库有着自己不同的实现方式。所以我们在理解隔离级别的时候需要针对具体的数据库。综上所述,我们看到了MySQL InnoDB引擎对于REPEATABLE READ隔离级别有着不同于其他数据库的实现方式。而该实现方式符合ANSI SQL STANDARD,并非属于实现上的BUG。

MySQL · myrocks · MyRocks之memtable切换与刷盘

$
0
0

概述

MyRocks的memtable默认是skiplist,其大小和个数分别由参数write_buffer_size和max_write_buffer_number控制。数据写入时先写入active memtable, 当active memtable写满时,active memtable会转化为immutable memtable. immutable memtable数据是不会变化的,最终会刷入level0的sst文件中。
屏幕快照 2017-06-16 上午6.16.58.png

memtable 内存分配

RocksDB有自己的内存分配机制,称为Arena. Arena由固定的inline_block_和动态的blocks_组成。
inline_block_固定为2048bytes, blocks_由一系列的block组成,这些block大小一般为KBlockSize, 但从arena申请较大内存时(> KBlockSize/4)单独分配一个所申请大小的block. KBlockSize由参数arena_block_size指定,arena_block_size 不指定时默认为write_buffer_size的1/8.

屏幕快照 2017-06-16 上午6.56.39.png

这里有两个重要的概念

  • blocks_memory_
    Arena当前已分配的内存

  • alloc_bytes_remaining_
    Arena当前block已分配但未使用的内存,注意不是整个Arena已分配而未使用的内存

RocksDB在实际使用内存中用的是ConcurrentArena, 它是在Arena的基础上封装,是线程安全的。
同时ConcurrentArena为了提高并发对内存进行了分片,分片数由cpu个数决定,例如cpu核数为24, 则分片数为32,以下是分片的算法

// find a power of two >= num_cpus and >= 8
  auto num_cpus = std::thread::hardware_concurrency();
  index_mask_ = 7;
  while (index_mask_ + 1 < num_cpus) {
    index_mask_ = index_mask_ * 2 + 1;
  }

  shards_.reset(new Shard[index_mask_ + 1]);

每个分片都有已分配但未使用的内存, 分片越多浪费的内存越多。

一个有趣的例子

测试环境:CPU核数64,write_buffer_size=1G, arena_block_size=0
根据前面的算法,CPU核数64, 内存分片数为64, arena_block_size 默认为write_buffer_size的1/8,对齐后是131072000

我们用1200个连接进行并发插入,这样能够充分使用内存分片数
这是测试某个瞬间取得的内存数据

allocated_memory:1179650048
AllocatedAndUnused:1172297392
write_buffer_size:1048576000
BlockSize:131072000

注意AllocatedAndUnused和allocated_memory是如此的接近,也就是说存在巨大的内存浪费。然而这不是最严重的,更严重的是这种情况导致memtable的切换,后面会进行分析。

memtable 切换

memtable 发生切换的条件有

  1. memtable内存超过write_buffer_size会切换
  2. WAL日志满,WAL日志超过rocksdb_max_total_wal_size,会从所有的colomn family中找出含有最老日志(the earliest log containing a prepared section)的memtable进行切换,详见HandleWALFull
  3. Buffer满,全局的write buffer超过rocksdb_db_write_buffer_size时,会从所有的colomn family中找出最先创建的memtable进行切换,详见HandleWriteBufferFull
  4. flush memtable前会切换memtable, 下节会介绍

下面详细介绍memtable满切换

  • memtable 满切换

memtable内存超过write_buffer_size会切换,由于arena的内存使用,memtable控制内存使用的算法更加精细,切换条件从源码中很容易理解

bool MemTable::ShouldFlushNow() const {
  // This constant variable can be interpreted as: if we still have more than
  // "kAllowOverAllocationRatio * kArenaBlockSize" space left, we'd try to over
  // allocate one more block.
  const double kAllowOverAllocationRatio = 0.6;

  // If arena still have room for new block allocation, we can safely say it
  // shouldn't flush.
  auto allocated_memory = table_->ApproximateMemoryUsage() +
                          range_del_table_->ApproximateMemoryUsage() +
                          arena_.MemoryAllocatedBytes();

  // if we can still allocate one more block without exceeding the
  // over-allocation ratio, then we should not flush.
  if (allocated_memory + kArenaBlockSize <
      moptions_.write_buffer_size +
      kArenaBlockSize * kAllowOverAllocationRatio) {
    return false;
  }

  // if user keeps adding entries that exceeds moptions.write_buffer_size,
  // we need to flush earlier even though we still have much available
  // memory left.
  if (allocated_memory > moptions_.write_buffer_size +
      kArenaBlockSize * kAllowOverAllocationRatio) {
    return true;
  }

 return arena_.AllocatedAndUnused() < kArenaBlockSize / 4;
}

而上一节举出的例子正好符合切换的条件,正如前面所说的,内存都分配好了,还没来得及使用就发生切换了,白忙活了一场。

这里的现象是虽然write_buffer_size是1G,但最后刷到level0的sst都远远小于1G。

那么如何避免这种情况呢

  • 减少内存分片数,不建议
  • 调小arena_block_size, 亲测可用

这里有一个原则是arena_block_size*内存分片数应该小于write_buffer_size

  • memtable 切换实现
  1. NewWritableFile //创建日志文件
  2. ConstructNewMemtable //创建memtable
  3. cfd->imm()->Add(cfd->mem(), &context->memtables_to_free_); //设置immutable
  4. cfd->SetMemtable(new_mem); //设置新的memtable

flush memtable

immutable memtable会不断flush到level0的SST文件中

触发flush的条件有

  • WAL日志满,WAL日志超过rocksdb_max_total_wal_size,会从所有的colomn family中找出含有最老日志(the earliest log containing a prepared section)的column family进行flush,详见HandleWALFull
  • Buffer满,全局的write buffer超过rocksdb_db_write_buffer_size时,会从所有的colomn family中找出最先创建的memtable的column family进行flush,详见HandleWriteBufferFull
  • 手动设置参数force_flush_memtable_now/rocksdb_force_flush_memtable_and_lzero_now时
  • CompactRange时
  • 创建checkpoint时
  • shutdown时avoid_flush_during_shutdown=0会flush所有memtable

other

rocksdb中设置max_background_flushes=-1可以禁止flush,而MyRocks中rocksdb_max_background_flushes最小值限制为0. 因此,MyRocks若要禁止flush需放开此限制。

PgSQL · 最佳实践 · 云上的数据迁移

$
0
0

背景

大多数使用云产品作为 IT 解决方案的客户同时使用多款云产品是一个普遍现象。
用户在多款云产品之间转移数据成为一个基础的需求。

例如

  • 1. 用户把线下机房中的 Oracle 数据库中的数据 迁移到云上 RDS PPAS 中。
  • 2. 使用 RDS MYSQL 做为数据库支撑交易型业务场景,同时使用 HybridDB for PostgreSQL 作数据仓库解决方案。
  • 3. 把 ODPS 中的大量离线数据导入到 HybridDB for PostgreSQL 进行实时分析。

上述场景都不可避免的需要进行云上的数据迁移。本文给大家聊聊这方面的一些解决方案,希望能帮助大家用好云产品。

一:关于硬件

在开始数据迁移之前,我们要对云相关的硬件有一些了解,这往往决定了我们能做到的最好情况,有助于我们选择最终解决方案。

1. 同一可用区

如果数据在云上,且在同一可用区间进行交换,那么恭喜你,这是最有效率的数据交换方式,也是我们最推荐的场景。用户的数据应该尽量在一个可用区。

现阶段的云产品所配置的网络最差都是千兆网络,万兆已经基本普及。数据的迁移在一个可用区间经过的交换机最小,因此延迟低,带宽较大,可以最大比较理想的吞吐量。

因此,后端数据库、前端的 ECS、存在大量数据的的 OSS 都应该在选择在同一个可用区。

2. 跨可用区、城市间可用区

部分有较高可用性要求的客户,会选择同城多可用区部署,甚至跨城市部署。进一步,阿里云有很多数据产品支持原生的多可用区部署方案。

阿里云在同城或跨城市的可用区间是通过网络专线连接。在这样的网络产品中交换数据效率虽然没有再同一可用区高,但依然能保证较高的网络质量。

从网络通讯效率角度,自然是:

同可用区 > 同城多可用区间 > 跨城多可用区间

例如:

(华东一 可用区B 内部) > (华东一 可用区B 和 华东一 可用区C 间) > (华东一 可用区B 和 华北一 可用区B 间)

3. 公网和 VPN 网络

这是效率最差的情况,也是背景章节中的数据上云场景的典型。因为该场景的物理通道是公共的且不可控。往往延迟较大,且质量有较大波动。

先天不足的情况,自然需要用软件做适当的弥补,通常建议用户选取具有下列特性的服务。

  • a: 支持重试机制,支持断点续传,大任务不能由于一个异常导致整个失败。
  • b: 支持并发机制,使用大并发增大吞吐量。
  • c: 使用增量数据迁移减少服务的停机时间。

接下来聊一聊数据交换中的数据格式问题

二:关于数据格式

在不同数据产品间转移数据通常有两种方式

1. 不落地的数据迁移

软件或服务同时连接到源数据端和目的端,把数据从源端拉出来,转换成目的端识别的格式后立即写入到目的端。

该和方法不需要数据中转空间,但要求的网络质量较高。如果数据量超大,如上 TB,那么迁移时间也比较长。

阿里云开源产品 rds_dbsync CDP, 云服务 DTS 都属于这类。

2. 通过通用文件格式的数据迁移

如果您的数据量较大,则建议使用离线迁移转移数据,例如几十 TB 的数仓数据。
离线迁移是把全量数据导出成一种通用的数据组织格式,再导入到目的数据库。

相比不落地数据迁移,他有这些优势

  • 1)离线导出的数据通常都会进行压缩,压缩一般都在 1:2 到 1:5 之间,能较大节省网络开销,从而提升整体效率。
  • 2)离线方式很容易并行化,并行是提高效率的最有效手段。

基于通用文件的数据迁移,数据文件的格式是其中的关键。文件需要明确的交代清楚数据的组织方式。

目前常用的通用文件格式有 TXT/CSV TSV ORC Parquet protobuf 等。
这里部分数据格式已经自带数据压缩,例如 ORC Parquet。 对于未压缩的格式,如 CSV 可以自由选择数据压缩格式,例如 gzip bzip2 snappy 等。

2.1 通过 TEXT/CSV 文件中转数据

  • 对于结构化数据,比较理想的数据格式是 CSV,CSV 是一种通用的数据格式标准,大家可以参考资料1
  • PostgreSQL CSV 参数在资料2中。适用于社区和阿里云的 PostgreSQL 已经 Greenplum 和 HybridDB for PostgreSQL。
  • 任何符合 CSV 标准的文件都可以导入 PostgreSQL 系列产品。
    • PostgreSQL 推送式导入数据 COPY
    • HybridDB for PostgreSQL 推送式写数据 COPY

CSV 相对简单的文本格式的优势是定义了清楚的语意,用于很容易处理一些复杂的场景

  • CSV 行分割符号是 ‘\n’ 也就是换行符
  • 定义 DELIMITER 用于定义列的分割符
    • 当用户数据中包括 DELIMITER 时,则需要配合 QUOTE 参数。
    • DELIMITER 需要是单字节字符,推荐是 ‘|’,‘,’ 或一些不常出现的字符。
  • QUOTE 以列为单位包裹有特殊字符的用户数据
    • 用户包含有特殊字符的 text 类型字符串会被 QUOTE 包裹,用于区分用户数据和 CSV 控制数据。
    • 如果不必要,例如整数,数据不会被 QUOTE 包裹(优化效率)。
    • QUOTE 不能和 DELIMITER 相同,默认 QUOTE 是双引号。
    • 当用户数据中包含了 QUOTE 字符,则需要设置转义字符 escape。
  • ESCAPE 特殊字符转义
    • 转义用户数据中的和 QUOTE 相同的字符。
    • ESCAPE 默认和 QUOTE 相同,也就是双引号。
    • 也支持设置成 ‘\’(MySQL 默认的转义字符)。

2.2 用 OSS 中专数据

OSS 和 AWS 的 S3 一样,是云上廉价的存储服务,它打通了几乎所有的云产品。我们推荐用户使用它来中专大容量数据。

OSS 支持跨可用区数据转储数据(跨区域复制),用户可以很高效的把大量数据转移到另一个可用区。

目前,云裳的 PostgreSQL 和 HybridDB for PostgreSQL 都支持 OSS 数据源的读写。

  • PostgreSQL + OSS 读写外部数据源 oss_fdw
  • HybridDB for PostgreSQL + OSS 并行的导入导出数据 oss_ext

总结

本期分享了云上和数据转移相关的几个简单技巧,希望能帮到大家用好云。我们的产品在快速迭代,也请大家多反馈问题,帮助我们进步。

参考资料

  1. CSV 格式标准
  2. PostgreSQL COPY
  3. PostgreSQL + OSS oss_fdw
  4. HybridDB for PostgreSQL COPY
  5. HybridDB for PostgreSQL + OSS oss_ext
  6. rds_dbsync

MySQL · 社区新闻 · MariaDB 10.2 GA

$
0
0

简介

2017-5-23,MariaDB终于GA了,我们贡献的Flashback也作为正式功能发布了。当然还存在几个Bug,不过截止本文发稿之前已经提交了Fix,目前Flashback没有发现新的Bug。阿里云上我们也即将公开我们的Flashback用户接口。
MariaDB 10.2 将成为目前主要的稳定版本,然后官方承诺支持到2022年5月。

我们就来看下10.2发布了哪些新的特性和功能:

  • InnoDB 成为了默认引擎
    直到10.1版本,MariaDB还是以XtraDB作为默认引擎。考虑到截止10.2发布之前,MariaDB认为XtraDB还不足够稳定,因此不再作为默认引擎(当然开源社区也是有江湖的。。。。)。官方还写了一篇文章专门阐述为啥要用InnoDB而不用XtraDB了:https://mariadb.com/kb/en/mariadb/why-does-mariadb-102-use-innodb-instead-of-xtradb/

  • 语法和通用功能
    • MyRocks成为了可用引擎,然而还是Alpha版,只能尝尝鲜 (MDEV-9658)。
    • Window functions(窗口函数)已经可以使用了。
    • 增加了 SHOW CREATE USER 语法可以看创建用户的语句
    • 新的 CREATE USER 选项,支持资源限制(例如每小时的最大语句执行量,每小时最大更新量之类的)和TLS/SSL
    • 新的 ALTER USER 语法支持修改资源限制。
    • Recursive Common Table Expressions (MDEV-9864),可以利用CTE写复杂的分析语句了。
    • 支持 WITH 语句。 WITH 是CTE的一个语法,允许用户在一个查询中多次引用一个子查询表达式(MDEV-8308 & MDEV-9864) — 来自于 Galina Shalygina 的贡献
    • 支持 CHECK CONSTRAINT 语法 (MDEV-7563)
    • 支持表达式的默认值,之前MySQL字段默认值只能是常量或者时间戳,现在可以写各种表达式来计算默认值了 (MDEV-10134)
    • BLOB 和TEXT 字段支持默认值了。
    • 为虚拟计算列加了很多约束过滤条件,细节可以参照 https://mariadb.com/kb/en/mariadb/virtual-computed-columns/
    • DECIMAL 字段支持的精度从30位增加到了38位,兼容Oracle (MDEV-10138)。
    • List分区方式增加了一个DEFAULT选项,可以把没有匹配到的值全部塞到DEFAULT分区 (MDEV-8348)
    • Oracle风格的 EXECUTE IMMEDIATE 语句 (MDEV-10585)
    • PREPARE 目前能解析大部分表达式了,比如 PREPARE stmt FROM CONCAT(‘SELECT * FROM ‘, table_name); (MDEV-10866)
    • InnoDB 也能支持空间索引了!
    • 增加 ed25519 验证插件 (MDEV-12160)
    • 更友好的InnoDB崩溃恢复过程报告,之前信息实在是太多,做了简化 (MDEV-11027)
    • 优化了InnoDB启动和关闭过程代码流程。
    • 为 Windows, CentOS, RHEL, Fedora 发行版的包增加了AWS Key Management插件。
    • 原子写除了能支持FusionIO,也能自动识别宝存的SSD卡了。
  • 不向下兼容的改动
    • TokuDB从默认编译中剔除了,现在作为一个独立的引擎包 mariadb-plugin-tokudb,需要自行安装,默认是没有的
    • SQL_MODE 变化了;特别是,没有默认值的 NOT NULL 字段在插入时如果没有带一个值,不会再提供一个伪造的值进去了,直接报错。
    • 从老版本的MySQL服务器复制到MariaDB需要设置binlog_checksum为NONE,因为不兼容。
    • 新的保留字: RECURSIVE 和 ROWS。
  • 触发器
    • 同一个时间可以创建多个触发器了,这点跟Oracle一样了,之前每张表每种触发事件只能创建一个触发器 (MDEV-6112)。
    • CREATE TRIGGER 语句增加了 FOLLOWS/PRECEDES 子句,用以表示当前的触发器跟同类型的触发器执行顺序谁先谁后。
    • 触发器的执行都被计入了 Executed_triggers 状态变量 (MDEV-10915)
    • SHOW TRIGGERS and SHOW CREATE TRIGGER now include the date and time the trigger was created
  • 复制和Binlog
    • 支持DML闪回的Flashback工具已经可以用了(MDEV-10570) – 这就是我贡献的代码了。
    • 新变量 read_binlog_speed_limit 可以用以限制Slave从出库读取Binlog的速度 (MDEV-11064) — 这是腾讯互娱DBA团队的贡献。
    • 支持延迟复制,备库可以强制比主库延迟一段时间 (MDEV-7145)。
    • 提供Binlog中的Event压缩功能 (MDEV-11065) – 也是腾讯互娱DBA团队的贡献。
    • 默认Binlog格式修改为MIXED (MDEV-7635)。
    • replicate_annotate_row_events 默认值改为 ON (MDEV-7635)。
    • slave_net_timeout 默认值减小到 60 秒 (MDEV-7635)。
    • 默认 server_id 从0 修改为 1。
  • GeoJSON / JSON
    • JSON 函数支持(MDEV-9143)
    • 实现了ST_AsGeoJSON 和 ST_GeomFromGeoJSON 函数,因此空间特性可以用GeoJSON 格式导入导出(MDEV-11042)
  • Information Schema
    • 增加了一个插件可以报告所有用户的变量值,在 USER_VARIABLES 表中 (MDEV-7331)。之前经常会遇到问题就是客户说他的变量值不对,我们却无法验证,这个功能很好的解决了这个问题。
  • EXPLAIN
    • EXPLAIN FORMAT=JSON 现在会在 outer_ref_condition 列显示每次循环匹配时SELECT检查的条件 (MDEV-9652)。
    • EXPLAN FORMAT=JSON 现在会在 sort_key 列回显示filesort操作使用的排序规则 (commit 2078392)。
    • EXPLAIN 曾经错误的展示了优化器如何决定ORDER BY子句和DISTINCT操作。这是个长期存在的问题了,包括MySQL本身。在 MDEV-8646 这个Issue中解决了这个问题(MDEV-7982, MDEV-8857, MDEV-7885, MDEV-326中有Test Case)。
  • 优化点
    • 设置连接更快了,因为把THD创建的工作挪到了新创建的线程中,之前是管理所有连接的那个线程来统一创建THD (MDEV-6150)。
    • 条件下推到non-mergeable的视图和子表中 (MDEV-9197) — 还是 Galina Shalygina 贡献的代码
    • ANALYZE TABLE 的代码重构了,在收集引擎无关的统计信息时并不需要锁住整个表 (MDEV-7901)。
    • 内部 CRC32 函数在Power8下使用了优化过的实现 — MDEV-9872。
    • Table cache 可以自动分区来减少冲突 (MDEV-10296)。
  • 兼容性
    • NO PAD的校对字符集,就是说,在比较字符串时,不会再启动处理掉末尾的空格 (MDEV-9711) — 这是 Daniil Medvedev 贡献的代码。
    • MariaDB 目前可以用于启动高于 MySQL 5.7.6 版本的数据文件目录 (MDEV-11170)。
  • CONNECT引擎
    • CONNECT引擎支持ZIP压缩文件的表 (MDEV-11295)。
    • CONNECT引擎目前支持JDBC表类型 (MDEV-9765)。
  • 系统变量
    变量的改动如下:
    • 可以关闭死锁检测新变量 innodb_deadlock_detect,这个也是阿里提供的思路,淘宝很早就在使用这个功能。
    • aria_recover 重命名为 aria_recover_options (MDEV-8542)。
    • aria_recover 和 myisam_recover_options 的默认值修改为 BACKUP,QUICK。
    • 服务器版本可以随便伪造一个字串,因为有的应用会检查版本号 (MDEV-7780),我们也遇到过这种问题,一样的思路。
    • slave_parallel_workers 目前作为 slave_parallel_threads 的同义变量。
    • 新状态变量 com_alter_user, com_multi and com_show_create_user。
    • 新变量 innodb_tmpdir 可以设置一个目录来存储InnoDB临时表文件。
    • 新变量 read_binlog_speed_limit permits 可以限制Slave读取Binlog的速度 (MDEV-11064)。
    • innodb_log_files_in_group 目前可以设置为1 (MDEV-12061).
    • 线程池现在可以给有活跃事务的连接更高的优先级。这可以通过新的 thread_pool_prio_kickup_timer 和 thread_pool_priority 变量来控制 (MDEV-10297)。
    • group_concat_max_len 的默认值改为 1M (MDEV-7635)。
    • sql_mode 默认值改为STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO, NO_AUTO_CREATE_USER, NO_ENGINE_SUBSTITUTION (MDEV-7635) (MariaDB 10.2.4 开始)。
    • innodb_compression_algorithm 默认值改为 zlib - 但是这并不意味着页面默认就会压缩 (MDEV-11838)。
    • innodb_log_compressed_pages 默认值改为ON (MDEV-7635)。
    • innodb_use_atomic_writes 和innodb_use_trim changed 默认值改为ON。
    • 没用的 innodb_api_* 变量被删除了 (MDEV-12050)。
  • 新的状态变量
    • innodb_have_punch_hole
    • innodb_pages0read
    • innodb_scrub_log
    • innodb_encryption_num_key_requests
  • 脚本
    • mysqlbinlog 增加了连续Binlog备份支持,利用–stop-never变量,可以一直等待新的日志 (MDEV-8713)。
    • mysql_zap 和 mysqlbug 被移除了 (MDEV-7376, MDEV-8654)。
  • 其他改动
    • 添加 OpenSSL 1.1 和 LibreSSL 的支持 (MDEV-10332)。
    • 在InnoDB持久化 AUTO_INCREMENT (MDEV-6076),这是从我们AliSQL中Port的功能。
    • 支持 COM_RESET_CONNECTION (MDEV-10340)。
    • “fast mutexes” 被移除了。因为这玩意并不比普通的mutex快,已经被默认关闭很多年了 (MDEV-8111)。
    • 旧的GPL客户端库已经去掉了,现在MariaDB Server使用了新的LGPL的Connector/C客户端链接库。
    • MariaDB 不再使用 jemalloc 编译。
    • TokuDB 现在是一个独立的引擎包,不再是MariaDb打包的默认组件 (因为TokuDb还需要Jemalloc)。

最新的MariaDB 10.2 GA就是这些改动了,希望对大家有帮助,也希望更多人参与MariaDb的开发,看到更多的人给MariaDB贡献代码!

Viewing all 692 articles
Browse latest View live