SQL Serer闩锁 和 闩锁超时故障排除
翻译自:https://mssqlwiki.com/2012/09/07/latch-timeout-and-sql-server-latch/
在一个多线程的进程里,当一个线程在内存里更新一个数据或索引页,而另一个线程正在读取相同的页,将会发生什么?
当第一个线程在内存里读取一个数据或索引页,而第二个线程正在从内存里释放相同的页,将会发生什么?
答案是:我们将获得数据或数据结构不一致的结果。为了避免不一致,SQL Server使用同步机制像锁(Locks)、闩锁(Latches)和自旋锁(Spinlocks)。
在这篇博文里,我们将讨论关于闩锁的一些关键点和如何排除闩锁超时dump故障。
什么是闩锁(Latch)?
它们通过多线程控制对数据页和结构的并发访问。闩锁提供数据页的物理数据一致性,并提供数据结果的同步。闩锁不可以像锁一样被用户控制。
闩锁的类型:
Buffer(BUF) Latch
用于同步访问BUF结构和它们相关的数据库页。
Buffer "IO" Latch
Buffer Latch的一个子集,用于当BUF和相关的数据/索引页正在一个IO操作(从磁盘读取页或者写入页到磁盘)中间时。
Non-Buffer(Non-BUF) Latch
这些闩锁被用于同步一般的内存中数据结构,这些结构通常被并行线程、自动增长操作和收缩操作等查询/任务执行所使用。
闩锁模式:
Keep(KP) Latches
用于确保当页面正在使用时,不会从内存释放。
Shared(SH) Latches
用于对数据结构的只读访问,和阻止其他线程的写访问。
这个模式允许共享访问。
SH可兼容于KP、SH和UP。应该注意的是,尽管通常SH表明了只读访问,但不总是这样。对于Buffer Latches,SH是为了读取一个数据页的最小模式要求。
Update(UP) Latches
允许对数据结构(兼容于SH和KP)的读访问,但是阻止其他EX-latch访问。
当页分裂检测关闭并且当AWE没有启用时用于写操作。
Exclusive(EX) Latches
阻止发生在被闩锁结构上的读取活动。EX只兼容于KP。
当页分裂检测开启或者AWE启用时在读IO和写IO期间。
Destroy(DT) Latches
用于当从Buffer Pool移除BUFS,要么通过添加它们到空闲列表,要么取消映射AWE Buffers。
闩锁兼容性:
KP SH UP EX DT
KP Y Y Y Y N
SH Y Y Y N N
UP Y Y N N N
EX Y N N N N
DT N N N N N
如何识别闩锁争用?
闩锁争用可以通过在sysprocesses里的等待类型来识别。
PAGEIOLATCH_*:
这个sysprocesses里的等待类型表明SQL Server正在等待一个Buffer Pool页的物理I/O完成。
1.PAGEIOLATCH_*通常通过调优查询来解决,该查询会执行大量的IO操作(通常通过添加、修改和移除索引或统计信息来介绍物理IO数量)。
2.识别是否有磁盘瓶颈并修复它们(PAGEIOLATCH等待时间(例如大于30ms))。
PAGELATCH_*:
这个sysprocesses里的等待类型表明SQL Server正在等待访问一个数据库页,但是该页没有经历物理IO。
1.这个问题通常由在同一时间试图访问相同物理页的大量会话导致。你有应该查看spid的等待资源。这个wait_resource是被访问的页号(格式是dbid:file:pageno)。
2.我们可以使用DBCC PAGE来识别对象或者发生争用的页类型。它也帮助我们确定是否争用是用于分配、数据或文本。
3.如果SQL Server最频繁等待的页面在tempdb数据库,在dbid为2对于一个页号检查等待资源列。你可能面临着在这里提到的tempdb闩锁争用:http://support.microsoft.com/kb/328551
4.如果页在一个用户数据库,检查是否表在一个单调键像标识列上有一个聚集索引,所有的线程正在争用表末尾的相同页。在这种情况下,我们需要选择一个不同的聚集索引键,将工作分散到不同的页。
LATCH_*:
Non-buf闩锁等待可以被各种事情导致。我们可以在sysprocesses里使用这个等待资源列来确定包含的闩锁类型(KB 822101)。
1.一个非常普通的LATCH_EX等待是由于运行一个Profiler跟踪或者sp_trace_getdata参考KB 929728获取更多信息。
2.自动增长和自动收缩。
当一个闩锁被线程请求,并且如果由于其他线程在相同的页或数据结构上持有一个不兼容的闩锁,而这个闩锁不能被立即授予,那么这个请求者必需等待闩锁可被授予。如果等待间隔达到5分钟(waittime 300),类似以下的一条警告信息在SQL Server错误日志中输出,并且所有线程的一个mini dump被捕获。警告信息对buffer和non-buffer latches有所区别。
844: Time out occurred while waiting for buffer latch — type %d, bp %p, page %d:%d, stat %#x, database id: %d, allocation unit id: %I64d%ls, task 0x%p : %d, waittime %d, flags 0x%I64x, owning task 0x%p. Continuing to wait.
846: A time-out occurred while waiting for buffer latch — type %d, bp %p, page %d:%d, stat %#x, database id: %d, allocation unit Id: %I64d%ls, task 0x%p : %d, waittime %d, flags 0x%I64x, owning task 0x%p. Not continuing to wait.
847: Timeout occurred while waiting for latch: class ‘%ls’, id %p, type %d, Task 0x%p : %d, waittime %d, flags 0x%I64x, owning task 0x%p. Continuing to wait.
拆分以上警告
类型(type):
当前闩锁获取请求的闩锁模式。这个一个使用如下匹配的numerical值:
0 – NL (not used); 1 – KP; 2 – SH; 3 – UP; 4 – EX; 5 – DT.
任务(task):
我们尝试得到闩锁的任务。
等待时间(Waittime):
等待闩锁获取请求的以秒为单位的总时间。
拥有的任务(owning task):
可用的拥有闩锁的任务地址。
bp(只有Buffer latches):
与Buffer latch对应的BUF结构的地址。
page(只有Buffer latches):
对于当前包含在BUF结构中的页的页ID。
database id(只有Buffer latches):
对于在BUF里的页的数据库ID。
像排除SQL Server里的阻塞问题一样,当有一个闩锁争用或者超时dump,识别闩锁的所有者并故障排除为什么闩锁被该所有者长时间持有。
当有闩锁超时dump,你可以看到类似以下一个的警告信息。在dump是非常重要的用于找到闩锁的所有者线程之前,警告错误信息打印在SQL Server错误日志里。
2012-01-18 00:52:03.16 spid69 A time-out occurred while waiting for buffer latch — type 4, bp 00000000ECFDAA00, page 1:6088, stat 0x4c1010f, database id: 4, allocation unit Id: 72057594043367424, task 0x0000000006E096D8 : 0, waittime 300, flags 0x19,
owning task 0x0000000006E08328. Not continuing to wait.
spid21s **Dump thread – spid = 21, PSS = 0x0000000094622B60, EC = 0x0000000094622B70
spid21s ***Stack Dump being sent to E:\Data\Disk1\MSSQL.1\MSSQL\LOG\SQLDump0009.txt
spid21s * *******************************************************************************
spid21s * BEGIN STACK DUMP:
spid21s * 02/28/12 00:32:03 spid 21
spid21s * Latch timeout
Timeout occurred while waiting for latch: class ‘ACCESS_METHODS_HOBT_COUNT’, id 00000002D8C32E70, type 2, Task 0x00000000008FCBC8 : 7, waittime 300, flags 0x1a, owning task 0x00000000050E1288. Continuing to wait.
Timeout occurred while waiting for latch: class ‘ACCESS_METHODS_HOBT_VIRTUAL_ROOT’, id 00000002D8C32E70, type 2, Task 0x00000000008FCBC8 : 7, waittime 300, flags 0x1a, owning task 0x00000000050E1288. Continuing to wait.
从以上错误信息,我们可以很容易理解,我们正尝试在数据库ID为4,页1:6088(第一个文件的6088页)请求闩锁,并且因为任务0x0000000006E08328(在警告信息中拥有任务0x0000000006E08328)正在它上面持有一个闩锁而超时。
注意:任务只是被线程执行的一个工作请求。(就像system task、login task和ghost cleanup task等)。执行这个任务的线程将按需持有需要的闩锁。
让我们看看如何分析闩锁超时dump和使用拥有任务0x0000000006E08328获取闩锁的拥有线程。
去分析dump,从这里http://sdrv.ms/MO6ytG下载和安装Windows Debugger。
步骤1:
打开Windbg。选择“File”菜单,选择“Open crash dump”,选择“Dump file”(SQLDump000#.mdmp)。
步骤2:
在命令行窗口输入
.sympath srv*c:\Websymbols*http://msdl.microsoft.com/download/symbols;
步骤3:
输入.reload /f并回车。这将强制debugger立即加载所有的符号。
步骤4:
通过使用debugger命令lmvm验证是否符号被SQL Server加载。
0:002> lmvm sqlservr
start end module name
00000000`01000000 00000000`03679000 sqlservr T (pdb symbols) c:\websymbols\sqlservr.pdb\21E4AC6E96294A529C9D99826B5A7C032\sqlservr.pdb
Loaded symbol p_w_picpath file: sqlservr.exe
Image path: C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Binn\sqlservr.exe
Image name: sqlservr.exe
Timestamp: Wed Oct 07 21:15:52 2009 (4ACD6778)
CheckSum: 025FEB5E
ImageSize: 02679000
File version: 2005.90.4266.0
Product version: 9.0.4266.0
File flags: 0 (Mask 3F)
File OS: 40000 NT Base
File type: 1.0 App
File date: 00000000.00000000
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
步骤5:
使用以下命令来搜索线程堆栈来识别与拥有的任务相关的线程,并且它是拥有闩锁的线程。在你的错误日志里使用拥有的任务替代0X0000000006E08328
~*e .echo ThreadId:; ?? @$tid; r? @$t1 = ((ntdll!_NT_TIB *)@$teb)->StackLimit; r? @$t2 = ((ntdll!_NT_TIB *)@$teb)->StackBase; s -d @$t1 @$t2 0X0000000006E08328
ThreadId:
unsigned int 0x93c
ThreadId:
unsigned int 0x9a0
ThreadId:
unsigned int 0x9b4
00000000`091fdaf0 06e08328 00000000 00000000 00000000 (……………
00000000`091fdcb8 06e08328 00000000 091fdd70 00000000 (…….p…….
00000000`091fded0 06e08328 00000000 06e0e798 00000000 (……………
00000000`091fdf38 06e08328 00000000 00000002 00000000 (……………
00000000`091fec60 06e08328 00000000 0168883a 00000000 (…….:.h…..
00000000`091ff260 06e08328 00000000 000007d0 00000000 (……………
00000000`091ff2d0 06e08328 00000000 00000020 00000000 (……. …….
00000000`091ff5f8 06e08328 00000000 800306c0 00000000 (……………
00000000`091ff6c0 06e08328 00000000 00000000 00000000 (……………
00000000`091ff930 06e08328 00000000 00000000 00000001 (……………
00000000`091ff9b8 06e08328 00000000 00000000 00000000 (……………
00000000`091ffa38 06e08328 00000000 00000000 00000000 (……………
00000000`091ffc10 06e08328 00000000 03684080 00000000 (……..@h…..
00000000`091ffc90 06e08328 00000000 00000000 00000000 (……………
ThreadId:
unsigned int 0x9b8
ThreadId:
unsigned int 0x9bc
ThreadId:
unsigned int 0x9c0
……………
…………..
步骤6:
从以上输出,我们可以看到线程0x9b4与拥有的任务的指针相关,并且它是拥有闩锁的线程。让我们切换到线程(0x9b4),它正在执行拥有的任务,然后浏览这个堆栈来查看为什么这个线程长时间持有闩锁。
步骤7:
~~[0x9b4]s ==> Switching to the thread (Replace 0x9b4 with your thread id which has reference to the po
ntdll!ZwWaitForSingleObject+0xa:
00000000`77ef047a c3 ret
步骤8:
0:002> kC ==> Print the stack
Call Site
ntdll!ZwWaitForSingleObject
kernel32!WaitForSingleObjectEx
sqlservr!SOS_Scheduler::SwitchContext
sqlservr!SOS_Scheduler::Suspend
sqlservr!SOS_Event::Wait
sqlservr!BPool::FlushCache
sqlservr!checkpoint2
sqlservr!alloca_probe
sqlservr!ProcessCheckpointRequest
sqlservr!CheckpointLoop
sqlservr!ckptproc
sqlservr!SOS_Task::Param::Execute
sqlservr!SOS_Scheduler::RunTask
sqlservr!SOS_Scheduler::ProcessTasks
sqlservr!SchedulerManager::WorkerEntryPoint
sqlservr!SystemThread::RunWorker
sqlservr!SystemThreadDispatcher::ProcessWorker
sqlservr!SchedulerManager::ThreadEntryPoint
msvcr80!endthreadex
msvcr80!endthreadex
从以上堆栈,我们可以理解,拥有闩锁的线程正执行检查点并刷新缓存(脏页)到磁盘。如果刷新缓存到磁盘(检查点)花费很长时间,那么显然是有磁盘瓶颈。
类似的,对于其他闩锁超时问题,首先识别闩锁的拥有者线程,读取拥有者线程的堆栈,来理解拥有者线程执行的任务,并排除由拥有者线程执行的任务引起的性能故障。
如果你想查看等待的线程的堆栈,那么在错误日志里从闩锁超时警告信息获取任务(任务0x0000000006E096D8)代替拥有者任务(任务0x0000000006E08328),并使用在步骤5中提到的命令。
我希望这篇博文将帮助你学习和排除闩锁超时故障。