Carry の Blog Carry の Blog
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Carry の Blog

好记性不如烂键盘
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • MySQL

  • Redis

  • 高性能KV

  • TiDB

  • Elasticsearch

  • 数据管道

  • 其他数据库

    • SQL Server 2019 for Linux 安装与配置 Always On 高可用集群实战教程
    • MongoDB 集群的安装部署详细流程
    • MongoDB 集群架构介绍
    • 当监控说没事而 DMV 说有事——N9E 与 SQL Server 指标交叉验证实战
    • 托管 SQL Server 的运维边界:哪些 DBA 手段会失效,以及用什么替代
      • 一、先确认自己站在哪一侧
      • 二、逐条失效清单
        • 1. xp_cmdshell —— 彻底没有
        • 2. BACKUP / RESTORE DATABASE —— 被完全接管
        • 3. DBCC 的缓存类命令 —— 拒绝
        • 4. sp_configure —— 读得到,改不了
        • 5. SQL Server Agent —— 看不到自己的作业
        • 6. 文件系统 —— 路径可见,内容不可及
        • 7. 边界的另一半:rds_* 那组过程
      • 三、还能做的部分:托管环境下的 DMV 巡检
        • 1. 容量水位:要按文件看,不要按库看
        • 2. 等待统计:托管实例的噪音更大
        • 3. 慢查询:按累计耗时排,不要按单次
      • 四、一张迁移前的自查表
      • 小结
  • 数据库
  • 其他数据库
Carry の Blog
2026-08-28
目录

托管 SQL Server 的运维边界:哪些 DBA 手段会失效,以及用什么替代原创

从自建 SQL Server 转到云托管实例(AWS RDS / Azure SQL MI 这类),最容易踩的不是性能问题, 而是手上那套 DBA 动作有一半会直接报权限错。托管服务把 sysadmin 收走了,你拿到的是一个 被裁剪过的高权限账号——能做绝大部分日常运维,但凡是「可能动到实例底层或宿主机」的操作全部封死。

这篇把边界逐条实测一遍:每条给出真实报错、失效原因,以及托管环境下的替代做法。 实例是一台 SQL Server 2019 Enterprise(RTM-CU32-GDR,15.0.4455.2)托管实例, 4 个用户库 + 一个服务商专用管理库,最大的业务库约 48 GB。

# 一、先确认自己站在哪一侧

上来先跑这一条,它决定了后面所有判断:

SELECT IS_SRVROLEMEMBER('sysadmin') AS IsSysAdmin;
1

托管实例上这里返回 0。不用怀疑账号配错了——这是设计如此。试图自己加进去会得到:

Msg 15151, Level 16, State 1
Cannot alter the server role 'sysadmin', because it does not exist or you do not have permission.
1
2

报错文本里的 "does not exist" 有迷惑性:sysadmin 当然存在,只是你连它的元数据都看不到, SQL Server 对无权限对象一律报「不存在或无权限」。看到这条不要去排查角色是否被删,直接接受边界。

顺带一个容易误判的点:SERVERPROPERTY('MachineName') 会返回一个云厂商自动生成的主机名 (形如 EC2AMAZ-XXXXXXX)。这台机器你永远登不上去,这个名字只有一个用途——确认发生过实例替换。

# 二、逐条失效清单

下面每条都是实测跑出来的报错,不是文档摘抄。

# 1. xp_cmdshell —— 彻底没有

EXEC xp_cmdshell 'dir';
1
Msg 229, Level 14, State 5
The EXECUTE permission was denied on the object 'xp_cmdshell',
database 'mssqlsystemresource', schema 'sys'.
1
2
3

注意报错落在 mssqlsystemresource 上——不是「没开启」,是执行权限在资源库层面就被拒, 所以 sp_configure 那条常见的启用命令也救不回来(而 sp_configure 本身也被封,见第 4 条)。

替代:任何依赖 xp_cmdshell 的运维脚本(调 robocopy、读目录、拉外部文件)都必须重写为 「数据库外部的调度器 + 只读 SQL」。把 shell 那一层挪到你自己的运维机上,SQL 只负责取数。

# 2. BACKUP / RESTORE DATABASE —— 被完全接管

