[译]Oracle 11.2 and the direct path read event

来自Frits Hoogland的博客, 原文链接: http://fritshoogland.wordpress.com/2013/01/04/oracle-11-2-and-the-direct-path-read-event/

在我之前的文章中, 我提及了对于全段扫描, Oracle利用操作系统新的实现方式. 全段扫描可以从执行计划中的’TABLE ACCESS FULL’,’FAST FULL INDEX SCAn’和’BITMAP FULL SCAN’确认, 这里的段可以是一个分区, 一个表, 或者一个索引.

你可以从我的另一篇文章中, 了解Oracle是如何选择多块读和直接路径读, 以及两者的区别. 奇怪的是, 对于’direct path reads’, Oracle几乎没有发布什么文档.

这篇文章是研究Oracle 11.2.0.3中’direct path reads’的实现方式, 实验环境是Linux 6.3 X64, kernel 2.6.39-300.17.3.el6uek.x86_64. Database 11.2.0.3 64 bit, no PSU/CPU. 数据库用了两块asm磁盘, clusterware处于单机模式. 虚拟机的用了VMWare Professional 5.0.2, 跑在Macbook OSX 10.8.2, 磁盘用了SSD.

我用gdb对于一个简单的查询进行跟踪 ‘select count(*) from t2′, 表t2没有索引和约束, 它足够大, 数据库引擎会选择’direct path reads’进行全段扫描. Oracle为了提高性能, 引入了多种机制没有文档记录的方式, 尤其是’direct path reads’, 使得前台进程可以一次调用提交多个IO请求, 之后再确认这些请求是否完成, 以提高IO请求的并发数.

1. 清晰明了的观察IO请求

首先, 和前一篇文章一样, 用sqlplus准备一个数据库前台进程, 以root用户挂载这个进程’gdb -p PID’.

现在, 对对IO相关的操作系统调用io_submit()和io_getevents()设置断点, 用”C”命令让进程继续执行:

(gdb) rbreak ^io_.*
Breakpoint 1 at 0xa08c20
 io_prep_pwritev;
Note: breakpoint 1 also set at pc 0xa08c20.
...
Breakpoint 45 at 0x7f2e71b1dc0c
 io_prep_poll;
(gdb) commands
Type commands for breakpoint(s) 1-45, one per line.
End with a line saying just "end".
>silent
>f
>c
>end
(gdb) c
Continuing.

在sqlplus中执行一个全段扫描的sql, 观察输出:

#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()

首先, 为什么有两个io_submit (io_submit@plt() 和io_submit () from /lib64/libaio.so.1)和仅仅一个io_getevents(io_getevents@plt ())呢?

需要对plt解释一下, plt是procedure linkage table的缩写. plt构造了可执行程序所使用的动态链接库提供的函数, 对于Oracle可执行程序, 也是这样. 如果你观察@ptl调用的地址, 你在Oracle可执行文件中也可以同样看到(/proc/maps), ‘io_submit () from /lib64/libaio.so.1’这个函数来自动态链接库libaio.so.1(也可以从/proc/maps中看到).

对于io_getevents, 我们只看到io_getevents@plt, 这可能是可执行文件伪造了系统调用, 或者我们错过了什么. 这需要我们进一步的研究libaio这个库本身的符号链接, 使用’nm -D’命令:

# nm -D /lib64/libaio.so.1
0000000000000000 A LIBAIO_0.1
0000000000000000 A LIBAIO_0.4
0000003f38200710 T io_cancel
0000003f38200670 T io_cancel
0000003f38200690 T io_destroy
0000003f382006a0 T io_getevents
0000003f38200620 T io_getevents
0000003f38200570 T io_queue_init
0000003f38200590 T io_queue_release
0000003f382005b0 T io_queue_run
0000003f382005a0 T io_queue_wait
0000003f382006e0 T io_queue_wait
0000003f38200680 T io_setup
0000003f38200660 T io_submit

啊! 有趣的是, 这里有两个io_getevents!

让我们看看Oracle程序是如何使用io_getevents调用的:

(gdb) del
Delete all breakpoints? (y or n) y
(gdb) rbreak ^io_get.*
Breakpoint 46 at 0x3f382006a0
 io_getevents;
...
Breakpoint 53 at 0xa09030
 io_getevents@plt;
.
And list the breakpoints:

