显示标签为“innodb”的博文。显示所有博文
显示标签为“innodb”的博文。显示所有博文

2012年4月4日星期三

对innodb MVCC实现的一点点思考

不得不说MVCC思想对关系数据库的影响很大,读不阻塞写,写也不会阻塞读,大大提高了并发性。当前主流的数据库基本上都实现了MVCC,比如Oracle、MySQL、PostgreSQL等等。现在来谈谈MySQL(innodb)是怎么实现MVCC的,它的优缺点是什么。

innodb对MVCC的实现是通过在每个事物开启时创建一个当前系统活跃事务的副本(read_view),然后每次读取行的时候,通过这个行上的trx_id与read_view里面的trx_id进行比较。说到行上的trx_id,那么首先得说明一下innodb在每行上有三个隐藏字段:DB_ROW_ID、DB_TX_ID(这个就是行上的事务id)、DB_ROLL_PTR(指向当前行的回滚段指针)。具体的源码如下:

/*********************************************************************//**
Checks if a read view sees the specified transaction.
@return TRUE if sees */
UNIV_INLINE
ibool
read_view_sees_trx_id(
/*==================*/
const read_view_t* view, /*!< in: read view */
trx_id_t trx_id) /*!< in: trx id */
{
ulint n_ids;
ulint i;


if (trx_id < view->up_limit_id) {


return(TRUE);
}


if (trx_id >= view->low_limit_id) {


return(FALSE);
}


/* We go through the trx ids in the array smallest first: this order
may save CPU time, because if there was a very long running
transaction in the trx id array, its trx id is looked at first, and
the first two comparisons may well decide the visibility of trx_id. */


n_ids = view->n_trx_ids;


for (i = 0; i < n_ids; i++) {
trx_id_t view_trx_id
= read_view_get_nth_trx_id(view, n_ids - i - 1);


if (trx_id <= view_trx_id) {
return(trx_id != view_trx_id);
}
}


return(TRUE);
}

在我参考的一片文章[1]里面,作者说每次与行上的trx_id比较的时候
并不是根据当前事务的tx id,而是根据read view最早一个事务的tx id(read view->up_limit_id)来做比较的,我觉得作者只说对了一部分,这个read_view->up_limit_id确实是一个参考的参数(up_limit_id是当前事务的read_view里面trx_id最小的,这就好比我们求素数的时候,首先直接将n%2==0的剔除,这里的与read_view->up_limit_id比较也是一样的效果,实际上再源码里面也有说明),而在for循环里面我们可以看到,从read_view里面找到一个view_trx_id >= trx_id(行当前的事务id),至于为什么trx_id!=view_trx_id,这是因为两者相等的话,那就是同一个事务id,与自己比较没有意义。那么最后return(TRUE)呢?这是因为如果事务开启的时候没有活动的事务,那么read_view也就为空(本质上是view_n_trx_ids=0)这种情况自然事务可以读当前行。

那么innodb这样设计有什么缺点吗?依据我自己的见解,这样中设计比较浪费内存,因为如果在极端的情况下,有些事务耗时比较长,系统事务比较多的情况下,那么每一个新的事务开启的时候,都会导致read_view(当前活动事务副本)需要被创建一份,而且创建read_view的开销肯定也不小。Oracle的设计就不同,貌似它是在每个block(或者行)上有一个事务槽,所以每次去判断是直接读当前行还是读undo段时就利用当前事务的id与槽里面的事务id进行比较,这样应该来得比较innodb这种设计高效吧。




参考文章:


2012年3月10日星期六

[转载]How Logs Work On MySQL With InnoDB Tables

这篇帖子个人感觉写得非常棒,不转不行


In this article I will describe how logs work with MySQL and InnoDB. Very little of this material is in the manual. I gleaned it from the source code while researching for our latest book (SQL Performance Tuning by Peter Gulutzan and Trudy Pelzer).
If you've ever worked with MySQL and InnoDB tables then this article should give you some "insider" knowledge that you wont find anywhere else, so sit back and get ready to read!

Page 1
When you change data with UPDATE, INSERT or DELETE queries you're changing the data in two places: the log buffer and the data buffers. Buffers are fixed-length, typically a multiple of 512 bytes. And they are in memory – InnoDB won't write them to disk... yet.

LOG BUFFER           DATA BUFFER
=================    ===============
= Log Record #1 =    = Page Header =
= Log Record #2 =    = Data Row    =
= Log Record #3 =    = Data Row    =
= Log Record #4 =    = Data Row    =
=================    ===============

