☰
SQL Server资源调控器:连接分类、资源池与CPU内存隔离避坑
2026/11/4 9:11:31 网站建设 项目流程

简介:这份文档面向 SQL Server 2008 R2 的数据库管理员与解决方案供应商,聚焦同一服务器托管多个数据库时 CPU 和内存资源争抢、难以动态调度的问题。内容以资源控制器为主线,比较 SQL Server 2005 通过独立实例与处理器亲和度分配资源的局限,以及虚拟化方案带来的额外开销,并展开资源池、工作负载组、最小值保障、最大值限制和请求分类转发等机制,说明短时 CPU 高峰属于正常现象,也提示精细配置需编写脚本并可借助 MSDN 文章完善。文中还涉及默认系统资源池、默认资源池及工作负载组的用途,便于读者落实到策略设计。资源包共 1 个 docx 文件,约 84KB,内容集中,便于快速查阅和方案比对。已有 1355 人学习,适合需要理解资源分配原理、评估资源池策略或为多库环境做容量规划的中高级读者,可作为 CPU 与内存优化配置的参考资料。

1. 结论先行:资源调控器调控的不是数据库,是连接

先给一个反直觉的结论:SQL Server 2008 R2 资源调控器里那些百分比,作用点不在数据库,而在连接。你完全可能把某个资源池的 CPU 上限压到 20%,然后发现那个业务照样把服务器吃满——因为它的连接压根没被分进这个池。真正决定一条请求归属哪个资源池的,是连接建立那一刻由分类器函数返回的一个字符串。这个设计让资源调控器摆脱了“一个数据库一套实例”的笨重做法,代价是把“谁算哪个池”这件事变成了你必须自己写代码来回答的问题。理解这一点,后面所有关于最小值、最大值、工作负载组、授予等待的现象才解释得通。

2. 从实例隔离到请求分类:三种老方案各自堵在哪

多库共用一台服务器时,资源争抢不是一个模糊的“性能问题”,它可以拆成三个很具体的失控点:某个库的长查询把 CPU 核占满、某个库的缓冲池把内存挤光、某个库的并发把 worker 耗尽。2008 R2 之前的三条路,本质上都是试图用“边界”去替代“调度”,所以每条路都堵在同一个地方——边界一旦划下,资源就失去了流动性。

2.1 处理器亲和性:绑死的核借不回来

最经典的做法是给每个数据库建独立实例,然后用处理器亲和性把 CPU 核分配给实例。配置层面就是一条sp_configure 'affinity mask',比如把 8 核机器上的 0~3 号核划给实例 A,4~7 号给实例 B。它确实有效:A 的查询再重,也烧不到 B 的核上。

问题在于这是静态切分。划给 A 的四个核,在 A 完全空闲的凌晨也依然是 A 的,B 那边排队等 CPU 的请求一点都借不到。更麻烦的是它划错了粒度——实例边界不等于业务边界。一个实例里如果还塞着多个库,库与库之间的争抢在实例内部照旧发生;反过来,同一套业务如果被拆到两个实例上,共享的缓存、执行计划、连接池又全都断了。血泪经验是:亲和性一旦配下去,调回来需要重启实例,等于没有后悔药。这就是为什么它只适合“负载长期稳定、彼此完全无关”的场景,稍微有点潮汐特征就不成立。

2.2 虚拟化切分:隔离的代价是双份开销

第二条路是把每个数据库塞进一台虚拟机。这个方案在隔离性上确实彻底,坏处是每一层隔离都要收税。虚拟机里有完整的操作系统,操作系统自己要吃内存、跑后台线程、维护文件缓存;虚拟化层自己做 CPU 调度和内存气球回收,也要占一份。对于 SQL Server 这种对内存极其敏感的负载,虚拟机里那部分被操作系统拿走的物理内存,本来可以全部变成缓冲池的页。

