前三篇我们处理的对象都是「一堆 HTML / CSS / JS」——构建产物是文件,部署动作是把文件放进 Nginx 目录。这一篇要迈出新的一步:当部署对象从「静态文件」变成「需要运行时、依赖库、外部服务的后端进程」时,前三篇的工具还够用吗?哪里不够?
这一篇围绕三个新问题展开:
- 在哪构建:服务器构建,还是本地构建后传产物?
- 在哪跑:怎么启动一个持续运行的后端进程?
- 怎么保活:进程死了谁拉起来?
本质一致:前端后端都是「服务器进程」
前 3 篇看似在讲「静态文件」,其实那个进程一直都在——它叫 Nginx,是的标准预装件,开机自启、监听 80 / 443 端口,浏览器请求来了 Nginx 读 dist/ 里的文件返回。我们没显式讨论「进程」,是因为 Nginx 把这件事藏起来了:它几乎不会因为你的代码内容挂掉,存在感很低。
后端不一样:进程换成你自己启的 Java / Python 进程。但链路结构没变——都是「用户浏览器 → 服务器上一个进程在监听端口 → 处理后返回响应」。区别只是:前端的进程是云服务器预装的 Nginx,后端的进程是用户自己拉起的 Java / Python。
链路看起来是这样:
前端:浏览器 → Nginx(:80/443,公网入口) → 读 dist/ 文件 → 返回 HTML/CSS/JS
后端:浏览器 → Nginx(:80/443,公网入口) → 反代到 :8080 → Java/Python 进程 → 返回 JSON
↑
这一段由 Nginx 维护
后端进程一般监听内部端口,不直接暴露公网客户端只跟 Nginx 通信——它根本不知道后端进程的端口是什么。后端进程死了,客户端看到的是「502 Bad Gateway」,但它不知道是 Nginx 挂还是后端挂。
这样看,前端和后端在「部署」这件事上的差异,本质只有两条:
- 进程是不是预装件——Nginx 是云服务器默认装的,几乎不用管;后端进程要自己拉起、自己保活。
- 进程挂了用户怎么感知——Nginx 挂了是基础设施问题;后端进程挂了是应用层问题,运维要先怀疑后端、再去查 Nginx 日志。
依赖上看,后端比前端厚一叠:除了 Nginx 这层「系统级」依赖,还要叠加 pom.xml / requirements.txt 这层「业务级」依赖,版本要对齐、编译环境要齐备、跨平台要一致。这才是这一篇真正要展开的事——进程从「预装件」换成「用户自己启的」,依赖从「系统级」叠到「业务级」。
在哪构建:双栈对比
03 篇给出的范式是「git push 源码,服务器 git pull + 构建 + 落盘」。这条范式仍然适用——但「构建」这一步在后端栈里要复杂得多。用两个最常见的栈对比看:
| 步骤 | Spring Boot(Java) | Flask(Python) |
|---|---|---|
| 依赖声明 | pom.xml(Maven) / build.gradle(Gradle) | requirements.txt 或 pyproject.toml |
| 依赖管理 | mvn dependency:resolve / gradle dependencies | pip install -r requirements.txt |
| 构建产物 | 可执行 jar / war | 源码 + 虚拟环境 / pip freeze 锁文件 |
| 启动命令 | java -jar app.jar | python app.py(生产用 gunicorn app:app) |
| 配置文件 | application.yml / application.properties | 环境变量 / config.py |
| 外部依赖 | MySQL 驱动、Redis 客户端、HTTP 客户端等 | 同 |
两类构建路线,相同的痛点
路线 A · 服务器构建:git pull 源码 → 服务器上 mvn package / pip install → 启动。
- Maven 首次下载依赖要十几分钟;
pip install psycopg2这类带 C 扩展的包需要服务器装libpq-dev。 - 服务器 CPU/内存被构建过程占用,可能影响正在运行的线上服务。
- 本地是 Java 17,服务器是 Java 11——构建直接失败。
路线 B · 本地构建后传产物:本地 mvn package 出 jar / 本地 pip install 出 wheel → 上传 → 服务器启动。
- Spring Boot 的 jar 相对干净(自带依赖),跨机器跑成功率高。
- Flask 的
pip install产物是针对本地 macOS 编译的.so文件,上传到 Linux 服务器常常GLIBC_2.28 not found。 - 「在我电脑上能跑」—— ≠ 服务器环境,本地产物 ≠ 服务器能用的产物。
两种路线都解决不了所有问题。Spring Boot 因为自带 fat jar 比 Flask 更「可移植」,但两类栈共享同一个根本问题:构建动作和运行环境是割裂的——我们只传了代码和依赖,没传「运行环境本身」。
运维视角:三类被低估的痛点
上面那两类痛点——「构建时长」「跨平台产物不一致」——更多是技术视角。真实部署还会撞上运维视角的三类问题,跟栈无关,跟「在哪构建」有关。
痛点 1 · 服务器会变「脏」
只要走「服务器构建」路线,服务器就不可避免地变脏:
- 装 Java?装
maven,配置JAVA_HOME和MAVEN_OPTS。 - 装 Python?装
pip,可能再装pyenv、再装virtualenv。 - 装 Node?装
nvm,挂一堆 npm 全局包。
更麻烦的是版本对齐:本地 Java 17,服务器也是 Java 17——这条不靠自动工具很难守住。半年后本地的项目换到 Java 21,服务器没升,构建直接失败。生产服务器成了「另一台开发机」,还要小心不要让 apt 升级顺手把 JDK 也升级了。
「在服务器上跑业务进程」和「在服务器上装构建工具」是两类完全不同的责任——混在一起,服务器就开始变得不可控。
痛点 2 · 构建是重资源任务,进程是轻资源任务
服务器的工作是运行一个常驻进程——Spring Boot 应用监听 8080,每秒处理几十个请求,CPU / 内存占用稳定。
构建不一样。mvn package 第一次要下载几百 MB 依赖、还要编译 Java 源码;pip install numpy 要从源码编译 C 扩展。编译 / 链接是 CPU 密集 + 内存密集——短时间把 CPU 打满、内存吃光。
如果在同一台服务器上边跑线上服务边构建:
- 构建期间响应延迟飙升
- 极端情况下 OOM(Out of Memory)导致应用进程被内核杀掉
- 更隐蔽:构建过程产生的临时文件、缓存目录、
target//.venv/等占用磁盘
把构建和运行混在同一台机器上,短期能跑,长期必出问题。
痛点 3 · 网络环境不对等
「本地能装的东西,服务器不一定能装」——这不是机器差异,是网络环境差异:
- 本地(Mac / 办公网):访问
docker.io、registry.npmjs.org、repo.maven.apache.org、pypi.org通常很顺畅。 - 云服务器(特别是国内云):上面那些源常常慢、不稳、甚至被墙。要么挂代理,要么换镜像源,要么干脆「构建在本地做,产物传上去」。
这意味着「服务器构建」路线在网络层就多一道门槛——除非你提前配好镜像源 / 私有仓库 / 代理,否则 mvn package 等上十几分钟是常态。
三类痛点的共同方向
| 痛点 | 表面症状 | 根本原因 |
|---|---|---|
| 服务器变脏 | 包管理器冲突、版本不一致 | 构建工具和运行时共用一台机器 |
| 构建打满资源 | 线上服务卡顿 / OOM | 构建任务和运行任务共享资源 |
| 网络不对等 | 依赖下载慢 / 失败 | 服务器网络环境受限 |
三类痛点指向同一个方向:构建动作不该和运行动作挤在同一台服务器上。要么把构建搬到另一台专门的「构建机」,要么把构建连同它的运行环境一起打包、让运行时容器化——下一篇的 Docker 就是后者:把「代码 + 依赖 + 运行时」打成一个镜像,服务器只负责跑这个镜像,不再装任何构建工具。
在哪跑:进程要「持续活着」
前 3 篇里那个「进程」叫 Nginx——云服务器预装件,开机自启,几乎不会因为你的代码而死。后端进程不一样:是你手动启的,要监听内部端口、要持续响应、要能被重启。它对客户端不可见——客户端只看到 Nginx,看不到 :8080 上的 Java 进程。
# Spring Boot(监听内部端口 8080;不直接暴露公网,由 Nginx 反代进来)
java -jar app.jar
# Flask(生产用 gunicorn;同上监听内部端口,不直接暴露公网)
gunicorn -w 4 -b 0.0.0.0:8000 app:app启动之后还要管这些事:
| 维度 | 前端(Nginx) | 后端(Java/Python 进程) |
|---|---|---|
| 端口 | 80 / 443(公网) | 8080 / 8000(内部,由 Nginx 反代进来) |
| 客户端能不能直接看到进程 | 能(直接打 Nginx) | 不能(必须经过 Nginx 反代) |
| 进程挂了用户怎么感知 | Nginx 自身 502 | Nginx 502,但 Nginx 还在——运维要先怀疑后端 |
| 重启策略 | 替换文件即可 | kill 旧进程 → 启动新进程 → 健康检查 |
| 配置变更 | 极少 | DB 连接串、密钥、特性开关(环境变量或配置文件) |
| 日志 | Nginx access log | 应用 stdout + 框架日志(Spring Boot / Flask) |
| 启动慢 | 无 | JVM 启动 5–15 秒;Flask 几乎瞬时 |
| 内存管理 | 无 | JVM 堆内存 -Xmx、Python 进程数 |
Spring Boot 独有:
- JVM 启动慢、预热慢;要理解 classpath、堆内存、GC。
- fat jar 让「跨机器传产物」这条路线变得可行——这是 Spring Boot 的天然优势。
Flask 独有:
- Flask 自带的
app.run()是 dev server,不能用(性能差、稳定性差、无并发处理)。 - 生产用
gunicorn(同步)或uvicorn(异步 ASGI)这类 WSGI / ASGI server。
怎么保活:进程死了谁拉起来
这是这一篇的新维度,前 3 篇根本没碰到。
# 临时调试:终端关了进程就死
nohup java -jar app.jar &
# Linux 主流:服务器开机自启 + 失败自动重启
systemctl enable --now myapp
# 容器化(后续篇章):容器即「带运行时的产物」
docker run -d --restart=unless-stopped myapp:latest这一篇点到为止——给一个最小的 systemd unit 样例,让读者知道「保活」是个真问题,但完整方案是后面的内容。
/etc/systemd/system/myapp.service(Spring Boot 示例):
[Unit]
Description=My Spring Boot App
After=network.target
[Service]
User=ubuntu
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
Restart=on-failure
RestartSec=5
Environment=SPRING_PROFILES=prod
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myappFlask 用 gunicorn 的 systemd unit 结构一样,只是 ExecStart 换成 gunicorn ...。保活这一层一旦加上,「部署」就从「传文件」升级成了「传文件 + 拉起服务 + 失败自愈」三件事。
闭环回到 02 节的范式
这一篇更准确的定位其实是「范式显式化」——前 3 篇默认掉的隐含项,现在要变成显式项:
- 前 3 篇的 (公网验证通过)早就隐含「Nginx 进程持续运行」——只是那时候的进程是预装件,我们没显式讨论。
- 这一篇的 把 拆开成两层:
- :Nginx 等基础设施层仍然存活(公网入口能响应)
- :后端应用进程也存活(Nginx 反代得到正常响应,不再 502)
写成函数形式:
- :后端源码空间(Spring Boot / Flask,含依赖锁文件
pom.xml/requirements.txt) - :
git push源码(与上一篇一致) - :服务器动作链——和 比起来,每一步的「内部」变重了:拉源码 →
mvn package/pip install→ 启动 JVM / Gunicorn → systemd 拉起 → 健康检查 - :从单一的「公网通」拆成「Nginx 通 + 后端进程通」两层
没比 多什么「本质」类型,但 多了一层。后端进程死了 → Nginx 502 → 失效 → 失效——「保活」是应用层责任,但用户感知和基础设施层同一张脸。这就是为什么这一篇要新增「进程保活」这一段动作:动作结构没变,结果的内涵扩了一层。
和 02 节呼应: 一直在升级, / 一直在扩容,但每次都是一次平稳的扩容——前 3 篇默认掉的隐含项,被显式地纳入范式。
小结
这一篇更准确的定位是范式显式化:前端和后端在「服务器进程」这件事上是同一个东西——区别只在于前端的进程是云服务器预装的 Nginx,后端的进程是用户自己启的 Java / Python。链路结构没变(用户 → Nginx → 处理 → 返回),变的只是 Nginx 后面那个进程是什么、谁负责拉起、谁负责保活。
后端相比前端要新管三件事:构建策略(构建动作在哪做)、进程生命周期(启动、监听、重启)、进程保活(死了谁拉起)。Spring Boot 自带 fat jar 让产物更可移植,Flask 跨机器则要看 Python 解释器和系统库的脸色——但两类栈的共同痛点都是「代码 / 依赖 / 运行环境割裂」。
下一步进入 第 05 篇:当这三者都要打包过去、且要保证任何机器都能跑时,容器化登场——Docker 把「运行环境」一并打进镜像,从此「在我电脑上能跑」变成「在任何机器上能跑」。
思考
- Spring Boot 的
java -jar app.jar和 Flask 的python app.py都启动了,但哪一个在生产环境不应该用?为什么? - 服务器上
pip install psycopg2失败提示缺libpq-dev,这属于「服务器构建」还是「本地构建」路线的问题? nohup java -jar app.jar &和systemctl start myapp有什么区别?服务器重启后谁会活下来?- 这一篇比 03 篇的部署动作多出了哪一类新动作?