接手一个 JavaWeb 项目,部署到 Linux 服务器上,这基本是每个 Java 开发者都躲不过去的一道坎。本地跑得好好的项目,一上服务器就是各种状况:端口起不来、数据库隔三差五连不上、日志里动不动就是内存溢出,甚至有的项目部署完第二天才发现配置文件里还写着测试环境的地址。这篇文章就专门聊聊 Linux 上部署 JavaWeb 项目的完整过程,从环境选型、JDK 和 Tomcat 安装、数据库对接,到 Maven 打包、war 包上传、Nginx 反向代理,再到常见的坑怎么排查,一次性讲透。
这不是一篇只给命令的复制粘贴文档。命令谁都能搜到,真正值钱的是每一步背后的为什么:为什么这个参数要这么设,为什么部署前要先规划目录,为什么连接池初始值不能拍脑袋写。全文基于我在多个项目里反复部署总结出来的实操经验,适合刚接触服务器部署的 Java 开发者,也适合负责运维交接的同学。看完之后,你不仅能照着把项目跑起来,还能在出问题时知道往哪个方向查。
1. 部署前的选型思考与环境规划
1.1 技术栈选型:先想明白这四件事
很多同学拿到项目第一反应是“装 JDK、装 Tomcat、丢 war 包”,这个流程本身没错,但真正踩过几次坑之后你会发现,选型比操作更重要。部署前至少要确认四件事:
- 项目是传统 war 包(Spring MVC、SSH 这类)还是 Spring Boot 可执行 jar?这决定了运行环境用 Tomcat 还是直接
java -jar。 - 数据库是哪一种?MySQL、PostgreSQL 还是 Oracle?版本、字符集、连接驱动这些都要提前对上。
- 前端静态资源多不多?有没有需要外挂的附件目录、上传目录?
- 单机部署还是集群?如果后面要扩机器,Nginx 这层一开始最好就留好。
这四件事直接决定后面几个小时的工作量。我之前接过一个项目,代码是早期 Spring MVC 写的,结果部署时发现本地一直在用 root 账号连库,服务器上压根没给业务账号授权,调试调了一晚上。问题不在技术难度,而在于部署前没人把账号、路径、端口这些约定提前定下来。
另外要特别提醒一点:看清楚项目的 Maven 打包方式。Spring Boot 项目默认打成 jar,但如果你非要用传统 Tomcat 方式部署,需要改 packaging 并排除内嵌 Tomcat;反过来,传统 war 项目打成 jar 也没法直接跑。这个错位问题特别常见,部署前先mvn help:evaluate -Dexpression=packaging或者直接看 pom.xml 确认一下。
1.2 环境规划和目录设计:少走弯路的第一步
服务器登录之后,先别急着装软件。花五分钟把环境摸一遍,比什么都强。我习惯先执行这几条命令:
cat /etc/os-release free -h df -h nproc看什么?系统版本决定包管理器是 yum 还是 apt;内存大小决定 JVM 堆参数怎么给;磁盘剩余空间决定日志和备份能不能放得下;CPU 核数影响 Tomcat 线程池的参考值。这些信息不看,后面全是盲猜。
摸完环境后,统一规划目录。我用得比较顺的一套结构是这样:
/opt/java JDK 安装目录 /opt/tomcat Tomcat 安装目录 /opt/app/backup 项目备份目录 /data/mysql MySQL 数据目录 /data/logs 应用日志归档目录 /data/uploads 用户上传附件目录为什么要把安装目录放在 /opt 下面而不是 /root 或 /home?原因很简单:第一,/opt 是 Linux 规范里专门放第三方软件的目录,后续人接手时一眼就能找到;第二,/root 下文件权限敏感,如果哪天需要用普通用户执行部署脚本,路径权限会很别扭;第三,统一根路径之后,备份、迁移、写自动化脚本都特别省事。我见过有项目装在 /home/xxx/software 下面,结果人离职之后新运维找了三天才把服务找全。
还有一个容易忽视的点:上传目录和日志目录千万别放 Tomcat 内部。Tomcat 升级或者重新解压,webapps 目录会被覆盖,放在里面的用户附件就全没了。项目里凡是涉及upload、temp这类路径,配置里全部指向 /data 下的独立目录,这是我在真实项目里吃过亏之后养成的习惯。
1.3 项目发布清单:把所有东西写在一张纸上
部署不是单点动作,而是一整套资源协调。我在正式操作之前,一定会先列一份发布清单,核对一项勾一项:
- 打包产物:war 包或 jar 包、版本号、构建时间;
- SQL 脚本:建库脚本、初始化数据脚本、增量变更脚本;
- 配置文件模板:数据库连接、Redis 连接、日志级别、文件存储路径;
- 第三方依赖:字体文件、so 动态库、地图 SDK 之类的额外资源;
- 静态资源:前端页面、图片、CSS/JS 是否单独部署;
- 运维约定:服务端口、管理账号、备份策略、告警联系人。
这份清单看起来不起眼,但在多人协作或者跨团队交接时是救命的东西。项目开机走查的时候,经常是代码包有了,SQL 忘了同步;配置文件改好了,附件目录没建;端口开了,防火墙策略忘配了。提前列清楚,部署过程就像照着菜谱做菜,每一步都是确定的,不容易漏。
2. JDK 与 Tomcat 的安装配置
2.1 JDK 安装与 JAVA_HOME 环境变量:这一步不能用 PATH 蒙混过关
JDK 安装本身不难,难在细节。首先确认项目依赖的 JDK 版本:Java 8 项目就别硬上 JDK 17,编译时用的某些库在老版本上才正常。下载 JDK 时注意,官方压缩包解压出来的目录名通常带版本号,比如jdk1.8.0_202,不同小版本的目录名不一样,配置环境变量时要写实际路径。
安装步骤很简单,解压到规划好的目录,然后配置环境变量:
tar -zxvf jdk-8u202-linux-x64.tar.gz -C /opt/java/ vim /etc/profile在/etc/profile末尾追加:
export JAVA_HOME=/opt/java/jdk1.8.0_202 export PATH=$JAVA_HOME/bin:$PATH export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar然后执行source /etc/profile,再用java -version和echo $JAVA_HOME验证。这里有个特别多见的坑:只改了 PATH,没设置 JAVA_HOME,结果java命令能用了,Tomcat 一启动就报Neither the JAVA_HOME nor the JRE_HOME environment variable is defined。原因是 Tomcat 的启动脚本catalina.sh和setclasspath.sh会直接读取 JAVA_HOME 变量,只配 PATH 是不被识别的。
再说一个容易被忽略的权限坑:如果服务器上有多个业务账号,最好让部署账号对 /opt/java 目录有可读可执行权限,不要只给 root 装好然后把普通用户扔到一边。很多问题其实不是软件问题,而是权限目录设得太死,启动脚本执行不了。
2.2 Tomcat 安装、启动与 systemd 自启配置
Tomcat 的版本选择和 JDK 版本要匹配。Tomcat 9 对应 Java 8 及以上,Tomcat 10 对应 Java 11 及以上,而且包名从javax.servlet改成了jakarta.servlet,传统项目不要盲目升大版本。下载 tar.gz 解压到 /opt/tomcat 后,第一件事不是启动,而是先改三样东西:
- 修改默认端口。默认 8080 端口太常见,很容易撞端口,有安全要求的话建议改成 8081 或某个自定义端口。改
conf/server.xml里的<Connector port="8080"。 - 关闭不安全的默认 Tomcat 管理页面。如果不需要远程部署功能,把
webapps下默认自带的目录清掉,只留自己的应用。 - 修改 shutdown 端口。
server.xml里默认的<Server port="8005" shutdown="SHUTDOWN">在公网环境下建议改掉或者配合防火墙禁掉外部访问。
修改完基本配置后,启动命令是:
cd /opt/tomcat/bin ./startup.sh启动后立刻看日志:
tail -f /opt/tomcat/logs/catalina.out看到Server startup in [xx] milliseconds就说明启动成功。接着用ps -ef | grep java确认进程确实活着,用ss -tlnp | grep 8081确认端口在监听。很多人只看一个startup.sh没报错就以为成功了,实际上进程可能秒退,端口根本没起来。
为了让它开机自动启动,我一般写一个 systemd 服务文件,这样比直接往 rc.local 里塞脚本干净得多:
[Unit] Description=Tomcat Application Server After=syslog.target network.target [Service] Type=forking Environment=JAVA_HOME=/opt/java/jdk1.8.0_202 PIDFile=/opt/tomcat/tomcat.pid ExecStart=/opt/tomcat/bin/startup.sh ExecStop=/opt/tomcat/bin/shutdown.sh User=tomcat Group=tomcat Restart=on-failure [Install] WantedBy=multi-user.target保存到/etc/systemd/system/tomcat.service,然后systemctl daemon-reload && systemctl enable --now tomcat。注意这里用户用的是独立创建的 tomcat 账号,没有直接用 root 跑 Tomcat,这是出于安全考虑,避免应用被入侵后直接拿到 root 权限。
2.3 JVM 参数与连接线程池:调优要基于真实资源
很多人部署完 Tomcat 就直接用默认参数,结果高并发一来就卡死。JVM 参数应该在启动前就设置好,而不是等 OOM 了再改。编辑/opt/tomcat/bin/catalina.sh,在脚本头部加入:
CATALINA_OPTS="-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -Dfile.encoding=UTF-8"这几个参数的含义要搞清楚。-Xms是初始堆大小,-Xmx是最大堆大小,两者保持一致可以避免运行期堆大小频繁伸缩带来的性能抖动。-XX:MaxMetaspaceSize限制元空间大小,很多项目在热部署后报java.lang.OutOfMemoryError: Metaspace,就是因为这个值没限制。-XX:+UseG1GC在堆较大的场景下比默认的并行收集器更稳。
堆大小怎么定?经验上,如果服务器总内存 4G,Java 堆最多给 2G 左右,剩下的要留给操作系统、数据库和可能的其他进程。给 3G 以上反而会因为内存紧张触发系统 swap,进而导致 GC 时间拉长,性能断崖式下降。所谓“参数越大越好”是新手最容易踩的坑。
Tomcat 连接线程池也在conf/server.xml里调,我常用的参考值:
<Connector port="8081" protocol="HTTP/1.1" maxThreads="200" minSpareThreads="20" acceptCount="100" connectionTimeout="20000" maxPostSize="-1" URIEncoding="UTF-8" />补充说明一下这几个参数的关系:maxThreads 是能同时处理的请求线程数,不是越大越好,每个线程都要占用内存,200 对于大多数中小项目足够了;acceptCount 是队列里能等的请求数,超出的请求会直接被拒绝;maxPostSize 默认 2MB,如果业务里要上传大文件,这个值不改成 -1 就会出现表单提交 400。
3. 数据库准备与项目配置对接
3.1 MySQL 安装、建库与业务账号授权:不要用 root 连业务库
JavaWeb 项目绝大多数逃不开 MySQL。在 Linux 上安装 MySQL 可以用系统包管理器直接装,也可以离线 rpm 方式。装完后的第一件事是初始化安全配置:
systemctl start mysqld mysql_secure_installation初始化完成后登录数据库,创建业务账号和数据库。这一步的关键是数据库、账号、权限、字符集四件事要一次到位:
CREATE DATABASE appdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'app_user'@'localhost' IDENTIFIED BY '换成强密码'; GRANT ALL PRIVILEGES ON appdb.* TO 'app_user'@'localhost'; ALTER USER 'app_user'@'localhost' IDENTIFIED BY '换成强密码'; FLUSH PRIVILEGES;这里有两个细节值得展开。第一,为什么业务账号的 host 要写成 localhost 而不是 %?如果应用和数据库在同一台机器,localhost 就够了,host 范围越大被攻击面也越大。当然如果数据库单独一台机器,就需要写 application 所在机器的 IP 或者内网网段。第二,为什么字符集要用 utf8mb4 而不是 utf8?MySQL 的 utf8 其实是 utf8mb3,不支持四字节的 emoji 和生僻字,一旦业务数据里出现了这些字符,入库直接报Incorrect string value错误,到时候再改字符集就是大工程了。
另外,如果应用和数据库不在同一台机器,还要注意两件事:一是 MySQL 的bind-address要改成服务可达的地址,而不是默认的 127.0.0.1;二是机器防火墙要放行 3306 端口。很多新手配置完账号发现连不上,其实问题根本不在 MySQL,而是防火墙把请求挡在了外面。
3.2 初始化数据导入与连接配置:参数含义比抄配置更重要
数据导入一般用项目提供的 SQL 脚本,比如init.sql,执行方式:
mysql -h127.0.0.1 -uapp_user -p appdb < init.sql执行完别急着走,先验证一下:
USE appdb; SHOW TABLES; SELECT COUNT(*) FROM user;确认表结构和数据条数都符合预期再往下走。这一步能帮我提前发现 SQL 脚本里路径写错、语句遗漏、编码不对这些问题。
然后是项目侧的数据库连接配置。以 Spring Boot 项目为例,典型的配置长这样:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/appdb?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai spring.datasource.username=app_user spring.datasource.password=你的密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver这里面几个参数我解释一下:characterEncoding=utf8保证应用和数据库之间传输数据按 UTF-8 处理;serverTimezone=Asia/Shanghai解决 JDBC 驱动的时区报错,不配置的话高版本驱动会直接抛The server time zone value 'CST' is unrecognized;useSSL=false是因为本地内网连接一般不需要加密,配置了反而多一层握手开销。
还有一类老项目使用的 jdbc.properties,放在WEB-INF/classes下面。如果配置文件是打进 war 包里的,每次改数据库地址都要重新打包,特别不方便。我的做法是把配置文件外置到/opt/app/config/目录,Tomcat 启动时通过 classpath 引用外部目录。这样以后换库、改密码,只需要编辑外置文件再重启,不用重新构建项目,运维成本低很多。
3.3 连接池参数与数据备份:生产环境必备的两件事
连接池我用的比较多的是 HikariCP 或者 Druid。以 Druid 为例,一套相对稳妥的初始配置:
spring.datasource.druid.initial-size=5 spring.datasource.druid.min-idle=5 spring.datasource.druid.max-active=20 spring.datasource.druid.max-wait=60000 spring.datasource.druid.validation-query=SELECT 1这几个值的思路是:初始连接数不要太多,5 个足够应付启动阶段;最大连接数 20 对于中小项目足够,但也要结合 maxThreads 来配,线程数 200 却只有 20 个数据库连接的话,请求还是会排队;max-wait 是借出连接的等待时间,超过 60 秒就直接报错,避免请求无限挂起。连接池参数如果只是从网上抄一个,不结合自己的并发量,效果往往适得其反。
数据库备份这件事虽然不属于“部署”的范畴,但部署完不配备份,相当于裸奔。我一般会写一个简单的备份脚本:
#!/bin/bash BACKUP_DIR=/data/mysql/backup mkdir -p $BACKUP_DIR mysqldump -uapp_user -p密码 --databases appdb > $BACKUP_DIR/appdb_$(date +%F).sql find $BACKUP_DIR -mtime +7 -delete配合 crontab 每天凌晨执行一次,7 天内的备份都保留,超过的自动清理。第一次真正遇到数据库误删数据的情况时,你会感谢当时配了备份。
4. 打包部署全过程记录
4.1 本地 Maven 打包:一段干净的构建日志值千金
部署的第一步在本地,把项目打成可部署的产物。Maven 打包命令并不复杂:
mvn clean package -DskipTests但这里有几个参数我会特别留意。第一,-DskipTests跳过测试代码的编译和执行,注意这和-Dmaven.test.skip=true有区别:前者是跳过测试执行但会编译测试类,后者连测试类都不编译。CI 流程不严格的话,直接用-DskipTests在节省时间的同时还能保证测试类语法正确。第二,如果项目用了多环境配置,比如application-dev.yml和application-prod.yml,打包时要指定激活的 profile:
mvn clean package -DskipTests -Pprod第三,打包前确认 pom.xml 里的 packaging 标签。Spring Boot 项目如果要用外部 Tomcat 部署,packaging 要改成war,并且继承SpringBootServletInitializer;如果直接java -jar运行,packaging 保持jar就行。很多项目部署方式模糊,就是因为这一步没敲定。
打包完成后,target 目录下会生成xxx.war或xxx.jar。看一眼文件大小和修改时间,正常的 Spring Boot jar 应该有几十 MB,如果只有几 KB,多半是编译出问题了,这种包别往服务器上传。
4.2 上传部署与配置外置:war 包放哪里其实有讲究
打好的包上传到服务器,工具可以选择scp、sftp,或者装了 lrzsz 的机器直接用rz命令,我平时最常用的是 scp:
scp -P 22 target/app.war 用户@服务器地址:/opt/tomcat/webapps/war 包放进 webapps 目录之后,Tomcat 会在启动时自动解压成同名目录。这里有两个版本路径的细节要说清楚。如果你希望应用直接通过根路径访问,比如http://服务器地址/,就把 war 包改名为ROOT.war再放进去;如果希望带项目名访问,比如http://服务器地址/app/,就保持app.war这个名字。很多初学者分不清这个,结果明明部署好了,访问路径写错,白白排查半天。
在把 war 包放进 webapps 之前,我会先清空该目录下的旧版本:
rm -rf /opt/tomcat/webapps/app*这个操作我建议每次部署都做。Tomcat 解压目录如果残留了旧的文件,新版本里已经删除的 class 或 JSP 可能还在,运行时会出一些很诡异的问题。清掉旧目录,让新 war 包从零解压,干净利落。
配置文件外置的做法,我在 3.2 节提到过。传统 war 包项目的 Spring 配置文件默认在 classpath 下,从外部指定无非是在 Tomcat 的conf/catalina.properties里加common.loader路径,或者在启动参数里加-Dspring.config.location。Spring Boot 项目更简单,war 包旁边的config目录天然就是外部配置目录。每次部署只要保证 war 包最新,配置文件不动不丢,这是我在多个项目里反复验证过的省心方案。
4.3 启动验证与日志跟踪:确认它是真的活着,而不是“好像活着”
部署完成后,执行启动:
/opt/tomcat/bin/startup.sh然后立刻看日志,我用两三个终端窗口,一个跟踪日志,一个查进程端口:
tail -f /opt/tomcat/logs/catalina.out ps -ef | grep java ss -tlnp | grep 8081 curl -I http://127.0.0.1:8081/app启动成功的标志不是startup.sh退出了,而是日志里出现Server startup in。紧接着用 curl 请求首页,检查 HTTP 状态码是 200 还是 404。这里我遇到过最典型的情况:war 包名和 context path 不匹配,curl 打根路径返回 404,加上正确项目路径就返回 200。这个小问题能折腾掉半小时。
启动时如果日志里一个异常都没有,但页面 500,那通常是数据库连接或者初始化数据的问题。一个个查:连接池通不通,SQL 对不对,附件目录写不写得进去。我的排查顺序永远是先看日志,再看端口,最后看数据库,因为日志里包含的信息量最大。
5. Nginx 反向代理与 HTTPS 接入
5.1 Nginx 安装与反向代理:为什么要多一层 80 端口的门面
Tomcat 默认跑在 8081 端口,直接对外暴露有两个问题:一是端口不像 80 和 443 那么友好,用户访问要带端口号;二是 Tomcat 本身擅长处理动态请求,对静态文件和并发连接的优化能力不如 Nginx。所以在部署架构里,我几乎一定会加一层 Nginx 做反向代理。
安装 Nginx 很简单:
yum install nginx -y systemctl enable --now nginx然后编辑/etc/nginx/conf.d/app.conf:
server { listen 80; server_name 你的域名或服务器IP; location / { proxy_pass http://127.0.0.1:8081; 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。Host不传的话,Tomcat 拿到的请求头里没有正确域名,如果应用里有基于域名的逻辑(比如生成绝对链接),就会生成错地址。X-Real-IP和X-Forwarded-For是为了让后端拿得到真实客户端 IP,否则 Tomcat 看到的所有请求都来自 127.0.0.1,后续做访问日志分析、风控、限流全都没法干。
如果后面要扩容成多台 Tomcat,可以在 Nginx 里配置负载均衡:
upstream backend { server 127.0.0.1:8081 weight=5; server 127.0.0.1:8082 weight=5; }weight参数控制两台机器的流量比例,这是扩展为集群的第一步。很多项目开始是单机部署,后来流量大了才引入负载均衡,提前预留好 upstream 结构,切换成本会小很多。
5.2 静态资源分离与缓存策略:把 Tomcat 从体力活里解放出来
JavaWeb 项目里有大量 CSS、JS、图片这类静态资源。如果这些请求全部穿透到 Tomcat,Tomcat 每次都要走一遍 Servlet 容器逻辑,白白消耗线程。在 Nginx 层直接处理静态资源,是性价比最高的一步优化。
我常用下面这段配置:
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ { root /opt/app/static; expires 7d; add_header Cache-Control "public"; }expires 7d是告诉浏览器这些资源 7 天内可以直接用本地缓存,不用重新请求服务器。这样静态资源在浏览器端有缓存,在 Nginx 端不走代理,Tomcat 的负载会明显降下来。实测在一些图片较多的管理系统里,加了这层之后 Tomcat 的请求量能少三四成。
还有一个细节:如果项目里有用户上传的附件,比如/uploads/路径,这个目录不应该走 Tomcat 的默认处理,也应该交给 Nginx:
location /uploads/ { alias /data/uploads/; }这样用户在浏览器里访问附件时,实际上是 Nginx 直接读磁盘文件返回,不经过后端应用。上传目录通过软链接或者配置映射到 /data 下,配合前面的目录规划,整个文件流就顺了。
5.3 HTTPS 与安全加固思路:上线之前该做的几件事
现在很多环境对 HTTPS 已经是硬性要求。如果域名已经备案,我推荐用自动证书方案,首次申请和续期都能自动化,不用自己管理证书文件。如果是内网环境或者只是测试服务器,可以用 openssl 生成自签证书先撑起来,浏览器会提示证书不受信任,但至少加密通道是通的。
Nginx 配置 HTTPS 的标准做法:
server { listen 443 ssl; server_name 你的域名; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; location / { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 80; server_name 你的域名; return 301 https://$host$request_uri; }这里有个容易忽视的点:加了 Nginx 终结 HTTPS 之后,Tomcat 里保存的应用访问协议还是 HTTP,如果代码里有基于协议生成 URL 的逻辑,需要让 Nginx 把X-Forwarded-Proto传给后端,否则可能出现页面加载了但回调地址是 http 的混乱情况。
安全加固方面,我建议上线前至少做这几件事:修改 SSH 默认端口并禁止 root 直接登录;防火墙只放行 80、443、SSH 端口;关闭 Tomcat 的 manager 和 host-manager 页面;不要让 MySQL 3306 端口对公网开放。这些不用花太多时间,但对整体安全性提升非常明显。
6. 常见问题与排查实录
6.1 启动失败与端口占用:先分清是进程问题还是资源问题
部署过程中遇到问题太正常了,关键是有一套固定的排查思路。启动失败一般分三类:端口被占用、资源不足、脚本执行异常。
端口占用是最容易复现的。启动时日志会报Port 8081 was already in use,或者Address already in use。排查命令:
ss -tlnp | grep 8081 lsof -i:8081看到占用进程后,如果是残留的旧进程,kill 掉再启;如果是其他服务占用,就得改端口。碰到启动后进程秒退的情况,先看catalina.out末尾的堆栈,绝大多数问题是 JDK 版本不匹配、目录权限不对、或者是server.xml写错了。别急着在网上搜“Tomcat 启动失败”,把日志最后二十行看完,答案通常就在里面。
资源不足导致的启动失败相对隐蔽。常见的是java.net.SocketException: No buffer space available或者OutOfMemoryError: unable to create new native thread,前者往往和系统网络参数有关,后者通常是进程数或内存不够。用ulimit -n和/proc/sys/kernel/pid_max检查一下限制,必要时提高进程和文件描述符上限。
6.2 数据库连接异常:不要只盯着密码看
数据库连不上,是部署阶段最消耗耐心的问题,因为出错环节太多。我把常见的错误和原因整理成了一张表:
| 报错现象 | 可能原因 | 排查方向 |
|---|---|---|
| Communications link failure | 网络不通或防火墙拦截 | 检查 3306 端口连通性,telnet 127.0.0.1 3306 |
| Access denied for user | 账号密码错误或 host 授权不对 | 核对密码、检查用户表 host 字段 |
| Unknown database | 数据库不存在或名字写错 | SHOW DATABASES; |
| SSL connection error | 驱动版本与 MySQL 版本不匹配 | 升级驱动,或连接串加useSSL=false |
| Connection refused | MySQL 没启动或 bind-address 限制 | systemctl status mysqld,检查 my.cnf |
| The server time zone value | JDBC 时区参数没设置 | 连接串加serverTimezone=Asia/Shanghai |
这些问题的常见性排序,密码写错和 IP 放行没做排在前两位。如果你的配置看起来都对,但就是连不上,可以手工用mysql客户端命令模拟一遍登录,看能不能进入数据库。客户端能进说明连接层没问题,问题多半在应用侧的驱动或连接串格式上。
6.3 内存溢出与 CPU 飙高:从日志里找出真凶
跑一段时间后应用突然变慢,或者直接 502、504,常见的元凶是内存泄漏和高 CPU 占用。内存溢出的日志里会出现关键词:
java.lang.OutOfMemoryError: Java heap space java.lang.OutOfMemoryError: Metaspace前者是堆内存不够,后者是元空间被打满。如果是 heap space,先确认堆参数到底配了多大,jmap -heap <pid>可以查看实际生效的值;如果是 Metaspace,通常在频繁热部署或者动态生成类过多的场景下出现。
CPU 突然飙高,我一般用top先找到 java 进程,再用top -Hp <pid>看线程占用,最后用jstack <pid>把线程栈打出来,看热点代码在哪里。常见的两个场景:一个是没有索引的慢 SQL 导致数据库端 CPU 高,应用端线程全在等数据库返回;另一个是代码里出现了死循环或者大量的正则匹配。前者要优化 SQL,后者要修代码。部署阶段能做的,是先把 JVM 参数和线程池配好,给排查留出空间。
6.4 日志时间不对与端口访问不了:两个容易被忽略的小问题
还有两个我踩过多次的坑顺带提一下。第一是服务器时区没有设置成 Asia/Shanghai,导致 Tomcat 日志时间比业务时间晚 8 小时。排查问题看日志时,时间对不上非常误事。建议:
timedatectl set-timezone Asia/Shanghai timedatectl status第二是访问不了服务时,先在内网 curl 一下本机端口,通的话说明应用是活的,问题在防火墙或者安全组;不通的话问题在后端。很多时候新手在服务器上敲 curl 通了,就用浏览器从外部访问,结果发现不通,那是因为还没按顺序检查防火墙。网关和安全组是两层东西,要分别确认。
现实中的一点体会
整套流程反复走下来,我最大的感受是:部署从来不是“把文件放上去”那么简单,而是一个把环境、代码、配置、数据串起来的过程。每个环节之间都可能埋着坑,但只要提前把选型定清楚、把目录和账号规划好、把配置和代码分开管理,大部分坑都能在动手之前避开。
另外一个我觉得特别重要的习惯是:每次部署都记录下操作步骤和实际使用的版本号、参数值。项目隔三个月再部署一次,如果所有信息都在,照着文档半小时就能完成;如果全靠记忆,总要重新踩一遍当年踩过的坑。现在我会把部署文档和脚本一起提交到项目仓库里,让整个流程变成可以重复执行、可以交接给别人的东西。
最后分享一个小技巧:如果你觉得这个项目还会多次部署,花点时间把那套部署步骤写成 Shell 脚本,从环境检测、安装 JDK、拉部署包、备份旧版本、替换新版本到检查健康状态,一步到位。脚本跑通了之后,部署就从“操作”变成了“执行”,省下来的时间可以干更多有价值的事。