更本质的问题是反馈环变长了。数据库想在压力大时多要一点 CPU,得先让虚拟机内部的指标涨上去,再让虚拟化层观察到,再重新分配物理核——这个链路的延迟是分钟级的,而数据库的负载尖峰往往是秒级的。所以虚拟化能给你“不互相打死”的保证,给不了“忙的时候多拿一点、闲的时候还回去”的弹性。原文里那句“按需分配为基础的资源动态分配可能有些困难”,说的就是这个。

2.3 分类器把资源边界下移到了连接

资源调控器换了个思路:不划物理边界,改划逻辑边界,然后让调度器在边界之间动态分账。它把模型拆成三层:

层次对象作用
分类器函数一个 schema-bound 的标量函数连接建立时执行,返回该连接所属的工作负载组名
工作负载组请求的容器承载并发上限、并行度、单请求内存上限、优先级
资源池CPU 与内存的账本定义这一组请求能拿到的最小和最大资源百分比

关键是这个顺序:连接进来先被分类,分类结果决定它进哪个组,组又挂在某个池上,池才有百分比。所以“给数据库限 CPU”这个说法在资源调控器里是不准确的,准确的说法是“给某一类连接限 CPU,这类连接由分类器函数识别出来”。

默认状态下实例里有两个池:内部池(internal)承载数据库引擎自身的后台任务,默认池(default)承载所有没被特别分类的用户连接。两个池下各有一个同名的工作负载组。如果你只配了池的百分比却没挂分类器函数,所有用户连接仍然全部落在默认池的默认组里,你新建的那些池一个请求都不会收到——配置文件看上去很完整,实际一点作用都没有。这就是这个机制最常见的“翻车”现场。

3. 资源池与工作负载组:两层容器如何把百分比变成约束

三层结构里,分类器只是路由,真正决定“能拿多少”的是池和组。这两层的职责划分得很清楚:池管资源账本,组管请求形态。搞混这两者,配置就会写成“想限并发却去调池的百分比”这种驴唇不对马嘴的样子。

3.1 最小值与最大值:保底和封顶是两种语义

资源池的四个数字里,两个最小值是硬承诺,两个最大值是软约束,语义完全不同。

MIN_CPU_PERCENT和MIN_MEMORY_PERCENT表示“这部分资源是专门留给这个池的,无论系统多忙都拿得到”。因为它是承诺,所以有个硬性校验:所有池的最小值加起来不能超过 100%。剩下的那部分没有归属,谁需要谁用,是真正意义上的共享池。这个设计动机很直白——如果每个池都只有最大值没有最小值,那么在系统整体吃紧的时候,先跑起来的重负载会把空闲资源全部吃掉,后到的轻负载即使优先级更高也只能干等。

MAX_CPU_PERCENT和MAX_MEMORY_PERCENT则是防止某个池把没被最小值保护的那部分资源独吞。这里有个容易被忽略的行为:最大值不是硬截断。原文特别点出“池也有可能触发简短的 CPU 百分之百高峰”,这不是 bug。百分比约束是调度器周期性采样后做的偏置,采样间隔之内,一个池把某个核跑满是完全可能的。它的效果体现在平均值上,不体现在瞬时值上。拿任务管理器看那一下 100% 就断定配置失效,是最常见的误判。

MIN_CPU_PERCENT还有一个隐含作用:它决定了池之间的相对权重。两个池最小值分别是 20 和 40,在资源争抢时它们拿到的 CPU 大致是 1:2 的关系。所以哪怕你只关心“谁优先”,也得通过最小值来表达,而不是只调最大值。

3.2 工作负载组:同池之内的第二层策略