For example, after "INSERT INTO Jobs VALUES (1,2,3)" the log buffer will have one new log record -- call it Log Record #5 -- containing a row identifier and the new contents. Meanwhile, the data buffer will have one new row, but it will also have a mark in the page header saying "the latest log record for this page is Log Record #5". In this example "#5" is the Log Sequence Number (LSN), and it's vital for scheduling later operations.

Some details about the data-change:

(a) An INSERT log record contains only new data, which is enough so that the procedure can be repeated on the same page if necessary. This is called a "redo" entry.

(b) The LSN isn't a field in the log record, instead it's an absolute address for a file and byte offset.[1]

After InnoDB has changed the log buffer and the data buffers, it's all over but the disk writing. But that's where things get complex. There are several threads monitoring buffer activity, and three situations -- overflow, checkpoint, and commit -- that result in disk writes.

What Happens With Overflows?
Overflow is rare because InnoDB takes pro-active measures to prevent buffers from filling up (see "what happens with checkpoints" below). Still, let's discuss the two possible cases.

One: if the log buffer gets full, InnoDB writes the buffer at the "end" of the log. I've put the word "end" inside quotes because a log file, or more precisely a group of log files, looks like a snake swallowing its tail. If there's only room for four log records and we're writing #5, then it will have to go at the start of the file.

LOG FILE(S) BEFORE WRITING LOG RECORD #5
=================
= Log Record #1 =
= Log Record #2 =
= Log Record #3 =
= Log Record #4 =
=================

LOG FILE(S) AFTER WRITING LOG RECORD #5
=================
= Log Record #5 =
= Log Record #2 =
= Log Record #3 =
= Log Record #4 =
=================

There's no such thing as a log that grows forever. Even though InnoDB uses some compression tricks, the log files would get too big to fit on any disk drive. So InnoDB writes "in a circle" and that means it must overwrite old log records. This circular logging policy has implications which we'll come back to later.

Two: if the data buffers get full, InnoDB writes the least recently used buffer to the database -- but not too quickly! This is where the page header's LSN becomes useful. First, InnoDB checks whether it's greater than the LSN of the last log record in the log file. If it's greater, then InnoDB must write out log records first, until the log catches up with the data, and only then can it write the data. In other words data page writing cannot occur until writing of the corresponding log record has occurred. That is the "Write-Ahead Logging" principle which is common to all important DBMSs except InterBase.

What Happens With Checkpoints?
I said that InnoDB takes some pro-active measures against overflows, and the most important of these measures is checkpointing. There is a separate thread, or a combination of threads that are separate from the thread that changes the buffers. At fixed intervals the checkpointer will wake, look for buffer changes, and ensure that writes happen.

By my count, most DBMS's would write everything at this time so there are no buffers left which are changed but unwritten. To use the usual jargon, the DBMS would flush all "dirty" buffers with a "Sharp Checkpoint". But InnoDB only ensures that (a) log and data buffers aren't getting fuller than a fixed threshold point, (b) log writing stays ahead of data page writing, (c) there is no data buffer whose page-header LSN corresponds to a log record that's about to be overwritten. In the jargon, this means InnoDB is a "Fuzzy Checkpoint" aficionado.

At checkpoint time it's possible to write another log record which says, in effect: at this moment a checkpoint occurred, so it's certain that the database is up to date except for a few dirty pages, and here is a list of the dirty pages. This information can be useful during a recovery, so I'll mention it again later.

Page2

At COMMIT time, InnoDB will NOT write all dirty data pages to disk. I emphasize that because it's easy to think that committing changes means writing everything on a persistent medium. Well, the InnoDB folks are smarter than that. They realize that only the log records need writing. The writing of dirty data pages can happen at overflow or checkpoint time, because their contents are redundant. If the log survives a crash, remaking of data pages is possible using the information in the log records.[2]

So InnoDB should only write the log. Or to be exact, InnoDB should write log records, until it has written all log records which apply to the transaction that's committing. Since all log writing is serial, that means InnoDB must write log records for other transactions too, but that's okay.[3]

Here I must get critical, because that's not what InnoDB does, necessarily. If MySQL's my.cnf's innodb_flush_log_at_trx_commit switch is zero, then InnoDB will avoid log writing at commit time. This means that a successful COMMIT won't "guarantee" that all data changes have become persistent, which is what the ANSI/ISO standard demands. The persistence guarantee applies only for checkpoints.

