一场计划中的客户演示,却因为一次看似寻常的部署彻底搅乱了节奏。supermarket--bot 是一款用 Go 编写的 WhatsApp 订单机器人,面向小微企业,可直接在 WhatsApp 内处理完整的订单流程——消息通过 Twilio 收发、支付走 Paystack 实时结算、商品图片托管在 Cloudinary,再配上一个管理员后台。就在为一家餐厅客户做演示的前一晚,主开发者按惯例向 Railway 推送了一次常规部署,没想到应用死活起不来:每次部署都在健康检查上卡死,随后容器被无情杀死。应用本身没有报错,就是一直不健康。
起初所有人都瞄向了网络和端口绑定。毕竟 Docker 加 Railway 的这种组合,EXPOSE 与动态端口的配合是出了名的坑。容器需要暴露端口,Railway 则会注入 $PORT 环境变量。开发者的第一反应是修改 Dockerfile,把写死的 EXPOSE 8080 改成 EXPOSE $PORT,结果没用;接着干脆删掉 EXPOSE 指令,期待 Railway 在运行时自己搞定——可问题依旧。于是又新建了一个 railway.toml 配置文件,显式声明健康检查路径,再在代码里把监听地址硬绑到 0.0.0.0 上。甚至额外加了一条日志,专门打印 Railway 注入的 $PORT 值,想看看平台到底给的是什么端口。这一圈操作下来,日志清清楚楚显示端口一直是正确的——8050、8080,Railway 的注入毫无差错。这时候网络端口的猜想才彻底被推翻,已经过去了好几个小时。
转机出现在一个被忽略的角落:健康检查的超时窗口。Railway 默认的健康检查超时是 30 秒,意味着从容器启动到必须返回 200 响应,只有半分钟。而 supermarket--bot 在冷启动时要连接到托管在云端的 PostgreSQL 数据库,建立连接、执行迁移、准备连接池,这一套初始化流程在资源紧张或网络抖动时,轻松突破 30 秒。容器明明还在埋头启动,健康检查却已经判定它“失联”,随后毫不留情地执行重启。问题根本不是应用不可达,而是启动还没结束就被强制终止了。
真正的修复完全落在配置上:将 healthcheckTimeout 从 30 秒先调至 60 秒,通过后为了保险再拉长到 120 秒,并配上 failure-restart 重启策略。改完推送,部署首轮就顺利变成绿色。但这次故障已经让客户演示整整推迟了两天。
复盘整个过程,时间几乎都浪费在误判的方向上。应用依然可以稳健工作,只是冷启动的开销被平台默认值挡住了。事后,开发者用 sentry-go 的分布式追踪对启动阶段的数据库连接进行了打点监控,确保下一次不会再用猜的方式去定位类似延时。一次没有代码改动的修复,却比任何代码改动都更值得记录——它提醒着,在容器化的世界,基础设施的计时器有时比应用逻辑本身更先决定生与死。
热门跟贴