BACKUP DATABASE [业务库] TO DISK = 'C:\temp\test.bak';
1
Msg 262, Level 14, State 1
BACKUP DATABASE permission denied in database '业务库'.
Msg 3013, Level 16, State 1
BACKUP DATABASE is terminating abnormally.
1
2
3
4

RESTORE 同理:

Msg 3110, Level 14, State 1
User does not have permission to RESTORE database '业务库'.
1
2

这是影响最大的一条。自建时代那套「备份策略 = 我自己写的 SQL Agent 作业」在托管上完全不成立: 备份是托管服务的职责,你只能通过控制台/API 设置保留期与备份窗口, 以及走服务商提供的原生备份存储过程(RDS 是 rdsadmin 库里那组 rds_* 过程) 把 .bak 导入/导出到对象存储。

替代:

  • 日常备份 → 交给托管快照 + PITR,不要自己造;
  • 需要把库搬到别处 → 走服务商的原生备份过程导出到对象存储,再在目标端导入;
  • 恢复演练必须换套路:托管环境下「验证备份可用」等于「实际发起一次时间点还原到新实例」, 没有办法在原实例上 RESTORE VERIFYONLY。这一步很多团队迁上云后就悄悄不做了,是真实风险。

# 3. DBCC 的缓存类命令 —— 拒绝

DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
1
2
Msg 2571, Level 14, State 1
User '<账号>' does not have permission to run DBCC DROPCLEANBUFFERS.
1
2

两条都是 Msg 2571。这直接影响的是性能测试方法论:自建时代「清缓存 → 跑 SQL → 看冷启动耗时」 这套对比手段在托管上做不了。

替代:改用 SET STATISTICS IO, TIME ON + 执行计划里的实际读页数做横向对比, 不追求冷缓存下的绝对耗时,只比较同一缓存状态下不同写法的逻辑读差异。逻辑读是与缓存无关的稳定指标, 本来就比墙钟时间更适合做优化对比。

注意不是所有 DBCC 都被封——只读的诊断类仍然可用:

DECLARE @T TABLE (TraceFlag INT, Status INT, Global INT, [Session] INT);
INSERT INTO @T EXEC('DBCC TRACESTATUS(-1)');
SELECT * FROM @T;
1
2
3

这条能正常返回。判断规律很简单:只读的 DBCC 基本都在,会改变实例状态的一律没有。

# 4. sp_configure —— 读得到,改不了

EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
1
2
Msg 15247, Level 16, State 1
User does not have permission to perform this action.
Msg 5812, Level 14, State 1
You do not have permission to run the RECONFIGURE statement.
1
2
3
4

但不带参数的 EXEC sp_configure; 是可以跑的——你能看到全部实例级配置的当前值, 只是不能改。这个「可读不可写」的组合在托管环境里非常典型,值得单独记住: 巡检脚本里所有读配置的部分照常工作,所有改配置的部分要挪到控制台的参数组里去。

替代:实例级参数(max degree of parallelism、cost threshold、max server memory……) 统统改走托管服务的参数组,改完通常需要一次重启或等待动态生效——这也意味着 「临时调一下 MAXDOP 试试效果」这种操作在生产上成本变高了,要提前规划变更窗口。

# 5. SQL Server Agent —— 看不到自己的作业

EXEC msdb.dbo.sp_help_job;
1
Msg 229, Level 14, State 5
The EXECUTE permission was denied on the object 'sp_help_job',
database 'msdb', schema 'dbo'.
1
2
3

Agent 在托管实例上是被服务商用来跑内部维护任务的,你通常拿到的是一个受限子集 (能建作业但看不到全局),具体范围因服务商与版本而异——上线前一定要实测一遍,别照抄文档。

替代:把定时任务移出数据库。用外部调度器(cron / 托管调度服务 / 你自己的运维平台) 连上来执行 SQL,好处是作业定义、日志、告警都在你自己的可观测体系里, 不用再为「作业失败了但没人知道」写一套 Agent 通知。

# 6. 文件系统 —— 路径可见,内容不可及

SELECT physical_name FROM sys.database_files;
1

这条能跑,会返回形如 D:\rdsdbdata\DATA\<库名>.mdf 的真实路径。 但这只是元数据——没有 xp_cmdshell、没有 OPENROWSET(BULK) 的本地路径权限, 你看得到路径,碰不到文件。

