InnoDB的多版本一致性读的实现

InnoDB是支持MVCC多版本一致性读的,因此和其他实现了MVCC的系统如Oracle,PostgreSQL一样,读不会阻塞写,写也不会阻塞读。虽然同样是MVCC,各家的实现是不太一样的。Oracle通过在block头部的事务列表,和记录中的锁标志位,加上回滚段,个人认为实现上是最优雅的方式。 而PostgreSQL则更是将多个版本的数据都放在表中,而没有单独的回滚段,导致的一个结果是回滚非常快,却付出了查询性能降低的代价。

InnoDB的实现更像Oracle,同样是将数据的旧版本存放在单独的回滚段中,但是也有不同。之前还以为整体实现都会跟Oracle不会有太大的出入,也一直没有太在意去看具体实现。今晚晚上下班准备回家时,刚好路过几个同事在交流分享这个问题,遇到一个疑问:

我们知道,InnoDB表会有三个隐藏字段,6字节的DB_ROW_ID,6字节的DB_TX_ID,7字节的DB_ROLL_PTR(指向对应回滚段的地址),这个可以通过innodb monitor看到,当然如果你熟悉innodb文件结构,也可以直接od ibd文件来验证。一致性读主要跟后两者有关系。InnoDB内部维护了一个递增的tx id counter,其当前值可以通过show engine innodb status获得

echo "show engine innodb status\G" | mysql -uroot | grep "Trx id counter"

假设有一个表,当前已经有两条记录。这时候我们开始一个实验,开启两个session,A和B,都设置autocommit=0

MySQL>set global autocommit=0;

T1时间:
A开始一个事务,执行一条select,可以看到已有的两条记录,show engine innodb status可以知道A的tx id,假设为7430
T2时间:
B开始一个事务,执行一条select,可以看到已有的两条记录,可以知道B的tx id,为7431
T3时间:
A中insert一条记录,此时A再select能看到,所以返回三条记录,而B无法看到,还是返回两条记录。
T4时间:
A中执行commit提交事务,分别在A和B中select,得到的结果和T3时间相同。

Ok,假设一致性读是根据事务先后,也就是tx id来比较的话,如果B事务的一致性读是通过B的tx id即7431来和A事务中insert的这条记录的tx id即7430来比较的话,由于A.tx_id < B.tx_id,那么B应该能都到A的记录(tx id是递增的,所以越小说明事务越早开始),但如果能读到,则显然不符合多版本一致性。 因此结果是正确的,那么就是InnoDB的一致性读的实现方式不是像我们按照经验来测试的那样了。通过google和察看代码,原来InnoDB还真是有一个感觉上很山寨的设计,由于tx id是事务一开始就分配的,事务中的变化也没有记录一个类似于Oracle的SCN的逻辑时钟,于是由了如下的实现: InnoDB每个事务在开始的时候,会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),然后一致性读去比较记录的tx id的时候,并不是根据当前事务的tx id,而是根据read view最早一个事务的tx id(read view->up_limit_id)来做比较的,这样就能确保在事务B之前没有提交的所有事务的变更,B事务都是看不到的。当然,这里还有个小问题要处理一下,就是当前事务自身的变更还是需要看到的。

有兴趣的可以去仔细看看代码的实现,在storage/innobase/read/read0read.c中实现了创建read view的函数read_view_open_now,在storage/innobase/include/read0read.ic中实现了判断一致性读是否可见的read_view_sees_trx_id。以下代码摘自5.5.8:

UNIV_INTERN
read_view_t*
read_view_open_now(
/*===============*/
	trx_id_t	cr_trx_id,	/*!< in: trx_id of creating
					transaction, or 0 used in purge */
	mem_heap_t*	heap)		/*!< in: memory heap from which
					allocated */
{
	read_view_t*	view;
	trx_t*		trx;
	ulint		n;
	ut_ad(mutex_own(&kernel_mutex));
	view = read_view_create_low(UT_LIST_GET_LEN(trx_sys->trx_list), heap);
	view->creator_trx_id = cr_trx_id;
	view->type = VIEW_NORMAL;
	view->undo_no = 0;
	/* No future transactions should be visible in the view */
	view->low_limit_no = trx_sys->max_trx_id;
	view->low_limit_id = view->low_limit_no;
	n = 0;
	trx = UT_LIST_GET_FIRST(trx_sys->trx_list);
	/* No active transaction should be visible, except cr_trx */
	while (trx) {
		if (trx->id != cr_trx_id
		    && (trx->conc_state == TRX_ACTIVE
			|| trx->conc_state == TRX_PREPARED)) {
			read_view_set_nth_trx_id(view, n, trx->id);
			n++;
			/* NOTE that a transaction whose trx number is <
			trx_sys->max_trx_id can still be active, if it is
			in the middle of its commit! Note that when a
			transaction starts, we initialize trx->no to
			IB_ULONGLONG_MAX. */
			if (view->low_limit_no > trx->no) {
				view->low_limit_no = trx->no;
			}
		}
		trx = UT_LIST_GET_NEXT(trx_list, trx);
	}
	view->n_trx_ids = n;
	if (n > 0) {
		/* The last active transaction has the smallest id: */
		view->up_limit_id = read_view_get_nth_trx_id(view, n - 1);
	} else {
		view->up_limit_id = view->low_limit_id;
	}
	UT_LIST_ADD_FIRST(view_list, trx_sys->view_list, view);
	return(view);
}

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);
}

