SQL Server 内存泄露”(memory leak)的案例—游标导致的内存问题

问题描述:客户反映SQL Server运行一段时间就会报出内存不足的错误,怀疑是有内存泄露。从SQL Server的error log里面看如下错误信息:

2009-05-14 10:54:20.71 server Error: 17803, Severity: 20, State: 17
2009-05-14 10:54:20.71 server Insufficient memory available..

对于这种内存错误首先我们应该检查当前SQL Server的内存配置:

1. 32位的SQL Server还是64位的SQL Server?

2. 如果是32位的SQL Server,有没有启用AWE的选项。

3. 是否有设置最大服务器内存?

讲解这个问题之前需要先介绍一下32位和64位SQL Server在内存使用上的不同:

32位的应用程序在32位系统上的内存寻址空间是2GB的。我们可以使用AWE的方式使SQL Server使用超过2GB的物理内存,但是,寻址空间依然是2GB。

通过AWE扩展出来的内存,只可以用来作为数据缓冲区使用。除了数据缓存,SQL Server还需要使用内存来存储所有的执行计划,锁资源,用户连接信息,优化器使用作为评估语句执行计划的内存,语句执行内存等等。这些部分加起来不能超过2GB的内存。因此,即使我们为32位的SQL Server扩展了内存,一旦这2GB的内存不够提供给除了数据缓存的其他部分使用,SQL Server依然有面对内存不足的问题。本文中讨论的内存问题就是如此。

这里提供一篇文档,具体说明了如何为32位的SQL Server扩展内存:https://support.microsoft.com/default.aspx?scid=kb;en-us;274750

一旦我们使用了AWE选项为SQL Server扩展内存,我们一定要在sp_configure里面设置max server memory ,以保证OS可以保留足够的物理内存。

我们回到这个内存的错误,检查系统的内存配置:该系统是32位的SQL Server 2000,启用了AWE选项,最大服务器内存设置为7500MB。这样我们有个初步的推断,问题可能是由于2GB限制以下的某个部分内存使用过多导致的。

接下来我们介绍另一个很重要的命令,这个命令在我们处理内存问题时经常会使用:

DBCC memorystatus

这个命令是用来输出当前SQL Server的内存使用情况的。在SQL Server 2005以后,我们引入了一个新的DMV,其中包含了更详细的内存分配信息:sys.dm_os_memory_clerks

在这个问题中,由于系统是SQL Server 2000,所以我们使用dbcc memorystatus来查看SQL Server的内存情况。这里有两篇文章分别介绍了SQL 2000和SQL 2005中如何查看dbcc memorystatus的结果:

https://support.microsoft.com/default.aspx?scid=kb;en-us;271624

https://support.microsoft.com/default.aspx?scid=kb;en-us;907877

我们进一步检查SQL Server 的error log:

2009-05-06 16:20:22.38 spid215 BPool::Map: no remappable address found.

2009-05-06 16:20:22.46 spid241 BPool::Map: no remappable address found.

2009-05-06 16:20:22.50 spid8 BPool::Map: no remappable address found.

2009-05-06 16:20:22.52 spid242 Buffer Distribution: Stolen=190614 Free=196 Procedures=271

Inram=0 Dirty=104759 Kept=0

I/O=0, Latched=35, Other=664125

2009-05-06 16:20:22.52 spid242 Buffer Counts: Commited=960000 Target=960000 Hashed=768919

InternalReservation=529 ExternalReservation=1426 Min Free=256 Visible= 191224

2009-05-06 16:20:22.52 spid242 Procedure Cache: TotalProcs=67 TotalPages=271 InUsePages=197

2009-05-06 16:20:22.52 spid242 Dynamic Memory Manager: Stolen=190767 OS Reserved=2584

OS Committed=2542

OS In Use=2538

Query Plan=156155 Optimizer=0

General=15253

Utilities=401 Connection=4046

2009-05-06 16:20:22.52 spid242 Global Memory Objects: Resource=9815 Locks=16467

SQLCache=76 Replication=2

LockBytes=2 ServerGlobal=28