(gdb) info break
Num     Type           Disp Enb Address            What
46      breakpoint     keep y   0x0000003f382006a0 io_getevents
47      breakpoint     keep y   0x0000000000a09030 io_getevents@plt
48      breakpoint     keep y   0x0000003f382006a0 io_getevents
49      breakpoint     keep y   0x0000000000a09030 io_getevents@plt
50      breakpoint     keep y   0x0000003f382006a0 io_getevents
51      breakpoint     keep y   0x0000003f382006a0 io_getevents
52      breakpoint     keep y   0x0000003f382006a0 io_getevents
53      breakpoint     keep y   0x0000000000a09030 io_getevents@plt

所以, io_getevents的断点在地址0x0000003f382006a0! 我们我们观察libaio的内容, 就会发现io_getevents有两个地址0x0000003f382006a0和0x0000003f38200620.

让我们用gdb也在第二个io_getevents的地址设置断点:

(gdb) break *0x0000003f38200620
Breakpoint 54 at 0x3f38200620
(gdb) commands
Type commands for breakpoint(s) 54, one per line.
End with a line saying just "end".
>silent
>f
>c
>end
(gdb) c
Continuing.

现在在sqlplus执行全段扫描的sql, 以下是gdb的输出:

#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000003f38200620 in io_getevents () from /lib64/libaio.so.1

好的, 现在我们也看到两个io_getevents系统调用了!

在gdb中恰当的设置断点之后, 我们可以观察到操作系统进行异步IO的系统调用, io_submit和io_getevents. Oracle还是用了其他的系统调用, 不过我们在这里只关注io_submit和io_getevents.

2. 加入等待事件, 对直接路径读的IO调用进行测量

现在我加入两个断点, 观察等待事件如何测量IO(kslwtbctx(进入等待)和kslwtectx(结束等待)):

(gdb) rbreak ^kslwt[be]ctx
Breakpoint 55 at 0x8f9a652
 kslwtbctx;
Breakpoint 56 at 0x8fa1334
 kslwtectx;
(gdb) commands
Type commands for breakpoint(s) 55-56, one per line.
End with a line saying just "end".
>silent
>f
>c
>end

如果重新执行全段扫描, 我们可以确定对于这些IO相关的调用(io_submit->io_getevents), 没有等待事件对它们进行测量, 因为我们没有看到kslwtbctx和kslwtectx. 只有IO调用结束后, 我才能看到kslwtbctx和kslwtectx, 这是期望的正常的现象. 因为这个虚拟机的磁盘在SSD上, 而且这些磁盘(也就是VMWare Fusion所在的一个文件)很有可能已经在宿主系统OS X的缓存中了, 所以IO的操作非常快, 以至于Oracle不会对它们进行测量.

就像之前文章介绍的, 最后调用kslwtbctx意味着进入等待事件’SQL*Net message from client’, 这个前台进程空闲中, 等待着用户的输入:

#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000003f38200620 in io_getevents () from /lib64/libaio.so.1
#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000003f38200620 in io_getevents () from /lib64/libaio.so.1
#0  0x0000000008f9a652 in kslwtbctx ()
#0  0x0000000008fa1334 in kslwtectx ()
#0  0x0000000008f9a652 in kslwtbctx ()
#0  0x0000000008fa1334 in kslwtectx ()
#0  0x0000000008f9a652 in kslwtbctx ()
#0  0x0000000008fa1334 in kslwtectx ()
#0  0x0000000008f9a652 in kslwtbctx ()
#0  0x0000000008fa1334 in kslwtectx ()
#0  0x0000000008f9a652 in kslwtbctx ()

现在我们是使用cgroups, 限制IO. 如果我把ASM-disk限制为1 IOPS, 执行全段扫描的SQL, 可以看到如下的gdb输出:

#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  0x0000003f38200660 in io_submit () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000003f38200620 in io_getevents () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000003f38200620 in io_getevents () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000003f38200620 in io_getevents () from /lib64/libaio.so.1
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000003f38200620 in io_getevents () from /lib64/libaio.so.1
#0  0x0000000008f9a652 in kslwtbctx ()
#0  0x0000000000a09030 in io_getevents@plt ()
#0  0x0000003f38200620 in io_getevents () from /lib64/libaio.so.1
#0  0x0000000008fa1334 in kslwtectx ()

