SQL Server 管理系列(第七期):故障排查实战——从报警到解决的完整流程

0 阅读11分钟

SQL Server 管理系列(第七期):故障排查实战——从报警到解决的完整流程

前六期我们系统学习了安装配置、安全加固、备份恢复、高可用、自动化运维和性能监控。这一期,作为管理系列的收官之作,我们将把这些知识融会贯通,通过真实故障场景,走完故障排查的完整流程。从收到报警到定位根因,从应急处理到永久修复,再到事后复盘——让你面对生产故障时不再慌乱。

一、故障排查方法论

1.1 故障响应五步法

┌─────────────────────────────────────────────────────────────┐
│                    故障响应流程                              │
│                                                             │
│  第1步:收到报警 ──→ 确认影响范围                            │
│        │                                                    │
│        ▼                                                    │
│  第2步:快速止血 ──→ 应急处理(Kill会话、重启服务等)         │
│        │                                                    │
│        ▼                                                    │
│  第3步:定位根因 ──→ 分析日志、DMV、执行计划                  │
│        │                                                    │
│        ▼                                                    │
│  第4步:永久修复 ──→ 代码变更、配置调整、架构优化             │
│        │                                                    │
│        ▼                                                    │
│  第5步:事后复盘 ──→ 故障报告、改进措施、监控增强             │
└─────────────────────────────────────────────────────────────┘

1.2 DBA 应急工具箱

工具用途命令
SP_WHO2查看当前会话SP_WHO2SP_WHO
SP_WHOISACTIVE高级会话分析(需安装)EXEC dbo.sp_WhoIsActive
DBCC INPUTBUFFER查看会话最后执行的SQLDBCC INPUTBUFFER(SPID)
KILL终止会话KILL SPID
ALTER DATABASE设置数据库状态ALTER DATABASE ... SET SINGLE_USER
DBCC CHECKDB数据库完整性检查DBCC CHECKDB(DBName)
DBCC SQLPERF日志空间使用DBCC SQLPERF(LOGSPACE)

二、故障场景一:CPU 飙升到 100%

2.1 症状识别

报警信息:
- CPU 使用率持续 95%+
- 用户反馈查询超时
- 新增查询大量出现

2.2 应急处理(5分钟内)

-- 1. 立即查看当前高 CPU 的会话
SELECT 
    r.session_id,
    s.login_name,
    r.cpu_time,
    r.total_elapsed_time,
    r.status,
    r.command,
    SUBSTRING(st.text, (r.statement_start_offset/2)+1,
        ((CASE r.statement_end_offset 
            WHEN -1 THEN DATALENGTH(st.text)
            ELSE r.statement_end_offset
        END - r.statement_start_offset)/2) + 1) AS query_text
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
WHERE r.status NOT IN ('background', 'sleeping')
  AND r.cpu_time > 0
ORDER BY r.cpu_time DESC;

-- 2. 找到 CPU 消耗最高的查询(历史累计)
SELECT TOP 5
    qs.total_worker_time / 1000000 AS total_cpu_sec,
    qs.execution_count,
    qs.total_worker_time / qs.execution_count / 1000000 AS avg_cpu_sec,
    SUBSTRING(st.text, (qs.statement_start_offset/2)+1, 500) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY qs.total_worker_time DESC;

-- 3. 应急:终止异常会话(确认后执行)
KILL <session_id>;

2.3 根因定位

-- 检查是否有大量并行查询
SELECT 
    session_id,
    scheduler_id,
    status,
    command,
    wait_type,
    wait_time
FROM sys.dm_exec_requests
WHERE command LIKE '%SELECT%'
  AND session_id IN (
      SELECT session_id FROM sys.dm_os_tasks 
      GROUP BY session_id HAVING COUNT(*) > 8
  );

-- 检查参数嗅探问题
SELECT 
    qs.execution_count,
    qs.total_worker_time / qs.execution_count / 1000000 AS avg_cpu_sec,
    qs.total_elapsed_time / qs.execution_count / 1000000 AS avg_elapsed_sec,
    qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
    SUBSTRING(st.text, (qs.statement_start_offset/2)+1, 500) AS query_text,
    qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
OUTER APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qs.execution_count > 10
  AND qs.total_worker_time / qs.execution_count > 1000000  -- 平均超过1秒CPU
ORDER BY qs.total_worker_time / qs.execution_count DESC;

2.4 解决方案

原因解决方案
缺少索引创建覆盖索引
统计信息过期UPDATE STATISTICS
参数嗅探使用 OPTION (RECOMPILE) 或本地变量
并行度过高调整 MAXDOP
循环/游标改为集合操作
-- 针对参数嗅探的快速修复
EXEC sp_recompile 'YourStoredProcedure';
-- 或
ALTER DATABASE YourDB SET PARAMETERIZATION FORCED;

三、故障场景二:数据库阻塞(应用卡死)

3.1 症状识别

报警信息:
- 大量超时错误
- 应用程序无响应
- 用户反馈"转圈"

3.2 应急处理

-- 1. 查看阻塞链
SELECT 
    blocked.session_id AS blocked_session,
    blocking.session_id AS blocking_session,
    blocked.wait_time / 1000 AS wait_sec,
    blocked.wait_type,
    blocking.last_request_start_time,
    blocking.last_request_end_time,
    (SELECT TEXT FROM sys.dm_exec_sql_text(blocked.sql_handle)) AS blocked_sql,
    (SELECT TEXT FROM sys.dm_exec_sql_text(blocking.sql_handle)) AS blocking_sql
FROM sys.dm_exec_requests blocked
JOIN sys.dm_exec_requests blocking 
    ON blocked.blocking_session_id = blocking.session_id
WHERE blocked.blocking_session_id > 0;

-- 2. 查找阻塞源头(最顶层的阻塞会话)
SELECT 
    session_id,
    blocking_session_id,
    wait_time,
    wait_type,
    last_wait_type,
    text
FROM sys.dm_exec_requests
CROSS APPLY sys.dm_exec_sql_text(sql_handle)
WHERE session_id IN (
    SELECT blocking_session_id 
    FROM sys.dm_exec_requests 
    WHERE blocking_session_id > 0
    EXCEPT 
    SELECT session_id 
    FROM sys.dm_exec_requests 
    WHERE blocking_session_id > 0
);

-- 3. 查看锁详细信息
SELECT 
    request_session_id,
    resource_type,
    resource_database_id,
    resource_description,
    request_mode,
    request_status,
    request_owner_type
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID()
ORDER BY request_session_id, resource_type;

-- 4. 应急:终止阻塞源头(确认后执行)
KILL <blocking_session_id>;

3.3 根因定位

-- 查找长期未提交的事务
SELECT 
    st.session_id,
    st.transaction_id,
    at.transaction_begin_time,
    DATEDIFF(MINUTE, at.transaction_begin_time, GETDATE()) AS duration_min,
    at.transaction_state,
    at.transaction_type,
    s.login_name,
    s.program_name,
    s.host_name
FROM sys.dm_tran_session_transactions st
JOIN sys.dm_tran_active_transactions at 
    ON st.transaction_id = at.transaction_id
JOIN sys.dm_exec_sessions s 
    ON st.session_id = s.session_id
WHERE at.transaction_begin_time < DATEADD(MINUTE, -5, GETDATE())
ORDER BY at.transaction_begin_time;

-- 查找锁升级(行锁→表锁)
SELECT 
    object_name(p.object_id) AS TableName,
    p.index_id,
    lock_count = COUNT(*)
FROM sys.dm_tran_locks l
JOIN sys.partitions p ON l.resource_associated_entity_id = p.hobt_id
WHERE l.resource_type = 'OBJECT'
GROUP BY p.object_id, p.index_id
ORDER BY lock_count DESC;

3.4 解决方案

原因解决方案
应用程序未提交事务修复代码,添加超时机制
事务过长拆分事务,减少锁定范围
缺失索引添加索引,减少锁定的行数
锁升级调整锁升级阈值,或优化查询
死锁统一资源访问顺序,应用重试
-- 设置锁超时(应用连接字符串)
-- 或在查询中设置
SET LOCK_TIMEOUT 30000;  -- 30秒后自动超时

-- 启用 RCSI 减少读写阻塞
ALTER DATABASE YourDB SET READ_COMMITTED_SNAPSHOT ON;

四、故障场景三:磁盘空间爆满

4.1 症状识别

报警信息:
- 磁盘剩余空间 < 10%
- INSERT/UPDATE 失败(日志满)
- 备份失败

4.2 应急处理

-- 1. 查看磁盘空间
EXEC xp_fixeddrives;

-- 2. 查看数据库文件大小
SELECT 
    DB_NAME(database_id) AS DBName,
    name AS FileName,
    type_desc,
    size/128.0 AS CurrentSizeMB,
    size/128.0 - FILEPROPERTY(name, 'SpaceUsed')/128.0 AS FreeMB,
    growth/128.0 AS GrowthMB,
    is_percent_growth
FROM sys.master_files
WHERE database_id = DB_ID()
ORDER BY CurrentSizeMB DESC;

-- 3. 查看日志空间使用
DBCC SQLPERF(LOGSPACE);

-- 4. 查看最大表占用的空间
SELECT TOP 10
    OBJECT_NAME(p.object_id) AS TableName,
    SUM(a.total_pages) * 8 / 1024 AS TotalMB,
    SUM(a.used_pages) * 8 / 1024 AS UsedMB,
    SUM(a.data_pages) * 8 / 1024 AS DataMB
FROM sys.partitions p
JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE p.object_id > 100
GROUP BY p.object_id
ORDER BY TotalMB DESC;

4.3 根因定位

-- 查找日志满的原因
-- 原因1:数据库在 FULL 恢复模式但从未备份日志
SELECT name, recovery_model_desc, log_reuse_wait_desc
FROM sys.databases
WHERE name = DB_NAME();
-- log_reuse_wait_desc = 'LOG_BACKUP' 表示需要备份日志

-- 原因2:长期未提交的事务阻止日志截断
SELECT 
    st.session_id,
    at.transaction_begin_time,
    DATEDIFF(MINUTE, at.transaction_begin_time, GETDATE()) AS duration_min,
    at.transaction_state,
    s.login_name,
    s.program_name
FROM sys.dm_tran_session_transactions st
JOIN sys.dm_tran_active_transactions at ON st.transaction_id = at.transaction_id
JOIN sys.dm_exec_sessions s ON st.session_id = s.session_id
WHERE at.transaction_state = 2  -- 活动事务
ORDER BY at.transaction_begin_time;

-- 原因3:数据文件增长设置不当(百分比增长)
SELECT 
    name,
    type_desc,
    growth,
    is_percent_growth,
    CASE WHEN is_percent_growth = 1 THEN '⚠️ 危险:百分比增长' ELSE '✅ 正常' END AS Status
FROM sys.database_files;

4.4 解决方案

-- 应急:清理空间
-- 1. 备份日志(如果是 FULL 模式)
BACKUP LOG YourDB TO DISK = 'G:\Backup\YourDB_Log.trn';

-- 2. 或切换到 SIMPLE 模式(开发/测试环境)
ALTER DATABASE YourDB SET RECOVERY SIMPLE;

-- 3. 收缩日志文件
DBCC SHRINKFILE (YourDB_Log, 1024);  -- 收缩到 1GB

-- 4. 收缩数据文件(谨慎!会导致碎片)
DBCC SHRINKFILE (YourDB_Data, 51200);  -- 收缩到 50GB

-- 5. 删除旧备份文件
-- 使用 PowerShell 或 CmdExec

-- 永久修复
-- 1. 修改文件增长为固定值
ALTER DATABASE YourDB MODIFY FILE 
(NAME = YourDB_Data, FILEGROWTH = 1024MB);

-- 2. 建立定期日志备份作业(每小时)

-- 3. 清理历史数据
DELETE FROM LogTable WHERE LogDate < DATEADD(month, -3, GETDATE());

-- 4. 使用分区表快速归档

五、故障场景四:Always On 同步延迟

5.1 症状识别

报警信息:
- 辅助副本数据延迟
- 只读查询返回过期数据
- 故障转移后数据丢失

5.2 应急处理

-- 1. 查看 AG 同步状态
SELECT 
    ag.name AS AGName,
    ar.replica_server_name,
    rs.role_desc,
    rs.synchronization_health_desc,
    rs.connected_state_desc,
    rs.last_connect_error_number,
    rs.last_connect_error_description
FROM sys.availability_groups ag
JOIN sys.dm_hadr_availability_replica_states rs 
    ON ag.group_id = rs.group_id
JOIN sys.availability_replicas ar 
    ON rs.replica_id = ar.replica_id;

-- 2. 查看数据库同步延迟
SELECT 
    DB_NAME(database_id) AS DBName,
    synchronization_state_desc,
    synchronization_health_desc,
    last_commit_time,
    DATEDIFF(SECOND, last_commit_time, GETDATE()) AS seconds_behind,
    log_send_queue_size,
    log_send_rate,
    redo_queue_size,
    redo_rate
FROM sys.dm_hadr_database_replica_states
WHERE is_primary_replica = 0;

-- 3. 查看发送和重做队列
SELECT 
    DB_NAME(database_id) AS DBName,
    log_send_queue_size / 1024 AS log_send_queue_MB,
    redo_queue_size / 1024 AS redo_queue_MB,
    log_send_rate / 1024 AS log_send_rate_KBps,
    redo_rate / 1024 AS redo_rate_KBps