Anyway, you can set innodb_flush_log_at_trx_commit to one. In that case all's well, InnoDB will write the log. Also InnoDB will flush.

I'd better explain what flushing is, eh? Usually it's enough to merely write, but all modern operating systems will cache writes for efficiency reasons. To get the "guarantee" InnoDB must insist to the operating system "I really mean write, I want that disk write head on a platter, don't return till the physical operation is complete." This means that on a Windows system InnoDB calls the Windows-API function FlushFilBuffers, a call meaning "flush the cache." Here InnoDB parts company with Microsoft: SQL Server 2000 would use a "write through" option during the write, rather than flushing after the write.

Recovery
We come now to the point that makes all the logging trouble worthwhile: if a crash happens, you can recover your data.

For a crash that didn't melt the disk drive, recovery is automatic. InnoDB reads the last checkpoint log record, sees if the "dirty pages" were written before the crash, and (if they weren't) reads the log records which affected the page and applies them. This is called "rolling forward" and it's easy for two reasons: (1) because the LSN is there so InnoDB just has to compare numbers to get into synch, (2) because I left out a few details.

Fine. Now, what about a crash that did melt the disk drive? Then the recovery scenario depends on your preparation.

Scenario one: the log is gone. Well, you should have prepared by having a log copy on a separate drive. InnoDB has no explicit option but there are operating-system-specific ways.

Scenario two: the database is gone and the log is overwritten. Well, you should have anticipated that with circular logging, log record #5 will overwrite log record #1. Remember? Therefore if you didn't take a backup after the writing of log record #1, you've lost data.

Scenario three: the database is gone and the log is okay. In that case, congratulations. You just have to restore your last backup copy, and roll the whole log forward. There would be complications if you had to back up the log itself several times since the last full backup ("archive logging"), but I've assumed that option is currently inoperative. By the way, I am not discussing the role of MySQL's binlog. It's essential to the process but isn't part of InnoDB, so out of scope.

Page4
With an understanding of InnoDB logging, you know some things need watching. In no particular order:
use large log files so overwriting won't happen since backups
  • keep log files and data files on separate disk drives
  • make sure innodb_flush_log_at_trx_commit is set right

2012年3月9日星期五

innodb_fast_shutdown的内幕


参数innodb_fast_shutdown提醒了我一直没研究过的innodb特性之一——insert buffer。首先简述这个参数,然后谈谈insert buffer的实现。

Innodb_fast_shutdown告诉innodb在它关闭的时候该做什么工作。有三个值可以选择:
1.  0表示在innodb关闭的时候,需要purge all, merge insert buffer,flush dirty pages。这是最慢的一种关闭方式,但是restart的时候也是最快的。后面将介绍purge all,merge insert buffer,flush dirty pages这三者的含义。
2.  1表示在innodb关闭的时候,它不需要purge allmerge insert buffer,只需要flush dirty page
3.  2表示在innodb关闭的时候,它不需要purge allmerge insert buffer,也不进行flush dirty page,只将log buffer里面的日志flushlog files。因此等下进行恢复的时候它是最耗时的。

那么在mysql restart的时候它的恢复流程(也称作crash recovery)是怎么样的呢?
1.   如果在上次关闭innodb的时候是在innodb_fast_shutdown=2或是mysql crash这种情况,那么它会利用redo log重做那些已经提交了的事务。
2.   接下来的操作就是这么几个:
a>     Rollback uncompleted transitions 取消那些没有提交的事务
b>     Purge all 清除无用的undo
c>      Merge insert buffer 合并插入缓冲