这个组合会造成一个具体的误判:看到 D:\rdsdbdata\ 就以为能按自建思路规划文件布局。 实际上数据文件放在哪个盘、怎么分卷,全部由托管层决定,你唯一能控制的是 文件组与文件的逻辑划分(下一节会用到)。

# 7. 边界的另一半:rds_* 那组过程

被拿走的能力,服务商会以存储过程的形式还回来一小部分。在 RDS 上它们分散在 rdsadmin 与 msdb 里,用这条列出来:

SELECT name FROM sys.procedures WHERE name LIKE 'rds[_]%' ORDER BY name;
1

覆盖的是「必须有但又不能给你 sysadmin」的那几件事:原生备份还原、库的删除与重命名、 排序规则修改、日志文件收缩等。迁移前把这份清单和你现有的运维脚本对一遍, 每条被封的操作要么在这里找到对应过程,要么就得改架构。

# 三、还能做的部分:托管环境下的 DMV 巡检

边界看完了,反过来说好消息:DMV 几乎全部可用。托管收走的是「改」,不是「看」。 日常巡检的主力——慢查询、等待、阻塞、索引、容量——一条都没少。

下面三块是我实际在跑的,附上托管环境特有的判读陷阱。

# 1. 容量水位:要按文件看,不要按库看

SELECT
    name                AS [FileName],
    type_desc           AS [Type],
    CAST(size * 8.0 / 1024 / 1024 AS DECIMAL(10,2))                          AS [SizeGB],
    CAST(FILEPROPERTY(name, 'SpaceUsed') * 8.0 / 1024 / 1024 AS DECIMAL(10,2)) AS [UsedGB],
    CAST((size - FILEPROPERTY(name, 'SpaceUsed')) * 8.0 / 1024 / 1024 AS DECIMAL(10,3)) AS [FreeGB],
    CASE WHEN max_size = 0  THEN 'NoGrow'
         WHEN max_size = -1 THEN 'Unlimited'
         ELSE CONVERT(VARCHAR(20), max_size * 8.0 / 1024 / 1024) + 'MB' END AS [MaxSize],
    growth              AS [Growth8KB],
    is_percent_growth   AS [IsPct]
FROM sys.database_files
ORDER BY type_desc, name;
1
2
3
4
5
6
7
8
9
10
11
12
13

一台真实业务库的输出(库名已脱敏):

FileName Type SizeGB UsedGB FreeGB MaxSize Growth(8KB页)
业务库_log LOG 8.76 0.03 8.73 2048MB 32768
业务库 ROWS 2.19 1.93 0.26 Unlimited 16384
业务库_Money03 ROWS 43.85 40.06 3.79 Unlimited 65536
业务库_Report01 ROWS 1.07 0.75 0.32 Unlimited 8192

这张表要看三件事,第三件是托管环境下最容易被忽略的:

  1. 单文件剩余空间,不是整库剩余。上面这个库整体看还很宽松,但 Money03 这个文件 已经用掉 91%,热点全在它身上——按库汇总的告警看不出来。
  2. Growth8KB 换算成实际大小:65536 × 8KB = 512MB,8192 × 8KB = 64MB。 增长步长过小会导致频繁自动增长(每次都是一次同步的文件扩展,期间写入会卡), 过大则单次扩展时间长。几十 GB 量级的热文件,512MB 是合理的;64MB 偏小。
  3. is_percent_growth 必须是 0。百分比增长在文件变大后会失控——一个 44GB 的文件按 10% 增长, 单次要扩 4.4GB。这条在自建时代也成立,但托管环境下你无法用瞬时 I/O 指标去印证扩展带来的抖动 (宿主机指标不归你),只能靠事前把它配对。

另外注意 tempdb:这台实例的 tempdb 有 223 GB,实际用量 0.06 GB。 托管服务通常按实例规格预分配一个很大的 tempdb,看到它「几乎全空」不是异常, 不要照搬自建时代「tempdb 使用率」那套告警阈值,会一直不触发、形同虚设。 真正该盯的是 tempdb 的瞬时增长与版本存储占用,不是稳态使用率。

# 2. 等待统计:托管实例的噪音更大

标准写法是 sys.dm_os_wait_stats 排序取 top,但必须过滤空闲等待,否则结论全错。 先看一个没过滤干净的真实输出:

wait_type wait_time (秒) waiting_tasks
SOS_WORK_DISPATCHER 93,790,486 402,317,317
HADR_FILESTREAM_IOMGR_IOCOMPLETION 1,342,875 2,680,068
HADR_CLUSAPI_CALL 1,342,871 5,364,516
DIRTY_PAGE_POLL 1,342,865 13,299,100
QDS_PERSIST_TASK_MAIN_LOOP_SLEEP 1,342,824 22,381
SP_SERVER_DIAGNOSTICS_SLEEP 1,342,803 1,881,340
CXPACKET 24,971 13,153,871
CXCONSUMER 19,668 8,812,995
BACKUPIO 5,196 2,586,306

前六行全是噪音,一条都不能用来做判断:

  • SOS_WORK_DISPATCHER 是后台工作线程池的空闲等待,数值大得离谱是正常的;
  • 中间四条的等待时长几乎完全相同(都是 1,342,8xx 秒)——这个「整齐」本身就是识别信号: 它们都等于实例的正常运行时长(约 15.5 天),说明这些线程从启动起就一直在睡, 从没被真正阻塞过。看到一组数值高度接近且约等于 uptime 的等待类型,直接全部划掉。

有意思的是那两条 HADR_*。这个实例并没有配置可用性组,为什么会有 Always On 相关的等待? 因为托管服务的多可用区高可用就是用 Always On 实现的,只是控制面在服务商手里。 这算是托管边界的一个侧写:底层机制还在,只是你既看不到也管不了它—— 既然管不了,就更没有理由把它算进你的等待分析里。

过滤之后,真正有信息量的是最后三条:CXPACKET / CXCONSUMER 说明存在并行查询, 且 CXPACKET(协调线程等待)比 CXCONSUMER(消费者等待)高, 提示并行度分配可能不均——但在托管环境下你不能直接改 MAXDOP 去验证,得走参数组变更窗口(见第 4 条)。 BACKUPIO 的存在则印证了托管备份确实在按窗口跑。

一份可以直接用的过滤版本:

SELECT TOP 10
    wait_type,
    wait_time_ms / 1000.0 AS wait_time_sec,
    waiting_tasks_count,
    wait_time_ms / NULLIF(waiting_tasks_count, 0) AS avg_wait_ms
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
    'CLR_SEMAPHORE','LAZYWRITER_SLEEP','RESOURCE_QUEUE','SLEEP_TASK',
    'SLEEP_SYSTEMTASK','WAITFOR','LOGMGR_QUEUE','CHECKPOINT_QUEUE',
    'REQUEST_FOR_DEADLOCK_SEARCH','XE_TIMER_EVENT','BROKER_TO_FLUSH',
    'BROKER_TASK_STOP','CLR_MANUAL_EVENT','CLR_AUTO_EVENT',
    'DISPATCHER_QUEUE_SEMAPHORE','FT_IFTS_SCHEDULER_IDLE_WAIT',
    'XE_DISPATCHER_WAIT','XE_DISPATCHER_JOIN',
    -- 下面这些是托管实例上必须补的
    'SOS_WORK_DISPATCHER','DIRTY_PAGE_POLL','SP_SERVER_DIAGNOSTICS_SLEEP',
    'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','QDS_ASYNC_QUEUE',
    'HADR_FILESTREAM_IOMGR_IOCOMPLETION','HADR_CLUSAPI_CALL',
    'HADR_WORK_QUEUE','HADR_TIMER_TASK','HADR_LOGCAPTURE_WAIT'
)
ORDER BY wait_time_ms DESC;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

还有一个前提容易忘:wait_stats 是自实例启动以来的累计值,托管实例会因为维护窗口、 版本升级、可用区切换而重启,计数随之清零。所以做趋势对比必须自己落一份快照按差值比, 直接看绝对值只能得到「从上次重启到现在」这个含糊的口径。

# 3. 慢查询:按累计耗时排,不要按单次

SELECT TOP 10
    SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
        ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
          ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS query_text,
    qs.execution_count,
    CAST(qs.total_elapsed_time / 1000000.0 AS DECIMAL(12,2)) AS total_elapsed_sec,
    CAST(qs.last_elapsed_time  / 1000000.0 AS DECIMAL(12,2)) AS last_elapsed_sec,
    qs.total_logical_reads
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_elapsed_time DESC;
1
2
3
4
5
6
7
8
9
10
11