FROM sys.dm_hadr_database_replica_states
WHERE is_primary_replica = 0;

5.3 根因定位

-- 检查网络延迟
SELECT 
    ar.replica_server_name,
    ar.endpoint_url,
    rs.log_send_queue_size,
    rs.log_send_rate
FROM sys.dm_hadr_availability_replica_states rs
JOIN sys.availability_replicas ar ON rs.replica_id = ar.replica_id
WHERE rs.role_desc = 'PRIMARY';

-- 检查辅助副本重做瓶颈
SELECT 
    session_id,
    command,
    status,
    wait_type,
    wait_time,
    blocking_session_id
FROM sys.dm_exec_requests
WHERE database_id = DB_ID('YourDB')
  AND command LIKE '%REDO%';

-- 检查网络带宽
-- 使用性能计数器:Network Interface\Bytes Total/sec

5.4 解决方案

原因解决方案
网络带宽不足压缩日志流、升级网络、异步模式
辅助副本 I/O 慢将辅助副本放在更快的磁盘上
重做线程瓶颈增加重做线程数(2016+)
大事务拆分大事务为小批量
同步模式评估是否可改为异步
-- 修改为异步提交(减轻网络压力)
ALTER AVAILABILITY GROUP [AG_YourDB]
MODIFY REPLICA ON N'SecondaryNode'
WITH (AVAILABILITY_MODE = ASYNCHRONOUS_COMMIT);

-- 启用日志流压缩(SQL Server 2016+)
ALTER AVAILABILITY GROUP [AG_YourDB]
MODIFY REPLICA ON N'SecondaryNode'
WITH (COMPRESSION = ENABLED);

六、故障排查决策树

收到报警
    │
    ├─ CPU 飙升 (>80%)
    │   ├─ 查询高CPU会话 → 找到异常查询
    │   ├─ 检查执行计划 → 缺少索引/统计信息过期/参数嗅探
    │   └─ 应急:KILL会话 / 添加索引
    │
    ├─ 数据库阻塞
    │   ├─ 查找阻塞链 → 定位源头会话
    │   ├─ 检查事务 → 长事务/未提交事务
    │   ├─ 检查锁 → 锁升级/死锁
    │   └─ 应急:KILL源头 / 启用RCSI
    │
    ├─ 磁盘空间不足
    │   ├─ 查看各文件大小 → 数据文件/日志文件
    │   ├─ 检查日志 → 未备份日志/长事务阻止截断
    │   ├─ 检查数据增长 → 历史数据积累
    │   └─ 应急:备份日志/收缩文件/删除旧数据
    │
    ├─ AG 同步延迟
    │   ├─ 查看延迟秒数 → 发送队列/重做队列
    │   ├─ 检查网络 → 带宽/延迟
    │   ├─ 检查辅助副本 → I/O瓶颈
    │   └─ 应急:改为异步/压缩传输
    │
    └─ 数据库损坏
        ├─ DBCC CHECKDB → 确定损坏范围
        ├─ 检查备份 → 找到最近的完好备份
        └─ 应急:从备份恢复 / 尝试修复

七、应急预案模板

7.1 故障记录表

CREATE TABLE DBA_IncidentLog (
    IncidentID INT IDENTITY PRIMARY KEY,
    OccurrenceTime DATETIME DEFAULT GETDATE(),
    ResolvedTime DATETIME,
    Severity VARCHAR(20),  -- 'Critical', 'High', 'Medium', 'Low'
    Category VARCHAR(50),   -- 'CPU', 'Blocking', 'Disk', 'AG', 'Corruption'
    Symptom NVARCHAR(500),
    RootCause NVARCHAR(1000),
    Solution NVARCHAR(2000),
    ImpactDuration INT,  -- 分钟
    ImpactScope NVARCHAR(500),
    PreventiveAction NVARCHAR(1000),
    ReportedBy VARCHAR(100),
    ResolvedBy VARCHAR(100)
);

-- 插入故障记录
INSERT INTO DBA_IncidentLog (
    OccurrenceTime, Severity, Category, Symptom, 
    RootCause, Solution, ImpactDuration, PreventiveAction,
    ReportedBy, ResolvedBy
)
VALUES (
    '2024-01-15 14:23:00', 'Critical', 'Blocking',
    '订单系统大面积超时,用户无法下单',
    '应用程序开启事务后未提交,阻塞了所有订单插入',
    'KILL阻塞会话,修复应用程序事务逻辑',
    15,
    '添加应用程序连接池事务超时设置,监控长事务',
    'MonitoringSystem', 'DBA_OnCall'
);

