尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Hono + pm2 部署后接口无响应?从端口监听到进程守护的排查指南
先说场景。你在本地开发一个 Hono 服务tsx或nodemon跑得好好的接口一发就通。部署到服务器上之后习惯性上了 pm2pm2 list一看状态 online日志也一片祥和。但你用浏览器一访问转圈、超时、连不上或者直接ECONNREFUSED。给你发消息反馈API 没响应的人已经排到了下班后。我自己的生产环境同样踩过这个坑查了三个小时才意识到问题可能不在代码而在pm2 到底有没有让你那个 Hono 服务真正听起来。这个坑 Hono 用户特别容易掉进去因为 Hono 和 Express 在启动方式上有一处特别容易被忽略的差异Express 有app.listen()而 Hono 返回的app只是一个 fetch 处理函数不主动监听端口。你这层如果没接好pm2 当然觉得进程活着但外面根本敲不开门。这篇文章就把这类问题从头到尾捋清楚从现象判断、逐层排查看到根因分析、修复方案最后附上我现在的生产配置模板。不管你是刚接触 Hono 的 Node 新手还是被 pm2 折磨过的老油条照着这套思路排查一遍基本能把在线但没响应的问题按在地上摩擦。1. 先把没响应这件事拆清楚1.1 现象分类不是所有没响应都是同一个病遇到问题第一步不是改代码而是先精确描述现象。我自己总结过Hono pm2 的没响应至少分三种长相连接被拒绝curl -v直接报Connection refused说明目标端口上根本没有进程在监听。这种最干脆问题往往出在服务没起来、端口写错、或者起了但马上崩了。连接一直挂着不返回curl 卡住不动直到超时。说明 TCP 层也许握手了但请求没有被真正处理或者进程假死、事件循环被阻塞。Hono 的代码里如果有同步死循环、或者某个中间件中await了一个永远不会 resolve 的 Promise就会出现这种玄学。连接成功但返回 404 / 403 / 空响应服务活着、端口也通但路由没对上或者被反向代理、限流挡了。这种情况严格说不算没响应但用户感知是一致的我访问的 API 不通。我在生产环境遇到的是第一种端口上压根没人监听。但排查过程中我顺手把另外两种也验证了一遍因为你只跑一次pm2 list就下结论的话很容易被表象骗了。1.2 理解 pm2 的在线到底是什么意思pm2 本质是个进程守护器它做的事情是把你的 Node 进程拉起来盯着它的存活状态挂了就按策略重启顺便接管日志。但注意pm2 判断进程活着的标准只是这个进程还没有退出它不关心你的服务有没有正常监听端口、有没有成功处理请求。生活化一点pm2 像个宿管他只负责确认你房间灯还亮着、人没跑。至于你在房间里是认真学习还是躺着刷手机他完全不管。你的 Hono 进程如果只是加载了一堆文件、定义了一个app然后因为某个定时器或者数据库连接池的句柄一直没释放进程就会一直活着但根本没有任何端口在监听。所以遇到pm2 show里 status 是 online第一反应不该是服务没问题而应该是进程没死但服务未必在干活。1.3 Hono 启动方式的特殊性它不是一个自带 listen 的框架这里必须多说几句因为太多人在这里栽跟头。Express 的经典写法是const express require(express) const app express() app.listen(3000, () console.log(started))app.listen()会真正创建 HTTP server 并占住端口。但 Hono 的app对象本身只负责给定一个 Request返回一个 Response它是一个标准的 fetch handlerimport { Hono } from hono const app new Hono() app.get(/health, (c) c.json({ ok: true })) export default app这个app设计出来是为了在 Cloudflare Workers、Deno、Bun 这些平台直接用的那些平台会自动调用你的 handler。但在 Node.js 里没人替你调用你必须自己用适配器把它变成真正的 HTTP 服务。最常见的适配器是hono/node-serverimport { serve } from hono/node-server import app from ./app serve({ fetch: app.fetch, port: 3000, hostname: 0.0.0.0, }, (info) { console.log(Listening on ${info.port}) })这段代码才是真正让服务响起来的那一下。我见过太多项目里app.ts写好了、路由全通了但 pm2 启动的入口文件根本没有调用serve()或者入口文件只 export 了app。结果进程被 pm2 拉起来之后没有任何端口监听pm2 因为残余句柄还觉得它活着——完美复刻在线但没响应。2. 排查路线图一层层剥开问题2.1 第一步看日志比看状态更靠谱不管三七二十一先拉日志pm2 logs hono-app --lines 200 --nostream重点看两类东西一是启动时有没有报错二是你自己的服务有没有打印出Listening on ...。hono/node-server支持传一个回调函数在成功监听后执行很多同学会在这里打印日志。如果日志里压根没有这行说明serve()没执行成功或者压根没执行。再配合pm2 describe hono-app看两样关键信息restarts字段如果数值一直在涨说明进程在反复重启。每次重启间隔很短时pm2 可能显示 online但其实服务一直处于刚起来又死了的循环里。script path和exec cwd确认 pm2 启动的到底是哪个文件、工作目录在哪儿。这一步能抓到我想启动dist/index.js结果它启动的是src/index.ts之类的低级错误。2.2 第二步直接探测端口别猜日志看不出来就直接探端口。SSH 到服务器上先监听你程序里配置的那个端口这里假设 3000ss -tlnp | grep 3000如果输出里什么都没有说明没有任何进程在监听 3000问题直接就坐实了。如果输出里有记下对应的 PID 和进程名再验证一下是不是你预期的那个 Node 进程ps aux | grep 你的进程名另一种情况也要注意端口上确实有进程但不是你的进程。我之前就遇到过某个旧服务还占着 3000新的 Hono 服务serve()直接抛EADDRINUSE然后被 pm2 反复重启日志里全是报错但旧服务还在正常响应导致我一度以为 Hono 服务是通的只是有新代码没生效。2.3 第三步分清 localhost 通不通端口有监听之后再判断通不通要分两个层面。先在本机测curl -i http://127.0.0.1:3000/health如果本机通了再从你自己电脑上访问服务器公网 IP 的对应端口。如果本机通、外部不通那基本就是绑定地址或者防火墙/安全组的问题对应hostname: 127.0.0.1和hostname: 0.0.0.0的区别。这里有个容易混淆的点curl http://localhost:3000走的是 IPv6 的::1还是 IPv4 的127.0.0.1取决于你系统的 hosts 配置。所以我建议直接用 IP 来测别用localhost少一层干扰。2.4 第四步查环境变量有没有失踪很多人的 Hono 服务会读PORT、DATABASE_URL、JWT_SECRET这些环境变量。本地跑的时候用.env或者 shell 里 export 了一切正常上了 pm2环境变量没有跟着走服务启动时读不到PORT就默认为空或者用了代码里的兜底值但你访问的是你以为的那个端口自然对不上。pm2 拉起的进程环境跟你 SSH 登录 shell 的环境并不完全一样。你执行pm2 start时 shell 里已有的变量会继承但你后来在.bashrc里加的变量、或者.env文件里的变量pm2 的进程未必能读到。排查时用这条命令直接看进程环境pm2 env 0这个0是 pm2 的进程 ID以pm2 list第一列为准。重点看NODE_ENV和自定义的PORT是否如你预期。2.5 第五步把反向代理和防火墙也盘一遍如果你的服务前面还挂着 Nginx、Caddy 或者云厂商的负载均衡那还要往前端再追一层。我遇到过一个非常隐蔽的问题Nginx 配置的反代目标还是旧服务的端口Hono 服务换了个新端口之后Nginx 一直在往后端旧端口转发新服务这边完全没收到请求表面上就像新 API 没响应。防火墙层面用ufw status或者云控制台里的安全组规则确认目标端口对外的入方向放开了。这一步很多人直接跳过其实非常关键尤其是你本机 curl 通了、但外部访问超时的时候九成是这里。3. 常见根因速查你大概率是这五种情况之一排查过程会筛掉很多枝节最后通常会落在下面这几个根因上。我直接整理成一张表方便你对号入座症状表现根因核心证据修复方向本机 curl 拒绝连接pm2 启动入口文件没调用serve()日志无 Listening 输出ss无端口监听确认入口文件调用serve()且 pm2 指向正确文件本机通、外部不通绑定地址是127.0.0.1ss -tlnp显示监听地址为 127.0.0.1serve()中显式设置hostname: 0.0.0.0请求超时不返回进程假死或事件循环阻塞curl 挂起直到超时进程 CPU/内存异常检查中间件死循环、弹珠式 promise、数据库连接池pm2 状态反复重启端口被占或启动即抛错restarts计数增长日志有报错堆栈清理旧进程或修改端口端口对但路由 404路径前缀不一致 / 反代配置错直接访问后端端口通走域名/网关不通核对 Nginx 转发路径确认 Hono 的 basePath3.1 入口文件没有真正启动服务这个根因前面已经强调过但它值得单独列一节因为你可能觉得自己写了serve()实际上 pm2 启动的是另一个文件。常见误区是项目里有app.ts只定义 Hono 实例和server.ts调用serve()但 pm2 启动命令写的是pm2 start app.ts进程起来了却没有任何端口监听。解决方案很简单要么 pm2 启动server.ts要么在app.ts里直接调用serve()。我个人推荐后一种写法因为部署入口越少越不容易出错。一个文件搞定 Hono 的启动逻辑pm2 的script就永远指向这一个文件。3.2 绑定地址问题导致只有本机能访问serve()如果不显式指定hostname某些版本默认绑在localhost上意味着只有服务器本机自己能访问外部请求直接被内核拒之门外。你在服务器上curl通但别人从公网访问就超时恰恰就是这个原因。稳妥的做法是写死serve({ fetch: app.fetch, port: Number(process.env.PORT || 3000), hostname: 0.0.0.0, })0.0.0.0表示监听所有网络接口这样无论外部走公网 IP 还是内网 IPTCP 都能进来。之后如果有 Nginx 挂在前面再考虑是不是要更严格地限制监听地址。3.3 pm2 的 cluster 模式把端口抢坏了这个坑我用-i参数踩过一次。pm2 支持pm2 start server.js -i 0按 CPU 核心数启动多个实例但默认的 cluster 模式只对标准 Node HTTP server 有奇效对 Hono 这种依赖独立serve()适配器的进程来说直接多开会在同一端口上撞车。具体表现就是第一个实例成功占了端口其余实例全部EADDRINUSE报错重启pm2 列表里一个 online、几个 errored看起来很诡异。就算全起来了如果你没有用共享端口的技术多个进程同时listen同一个端口本来就是不可能的。如果你确实需要多实例正确做法是保持fork模式-i 1或者不传-i外层用负载均衡器转发或者用 Node 原生的cluster模块 HTTP server实现共享端口。对 Hono hono/node-server来说我个人的建议是先单实例跑稳再谈横向扩展。3.4 环境变量缺失导致端口、密钥对不上刚才聊过环境变量这里给一个我真实踩过的例子代码里读process.env.PORT ?? 3000本地.env里配的是PORT8080于是本地一直跑在 8080。部署时 pm2 的 ecosystem 配置里没写PORT服务就默默跑到了 3000。前端同事访问的还是 8080理所当然没有响应。检查时一定记住环境变量要跟着 pm2 的配置走而不是依赖你 SSH 登录时的 shell 环境。用ecosystem.config.cjs里的env字段显式注入才能保证每次启动的一致性。3.5 进程假死或者陷入重启循环有时候ss -tlnp显示端口有监听curl 却一直超时。这是最难受的一种因为表面上一切正常。我那次具体原因是Hono 的一个中间件里await了一个永远不会 resolve 的数据库连接 Promise导致请求处理函数永远挂起但进程本身还活着事件循环也没空转就是卡死。排查这种问题要看进程的 CPU 和内存pm2 monit如果某个进程 CPU 飙高、内存不断上涨往往是代码里有死循环或内存泄漏。如果进程安静得像睡着了一样多半是某个异步任务没回来。遇到这种情况最快的定位方式是看pm2 logs里有没有最后一条日志停在某个位置然后用在可疑代码处加日志的办法慢慢逼近。4. 完整实操记录从没响应到恢复我做了什么这一节我按时间线写尽量还原当时的操作顺序你可以照着走一遍。4.1 复现现场先确认症状到底长什么样当时我的服务器上跑着pm2 list输出大致这样id name namespace version mode pid uptime ↺ status 0 hono-api default 1.0.0 fork 12345 3h 0 online状态 online重启次数 0看起来完美。但curl http://127.0.0.1:3000/health直接拒绝连接。我心里先排除了一部分然后继续看pm2 logs hono-api --lines 50 --nostream日志最后一行是我启动时代码里打的第一行process started但没有Listening on 3000。我心里有数了再执行ss -tlnp | grep 3000输出为空。这时候基本锁定进程活着但根本没有服务在监听端口。于是去看 pm2 到底启动的哪个文件pm2 describe hono-api结果显示script path指向的是dist/app.js这就有意思了。我的项目结构里dist/app.js是编译后的 Hono 实例定义文件真正的服务启动逻辑在dist/server.js。我 pm2 配置里写的是app.js当然不会有人去listen再加上 Node 进程因为hono/node-server初始化时引入的一些 handle 没有退出于是 pm2 认为它 online实际是个植物人进程。4.2 用 ecosystem 配置文件根治发现问题后我没有只改启动命令而是顺手把 pm2 的管理方式从命令行参数换成了ecosystem.config.cjs。这样配置可以纳入 Git 管理每次部署不会因为少打一个参数而出幺蛾子。ecosystem.config.cjs长这样module.exports { apps: [ { name: hono-api, script: ./dist/server.js, instances: 1, exec_mode: fork, watch: false, max_memory_restart: 512M, env: { NODE_ENV: production, PORT: 3000, DATABASE_URL: mysql://user:passhost:3306/hono_db, JWT_SECRET: your-secret-here, }, }, ], }启动方式变成pm2 start ecosystem.config.cjs pm2 save这里几个参数我逐个解释script必须指向真正调用serve()的文件不能指向只定义 Hono 实例的文件。instances: 1exec_mode: fork就是为了绕开上一节说的 cluster 端口冲突。max_memory_restart: 512M是给进程设置一个内存红线超过自动重启。生产环境我强烈建议加上很多假死其实就是内存泄漏导致的。env里显式写清楚所有运行时需要的变量部署机上的.env反而成了兜底。改完之后我重新启动pm2 delete hono-api pm2 start ecosystem.config.cjs pm2 logs hono-api这次日志里出现了那一行期待已久的Listening on http://0.0.0.0:3000。4.3 验证清单确认服务真的恢复了很多人改完配置重新启动后 curl 一下通了就完事了。但别急着下班我建议按下面这张清单过一遍防止还有暗雷ss -tlnp | grep 3000确认监听地址是0.0.0.0:3000而不是127.0.0.1:3000。curl http://127.0.0.1:3000/health本机探测确认返回 JSON。curl http://服务器内网IP:3000/health确认局域网内可达。从自己电脑访问http://公网IP:3000/health确认外部可达。这一步要提前确认安全组放行了 3000 端口。pm2 status确认进程稳定重启次数没有悄悄上涨。pm2 save把当前进程列表持久化防止服务器重启后 pm2 忘了拉起服务。我当时在第三步就发现内网不通排查了一圈才发现安全组没有放行 3000。这个放行操作不在代码范围但属于访问 API 没响应的常见外因别忽略。4.4 我的 Hono 生产入口长这样既然聊到这里我把当前项目里真正用来做生产入口的代码贴出来抛砖引玉// src/server.ts import { serve } from hono/node-server import { app } from ./app const port Number(process.env.PORT || 3000) const hostname process.env.HOST || 0.0.0.0 serve( { fetch: app.fetch, port, hostname, }, (info) { console.log([hono] listening on http://${hostname}:${info.port} (pid: ${process.pid})) } )注意这里我用的是命名导出{ app }而不是默认导出default。这是个细节但很实用当你从app.ts里同时导出app和某个需要在启动时初始化的工具函数时命名导出不容易因为模块编译方式不同CommonJS 还是 ESM而出现app.default是undefined的尴尬。5. 几条我拿时间换来的避坑心得5.1 排障心法日志先行怀疑一切我踩过一次之后总结出一句话pm2 的 online 状态只能证明进程没死不能证明服务在服务。任何没响应的问题第一件事永远是看日志第二件事是确认端口。别上来就改代码加 try/catch那样大概率越改越乱。日志要看原始输出不要只看 pm2 的 status。pm2 logs里如果出现EADDRINUSE、MODULE_NOT_FOUND、SyntaxError那答案已经写在脸上了。如果日志安静得可疑再考虑是不是假死。5.2 端口和监听地址永远显式声明我现在的代码里port和hostname都是从环境变量读带默认值但保证赋值的时候一定是显式的。不要依赖serve()的默认绑定行为不同版本的hono/node-server对默认 host 的处理不完全一致写清楚0.0.0.0能省掉一次线上事故。另外服务器上如果同时跑多个 Node 项目我建议每个项目用独立端口并且在 pm2 的 name 里带上端口号比如hono-api-3000。这样ss -tlnp查端口、看到多个 Node 进程时能一眼分辨谁是谁。5.3 pm2 的常用命令和参数值得固化到记忆里平时我最常用的就这几条基本覆盖了日常运维pm2 list # 查看所有进程状态、重启次数 pm2 logs name --lines 100 # 看最近日志 pm2 env id # 查看某个进程的环境变量 pm2 restart name --update-env # 重启并刷新环境变量 pm2 reload name # 零停机重载配合 cluster 模式使用 pm2 save # 保存当前进程列表 pm2 startup # 生成开机自启脚本restart和reload的区别很多人不知道。restart是杀掉再拉起会短暂断服务reload是逐个实例替换需要 cluster 模式才能发挥真正作用。单实例 fork 模式下reload效果其实和restart差不多。5.4 Node 版本和编译产物也是隐藏变量最后提醒一个不太起眼但真实存在的坑Hono 对 Node 版本有要求hono/node-server建议在较新的 Node LTS 上跑。如果你的服务器上 Node 版本太老某些 API 不存在服务可能在启动阶段就抛错。还有如果项目用 TypeScript 编译到dist一定要保证 pm2 启动的是编译后的dist文件而不是直接用tsx跑源码。生产环境跑源码头疼的是服务器没装tsx、版本不一致、编译缓存之类的问题全来了。老老实实npm run build之后让 pm2 指向dist里的入口文件干净利落。我个人现在对这个组合的体会是Hono 本身很轻很稳问题几乎都出在启动姿势上。只要你把谁负责监听端口、监听哪个端口、以什么地址监听这三件事搞得明明白白pm2 Hono 这套组合可以非常省心。如果你也遇到过类似的问题建议先按第 2 节的路线图走一遍把日志、端口、环境变量、防火墙四层都确认完大概率问题就自己浮现了。
RELATED

