企业级 AI 网关这种 Spring Boot 后端加 Vue 前端分离架构的项目,上线的核心不是把代码跑起来,而是把 Nginx 反向代理、SSE 长连接、多模型服务调用这三条链路在服务器上一次性打通。我们项目采用「后端 jar 包 + systemd 守护」与「前端 dist 静态资源 + Nginx 托管」的组合,中间用同一个 Nginx 做流量入口,把 /api 转发到后端、把静态资源直出,并在网关层关闭响应缓冲以保证对话流式输出不卡顿。这套流程从准备环境到可回滚发布,完整走下来大约分七个阶段,下面按实战顺序展开。
一、上线前的环境与物料准备
部署动作本身只占整个上线过程的一小部分,真正的体力活在环境一致性。先做两件事:锁定运行时版本,准备生产配置。
- 服务器统一装 OpenJDK 17 与 Node.js 18 LTS,本地开发环境与线上保持一致,避免编译版本与运行时版本错位;
- 把数据库、Redis、上游模型服务的连接串抽进
application-prod.yml,密码通过环境变量注入,不写死在仓库里; - 在安全组放行 80、443 与 SSH 端口,后端端口不出现在公网,只允许内网访问。
生产配置里最容易漏的是时区与连接池参数。网关要访问多个模型服务,超时与连接数直接影响吞吐:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://10.0.0.8:3306/ai_gateway?useUnicode=true&characterEncoding=utf8
data:
redis:
host: 10.0.0.9
port: 6379
ai:
model:
openai:
base-url: ${OPENAI_BASE_URL}
api-key: ${OPENAI_API_KEY}
qwen:
api-key: ${QWEN_API_KEY}
二、后端打包与进程守护
后端用 Maven 打成可执行 fat-jar,内嵌 Tomcat,不需要额外装应用服务器。打包命令固定为:
mvn clean package -DskipTests
产物 target/ai-gateway.jar 上传到 /opt/ai-gateway/。直接 nohup java -jar 跑起来也能用,但终端一关、服务器一重启,进程就没了。生产环境用 systemd 托管,崩溃自动拉起,开机自启,日志归 journald 统一管理:
[Unit]
Description=AI Gateway Backend
After=network.target mysqld.service redis.service
[Service]
User=gateway
WorkingDirectory=/opt/ai-gateway
ExecStart=/usr/bin/java -Xms1g -Xmx2g -jar /opt/ai-gateway/ai-gateway.jar --spring.profiles.active=prod
Restart=always
RestartSec=5
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target
JVM 堆大小按并发连接数调整。网关持有 SSE 长连接,连接对象本身占内存不高,但请求线程与异步任务执行器要留足余量,Restart=always 加上健康检查脚本,可以在进程假死时兜底。
三、前端构建与 Nginx 托管
前端是 Vue 项目,打包前把 API 基地址改到同域相对路径 /api,由 Nginx 统一转发,规避跨域问题。构建命令:
npm run build
产物 dist/ 上传到 /var/www/ai-gateway-web/,权限收给部署用户。Nginx 站点配置承担两件事:静态资源直出与 API 反代。
server {
listen 80;
server_name gateway.example.com;
root /var/www/ai-gateway-web;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
try_files 回退到 index.html 是 Vue 路由的 history 模式必需配置,否则刷新二级页面直接 404。
四、SSE 流式接口的反向代理专项配置
这一步是 AI 网关部署与普通 CRUD 项目的关键差异点。对话接口走 SSE,响应体是边生成边推送的事件流。Nginx 默认开启响应缓冲,会把上游数据攒到一定量才发给客户端,表现就是聊天窗口”憋半天出一大段”,和逐字流出的预期完全相反。
在 /api/ 的 location 里追加三条:
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
proxy_set_header X-Accel-Buffering no;
add_header Cache-Control "no-cache";
}
同时后端在响应头里显式声明 Content-Type: text/event-stream 与 Cache-Control: no-cache,双重保证事件不被中间层缓存。proxy_read_timeout 从默认 60 秒调大到 300 秒,因为大模型生成长文时单次间隔可能超过 60 秒,心跳包也能在这个维度兜底。
流式事件格式也要在生产环境复查一遍:每条事件以 data: 开头、空行结尾,结束标记统一用 [DONE],异常时先推一条错误事件再关连接。这些约定在开发环境写代码时就已固定,上线时重点确认代理层没有改动事件内容。另外网关接入了多家模型服务,出站请求需要走代理或加白名单,部署时把各家的域名与证书一并配好,避免线上环境 DNS 或证书校验不一致导致首次调用失败。
五、HTTPS 与上线前自检
生产环境一律上 HTTPS。证书用 Let’s Encrypt 签的免费 DV 证书,Certbot 一键签发并配置自动续期,每周一凌晨执行一次续期任务。443 端口上同时挂好前端页面与 API 反代,80 端口 301 跳转。
上线前自检按这个顺序执行:
- 后端单独验证:
curl -i http://127.0.0.1:8080/actuator/health返回 UP; - SSE 链路验证:用
curl -N直连后端流式接口,确认事件按data:分片逐条到达; - 经过 Nginx 复测同上,确认缓冲已关闭、事件实时透传;
- 前端页面走 HTTPS 访问,登录、对话、历史记录三块功能逐项过一遍;
- 观察日志与内存曲线,确认无 OOM 风险后把流量完整切过来。
六、三种部署方式的取舍
同一个项目,我们评估过三套部署方案,最终选了 systemd 直跑,理由写在表格里:
| 部署方式 | 上手成本 | 运维能力 | 资源占用 | 适用阶段 |
|---|---|---|---|---|
| nohup 后台运行 | 最低 | 弱,重启即失联 | 低 | 联调、试运行 |
| systemd 服务托管 | 低 | 中,支持自启与崩溃拉起 | 低 | 单机生产 |
| Docker 容器化 | 中 | 强,配合 Compose 编排多服务 | 中 | 多服务、迁移频繁 |
当前网关服务数量有限,单机 systemd 足够。如果后续把网关、MySQL、Redis 全部容器化并引入 CI/CD,再整体切到 Docker Compose,部署脚本一次性写好。
七、发布与回滚
发布用「蓝绿切换」思路:新 jar 先以 --server.port=8081 起在旁路,压测与冒烟通过后,改 Nginx 的 proxy_pass 指向 8081,观察十分钟无异常再停掉旧进程。回滚就是改回 8080,全程不动数据库结构,秒级完成。前端同理,dist 目录用时间戳目录存放旧版本,发布失败时改 Nginx 的 root 指向即可回退。
压测环节专门模拟了并发对话场景:用脚本同时发起 30 路 SSE 连接,重点看两个指标,一是事件到达间隔是否稳定,二是后端线程池与连接数是否被打满。压测期间出现过一次 Tomcat 默认线程池耗尽导致新连接排队的问题,后来把异步任务执行器换成有界队列并设置合理拒绝策略,才把并发连接数提上去。这类问题只会在流式长连接场景暴露,普通接口压测测不出来。
常见问题(FAQ)
Q1:前端刷新页面报 404 是什么原因?
Vue history 路由在 Nginx 缺少 try_files 回退规则,补上 try_files $uri $uri/ /index.html 即修复。
Q2:SSE 接口不流式、一次性返回怎么办?
Nginx 默认缓冲响应,在 location 里加 proxy_buffering off 并设置 X-Accel-Buffering: no。
Q3:服务重启后端口被占用怎么处理?
先 netstat -tlnp 定位占用进程,确认是残留旧进程后停掉,再启动新服务。