简介:这份文档面向使用 SQL Server 的数据库管理员与解决方案供应商,聚焦多数据库共存时 CPU 与内存资源争抢这一常见难题,系统讲解 SQL Server 2008 R2 资源控制器的分配与优化思路。内容对比了 SQL Server 2005 依赖独立实例与处理器亲和性的旧方案,以及服务器虚拟化在资源动态调配上的局限,进而展开资源池与工作负载组的机制:默认系统池与默认池的划分、CPU 和内存最小值与最大值的百分比含义、最小值总和不超过 100% 的约束,以及短暂 CPU 峰值属正常行为的说明,并提及借助 MSDN 脚本实现请求分类与转发。资源包为 1 个 docx 文档,约 84KB,篇幅精炼、便于随查随用。目前已有 1355 人学习,适合需要为多库环境规划资源配额、排查资源竞争问题的技术人员参考。
1. 资源控制器:一台物理机上多个库抢 CPU 的老问题,终于有解了
上周有个做 ERP 的朋友找我,说他们一台数据库服务器上跑了三个库:一个订单库、一个报表库、一个历史归档库。白天订单库被报表查询拖得半死,晚上报表库又闲着,归档库偶尔跑批把 CPU 顶到 95%。他试过拆实例、试过处理器亲和性,结果资源一旦分出去就锁死了,订单库再忙也借不到归档库的空闲核。这个场景在 SQL Server 2008 R2 之前基本无解,直到资源控制器(Resource Governor)出来。它把 CPU 和内存按资源池切分,池内再挂工作负载组,请求进来先分类再路由,最小值和最大值一配,谁也别想一口吃完整台机器。这篇就按“是什么、怎么配、坑在哪”拆一遍,适合还在维护 2008 R2 的老 DBA 和需要给多租户库做隔离的从业者。
2. 资源池与工作负载组:先把 CPU 和内存的账算清楚
资源控制器的核心模型只有两层:资源池(Resource Pool)和工作负载组(Workload Group)。资源池是物理资源的抽象,直接对应 CPU 带宽和内存上限;工作负载组是请求的容器,挂在池下面,一个池可以挂多个组。默认安装完就有两个池:内部池(internal)和默认池(default),内部池留给系统进程,默认池接所有没被分类的请求。你要做的就是把业务请求从默认池里“捞”出来,塞进自建的池和组。
2.1 最小值和最大值到底怎么算
每个池可以配MIN_CPU_PERCENT、MAX_CPU_PERCENT、MIN_MEMORY_PERCENT、MAX_MEMORY_PERCENT四个值,都是整台服务器资源的百分比。最小值是“保底”,所有池的最小值加起来不能超过 100%,超了创建语句直接报错;最大值是“封顶”,但注意它不保证绝对不越线,短时间的 CPU 尖峰仍可能冲到 100%,这是正常行为,别拿监控图上的毛刺当故障。
内存这块要特别小心:MIN_MEMORY_PERCENT不是“预留”,而是“下限保护”,它影响的是缓冲池的分配倾向,不是硬隔离。真正卡内存的还是max server memory这个实例级参数,资源控制器的内存百分比是在这个盘子里面再切。很多人配完池发现内存没变化,就是忘了先设实例级上限。
| 参数 | 作用 | 常见取值思路 |
|---|---|---|
| MIN_CPU_PERCENT | 池的 CPU 保底份额 | 核心业务给 30~50,报表类给 10~20 |
| MAX_CPU_PERCENT | 池的 CPU 封顶 | 防止单池打满,一般留 10% 余量给系统池 |
| MIN_MEMORY_PERCENT | 缓冲池分配下限 | 按业务库实际热数据量估,别拍脑袋 |
| MAX_MEMORY_PERCENT | 缓冲池分配上限 | 归档类库压到 10~15,避免挤占订单库 |
2.2 建池、建组、写分类函数的三段式
配置顺序不能乱:先建资源池,再建工作负载组并绑定池,最后写分类函数(Classifier Function)并启用。分类函数是个标量 UDF,返回工作负载组的名字,SQL Server 在每次连接建立时调用一次,所以函数里别写查表的逻辑,否则登录风暴时会把性能拖垮。
-- 1. 建两个资源池:订单库保底 40% CPU,报表库封顶 30% CREATE RESOURCE POOL pool_orders WITH (MIN_CPU_PERCENT = 40, MAX_CPU_PERCENT = 70, MIN_MEMORY_PERCENT = 30, MAX_MEMORY_PERCENT = 60); CREATE RESOURCE POOL pool_reports WITH (MIN_CPU_PERCENT = 10, MAX_CPU_PERCENT = 30, MIN_MEMORY_PERCENT = 10, MAX_MEMORY_PERCENT = 25); -- 2. 建工作负载组,绑定到对应池 CREATE WORKLOAD GROUP wg_orders USING pool_orders; CREATE WORKLOAD GROUP wg_reports USING pool_reports;上面这段只建了骨架,MIN_CPU_PERCENT之和 50%、内存最小值之和 40%,都在 100% 以内,能过。接下来是分类函数,按登录名或应用名把请求分流:
-- 3. 分类函数:按登录名路由,返回组名 CREATE FUNCTION dbo.fn_classifier() RETURNS SYSNAME WITH SCHEMABINDING AS BEGIN DECLARE @grp SYSNAME = 'default'; IF SUSER_SNAME() = 'app_orders' SET @grp = 'wg_orders'; IF SUSER_SNAME() = 'app_reports' SET @grp = 'wg_reports'; RETURN @grp; END; GO -- 4. 绑定分类函数并启用 ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = dbo.fn_classifier); ALTER RESOURCE GOVERNOR RECONFIGURE;逻辑说明:函数里用SUSER_SNAME()拿登录名做判断,WITH SCHEMABINDING是硬性要求,不加会报错。返回'default'的请求走默认池,不受限。ALTER RESOURCE GOVERNOR RECONFIGURE是让配置生效的关键一步,只改不 RECONFIGURE 等于白写。参数上,SYSNAME是返回类型,别改成VARCHAR,组名长度对不上会静默落到默认组。
2.3 验证配置是否真的生效
配完别急着下班,用 DMV 看一眼实际路由结果。sys.dm_resource_governor_workload_groups能看到每个组的当前请求数和 CPU 使用,sys.dm_resource_governor_resource_pools看池级统计。如果wg_orders的total_request_count一直是 0,说明分类函数没命中,八成是登录名写错或者函数没 RECONFIGURE。
SELECT g.name AS group_name, g.total_request_count, g.total_cpu_usage_ms, p.name AS pool_name FROM sys.dm_resource_governor_workload_groups g JOIN sys.dm_resource_governor_resource_pools p ON g.pool_id = p.pool_id;跑一段业务后再查,total_cpu_usage_ms应该跟着业务量涨。如果所有请求都堆在default组,回头查分类函数的返回值,常见原因是函数里用了ORIGINAL_LOGIN()但连接串走的是应用账号,两者对不上。
3. 分类函数的写法边界:哪些能写、哪些一写就翻车
分类函数是整个方案里最容易出问题的地方,因为它跑在连接建立的关键路径上,写错一个细节就是登录超时或者路由全乱。这一章把能踩的边界摊开讲。
3.1 函数里禁止的操作清单
分类函数必须是确定性的、轻量的,官方明确限制了几类操作:不能访问表、不能调存储过程、不能做动态 SQL、不能用GETDATE()这类非确定性函数。我见过有人在里面查配置表拿组名,结果高并发登录时锁等待直接把连接池打满。正确做法是把路由规则硬编码在函数里,或者用APP_NAME()、SUSER_SNAME()、HOST_NAME()这些连接上下文函数做判断,它们不碰磁盘。
-- 反例:函数里查表,登录风暴时必翻车 -- SELECT @grp = group_name FROM dbo.route_config WHERE login_name = SUSER_SNAME(); -- 正例:用 APP_NAME 区分报表工具和业务系统 IF APP_NAME() LIKE '%ReportServer%' SET @grp = 'wg_reports'; IF APP_NAME() LIKE '%ERP%' SET @grp = 'wg_orders';APP_NAME()取的是连接串里的 Application Name,改造成本低,业务方不用改代码,只在连接字符串加一个参数就行。这是我在多租户环境里最常用的分流手段,比按登录名分更灵活,因为同一个账号可能既跑报表又跑交易。
3.2 组级参数:别只盯着池,组上还有细调项
工作负载组自己也有几个参数值得配:GROUP_MAX_REQUESTS限制组内并发请求数,MAX_DOP限制并行度,REQUEST_MAX_CPU_TIME_SEC限制单请求 CPU 时间。报表库最怕一条烂 SQL 跑满核,给它设MAX_DOP = 2和REQUEST_MAX_CPU_TIME_SEC = 300,超时的请求会被自动终止,比人工 kill 靠谱。
ALTER WORKLOAD GROUP wg_reports WITH (MAX_DOP = 2, REQUEST_MAX_CPU_TIME_SEC = 300, GROUP_MAX_REQUESTS = 50);GROUP_MAX_REQUESTS设太小会让正常请求排队,设太大失去保护意义,一般按业务峰值并发的 1.5 倍估。REQUEST_MAX_CPU_TIME_SEC的单位是秒,注意它统计的是 CPU 时间不是墙钟时间,一条等锁的 SQL 不会被它干掉,别指望用它治阻塞。
3.3 资源池的“借还”机制与监控口径
资源控制器的一个好处是池之间可以“借”资源:当某个池没用到自己的最小值,空闲部分可以被其他池临时使用,但一旦原池有需求,借出去的部分会被收回。这个机制导致监控图上的 CPU 曲线和配置值对不上,别慌,看sys.dm_resource_governor_resource_pools里的used_cpu_percent和target_cpu_percent两个字段,后者才是当前实际分配目标。
内存方面,used_memory_kb反映池内缓冲池占用,如果某个池的used_memory_kb长期贴着MAX_MEMORY_PERCENT算出来的上限,说明该池内存吃紧,要么调高上限,要么优化查询减少缓冲池压力。这里没有后悔药,配低了只能改配置再 RECONFIGURE,改完不会立刻释放已占内存,要等缓冲池自然淘汰。
4. 避坑与排查:五条血泪经验
4.1 现象:配完池,CPU 还是被一个库打满
原因:分类函数没命中,请求全落在默认池,默认池没有上限。解决:查sys.dm_resource_governor_workload_groups的请求计数,确认目标组有没有流量;再查分类函数返回值,用SELECT dbo.fn_classifier()在目标连接上手动跑一次,看返回是不是预期组名。登录名大小写、域名前缀都可能导致不匹配。
4.2 现象:创建资源池报错“最小值总和超过 100%”
原因:多个池的MIN_CPU_PERCENT或MIN_MEMORY_PERCENT累加超限。解决:先把所有池的最小值列出来加总,CPU 和内存分别算,任一维度超 100% 都会失败。常见误区是只算 CPU 忘了内存,内存最小值之和同样受限。调低非核心池的保底值,把余量留给订单类业务。
4.3 现象:分类函数改完不生效,重启服务才好
原因:只改了函数定义,没执行ALTER RESOURCE GOVERNOR RECONFIGURE。解决:每次改完分类函数或池参数,必须跟一条 RECONFIGURE。另外改函数用ALTER FUNCTION,别 DROP 再 CREATE,DROP 期间有连接进来会直接报错。
4.4 现象:报表库被限流后,业务方反馈“查询变慢但 CPU 没满”
原因:MAX_DOP或GROUP_MAX_REQUESTS设得太紧,请求在组内排队。解决:看sys.dm_resource_governor_workload_groups的queued_request_count,大于 0 说明在排队。适当放宽GROUP_MAX_REQUESTS,或者把MAX_DOP从 1 调到 2~4,具体看报表 SQL 的并行计划质量。
4.5 现象:内存配置改了,缓冲池没释放
原因:资源控制器的内存百分比只影响新分配的缓冲页倾向,已缓存的数据页不会主动踢出。解决:这是设计行为,不是 bug。要快速验证效果,可以在维护窗口执行DBCC DROPCLEANBUFFERS(仅测试环境),生产环境靠自然淘汰,观察一两天used_memory_kb的变化趋势再判断。
5. 进阶:用 DMV 做持续验证与参数微调
配完只是开始,资源控制器的参数需要跟着业务节奏调。我一般会留一个轻量监控脚本,每天跑一次,把池级和组级的关键指标落一张表,连续看一周再决定要不要动参数。下面这个查询把池的使用率、目标值和排队情况一次拉出来:
SELECT p.name AS pool_name, p.used_cpu_percent, p.target_cpu_percent, p.used_memory_kb / 1024 AS used_mem_mb, g.name AS group_name, g.queued_request_count, g.total_request_count FROM sys.dm_resource_governor_resource_pools p LEFT JOIN sys.dm_resource_governor_workload_groups g ON p.pool_id = g.pool_id ORDER BY p.name, g.name;判断标准很简单:used_cpu_percent长期贴着target_cpu_percent且queued_request_count大于 0,说明该池配额不够,考虑调高MAX_CPU_PERCENT或优化 SQL;used_cpu_percent远低于MIN_CPU_PERCENT且业务方没抱怨,说明保底给多了,可以把余量让给别的池。内存看used_mem_mb和上限的比值,超过 85% 就该警惕。
还有一个容易被忽略的点:资源控制器的配置存在master库里,迁移实例或者做日志传送时,这些配置不会自动跟过去,需要手动脚本化。我习惯把建池、建组、分类函数、RECONFIGURE 全部写进一个幂等脚本,脚本开头先判断对象是否存在,存在就 ALTER,不存在就 CREATE,这样换环境时一把跑完,不用凭记忆补步骤。
-- 幂等写法示例:池存在则改,不存在则建 IF EXISTS (SELECT 1 FROM sys.resource_governor_resource_pools WHERE name = 'pool_orders') ALTER RESOURCE POOL pool_orders WITH (MAX_CPU_PERCENT = 70); ELSE CREATE RESOURCE POOL pool_orders WITH (MIN_CPU_PERCENT = 40, MAX_CPU_PERCENT = 70);从那以后我每次上资源控制器,都强制先跑一遍幂等脚本再 RECONFIGURE,然后拿 DMV 对一遍组计数,确认流量真的进了目标组才收工。这套流程救过我两次——一次是分类函数里登录名拼错,一次是内存最小值之和超限导致建池失败,都是脚本跑完立刻暴露的。希望帮到你。
本文还有配套的精品资源,点击获取