ARTICLE DETAIL

资讯详情

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

自研前后端项目部署全流程:Linux+Nginx+systemd实战指南

自研前后端项目部署全流程:Linux+Nginx+systemd实战指南 部署这件事说复杂不复杂说简单也真不简单。尤其是自研的前后端项目代码写完只是第一步真正让项目跑在服务器上、稳定对外提供服务往往才是团队最容易翻车的环节。我自己经手过好几个从零搭起来的项目部署流程过程中踩过不少坑也沉淀下来一套相对成熟的打法。这套流程用到的技术其实都很基础Linux服务器、Nginx、systemd、Java/Node环境、MySQL/Redis这类常规组件但把这些东西组织成一条稳定可复用的部署链路才是核心难点。今天就把我这套自研前后端项目的部署流程完整拆开讲讲适合那些后端接口和前端页面都要自己部署的小团队参考也适合刚接触部署的开发者照着落地。先交代一下项目背景方便大家对号入座。这套部署流程针对的是典型的前后端分离项目后端是一套Spring Boot应用负责提供REST API前端是Vue或React构建出来的静态资源通过Nginx托管同时Nginx承担反向代理的角色把/api开头的请求转发给后端服务。数据库用MySQL缓存用Redis服务器是Ubuntu 22.04。这个组合可以说是国内自研项目里最常见的标配了跑通这一套换到其他技术栈也只是细节差异整体思路完全通用。1. 部署思路与整体架构设计1.1 核心思路一条链路串起整个部署流程很多新手部署项目时习惯“哪里不会配哪里”今天装个数据库明天配个Nginx东一榔头西一棒槌最后项目能跑起来但没人说得清整个请求链路是怎么走的。我自己的做法是反过来先在纸上把整条链路画出来再按链路去准备环境和配置。这条链路其实很简单用户浏览器 - 域名DNS解析 - 服务器IP - Nginx443端口HTTPS ├── 静态资源直接返回前端构建产物 └── 反向代理 /api/* - http://127.0.0.1:8080 - Spring Boot - MySQL / Redis为什么这样设计因为Nginx处理并发静态文件的能力非常强而且作为统一入口可以在这一层集中解决HTTPS证书、请求转发、日志记录、限流等通用问题。后端服务则专心处理业务逻辑不需要关心前端资源怎么分发。这和开一家店很像——前端是门面后端是后厨Nginx就是那个负责招待客人的前台客人直接面对前台前台再决定是端菜还是找后厨。画完链路图之后所有部署工作的顺序就变得非常清晰先准备底层环境操作系统、运行时、数据库再部署后端服务然后构建前端资源并配置Nginx最后接入HTTPS完成收尾。每一层都只依赖它下面的层出问题时排查路径也明确——从用户浏览器开始一层层往下找问题即可。1.2 为什么选择Nginx systemd这套组合部署方案其实有很多种选择可以用Docker容器化可以用Tomcat直接跑JSP也可以让前端用Node进程托管。但我实际用下来对于大部分自研项目Nginx systemd这套“裸金属”组合往往是最省心、最透明的。systemd是Ubuntu自带的进程管理工具用它来守护Java进程好处是配置简单、崩溃自动重启、开机自启、日志统一交给journalctl管理。这比自己在后台nohup java -jar xxx.jar 加一堆shell脚本要正规得多。比如后端服务半夜内存溢出崩了systemd会在几秒内自动重启服务用户几乎感知不到这就是进程守护的价值。Nginx的选择则更多是基于性能和配置灵活性的考量。一个前端构建产物通常就是一堆静态文件Nginx处理这类请求可以做到几十毫秒内响应而Node.js或者Java应用服务器去托管静态文件则要消耗更多CPU和内存资源。更不用说Nginx的反向代理配置非常成熟负载均衡、WebSocket支持、缓存、压缩这些功能都有现成的配置项不需要额外造轮子。那什么情况下才需要引入Docker呢我的判断标准是如果项目涉及的服务特别多比如有消息队列、定时任务、微服务网关等等服务间依赖关系复杂或者需要频繁在不同环境间迁移这时候Docker才能体现出它的价值。单就一个Spring Boot后端加一个前端静态站这种规模直接用原生命令部署反而少了一层容器抽象带来的排查成本。1.3 服务器环境规划与资源评估做事之前先规划好“地皮”和“水电”部署也一样需要先评估服务器资源和目录结构。我先给出一个通用的资源评估参考2核4G内存的云主机对中小规模的自研项目来说是够用的基准线。以我这边的实际项目为例服务器配置是2核4G40G系统盘额外挂载了50G数据盘。2核主要考虑的是后端服务编译和运行时的CPU消耗前端构建其实是在本地或CI上完成的不会在服务器上跑npm build这样能大幅降低服务器压力。4G内存跑Spring Boot加MySQL加Redis有些紧张所以要精打细算JVM堆内存设置在1GB到1.5GBMySQL的innodb_buffer_pool_size控制在512MBRedis保持默认配置即可。目录规划是我特别想强调的一环很多踩坑事故都是目录混乱导致的。我习惯用一套固化的目录结构/data/app/backend后端jar包和配置文件/data/app/frontend前端构建产物/data/logs集中存放Nginx和应用日志/data/backup数据库备份和工作目录备份/data/sslHTTPS证书文件目录一旦固定脚本、日志、备份的路径就全部确定下来部署流程才真正具有可复制性。2. 环境准备与基础组件搭建2.1 服务器基础初始化拿到一台新服务器第一件事不是装软件而是做基础安全加固和系统更新。我见过很多项目上线后被人扫描端口、爆破密码的大多是因为初始环境太“裸奔”。首先用root登录创建一个具备sudo权限的普通用户比如部署用户叫deploy。生产环境不建议直接用root跑任何应用服务。原因很好理解如果Java进程被利用执行了系统命令root权限意味着攻击者可以直接控制整台机器而用一个受限的普通用户跑服务即使出事也只是影响该用户目录下的文件。创建用户的命令如下# 以root执行 adduser deploy usermod -aG sudo deploy su - deploy然后是系统更新和基础工具sudo apt update sudo apt upgrade -y sudo apt install -y curl wget git vim ufw接下来配置防火墙我习惯用UFW只开放必要的端口22SSH、80HTTP、443HTTPS其他端口一律拒绝外部访问。后端8080端口不需要对外开放因为Nginx通过内网反向代理访问它外部用户只跟Nginx打交道。这点至关重要如果8080端口暴露出去别人就可以绕过Nginx直接访问后端接口一些安全策略也形同虚设。sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable2.2 MySQL与Redis部署数据库建议直接用apt安装稳定且维护方便。MySQL 8.0在Ubuntu 22.04上有官方源支持安装过程没什么特别之处sudo apt install -y mysql-server sudo systemctl enable mysql --now装完之后第一件事是执行安全初始化脚本sudo mysql_secure_installation这个脚本会引导设置root密码、移除匿名用户、禁用root远程登录等操作我强烈建议完整的执行一遍不要在那些提示上偷懒跳过。创建业务数据库和账号这部分我有一个很深的体会绝对不要让业务代码使用root账号连接数据库。应该为每个项目单独创建数据库和账号账号权限只授予该数据库这样即使数据库连接信息泄露影响面也被限制在单个库内。CREATE DATABASE appdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER appuserlocalhost IDENTIFIED BY 这里放一个高强度密码; GRANT ALL PRIVILEGES ON appdb.* TO appuserlocalhost; FLUSH PRIVILEGES;字符集选择utf8mb4这一点是老生常谈但总有人踩坑。如果你的业务里有可能存用户昵称、表情符号等字符用utf8mb4才能正确存储否则会报乱码甚至数据写入失败。Redis的安装也很简单sudo apt install -y redis-server装好后需要修改/etc/redis/redis.conf核心配置如下bind 127.0.0.1—— 只允许本机访问外部网络一律拒绝requirepass—— 设置访问密码即使是本机访问也建议要密码appendonly yes—— 开启AOF持久化防止重启丢数据Redis默认配置会绑定在127.0.0.1上所以不需要防火墙额外防护。如果你在云服务器安全组里顺手开了6379端口赶紧关掉Redis的渗透事故几乎都是端口裸奔导致的。2.3 JDK环境安装我用的Spring Boot版本是3.x对应需要JDK 17。在Ubuntu上安装OpenJDK非常方便sudo apt install -y openjdk-17-jdk java -version这里要注意的是自研项目可能在本地用了某个特定小版本的JDK比如17.0.8而服务器apt装的可能更新比如17.0.13。这种情况绝大多数时候没问题JDK是向后兼容的只要大版本一致编译产物可以正常运行。但如果你的项目用到了比较偏门的依赖或JDK内部API还是建议在本地用maven打包时指定--release 17编译参数保证编译目标版本正确。JAVA_HOME环境变量可以加到/etc/environment里便于相关工具识别echo JAVA_HOME/usr/lib/jvm/java-17-openjdk-amd64 | sudo tee -a /etc/environment source /etc/environment3. 后端服务打包与部署3.1 Spring Boot打包的两种方式后端部署的第一步是把代码变成可执行产物。Spring Boot项目通常有war和jar两种打包方式我强烈推荐jar包方式。jar包内嵌Tomcat启动就是一个java -jar命令不需要额外安装和配置Tomcat服务器部署成本最低。war包方式适合需要和其他Web应用共用同一个Tomcat实例的场景自研项目基本没有这种需求。打包命令很简单# 在项目根目录执行跳过测试加快打包速度 mvn clean package -DskipTests打包产物在target/xxx.jar。这里我想强调一个实践细节jar包的配置文件处理。很多人习惯把application.yml打进jar包内部这样部署时改一个数据库密码就得重新打包上传非常痛苦。我的做法是把配置文件外置。Spring Boot支持从jar包所在目录的config子目录加载同名配置文件优先级高于jar包内的配置。具体做法是mkdir -p /data/app/backend/config # 将本地 application-prod.yml 上传到该目录一个整洁的部署目录大概是这样/data/app/backend/ ├── app.jar # 后端可执行jar包 └── config/ └── application-prod.yml启动时指定--spring.profiles.activeprod激活生产环境配置这样所有环境相关的敏感信息数据库密码、Redis密码、密钥都只存在于服务器文件系统中不会出现在代码仓库里。3.2 上传产物与目录权限将jar包和配置文件上传到服务器我习惯用scp简单稳定scp target/app.jar deployyour-server-ip:/data/app/backend/ scp src/main/resources/application-prod.yml deployyour-server-ip:/data/app/backend/config/上传后要用chown调整属主sudo chown -R deploy:deploy /data/app/backend权限这一步容易被忽略但很有讲究。如果jar包属于root用户你部署时用普通用户执行可能没权限读取到配置文件跑起来就会怪怪的。让部署用户对部署目录有完整读写权限是减少日常操作烦恼的前提。3.3 systemd守护进程配置这是后端部署最核心的一步。在/etc/systemd/system/app.service写入如下配置[Unit] DescriptionApp Backend Service Afternetwork.target mysql.service redis-server.service [Service] Userdeploy WorkingDirectory/data/app/backend ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /data/app/backend/app.jar --spring.profiles.activeprod SuccessExitStatus143 Restartalways RestartSec10 [Install] WantedBymulti-user.target解释几个关键参数。Userdeploy指定以普通用户身份运行这是安全原则的落地体现。Restartalways表示进程异常退出时自动重启RestartSec10是重启前的等待时间避免频繁重启导致系统资源被耗尽。SuccessExitStatus143是配合优雅关闭的Java应用在收到SIGTERM信号关停时退出码是143标记为正常退出systemd就不会认为服务跑挂了。启动并设置开机自启sudo systemctl daemon-reload sudo systemctl enable app --now sudo systemctl status app查看启动日志journalctl -u app -f看到Started App Backend Service以及Spring Boot日志里的“Started”字样说明服务已经起来了。这时候本地验证一下curl http://127.0.0.1:8080/api/ping如果返回了预期的JSON响应后端部署就算大体成功了。3.4 数据库结构初始化自研项目一般会有一套SQL脚本可能是建表语句加基础数据。部署时要考虑的是脚本的幂等性——也就是同一套脚本能不能重复执行而不报错。我的做法是分开执行建库建用户脚本只执行一次表结构脚本用CREATE TABLE IF NOT EXISTS包裹初始化数据脚本要么做好存在性判断要么只在首次部署时执行。把这三类脚本分放在不同目录既方便执行也方便后续增量更新。首次导入数据的命令参考mysql -uappuser -p appdb /data/app/sql/schema.sql mysql -uappuser -p appdb /data/app/sql/data.sql导入完成后做个简单验证比如查询一下用户表的行数mysql -uappuser -p appdb -e SELECT COUNT(*) FROM users;我踩过一次比较深刻的坑开发环境的SQL脚本里表结构和线上差别很大因为项目经历了多个迭代阶段后来一位同事重新导入了整份schema.sql结果把线上表结构改了导致后端代码查不到字段。从那以后我要求所有表结构变动都必须提供增量脚本绝不直接改线上表结构全套数据导入只允许发生在全新环境上。4. 前端构建与Nginx发布4.1 Vue项目构建前端部署的本质是“构建产物托管”。Vue项目开发时通过webpack-dev-server或vite跑在本地但这个开发服务器绝对不能用于生产环境。生产环境需要先执行构建命令生成压缩后的静态文件npm install npm run build构建产物默认输出到dist目录。Vite和Webpack的区别只是打包速度产物结构大同小异部署逻辑完全一致。这里有个重要细节Vue Router如果启用了HTML5 history模式即路由不带#号那么直接访问/about这样的深层路径时Nginx会找不到对应的物理文件从而返回404。这个问题在本地开发时不会暴露因为dev server内部做了路由回退处理只有部署到Nginx后才能体会到崩溃的滋味。构建产物上传到/data/app/frontend之后先本地确认一下dist里的index.html引用的JS/CSS路径是相对路径还是绝对路径。如果资源路径写死了/assets/xxx.js那就必须把前端资源放在Nginx的根目录下如果用的是相对路径./assets/xxx.js部署位置会更灵活。通常我建议配合路由base配置让产物路径与应用部署路径保持一致避免出现资源加载不出来但又检查不出问题的尴尬局面。4.2 Nginx配置要点Nginx安装和配置是我认为整套流程中必须精益求精的环节。先安装sudo apt install -y nginx然后在/etc/nginx/conf.d/下新建站点配置文件命名建议是项目名比如app.conf。先讲HTTP版本的基础配置后面再补HTTPSserver { listen 80; server_name your-domain.com; root /data/app/frontend; index index.html; # 前端路由history模式回退 location / { try_files $uri $uri/ /index.html; } # API反向代理 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; proxy_set_header X-Forwarded-Proto $scheme; } }这段配置解决了前端部署中两个最常见的问题。try_files $uri $uri/ /index.html这行是history路由的关键它先尝试按照请求路径找真实文件找不到就回退到入口index.html把路由交还给前端代码处理。proxy_pass则把所有/api开头的请求转发给同一台服务器上的Spring Boot服务。有的项目前端和后端域名不一致或者API路径没统一加/api前缀此时就会遇到跨域问题。我在早期项目里吃过不少跨域的苦后来想明白了一件事前后端分离项目在开发阶段用代理解决跨域在部署阶段用Nginx反向代理解决跨域这才是不需要污染后端代码的最优解。后端不需要配置任何CORS允许源因为对浏览器来说所有请求都发给了同一个域名同源策略天然满足。关于代理配置还想补充一个易踩的坑proxy_pass后面带不带路径意义完全不同。如果写proxy_pass http://127.0.0.1:8080;请求会原样转发也就是/api/user到后端还是/api/user。如果写成proxy_pass http://127.0.0.1:8080/;Nginx会把location匹配的部分去掉再转发/api/user转发到后端变成/user。两种写法取决于后端接口是否带/api前缀我的习惯是后端Controller统一不加/api前缀Nginx负责加这样后端代码更干净。测试配置并加载sudo nginx -t sudo systemctl reload nginx此时如果域名解析已生效浏览器访问http://your-domain.com就能看到前端页面了。4.3 WebSocket等特殊场景的代理配置现在的自研项目很多带实时通知、在线聊天之类的功能WebSocket在开发环境很常见部署时Nginx也需要专门适配。如果后端有WebSocket接口Nginx的location配置要加入升级请求头处理location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }Upgrade和Connection这两个请求头告诉Nginx把连接从普通HTTP升级到WebSocket协议proxy_read_timeout设为较长的时间是防止意外的连接断开。如果没有这段配置WebSocket握手会失败前端控制台会不断报错而且在浏览器层面看到的现象是“连接已建立但立即断开”如果不熟悉代理层逻辑排查起来非常费劲。同样文件上传场景如果涉及大文件Nginx默认的client_max_body_size是1M需要在server块里调大client_max_body_size 50m;我遇到过同事上传一个5M的Excel文件报413错误网上查半天最后还是问到了这个配置项。Nginx默认限制太小自研项目涉及导入导出功能时必须提前设置。5. HTTPS证书与域名接入5.1 证书申请与配置现在的Web项目没有HTTPS基本等于裸奔。不光是因为浏览器会对HTTP站点提示“不安全”更重要的是HTTP下传输的所有内容包括用户密码和登录凭证都是明文在各个节点可见的。自研项目虽然不是大型商业站但只要牵扯到用户名密码加密传输就是底线。我用的方案是Certbot申请Let‘s Encrypt免费证书。安装和申请过程已经非常傻瓜化sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d your-domain.comCertbot会自动修改Nginx配置加上证书路径并配置好自动续期。Lets Encrypt证书有效期3个月但Certbot会通过systemd timer自动续签不需要人工介入。我部署完成后都会手动测试一次续期命令确认定时任务确实生效sudo certbot renew --dry-run证书申请完成后的Nginx配置大概长这样server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; root /data/app/frontend; 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; proxy_set_header X-Forwarded-Proto $scheme; } }这一步完成之后用户访问https://your-domain.com就完全安全了。5.2 HTTP强制跳转HTTPS证书配好以后还需要让所有HTTP访问自动跳转到HTTPS。这一方面是用户体验的考虑另一方面也防止用户无意中以明文方式传输数据。在监听80端口的server块里加一个returnserver { listen 80; server_name your-domain.com; return 301 https://$host$request_uri; }注意这个301跳转是Council级别的操作一旦部署后搜索引擎会重新收录新地址域名结构有大的改动时会伴随短暂流量波动这是正常的。跳转配置完成后用浏览器测试http://your-domain.com应该能自动跳到https://开头。还要留心静默的HTTP跳转在混合内容场景下的表现如果前端代码里还有硬编码的http://接口地址、图片地址或者WebSocket地址它们在HTTPS页面里会被浏览器直接拦截表现为图片加载失败或接口请求报错。这是全站启用HTTPS后最常见的排查难点建议部署后打开浏览器控制台看一眼Network把所有Mixed Content都清干净。5.3 安全头配置可选项关于安全我想聊聊安全头配置这块属于“可以不用但不建议完全不用”的范畴。在Nginx的server块中可以顺手加上几个安全头add_header X-Frame-Options SAMEORIGIN always; add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy strict-origin-when-cross-origin always;X-Frame-Options防止页面被嵌入到第三方网页的iframe中能降低点击劫持风险X-Content-Type-Options防止浏览器对资源类型做MIME嗅探Referrer-Policy控制跨域请求时Referrer信息的发送策略。这三个头对应用本身逻辑没有任何影响属于低成本高收益的安全加固。关于HSTSHTTP严格传输安全我的建议是谨慎开启。HSTS会告诉浏览器“未来一段时间内只允许HTTPS访问这个域名”开启后如果证书出问题用户在强制刷新之前连HTTP降级通道都关闭了影响范围比较大。小项目建议先不加HSTS等运维成熟了再考虑。6. 自动化部署脚本与回滚机制6.1 一键部署脚本设计部署到一定次数之后手工敲命令、传文件、重启服务的模式就效率太低了也容易出错。我通常会写一个部署脚本来固化整个流程。脚本的思路并不复杂拉取或用scp上传新构建产物停服务替换文件重启服务验证健康检查接口。后端脚本可以用bash实现大概长这样#!/bin/bash # deploy-backend.sh APP_DIR/data/app/backend JAR_NAMEapp.jar BACKUP_DIR/data/backup/backend # 1. 备份当前版本 if [ -f $APP_DIR/$JAR_NAME ]; then timestamp$(date %Y%m%d%H%M%S) cp $APP_DIR/$JAR_NAME $BACKUP_DIR/app-$timestamp.jar echo Backup done: app-$timestamp.jar fi # 2. 覆盖新版本 cp /tmp/upload/app.jar $APP_DIR/$JAR_NAME || exit 1 # 3. 重启服务 sudo systemctl restart app sleep 10 # 4. 健康检查 status$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:8080/api/ping) if [ $status -eq 200 ]; then echo Deploy SUCCESS else echo Deploy FAILED, consider rollback fi这个脚本的要点有三处先说大势再说本章节要解决什么然后在具体实现里点出关键细节——备份当前版本是保证可回滚的前提健康的检查接口是确认部署成功的唯一可信依据失败时脚本只是提示而非自动回滚因为自动回滚也涉及判断逻辑的复杂度人工介入会更稳妥。前端的部署脚本更简单核心就是覆盖dist目录并reload Nginx。前端静态文件没有进程需要重启Nginx reload是平滑的不会中断线上服务。6.2 回滚机制与版本管理部署流程里最容易忽视、线上出事时最救命的就是回滚机制。我的策略很简单保留最近N个版本的备份并在部署目录里用软链接指向当前版本。以jar包为例ls -al /data/app/backend/ lrwxrwxrwx app.jar - /data/backup/backend/app-20240615203000.jar每次部署时先做新版本jar的软链接验证通过后再把app.jar指向新版本重启服务。如果新版本有问题只需改一下软链接指回旧版本再重启服务几十秒就能完成回滚。这种方式比直接把jar覆盖掉要安全得多因为旧版本文件还在那里不会因为一个cp命令把最后赖以求生的资本覆盖掉。实际项目里我还会结合数据库迁移来考虑回滚。如果本次发布的代码同时伴随了数据库表结构变更回滚代码的同时数据库结构却已经更新新旧代码可能不兼容。从这个角度讲数据库的兼容性修改要尽可能做到“新增字段不删旧字段修改逻辑先兼容旧逻辑”这样代码回滚后数据库仍然能配合旧版本正常工作。6.3 数据库备份与定时任务生产环境的数据是无价的这个认知往往是被坑过之后才有。数据库备份必须自动化、必须异地存放。我采用的方案是mysqldump加 crontab 定时任务#!/bin/bash # /data/backup/backup-mysql.sh timestamp$(date %Y%m%d%H%M%S) mysqldump -uappuser -p密码 appdb /data/backup/appdb-$timestamp.sql find /data/backup -name appdb-*.sql -mtime 7 -deletecrontab配置0 3 * * * /data/backup/backup-mysql.sh每天凌晨3点备份保留最近7天。这个策略虽然简单但覆盖面很广最多丢失24小时的数据这对大多数中小自研项目来说是可以接受的容灾范围。如果项目体量再大一点、对数据丢失容忍度极低就需要升级到binlog增量备份加云存储上传这里不再展开。我自己吃过一次亏当时只部署了定时备份脚本但没检查备份文件的完整性直到一次磁盘故障恢复时才发现备份文件是0字节。从此我要求备份脚本执行后必须检查文件大小备份文件小于某阈值就报警不能只“执行了”就算完。定时任务除了数据库备份还有日志清理任务。Nginx和应用日志会持续增长两三个月不清理就能把磁盘占满导致服务异常。日志轮转用logrotate是标准做法Ubuntu自带看一眼/etc/logrotate.d下是否针对Nginx和我们应用日志都配了策略即可。7. 线上问题排查与运维心得7.1 分层排查思路部署完成不是终点线上迟早会出问题。排查问题我有一套固定的分层方法从用户侧开始逐层向下几分钟内就能锁定大致范围第一层是浏览器控制台和Network面板。看请求是否发出、服务器响应状态是什么、有没有资源加载失败、控制台有没有JS报错。这一步能快速区分是前端问题还是后端问题。第二层是Nginx日志。/var/log/nginx/access.log能看到每一条请求的响应状态码error.log记录代理失败、上游超时、静态资源404等错误。第三层是后端日志。通过journalctl -u app -f或者应用自身的logback文件定位具体业务异常或SQL错误。最后一层才是数据库和Redis状态。使用这套排查方法最常见的场景就是502 Bad Gateway。这个错误意味着Nginx把请求转发给后端但后端没有正常响应。可能原因有很多后端服务崩溃、反向代理配置错误、后端启动超时、端口被占用等。先看Nginx error日志再systemctl status app看服务状态再查应用日志。如果日志显示启动过程中就报错那就是启动环境或配置问题如果日志根本没有新内容而Nginx报502那问题出在Nginx和后端的连接上比如proxy_pass写错了端口。7.2 常见问题速查表运维半年总结下来的高频问题我整理成了一张速查表每次遇到类似症状直接从表里找答案症状可能原因排查顺序与解决思路404 首页正常刷新子路由404Nginx没有配置history回退检查location / 是否包含try_files $uri $uri/ /index.html接口请求成功但跨域报错后端直接返回带CORS但前端域名不一致统一通过Nginx同域代理后端不配置CORS502 Bad Gateway后端服务未启动或代理端口错误systemctl status app、curl 127.0.0.1:8080测试再查Nginx error.log504 Gateway Timeout后端处理超时或Nginx超时配置过短检查后端慢查询、调整proxy_read_timeout前端页面能开接口全部401前后端时间不同步导致JWT校验失败date检查服务器时间配置NTP同步CPU持续100%某个请求导致死循环或大循环用top定位进程jstack导出线程栈分析磁盘空间告警日志或备份文件膨胀清理日志目录排查access.log和mysqldump备份策略图片或资源加载失败静态资源缓存策略或路径错误浏览器Network看资源请求URL确认产物路径是否对了WebSocket连接不上Nginx缺少Upgrade头配置检查location是否配置了Upgrade和Connection部署后旧版本代码还在跑systemd没有重新加载或服务没重启sudo systemctl daemon-reload sudo systemctl restart app这张表不能替代日志但能大大缩短排查时间尤其是半夜被喊起来处理线上问题时对着表先走一遍往往能快速定位到方向。7.3 一些实操心得最后分享一点心得。做自研项目的部署我觉得最重要的不是技术多新多炫而是流程要“可复现”。同一套部署步骤今天能成功三个月后来了新同事照着手册操作也必须能成功。这就要求所有操作尽可能脚本化、文档化不要让任何一步只存在于某个人的脑袋里。第二个心得是关于部署时间的。我强烈建议固定一个发布会窗口比如工作日下午两点到四点之间。不要在周五下午发版更不要在节前最后一天发版。万一线上出了问题没有人愿意在假期或周末处理生产事故。我的团队现在有个不成文的规定没有回滚方案不发布没有健康检查不发布没有备份验证不发布。这三点每次发布前过一遍能挡掉绝大多数低级故障。第三个心得是监控的“最少必要项”。小项目不需要上非常复杂的监控系统但至少要做到后端服务挂了能有人知道。实现这个目标有两个轻量方案一是给systemd服务配置失败邮件通知或webhook通知二是用云服务商自带的健康检查出问题自动发警报。我用过最简单有效的方案就是写一个每分钟跑一次的cron脚本检测Nginx可用性和后端健康接口挂了就往群里发条消息。这一点点代码量带来的安全感远超过投入的成本。部署这件事做顺了会大大提升开发的幸福感。项目功能开发得再好发布不顺畅用户用不上一切都白搭。希望这篇文章的流程和细节能帮大家把发布这件事变得踏实一些。
返回列表