池是资源边界,组是策略边界。同一个池下面可以挂多个组,它们共享这个池的 CPU 和内存额度,但每个组还能再套一层自己的限制:

  • IMPORTANCE:取值 LOW / MEDIUM / HIGH,只在池内 CPU 有争抢时生效,决定组之间的相对权重。平时看不出差别,一旦打满就是它说话。
  • MAX_DOP:限制这个组的单条查询最多用几个并行线程。对于报表类请求,把它压在 4 以内往往比限 CPU 更有效,因为它直接减少了并行等待和 CXPACKET 的源头。
  • GROUP_MAX_REQUESTS:组内同时活跃的请求数上限。超过之后新请求进入排队,而不是直接报错。这是真正的“限流”开关,用来保护下游而不是保护数据库本身。
  • REQUEST_MAX_MEMORY_GRANT_PERCENT:单条查询最多能吃掉池内存的百分之多少。防止一条大排序把整个池的授予额度一次性抽干,让同池其他查询全部进入授予等待。

我一般会这么理解这层设计:池回答“这一类业务总共能用多少”,组回答“这一类业务内部,单个请求能有多放肆”。报表和 ETL 完全可以放进同一个池,但用两个组区分——ETL 组给 HIGH 重要度和较大的单请求授予,报表组给 MEDIUM 和小 DOP,这样在池额度固定的前提下,ETL 不会被报表的长排序拖住。

3.3 分类器函数:建连那一刻的一次决策

分类器函数是整个机制里唯一需要写代码的部分,也是原文说“需要编写大量脚本”的根源。它的形态很简单:

USE master; GO CREATE FUNCTION dbo.RG_Classifier() RETURNS sysname -- 必须返回 sysname;返回 NULL 或未定义的组名会静默回退到默认组 WITH SCHEMABINDING -- 强制要求,不加这个无法挂到资源调控器上 AS BEGIN DECLARE @grp sysname; -- 只做字符串映射,不要在这里联查业务表: -- 这个函数每建立一条连接就会被执行一次,连接风暴时它会被放大成登录超时 SET @grp = CASE SUSER_SNAME() WHEN N'report_svc' THEN N'wg_report' WHEN N'etl_svc' THEN N'wg_etl' ELSE N'wg_default' END; RETURN @grp; END; GO

三个技术点必须说清楚。第一,返回类型是sysname,返回NULL、空串或者一个不存在的工作负载组名,引擎不会报错,而是把这条请求放进默认池的默认组——这种静默回退是排查时最大的黑匣子,配置看起来全对,sys.dm_exec_sessions里的group_id却一直是默认值。

第二,WITH SCHEMABINDING不是可选项。资源调控器要在每次连接时调用这个函数,它需要保证函数的定义在运行期不会漂移,所以强制绑定架构。

第三,也是最容易被低估的:这个函数在每条连接建立时执行一次,结果缓存在该连接的上下文里,之后这条连接上的所有请求都沿用这个分类,直到连接被重置或重建。这带来了两个后果——好处是分类开销被摊薄到“每连接一次”而不是“每查询一次”,这让它可以在生产环境上用;坏处是分类结果不会随业务状态变化,你改了分类逻辑,已经建好的连接不会重新分类。应用侧如果用了连接池,那些池里的物理连接会一直保持旧的归属,直到池被清空。

配置的落地方式是两条语句:

ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = dbo.RG_Classifier); ALTER RESOURCE GOVERNOR RECONFIGURE; -- 必须显式重配置,否则只写进了元数据 GO

RECONFIGURE这一步经常被漏掉。ALTER RESOURCE GOVERNOR只是把配置写进系统表,RECONFIGURE才让它对运行期生效。漏掉的话,sys.dm_resource_governor_configuration里的is_reconfiguration_pending会是 1,而所有请求的归属不会有任何变化。

4. CPU 与内存:两套完全不对称的调控机制

资源调控器同时管 CPU 和内存,但这两者的实现方式差别极大。CPU 是一个“可以被节流”的资源——调度器不给时间片,线程就等着;内存则是一个“已经被占住”的资源——页已经在缓冲池里了,你不能凭空把它抽走而不引起物理读。这个不对称决定了它们的表现形态完全不同:CPU 超限表现为变慢,内存超限表现为物理读变多、授予等待变长。

