1. 下单之前:ECS规格选择里的隐性成本与部署后遗症
1.1 选2核4G还是2核2G,先算一笔JVM内存账
很多教程喜欢直接跳过服务器购买这一步,上来就教装环境,但以我这些年帮人收尾部署的经验来看,一半以上的“部署困难户”在买服务器那一刻就埋下了雷。
Java后端和静态网站完全不一样,它一旦跑起来就是一头内存怪兽。JVM默认的堆内存大小是物理内存的四分之一,也就是说一台2G内存的机器,你不做任何配置直接 java -jar,JVM会给自己划走512M。如果这台机器上还要跑MySQL、Nacos、Redis,内存分分钟爆给你看。
我见过太多人买了2C2G的入门机型,压缩包解压完系统就剩不到600M可用内存,然后各种 OutOfMemoryError 轮番轰炸。热搜词里那句 java: outofmemoryerror: insufficient memory 就是这么来的,而且是高频词。所以如果你准备部署的是Spring Boot单体应用 + MySQL,还没有别的中间件,2C4G是底线,2C2G大概率会让你在后半夜爬起来救服务。
另外有个特别容易踩的坑是阿里云的突发性能实例(t5/t6系列)。这类实例价格便宜很多,但它的CPU是有积分机制的。简单说,你买的时候那点价格对应的是一个基准CPU性能,平时的突发能力靠积分池撑着,积分用完CPU就会被压制到基准线,表现出的症状就是“为什么服务器突然慢得像蜗牛”。如果你的服务是7x24小时持续对外提供接口的,千万别图便宜选t系列,直接上通用型g系列或者计算型c系列,哪怕少买一年,也别让CPU成为系统的天花板。
1.2 系统镜像、安全组和登录方式,三项基础决定别做错
系统镜像这块,阿里云默认提供的选择很多,CentOS、Ubuntu、Debian、Alibaba Cloud Linux都有。我的建议是:CentOS 7虽然网上教程最多,但它已经停止维护,各大软件源对它的支持也在收缩,新环境就别往里跳了。现在阿里云主推的Alibaba Cloud Linux 3是个不错的选择,它向下兼容CentOS的使用习惯,yum命令照用,阿里云自己的优化和文档支持也最到位,遇到问题能搜到一手资料。Ubuntu 22.04/24.04也好,不过习惯了RHEL系老手切过去,apt和systemctl的差异要适应一阵子。
然后是安全组,这个我放到基础决定里说,因为它是新手第一大坑。阿里云ECS的端口放行分两层:云平台安全组和系统内部防火墙。安全组规则在阿里云控制台的ECS实例详情页里编辑,很多教程会在系统里帮你关firewalld,但如果你在安全组层面压根没放行端口,外部永远连不进来。正确的思路是:
- 安全组放行需要对外开放的端口(22、80、443、8080等)
- 系统防火墙保持开启,内部再限制一遍细粒度访问
- 3306这类数据库端口不要对全宇宙开放,只放行你自己的办公网IP或跳板机IP
登录方式上,我建议直接选密钥对。密码登录意味着服务器上有一个可以被爆破的入口,而密钥登录安全性高得多。密钥对的私钥文件保存好,丢了只能通过控制台重置,那个体验我不想再经历第二次。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java运行环境:JDK版本选择与JVM参数调优基础
2.1 不再纠结OpenJDK还是Oracle JDK,版本和源才是关键
服务器上装Java,我现在的做法是无脑OpenJDK。Oracle JDK的许可条款一变再变,而OpenJDK在服务器部署场景下和它的行为差异几乎可以忽略。
安装时优先用系统的包管理器,不要手动解压tar.gz。以Alibaba Cloud Linux / CentOS为例:
bash复制# 安装Java 8
sudo yum install -y java-1.8.0-openjdk-devel
# 或者安装Java 17
sudo yum install -y java-17-openjdk-devel
如果系统源里没有,可以先将yum源替换成阿里云镜像源,速度会明显提升。安装完之后,有个细节很多人会忽略:java -version 显示的版本不一定是你刚装的版本。因为系统里可能自带了一个旧OpenJDK,/usr/bin/java 指向的是旧的那个。这时候用 alternatives --config java 手动切换一下,或者更彻底一点,在 /etc/profile 里显式设置 JAVA_HOME 再重新登录会话。
bash复制export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export PATH=$JAVA_HOME/bin:$PATH
JDK版本选8还是17,取决于项目本身。老项目用8稳,新项目建议17或21,Spring Boot 3.x要求JDK 17起步,这个没得选。有一个小提醒:即使你用JDK 8,也尽量用8u202之后的版本,因为之后的版本经历了多次安全修复和企业级特性更新,线上环境别用太旧的build。
2.2 JVM参数:2G内存的机器怎么设置才不冤枉
很多开发者的习惯是本地IDE里点击运行,从不关心JVM参数,好,到了服务器上直接裸跑,结果就遇到了 insufficient memory。这里有两个层面要搞清楚:
第一层是操作系统层面的物理内存不足。一台2G的机器,系统本身占掉500M左右,MySQL如果占400M,Nacos如果单独部署再占300M,留给Java的物理空间可能只有700M左右。此时JVM启动时按默认比例分配堆内存,系统会直接拒绝分配,你看到的报错就是 insufficient memory,这是最原始的原因。
第二层是JVM内部的OOM。堆内存不够了,GC怎么也回收不出空间,抛的是 java.lang.OutOfMemoryError: Java heap space,和上面的报错完全不同。
针对2C4G的经典ECS配置,我常用的一套参数是这样的:
| 物理内存 | 推荐JVM堆参数 | 适用场景 |
|---|---|---|
| 2G | -Xms256m -Xmx512m | 单模块Spring Boot应用,无其他中间件 |
| 4G | -Xms512m -Xmx1g | 单体应用 + MySQL同机部署 |
| 8G | -Xms1g -Xmx2g | Spring Cloud微服务单节点或高并发单体 |
关键是 -Xms 和 -Xmx 要设置相同值。如果初始堆256M,最大堆512M,JVM会在内存压力下反复扩容缩容,期间会触发频繁的Full GC,表现为接口偶尔突然变慢,但CPU又不是很忙。把两个值调成一样,JVM启动时一次性分配到位,GC行为会稳定很多。
-XX:+UseG1GC 这个参数在JDK 8u201+之后或JDK 11以上的默认GC就是G1,不用特意加。JDK 8早期版本可以显式指定,但我还是建议:要么升级JDK版本,要么就不动这个参数,让JVM用默认策略。
另外,如果用的是Spring Boot应用,建议把JVM参数写在启动命令里通过 -D 传入,比如 -Dspring.profiles.active=prod,这和后面讲systemd部署是配套的。
3. MySQL整备与Nacos连接失败的完整排查链路
3.1 MySQL 8.0的安装与初始化的几个细节坑
到了数据库环节,热搜词里 “ecs配置nacos的mysql一直报错,访问不了mysql” 是高频中的高频,我也曾在凌晨被类似的求助电话叫醒,对这条排查链路太熟了。
先说说MySQL本身的安装。如果你的ECS系统源里没有MySQL,可以先通过阿里云镜像站配置MySQL的yum源。安装之后,第一次启动MySQL,root用户的初始密码不会告诉你,而是写在一个临时文件里:
bash复制sudo grep 'temporary password' /var/log/mysqld.log
这个临时密码是MySQL 8.0的默认安全策略生成的,第一次登录时会被强制要求修改:
bash复制mysql -uroot -p
ALTER USER 'root'@'localhost' IDENTIFIED BY '你的新密码';
这里有个常见的卡点:MySQL 8.0默认安装了validate_password组件,要求密码至少含大写字母、小写字母、数字、特殊字符,长度至少8位。如果你设置的密码不够复杂,会收到报错 ERROR 1819 (HY000): Your password does not satisfy the current policy requirements。新手在这里反复尝试,容易被搞懵。建议在开发环境就把这个组件关掉,或者老老实实设置一个足够复杂的密码。
另外提一点:MySQL 8.0的配置文件默认在 /etc/my.cnf,字符集默认是utf8mb4,这点比老版本好很多,不用再手动改。
3.2 “访问不了MySQL”的四种可能:按这个顺序查,半小时内解决
Nacos连不上MySQL,或者你在本地用Navicat连不上ECS上的MySQL,我先给一个标准的排查顺序,每一步都对应明确的验证方法和解决方案。
| 排查点 | 判断方法 | 解决办法 |
|---|---|---|
| MySQL用户授权 | 执行 SELECT user, host FROM mysql.user; 查看root的host |
将host改为 % 或新建专用用户 |
| 安全组/防火墙 | 在ECS本机 mysql -uroot -p 能连,远程却超时 |
控制台安全组放行3306,或确认本机防火墙规则 |
| 监听地址 | netstat -tlnp | grep 3306,看到 127.0.0.1:3306 |
修改bind-address,注释掉127.0.0.1或改为0.0.0.0 |
| 认证插件 | 连接报 Authentication plugin 'caching_sha2_password' 相关错误 |
将用户认证插件改为 mysql_native_password |
第一类:MySQL用户授权问题。MySQL默认只允许root从localhost登录,即使安全组、防火墙全开了,远程连接依然会被拒绝。改法如下:
sql复制CREATE USER 'nacos'@'%' IDENTIFIED BY '你的强密码';
GRANT ALL PRIVILEGES ON *.* TO 'nacos'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
第二类:安全组没放行。这个是新手最容易犯的。你在本机 mysql -uroot -p 连得通,但Navicat连不上,第一反应就是去改配置,结果其实只是阿里云控制台上的3306端口压根没有添加放行规则。去ECS实例详情页的安全组里确认一下,比盲改配置高效得多。
第三类:bind-address参数。MySQL 8.0默认监听在127.0.0.1,意味着只允许本机连接。netstat -tlnp 看到的是 127.0.0.1:3306 而不是 0.0.0.0:3306,就说明还没对外监听。修改 /etc/my.cnf,把 bind-address = 127.0.0.1 这一行注释掉,重启MySQL即可。
第四类:认证插件不兼容,这是Nacos连接MySQL报错里特别具有迷惑性的一个。MySQL 8.0默认的认证插件是 caching_sha2_password,但一些中间件(包括某些版本的Nacos客户端)只支持旧的 mysql_native_password。报错信息会明确提示认证插件,解决办法:
sql复制ALTER USER 'nacos'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;
顺便把Nacos侧的配置也贴一下,因为它在错误信息里不会直接告诉你“密码错了”,而是反复提示连接超时或Communications link failure。在Nacos的 conf/application.properties 里,重点是这三行:
properties复制spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?serverTimezone=Asia/Shanghai&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000
db.user.0=nacos
db.password.0=你的密码
注意 serverTimezone=Asia/Shanghai,不加时报时区错;connectTimeout 调小,连不上时快速失败,而不是白白等半天。
4. 项目打包与上传:Maven阿里云镜像加速和“本地能跑线上报错”的怪事
4.1 配置Maven阿里云仓库:下载依赖慢的问题一次解决
热搜词里“maven配置阿里云仓库”也是一个高频问题,原因是默认的中央仓库服务器在国外,国内拉取依赖慢到你怀疑人生。阿里云提供了一个很完整的maven公共仓库镜像,配置起来非常简单,在 ~/.m2/settings.xml 里加一段:
xml复制<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
这个 https://maven.aliyun.com/repository/public 是聚合仓库,同时代理了中央仓库、spring插件仓库等常用源,日常用完全够了。如果项目里引了一些特殊依赖(比如oracle驱动这种不开放到中央仓库的),可以再单独加一个仓库地址。
有一点要提醒:如果项目内部用了私服Nexus,mirrorOf 不能写成 *,否则会把请求私服的依赖也拦截到阿里云上,导致找不到。这时要写成:
xml复制<mirrorOf>*,!nexus-repository</mirrorOf>
排除掉私服仓库的id,让私服的依赖还是走私服。
4.2 打包时最容易忽略的两件事:跳过测试和多环境配置
打包命令我用得最多的是这一条:
bash复制mvn clean package -DskipTests -Pprod
-DskipTests 是跳过测试用例的编译和执行,-Pprod 是激活生产环境的profile。很多新手打包时被测试卡住,跑到一半报错失败,就是因为没有跳过测试。想想本地测试环境都有自己的配置,但生产环境一堆依赖不一定齐全,测试跑了也白跑。
多环境配置是“本地能跑线上就挂”这类问题的核心解药。Spring Boot项目的标准做法是:
- application.yml:放公共配置
- application-dev.yml:放开发环境
- application-prod.yml:放生产环境
示例:
yaml复制# application-prod.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/yourdb?serverTimezone=Asia/Shanghai
username: youruser
password: yourpassword
redis:
host: localhost
port: 6379
生产环境配置里有一件事必须反复强调:不要写死成内网IP或者某个特定环境的地址。有人图省事把数据库地址直接写成测试环境的内网IP,部署到线上才发现连的是别人的库,这个错低級但真实存在。如果你的MySQL也是跑在同一台ECS上,用localhost;如果数据库独立机器,用它的内网IP,不要用公网IP绕,走内网速度更快也更安全。
4.3 上传jar包与启动前的最后检查清单
jar包打好之后,上传方式看个人习惯。
bash复制# 用scp从本地直接推到服务器
scp target/app.jar root@你的公网IP:/opt/app/
# 或者在服务器上用rz命令(需要先安装lrzsz)
rz
上传完成后,先不要急着用nohup后台运行,我习惯先前台执行一次,把启动日志完整看一遍:
bash复制cd /opt/app
java -jar app.jar --spring.profiles.active=prod
看到 Started Application in x.xx seconds 字样,说明启动成功。再用 netstat -tlnp | grep 8080 确认端口在监听。一切正常后,Ctrl+C停掉,进入正式的进程管理环节。
这一步还能检查一个经常被忽略的问题:jar包大小。如果打出来的jar包只有几百KB,大概率是Spring Boot的maven插件没配好,依赖没有打进去,启动时必然报 ClassNotFoundException。正常的Spring Boot全家桶jar包,在50MB以上很常见。
5. 进程常驻与开机自启:从nohup到systemd的部署实践
5.1 nohup启动的局限:进程说挂就挂,系统重启就失联
网上大量教程教的是这一行:
bash复制nohup java -jar app.jar > app.log 2>&1 &
这个命令的作用是让进程在后台运行,终端关闭后不退出。但它的局限很明显:
- 进程异常退出后,没有任何机制把它拉起来
- 服务器重启后,不会自动恢复服务
- 日志输出全部重定向到app.log,没有日志轮转,时间长了文件可以到几个G
- 每次启动都要手动记录PID,停止服务时要自己
kill,非常原始
我见过不少用nohup部署的应用,夜里OOM崩了,第二天早上用户反馈才发现接口全挂。你问为什么没人管,因为进程根本没被管起来。
5.2 用systemd管好Java进程:一个unit文件搞定自动拉起和开机自启
现代Linux发行版都用systemd,这是最正规也最省心的进程管理方式。创建一个服务文件:
bash复制sudo vim /etc/systemd/system/app.service
内容如下:
ini复制[Unit]
Description=Java Backend Service
After=network.target mysqld.service
[Service]
Type=simple
User=deploy
WorkingDirectory=/opt/app
Environment="JAVA_HOME=/usr/lib/jvm/java-17-openjdk"
ExecStart=/usr/lib/jvm/java-17-openjdk/bin/java -Xms512m -Xmx512m -jar /opt/app/app.jar --spring.profiles.active=prod
SuccessExitStatus=143
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
逐行解释几个关键参数:
After=network.target mysqld.service:确保网络和MySQL先于应用启动,避免应用启动时数据库还没就绪User=deploy:不要用root跑应用,单独建一个低权限用户更安全Restart=always:进程异常退出后,5秒后自动重启SuccessExitStatus=143:这个很关键。Java进程收到systemd的停止信号(SIGTERM)时,退出的状态码是143,不声明这个字段的话,systemctl会把正常停止当作启动失败,日志里会记成错误
启动和设置开机自启:
bash复制sudo systemctl daemon-reload
sudo systemctl start app
sudo systemctl enable app
日常运维命令:
bash复制systemctl status app # 查看状态
systemctl restart app # 重启
systemctl stop app # 停止
journalctl -u app -f # 实时看日志
journalctl的日志相比nohup的app.log有个巨大优势:它按服务隔离,有索引,可以按时间查询,不会无限膨胀。当然你也可以在systemd里配置StandardOutput=append:/var/log/app.log 这样的方式输出到指定文件。
5.3 发布更新、回滚与磁盘卫生的一点点经验
有了systemd之后,发布新版本就变成了一套固定动作:
bash复制systemctl stop app
cp /opt/app/app.jar /opt/app/backup/app.jar.$(date +%Y%m%d%H%M).bak
# 上传新jar包到/opt/app/
systemctl start app
tail -f /var/log/app.log
注意这里停止服务用的不是 kill -9,而是 systemctl stop。它会先发SIGTERM让Spring Boot优雅停机,让正在处理中的请求有足够时间完成,然后再结束进程。直接 kill -9 强杀会出现数据不一致、连接池没释放的问题。
回滚也一样简单:systemctl stop app,把备份的jar复制回来,systemctl start app。所以备份目录保留最近两到三个版本就够,别无限攒,磁盘空间是钱。
说到磁盘,我顺手提醒一下:/opt 分区经常被忽略,日志、备份、jar包都堆在里面。建议给 /opt 单独划分一个数据盘(比如200G),或者至少定期检查 df -h,防止磁盘满了引发各种奇怪故障。这也回到第一节说的选型:买ECS时可以把数据盘加上,最初看起来每个月的成本多了几十块,但一旦因为你没规划空间导致服务宕掉,损失远不止这点钱。
6. 域名解析与HTTPS:免费SSL证书让接口跑得更稳更体面
6.1 域名备案与解析:中国大陆服务器的前置条件
服务跑起来了,但如果你的接口要面向外部用户,一直用 http://公网IP:8080 这种方式既不好记也不安全,还容易因为暴露端口被扫描爆破。正规的做法是绑定域名,并启用HTTPS。
这里有个绕不开的环节:中国大陆的云服务器上绑定域名,域名必须完成ICP备案。备案通常需要几个工作日到两周不等,不是一个即时操作。建议在部署前就先去阿里云控制台提备案申请,部署调试期间先用IP+端口应付着,备案通过后再上域名和HTTPS。
备案通过后,在域名解析控制台添加一条A记录,把域名指向ECS的公网IP。等DNS生效(一般几分钟到几小时),就可以用域名访问了。
6.2 免费SSL证书的申请、部署与免费续期
阿里云提供免费版SSL证书,单域名,有效期现在是3个月。很多人不知道的是:免费证书到期后需要手动续期,不是自动的。这里给出完整流程。
第一步,在阿里云控制台搜索“SSL证书”,进入证书服务,选择“免费证书”,申请一张单域名证书。填写你的域名,验证方式选DNS验证,系统会给你一条TXT记录,你去域名解析后台添加这条记录,等验证通过后证书就会签发。
第二步,下载Nginx格式的证书,得到两个文件:.pem(证书文件)和 .key(私钥文件)。把这两个文件上传到ECS的 /etc/nginx/ssl/ 目录。
第三步,配置Nginx反向代理。用Nginx代理Java应用的好处很多,比如可以统一处理HTTPS、可以做负载均衡、可以拦截恶意请求。一个最小可用的配置如下:
nginx复制server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/nginx/ssl/yourdomain.com.pem;
ssl_certificate_key /etc/nginx/ssl/yourdomain.com.key;
location / {
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;
}
}
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
配置好后:
bash复制nginx -t # 检查配置语法
systemctl reload nginx
proxy_set_header X-Forwarded-Proto $scheme 这行很关键,它告诉后端这次请求是通过HTTPS进来的,否则Spring Boot在生成重定向地址时可能使用http,导致回调地址错误。
然后再去ECS安全组把80和443端口放行,HTTPS就生效了。
关于免费续期,我分享一个实测有用的做法:证书有效期3个月,意味着你一年要续4次。由于免费证书提交申请后需要DNS验证,整个过程大约15分钟,我建议在手机日历里设两个提醒日:到期前10天和到期前3天。到期前10天提醒你申请新证书并部署,到期前3天作为兜底提醒,防止忘了。现在阿里云也在推自动DNS验证和证书托管,如果你的域名DNS也在阿里云,可以在证书服务里开启自动续期,省去手动操作的麻烦。
最后说说为什么要上HTTPS,不只是为了地址栏那个锁图标。现代浏览器的安全策略越来越严格,如果你在HTTPS页面里混入HTTP请求,会被浏览器拦截,前端工程师会因此抓狂。接口服务本身就应该是HTTPS的,尤其是涉及登录、支付、用户数据的场景,明文传输就是裸奔。
至此,从购入ECS到域名HTTPS全部跑通,一套Java后端服务的线上链路就完整了。这套流程我用了很多年,也帮很多朋友复现过,踩坑的点基本都集中在安全组、MySQL认证插件、JVM参数、systemd配置这几处,希望这篇能帮你绕过这些暗礁。