实际上, 以上是IO很慢时, 典型的异步IO模式: io_submit提出IO请求, 紧接着四个io_getevents调用, 如果IO不是非常快的话(还有未完成的IO请求), 前台进程会注册一个等待事件, 调用另一个io_getevents, 结束等待事件(取决于现实世界中系统的IO能力, 我们更可能看到介于以上两种输出的结果).

这个模式有多种形式, 比如, 当扫描开始时, 在异步iO的初始化之后(比如操作系统的IO context和Oracle进程的IO slots), 首先是两个io_submit调用, 先完成至少两个异步IO请求.

io_submit之后的4个io_getevents很明显是非阻塞的. 因为在屏幕上我可以看到这四个调用很快的完成, 直到等待时间的注册, 屏幕的输出在另一个io_getevents调用后停止. 这当然是我的猜想, 我们可以证实这个猜想吗?

3. 更深入的观察debug信息

为了更深入的观察io_getevents调用, 我们需要知道io_getevents函数的参数, 这需要安装带有debug信息的libaio包(可以在http://oss.oracle.com/ol6/debuginfo下载). 安装之后, 为了获得详细的debug信息, 需要重启gdb回话:

这是gdb新的输出:

#0 0x0000000002cfb352 in io_prep_pread ()
#0 0x0000000000a09bb0 in io_submit@plt ()
#0 io_submit (ctx=0x7ff6ceb2c000, nr=1, iocbs=0x7fff2a4e09e0) at io_submit.c:23
23 io_syscall3(int, io_submit, io_submit, io_context_t, ctx, long, nr, struct iocb **, iocbs)
#0 0x0000000000a09030 in io_getevents@plt ()
#0 io_getevents_0_4 (ctx=0x7ff6ceb2c000, min_nr=2, nr=128, events=0x7fff2a4e9048, timeout=0x7fff2a4ea050) at io_getevents.c:46
46 if (ring==NULL || ring->magic != AIO_RING_MAGIC)
#0 0x0000000000a09030 in io_getevents@plt ()
#0 io_getevents_0_4 (ctx=0x7ff6ceb2c000, min_nr=2, nr=128, events=0x7fff2a4ec128, timeout=0x7fff2a4ed130) at io_getevents.c:46
46 if (ring==NULL || ring->magic != AIO_RING_MAGIC)
#0 0x0000000000a09030 in io_getevents@plt ()
#0 io_getevents_0_4 (ctx=0x7ff6ceb2c000, min_nr=2, nr=128, events=0x7fff2a4e8e48, timeout=0x7fff2a4e9e50) at io_getevents.c:46
46 if (ring==NULL || ring->magic != AIO_RING_MAGIC)
#0 0x0000000000a09030 in io_getevents@plt ()
#0 io_getevents_0_4 (ctx=0x7ff6ceb2c000, min_nr=2, nr=128, events=0x7fff2a4ebf28, timeout=0x7fff2a4ecf30) at io_getevents.c:46
46 if (ring==NULL || ring->magic != AIO_RING_MAGIC)
#0 0x0000000008f9a652 in kslwtbctx ()
#0 0x0000000000a09030 in io_getevents@plt ()
#0 io_getevents_0_4 (ctx=0x7ff6ceb2c000, min_nr=1, nr=128, events=0x7fff2a4e8e38, timeout=0x7fff2a4e9e40) at io_getevents.c:46
46 if (ring==NULL || ring->magic != AIO_RING_MAGIC)
#0 0x0000000008fa1334 in kslwtectx ()

现在我们可以看到很多有趣的内容!

第一, 所有的IO调用使用同一个’aio_context': ctx=0x7ff6ceb2c000. 这意味着这个sql查询的每个异步IO操作都可见, 而且IO请求被提交顺序和被确认完成的顺序未必相同.

接着是参数min_nr, 非阻塞的io_getevents调用min_nr是2, 阻塞的io_getevents调用min_nr是1.

参数nr是在这个aio_context下, 可以确认的最大IO数量. 据说我所知, 进程slots的数目不能超过32, 那实际上每次确认的请求不能超过32(虽然我们看到nr=128).

io_getevents最后的参数是timeout, 这对于理解IO过程很关键. timeout是指向一个timeout结构的指针. 为了得到timeout值, 我们需要打印出timeout结构的内容.

让我们用gdb, 在断点处打印出timeout结构:

(gdb) del
Delete all breakpoints? (y or n) y
(gdb) break *0x0000003f38200620
Breakpoint 38 at 0x3f38200620: file io_getevents.c, line 46.
(gdb) commands
Type commands for breakpoint(s) 38, one per line.
End with a line saying just "end".
>print *timeout
>c
>end
(gdb) rbreak ^kslwt[be]ctx
Breakpoint 39 at 0x8f9a652
 kslwtbctx;
Breakpoint 40 at 0x8fa1334
 kslwtectx;
(gdb) commands
Type commands for breakpoint(s) 39-40, one per line.
End with a line saying just "end".
>silent
>f
>end
(gdb) rbreak ^io_.*
Breakpoint 41 at 0x3f38200570: file io_queue_init.c, line 28.
int io_queue_init(int, io_context_t *);
...
Breakpoint 74 at 0x7f549fd44c0c
 io_prep_poll;
(gdb) commands
Type commands for breakpoint(s) 41-74, one per line.
End with a line saying just "end".
>silent
>f
>c
>end
(gdb) c
Continuing.

请留意断点38(*0x0000003f38200620, 真正的io_getevents调用)处的”print *timeout”.
“print *timeout”会打印出指针所指的内容, 现在, 我们可以确认我之前的猜想是否正确:

#0  0x0000000002cfb352 in io_prep_pread ()
#0  0x0000000000a09bb0 in io_submit@plt ()
#0  io_submit (ctx=0x7f54a1956000, nr=1, iocbs=0x7fff78f059b0) at io_submit.c:23
23  io_syscall3(int, io_submit, io_submit, io_context_t, ctx, long, nr, struct iocb **, iocbs)
#0  0x0000000000a09030 in io_getevents@plt ()
 
Breakpoint 38, io_getevents_0_4 (ctx=0x7f54a1956000, min_nr=3, nr=128, events=0x7fff78f0dfd8, timeout=0x7fff78f0efe0) at io_getevents.c:46
46      if (ring==NULL || ring->magic != AIO_RING_MAGIC)
$21 = {tv_sec = 0, tv_nsec = 0}
#0  0x0000000000a09030 in io_getevents@plt ()
 
Breakpoint 38, io_getevents_0_4 (ctx=0x7f54a1956000, min_nr=3, nr=128, events=0x7fff78f110b8, timeout=0x7fff78f120c0) at io_getevents.c:46
46      if (ring==NULL || ring->magic != AIO_RING_MAGIC)
$22 = {tv_sec = 0, tv_nsec = 0}
#0  0x0000000000a09030 in io_getevents@plt ()
 
Breakpoint 38, io_getevents_0_4 (ctx=0x7f54a1956000, min_nr=3, nr=128, events=0x7fff78f0ddd8, timeout=0x7fff78f0ede0) at io_getevents.c:46
46      if (ring==NULL || ring->magic != AIO_RING_MAGIC)
$23 = {tv_sec = 0, tv_nsec = 0}
#0  0x0000000000a09030 in io_getevents@plt ()
 

Breakpoint 38, io_getevents_0_4 (ctx=0x7f54a1956000, min_nr=3, nr=128, events=0x7fff78f10eb8, timeout=0x7fff78f11ec0) at io_getevents.c:46
46      if (ring==NULL || ring->magic != AIO_RING_MAGIC)
$24 = {tv_sec = 0, tv_nsec = 0}
#0  0x0000000008f9a652 in kslwtbctx ()
#0  0x0000000000a09030 in io_getevents@plt ()
 
Breakpoint 38, io_getevents_0_4 (ctx=0x7f54a1956000, min_nr=1, nr=128, events=0x7fff78f0ddc8, timeout=0x7fff78f0edd0) at io_getevents.c:46
46      if (ring==NULL || ring->magic != AIO_RING_MAGIC)
$25 = {tv_sec = 600, tv_nsec = 0}

是的! gdb输出中timeout结构的内容证实了我的猜想! 没有被测量的4个io_getevents是非阻塞调用(tv_sec=0), 他们只是检查异步IO的完成队列(ctx), 确认是否有3个IO请求已经完成, 然后注册一个等待事件, 调用一个阻塞的io_getevents, 查看同一个完成队列(ctx), 这次只需确认一个IO请求, 这次调用的timeout时间是600秒(tv_sec = 600).

4. 结论

这篇文章研究了’direct path reads’本质是如何工作的. 这个主题显然有更多的内容可以探讨, 尤其是11g中引入了智能的”auto tune”机制(以更好的利用异步IO提高性能)之后.

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>