Xact=5011

2009-05-06 16:20:22.52 spid242 Query Memory Manager: Grants=11 Waiting=15 Maximum=1512 Available=0

这里的输出结果就是DBCC memorystatus的一部分。Buffer Counts: Commited=960000 Target=960000 在这里的commited的值,是当前buffer pool的大小,target的值是计算出来的buffer pool的大小。如果target的值大于commited的值,说明buffer还要继续增长,反之,则是buffer pool要收缩。Hashed=768919这个是数据缓存的大小,即AWE扩展出来的这个部分。我们可以简单的计算一下,960000*8k,刚好就是7500MB。其中数据缓存是6000MB左右。剩下的部分总共使用了1500MB。

接下来查看Dynamic memory manager的部分:

Stolen. 是buffer pool中如下5个部分的总和(General, Query Plan, Optimizer, Utilities, Connection). 这个部分的内存分配页面都是小于8KB的。这里的stole的总和是190767,基本上等于960000-768919的差值。 这说明buffer pool中除去数据缓存的部分,剩下的内存就都是这5个部分使用了。

在stolen的部分中,我们看到Queryplan 的值非常高,156155*8k=1219MB。Plan cache是用来缓存语句的执行计划的。在32位SQL Server有2GB的内存地址的限制情况下,单独的plan cache使用到了大于1200MB是非常惊人的了,这也是我们这个内存问题的根本原因。

接下来我们要研究为什么这个系统的plan cache会增长到1.2GB。通常情况下,SQL Server会定期的去清除长时间未使用的语句缓存,保证plan cache的部分不会涨得过大。我们同样也提供一个命令去手动的清除plan cache的内存:dbcc freeproccache

这个命令执行完以后,会将当前没有正在被语句使用的缓存的执行计划从SQL Server的内存中全部清除。我们在SQL Server上执行dbcc freeproccache命令后,再次使用dbcc memorystatus来检查queryplan的部分。在这套系统中,我们发现dbcc freeproccache并没有成功清除掉Queryplan的部分,这个部分依然显示超过1200MB。这就是为什么SQL Server也同样不同清除Queryplan,而导致Queryplan涨到超过1200MB的原因了。

前面我们讲过,dbcc freeproccache可以强制清除那些没有被语句正在使用的执行计划。如果不能清除,说明这些执行计划都在被使用中。那么什么情况会导致所有的执行计划都在被使用中呢?我们联想到问题的描述是这个内存的时候是慢慢增长上来的,那么这个情况就很有可能是应用程序中遗留了游标没有关闭。

检查系统中的活动游标,我们引入了另一个命令:DBCC ACTIVECURSORS 这个命令会将当前系统所有未关闭的游标打印出来:

SPID Cursor Id Pages Stmt

--------------- ------------------------------------------------------------------

55 180150581 2 select * from MESSAGE_DATA where MSG_NUMBER = @P1

55 180150580 2 select mhead.msg_number,customer_id,originator,status,queue,

55 180150577 4 select macc.delivery_time,macc.msg_number,macc.recipient_num

55 180150576 3 select mhis.msg_number,mhis.recipient_number,mhis.update_tim

55 180150568 4 select mh.originator,mh.datatype_id,mh.creation_time,mh.reci

55 180150547 8 select mh.msg_number,mh.orig_msg_number,mh.child_msg_number,

55 180150460 8 select customer_id, company, contact_name, contact_phone, ma

62 180150847 10 select pii.msg_number, pii.item_number, pii.type, pii.amount

62 180150710 10 select pii.msg_number, pii.item_number, pii.type, pii.amount

62 180150661 10 select pii.msg_number, pii.item_number, pii.type, pii.amount

…….

这里输出了总共9600多个活动游标,并且同时输出了游标使用的语句。

到目前为止,问题就很清楚了。使用JDBC的应用程序遗漏了某些游标没有关系,因此导致这些游标使用的语句的执行计划一直无法被SQL Server清除。因此导致了QueryPlan占用了大量的内存,数据库报出内存不足的错误。