ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows下用winsw将Node.js服务注册为系统服务:自启、崩溃恢复与日志轮转实战

Windows下用winsw将Node.js服务注册为系统服务:自启、崩溃恢复与日志轮转实战 neoj-community 跑在我一台 Windows 服务器上已经一两个月了。最初图省事直接在远程桌面里开了个终端窗口npm start 一把梭——反正关掉远程桌面它也不会断。直到上周机房意外重启我又一次半夜爬起来登录服务器手动敲命令、盯着日志确认服务恢复才下定决心把 neoj-community 正式注册成 Windows 系统服务。这篇文章就把整个过程写下来包括工具选型、winsw 配置细节、环境变量传递的坑、端口占用排查链路以及注册之后自启、崩溃恢复、日志轮转那套收尾工作。目标是让和我一样平时不太折腾 Windows 服务的人照着文章也能把任意一个 Node/Python/Java 写的社区版服务老老实实变成开机自启、崩了自动拉起的后台服务。1. 这事非办不可手工启动模式的三个痛点1.1 终端窗口与服务的生命周期绑定Windows 下直接npm start启动 neoj-community进程挂在终端窗口下。这个临时窗口一旦被误关、远程桌面会话断开时被系统回收、或者不小心按到 CtrlC服务进程就跟着结束了。我以前总以为服务跑在远程服务器上就很安全直到我自己远程连上去想看看实时日志手一滑把窗口关掉整站直接 502。你说日志是看不了还是命令是敲不了都不是就是窗口和进程绑定的模型太脆弱。真正要长时间跑的东西必须脱离用户会话由 Windows 服务管理器独立托管。1.2 开机不会自启每次重启都要人肉操作手工启动模式下系统一重启neoj-community 就没了。你不可能指望每次重启都有人主动开终端、切目录、敲启动命令。更麻烦的是这台机器可能放在机房里远程桌面连上去之前你根本不知道服务是死的等你知道的时候用户已经反映了。这件事的解决办法不是记住要手动启动而是把注册服务做成随系统启动自动拉起。Windows 服务Service本来就有 Startup Type 的概念服务管理器会在系统启动阶段按配置启动对应程序不需要登录用户也不需要人工介入。1.3 进程一崩就没人管日志还不知道去哪找Node 服务崩溃这事不常发生但一旦发生就很要命没有守护进程没有自动重启没有统一的日志输出位置。如果你没做额外的日志采集控制台输出就在那个已经消失的窗口里崩溃信息根本留不下来。把这些痛点串起来看需求非常明确需要一个工具让 neoj-community 脱离终端窗口、开机自启、崩溃自动拉起、日志落盘可查。这就是这篇的落点。2. 先对比再动手winsw、NSSM、sc.exe 和计划任务2.1 我为什么把 winsw 放在第一顺位Windows 下注册服务的方案其实不少但适合把一个命令行进程变成系统服务的无非这几个winsw、NSSM、系统自带的 sc.exe以及不太正经的任务计划程序。先说结论我选了 winsw。原因很直接winsw 是纯 XML 配置驱动的一个 exe 加一个 xml 文件就能把 neoj-community 变成服务。它的配置项覆盖了我需要的全部能力可执行文件路径、命令行参数、工作目录、环境变量、日志输出、失败重启策略。而且配置文件是纯文本可以直接扔进 Git 管理换一台机器部署时直接复制整套配置就行不需要像 NSSM 那样打开图形界面一个个点。另外 winsw 体积小单文件无额外依赖这正好符合系统服务应该够轻、够独立的原则。2.2 四套方案的关键参数对比我把四套方案按我实际关心的维度列了个表格不一定全面但足够做决策了方案能否设置工作目录能否注入环境变量崩溃自动重启日志落盘配置可脚本化winsw可以可以env 标签可以onfailure内置支持滚动强XML 进版本库NSSM可以可以GUI/命令行可以可以但策略配置相对零散中等GUI 为主sc.exe不行不行不行不行弱只适合极简单场景任务计划程序可以通过启动于设置可以但很绕有限有限还得配置保留上次操作弱GUI 为主2.3 什么情况下 NSSM 反而更合适NSSM 不是不好它甚至比 winsw 更适合某些场景比如你不太想写 XML或者团队里有不熟悉命令行的人需要日常维护服务参数NSSM 的图形界面安装方式更友好右键菜单里还能直接查看服务状态、修改启动参数。我个人只在两种情况下会考虑 NSSM第一种是临时给同事演示 怎么把 exe 变服务图形化点几步更直观第二种是目标程序本身参数极多且经常变GUI 修改比改 XML 再重启服务更快。但对 neoj-community 这种部署方式相对固定的服务我还是倾向 winsw因为配置即代码出错可以回滚审计也方便。sc.exe 就不推荐了它连工作目录都指定不了很多程序在非预期目录下运行会直接报错环境变量也无法按服务维度注入排除。任务计划程序的问题我在后面单独展开。3. 动手配置winsw 注册 neoj-community 的全过程3.1 前置准备把程序放到一个固定目录在注册之前先把 neoj-community 放到一个固定的、不带空格的目录里。比如D:\apps\neoj-community不要放在桌面或个人用户目录下否则服务账户访问权限会很麻烦。然后准备一个专门放 winsw 的目录比如D:\services\winsw。从 winsw 的 GitHub releases 页面下载 exe 文件重命名为winsw.exe放在这个目录里。我习惯在同一个目录下再建一个conf子目录专门放 XML 配置但这只是个人习惯不强求。这里有个重要细节winsw 的配置文件默认是和 winsw.exe 同目录、同名的 xml 文件。也就是说如果 exe 叫winsw.exe配置就叫winsw.xml。当然也可以通过-c参数指定别的配置路径但默认找同目录同名文件最省心。3.2 编写 winsw XML 配置文件我直接给出一个能跑的配置样例针对 neoj-community 这种 Node.js 服务service idneoj-community/id nameNeoJ Community Service/name descriptionNeoJ Community self-hosted service/description executableD:\apps\nodejs\node.exe/executable argumentsD:\apps\neoj-community\server\src\index.js/arguments workingdirectoryD:\apps\neoj-community/workingdirectory env namePORT value3000 / env nameNODE_ENV valueproduction / log moderoll-by-size sizeThreshold10240/sizeThreshold keepFiles8/keepFiles /log onfailure actionrestart delay10 sec / onfailure actionrestart delay20 sec / onfailure actionrestart delay30 sec / /service几个字段逐一说明id是服务在 Windows 内部的唯一标识安装后就是你用sc query看到的服务名。注意这个 ID 不能重复卸载旧服务前不要直接装新的。executable要写绝对路径。我直接指定node.exe的完整路径而不是npm.cmd原因后面讲。arguments指定要运行的脚本入口。如果你的项目入口在dist/main.js这里就改成对应路径。workingdirectory非常关键很多程序依赖相对路径读取配置或静态资源这一步错了一切都会跟着错。env标签用于注入环境变量比如端口、NODE_ENV效果等价于在系统环境变量里设置但只对这个服务生效不会污染全局。日志部分我选了按大小滚动单文件超过 10MB 就切分保留最近 8 个日志文件。这里可以根据你实际日志量调整阈值。3.3 注册、启动、验证一条龙在D:\services\winsw目录下打开管理员 PowerShell 或 CMD依次执行.\winsw.exe install .\winsw.exe start .\winsw.exe statusinstall将服务信息写入 Windows 服务管理器此时在services.msc里就能看到名为NeoJ Community Service的服务。start启动它status查询状态正常会输出Running。如果需要确认程序本身的健康状态可以访问 http://localhost:3000 看页面是否正常或者直接看 winsw 生成的日志文件。3.4 把环境变量写进配置而不是写进系统有同学图省事在系统属性-环境变量里直接加PORT3000。这样不是不行但问题在于环境变量变成全局的会造成服务之间互相干扰。比如你以后在 Windows 上再跑一个需要监听 3001 的服务谁都不希望因为一个全局变量导致两个服务同时抢端口。winsw 的env标签就是在服务启动时临时设置环境变量作用域只对这一个服务实例有效。这个做法对 neoj-community 这种可能需要多实例部署的项目尤其重要你想开两个实例监听不同端口同一个程序目录用两个 XML 配置分别指定 PORT就能在同一台 Windows 上并行跑互不干扰。写进系统全局变量反而做不到。4. 运行期踩坑实录端口占用、权限与日志4.1 3000 端口被占用的完整排查链路第一次注册完服务我以为万事大吉结果winsw start后状态从Starting秒变Stopped。第一反应是看日志日志里只有一句类似Error: listen EADDRINUSE: address already in use 0.0.0.0:3000。端口占了。很多人在这一步直接去任务管理器里找可疑进程一顿乱杀这是最不推荐的。正确的排查链路是netstat -ano | findstr :3000输出里最后那列是 PID然后tasklist /FI PID eq 1234查清楚这个 PID 是什么进程、路径在哪再决定是否结束它。我之前就是没查直接杀后来发现占端口的是我自己几分钟前手工启动的旧实例进程结束完之后端口释放新服务才正常起来。这里有一个排查顺序的建议先看是不是自己旧实例没关干净再看是不是别的服务占端口。前者是开发期最常见的原因后者往往是配置冲突处理方式完全不同。4.2 服务启动又秒退先看环境变量和路径如果日志里没有 EADDRINUSE而是服务启动后立刻退出优先级最高的怀疑对象永远是三个路径、工作目录、环境变量。先说路径executable指向的 node.exe 是否存在、arguments里的入口文件是否存在都检查一遍。别觉得这很蠢我在目录调整后经常因为 XML 里还写着旧路径而踩坑。再说工作目录如果你的项目读取./config/config.json这类相对路径而 winsw 没设置workingdirectory服务会在C:\Windows\System32下启动文件自然找不到。这个报错有时候不太明显可能是一个ENOENT也可能是配置文件加载失败后程序主动退出。最后是环境变量有些项目依赖DATABASE_URL这种就必须写在env里。尤其注意 Node 项目中process.env.PORT不一定有默认值不设就取不到监听端口。我后来把自己的习惯固定下来端口、环境模式、数据库链接串全写进 XML不在代码里给默认值。4.3 日志写不进去的权限问题winsw 生成的日志文件默认写在 exe 同目录下但服务账户有时候没有写权限。这个问题出现在我一次把 winsw.exe 放在C:\Program Files\下时——Program Files 目录默认写权限受限服务一启动写日志就失败进程直接退出。解决办法就两个要么把 winsw 和日志放在普通数据目录比如D:\services\winsw要么显式在 XML 里配置logpath指向一个有写权限的目录。我更推荐前者干净直接。这个坑其实也提醒了一件事服务账户和你的管理员账户是两回事别用我能读写来判断服务能读写尤其是涉及C:\Program Files、系统盘根目录时提前给日志目录授权比事后排错轻松得多。5. 注册完还没完自启、崩溃恢复、日志轮转与更新5.1 崩溃自动重启的配置经验winsw 的onfailure标签可以在服务进程非正常退出时自动重启。我上面的配置里写了三行意思是首次失败等 10 秒重启再次失败等 20 秒第三次等 30 秒。这个延时递进设计是有意的如果程序是因为依赖还没就绪而崩的比如数据库没起来给它一点缓冲时间再拉起比疯狂重启更合理。但要注意onfailure 只能覆盖进程异常退出的场景如果你的服务死锁了、进程还活着但不再响应请求这种半死状态重启机制是救不了的。我的经验是外面再挂一个简单的健康检查脚本定期请求健康检查接口连续失败几次就调用winsw restart。这一步不是必须的但对自托管的社区项目来说能有效的避免服务看着在跑其实已经挂了的尴尬。5.2 日志滚动和按天切分我在第 3 节配置里用的是按大小滚动适合日志量比较平均的场景。如果你的 neoj-community 访问量波动大可以考虑按时间滚动log moderoll-by-time patternyyyyMMdd/pattern /log这样每天一个日志文件排查时按日期找就行。另外keepFiles或按时间模式下的保留策略决定日志保留多久建议至少保留 30 天的日志否则出了问题想回溯都没有依据。如果你希望日志同时输出到标准输出和文件方便调试可以在log之外配置log modeappend但生产环境我更推荐只保留文件模式避免 Console 输出堆在服务进程里占内存。5.3 升级 neoj-community 时怎么不中断服务服务注册之后升级过程有了明确套路停服务.\winsw.exe stop备份旧目录或直接覆盖新版本文件我习惯先复制一份旧目录做回滚点如果有依赖包更新执行安装依赖或构建步骤启动服务.\winsw.exe start查日志确认启动成功。这套流程的优点是全程可以用脚本完成不会出现管理员忘了关服务直接覆盖文件的问题。如果你对停机时间敏感可以在目录层面做蓝绿切换保留neoj-community和neoj-community-new两个目录新的启动后把 XML 里的arguments和workingdirectory切到新目录停旧起新中间只隔几秒钟。5.4 防火墙与访问安全neoj-community 作为 Web 服务注册成系统服务后端口会稳定对外暴露。如果你只在局域网用在 Windows 防火墙里放行该端口即可如果可能暴露到公网强烈建议不要在 Windows 上裸奔端口。我个人的做法是neoj-community 只监听内网地址由 Nginx 做入口转发并开启 HTTPS服务本身对外不可直达。这样即使某个版本安全通告来了也能在入口层快速阻断而不必紧急升级服务。Nginx 和 neoj-community 在同一台 Windows 上也完全可行注意别让两个进程抢同一个端口就行。6. 什么时候别用这套任务计划程序和 Docker 的边界6.1 任务计划程序看似能行实际很别扭任务计划程序Task Scheduler也是 Windows 自带能力不少人图省事直接用它来开机启动 neoj-community。如果只是偶尔跑一下脚本它完全够用。但作为长期服务运行问题会在细节里逐渐暴露计划任务是按登录会话设计的虽然可以配置不管用户是否登录都要运行但服务的运行状态、日志查看、停止操作都散落在任务计划程序界面里和services.msc的管理体验不一致崩溃重启策略不灵活任务计划里只有简单的失败后重启延时、重试次数配置不如 winsw 的 onfailure 直观如果想启动时自动拉起但用户登录后不要重复启动这类高级逻辑配置起来很容易踩坑。结论任务计划更适合定时同步开机执行某个清理脚本这类一次性动作不适合承载一个需要持续在线、可运维、可观测的 Web 服务。6.2 用 Docker 跑 neoj-community 的区别如果你已经把 Docker Desktop 装好直接把 neoj-community 跑在容器里确实是另一种好方案。Docker 的好处是环境隔离、升级回滚方便配合--restart always也能实现开机自启。但代价是 Docker Desktop 本身是个重量级应用需要 WSL2 或 Hyper-V 支撑对老机器不友好而且容器生命周期和服务管理器是两套体系换个开发机又要重新折腾 Docker 环境。我自己倾向于单机部署、简单可靠就用 winsw如果你已经在容器化运维的路子上那 Docker 自然更符合体系。这篇文章讲的是 winsw 路线但不代表它是唯一的正确答案。6.3 卸载服务与回退到手工启动如果哪天你想把 neoj-community 从系统服务变回手工启动操作很简单.\winsw.exe stop .\winsw.exe uninstalluninstall会从服务管理器里移除注册信息不影响程序目录里的任何文件。之后你就可以继续用npm start手动启动了。我个人的建议是除非你有明确的调试需求否则回退到手工启动没有意义。注册成服务并不妨碍你手工看日志也不影响修改代码后重启它只是把这个服务的生命周期管理交给系统。再往后你还可以考虑用 winsw 同时注册多个相关服务。比如 neoj-community 依赖一个独立的任务队列那就再写一个 XML 文件用不同 id 注册成第二个服务两个服务都是一样的管理套路。这套方法在 Windows 上基本是通用的今天能管 neoj-community明天就能管别的自托管服务。
返回列表