4.1 CPU:百分比怎么变成调度节流

CPU 调控的实现位置在调度器层。引擎内部有一个周期性的评估:统计每个资源池在最近一段时间里实际消耗的 CPU,和该池的最小值、最大值做比较。如果某个池持续超过MAX_CPU_PERCENT,调度器就会降低这个池的工作线程被分配到 CPU 时间片的频率,这些线程会更多处于可运行但等待调度的状态。反过来,如果一个池的消耗低于它的最小值,其他池的线程在争抢时会更容易让位。

理解这个机制有两个要点。其一,它作用于“线程能不能拿到 CPU”,不作用于“一条查询能跑多久”。一条查询如果只需要一次短促的计算,即使它属于一个已经超限的池,也可能完整跑完——因为在采样窗口里它还没来得及被识别成超限。这就是短时峰值无法避免的原因。

其二,百分比的分母是整个实例的 CPU 容量,不是单核。8 核机器上给一个池 40% 的上限,意思是它平均能用大约 3.2 个核的算力,而不是“只能用某个核的 40%”。这也解释了为什么单条串行查询在受限池里依然可以跑满一个核——它消耗的是池额度里的一部分,只要额度没被别的线程同时消耗,它就能跑满。

MAX_DOP在这里是另一个维度的工具。它不限制池的总 CPU,它限制单条查询的并行宽度。并行查询的每个线程都要占一个 worker,都要参与调度,所以 DOP 降下来,池内 CPU 消耗的波动会明显变平滑。对于“整体 CPU 不超标但 P99 延迟很抖”的场景,调 DOP 往往比调百分比更立竿见影。

4.2 内存:被动约束与授予等待

内存这块必须先纠正一个预期:资源池的MIN_MEMORY_PERCENT和MAX_MEMORY_PERCENT控制的对象是有限的。它们主要作用于缓冲池(数据页缓存)和查询执行期的内存授予(排序、哈希连接、哈希聚合用的 workspace memory)。连接对象、锁结构、CLR、备份缓冲区、扩展事件缓冲这些不走这个账本。所以“给池配了 15% 内存,结果它还是吃了 30% 的服务器内存”这种现象是正常的,差的那部分不在调控范围内。

百分比的分母是实例可用的内存总量,也就是在“最大服务器内存”这个设置约束之后、SQL Server 真正能拿去用的那部分。这一点非常关键:如果你先把max server memory压到 8GB,再把两个池的最小内存分别配成 50% 和 50%,那实际含义是各 4GB,而不是物理内存的一半。配置前不确认这个基数,算出来的绝对值全是错的。

最小值的作用是在内存压力下保住缓冲池不被别的池挤掉。没有最小值保护的池,在整体内存吃紧时,缓冲池会被淘汰到很低的水平,页生命周期变短,物理读暴涨,表现出来就是“这个库平时挺快,一到高峰期就慢得离谱”——其实不是慢在 CPU,是慢在每次查询都要去磁盘捞页。

最大值则通过限制缓冲池的规模来生效。超过上限的池,新读进来的页会挤掉自己池内的老页,而不是去抢别的池的页。这种隔离的好处是可预测,代价是页命中率下降。

真正让 DBA 头疼的是授予等待。一条需要 2GB 内存做哈希连接的查询,如果它所在的池额度不够,或者单请求上限(REQUEST_MAX_MEMORY_GRANT_PERCENT)压得太低,它会进入授予队列等待。等不到怎么办?由REQUEST_MEMORY_GRANT_TIMEOUT_SEC决定:超时之后查询不会失败,而是拿一个更小的授予继续跑,代价是哈希表溢出到 tempdb。这是一条非常隐蔽的劣化路径——查询成功了,耗时却翻了几倍,错误日志里什么都没有,只有sys.dm_resource_governor_resource_pools里的授予超时计数在涨。