参考:
http://dev.mysql.com/doc/refman/5.1/en/innodb-multi-versioning.html
http://wangyuanzju.blog.163.com/blog/static/130292009107101544125/
http://bbs.chinaunix.net/thread-1773206-1-1.html

MySQL 5.5.8 GA版本发布

在这几天的OOW上没怎么听到MySQL的消息,session少而且很多讲师是sales,忽悠成份居多。倒是今天一大早在Google Reader上看到MySQL5.5.8 GA版本发布了,真是期待了很久的消息。当然,官方网站上宣传的在windows性能提升540%,在Linux上性能提升370%,未必有那么靠谱,尤其是已经使用了innodb plugin的,那这么高的性能提升就更是浮云了。

但是,MySQL5.5还是有很多值得期待的特性,其中比较吸引人的,像semi-replication,replication heartbeat,以及partition的可用性提升等。MySQL5.5.8已经将Innodb Plugin1.1.4版本做为内建的Innodb引擎。

MySQL5.5.8源代码编译工具在Linux平台放弃了autotool,而是采用了跨平台的cmake。因此从源代码编译的过程有些不一样。如果系统中没有cmake,还需要先安装:

sudo apt-get install cmake
tar zxvf mysql-5.5.8.tar.gz
cd mysql-5.5.8

CFLAGS="-O3 -g" 
CXX=gcc 
CXXFLAGS="-O3 -g -felide-constructors -fno-exceptions -fno-rtti"
export CFLAGS CXX CXXFLAGS
cmake -DCMAKE_INSTALL_PREFIX=/opt/mysql -DSYSCONFDIR=/opt/mysql

make -j 2
make install

关于MySQL5.5.8更多cmake选项,请参考文档

InnoDB线程并发检查机制

InnoDB在接受MySQL线程调用能时,有一个并发线程的检查机制,通过innodb_thread_concurrency参数进行控制。如果参数设置大于0,则表示检查机制开启,允许进入的线程数就是参数的值。等于0则禁用并发检查。

在新的MySQL线程调用Innodb接口前,Innodb会检查已经接受的请求线程数,如已经超过innodb_thread_concurrency设置的限制,则该请求线程会等待innodb_thread_sleep_delay微秒后尝试重新请求,如果第二次请求还是无法获得,则该线程会进入线程队列休眠。重试两次的机制是为了减少CPU的上下文切换的次数,以降低CPU消耗,这和Oracle中latch的spin机制是同样的道理。如果请求被Innodb接受,则会获得一个次数为innodb_concurrency_tickets(默认500次)的通行证,在次数用完之前,该线程重新请求时无须再进行前面所说innodb_thread_concurrency的检查。

上述检查逻辑在源码storage/innobase/srv/srv0srv.c(Innodb很多参数都可以在该文件中找到定义)的srv_conc_enter_innodb函数中,有兴趣的可以仔细阅读一下,代码比较浅显,不难理解。另外,如果是一个已经持有lock的线程,则通过调用srv_conc_force_enter_innodb函数可以无视该检查,这是为了避免线程长时间持有锁影响性能,且可能增加死锁的机率。除此之外,slave线程也是有无视检查直接通行的权限。

简单思考一下上述机制,可以得出一个初步的推论:在数据库并发请求较小的情况下,从性能上来说禁用检查机制应该是更好的,毕竟执行检查机制本身也需要加锁(Mutex)。当并发线程很高的情况下,则开启检查机制对性能更有利。至于具体innodb_thread_concurrency设置为多少,可能就需要在不同的条件下实际的做一下测试了,不同的硬件环境,不同的MySQL版本和Innodb版本,应该都会有一些区别。

源代码中对于innodb_thread_concurrency参数的注释如下:

/* The following controls how many threads we let inside InnoDB concurrently:
threads waiting for locks are not counted into the number because otherwise
we could get a deadlock. MySQL creates a thread for each user session, and
semaphore contention and convoy problems can occur withput this restriction.
Value 10 should be good if there are less than 4 processors + 4 disks in the
computer. Bigger computers need bigger values. Value 0 will disable the
concurrency check. */

ulong   srv_thread_concurrency  = 0;

因为检查机制需要Mutex保护(Mutex-based Model),所以开启检查本身也有性能消耗,并且扩展性也会受到限制,在MySQL5.4版本中引入了一种新的机制(Timer-based Model),这里就不讨论了,有兴趣的可以参考这里(需要翻墙),实际上XtraDB存储引擎里已经包含Timer-based Model,通过参数innodb_thread_concurrency_timer_based可以开启,默认为OFF。在MySQL5.4的srv0srv.c的源代码中的注释中,可以看到Google和Percona的版权声明,看来MySQL5.4中吸引了很多第三方的改进代码,值得期待。

从show innodb status看XtraDB的增强特性