真实输出的形态(SQL 文本已截断脱敏):

执行次数 累计耗时(秒) 单次耗时(秒) 累计逻辑读 形态
11,832,758 27,283 0.01 21.9 亿 带临时表的 CTE 查询
24,097,069 4,886 0.00 39.0 亿 ORM 生成的单表按条件取列
23,944,371 4,803 0.00 38.8 亿 同一张表的 COUNT(*)
8,242 2,901 0.06 1,405 万 报表聚合
44,829 2,536 0.05 3,046 万 递归 CTE 统计注册数

这张表最该讲的一点:排在第一梯队的全是单次 0.00~0.01 秒的查询。 按「单次执行超过 N 秒」的传统慢查询阈值去抓,这些一条都抓不到, 但它们贡献了绝大部分的累计耗时和几十亿次的逻辑读——真正压垮实例的是高频小查询,不是偶发大查询。

第 2、3 行尤其典型:同一张表,一条取数据一条 COUNT(*), 执行次数都是 2400 万级、逻辑读都在 39 亿级——这是典型的 ORM 分页写法 (先 count 总数再取当页),两条查询扫的是同一份数据。这类问题在慢查询日志里永远看不见, 只能靠 dm_exec_query_stats 按累计量排出来。

判读顺序建议:先按 total_elapsed_time 排一遍找总量大户, 再用 total_logical_reads / execution_count 算单次平均读页数找「本可以走索引却在扫表」的, 最后才看 last_elapsed_time 确认现在还在不在跑。只看其中一个维度都会漏。

同样注意这份数据的生命周期:dm_exec_query_stats 依附于计划缓存, 计划被逐出、重编译或实例重启,统计就没了。托管环境下你还不能用 DBCC FREEPROCCACHE 主动控制这个时机(见第 3 条),所以要么接受口径模糊,要么自己定时落快照。

# 四、一张迁移前的自查表

把上面的边界压缩成可执行的检查项,迁移前逐条过:

检查项 自建时的做法 托管上的结论
备份策略 自己写 Agent 作业 + 本地磁盘 必须重做:托管快照 + PITR;恢复演练改为「还原到新实例」
依赖 xp_cmdshell 的脚本 直接调 shell 必须重写:shell 移到库外,SQL 只取数
实例参数调整 sp_configure + RECONFIGURE 改走参数组,需要变更窗口;sp_configure 仍可只读查看
定时任务 SQL Server Agent 移到外部调度器,顺带把告警接进自己的监控
性能对比测试 清缓存跑冷启动 改用逻辑读做对比指标
文件布局规划 自己分盘 只能控制文件组的逻辑划分,物理布局归托管层
容量告警 按库剩余空间 改为按文件;tempdb 稳态使用率阈值作废
等待统计基线 累计值直接看 自己落快照按差值比;过滤列表要补托管特有的噪音项
慢查询阈值 单次耗时 > N 秒 补上按累计耗时排的维度,否则漏掉高频小查询

# 小结

托管数据库的核心变化不是「少了几个命令」,而是运维的落点从实例内部挪到了实例外部: 调度移到外部调度器、备份移到服务商的控制面、参数变更移到变更窗口、 性能对比从依赖实例状态改为依赖与状态无关的指标。

而剩下留在你手里的部分——DMV 全家桶——反而变得更重要了: 在一个碰不到宿主机、看不到磁盘、改不了缓存的环境里,DMV 几乎是你唯一的观测入口。 把巡检脚本按上面那张表核一遍,比纠结失去的那些权限有用得多。

站内相关:SQL Server 2019 for Linux 安装与配置 Always On 高可用集群实战教程(自建侧的对照)

#SQL Server#云数据库#巡检
上次更新: 8/28/2026

← 当监控说没事而 DMV 说有事——N9E 与 SQL Server 指标交叉验证实战

最近更新
01
当监控说没事而 DMV 说有事——N9E 与 SQL Server 指标交叉验证实战 原创
08-28
02
TiKV 节点 CPU 周期性打满,进程却只占 4%:一次热点 Region 的逆向排查 原创
08-28
03
TiKV 运行满 2 年会 panic:795 天单调时钟溢出 bug(tikv#11940)复盘与预警建设 原创
08-27
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式