4.3 用 DMV 验证调控是否真的生效

排查这类问题不能看任务管理器,得看两个 DMV。先确认请求的归属:

SELECT s.session_id, s.login_name, s.group_id, wg.name AS workload_group, s.status FROM sys.dm_exec_sessions AS s LEFT JOIN sys.dm_resource_governor_workload_groups AS wg ON wg.group_id = s.group_id WHERE s.is_user_process = 1; -- 只看用户连接,排除系统会话

这个查询要看的只有一件事:workload_group是不是你期望的那个名字。如果一整列都是default,那问题一定出在分类环节,池和组的配置再对也没用。

再确认池的实际消耗:

SELECT name, min_cpu_percent, max_cpu_percent, min_memory_percent, max_memory_percent, total_cpu_usage_ms, -- 累计 CPU 毫秒数,看增量而不是绝对值 total_memgrant_timeout_count -- 授予超时次数,内存配置是否过紧的直接证据 FROM sys.dm_resource_governor_resource_pools;

total_cpu_usage_ms是累计值,统计起点是实例启动时间,所以单看绝对值没有意义,得隔一段时间取两次求差,再除以时间窗口,才能得到这个池实际的 CPU 占比。这个数字和max_cpu_percent对比,才是判断“上限有没有被遵守”的正确方式。同样,total_memgrant_timeout_count如果随着压测持续增长,说明池的内存额度或者单请求授予上限压得过紧,该往上放了。

5. 避坑与排查:配置看着生效了,压测一点变化都没有

5.1 分类环节的坑

现象:分类器函数挂上去了,ALTER RESOURCE GOVERNOR RECONFIGURE也执行了,但sys.dm_exec_sessions里所有用户连接还是默认组。

原因:函数返回了NULL或者一个拼错的工作负载组名。引擎对这种情况不做任何提示,直接回退到默认池的默认组。另一个可能是登录名匹配写错了——SUSER_SNAME()返回的是登录名,而很多人误以为它是用户名,拿USER_NAME()的语义去写,结果永远匹配不上。

解决:先在查询窗口里手动执行一次SELECT dbo.RG_Classifier(),确认返回值确实是你建过的组名;再用SELECT * FROM sys.dm_resource_governor_workload_groups核对组名拼写。把分类器写成“查不到就返回默认组”的兜底结构,比让它返回NULL更容易排查。

现象:分类器上线后,高峰期应用出现大量登录超时。

原因:分类器函数里写了联查,甚至跨库查配置表。这个函数每建立一条连接执行一次,应用侧连接池一旦因为超时或异常开始重建连接,就会形成连接风暴,把函数的执行次数放大几十倍。它执行在连接建立的路径上,所有开销直接体现为登录变慢。

解决:函数体里只允许字符串运算和常量映射,最多一张小表的单行查找,且必须有索引覆盖。需要动态规则的话,用sp_settrigger...之类的机制定期刷新一张极小的映射表,而不是在函数里做实时计算。

现象:改了分类规则,重启应用后新连接生效了,但部分老会话还在旧组里。

原因:分类结果在连接建立时确定并缓存在该连接的上下文里。连接池里的物理连接不会因为配置变更而重新分类。

解决:变更分类逻辑后必须让应用侧连接池整体重建(重启进程或清空池),或者用RESET CONNECTION语义强制重连。在生产上这意味着要安排窗口,不能指望“改完就生效”。

5.2 资源与观测环节的坑

现象:池的上限配了 30%,压测时 CPU 还是冲到了 90% 以上。

原因:两个可能。一是最大值本身是软约束,采样窗口内的短时峰值压不住;二是压测的并发连接根本没进这个池,全部在默认池里跑,限制的是另一个池。