相关推荐

Python网络舆情分析系统实战:从环境搭建到情感分析可视化全流程

Python网络舆情分析系统实战:从环境搭建到情感分析可视化全流程

简介:这是一套基于Python技术栈的Web网络舆情分析系统完整项目资料,面向具备一定编程基础、希望深入Web开发与数据库集成的开发者及高校学生,可作为课程设计、毕业设计或技术进阶的实践参考。资源包共290个文件,约93.5MB&#xff…

📅 2026/9/28 17:17:50
YOLOv5养殖场肉鸡健康状态检测:权重与数据集落地方案

YOLOv5养殖场肉鸡健康状态检测:权重与数据集落地方案

简介:本资源面向从事智慧养殖与计算机视觉的开发者、学生及科研人员,提供YOLOv5养殖场肉鸡健康状态检测的权重文件与配套数据集,用于识别肉鸡异常与正常两类状态,可直接支撑目标检测训练与推理任务。压缩包共983个文件&#xff0c…

📅 2026/9/28 17:17:50
Windows下Codex本地代理切换失败修复:WinBridge Recovery开源工具

Windows下Codex本地代理切换失败修复:WinBridge Recovery开源工具

1. 从一个让人抓狂的报错说起如果你最近在 Windows 上折腾 Codex 相关的开发工具链,大概率见过这个让人血压飙升的报错:cc switch local proxy failed while handling codex endpoint /responses。这个报错最恶心的地方在于,它不告诉你具体哪…

