zoukankan      html  css  js  c++  java
  • pagelatch等待在tempdb的gsm页面上

    Each data file has a gam page, sql will update it when allocate space in the file. Will see contention on it if many sessions try to create temp table at same time, multiple data files can reduce contention.

    每个数据文件访问页面时,sql将更新文件中分配空间。将看到的争用,如果很多会话试图创建临时表在同一时间,多个数据文件可以减少争用。

    【问题描述】

    我们可能会在数据库的错误日志里,发现这么一条信息:

    A time-out occurred while waiting for buffer latch — type 4, bp 000000097BFDEDC0, page 1:19239, stat 0xc00009, database id: 5, allocation unit Id: 72057615247867904, task 0x0000000005E594C8 : 0, waittime 300, flags 0x1018, owning task 0x0000000000169DC8. Not continuing to wait.

    我们的问题是,这个错误到底是什么含义,在什么情况下会报上面的错误,以及如何解决?

    【背景介绍】

    Latch是SQL Server内部用来同步资源访问的一个数据结构。和操作系统的Critical Section或ReaderWriterLock类似。Latch保护了那些想保护的资源,使得访问同步有序。比方说,当某个线程获得某个资源的Latch的独占使用权时候,别的线程如果也需要访问这个Latch,则它必须等待。

    从大的方面来讲,有两种Latch, 一种叫Buffer Latch,另外一种叫I/O Latch.

    我们先来简短介绍一下I/O Latch。当SQL Server从硬盘上读取一个页时,会先在内存预留该页的空间。并且在该预留空间的某一个位BUF_IO设为1. 如果数据从硬盘读写完成,则该位设为0. 从硬盘读取页的期间,其他也需要访问该页的线程当然要等待,等待类型为PAGEIOLATCH_SH,直到读写完成,BUF_IO被设为0为止。因此,如果我们看到大量PAGEIOLATCH_SH等待,则基本可以断定问题是出在磁盘性能上面。

    另外一种Latch则称为Buffer Latch,用来保护内存里的数据结构,如Index, Data Pages, B树中的Non-Leaf页。当进程需要读取一个内存里的数据页时,该进程要先获取该数据页上的Buffer Latch。有各种类型的Latch, 包括独占Latch(PAGELATCH_EX)和共享Latch(PAGELATCH_SH)。

    下面来演示,为什么我们需要Latch。如下图所示,我们在页面100上,已经存放了两条记录。

    如果没有Latch锁的话,某进程在页面100上,插入如下数据:INSERT VALUES(3, 300),其结果如下:

    这时,另外一个进程要在页面100上,插入如下数据: INSERT VALUES(4, 400), 因为没有Latch锁,所以会覆盖之前的数据。导致数据插入出问题。

    正确的做法是,我们要在第一个线程进行操作时,加独占Latch锁。第二个线程必须要等待,直到第一个线程操作完成。如下图所示:

    开始第一条记录插入,随后修改m_freedata(值为141)以及Row的指针(值为126),在此期间,第二条插入语句处于等待Latch状态。第一条记录插入完成后,释放独占Latch锁。

    第二条记录开始插入,插入期间也会加独占Latch锁,以防止其他进程修改或读取页。完成后,也随即释放Latch锁。最后结果如图所示。由于有Latch锁,所以数据的插入可以有序的进行。

    LATCH申请模式】

    Latch在申请的时候有以下几种模式,

    • KP – Keep Latch 保证引用的结构不能被破坏
    • SH – Shared Latch, 读数据页的时候需要
    • UP – Update Latch 更改数据页的时候需要
    • EX – Exclusive Latch 独占模式,主要用于写数据页的时候需要
    • DT – Destroy Latch 在破坏引用的数据结构时所需要

    下表显示各种Latch申请的兼容模式:

    Y表明是兼容的,如果两个线程都去读某页,则他们都会去申请SH锁,因为SH锁是兼容的,则两个线程都不会互相妨碍。而N表明是不兼容的,必须要等待。直到前面一个Latch被释放为止。

    LATCH等待类型】

    Latch的等待主要有三种。

    1. Buffer (BUF) Latch. 用来保护索引或数据页。也包括PFS, GAM, SGAM和IAM数据页。等待类型是PAGELATCH_*模式
    2. Non-buffer(Non-BUF) Latch, 除了上述数据结构以外的其他内存结构。等待类型是LATCH_*模式
    3. IO Latch, 保护数据从磁盘到页面的读写过程,等待类型是PAGEIOLATCH_*模式

    我们可以查询下面的语句了解一下具体的等待种类:

    SELECT * FROM sys.dm_os_wait_stats WHERE wait_type like ‘%Latch%’

    【信息解读】

    下面我们来解读这段信息。

    A time-out occurred while waiting for buffer latch — type 4, bp 000000097BFDEDC0, page 1:19239, stat 0xc00009, database id: 5, allocation unit Id: 72057615247867904, task 0x0000000005E594C8 : 0, waittime 300, flags 0x1018, owning task 0x0000000000169DC8. Not continuing to wait.

    这里,type 4 是等待的类型。通过下面的查询,我们可以知道, type为4的Latch是EX模式。

    SELECT * FROM sys.dm_xe_map_values where name = ‘latch_mode’

    bp 则是BUF结构的指针,如果没有内存转储,我们没有办法通过这个指针获取更有用的信息。Page (1:19239)则是等待的页编号。Stat 0xc00009则是BUF结构的位信息。其中的0x000004位则是我们上面提到的BUF_IO位。0xc00009 & 0x000004 = 0, 说明该页已经完全导入到内存里的BUF结构里了。Allocation unit id则是相对应的分配单元。一个数据库的表可以有多个分配单元,通常分配单元的个数和这个表的索引个数相同。

    使用以下一些列查询,我们可以通过Allocation Unit ID得到其对应的表名。(当然我们也可以通过Page的具体信息来获得表名。用DBCC PAGE命令)

    –结果中的container_id就是partition_id。

    SELECT * FROM sys.allocation_units where allocation_unit_id=’72057615247867904’

    –得到object_id

    SELECT * FROM sys.partitions where partition_id = xxxx

    –得到name

    SELECT * FROM sys.objects where object_id = yyyy

    Waittime为300,单位是秒,也就是说,我们申请在页(1:19239)上的一个独占的Latch锁,但是申请了300秒(5分钟),还是没申请到。

    这里flags用来判断该latch是否是superLatch,对于分析latch等待用处不大,所以我们不进一步具体分析。

    Task地址和owning task地址在内存转储分析中,非常重要。尤其是owning task, 是这个task阻塞了别的进程获得Latch锁。

    【如何解决】

    Latch申请不到,是由于被其他进程占用的缘故。所以我们需要找到哪个进程占用该Latch一直不释放。然后采取相应的办法进行解决。通常来讲,都是由于磁盘性能所造成的,如果需要得知确切的原因,我们可能需要对内存转储进行分析。对于第一个Latch Timeout的错误,SQL Server会产生一个内存转储。而对于后续的Latch Timeout,则会在SQL Server的错误日志里,报告一个错误信息,但不会产生内存转储。如果要改变这个行为,即要求每次碰到Latch Timeout,都要生成一个内存转储,我们可以用traceflag 838,如下面的命令:

    dbcc traceon(838, -1)

    由于生成内存转储需要一点时间,有可能在生成内存转储的时候,Latch等待现象消失,在内存转储中,没有足够的信息。因此,我们可能要多次抓取内存转储,以对问题进行分析。

    【案例分析】

    下面是一个案例分析。我们在数据库的错误日志里,发现有下面的错误,并且产生了一个内存转储。

    A time-out occurred while waiting for buffer latch — type 2, bp 0000000088FBFA40, page 1:153568, stat 0xc0010b, database id: 10, allocation unit Id: 72057594047758336, task 0x000000000C8E2988 : 0, waittime 300, flags 0x1a, owning task 0x0000000005A2F048. Not continuing to wait.

    下面是对内存转储的的分析,为了简单起见,不相关的调用堆栈被省略。我们可以看到,下面的这个进程在获取Buffer Latch的时候,由于无法及时得到Latch锁,产生了上述错误信息,并生成一个内存转储。

    0a 00000026`ebe09350 00000000`0215d1b8 sqlservr!CDmpDump::Dump+0x7c

    0b 00000026`ebe093a0 00000000`0215dd0c sqlservr!SQLDumperLibraryInvoke+0x1a0

    0c 00000026`ebe093d0 00000000`021503f7 sqlservr!CImageHelper::DoMiniDump+0x3d4

    0d 00000026`ebe095d0 00000000`0206af82 sqlservr!stackTrace+0x82b

    0e 00000026`ebe0ab20 00000000`02069f69 sqlservr!LatchBase::DumpOnTimeoutIfNeeded+0x19a

    0f 00000026`ebe0abf0 00000000`008e7bcc sqlservr!LatchBase::PrintWarning+0x205

    10 00000026`ebe0ace0 00000000`000e7a29 sqlservr!LatchBase::Suspend+0xd12

    11 00000026`ebe0b950 00000000`000a0689 sqlservr!LatchBase::AcquireInternal+0x1ff

    12 00000026`ebe0b9f0 00000000`000a1792 sqlservr!BUF::AcquireLatch+0x8d

    13 00000026`ebe0bd10 00000000`000a166c sqlservr!BPool::Get+0xc7

    14 00000026`ebe0bd70 00000000`000a35a0 sqlservr!PageRef::Fix+0xbc

    15 00000026`ebe0bdd0 00000000`000a320a sqlservr!BTreeMgr::Seek+0x44d

    16 00000026`ebe0bff0 00000000`000a2e5d sqlservr!BTreeMgr::GetHPageIdWithKey+0x20a

    17 00000026`ebe0c070 00000000`000a38e8 sqlservr!IndexPageManager::GetPageWithKey+0xbd

    这时,错误信息中的owning task地址0x0000000005A2F048对于问题的分析就很重要了。我们通过owning task的地址,结合内存转储分析,可以找到对应于该owning task的进程编号。其调用堆栈如下:

    0:175> kM

    # Child-SP          RetAddr           Call Site

    00 00000026`f027fa68 000007fe`fcd910dc ntdll!ZwWaitForSingleObject+0xa

    01 00000026`f027fa70 00000000`00083b1a KERNELBASE!WaitForSingleObjectEx+0x79

    02 00000026`f027fb10 00000000`00082d76 sqlservr!SOS_Scheduler::SwitchContext+0x26d

    03 00000026`f027ffa0 00000000`00082700 sqlservr!SOS_Scheduler::SuspendNonPreemptive+0xca

    04 00000026`f027ffe0 00000000`000829ac sqlservr!SOS_Scheduler::Suspend+0x2d

    05 00000026`f0280010 00000000`000e7e46 sqlservr!EventInternal<Spinlock<153,1,0> >::Wait+0x1a8

    06 00000026`f0280060 00000000`000e7a29 sqlservr!LatchBase::Suspend+0x599

    07 00000026`f0280cd0 00000000`00645aac sqlservr!LatchBase::AcquireInternal+0x1ff

    08 00000026`f0280d70 00000000`00646a49 sqlservr!SQLServerLogMgr::GrowAFile+0x7c

    09 00000026`f0281150 00000000`000cb38b sqlservr!SQLServerLogMgr::ReserveAndAppend+0x1fb

    0a 00000026`f0281290 00000000`000cbe89 sqlservr!XdesRMReadWrite::GenerateLogRec+0x567

    0b 00000026`f02813d0 00000000`000cbc23 sqlservr!XdesRMReadWrite::LogBeginXact+0x243

    0c 00000026`f0281530 00000000`000cbc3f sqlservr!XdesRMReadWrite::MakeActive+0x64

    0d 00000026`f0281560 00000000`001746c1 sqlservr!XdesRMReadWrite::GenerateLogRec+0x87

    0e 00000026`f02816a0 00000000`00174860 sqlservr!PageRef::ModifyRow+0xce0

    0f 00000026`f02818f0 00000000`00176749 sqlservr!PageRef::ModifyColumnsInternal+0x1b3

    10 00000026`f0281b50 00000000`00175f71 sqlservr!IndexPageRef::Modify+0x2fd3

    上面的调用堆栈显示,这是在增长数据库日志文件。由于数据库日志无法及时增长完成,因此导致了这个线程长时间拥有Latch锁而不释放,而其他进程无法及时获取相应的Latch锁。但查看该日志文件的增长设定,每次增长为300M。并不是以百分比增长。似乎没有什么大的问题。

    进一步查看数据库的错误日志,在发生该警告之前,SQL Server已经有相关的磁盘性能警告:

    SQL Server has encountered 18 occurrence(s) of I/O requests taking longer than 15 seconds to complete on file [F:sqldatalogsxxxx_log.ldf] in database [xxxx] (10).  The OS file handle is 0x0000000000000A2C.  The offset of the latest long I/O is: 0x000004e6511e00

    通过收集磁盘性能信息,我们发现,在出问题的点上,Avg. Disk sec/Read达到了1.444. 通常这个值在0.03左右比较正常。而1.444这个值显然偏差非常大。

    【结论】

    在这个点附近,磁盘压力变得比较大,所以磁盘性能变差)。导致日志文件增长300M无法及时完成。因此该线程拥有的Latch锁无法释放,而其他进程则等待该Latch导致超时。对于这个问题,有以下几种解决方案:

    1. 更换更好的磁盘。即使在有压力的情况下,性能也不会变得特别差。
    2. 修改日志文件增长幅度,以每次增长50M为幅度。期望日志文件增长能快速完成。
    3. 预先对日志文件扩容。这样,就不会有增长日志文件的情况发生。

    由于短时间内无法更换磁盘,所以采用第三种方案,预先对日志文件扩容。采用这方案后,再也没有发生Latch timeout。

    当然,对于每个Latch timeout的现象,原因可能有所不同,不过通常都是以磁盘性能或操作系统性能造成的原因居多。我们有时候不需要对内存转储进行分析,而是看一下操作系统的日志和数据库的错误日志,也可能找到原因。

  • 相关阅读:
    python字典实现原理-哈希函数-解决哈希冲突方法
    ElasticSearch-倒排索引
    ElasticSearch-核心概念
    MarkdownPad2基础语法
    下载python3.6,进行编译安装,运行django程序
    linux-指令1
    注解和反射
    Htlm和Css
    JAVASE加强
    网络编程
  • 原文地址:https://www.cnblogs.com/kingwwz/p/5570093.html
Copyright © 2011-2022 走看看