解决:先用 4.3 的 DMV 确认归属,再看total_cpu_usage_ms的增量占比。如果长期占比确实在 30% 附近、只是瞬时冲高,那配置是有效的,不需要动;如果长期占比远超上限,再回头查归属。

现象:给池配了内存最小值,但那个库在高峰期还是疯狂物理读。

原因:最小值保的是缓冲池的下限,不保“页一定在缓存里”。如果这个库本身的活跃数据集远大于它分到的内存额度,那么再高的最小值也只是让淘汰发生在自己池内部,物理读该来还是会来。

解决:先算清楚这个库的热数据集大小,再倒推内存百分比是否合理。如果数据集确实超过额度,要么加内存、要么做冷热分离,靠调百分比解决不了根本问题。

现象:某个池的查询耗时在压测中翻了三倍,但没有报错,也没有明显的 CPU 瓶颈。

原因:授予超时。查询拿不到足够的内存授予,用一个缩水的额度继续执行,哈希操作溢出到 tempdb,耗时被磁盘 I/O 吃掉。

解决:盯住total_memgrant_timeout_count的增长,同时适当放宽REQUEST_MAX_MEMORY_GRANT_PERCENT或提高池的内存上限。这类问题在只看响应时间的监控里是完全隐形的,必须专门看授予指标。

6. 设计取舍:为什么要写脚本,以及什么时候不该动它

很多人第一次接触资源调控器时的疑问是:既然池和组都能在图形界面里配,为什么分类这一步非写脚本不可?

因为这本质上不是配置问题,是业务规则问题。图形界面能表达“这个池最多用 40% CPU”,但它没法表达“来自报表账号、且连接串里带了某个标识的请求算报表”。后者需要判断逻辑,判断逻辑就得有代码。引擎能做的只是把执行这个判断的时机选在一个代价可以接受的位置——连接建立时,而不是每条语句。这个选择直接决定了整个机制的形态:分类开销被摊薄到连接级别,代价是分类结果不随会话状态变化,动态性被牺牲掉了。

这个取舍还带来另外两个后果。一是资源调控器管不了“同一条连接内部”的差异。如果一个应用的所有功能共用一个连接池、一个登录账号,那么它的查询和写入在引擎眼里是同一类请求,你没法只给其中的写入限流。原文里提到的“给查询函数分配更多 CPU 时间而不是写操作”这种设想,在实现上要求这两类操作走不同的登录或不同的连接,否则分类器无从区分。二是它管不了 I/O。缓冲池的读写在内存层被调控,但物理磁盘的吞吐不在这套机制里——这也是后来版本才补上的方向。

那么什么时候不该用它?把三种方案的适用边界摊开来看比较清楚:

方案隔离强度弹性额外开销适用场景
实例 + 处理器亲和性强,物理隔离无,划死不能借每实例一套内存和后台进程负载长期稳定、彼此完全无关
虚拟机隔离强,含操作系统层弱,反馈延迟分钟级客户机系统 + 虚拟化层需要完整环境隔离、合规要求
资源调控器中,软上限强,秒级动态分账一次连接级函数调用同实例多业务、负载有潮汐

一个实际的判断标准是:如果你的瓶颈在磁盘 I/O 而不是 CPU 和内存,那么资源调控器基本帮不上忙,它调不了那个维度;如果你的所有业务共用一个登录账号、一个连接池,那么分类器没有可用的区分依据,得先改应用侧的连接管理,否则配了也是白配;如果你的场景是“一个库偶尔跑个大报表把整台机器拖垮”,那真正该做的可能是限制那条查询的 DOP 和授予上限,而不是搭一整套池和组。

我第一次在生产上配这套东西时,花了半天调百分比,压测数据一点没变。后来才发现是分类器函数返回了NULL,所有连接都在默认组里跑,等于什么都没配。从那以后,每次动资源调控器,我都会先跑一遍sys.dm_exec_sessions确认归属,再动百分比——先证明请求进去了,再谈限多少,这个顺序不能反。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询