下面详解purge allmerge insert bufferflush dirty page
1.   Purge all 这个操作主要是删除那些无用的undo页。对于delete操作,innodb是通过先将要删除的那一行标记为删除,而不是马上清除这一行,因为innodb实现了MVCC,这些undo段用来实现MVCC机制。MVCC也就是常说的多版本控制,即在读的时候不会加排他锁,这样大大提高了并发性。那么在一致性读的时候,怎么才能找到和事务开始的那个版本呢?对于主键索引,每个行都有一个事务ID和一个undo ID,这个undo ID指向了这行的先前版本的位置。对于非主键索引,也就是常说的secondary index,是通过先找主键索引再找到undo段。而对于update操作,则是先标记删除,然后insert一个新的行,接下来如果有一致性读,那么查找old version的行的原理和delete操作是一样的,详情见[1]。现在接着说purge all操作,随着DML的操作越来越多,那么回滚段必然也会越来越多导致占用了许多磁盘空间,那么innodb就会定期删除一些无用的undo页,首先,innodb重启的时候必然undo页都会无效所以会进行purge all操作,另外,随着时间的推移必然一些事务已经完成,它们已不再需要某些undo页,那么这些undomysqld running的时候也会定期的进行清除,主要是在master thread中进行,虽然mysql5.5里面增加了一个参数innodb_purge_threads来进行purge工作,但是这个参数的默认值是0,手册上解释说这个功能在mysql5.5中还不完善,增加它的目的只是表明这是innodb的发展方向。
2.   Merge insert buffer
Insert bufferinnodb的一个特性之一,在非聚簇、且不是唯一索引(即非主键索引、非唯一索引)的情况下,如果插入的索引行所属的页在buffer pool中就直接更新这个页,否则它会将这个索引行插入到insert buffer中,然后定期对这个insert buffer进行合并(合并的本质工作就是将insert buffer中的信息更新到真正的索引文件中去)。因为innodbsecondary index是非聚簇的,那么插入很有可能带来大量的随机I/O,而如果利用insert buffer对一些属于相同页的行进行合并,那么就会减少随机IO从而提高性能。但是这里需要注意的是,insert bufferdoublewrite buffer是类似的概念,他实际上属于system tablespace中的一部分[2],正由于它也是持久化存储,那么在服务器宕机或是重启之后这些信息不会丢失,所以也就有了在前面介绍innodb_fast_shutdown时所说:在innodb重启时,可能需要进行merge insert buffer。那么在什么情况下需要对insert buffer进行merge操作呢?
a>     innodb restart的时候
b>     master thread会定期的进行merge操作
c>       每次读取secondary index page时,如果所需页不在buffer pool,而这些页在insert buffer中的时候,这时需要先对insert buffer进行合并,然后才能被读取。
为什么这样呢?因为所有插入的索引行所属的页如果不在buffer pool中,而又在insert buffer中,那么它一定代表了页的最新状态(不理解?因为每次插入索引行的时候,如果所需页不在buffer pool中就直接插入到insert buffer中,而一旦insert buffer merge后相关的行也就不在insert buffer更新secondary index page了)。这时或许你会问那么为什么不直接读取insert buffer中的页然后继续操作而一定要合并(更新到索引文件)呢?因为在innodb中是数据文件(也就是主键索引)和索引文件缓存的,在insert buffer中读取了需要的页后,那么必然就会在buffer pool中缓存了这个页,而如果这个页还留在insert buffer中却不更新到secondary index page去,那么,第一,这将不能保证索引文件得到更新;第二,insert buffer的空间会被占用。而如果这一步将insert buffer 合并后,不但减小了insert buffer的使用空间,而且将这merge操作完成了一部分,减小了以后merge的负担(不是有句话叫做今日事今日毕么),不过这也减慢了读的操作,因为读操作必须等待这个页的合并。
3.   Flush dirty page
这是最好理解的一个概念了,刷新脏页到磁盘。Innodb是数据文件和索引文件缓存的(innodb中的数据文件本质上也是索引文件,只是习惯这么称呼而已),从磁盘读到buffer中的文件被修改后,那么就成了dirty page脏页。而如果这些修改页的操作被提交了之后这些页就必须被flush到磁盘上。


啰嗦了这么久基本上将mysqlinsert buffer工作原理大致说清楚了,不过需要注意的是在mysql5.5中这个insert buffer已经改名了,叫做change buffer,不见包含了insert buffer,而且包括了update bufferdelete buffer。最后提一句,随着SSDFusion IO这类型存储出现,很多时候我们考虑随机IO带来的影响或许对它们就不适用了。

因为没有读源码,这些理解是通过读其他的资料而来的,所以还留下了几个问题:
1.   实现insert buffer的数据结构是什么?我想应该是树状结构,因为这会为合并那一步提升效率。理由:第一,如果是无序链表的最开始的插入效率可能会比较高,但是最终判断哪些行在相同页或是相邻页的时候需要排序,这里的代价会比较高。而有序的链表在性能上没有二叉树这种结构效率高。
2.   Insert buffer占多大空间?如果很小那岂不是只能容纳几行?那么在系统压力的时候,有空间来应付插入压力么?而如果比较大的,那么怎么保证在合并时候的效率?


参考文档: