nginx服务化五套方案原理解剖与选型

把 nginx 做成 Windows 服务:nssm、WinSW、srvany、ServiceBase、计划任务 ------ 五套方案的原理解剖与选型

摘要:nginx 在 Windows 上没有官方的服务支持,想让它开机自启就得"套一层包装器"。本文先把 Windows 服务的契约(SCM / ServiceMain / SetServiceStatus)讲清楚,解释为什么 sc create 直挂 nginx.exe 注定失败;再逐个解剖 nssm、WinSW、srvany、.NET ServiceBase 自制包装器、计划任务/启动文件夹五种方案的工作原理、停止机制、日志能力与实测数据(含 PE 头实测);最后给一张选型决策表和三条通用铁律。全程用真实文件尺寸和导入表说话,不靠"听说"。


一、先搞懂对手:Windows 服务到底是个什么东西

1.1 nginx.exe 不是"服务程序"

Windows 服务的世界由 SCM(Service Control Manager,服务控制管理器) 统治。一个 exe 想成为服务,必须满足契约:

  1. 自己调用 StartServiceCtrlDispatcher,把 ServiceMain 函数指针注册给 SCM;
  2. ServiceMain 里先调 RegisterServiceCtrlHandlerEx 注册控制处理函数;
  3. 然后调 SetServiceStatus 报告状态:START_PENDING → RUNNING,停止时 STOP_PENDING → STOPPED;
  4. 全程能响应 SCM 发来的控制码:SERVICE_CONTROL_STOP / PAUSE / SHUTDOWN / INTERROGATE。

nginx.exe 一个都不满足。 它是个普通的控制台程序,main() 起来就去监听端口了,从不理会 SCM。

所以 sc create nginx binPath= "D:\nginx\nginx.exe" 这种"直挂"写法看着聪明,实测必然失败:SCM 拉起进程后等半天等不到 SERVICE_RUNNING,30 秒后判超时,给你一个 错误 1053「服务没有及时响应启动或控制请求」 ,顺手在事件日志里留下 7000/7009。就算把 ServiceStartToKill=0 之类的注册表技巧用上,也还是没人转发停止信号、没人守进程 ------ 服务管理器里点"停止",nginx 照跑不误。

1.2 于是就有了"包装器"这个行业

包装器的思路统一而朴素:包装器自己是个合法的服务程序(满足 1.1 的契约),它再把真正的业务程序当子进程拉起来,并替 SCM 看着它。

scss 复制代码
                      ┌─────────────────────────────────────────┐
   服务管理器 / net start │  SCM (services.exe)                     │
        ─────────────────▶│  读注册表 → 拉起"服务进程"(= 包装器)      │
                      └───────────────┬─────────────────────────┘
                                      │ StartServiceCtrlDispatcher
                                      ▼
                      ┌─────────────────────────────────────────┐
                      │  包装器进程 (nssm.exe / nginx-service.exe)│
                      │   ① SetServiceStatus(START_PENDING)      │
                      │   ② CreateProcess  → nginx.exe           │
                      │   ③ SetServiceStatus(RUNNING)            │
                      │   ④ 循环监视子进程退出码 / 重定向输出      │
                      └───────────────┬─────────────────────────┘
                                      │ 被托管进程
                                      ▼
                              nginx.exe (master) ── worker 进程

   停止时:SCM ──ControlService(STOP)──▶ 包装器 ──┬─ 发停止命令 / Ctrl+C / 杀进程树
                                                  └─ SetServiceStatus(STOPPED) 回报

所有方案的差别,就藏在这张图的几个空格里 :谁来实现这个包装器(原生 C++ / .NET / 老古董工具)、配置怎么写(命令行 / XML / 注册表 / 代码)、停止时怎么通知子进程(Ctrl+C / 自定义停止命令 / 直接 TerminateProcess)、输出往哪落(重定向到文件 / 丢弃),以及目标机需要什么运行时。


二、五套方案逐个解剖

2.1 nssm ------ 零依赖的原生包装器(Windows 上的"标准答案")

原理 :单个 C++ 原生 exe,自己实现服务契约,用 CreateProcess 拉起被托管程序,并周期性 WaitForSingleObject 监视;被托管程序退出就按配置策略处理(重启 / 忽略 / 退出)。

实测数据(自写 Python 解析 PE,nssm 2.24 官方包):

检查项 win32/nssm.exe win64/nssm.exe
大小 294,912 字节 331,264 字节
架构 x86 (0x014c) x64 (0x8664)
PE SubsystemVersion 5.0 5.2(Win7 = 6.1,加载器不会拒绝)
导入 DLL SHLWAPI / KERNEL32 / USER32 / COMDLG32 / ADVAPI32 / SHELL32 同左

导入表里没有 MSVCR*.dll、没有 VCRUNTIME140.dll、没有 api-ms-win-crt-* ------ 它是静态链接 CRT(工程 RuntimeLibrary="0" = /MT)的零依赖单文件程序:不需要 VC++ 运行库、不需要 UCRT、不需要 .NET。这是 nssm 最大的价值,也是它在老系统(Windows 2000 起)上一直吃香的原因。

配置方式:命令行/图形界面写进服务注册表。

c 复制代码
nssm install nginx D:\nginx\nginx.exe
nssm set nginx AppDirectory D:\nginx          ← 工作目录(最高频的坑)
nssm set nginx AppStdout D:\nginx\logs\out.log  ← 输出重定向(远程排障的生命线)
nssm set nginx AppStderr D:\nginx\logs\err.log
nssm set nginx AppExit Default Restart
nssm set nginx Start SERVICE_AUTO_START
nssm dump nginx                                 ← 打印全部配置,排障第一条命令

停止机制 :按 AppStopMethod* 依次尝试(默认顺序:ConsoleCtrl 控制台 Ctrl+C 事件 → WindowMessage WM_CLOSE → Threads → Terminate 强杀),逐级升级。对 nginx 这种控制台程序,Ctrl+C 那一步通常就能优雅退出。

它独有的坑 ------ "服务卡在 Paused":官方 README 原文:

NSSM will pause an increasingly longer time between subsequent restart attempts if the service fails to start in a timely manner, up to a maximum of four minutes. ... you can use the Windows service console (where the service will be shown in Paused state) to send a continue signal

也就是说:被托管程序反复"起来就秒退"时,nssm 会逐步延长重试间隔(最长 4 分钟),服务在服务管理器里显示为"暂停" 。很多人(包括一些技术文章)把这个现象误判成"nssm 和系统不兼容""系统缺组件",实际性质是nginx 自己起不来触发了 nssm 的保护机制。

适用 :任何 Windows(含精简/老系统),只要不想在目标机上装运行时。不适合:需要复杂依赖编排、需要把配置纳入版本管理的场景(配置在注册表里,不便 diff)。

2.2 WinSW ------ 用 XML 描述服务的包装器

原理 :.NET 写的包装器,继承 ServiceBase,实现服务契约;被托管程序的路径、参数、日志、失败恢复等全部写在同名 XML 里 。改名成 myapp.exe 后,它会找同目录的 myapp.xml。

配置方式:XML(可进 Git、可代码生成,这是它比 nssm 更"工程化"的地方)。

xml 复制代码
<service>
  <id>nginx</id>
  <name>nginx</name>
  <description>nginx web server</description>
  <executable>D:\nginx\nginx.exe</executable>
  <workingdirectory>D:\nginx</workingdirectory>
  <logpath>D:\nginx</logpath>
  <logmode>roll</logmode>
  <startmode>Automatic</startmode>
  <depend>MySQL</depend>
  <stopexecutable>D:\nginx\nginx.exe</stopexecutable>
  <stoparguments>-s stop</stoparguments>
  <onfailure action="restart" delay="10 sec"/>
  <resetfailure>1 hour</resetfailure>
</service>

服务管理 用自带子命令:install / uninstall / start / stop / stopwait / restart / status(status 会打印 NonExistent / Started / Stopped)。

关键语义(官方 v2.12.0 文档,务必记牢):

  • <startmode> 默认 Automatic;
  • 停止流程:默认先发 Ctrl+C ,等 <stoptimeout>(默认 15 秒),仍不退才 TerminateProcess;一旦配了 <stoparguments>,就改为先运行停止命令并等它退出;
  • 联动规则 :用了 <stoparguments>,启动参数就要用 <startarguments> 而不是 <arguments>;
  • 失败恢复用 <onfailure action="restart|reboot|none" delay="..."/>,计数重置用 <resetfailure>;
  • 还支持 <serviceaccount>(LocalSystem / LocalService / NetworkService / 域账号 / gMSA)、<delayedAutoStart/>、<priority>、<download>(启动前拉取文件,可做自更新)。

它最大的坑 ------ 目标机必须有 .NET 运行时,而且构建要挑对 。WinSW 的 .NET Framework 构建是纯托管程序 (实测 WinSW.NET4.exe 的导入表只有 mscoree.dll,内嵌 CLR 版本串 v4.0.30319),没有运行时就是一堆 IL 字节码,谁也跑不起来。

v2.12.0 的构建矩阵(尺寸实测,GitHub Releases):

构建 大小 运行时前提 备注
WinSW.NET2.exe 860,672 B (841 KB) .NET Framework 2.0/3.5 老系统友好
WinSW.NET4.exe 852,480 B (833 KB) .NET Framework 4.0 存量现场最常见的那个
WinSW.NET461.exe 655,872 B (641 KB) .NET Framework 4.6.1+ Win7 需 SP1 + SHA-2 补丁
WinSW-x86.exe / WinSW-x64.exe ≈16.5 MB / ≈17.4 MB 自包含(v2 = .NET Core 3.1;v3 = .NET 7) v3 的 .NET 7 自包含仅支持 Win10 1607+

为什么这点特别重要 :GitHub Releases 页面展示 2.12.0 稳定版和 3.x 预发布版两组资产,默认容易点到最新(3.x)那个 17 MB 的 exe;而它在 Win7/Server 2008 R2 上根本起不来。选错构建 → "WinSW 不支持 Win7"的传言就是这么来的。

2.3 srvany.exe + instsrv ------ 该进博物馆的老方案

原理 :微软 Windows Server 2003 Resource Kit 里的两个小工具。instsrv.exe 负责注册服务,srvany.exe 负责当那个"壳":启动时读注册表

ini 复制代码
HKLM\SYSTEM\CurrentControlSet\Services\<服务名>\Parameters
    Application   = D:\nginx\nginx.exe
    AppParameters = (启动参数)
    AppDirectory  = D:\nginx

然后 CreateProcess 拉起目标程序。

为什么不该再用:

  • 不看不管:srvany 拉起进程后就基本撒手,进程崩了它不知道,也不会重启;服务状态和实际进程状态可以对不上;
  • 不转发停止:点"停止服务",被托管进程不一定会跟着干净退出(往往只剩强杀一条路);
  • 没有日志:被托管程序的 stdout/stderr 没有落盘机制,出问题只能靠猜;
  • 依赖第三方包:需要下载 Resource Kit,很多现场是"某台机器上翻出来的 srvany.exe",来源与版本不可控。

后来的社区替代品(srvany-ng、Rust 写的 shawl)补了监控和日志,但选型逻辑上你已经不该在 2020 年代新项目里用 srvany 了。

2.4 自制 .NET ServiceBase 包装器("installutil 那一套")

原理 :自己写 C#:class MyService : ServiceBase + ServiceBase.Run(new MyService()),在 OnStart 里 Process.Start 拉起 nginx,在 OnStop 里杀掉。安装用 .NET SDK 自带工具:

bash 复制代码
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\installutil.exe nginx-service.exe
installutil.exe /u nginx-service.exe     ← 卸载

它的"标志性报错" ------ 直接双击或命令行运行这个 exe 时,会弹:

无法从命令行或调试程序启动服务。必须首先安装 Windows 服务(使用 installutil.exe),然后用 ServerExplorer、Windows 服务管理工具或 NET START 命令启动它。

这是 ServiceBase.Run 的标准提示 :它检测到父进程不是 SCM,于是拒绝执行。看到这句话,就等于看到"这是裸的 .NET ServiceBase 程序,不是 WinSW"(WinSW 双击时只会打印用法帮助)。这句话本身不代表 exe 坏了,但如果这台机器上服务从未被正确安装过,那"开机不启动"就有解释了。

什么时候它是对的:你需要极其定制化的行为(启动前跑一段业务逻辑、和自家系统深度集成)。

为什么大多数场景不该选它:监控进程、重定向日志、优雅停止、失败恢复、依赖顺序......这些 WinSW/nssm 都白送了,自写得写一遍还有 bug;换人维护时没有文档可查,只有一个 exe。

2.5 计划任务 / 启动文件夹 ------ 不是服务,不要混用

原理 :任务计划程序(taskschd.msc)在"计算机启动时"或"用户登录时"运行一个程序;启动文件夹(shell:startup)则是登录时执行。

为什么在交付场景是雷区:

  • 不在 SCM 里 :sc query、net start、"服务"面板全都看不到它,运维交接时等于隐形进程;
  • 依赖会话:启动文件夹里的东西必须有人登录才跑;任务计划虽然能配"不管用户是否登录",但需要保存密码,密码改了任务就静默失效;
  • 权限错位 :nginx 以某个登录用户身份跑,读写 logs/、绑定 80 端口的权限都跟着这个用户走;
  • 无失败恢复:进程崩了没人管,开机顺序也无法和其他服务编排;
  • 双开冲突:如果服务方式也配了,就会两个 nginx 抢 80 端口。

一句话:只有"给登录用户用的桌面程序"才用启动项;服务器上的常驻服务,一律做服务。


三、横向对比大表

维度 nssm WinSW srvany 自制 ServiceBase 计划任务/启动项
实现语言 C++ 原生 .NET C++ 原生(老) .NET / 自选 系统组件
目标机依赖 无 .NET Framework 4.0+ 或自包含构建 无(但要带 srvany.exe) 对应 .NET 版本 无
体积 ≈ 300 KB 833 KB(NET4)/ 17 MB(自包含) ≈ 30 KB + instsrv 视写法 ---
配置形式 服务注册表(GUI/命令行) XML(可进 Git) 注册表 Parameters 代码里硬编码 图形界面
进程监控/自动重启 ✅ AppExit + throttle ✅ onfailure/resetfailure ❌ 需自己写 ❌
优雅停止 ✅ Ctrl+C→WM_CLOSE→强杀 ✅ stoparguments / stoptimeout ❌ 基本靠强杀 需自己写 ❌
输出落盘 ✅ AppStdout/AppStderr ✅ .out/.err/.wrapper 日志 + 轮转 ❌ 需自己写 ❌
依赖顺序 ✅ depend ✅ <depend> ❌ 需自己写 ⚠️ 触发器凑
最低系统 Windows 2000+ Win7 SP1(Framework 构建) Win2000/2003 时代 Win7+ Win7+
许可证 Public domain MIT 微软旧工具包 自己定 ---
最好的场景 老系统 / 零依赖要求 需要 XML 化、要进版本管理 已有历史包袱 深度定制 桌面程序

四、选型决策树

arduino 复制代码
目标机能不能装运行时?
├─ 不能(老旧/精简系统、不允许装 .NET)
│   └─ ▶ nssm(零依赖,一个 300 KB 的 exe 搞定)
└─ 能
    ├─ 系统是 Win10 1607+ / Server 2016+
    │   └─ ▶ WinSW(XML 配置、功能最全,选 2.12.0 的 WinSW.NET4/NET461,或 3.x 的自包含构建)
    └─ 系统是 Win7 SP1 / Server 2008 R2
        ├─ 已装 .NET 4.x ▶ WinSW.NET4.exe(833 KB) 或 nssm
        └─ 干净系统(只有 3.5.1)
            ├─ 可以装 .NET 4.x ▶ 装完再用 WinSW.NET4.exe
            └─ 不想装 ▶ nssm

存量现场(已有包装器,正在跑)?
└─ ▶ 优先"修通现有方案":改配置、改启动类型、补日志,而不是换组件
     (动一次配置要停机、要审批,换组件的收益往往覆盖不了成本)

关于"WinSW vs nssm"再多说一句,避免站队:两者没有绝对优劣,只有适配与否。 nssm 胜在原生、零依赖、单文件;WinSW 胜在配置可读可版本化、功能项更细(服务账户、gMSA、下载扩展)。新建环境按上面决策树挑;接手存量现场,先修通。


五、三条通用铁律(无论用哪套方案)

铁律一:工作目录必须显式设置

这是 nginx 服务化最高频 的翻车点。包装器由 SCM 拉起时,工作目录默认是 C:\Windows\System32,而 nginx 的 conf/nginx.conf、logs/、pid 都是相对路径 ------ 手动双击测试一切正常,做成服务就秒退。

  • nssm:nssm set nginx AppDirectory D:\nginx
  • WinSW:<workingdirectory>D:\nginx</workingdirectory>
  • 保险做法:把 nginx.conf 里的 error_log、pid、access_log、root 全写成绝对路径 (Windows 下用正斜杠 /)。

铁律二:把输出落盘,别让服务"死了没遗言"

判断"服务为什么起不来",靠猜是猜不出来的。务必配好:

  • nssm:AppStdout / AppStderr;
  • WinSW:<logpath> + <logmode>roll</logmode>,现场看 xxx.wrapper.log(包装器自己的动作)、xxx.out.log / xxx.err.log(被托管程序的输出)。

补一个反直觉的点:nginx 正常工作时几乎不往 stdout/stderr 写东西,所以 .out/.err 是 0 KB 很正常 ,真正的线索在 wrapper 日志、logs\error.log 和 logs\nginx.pid。

铁律三:优雅停止,且必须验证

  • Windows 上停 nginx 的正确姿势是 nginx.exe -s stop(重载是 -s reload);
  • 直接 TerminateProcess 强杀会留下 pid 残留、worker 进程孤儿等隐患;
  • 验证方法 :看日志里停止时到底执行了什么。如果只有一行 ProcessKill,说明你的"优雅停止"配置根本没生效(WinSW 的 <stopexecutable> 写成了带参数的一行路径,就是个经典陷阱)。

六、收尾

Windows 上把 nginx 做成服务,本质是补上 nginx 缺失的那部分"服务契约实现" 。理解了 SCM 与包装器之间的关系,五种方案的差异就退化成了四个具体问题:目标机有没有运行时、配置写在哪、停止怎么通知、日志往哪落。

把这四个问题问清楚,选型基本不会错;把这四条落实到位,服务"开机不启动""起来就退""停了留残留"这三类问题也就少了一大半。

本文的二进制尺寸、PE 头 SubsystemVersion、导入表数据均为实测(WinSW v2.12.0 取自 GitHub Releases API 与官方包,nssm 2.24 取自 Chocolatey 官方包);WinSW 的 XML 语义与 nssm 的 Paused 机制均引用官方文档原文。

相关推荐
谁在黄金彼岸1 小时前
WinSW在Win7上失败真相-实测与修复
后端
花间相见1 小时前
【后端开发|Redis进阶01】—— Redis分布式锁全解:从SETNX到Redisson看门狗,再到RedLock
后端·面试
llqbzllll1 小时前
A2A 1.0 到底和 MCP 差在哪:从 Agent Card、Task 到 Java 最小互操作
后端
花间相见1 小时前
【后端开发|JVM基础01】—— Java垃圾回收全解:从分代内存模型到各收集器的分代职责
后端
geovindu1 小时前
rust: Abstract Factory pattern
开发语言·后端·设计模式·rust·抽象工厂模式
pe7er1 小时前
Spring Boot 日志最佳实践:接入、分级、异常记录与滚动拆分
后端
程序员老赵3 小时前
Docker 部署 Dolibarr:轻松搭建开源 ERP/CRM 平台
运维·前端·后端
量化分析码农3 小时前
【Python量化系统工程实战 #06】从本地脚本到云端部署:量化系统的最小可运行架构
后端