这章笔记积压了好一阵,最近翻出来整理,发现里面有几处当时踩坑的痕迹还挺值得记一笔的。第74章到第76章,恰好覆盖了SQL Server日常运维里最容易出问题的三个面:权限、命令行工具、资源治理。权限是每个DBA都绕不开的日常,SQLCMD是自动化脚本的基础,资源调控器则是多业务共库时保命的东西。三章内容跨度不小,但实际使用中它们是串在一条线上的——没有合理的权限规划,SQLCMD脚本跑起来会到处碰壁;没有资源调控器,一个失控的查询就能拖垮整个实例。这篇笔记就把这三块一起整理掉,该给原理的地方给原理,该给脚本的地方给脚本,都是我实测过能直接用的。
1. 权限或许可:理解SQL Server的授权模型
1.1 从一次"用户无法登录"的排查说起
先从一个最典型的场景说起。应用上线第二天,开发跑过来说数据库连不上,报错信息大概是用户 'app_user' 登录失败。第一次遇这事的人最容易懵,因为在SSMS里明明能看到这个用户存在,权限也给了,但就是登不上去。
这里涉及SQL Server权限模型的第一层概念:**登录名(Login)和数据库用户(User)**的区别。登录名是实例级别的,存在于master库中,负责验证"你能不能连上这台服务器";数据库用户是数据库级别的,存在于各个业务库中,负责验证"你连上之后能碰哪些数据"。两个层级都需要创建,而且要通过SID关联起来,缺一环都会导致登录失败。
sql复制-- 在master库创建登录名(实例级)
CREATE LOGIN [app_user] WITH PASSWORD = 'StrongPassword!', CHECK_POLICY = ON;
-- 在业务数据库创建用户(库级),并关联到已创建的登录名
USE [YourDatabase];
CREATE USER [app_user] FOR LOGIN [app_user];
很多新手只创建了Login,没创建User,或者反过来只建了User没建Login,就会出现各种奇怪的登录异常。SSMS里图形化操作容易让人忽略这一点,因为向导会把两步合并起来,但在脚本化部署时特别容易漏。这也是为什么我一直建议:权限相关的操作要写成脚本、走版本管理,而不是每次都在GUI里点。
1.2 固定服务器角色与固定数据库角色
理解了登录名和用户的关系之后,下一个问题就是:给多少权限合适?SQL Server提供了一套固定角色体系,用来简化权限授予,不需要一条条GRANT。
固定服务器角色作用在实例级别,常见的有这些:
| 角色 | 权限范围 | 适用场景 |
|---|---|---|
| sysadmin | 实例内所有权限,相当于超级管理员 | DBA专用,绝对不能乱给 |
| serveradmin | 修改服务器配置、关闭服务器 | 需要改全局配置的运维人员 |
| securityadmin | 管理登录名和权限 | 负责账号管理的运维 |
| dbcreator | 创建、修改、删除数据库 | 有新建库需求的开发负责人 |
| processadmin | 管理实例中运行的进程,可KILL会话 | 需要终止失控会话的运维 |
| setupadmin | 管理链接服务器等 | 少数特定场景 |
| bulkadmin | 执行BULK INSERT操作 | 需要批量导数的场景 |
固定数据库角色作用在单一数据库内,常用的是db_owner、db_datareader、db_datawriter、db_ddladmin这几个。db_datareader就是只读,db_datawriter就是只写不读,db_ddladmin能改表结构但改不了数据。
sql复制-- 给app_user在YourDatabase中增加只读权限
USE [YourDatabase];
ALTER ROLE [db_datareader] ADD MEMBER [app_user];
这里有个坑我踩过不止一次:如果同时授予了多个角色,权限是叠加的。比如app_user同时属于db_datareader和db_datawriter,那它既读也写。但如果你想让某个角色"只读但能执行存储过程",光给db_datareader是不够的,因为proc的EXECUTE权限是单独的,需要额外GRANT。
sql复制USE [YourDatabase];
GRANT EXECUTE ON SCHEMA::dbo TO [app_user];
1.3 权限的优先级:DENY高于GRANT,以及所有权链
SQL Server的权限判定有一条硬规则:DENY优先于GRANT。不管用户通过角色继承了多少个GRANT,只要存在一条显式的DENY,最终结果就是拒绝。这条规则在排障时特别有用——有时候用户突然没权限了,怎么查都查不到原因,最后发现是某个角色的DENY改错了。
另一个常被忽略的概念是所有权链接(Ownership Chaining)。如果存储过程的所有者(通常是dbo)和表的所有者是同一个,那么只要用户有EXECUTE权限,就能通过存储过程访问表数据,不需要单独授予表的SELECT权限。这在设计权限体系时非常实用,可以让应用只依赖一组存储过程接口,而不直接暴露表。
但所有权链也会带来安全隐患。如果存储过程的所有者是schema owner(比如某开发账号),而表和过程属于同一个schema,可能会意外把底表暴露出去。所以我的建议是:过程全部由dbo拥有,也就是创建时用EXECUTE AS OWNER或者确保创建者是dbo。
sql复制CREATE PROCEDURE dbo.GetUserData
@UserID INT
WITH EXECUTE AS OWNER
AS
BEGIN
SELECT UserName, Email
FROM dbo.Users
WHERE UserID = @UserID;
END;
这个WITH EXECUTE AS OWNER是最容易忽略但最有价值的一句。它让过程以其所有者的身份执行,权限判断的粒度从"用户对表"变成了"用户对过程",这是SQL Server权限设计里"以应用为中心"和"以数据为中心"两种思路的分水岭。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 权限规划实操:最小权限原则的落地
2.1 按场景拆解权限需求
权限这件事,理论说再多都不如直接套场景。我通常把权限需求分成四类:
- DBA场景:sysadmin或serveradmin,这类账号必须独立,不能和业务账号混用。
- 应用场景:只连接指定数据库,通常给db_datareader + db_datawriter + EXECUTE权限,必要时再加VIEW DEFINITION权限(允许查看表结构)。
- 报表场景:只读权限,通常给db_datareader,如果报表要调用函数或视图,再加对应对象的SELECT权限。
- 开发场景:需要能改结构,所以会给db_ddladmin,但一般不给db_owner,防止有人DROP或TRUNCATE整表。(虽然db_ddladmin也能DROP表,但至少权限边界清晰一点)
每个场景的权限边界要写清楚,不能想加就加。权限变更必须走审批流,这是一条经验铁律——我见过太多因为"临时加一下"导致的数据事故,"临时"往往就变成"长期"了。
2.2 权限检查脚本与常见误区
接手一个老库的时候,第一步就是盘点存量权限。下面这个脚本能查出某数据库内所有用户及其角色成员关系:
sql复制USE [YourDatabase];
SELECT
dp.name AS principal_name,
dp.type_desc AS principal_type,
ISNULL(rm.role_name, 'NO_ROLE') AS database_role
FROM sys.database_principals dp
LEFT JOIN (
SELECT
member_principal_id,
role_principal_id,
USER_NAME(role_principal_id) AS role_name
FROM sys.database_role_members
) rm ON dp.principal_id = rm.member_principal_id
WHERE dp.type IN ('S', 'U') -- S=SQL User, U=Windows User
ORDER BY dp.name;
如果要查账号级别授予的具体对象权限,用这个:
sql复制SELECT
princ.name AS user_name,
obj.name AS object_name,
perm.permission_name,
perm.state_desc
FROM sys.database_principals princ
JOIN sys.database_permissions perm ON princ.principal_id = perm.grantee_principal_id
LEFT JOIN sys.objects obj ON perm.major_id = obj.object_id
WHERE princ.type IN ('S', 'U')
ORDER BY princ.name, obj.name;
关于权限检查,两个常见误区需要提醒。第一,VIEW DEFINITION权限经常被遗忘,导致应用能连库但ORM工具读不到表结构,报错信息千奇百怪。第二,EXECUTE权限默认是授予public角色的,但如果你把数据库的TRUSTWORTHY设为ON,等于告诉SQL Server"这个库里的对象可以访问外部资源",很多权限设置会因此失去意义。一般业务库不建议开这个选项。
2.3 权限变更的审计与回滚方案
权限变更最怕的是"改完就忘"。我的习惯是每次变更前先导出变更前的权限快照,变更后再导一份,两个快照做对比,确认改动范围符合预期。
sql复制-- 导出当前数据库权限快照
USE [YourDatabase];
SELECT
princ.name AS user_name,
ISNULL(perm.permission_name, 'ROLE_MEMBER') AS permission_name,
ISNULL(perm.state_desc, 'MEMBER_OF') AS state_desc,
ISNULL(obj.name, ISNULL(rm.role_name, '')) AS object_name
FROM sys.database_principals princ
LEFT JOIN sys.database_permissions perm ON princ.principal_id = perm.grantee_principal_id
LEFT JOIN sys.objects obj ON perm.major_id = obj.object_id
LEFT JOIN (
SELECT member_principal_id, USER_NAME(role_principal_id) AS role_name
FROM sys.database_role_members
) rm ON princ.principal_id = rm.member_principal_id
WHERE princ.type IN ('S', 'U');
把这条脚本存成sp_export_permissions.sql,每次变更前后各跑一次,结果导出到文本文件再对比。回滚时直接把差异部分生成反向GRANT/DENY脚本即可。这套流程在多次生产变更中帮我兜了底,值得养成习惯。
3. SQLCMD:命令行运维的第一把刀
3.1 SQLCMD是什么,以及它解决了什么问题
SQLCMD是SQL Server自带的命令行工具,可以执行T-SQL脚本、批量处理任务、自动化部署。很多DBA日常工作离不开SSMS,但SSMS属于交互式工具,无法在无人值守的深夜自动执行任务,而SQLCMD天生就是干这个的。
SQLCMD本质上是sqlcmd.exe这个程序,安装SQL Server Management Studio时会一并装好,也可以通过安装SQL Server命令行工具单独获得。它的工作原理很简单:通过ODBC或OLEDB驱动连接SQL Server,逐批执行T-SQL语句,支持脚本文件、变量、条件逻辑等高级特性。
很多人觉得SQLCMD就是"命令行里的SSMS",这个理解不准确。SQLCMD真正的价值在于把数据库运维变成可脚本化、可编排的流程。你可以把建表、授权、插入初始化数据写成一个.sql文件,然后一行命令在任意环境执行;也可以写一个循环,对服务器列表逐台执行检查脚本;还可以在PowerShell里调用SQLCMD,嵌入到自动化发布管道中。
3.2 SQLCMD核心参数一览
SQLCMD的参数不算多,但每个都值得吃透。
| 参数 | 作用 | 示例 |
|---|---|---|
-S |
指定服务器实例 | -S localhost 或 -S .\SQLEXPRESS |
-U / -P |
SQL认证的用户名密码 | -U sa -P 'YourPassword' |
-E |
使用Windows认证 | -E |
-d |
指定连接的数据库 | -d YourDatabase |
-i |
输入脚本文件 | -i deploy.sql |
-o |
输出结果到文件 | -o output.txt |
-Q |
执行单条SQL后退出 | -Q "SELECT @@VERSION" |
-q |
执行单条SQL但不退出(交互式) | -q "SELECT 1" |
-v |
定义脚本变量 | -v dbname="MyDB" |
-b |
出错时返回错误码,可被脚本捕获 | -b |
-C |
信任服务器证书(启用SSL时) | -C |
这里要注意-b这个参数,运维脚本里几乎是必加的。不带-b时,SQLCMD跑了报错也可能返回退出码0,脚本以为成功了,白忙一场。加上-b之后,任何T-SQL错误都会让SQLCMD返回非零退出码,PowerShell或批处理才能正确判断执行结果。
bash复制sqlcmd -S localhost -U sa -P 'YourPassword' -d master -b -Q "RESTORE DATABASE [YourDB] FROM DISK='D:\backup\YourDB.bak' WITH REPLACE"
echo "Exit code: $?"
如果返回的退出码不是0,就说明恢复失败,可以继续处理后续逻辑。这个习惯在自动化部署中极其重要。
3.3 用SQLCMD做自动化部署的示例
一次典型的基础环境部署,用SSMS需要新建查询、逐段执行、人工确认,用SQLCMD可以一条命令串联起来。
假设要在一台新服务器上完成:创建数据库、创建登录名、创建用户、授权、执行初始化数据脚本。整个过程写成三个文件:
01_create_db.sql:
sql复制IF DB_ID('OrderSystem') IS NULL
BEGIN
CREATE DATABASE [OrderSystem];
END;
GO
02_create_login_user.sql:
sql复制USE [master];
IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = 'order_app')
BEGIN
CREATE LOGIN [order_app] WITH PASSWORD = 'AppPass@2024', CHECK_POLICY = ON;
END;
USE [OrderSystem];
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = 'order_app')
BEGIN
CREATE USER [order_app] FOR LOGIN [order_app];
ALTER ROLE [db_datareader] ADD MEMBER [order_app];
ALTER ROLE [db_datawriter] ADD MEMBER [order_app];
END;
GO
03_init_data.sql,里面是业务初始化数据,按需编写即可。
然后执行:
bash复制sqlcmd -S localhost -U sa -P 'YourPassword' -b -i 01_create_db.sql
sqlcmd -S localhost -U sa -P 'YourPassword' -b -i 02_create_login_user.sql
sqlcmd -S localhost -U sa -P 'YourPassword' -d OrderSystem -b -i 03_init_data.sql
这样三步就把环境搭起来了,整个过程不用打开一次SSMS。后续维护时,重新执行这套脚本就能重建环境,完全可以纳入CI/CD流程。
4. SQLCMD高级技巧:变量、脚本文件与错误处理
4.1 变量传递与脚本复用
脚本写多了之后,最痛苦的是"环境不同参数不同"。SQLCMD的-v参数正是解决这个问题的:在SQL脚本里用$(变量名)占位,执行时通过-v 变量名=值传入。
举个例子,一个建库脚本create_db_template.sql:
sql复制USE [master];
IF DB_ID('$(dbname)') IS NULL
BEGIN
CREATE DATABASE [$(dbname)];
END;
GO
部署时这样调用:
bash复制sqlcmd -S localhost -U sa -P 'YourPassword' -b -v dbname="OrderSystem" -i create_db_template.sql
sqlcmd -S localhost -U sa -P 'YourPassword' -b -v dbname="WarehouseSystem" -i create_db_template.sql
一套模板,多环境复用,这才是脚本化运维的日常状态。
SQLCMD还支持在脚本内通过:setvar命令来定义变量,适合在脚本头部统一配置默认值:
sql复制:setvar dbname "DefaultDB"
IF DB_ID('$(dbname)') IS NULL
BEGIN
CREATE DATABASE [$(dbname)];
END;
GO
如果既在脚本内:setvar指定了默认值,又用-v传入值,-v的优先级更高。这个规则很适合"默认值+环境覆盖"的部署策略。
4.2 错误处理与基于退出码的控制流
前面提到-b参数的重要性,这里展开讲一下完整的错误处理模式。
在批处理(cmd)中:
bat复制@echo off
setlocal
set DBNAME=OrderSystem
sqlcmd -S localhost -U sa -P "YourPassword" -b -Q "CREATE DATABASE [%DBNAME%]"
if %errorlevel% neq 0 (
echo [ERROR] Failed to create database [%DBNAME%]
exit /b %errorlevel%
)
echo [INFO] Database [%DBNAME%] created successfully.
在PowerShell中:
powershell复制$dbname = "OrderSystem"
$result = sqlcmd -S localhost -U sa -P "YourPassword" -b -Q "CREATE DATABASE [$dbname]" 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] Failed to create database [$dbname]" -ForegroundColor Red
exit $LASTEXITCODE
}
Write-Host "[INFO] Database [$dbname] created successfully." -ForegroundColor Green
这个$LASTEXITCODE判断是自动化部署成功与否的分水岭。我不止一次看到有人忘记处理退出码,结果SQL报错了后面的流程照常跑,最后客户拿着报错截图来找人。把-b和退出码检查当成肌肉记忆就好了。
4.3 SQLCMD与PowerShell的搭配场景
单条SQLCMD命令完成不了太复杂的编排,但PowerShell可以调用SQLCMD,两者配合能实现相当强大的自动化。
一个实际场景:批量检查多个SQL Server实例的版本和状态。
powershell复制$servers = @("server01", "server02", "server03", "server04")
foreach ($s in $servers) {
$sql = "SELECT @@SERVERNAME AS ServerName, @@VERSION AS Version"
Write-Host "=== Checking $s ===" -ForegroundColor Cyan
sqlcmd -S $s -U sa -P "YourPassword" -b -Q $sql
}
另一个场景:批量执行数据库完整性检查(DBCC CHECKDB),把结果输出到文件再统一处理。如果实例数量较多,SQLCMD的-i参数可以接受来自标准输入的脚本,结合PowerShell的管道可以实现动态拼接。
powershell复制$databases = @("OrderSystem", "WarehouseSystem", "ReportSystem")
$script = $databases | ForEach-Object {
"DBCC CHECKDB ([$_]) WITH NO_INFOMSGS;"
} | Out-String
$script | sqlcmd -S localhost -U sa -P "YourPassword" -b -d master
这个例子说明SQLCMD不只是"命令行执行SQL",它是可以被其他语言编排的数据库操作原语。在自动化运维和DevOps管道中,这一层编排能力是SSMS完全无法替代的。
4.4 SQLCMD中容易踩的坑
用SQLCMD的时间长了,几个印象深刻的坑值得记录:
坑一:编码问题导致的中文乱码。 如果SQL脚本文件保存为UTF-8 without BOM,SQLCMD读取时可能把中文当成乱码。解决方法是把脚本保存为UTF-8 with BOM,或者统一使用-f 65001指定代码页65001(UTF-8)。自己写脚本时注意这一点,能让很多奇怪问题直接消失。
坑二:GO批处理分隔符。 SQLCMD把GO解释为批处理的结束,而不是T-SQL语句。在某些场景下(比如往字段里插入字符串GO),可能会被SQLCMD误伤。如果脚本内容里不需要批处理分隔,可以用-y参数关闭,或者尽量在脚本中避免裸的GO关键词。
坑三:-Q和-q的顺序问题。 -Q执行完就退出,-q执行完不退出(进入交互模式)。如果SQL里连接了数据库并创建了临时表#temp,-Q退出后连接关闭、临时表销毁;-q保持会话,后续命令还能访问#temp。自动化脚本里几乎都用-Q,记住这一点就好。
坑四:用户名密码中的特殊字符。 如果密码包含引号、空格或者大小写敏感的特殊字符,在PowerShell或批处理中容易因为转义问题出错。我的习惯是:能不用-P就不用-P,改用环境变量、-E(Windows认证)或者SQLCMD的凭据配置来传入密码。在CI/CD系统中,密码存在凭据管理器或密钥保险箱里,比明文写在命令行里安全得多。
5. 资源调控器:让SQL Server按优先级分配资源
5.1 为什么要用资源调控器
多业务共用一个数据库实例的时候,最头疼的问题就是:某个报表查询把CPU和IO吃满了,高峰期交易系统跟着遭殃。SQL Server本身没有"自动限速"的能力,如果不去干预,任何一个会话都有可能耗尽所有资源。
资源调控器(Resource Governor)就是干这个的。它允许你根据会话特征(比如登录名、应用程序名、主机名),把请求划分到不同的工作负荷组,每组绑定一个资源池,池子分别限制CPU、内存和IO的用量。通俗讲,就是给不同的业务流量划出"车道",并给每条车道设好最高速度,超速就限流。
SQL Server 2022之前的版本里,资源调控器只管控CPU和内存,IO管控能力较弱。SQL Server 2022开始引入了IOPS的管控能力,通过MIN_IOPS_PER_VOLUME和MAX_IOPS_PER_VOLUME参数来限制资源池的磁盘吞吐。如果你的本地或虚拟机磁盘是SSD,IO限制的效果非常明显。
5.2 资源调控器核心概念:资源池、工作负荷组、分类器函数
资源调控器由三个核心对象组成:
资源池(Resource Pool) 是资源分配的最小单位,类似于"容器"。每个池子有最小和最大的CPU带宽、内存用量、IOPS限制。例如给报表池分配最多30%的CPU,给核心交易池保证至少50%的CPU。
工作负荷组(Workload Group) 是资源的消费单位。每个组必须属于一个资源池,它会继承池子的资源限制。组内还可以设置REQUEST_MAX_MEMORY_GRANT_PERCENT(单次请求能申请的最大内存比例)、REQUEST_MAX_CPU_TIME_SEC(单次请求的CPU时间上限)、GROUP_MAX_REQUESTS(同时并发请求数上限)等参数。
分类器函数(Classifier Function) 是资源调控器的"入口闸门"。它接收每个新会话的属性(如SUSER_SNAME()、APP_NAME()、HOST_NAME()),返回该会话应归属的工作负荷组名称。分类器函数是在每次会话建立时执行的,不能对已有会话生效。
这三个对象配合使用,就构成了完整的资源治理体系。需要注意的是,资源调控器的分类器函数是应用于新会话的,修改分类器函数后已经存在的会话不会受到新规则影响,但新的连接会使用新的分类结果。
5.3 资源调控器配置实操
配置流程分四步:启用、创建资源池、创建工作负荷组、创建分类器函数。
第一步:启用资源调控器。
资源调控器默认是禁用的,需要先启用。
sql复制ALTER RESOURCE GOVERNOR RECONFIGURE;
如果还没有启用,执行这个语句会报错,需要先执行:
sql复制ALTER RESOURCE GOVERNOR ENABLE;
ALTER RESOURCE GOVERNOR RECONFIGURE;
第二步:创建资源池。
sql复制USE [master];
GO
-- 核心交易池:保证至少50% CPU
CREATE RESOURCE POOL [Pool_OLTP]
WITH (
MIN_CPU_PERCENT = 50,
MAX_CPU_PERCENT = 100,
CAP_CPU_PERCENT = 100,
MIN_MEMORY_PERCENT = 40,
MAX_MEMORY_PERCENT = 80
);
GO
-- 报表池:最高占用30% CPU
CREATE RESOURCE POOL [Pool_Report]
WITH (
MIN_CPU_PERCENT = 0,
MAX_CPU_PERCENT = 30,
CAP_CPU_PERCENT = 30,
MIN_MEMORY_PERCENT = 10,
MAX_MEMORY_PERCENT = 30
);
GO
第三步:创建工作负荷组,绑定到资源池。
sql复制CREATE WORKLOAD GROUP [Group_OLTP]
WITH (
REQUEST_MAX_MEMORY_GRANT_PERCENT = 10,
REQUEST_MAX_CPU_TIME_SEC = 30,
GROUP_MAX_REQUESTS = 0,
IMPORTANCE = HIGH
)
USING [Pool_OLTP];
GO
CREATE WORKLOAD GROUP [Group_Report]
WITH (
REQUEST_MAX_MEMORY_GRANT_PERCENT = 5,
REQUEST_MAX_CPU_TIME_SEC = 120,
GROUP_MAX_REQUESTS = 0,
IMPORTANCE = MEDIUM
)
USING [Pool_Report];
GO
第四步:创建分类器函数。
分类器函数必须是schema-bound(SCHEMABINDING)的函数,而且不能有入参。它通过当前会话的上下文(登录名、应用名、主机名等)来判断走哪个组。
sql复制USE [master];
GO
CREATE FUNCTION dbo.fn_classify_workload()
RETURNS SYSNAME
WITH SCHEMABINDING
AS
BEGIN
DECLARE @group_name SYSNAME;
IF SUSER_SNAME() = 'report_user'
SET @group_name = 'Group_Report';
ELSE IF APP_NAME() LIKE '%ReportServer%'
SET @group_name = 'Group_Report';
ELSE IF SUSER_SNAME() = 'oltp_app_user'
SET @group_name = 'Group_OLTP';
ELSE
SET @group_name = 'default'; -- 默认组
RETURN @group_name;
END;
GO
ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = dbo.fn_classify_workload);
ALTER RESOURCE GOVERNOR RECONFIGURE;
GO
完成这四步后,新连接会按分类器函数的规则自动分配到对应的工作负荷组。用report_user登录的请求全部进入报表组,被限制在30% CPU以内;oltp_app_user的请求进入交易组,保证至少50% CPU。
5.4 配置生效前的前置检查
还有一个前置检查经常被忽略:要启用资源调控器,服务器必须至少有2个逻辑CPU。单核处理器无法使用资源调控器,这在虚拟化环境里很常见,配置虚拟机时最少给2 vCPU,否则一切配置都不生效。
另外,下面这几条DMV能帮你确认配置是否生效:
sql复制SELECT
pool_id,
name AS pool_name,
stat.cpu_usage_percent,
stat.memory_usage_percent
FROM sys.dm_resource_governor_resource_pools AS rp
CROSS APPLY sys.dm_resource_governor_resource_pool_stats AS stat
WHERE rp.pool_id = stat.pool_id;
SELECT
group_id,
name AS group_name,
total_request_count,
total_cpu_limit_violation_count,
total_cpu_usage_ms
FROM sys.dm_resource_governor_workload_groups;
这些查询能反映当前资源分配的实际运行情况,是验证资源调控器是否生效的一手数据。
6. 资源调控器的验证与优化:如何确认它在起作用
6.1 模拟高负载的行为对比
配置完成后最关心的就是:到底有没有生效?最直接的办法是制造高并发请求,然后对比限流前后的表现。
比如用report_user登录,连续跑多个需要大开销的查询或循环插入测试数据,观察资源池的cpu_usage_percent是否被压到30%以下。反过来,同时用oltp_app_user登录跑批处理,观察它是否仍能获得比较高的CPU资源。
sql复制-- 模拟报表组的高负载:开多个会话执行大量循环
DECLARE @i INT = 0;
WHILE @i < 100000
BEGIN
INSERT INTO dbo.TestLog (LogTime, LogText)
VALUES (GETDATE(), REPLICATE('X', 1000));
SET @i = @i + 1;
END;
在多会话并发执行这段代码的同时,观察DMV中的资源池统计。如果Group_Report所在资源池的CPU占用被封顶在30%,说明资源调控器确实起作用了。
6.2 资源调控器经典坑点
资源调控器用了几年,下面几个坑印象最深刻:
坑一:临时表空间共享的假象。 有人在tempdb上发现IO还是很高,以为是资源调控器没生效。其实资源调控器管的是tempdb系统资源池之外的用户资源池,tempdb本身始终共享。如果临时表创建太频繁,IO瓶颈可能还是出在tempdb上。解决办法是减少临时表的使用,或者把tempdb放到独立且快速的磁盘上。
坑二:ALTER RESOURCE GOVERNOR RECONFIGURE之后配置没生效。 可能原因:分类器函数没有重新绑定,或者函数名写错。分类器函数的修改需要先ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = ...)再RECONFIGURE,顺序不能乱。还有一点:分类器函数的返回值和组名必须精确匹配,如果返回了不存在的组名,该会话会被导向默认组(default),看上去就像配置失效一样。
坑三:并发数限制的隐含影响。 GROUP_MAX_REQUESTS参数如果设置得太小,高并发时请求会被阻塞并排队,表现为延迟突然升高。这个参数的设计意图是保护资源池不被无限制的并发拖垮,但设得太小反而会造成应用超时。建议从0(不限制)开始,观察实际并发量再逐步调整。我曾经把GROUP_MAX_REQUESTS设为5,结果报表端出现大量排队,后来调整到50才恢复。
坑四:分类器函数对已有会话不生效。 分类器函数在连接建立时执行,已经建立的连接不会因为修改分类器函数而改变工作负荷组。这是资源调控器的固有行为。如果希望变更立即生效,需要让应用程序断开并重连,或者使用KILL命令终止对应会话。
6.3 资源调控器与Always On可用性组的关系
在Always On可用性组中,资源调控器有一些额外的注意事项。主副本上的资源调控器配置不会自动同步到辅助副本,需要在每个副本上单独配置。只读路由(Read-Only Routing)连接到的辅助副本,同样走辅助副本自己的资源调控器配置。
如果使用HADR特定的工作负荷组,SQL Server 2012及以上版本有一个内置的internal组用于系统内部任务,alwayson_secondary组可以用于限制辅助副本上的资源占用,这个组在可用性组被创建时自动添加。如果辅助副本上的备份、DBCC CHECKDB等任务经常影响主副本的同步,可以给alwayson_secondary组设置资源限制,让辅助副本任务尽量不抢占主副本的资源。
我实际遇到的一个场景:一台总容量有限的双副本集群,辅助副本上每天跑通宵的索引维护和CHECKDB,结果主副本的提交延迟升高,业务侧感知到写入变慢。后来在辅助副本上调整了alwayson_secondary组的CPU和内存上限,让维护任务避开业务高峰,主副本的响应才恢复平稳。
6.4 把三章串起来:一个综合运维场景
权限、SQLCMD、资源调控器三块看似独立,实际使用中经常要协同。
比如一次完整的"新业务线接入数据库"流程:
- 用SQLCMD脚本创建登录名和数据库用户,授予最小必要权限(第74章)。
- 用SQLCMD执行初始化脚本,建表、建存储过程、灌入初始数据。
- 配置资源调控器,新增一个工作负荷组绑定到该业务线专属的登录名,限制其CPU和内存上限(第76章)。
- 通过SQLCMD定时任务(SQL Agent或外部调度)检查资源池使用情况和权限变更记录,发现异常及时处理。
这三章内容的价值不只是各自能干什么,而是合在一起让"数据库运维"变成了一个可以被编程、被度量、被自动化的流程。权限决定了谁能进来,SQLCMD决定了怎么进来并执行什么,资源调控器决定了进来之后能用多少资源。少了任何一块,运维自动化都只是空中楼阁。
从实际运维的角度看,这三块知识最值得投入时间的地方不是记住每条语法,而是理解每个机制背后的设计意图:权限体系的边界是逻辑边界,SQLCMD的边界是流程自动化,资源调控器的边界是物理资源治理。想清楚这一点,再去看官方文档和报错信息,思路会清晰很多。