📅 2026/9/28 17:17:50
MORE NEWS

更多资讯

📰

V1项目封装复盘:PCB封装、接口封装与AI流式输出

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

为什么大型储能需要边缘 AI 协调控制器?RK3588+FPGA 高速采集方案

为什么大型储能需要边缘AI协调控制器?RK3588FPGA高速采集方案标签:# 储能协调控制器 #RK3588 #FPGA #边缘 AI #储能安全 #AIDC 数据中心储能 阅读对象:储能系统集成商、硬件研发工程师、BMS/EMS 开发、新能源方案选型前言随着大型集装箱储能、…

📰

RKNN Toolkit V1.7.3模型转换深度解析:ONNX到RKNN的算子映射与量化校准

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

从“替代人的手”到“替代人的脑”:工业AI正在跨越哪道分水岭?

一个工厂的“AI管家”,管的是什么?2026年9月,湖北某车间。生产线全速运转,却少见操作工的身影。AI系统全面接管生产流程后,这个百万吨级工厂年增效近2000万元。同月,容知日新发布“观星OS”设备智能运维操作…

📰

ESP32锂电池电压监测:分压电路设计与ADC校准实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

深度解读Work Agent长程任务执行的底层机制与落地边界

过去几年AI交互的形态发生了清晰的迭代路径,最早的单轮问答模式下,用户输入一个明确的短指令,AI返回对应的直接结果,整个交互链路完全由用户的提问质量决定输出上限。随后多轮对话能力的普及,让AI可以承接上下文信息&a…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