Windows 服务启动与恢复排查
Windows 服务启动与恢复排查
Windows 服务启动失败时,先看它走到了哪一步:服务账号有没有完成登录,进程有没有接入 SCM,启动处理是否返回,还是已经运行后又退出。1053、1069 和异常恢复配置对应不同阶段,不能都用“延长超时”解决。
下面先在 Windows Server 2022 检查服务登录与控制入口,再通过 Windows 10 LTSC 2021 的启动过程区分连接和初始化,最后验证 Server 2022 上的崩溃恢复。各示例使用独立测试服务,处理业务服务时先确认对应的失败阶段。
1. 错误码与日志
| 现象 | 优先检查 | 不能直接推出 |
|---|---|---|
| 1069,服务登录失败 | StartName、保存的凭据、服务登录允许/拒绝权、4625 | 一定只是密码错误 |
| 1053,未及时响应 | 服务入口、接入 SCM 的时间、状态回报、应用阶段日志 | 程序完全没执行,或所有初始化只能 30 秒 |
| 1067,进程意外终止 | 应用异常、退出码、转储、初始化日志 | 一定发生在同一初始化阶段 |
| 1068,依赖失败 | 配置的依赖及它们的状态 | 应该先改当前服务密码 |
| 运行后崩溃不重启 | 失败动作、实际退出路径、恢复标志、7031/7034 | 三次 restart 配置会自动限制为三次 |
在管理员 Windows PowerShell 5.1 使用 sc.exe 全名,避免 sc 别名指向 Set-Content。先取服务内部名称与配置:
$n = Read-Host 'Service name (not display name)'
# This example accepts simple internal names to keep the WMI filter exact.
if ($n -notmatch '^[A-Za-z0-9_.-]+$') { throw 'Use a confirmed simple service name.' }
Get-CimInstance Win32_Service -Filter "Name='$n'" |
Select-Object Name,State,ProcessId,PathName,StartName,StartMode,ExitCode
sc.exe qc $n
sc.exe qfailure $n
sc.exe qfailureflag $n
$since = (Get-Date).AddMinutes(-15)
Get-WinEvent -FilterHashtable @{
LogName='System';ProviderName='Service Control Manager';StartTime=$since
} | Where-Object { $_.ToXml().Contains($n) } |
Select-Object TimeCreated,Id,RecordId,Message按时间、服务名与 RecordId 关联同一次启动。事件号给排查方向,最后还要结合进程与业务输出;7009 说明 SCM 等待连接超时,但不能据此决定应用日志不用看。
2. 服务登录 1069
Server 2022 的 NAD0720261009Svc 使用本地普通账户。程序、目录权限保持一致,只改服务保存的密码,再只改 SeServiceLogonRight,得到:
| 条件 | 启动 | SCM | 4625 Status / SubStatus |
|---|---|---|---|
| 正确密码、允许服务登录 | 成功 | 正常转换 | 无本次失败 |
| 服务保存密码错误 | 1069 | 7038、7000 | 0xc000006d / 0xc000006a |
| 恢复正确密码 | 成功 | 正常转换 | 无本次失败 |
| 密码正确,移除该用户服务登录权 | 1069 | 7041、7000 | 0xc000015b / 0x0 |
| 恢复准确用户权利 | 成功 | 正常转换 | 无本次失败 |
失败记录的 LogonType=5,ProcessName 为 services.exe。第一组进一步状态指向密码不匹配,第二组指向请求的登录类型未获允许;两者都发生在应用业务开始之前。4625 字段与登录类型
按准确账号读取原始字段:
$account='NAD0720261009User'
Get-WinEvent -FilterHashtable @{
LogName='Security'
Id=4625
StartTime=(Get-Date).AddMinutes(-10)
} | ForEach-Object {
$xml=[xml]$_.ToXml()
$f=@{}
foreach($d in $xml.Event.EventData.Data){$f[$d.Name]=$d.'#text'}
if($f.TargetUserName -eq $account -and $f.LogonType -eq '5'){
[pscustomobject]@{
TimeCreated=$_.TimeCreated
RecordId=$_.RecordId
TargetUserName=$f.TargetUserName
TargetDomainName=$f.TargetDomainName
Status=$f.Status
SubStatus=$f.SubStatus
LogonType=$f.LogonType
ProcessName=$f.ProcessName
}
}
}密码分支在 services.msc 的“登录”页更新这个服务保存的密码,或者使用交互凭据的 CIM Change。这里是在更新服务配置,不是再次修改账号密码:
$name='NAD0720261009Svc'
$svc=Get-CimInstance Win32_Service -Filter "Name='$name'"
$cred=Get-Credential -UserName $svc.StartName -Message 'Enter current service account password'
$result=Invoke-CimMethod -InputObject $svc -MethodName Change -Arguments @{
StartPassword=$cred.GetNetworkCredential().Password
}
if($result.ReturnValue -ne 0){throw "Service Change failed: $($result.ReturnValue)"}
Start-Service $name
(Get-Service $name).WaitForStatus('Running',[TimeSpan]::FromSeconds(15))
Get-CimInstance Win32_Service -Filter "Name='$name'" |
Select-Object Name,State,StartName,ProcessId该接口需要在内存中取得明文密码传给本机 SCM,示例避免把密码写进命令历史,不代表秘密从未在进程内出现。更改前确认服务身份,CIM 返回 0 后仍需实际启动验收;不要为了绕开 1069 改为 LocalSystem。
权利分支导出并检查允许和拒绝列表,保留原有条目:
Get-LocalUser -Name 'NAD0720261009User' |
Select-Object Name,Enabled,PasswordExpires,PasswordLastSet,SID
secedit.exe /export /cfg "$env:ProgramData\service-rights-current.inf" /areas USER_RIGHTS
Select-String -Path "$env:ProgramData\service-rights-current.inf" -Pattern '^Se(ServiceLogonRight|DenyServiceLogonRight)\s*='拒绝服务登录优先,组成员命中的拒绝项也要检查。若该设置由域 GPO 管理,在服务所在计算机的策略范围修改;本地修改可能随后被覆盖。权利变化在下次账号登录时验证,不需要为了这一项重启整机。Log on as a service
本次恢复后同一普通账户写入时间从 08:34:44.4437852Z 增到 08:34:46.4597501Z。Running 与持续输出一起确认,不能拿改配置前残留的文件作证明。
3. 服务入口 1053
另一组 Server 2022 实验用同一个 ContractProbe.exe 提供 console、ordinary、service 三个入口。console 写文件后退出,ordinary 写文件后等待 90 秒却不接入 SCM,service 调用 ServiceBase.Run 并提供 OnStart/OnStop。
| 入口 | 结果 |
|---|---|
| 手动 console | 退出 0,能写文件 |
| 服务配置 service | Running,心跳更新,能正常停止 |
| 同一 EXE 改成 ordinary | 1053;7009、7000;最终 Stopped |
| 改回 service | 同身份与权限下恢复 |
故障 ordinary 分支在 07:38:40.5321141Z 写出心跳,7009 在 07:39:10.5321847Z 记录等待连接 30000 毫秒。程序已经执行并成功访问文件,缺的是服务入口;1053 不是“代码一行没跑”的证明。
SCM 需要程序及时连接服务控制调度器。New-Service/sc create 只注册配置,不会给任意 EXE 补一套协议。StartServiceCtrlDispatcher
下面保留最小复现程序。它是专用实验服务,使用 LocalSystem,只写自己的目录;不作为业务服务身份建议。整理示例通过 sc.exe 修改命令行,已有名字或目录时拒绝覆盖:
$ErrorActionPreference='Stop'
$r='C:\NAD0520261009'
$n='NAD0520261009'
if ((Test-Path $r) -or (Get-Service $n -ErrorAction SilentlyContinue)) {
throw 'Test name or directory already exists'
}
New-Item -ItemType Directory $r | Out-Null
$src=@'
using System;
using System.IO;
using System.ServiceProcess;
using System.Threading;
public class ContractProbe : ServiceBase {
static string root = @"C:\NAD0520261009";
Timer timer;
static void Work() { File.WriteAllText(Path.Combine(root,"heartbeat.txt"),DateTime.UtcNow.ToString("o")); }
public ContractProbe() { ServiceName="NAD0520261009"; CanStop=true; }
protected override void OnStart(string[] args) { Work(); timer=new Timer(s=>Work(),null,1000,1000); }
protected override void OnStop() { timer.Dispose(); File.WriteAllText(Path.Combine(root,"stopped.txt"),"stopped"); }
public static void Main(string[] args) {
if(args.Length>0 && args[0]=="service") Run(new ContractProbe());
else { Work(); if(args.Length>0 && args[0]=="console") return; Thread.Sleep(90000); }
}
}
'@
Add-Type -TypeDefinition $src -ReferencedAssemblies System.ServiceProcess.dll `
-OutputAssembly "$r\ContractProbe.exe" -OutputType ConsoleApplication
if (-not (Test-Path "$r\ContractProbe.exe")) { throw 'Compilation failed' }
$p=Start-Process "$r\ContractProbe.exe" -ArgumentList console -PassThru -Wait
$p.ExitCode
Get-Content "$r\heartbeat.txt"
New-Service -Name $n -BinaryPathName '"C:\NAD0520261009\ContractProbe.exe" service' `
-StartupType Manual | Out-Null
Start-Service $n
(Get-Service $n).WaitForStatus('Running',[TimeSpan]::FromSeconds(10))
Stop-Service $n
sc.exe config $n binPath= '"C:\NAD0520261009\ContractProbe.exe" ordinary'
if ($LASTEXITCODE -ne 0) { throw "Service configuration failed" }
sc.exe start $n最后一条故意触发超时。修复示例在确认失败进程已退出后,切回程序实际支持的入口:
$n = 'NAD0520261009'
sc.exe config $n binPath= '"C:\NAD0520261009\ContractProbe.exe" service'
if ($LASTEXITCODE -ne 0) { throw 'Service configuration failed.' }
Start-Service $n
(Get-Service $n).WaitForStatus('Running',[TimeSpan]::FromSeconds(15))
$a = Get-Content 'C:\NAD0520261009\heartbeat.txt' -Raw
Start-Sleep -Seconds 2
$b = Get-Content 'C:\NAD0520261009\heartbeat.txt' -Raw
if ($a -eq $b) { throw 'Heartbeat did not advance.' }
Stop-Service $n
(Get-Service $n).WaitForStatus('Stopped',[TimeSpan]::FromSeconds(15))
Get-Content 'C:\NAD0520261009\stopped.txt'路径双引号只包住 EXE,参数放在外面。只有普通入口的工具应使用它支持的宿主、计划任务或适当服务封装;具体封装的停止、信号和子进程清理能力要单独验证,不能一概说包装器只能强杀。
4. 初始化与超时
LTSC 2021 上把 cmd.exe /c ping -n 45 127.0.0.1 直接注册为 kbslow,同样约 30 秒返回 1053/7009;改 delayed-auto 后是 30.1 秒,结果不变。延迟自动改变开机时启动时机,不能为没有服务入口的程序补协议。
换成真实 ServiceBase 服务后,OnStart 阻塞 45 秒,该次 sc start 约 0.1 秒返回 START_PENDING,8 秒仍待启动,50 秒观察已 Running,且没有 7009。它表明这一次已经建立连接后的等待行为不同,不能据此保证所有框架和系统都可以无条件阻塞 45 秒。
异步版本日志如下:
18:17:51.1270045Z OnStart enter mode=async
18:17:51.1426420Z work begin
18:17:51.1426420Z OnStart return
18:18:36.1576729Z work done启动调用 0.07 秒返回,3 秒内看到 Running,业务工作随后完成。若业务尚未就绪,应用要显式提供 readiness 状态或拒绝提前请求,避免把 Running 当作数据库迁移、网络依赖等已就绪。
确需 pending 初始化时,及时上报合理的等待提示与进度。ServiceBase.RequestAdditionalTime 用于 pending 操作,不是把全局超时改大,也不是任意状态下都能调用。数值应反映下一段工作预计时间,不必机械递减,更不能无限续期掩盖挂死。RequestAdditionalTime
测试机上没有设置 ServicesPipeTimeout 覆盖值;它不是推荐默认修复。先修入口和状态回报,需要调整全局项的遗留场景另作基线、影响和重启安排。sc start 返回成功/1056 也不替代等待状态与真实请求。
5. 崩溃恢复
恢复实验的服务 KBEW0720261011 在 Server 2022 工作组机运行,每秒记录时间和 PID。初始 qfailure 无动作,强杀后连续 20 秒 Stopped/PID=0,心跳停止,出现 7034。
只修改恢复配置、保持 EXE 不变:
sc.exe failure KBEW0720261011 reset= 86400 actions= restart/1000/restart/1000/restart/1000
sc.exe qfailure KBEW0720261011
sc.exe qfailureflag KBEW0720261011再次模拟专用测试进程崩溃后,约 1.05 秒出现 START_PENDING,约 2.11 秒出现新 PID、Running,心跳以新 PID 继续,7031 包含重启动作。1000 毫秒只是配置的动作延迟,不是承诺一秒内业务恢复。
三项都是 restart 时,第三次后仍会继续重启。失败次数超过动作数组长度,SCM 继续重复最后一项。要限制反复崩溃,应设计明确的最后不操作策略、外部限频与告警,再在测试机验证,而不是依靠“三次额度用完”。
reset=86400 表示没有失败达到相应秒数后重置计数,不是每天零点清零。永不重置使用 INFINITE 语义,不能把 reset=0 解释成永不重置。具体数组与计数规则见 SERVICE_FAILURE_ACTIONS。
本次正常 Stop-Service 后 20 秒内没有重启,说明正常成功停止不命中该次失败路径。非崩溃、报告 STOPPED 但非零 Win32 退出码的行为还受 failureflag 控制,原实验没有覆盖这一分支;配置了恢复动作才有讨论该标志的前提。失败动作标志
不要用强杀验证任何未经确认的业务进程;共享宿主进程里还可能有其他服务。复现应保持专用 own-process 服务,确认可执行路径、服务 PID 和测试窗口,再采样配置、退出事件、新 PID 与业务产物。
6. 配置备份与回退
对已有服务先保存命令行、账号、启动类型和恢复配置:
$backup = Join-Path $env:ProgramData ('ServiceBackup-' + (Get-Date -Format yyyyMMdd-HHmmss))
New-Item -ItemType Directory -Path $backup -ErrorAction Stop | Out-Null
reg.exe export "HKLM\SYSTEM\CurrentControlSet\Services\$n" "$backup\service.reg"
if ($LASTEXITCODE -ne 0) { throw 'Registry export failed.' }
Get-CimInstance Win32_Service -Filter "Name='$n'" |
Select-Object Name,PathName,StartName,StartMode | Export-Clixml "$backup\service.clixml"
sc.exe qfailure $n | Out-File "$backup\recovery.txt"
sc.exe qfailureflag $n | Out-File "$backup\recovery-flag.txt"注册表导出不包含可直接拿来重设的服务账号明文密码,不能当万能还原包。通过 SCM 或服务属性页恢复相应配置,凭据单独管理;不要在运行中的业务服务上直接导入一整份旧注册表替代受控恢复。
实验清理先停专用服务并确认对应进程退出,再删除该服务及准确实验目录;不清历史 SCM/Security 日志。正常停止、删除失败配置等步骤要回读。Windows PowerShell 5.1 向原生命令传空字符串有兼容问题,清空 actions 的操作通过服务恢复页面或确认过的 cmd 包装完成,别仅凭命令没报错就认定动作已删除。
最终验收同时保留服务状态、准确运行身份、持续更新的输出或真实请求、停止回调和新的错误窗口。入口修好不代表凭据正确,Running 也不代表业务就绪;分阶段验证能把后续故障定位留在正确位置。