7.2 快速响应脚本

-- 创建一键诊断存储过程
CREATE PROC dbo.sp_EmergencyDiagnosis
AS
BEGIN
    SET NOCOUNT ON;
    
    PRINT '========== 紧急诊断报告 ==========';
    PRINT '生成时间: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT '';
    
    -- 1. 高CPU查询
    PRINT '>>> Top 3 当前高CPU会话:';
    SELECT TOP 3
        session_id,
        cpu_time,
        total_elapsed_time,
        SUBSTRING(st.text, (r.statement_start_offset/2)+1, 200) AS query
    FROM sys.dm_exec_requests r
    CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
    WHERE r.status NOT IN ('background', 'sleeping')
    ORDER BY r.cpu_time DESC;
    
    -- 2. 阻塞链
    PRINT '>>> 阻塞链信息:';
    SELECT 
        blocked.session_id AS blocked,
        blocking.session_id AS blocking,
        blocked.wait_time/1000 AS wait_sec,
        SUBSTRING(blk.text, 1, 200) AS blocking_query
    FROM sys.dm_exec_requests blocked
    JOIN sys.dm_exec_requests blocking 
        ON blocked.blocking_session_id = blocking.session_id
    CROSS APPLY sys.dm_exec_sql_text(blocking.sql_handle) blk
    WHERE blocked.blocking_session_id > 0;
    
    -- 3. 磁盘空间
    PRINT '>>> 磁盘空间:';
    EXEC xp_fixeddrives;
    
    -- 4. 日志空间
    PRINT '>>> 日志空间使用:';
    DBCC SQLPERF(LOGSPACE);
    
    -- 5. 内存压力
    PRINT '>>> 内存压力指标:';
    SELECT 
        (SELECT cntr_value FROM sys.dm_os_performance_counters 
         WHERE counter_name = 'Page life expectancy') AS PLE,
        (SELECT cntr_value FROM sys.dm_os_performance_counters 
         WHERE counter_name = 'Lazy writes/sec') AS LazyWrites;
    
    -- 6. AG 状态(如果有)
    IF EXISTS (SELECT 1 FROM sys.availability_groups)
    BEGIN
        PRINT '>>> Always On 状态:';
        SELECT 
            ar.replica_server_name,
            rs.role_desc,
            rs.synchronization_health_desc,
            DATEDIFF(SECOND, drs.last_commit_time, GETDATE()) AS seconds_behind
        FROM sys.availability_groups ag
        JOIN sys.dm_hadr_availability_replica_states rs 
            ON ag.group_id = rs.group_id
        JOIN sys.availability_replicas ar 
            ON rs.replica_id = ar.replica_id
        LEFT JOIN sys.dm_hadr_database_replica_states drs 
            ON rs.replica_id = drs.replica_id;
    END
END

八、核心总结

故障类型应急操作根因定位永久修复
CPU 飙升KILL 异常会话查询统计/执行计划添加索引/参数嗅探
数据库阻塞KILL 源头会话锁/事务分析优化事务/启用RCSI
磁盘爆满备份日志/收缩文件/日志分析调整增长/定期清理
AG 延迟改异步/压缩网络/重做分析优化网络/拆分事务

一句话记住本期内容(管理系列收官)

故障排查五步法——先止血再定位,从现象到根因,应急方案保恢复,永久修复防复发,事后复盘建体系。

管理系列回顾

期数主题核心技能
1安装配置磁盘分离、内存配置、tempdb优化
2安全加固最小权限、动态脱敏、TDE
3备份恢复备份链、时间点恢复、恢复演练
4高可用性Always On AG、FCI、日志传送
5自动化运维SQL Agent、多服务器管理
6性能监控DMV、等待统计、性能计数器
7故障排查应急响应、根因分析、复盘改进

最终寄语

DBA 的核心价值不是"数据库不宕机",而是"数据库宕机后能多快恢复"。

备份是你的最后防线,监控是你的早期预警,自动化是你的效率工具,而经验——来自每一次故障排查的复盘——是你最宝贵的财富。


📌 本文所有脚本适用于 SQL Server 2016-2022。建议在测试环境验证后应用到生产环境。

《SQL Server 管理系列》到此完结。感谢你的阅读,愿你的数据库永远稳定运行,即使出问题也能从容应对。