简介:围绕 SQL Server 2008 R2 的 CPU 与内存精细化分配,这份文档面向数据库管理员、解决方案供应商及需要多库共存调优的运维人员,重点讲清资源控制器相对 SQL Server 2005 处理器亲和性与虚拟机隔离方案的差异。压缩包内仅含 1 个 docx 文件,约 84KB,篇幅紧凑,便于随查随用。内容从资源池与工作负载组的定义入手,说明最小值与最大值的百分比含义、系统池与默认池的默认结构,以及请求分类后转发到对应负载组的思路,并提醒最大值设置下仍可能出现短时 CPU 峰值,同时指出需借助脚本与 MSDN 文章完成配置。已有 1355 人学习,适合作为多数据库资源争抢场景的方案参考与实施前速览。
1. 服务器只有一台,数据库却有好几个:资源调控器到底在管什么
早上九点半,报表库跑了一个大查询,CPU 一下子冲到 95%,在线下单的接口开始转圈,客服电话接连打进来。这种场面做数据库运维的人都不陌生:机器只有一台,CPU 和内存就那么多,可上面跑的数据库有三四个,谁都想多占一点。SQL Server 2008 R2 里有个专门负责协调资源分配的组件,中文一般叫资源调控器(Resource Governor),早一些的资料里也写成资源控制器。
说得直白一点:它允许你把整机的 CPU 和内存按百分比切成几个“额度池”,再根据连接的特征,把不同的请求分进不同的池子里,让该保底的保底、该限流的限流。这个机制把“最大优化分配”从凭直觉调参数,变成了一件可以写进脚本、可以复查的事——搞懂它,多库抢资源的老问题才有解。
2. 三件套拆开看:资源池、工作负载组、分类器函数各管什么
资源调控器第一次接触会觉得抽象,因为它是三个组件套在一起用的:资源池定额度,工作负载组定池子里怎么排队,分类器函数决定谁进哪个组。三者的关系搞不清,后面看脚本就全是天书。
2.1 先把整体想成一座合租公寓
整台服务器的 CPU 和内存,相当于一套合租公寓的总水电容量,容量是固定的,谁多用一点,别人就少一点。资源池像是跟房东签的用电协议:这间房保底多少度、最多用多少度,白纸黑字写下来。工作负载组像是住在这间房里的人,房间额度是大家的,但屋里还可以再排个优先级,谁先洗澡谁后洗澡。分类器函数则是门口那位保安,看你工牌上写的是哪个部门,决定你进哪间房。
这个比喻里最关键的一点是:额度按百分比算,参照物是整台机器,而不是某个数据库。所以你在脚本里写 20,意思不是“给这个库 20MB 内存”,而是“给这个池子保留整机内存的 20%”。理解这一点,后面所有数字才有意义。默认情况下系统会给你两个池子:一个内部池,供系统自己用,动不了;一个默认池,所有没被特别指派的连接都落在这里。
2.2 资源池:提前定好的保底值和上限
一个资源池上能配四个数,含义各不相同:
| 参数 | 含义 | 是否允许超额 |
|---|---|---|
| MIN_CPU_PERCENT | CPU 保底百分比,发生争用时才起作用 | 所有池的保底值合计不能超过 100% |
| MAX_CPU_PERCENT | CPU 上限百分比,属于软性上限 | 允许短暂冲高 |
| MIN_MEMORY_PERCENT | 内存保底百分比,划给池子专用 | 所有池的保底值合计不能超过 100% |
| MAX_MEMORY_PERCENT | 内存上限百分比 | 允许在没人抢时用满 |
保底值的意义在于“不管别人怎么抢,这部分始终是我的”。上限值的意义在于“别把没人用着的资源全占了”。两个数字中间那段空间,就是资源可以浮动、可以互相借用的部分,也是这套机制比固定分配灵活的地方。要注意,保底值不是“预扣”:如果某个池子当下没活干,它保底的那部分资源依然可以被别的池子用掉,只是别人要用的时候得让出来。
2.3 工作负载组:池子里再分配优先级
资源池只回答“这个池子有多少资源”,工作负载组回答“池子里这群请求谁先来、单个请求最多能吃多少”。它是请求真正的落脚点,一个池子可以挂好几个组。常用参数有这么几个:IMPORTANCE设优先级,分低中高三档,CPU 紧张时高优先级的组先跑;MAX_DOP限制组内查询的最大并行度,防止一个大查询把核全占了;GROUP_MAX_REQUESTS限制组里同时能有多少个请求在跑,相当于排队上限;REQUEST_MAX_CPU_TIME_SEC限制单个请求能消耗的 CPU 时间;REQUEST_MAX_MEMORY_GRANT_PERCENT限制单个请求最多能申请到池子内存的百分之几。
举个具体场景:报表池里如果只放一个组,一个跑歪了的大查询依然能把池子吃干净。把它拆成“交互式报表”和“批量导出”两个组,给批量导出设低优先级、限制并行度和并发数,交互式报表就不会被拖死。池子管总量,组管细账,两层配合才压得住。
2.4 分类器函数:登录那一刻决定你进哪间房
分类器是一段你自己写的函数,返回类型是SYSNAME,也就是一个工作负载组的名字。系统在连接建立的时候调用它一次,拿到名字就把这个会话塞进对应的组;如果返回空值,或者返回了一个并不存在的组名,这个连接就落进默认池的默认组。函数内部能用的素材很少,基本上就是连接属性:登录名SUSER_SNAME()、主机名HOST_NAME()、应用程序名APP_NAME()、登录名SUSER_NAME()、以及IS_SRVROLEMEMBER()。
这也是它和很多人想象中“按 SQL 语句内容分流”最大的不同:判断依据是“谁连上来的、从哪连的、用什么程序连的”,而不是“这条 SQL 查了哪张表”。函数本身有约束,要带WITH SCHEMABINDING,不能去查用户表,也不适合写复杂子查询,本质上就是看一眼连接属性、返回一个名字,越轻越好。
3. 为什么非要重新造一个组件:从“一个库一个实例”到“共享池”
在资源调控器出现之前,多数据库抢资源这事并不是没解,只是解法都带着明显副作用。理解这些老办法的毛病,才能明白新组件到底新在哪,也才不至于在配置时把老思路套上去。
3.1 单库不算问题,多库才是真麻烦
如果一台服务器只伺候一个数据库,资源消耗根本不是议题,整机都是它的。麻烦出在多库共存:几个库共用同一组 CPU 和内存,业务高峰还经常撞在一起,一个库的慢查询会变成所有库的慢查询。数据库恰恰是对资源相当敏感的那类应用,CPU 被抢走一点,查询计划可能就从走索引变成全表扫;内存被挤掉一块,缓存命中率掉下来,磁盘 IO 立刻顶上去。
这件事的吊诡之处在于:出问题的往往不是“总量不够”,而是“分配不均”。机器明明还有余量,某个库却因为抢不到而卡住;另一个库占着资源却闲着。所以真正要解决的不是扩容,而是“让资源能在库之间流动,同时给关键业务兜个底”。
3.2 老办法一:分实例加处理器亲和性,资源被提前固定
早期比较主流的做法,是给每个数据库单独建一个实例,再用处理器亲和性把每个实例绑定到指定的几个核上。隔离效果确实立竿见影:A 库的实例怎么折腾,也影响不到绑在另外几个核上的 B 库实例。但代价同样是立竿见影的——资源一旦被分出去,就再也回不来了。
设想一个场景:给 A 库分了 8 个核,给 B 库分了 4 个核。夜里 A 库几乎没流量,8 个核闲在那儿吹风;B 库在做月结,4 个核已经跑满了。这时候 B 库不可能去借用 A 库闲置的核,因为亲和性是硬绑定。要缓解只能人工调整配置、重启实例,调度粒度粗到以“天”为单位,而业务高峰是以“分钟”计的。这种“分了就锁死”的模型,本质上是用灵活性换隔离性。
3.3 老办法二:一个库一台虚拟机,隔离的代价是额外开销
另一条路是服务器虚拟化:每个虚拟机里只放一个数据库,靠虚拟机之间的边界实现隔离。这个思路比绑核优雅,至少资源可以按虚拟机整体调整。但多了一层虚拟化,就多了一层成本:每台虚拟机要跑一套完整的操作系统,这套系统本身要吃 CPU、要吃内存,而这些资源本来是可以留给数据库的。
更别扭的是动态分配。你希望的是“A 库闲下来的时候,资源自动流向 B 库”,但能不能做到,取决于所用的虚拟化软件支持到什么程度,有些环境下按需动态调整很难实现,甚至根本做不到。结果就是为了隔离,先交了一笔“平台税”,再接受一个未必灵活的调度模型。
3.4 共享池的核心变化:额度是百分比,而且能借能还
资源调控器换了个思路:不再把资源切成互不相通的孤岛,而是让所有池子共用一整块资源,只给每个池子标上保底值和上限。保底的那部分,是发生争用时必须让出来的;上限之外的那部分,是没人抢时可以用、有人抢时得吐出来的。
这就同时拿到了两边的好处。隔离性来自保底值:关键业务的那份谁也抢不走。灵活性来自浮动区间:低峰期闲置的额度,别的池子可以拿去用,不用人工干预,也不用重启实例。而“借”和“还”的时机,由系统按当前争用情况判断,粒度从“天”变成了“秒”。前面提到的那些靠人盯着、靠重启生效的老办法,到这里才算真正被替代。
3.5 它改的是分配方式,不是机器容量
有一个预期要先摆正:资源调控器是分配工具,不是性能工具。它能把资源从“不該多占的地方”挪到“急需的地方”,能让关键业务在高峰期不被拖垮,但它不会让整机凭空多出一个核、多出几 GB 内存。如果所有池子的保底值加起来已经贴近 100%,那说明机器本身就该扩了,再怎么调也是零和博弈。
所以“最大优化”这个词要拆开看:先确认容量够不够,再谈分配合不合理。容量不足时,调分配只能让痛苦来得均匀一点;容量够但分配失衡时,调分配才是真正见效的那一步。分清这两件事,能省下很多在参数上反复试错的时间。
4. 五个最容易搞混的地方:最小值、最大值和“按库分配”的错觉
配置出问题,十次里有八次不是脚本写错,而是概念理解偏了。下面这几组概念,名字看着像,作用差得很远,初学者几乎都会在这里混淆一次。
4.1 易混一:把最小值当成最大值用
易混点:以为MIN_CPU_PERCENT = 30的意思是“这个池子最多用 30% 的 CPU”。
为什么混:两个参数都叫“百分比”,都写在同一个CREATE RESOURCE POOL语句里,看名字很容易觉得一个是下限一个是上限,但下限到底“限”的是什么,很少有人细想。
怎么区分:记住最小值是“争用时的保底”,不是“平时的配额”。没有争用时,一个只设了 30% 保底、上限设成 100% 的池子,完全可以跑到 80%。反过来,如果所有池子的保底值加起来逼近 100%,就等于把机器切成固定份额,浮动空间没了,这时候它退化成了老式的固定分配。设保底值的正确姿势是:只给真正不能挨饿的业务设,剩下的留白,让资源自己流动。
4.2 易混二:以为它是按数据库分配的
易混点:觉得“一个数据库对应一个资源池”,池子就是数据库的资源配额。
为什么混:需求本身是从“多个库抢资源”来的,很容易顺着这个思路认为分配单位就是数据库。
怎么区分:实际的分配单位是连接。分类器看到的是一个会话的登录名、主机名、程序名,据此决定它进哪个组、用哪个池的资源。同一个数据库,用报表工具连进来走报表池,用业务系统连进来走交易池,完全合法。反过来说,如果所有连接都用同一个账号、同一个程序名连进来,那分类器就无从分辨,最后大家还是挤在默认池里——这也是很多配置“做了等于没做”的真实原因。
4.3 易混三:脚本跑完就以为生效了
易混点:CREATE RESOURCE POOL和CREATE WORKLOAD GROUP执行成功,就认为规则已经开始起作用。
为什么混:建表建索引都是执行完即生效,习惯成自然,就以为这套东西也一样。
怎么区分:结构建好只是第一步,还得把分类器函数挂上去,再显式执行一次ALTER RESOURCE GOVERNOR RECONFIGURE才算配置生效。更关键的是,分类只发生在连接建立的那一刻,已经连着的会话不会被重新分类,它们会一直待在原来的组里,直到断开重连。所以改完配置发现“没反应”,先别怀疑脚本,去看看那些连接是不是从早上一直挂着没动过。
4.4 易混四:以为内存百分比就是给缓冲池划地盘
易混点:认为给某个池设了 40% 的内存,就等于让它的数据缓存占了四成内存。
为什么混:平时说“数据库吃内存”,指的往往是缓冲池里缓存了多少数据页;而资源调控器里的内存,听起来像同一件事。
怎么区分:这两者不是一回事。资源调控器管的内存,主要是查询执行时的内存授予——排序、哈希连接、聚合这类操作需要申请一块工作内存,池子的内存上下限约束的是这块。数据页缓存由数据库引擎自己管,不在这套百分比的控制范围内。所以“给报表池限了内存,它的缓存为什么还那么大”这个问题,本身就问错了方向。
4.5 易混五:上限设了 50%,CPU 却冲到了 100%
易混点:MAX_CPU_PERCENT = 50,监控里却看到这个池子短时间内 CPU 打满,怀疑配置没生效。
为什么混:上限这两个字容易被理解成硬性封顶,越线就是异常。
怎么区分:这个上限是软性的,作用方式是“有争用时按比例让路”,而不是“到线就刹车”。所以短暂冲到高位属于正常行为,只要持续时间不长、不影响其他池子的保底,就不必紧张。真正需要警惕的是持续高位,那说明池子的负载已经超出它该有的份额,该做的是优化查询或调整保底与上限的比例,而不是反复检查配置有没有生效。
5. 一套能落地的配置骨架:从量数字到写脚本再到验证
概念清楚了,落到操作上是另一回事。资源调控器的配置分三步:先弄清楚现状,再搭结构,最后挂分类器并验证。跳过第一步直接写脚本,很容易配出一套看起来很合理、实际没起作用的规则。
5.1 动手之前先量三个数
配置的依据是观测数据,不是直觉。要量的第一个数是整机 CPU 的常态水位和峰值水位,这决定了你还有多少浮动空间可以分。第二个数是各业务连接的高峰时段是否重叠,如果交易系统和报表系统的高峰完全错开,那分配的紧迫性其实没那么高,浮动的收益也有限。第三个数是内存授予的峰值,也就是那些排序、哈希操作最凶的时候,一下子申请走了多少内存。
这三个数不用很精确,看一周的监控曲线就够。真正要避免的是“凭感觉给个 30%”,因为百分比一旦写进脚本,就会长期影响调度行为,改起来虽然不难,但排查“为什么这个库变慢了”的成本很高。宁可在纸上先把份额算一遍:几个池子的保底值加起来别贴着 100%,留出至少两三成的浮动空间。
5.2 建池和建组:先把结构搭出来
结构和参数可以一起写。下面这段只保留最关键的几个参数,实际配置时按前面量出来的数字调整:
-- 报表专用池:保底 20% CPU / 25% 内存,上限 60% / 50% CREATE RESOURCE POOL pool_report WITH ( MIN_CPU_PERCENT = 20, -- 争用时的 CPU 保底 MAX_CPU_PERCENT = 60, -- 软性上限,允许短暂冲高 MIN_MEMORY_PERCENT = 25, -- 争用时的内存保底 MAX_MEMORY_PERCENT = 50 -- 没人抢时可以用到这么多 ); GO -- 报表池下再分两个组:交互式查询、批量导出 CREATE WORKLOAD GROUP wg_report_online WITH ( IMPORTANCE = MEDIUM, -- 优先级:低/中/高 MAX_DOP = 4, -- 单查询最多 4 个并行 GROUP_MAX_REQUESTS = 30 -- 组内并发请求上限 ) USING pool_report; GO CREATE WORKLOAD GROUP wg_report_batch WITH ( IMPORTANCE = LOW, -- 批量任务让路 MAX_DOP = 2, GROUP_MAX_REQUESTS = 5, REQUEST_MAX_CPU_TIME_SEC = 600 -- 单请求 CPU 时间上限(秒) ) USING pool_report; GO这段脚本干的活是:先划出一个有保底、有上限的池子,再在这个池子里放两个组,一个跑交互式查询、一个跑批量导出。几个参数的含义值得单独说一句:MIN_CPU_PERCENT和MIN_MEMORY_PERCENT是争用时才兑现的承诺,平时不占着;MAX_CPU_PERCENT是软上限,允许短时越线;IMPORTANCE只影响 CPU 调度顺序,不改变内存分配;REQUEST_MAX_CPU_TIME_SEC超限时不会杀掉查询,只是记一条事件,用来事后排查哪个请求跑飞了;GROUP_MAX_REQUESTS则是实打实的排队上限,超出的请求会等着。
5.3 分类器函数:判断逻辑只能写得很轻
分类器是整套机制里唯一需要写业务判断的地方,也最容易写复杂。原则只有一条:只看连接属性,别想着查表。下面这段按应用程序名和登录名分流:
USE master; GO CREATE FUNCTION dbo.fn_rg_classifier() RETURNS SYSNAME WITH SCHEMABINDING AS BEGIN DECLARE @wg SYSNAME = N'default'; -- 兜底:认不出来就进默认组 IF APP_NAME() LIKE N'%ReportBatch%' -- 批量导出工具专用连接串 SET @wg = N'wg_report_batch'; ELSE IF APP_NAME() LIKE N'%Report%' -- 交互式报表工具 SET @wg = N'wg_report_online'; ELSE IF SUSER_SNAME() = N'report_reader' -- 只读报表账号兜底 SET @wg = N'wg_report_online'; RETURN @wg; END; GO -- 挂上分类器并让配置生效 ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = dbo.fn_rg_classifier); GO ALTER RESOURCE GOVERNOR RECONFIGURE; GO函数里用了两个信息源:APP_NAME()取的是客户端连接串里的应用程序名,SUSER_SNAME()取的是登录名。这两样东西在连接建立时就已经确定,所以判断几乎是零成本。返回值必须是已经存在的工作负载组名;写错名字或者返回空值都不会报错,连接会安静地落进默认组——这个“静默失败”是排查时最容易忽略的一点,配完一定要去确认会话是不是真的进了预期的组。
5.4 生效、重新分类与验证
配置生效之后,新连接会按新规则分类,老连接不会。如果想让已经挂着的连接重新走一遍分类,官方给的套路是先把分类器摘掉、重配一次,再挂回去:
-- 第一步:摘掉分类器并重配,让现有会话回到默认组 ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = NULL); ALTER RESOURCE GOVERNOR RECONFIGURE; GO -- 第二步:重新挂上分类器并重配,现有会话会被重新分类 ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = dbo.fn_rg_classifier); ALTER RESOURCE GOVERNOR RECONFIGURE; GO验证要落到具体数字上,别只看“配置界面显示成功”。可以查两个方向:一是看会话落在了哪个组,sys.dm_exec_sessions里有group_id,跟sys.dm_resource_governor_workload_groups对一下就知道;二是看池子和组的实际用量,sys.dm_resource_governor_resource_pools和sys.dm_resource_governor_workload_groups里能看到当前用了多少 CPU、多少内存、排队情况如何。
-- 看每个会话被分到了哪个工作负载组 SELECT s.session_id, s.login_name, s.program_name, g.name AS workload_group FROM sys.dm_exec_sessions AS s JOIN sys.dm_resource_governor_workload_groups AS g ON s.group_id = g.group_id WHERE s.is_user_process = 1;最靠谱的验证方式还是压一次:让批量任务跑起来,同时在另一个窗口跑交互式查询,看两边的耗时变化。配置对了,批量任务会把 CPU 让出来,交互式查询的响应时间应该明显稳住;配置没起作用,两边的表现会和配之前一模一样。
5.5 出问题怎么退回去
任何配置都要留退路,这套东西的退路很干脆:把分类器摘掉,所有连接就都回到默认池,行为和没配一样。
-- 紧急回退:摘掉分类器,资源分配恢复默认行为 ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = NULL); ALTER RESOURCE GOVERNOR RECONFIGURE; GO -- 确认已停用 SELECT * FROM sys.dm_resource_governor_configuration;池子和组可以先留着不删,等确认问题不是出在分配上再处理。常见的翻车点就那么几个:分类器返回了不存在的组名,所有人挤在默认池;保底值设得太满,机器退化成固定切分;只盯 CPU 忘了内存,结果查询因为内存授予排队而变慢。每一个都能靠前面的验证语句定位,怕的是不验证就上线,然后靠“感觉变快了”下结论。
6. 什么时候该用它,什么时候该老实加内存
这套机制有明确的适用边界。业务分级清晰、多个负载混跑在一台机器上、关键业务在高峰期被拖累——这类场景值得上。反过来,如果只有一两个数据库、负载曲线平缓、资源从来没紧张过,配这套东西纯属给自己找活干:多了一层分类逻辑,就多了一个可能出错的环节。
| 场景 | 建议 |
|---|---|
| 交易库和报表库混跑在同一台机器 | 值得配,给交易库设保底,给报表池设上限 |
| 多个业务库共用一台服务器,高峰撞车 | 值得配,按业务重要性分池分优先级 |
| 单个数据库,负载平稳 | 不必配,先优化慢查询 |
| 整机资源长期打满,所有业务都慢 | 先扩容,再谈分配 |
| 连接全用同一个账号、同一个程序名 | 先改连接管理,否则分类器无从下手 |
举个具体的例子。某公司的服务器上跑着交易库和报表库,报表每天凌晨跑批量、白天跑零散查询,交易的高峰在上午十点和下午三点。这种负载其实错开得不错,真正撞车的只有白天那几段。配置的重点就该放在“白天不让批量任务和零散报表把 CPU 抢光”上:给报表池设一个 20% 到 25% 的 CPU 保底、50% 左右的上限,批量导出组设低优先级、并行度压到 2、并发数压到 5,交易库留在默认池里不动。这样凌晨资源照样能给报表用满,白天交易高峰时报表池自动让路,不需要人半夜起来改配置。
我自己的习惯是:每次动这类配置之前,先把回退脚本写好贴在旁边,再压一次做对比。有一次为了省事跳过了压测,直接在生产上挂分类器,结果函数里匹配条件写宽了,把业务系统的连接也判进了批量组,第二天早上交易接口集体变慢,排查了两个小时才发现是分类器的问题——从那以后我每次改分类器,都强制先在测试库上跑一遍sys.dm_exec_sessions的对照查询,确认每个来源都落进该落的组,才敢往生产上推。资源分配这事,配得对不对,监控曲线比直觉诚实得多。
希望这些能帮你把 CPU 和内存的分配从“凭感觉”变成“有依据”。这份资料里把资源池、工作负载组和分类器脚本的细节都整理齐了,需要的时候取来照着改就行。
本文还有配套的精品资源,点击获取