XtraDB存储引擎相对于标准的InnoDB引擎做了很多的改进,从show innodb status的结果可以初步看到部分增强的特性。下面是一个XtraDB测试环境(Linux)的结果:

root@(none) 11:25:18>show innodb status\G
*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
091106 11:26:11 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 53 seconds
----------
BACKGROUND THREAD
----------
srv_master_thread loops: 8 1_second, 8 sleeps, 0 10_second, 15 background, 15 flush
srv_master_thread log flush and writes: 8  log writes only: 39
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 9, signal count 9
Mutex spin waits 2, rounds 60, OS waits 0
RW-shared spins 9, OS waits 9; RW-excl spins 0, OS waits 0
Spin rounds per wait: 30.00 mutex, 30.00 RW-shared, 0.00 RW-excl
--------
FILE I/O
--------
I/O thread 0 state: waiting for i/o request (insert buffer thread)
I/O thread 1 state: waiting for i/o request (log thread)
I/O thread 2 state: waiting for i/o request (read thread)
I/O thread 3 state: waiting for i/o request (read thread)
I/O thread 4 state: waiting for i/o request (read thread)
I/O thread 5 state: waiting for i/o request (read thread)
I/O thread 6 state: waiting for i/o request (write thread)
I/O thread 7 state: waiting for i/o request (write thread)
I/O thread 8 state: waiting for i/o request (write thread)
I/O thread 9 state: waiting for i/o request (write thread)
Pending normal aio reads: 0, aio writes: 0,
 ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0
Pending flushes (fsync) log: 0; buffer pool: 0
25 OS file reads, 48 OS file writes, 32 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.09 writes/s, 0.08 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2,
0 inserts, 0 merged recs, 0 merges
Hash table size 553193, node heap has 1 buffer(s)
0.00 hash searches/s, 0.02 non-hash searches/s
---
LOG
---
Log sequence number 48613
Log flushed up to   48613
Last checkpoint at  48613
Max checkpoint age  7782360
Modified age        0
Checkpoint age      0
0 pending log writes, 0 pending chkp writes
24 log i/o's done, 0.04 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 136806400; in additional pool allocated 0
Internal hash tables (constant factor + variable factor)
    Adaptive hash index 2233316         (2212772 + 20544)
    Page hash           69556
    Dictionary cache    576800  (553912 + 22888)
    File system         41904   (41336 + 568)
    Lock system         167076  (166436 + 640)
    Recovery system     0       (0 + 0)
    Threads             41524   (41348 + 176)
Dictionary memory allocated 22888
Buffer pool size        8191
Buffer pool size, bytes 134201344
Free buffers            8170
Database pages          20
Modified db pages       0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages read 19, created 2, written 45
0.00 reads/s, 0.00 creates/s, 0.04 writes/s
Buffer pool hit rate 1000 / 1000
LRU len: 20, unzip_LRU len: 0
I/O sum[2]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
1 read views open inside InnoDB
Main thread process no. 16905, id 2788809632, state: waiting for server activity
Number of rows inserted 4, updated 0, deleted 0, read 0
0.02 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
------------
TRANSACTIONS
------------
Trx id counter 509
Purge done for trx's n:o < 506 undo n:o < 0
History list length 1
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0, not started, process no 16905, OS thread id 2777967520
MySQL thread id 6, query id 196 dxmonitor 172.19.68.250 root
show innodb status
---TRANSACTION 508, ACTIVE 2 sec, process no 16905, OS thread id 2777766816
1 lock struct(s), heap size 320, 0 row lock(s), undo log entries 1
MySQL thread id 7, query id 195 dxmonitor 172.19.68.250 root
TABLE LOCK table `test`.`test` trx id 508 lock mode IX
----------------------------
END OF INNODB MONITOR OUTPUT
============================

1 row in set, 1 warning (0.00 sec)

从上面的结果可以看到:

  • 增加了BACKGROUND THREAD部分,显示一些后台线程的统计信息
  • SEMAPHORES部分,最后增加了一个关于spin的统计信息。引入了一个新的innodb_spin_wait_delay的参数对spin的行为进行控制
  • FILE I/O部分,innodb默认开启4个IO线程,由参数innodb_file_io_threads控制,但在unix/linux平台该参数无效,只能是默认的4个。而XtraDB/innodb plugin默认开启8个IO线程,并且读线程和写线程可以分别由参数innodb_read_io_threadsinnodb_write_io_threads控制。在多核CPU环境中,如果CPU和磁盘本身没有成为瓶颈,开启更多线程应该对提升IO性能有很好的作用。而读写线程的单独控制,则可以针对不同应用环境下的要求进行灵活配置。
  • LOG部分,增加了max checkpoint age/Modified age/Checkpoint age信息,这些信息可用于调整一些IO相关的参数,具体可参考这里
  • BUFFER POOL AND MEMORY部分,XtraDB更加详细的列出buffer pool中不同部分的详细信息,还有buffer pool的命中率和LRU的信息等,对内存的使用情况一目了然,拒绝黑箱操作。

关于XtraDB的show innodb status的更多改进,请参考这里

无觅相关文章插件,快速提升流量