news 2026/10/6 21:28:34

2G内存云服务器跑Spring Boot和MySQL的部署优化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2G内存云服务器跑Spring Boot和MySQL的部署优化实践

一台 2G 内存的云服务器,想同时跑 Spring Boot 应用和 MySQL,很多人第一反应是“别闹了”。我最初也是这么想的,结果第一次部署还真就翻车了:用的全是默认参数,Tomcat 默认 200 线程,Spring Boot 默认内存策略,MySQL 8.0 的 performance_schema 也没关,上线不到半天机器就卡到连 SSH 都敲不动。后来冷静下来重新规划,把 JVM 堆、连接池、线程池、MySQL 缓冲池全部按 2G 内存重新算了一遍,才终于把服务和数据库稳稳塞进去。这篇文章就是这次“从踩坑到上线”的完整记录,从选机、系统初始化、装 JDK 和 MySQL,到 Spring Boot 参数适配、systemd 托管、Nginx 反代,再到线上 OOM、连接池爆掉这类问题的排查思路。如果你是个人开发者,想在云服务器上跑自己的项目;或者公司只给测试环境批了一台 2G 小机器,这篇文章应该能帮你少走不少弯路。

1. 开局先认清现实:2G 内存到底够不够

1.1 先算一笔内存账

先说结论:2G 内存跑 Spring Boot + MySQL 是可行的,但前提是你必须知道每个组件大概吃多少内存,并且主动压掉默认参数里的浪费。

拿我这台服务器举例,操作系统最小化安装后常驻占用大概 200 到 300MB;MySQL 8.0 正常提供服务大约 400 到 600MB,其中 InnoDB 缓冲池是大头;Spring Boot 应用如果设置-Xmx512m,堆最多占 512MB,但 JVM 实际远不止这些,元空间、线程栈、JIT 编译器、GC 结构都要占额外内存,整体可能要 700 到 900MB。再加上 Nginx 几十MB,Page Cache、临时文件、突发请求所需的内存,2G 基本上是贴着上限走。

很多人在这一步就踩坑了,原因是默认配置完全不是按小内存机器设计的。JVM 默认最大堆是物理内存的四分之一,2G 机器上分到 512M,看起来不多但能接受;可是内嵌 Tomcat 默认会开 200 个线程,每个线程默认栈大小 1MB,光这一项就能吃掉 200MB 堆外内存。MySQL 8.0 默认开启 performance_schema,一个监控组件就能吃掉几百MB内存。问题不是“2G 能不能跑”,而是“默认参数大多是给 8G、16G 机器准备的”。

我最后实践出来的内存分配计划是这样:

组件内存占用预估说明
操作系统200 到 300MB最小化安装后
MySQL 8.0400 到 500MBbuffer pool 设 256M,关闭 performance_schema
Spring Boot JVM700 到 900MB-Xms256m -Xmx512m,线程池压到 50
Nginx20 到 50MB反向代理
其他缓冲和突发200 到 400MBPage Cache、日志、临时文件
swap2GB兜底,防止 OOM killer 直接杀进程

这样算下来,正常负载下内存占用在 1.6G 到 1.8G 之间,剩下几百MB留给系统做缓存和突发,再配 2G swap 做最后防线。只要流量别突然爆炸,是能稳住的。

1.2 适合跑什么样的项目

要说清楚一件事:2G 内存适合跑的绝对不是那种“目标用户是全网用户”的业务。它适合的是个人博客、低并发的业务后台 API、内部管理系统、学习用的练习项目,或者给两三个客户做演示环境。这类场景 QPS 往往是个位数,CPU 和内存压力都不大,2G 绰绰有余。

不适合的场景也很明确:电商秒杀、高并发接口、大量图片上传、定时跑大任务报表、在内存里做复杂计算。说得直接一点,如果业务流量真的起来了,2G 机器再优化也就是勉强顶一阵,该升配置还是要升配置。但反过来说,大多数个人项目在跑起来之前,根本不会有那么大的流量,与其一开始就买 4G、8G 的服务器过度消费,不如先用 2G 跑明白整个部署链路,把资源占用摸清楚,后面再扩容也知道该买多大内存。

我个人的判断标准很简单:如果这个项目的并发目标超过 50 个同时在线用户,我就不会用 2G 硬扛;如果只是“给一个小团队用的管理后台”,那 2G 完全够用,关键是别让内存被无效配置吃光。

2. 服务器选型与系统初始化

2.1 选机器和系统版本

2G 内存的云服务器,CPU 一般会给 1 核或 2 核。如果能选,我建议选 2 核,因为 JVM 启动、GC 和 Tomcat 处理请求对 CPU 都敏感,1 核在启动 Spring Boot 时会明显更慢,运行期一旦有点计算压力也容易卡。带宽和硬盘按需选,一般个人项目 20 到 40G 系统盘足够,带宽 3M 到 5M 也够用。

系统版本这里是个容易纠结的点。CentOS 7 已经停止维护,拿它做新项目不合适;Ubuntu/Debian 在云服务器上非常常见,包管理简单,适合新手;Rocky Linux 或 AlmaLinux 是 RHEL 的兼容发行版,很多公司生产环境都是这个路子。我这次用的是 Rocky Linux 9,主要是因为 systemd 管理服务、SELinux、firewalld 这些和主流企业环境一致,而且后面装官方 MySQL RPM 包也方便。

如果你用的是 Ubuntu,后面的dnf命令记得换成apt,其他思路完全一样。还有不少云厂商会给新用户提供免费试用的小机器,很多就是 2G 内存,拿来做学习和部署实战完全够用,没必要上来就买高配。

无论选哪家云厂商,有一个安全习惯必须养成:云安全组和服务器防火墙只放行 22、80、443 这三个端口。22 端口用来 SSH,80 和 443 是 HTTP 和 HTTPS 入口,MySQL 的 3306 端口绝对不能暴露到公网,否则很快会有扫描工具跑来爆破。如果你在本地要用 Navicat 之类的工具连数据库,后面用 SSH 隧道或者走应用层接口就行了。

2.2 系统基础设置

拿到一台新机器,先不要急着装环境,先把系统收拾利索。

首先是更新和基础工具:

dnf update -y dnf install -y curl wget unzip tar vim rsync

然后是时区。Java、MySQL、日志的时间戳都依赖系统时区,不统一的话排查问题会很痛苦:

timedatectl set-timezone Asia/Shanghai

接着创建一个部署用户,不要用 root 直接跑应用。这不是装样子,Spring Boot 进程一旦被攻破,root 权限意味着对方可以直接控制整台机器:

useradd -m -s /bin/bash deploy passwd deploy usermod -aG wheel deploy

然后创建 swap。这一步争议不大,2G 内存的机器确实需要 swap 兜底:

fallocate -l 2G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab echo 'vm.swappiness=10' >> /etc/sysctl.conf sysctl -p

为什么用 swappiness=10?因为我不想让系统积极使用 swap,只在内存真的紧张时才把冷数据换出去。如果 swappiness 保持默认 60,系统会太早把不太活跃的内存页交换到磁盘上,Java 进程一旦要从 swap 里读数据,速度会明显下降,表现出来就是“服务好像没死,但卡得没法用”。2G swap 的意义是防止 OOM killer 直接杀掉进程,不是用来替代物理内存的。

3. 安装 JDK 与 MySQL 的实操记录

3.1 JDK 安装细节

装 JDK 前先确认一下 Spring Boot 的版本。Spring Boot 2.x 用 Java 8 或 11 都行,Spring Boot 3.x 强制要求 Java 17 或更高。这次项目用的是 Spring Boot 3.2,所以装 OpenJDK 17。

在 Rocky Linux 9 上直接:

dnf install -y java-17-openjdk-devel java -version

注意不要只装java-17-openjdk,那个是 JRE,很多同学漏了-devel后缀,后面编译期项目在服务器上会报找不到javac。生产环境虽然只需要运行 jar 包,但直接装 JDK 最省心,避免奇奇怪怪的兼容问题。

如果你有多个 Java 版本切换的需求,用系统自带的 alternatives 管理:

alternatives --config java

Spring Boot 内置了 Tomcat,所以不需要单独安装 Tomcat。很多人第一次部署时习惯先去装个 Tomcat,再把 war 包丢进去,那都是老套路了,现代 Spring Boot 项目一个java -jar就能起来。

3.2 MySQL 安装方式对比与 RPM 安装流程

MySQL 安装方式有很多种,我在 2G 内存服务器上最推荐直接安装官方 RPM 包或发行版提供的 MySQL 服务包,不建议用 Docker 跑 MySQL。原因是 Docker 引擎本身就需要几百MB内存,容器里的进程再多一层面,相当于把本来就紧张的资源又分走一块,而且数据卷权限、容器重启策略、网络模式这些配置在 2G 小机器上会放大排查难度。我也见过有人docker run mysql失败了,一看错误就是没有给容器映射端口或者数据目录权限不对,这些在裸机上都不会遇到。

在线安装最省事的版本:

dnf install -y mysql-server systemctl start mysqld systemctl enable mysqld

不过要注意,某些发行版的 MySQL 初始化策略是生成临时密码写入日志,你需要这样找:

grep 'temporary password' /var/log/mysqld.log

然后用这个临时密码登录并修改:

mysql -uroot -p
ALTER USER 'root'@'localhost' IDENTIFIED BY 'Root@123456'; FLUSH PRIVILEGES;

MySQL 8.0 默认有 validate_password 组件,初始密码必须达到一定的复杂度,否则改密会报错。设置完密码后顺手跑一下安全初始化:

mysql_secure_installation

这个向导会问你是否删除匿名用户、禁止 root 远程登录、删除 test 数据库。到这一步建议全部选 Yes。

如果你需要离线安装或者指定 MySQL 版本,可以下载 MySQL 官方 RPM bundle 包,然后:

tar xvf mysql-8.0.36-1.el9.x86_64.rpm-bundle.tar dnf localinstall -y mysql-community-*.rpm

这里有个经验:RPM 安装时不要用rpm -ivh一个一个硬装,碰到依赖缺失还要手动去下载依赖,太浪费时间。直接用dnf localinstall,它会自动解析本地 RPM 包的依赖关系,遇到缺失的依赖也能从仓库里拉下来。

安装完成后,创建业务数据库和应用专用账号:

CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'App@123456'; GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost'; FLUSH PRIVILEGES;

这里我坚持用 utf8mb4,因为 utf8 在 MySQL 里存不了 emoji 和生僻字。还有,应用程序账号不要用 root,root 是留给运维管理的,应用账号只给某个库的权限就够了。

3.3 MySQL 8.0 参数调优

MySQL 装起来只是第一步,2G 内存下参数调优才是关键。修改/etc/my.cnf.d/mysql-server.cnf或者/etc/my.cnf,加入这样一段:

[mysqld] innodb_buffer_pool_size = 256M innodb_log_file_size = 64M innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 2 max_connections = 100 performance_schema = OFF skip-name-resolve long_query_time = 2 slow_query_log = ON slow_query_log_file = /var/log/mysql-slow.log

这些参数我一个个说清楚,知道为什么这么调比复制粘贴更有用:

innodb_buffer_pool_size = 256M是 InnoDB 用来缓存表数据和索引的内存区域。2G 内存下 256M 是平衡点,太小会导致 SQL 频繁走磁盘,太大就会挤压 Java 应用的内存。如果你项目的数据量真就几百MB,设成 256M 完全够了。

innodb_flush_log_at_trx_commit = 2是事务提交时只把 redo log 写到操作系统缓存,然后每秒刷一次盘,性能比默认值 1 好很多。代价是极端断电情况下可能丢最多 1 秒的事务。个人项目、内部系统可以接受,但如果是金融、交易类项目,必须保持默认值 1,不能顾此失彼。

performance_schema = OFF是我在 2G 机器上最推荐的设置。MySQL 8.0 默认开启 performance_schema,虽然它提供了很多性能监控数据,但代价是额外的 CPU 和内存开销,小内存机器上直接用SHOW PROCESSLIST和慢查询日志顶替就够了。

skip-name-resolve让 MySQL 不再对客户端 IP 做反向 DNS 解析。这个操作会减少连接耗时,也能省一点资源,但注意设置之后,授权表里的账号主机名必须写 IP 或 localhost,不能写域名。

slow_query_log和long_query_time = 2是给后续排查慢 SQL 用的,低流量应用开着问题不大,但日志文件要记得轮转,不然又会变成磁盘杀手。

改完配置重启 MySQL:

systemctl restart mysqld free -h

如果配置没有生效,用SHOW VARIABLES LIKE 'innodb_buffer_pool_size';验证一下。MySQL 8.0 即使关闭 performance_schema,自身基础占用也还有几百MB,不过这是正常现象,不要指望 MySQL 跑得像 Redis 那么省。

4. Spring Boot 项目本地瘦身与配置适配

4.1 依赖精简与启动参数

服务器端环境准备好了,接下来是在项目里做“瘦身”。很多 Spring Boot 项目启动慢、内存高,很大程度是依赖冗余造成的。用 IDEA 或 Maven 命令看一下依赖树,把无关的 starter 去掉。比如纯接口项目,spring-boot-starter-mail、spring-boot-starter-security用不到就别带;开发用的spring-boot-devtools一定要排除出生产包,它会额外占内存还可能在服务器上触发奇怪的重启。

打包命令:

mvn clean package -DskipTests

这里提醒一句:如果你用多模块 Maven 工程,记得先install公共模块,再打应用模块的包。我自己第一次打包时就遇到过子模块间依赖版本对不上,最后在服务器上跑起来才发现少了个 internal 类,白白折腾半天。

除了依赖,还有一个经常被忽略的开销是自动配置。Spring Boot 会按 classpath 扫描并自动装配很多组件。用不到的功能,可以通过@SpringBootApplication(exclude = ...)排除掉,例如:

@SpringBootApplication(exclude = { org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration.class })

但不要太激进,只排除明确不用的模块。如果你的服务不需要发邮件、不需要单独安全管理,排除之后启动确实会更快,内存也会降一点点。

4.2 Tomcat 与 HikariCP 参数适配 2G 内存

这是整篇文章最值得看的一段。Spring Boot 默认内嵌 Tomcat 线程数上限是 200,HikariCP 数据库连接池也有自己的默认值。这些默认设置在 2G 内存机器上是不合适的。

我在application.yml里是这样压的:

server: port: 8080 tomcat: max-threads: 50 min-spare-threads: 10 accept-count: 100 connection-timeout: 5000 keep-alive-timeout: 60000 spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

为什么 Tomcat 线程数要压到 50?因为每个线程默认栈是 1MB,200 个线程光栈内存就是 200MB,再加上线程调度开销,还没处理业务内存就先没了一半。50 个线程对低并发项目来说足够,配合 accept-count 100 做等待队列,即使有突发请求也不会一下子把内存打穿。如果你担心高峰期不够,可以先观察监控再加,不要一开始留太多余量。

HikariCP 为什么最大连接数设 20?连接池里的每个连接在 MySQL 端都是实实在在的内存占用,连接越多 MySQL 压力越大。20 个连接、5 个最小空闲,对单机应用完全够用。真正的并发瓶颈通常不在数据库连接数,而在慢 SQL 本身,连接池设再大也救不了全表扫描。

JVM 参数这样设置:

JAVA_OPTS="-Xms256m -Xmx512m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Dfile.encoding=UTF-8"

-Xms256m是让 JVM 启动时就预留 256M 堆,免得刚启动时频繁扩充堆内存;-Xmx512m是为堆上限封顶。-XX:MaxMetaspaceSize=256m是限制类元数据区,防止类加载多了失控。G1 垃圾收集器在 2 核机器上表现不错,如果你的服务器是 1 核,也可以换-XX:+UseSerialGC,内存占用会小一些,但 GC 停顿更明显。我个人还是偏向 G1,因为 Web 应用对响应时间有要求。

4.3 日志策略与临时目录问题

日志这个坑,通常不是第一天暴露的,而是在运行一周后慢慢体现出来。默认的 Spring Boot 日志会同时输出到控制台和文件,在 systemd 环境下控制台输出会被 journald 捕获,日积月累,/var/log/journal能占掉好几个G。日志文件不滚动的话,/opt/app/logs也会不断膨胀,最终把磁盘占满。

我建议生产环境用 logback 配置把日志输出到固定文件并做滚动,最简单的方式是直接在application.yml里配置:

logging: file: name: /opt/app/logs/myapp.log logback: rollingpolicy: max-file-size: 20MB max-history: 7

这样单个日志文件超过 20MB 就轮转,最多保留 7 天。同时可以把 ConsoleAppender 去掉,减少 systemd journal 的负担。

另一个容易忽略的是临时目录。Spring Boot 默认用/tmp作为内嵌 Tomcat 的临时工作目录,而很多操作系统的清理策略会定时清理/tmp下的文件。如果应用运行过程中发现找不到临时文件或者上传文件失败,可以考虑在配置里指定一个固定的临时目录:

server: tomcat: basedir: /opt/app/tomcat-base

我之前遇到过一次“文件上传到一半报 No space left on device”,检查了半天才发现是/tmp挂载的小分区被占满了,和应用日志位置完全不同,花了很长时间才定位到。

5. 部署上线全流程记录

5.1 目录规划与 jar 上传

部署时别把 jar 随便往/root下一扔就完事了。我习惯这样规划目录:

/opt/app ├── conf │ └── application-prod.yml ├── lib │ └── myapp-1.0.0.jar ├── logs └── backup

conf放外部配置文件,lib放 jar 包,logs放日志,backup放数据库备份和旧版本 jar。

上传 jar 用 rsync 比 scp 友好:

rsync -avzP target/myapp-1.0.0.jar deploy@server:/opt/app/lib/

然后建一个软链接:

ln -sfn /opt/app/lib/myapp-1.0.0.jar /opt/app/lib/current.jar

为什么用软链接?因为升级版本时只需要把新 jar 传上去,然后改一下current.jar指向,systemd 配置不用动;需要回滚到上一个版本,再把软链接指回去,重启服务就行。这在“呵,今天这个版本有问题”的时刻非常好用。

生产配置不要打进 jar 里,而是放在/opt/app/conf/application-prod.yml,启动时明确指定 profile:

java -jar /opt/app/lib/current.jar --spring.profiles.active=prod

数据库密码尤其不要写死在代码仓库里。我习惯用 environment 变量注入,例如application-prod.yml里:

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/myapp?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: myapp password: ${DB_PASSWORD}

然后/etc/myapp.env里写DB_PASSWORD=实际密码,并把文件权限设为 600,只允许 deploy 用户读取。这样即使 jar 包被下载,数据库密码也不会直接暴露。

5.2 用 systemd 托管 Spring Boot

再强调一次:不要用nohup java -jar xxx.jar &来跑生产应用。你现在图省事,后面进程挂了没人帮你拉起来,开机也不会自启,日志管理也麻烦。用 systemd 才是正经做法。

创建/etc/systemd/system/myapp.service:

[Unit] Description=My Spring Boot Application After=network.target mysqld.service Requires=mysqld.service [Service] Type=simple User=deploy Group=deploy WorkingDirectory=/opt/app EnvironmentFile=/etc/myapp.env ExecStart=/usr/bin/java -Xms256m -Xmx512m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -jar /opt/app/lib/current.jar --spring.profiles.active=prod SuccessExitStatus=143 Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target

说明几个关键点:

User=deploy让进程以普通用户身份运行,而不是 root。这是生产环境的基本安全意识。

EnvironmentFile=/etc/myapp.env会在启动前加载环境变量文件,Spring Boot 的${DB_PASSWORD}就从这里取。

SuccessExitStatus=143这个很多人不知道。systemd 停止服务时默认发 SIGTERM 信号给 JVM,Spring Boot 会执行优雅停机,然后进程退出。Java 进程收到 SIGTERM 后的退出码通常是 143,systemd 默认把非 0 退出码视为失败,所以你如果不加这一行,可能会看到“stop 一个服务,结果又被 systemd 自动拉起来”的诡异现象。

配置好之后:

systemctl daemon-reload systemctl enable --now myapp journalctl -u myapp -f

首次启动因为 JVM 需要加载大量类,慢一点是正常的。看到 “Started MyApplication” 或者端口监听出现后,再用:

ss -lntp | grep 8080

确认服务真的起来了。

5.3 Nginx 反向代理与 HTTPS

Spring Boot 的 8080 端口可以直接访问,但生产环境我建议在前面加一层 Nginx。这么做不是因为 Tomcat 不行,而是 Nginx 在处理静态资源、请求压缩、访问日志、限流、HTTPS 证书方面更顺手,而且证书更新时不用重启 Java 进程。

安装并启动:

dnf install -y nginx systemctl enable --now nginx

然后在/etc/nginx/conf.d/myapp.conf里:

server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; 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; proxy_connect_timeout 60s; proxy_read_timeout 60s; client_max_body_size 20m; } }

proxy_pass http://127.0.0.1:8080是指定后端服务地址。Spring Boot 还有一个配套设置,在application.yml里加上:

server: forward-headers-strategy: framework

否则应用不知道用户实际上是通过 HTTPS 访问的,某些重定向或链接会生成 http:// 地址。

HTTPS 证书现在主流做法是 Let's Encrypt + certbot,一条命令就能完成申请和配置:

dnf install -y certbot python3-certbot-nginx certbot --nginx -d example.com

申请前记得先把域名解析到服务器 IP,并保证 80 端口能被公网访问,certbot 要做域名验证。如果暂时不想上证书,先用 IP 直连也能跑,但生产环境一定要把 HTTPS 补上。证书快过期时 certbot 会自动续期,不需要你操心,这也是我推荐它而不是自己手动放证书文件的原因。

6. 线上问题与排查实录

6.1 内存不足与 OOM 的完整排查过程

这是 2G 内存服务器上最经典的问题。现象通常是运行几天后,应用响应变慢,最终 SSH 输入命令都要等好几秒,甚至服务直接消失。

第一步先看系统内存:

free -h df -h dmesg -T | grep -i "killed process"

如果dmesg里出现Out of memory: Kill process,说明操作系统 OOM killer 已经完全不管你的业务进程了,开始乱杀。这时候第一反应不应该是“给机器加内存”,而是先检查 JVM 和 MySQL 是不是有人越界乱吃。

看 JVM 内存状态:

jstat -gcutil <pid>

如果 Full GC 次数飙升,说明堆空间已经快满了。再用:

jstack <pid> | grep java.lang.Thread.State | wc -l

看看当前线程数,如果线程数已经几百个,那基本可以断定是线程池没压住,或者业务代码在线程池里堆积了大量任务。

在小内存机器上执行jmap -dump做堆转储要特别小心,因为堆转储文件通常和堆大小相当,可能在服务器上又占几百MB。我的建议是:低峰期抓取,抓到后立刻把 dump 文件拉到本地分析,不要在服务器上长时间保留。更多情况下,先看代码比看 dump 更有效——查一查有没有全表查询、有没有把大量数据一次性加载到内存里的逻辑,这类问题在 2G 机器上会被迅速放大。

我遇到的一次 OOM,最后定位到是定时任务里用list()查了整张表,几万条数据全塞进内存做过滤,直接把堆耗光。加了个LIMIT和数据库端过滤条件,内存占用立刻就下来了。

6.2 MySQL 连接数过多和连接被占用

另一个高频问题是应用报:

HikariPool-1 - Connection is not available, request timed out after 30000ms.

这表示应用从连接池拿不到连接了。登录 MySQL:

mysql -umyapp -p

然后:

SHOW FULL PROCESSLIST;

你会看到很多连接处于不同状态。大量的Sleep代表连接池里的空闲连接,这本身不可怕,但如果Threads_connected一直很高,比如超过 100,那就要怀疑连接池配置是不是太贪心了。大量Query状态且Time很高,说明有慢 SQL 在长期占用连接。

查看当前连接数和最大连接限制:

SHOW STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';

解决办法分两层:应用层把 HikariCP 的maximum-pool-size调小,10 到 20 足够,连接池不是越大越好;数据库层开慢查询日志,定位那条拖死连接池的 SQL。很多时候不是连接不够,而是每一条连接都被一条要跑几十秒的慢查询霸占着,连接轮转跟不上。

还有一种情况是代码里的事务没按时提交,连接被事务卡住不放,长时间不释放就占满了连接池。排查时可以在information_schema.INNODB_TRX里看看有没有长时间未结束的事务事务。

6.3 重启后服务起不来以及磁盘占满

在服务器上重启应用会遇到很多“明明我什么都没改就是起不来”的情况。这里给一套排查顺序:

第一,确认 MySQL 是否已经就绪。Spring Boot 启动时如果连不上数据库会直接失败退出。可以检查:

systemctl status mysqld ss -lntp | grep 3306

如果 MySQL 还没起来,应用当然会失败。systemd 的Requires=mysqld.service只是说“应用要跟着 MySQL 启动”,并不能保证 MySQL 内部初始化完成。针对这个问题,可以写一个启动前等待 MySQL 的脚本,或者在ExecStartPre里用mysqladmin ping探活。

第二,查看应用自己的日志:

journalctl -u myapp -e --no-pager

如果是端口被占用,你会看到Port 8080 was already in use。如果是密码环境变量没加载,会看到Access denied for user 'myapp'@'localhost'。

第三,检查软链接:

ls -l /opt/app/lib/current.jar

如果传了新 jar 但没有更新软链接,或者软链接指向的文件不存在,systemd 启动时会报找不到文件,这种低级问题我还真踩过一次。

第四,磁盘满。这个更隐蔽:

df -h

如果/分区使用率 100%,应用进程可能启动到一半就写不了日志,表现就是“一直 Restarting,但又不说为什么”。遇到这种情况先清理日志、回滚日志旧文件,把空间腾出来再谈其他问题。

6.4 部署常见问题速查表

做个速查表,方便以后排查:

现象可能原因快速排查解决建议
应用启动后立刻 OOMJVM 堆或线程数配置不合理dmesg -T | grep -i oom调低 Xmx、压线程池,检查业务代码
MySQL 服务起不来配置参数错误或数据目录权限异常journalctl -u mysqld -e修正配置,重置数据目录权限
JDBC 连接失败URL 写错、密码错误、3306 未放行telnet 127.0.0.1 3306确认本地连接,使用 127.0.0.1
SSL 连接错误本地连接时 useSSL 未设置JDBC 报 SSL 警告本地加useSSL=false&allowPublicKeyRetrieval=true,生产按需配置证书
日志磁盘占满日志没做滚动du -sh /opt/app/logs配置 logback 滚动策略或 logrotate
重启后无法访问端口被占用或 jar 软链接失效ss -lntp; ls -l current.jar清理占端口的进程,重建软链接
时间差 8 小时JVM/MySQL 时区不一致date; show variables like '%time%'统一 Asia/Shanghai,JDBC URL 加 serverTimezone
请求很慢但 CPU 不高连接池耗尽或慢 SQLSHOW FULL PROCESSLIST优化 SQL,检查连接池配置

7. 监控与后续维护建议

7.1 用 Spring Boot Admin 还是轻量脚本

不少同学看到“监控”第一反应是用 Spring Boot Admin 或者 Prometheus。这里我说点实在话:Spring Boot Admin 确实好用,但 2G 内存在本机跑一个 Admin Server 并不划算。Admin Server 本身也是一个 Spring Boot 应用,再小也得占 200M 左右内存,加上你的业务应用和 MySQL,内存就太紧了。

我更推荐这样组合:

应用侧开启 Spring Boot Actuator,暴露必要的端点:

management: endpoints: web: exposure: include: health,info,metrics

然后写一个轻量级监控脚本,每分钟检查一次服务状态:

if ! systemctl is-active --quiet myapp; then systemctl restart myapp echo "myapp restarted at $(date)" >> /opt/app/logs/restart.log fi

放到 crontab:* * * * * /opt/app/scripts/check_myapp.sh

如果老板或团队确实需要可视化面板,我建议把 Spring Boot Admin Server 部署在另一台开发机或本地电脑上,让服务器的应用通过 Actuator 端点注册过去,而不是硬塞进这台 2G 机器。这样既能看内存、线程、健康状态,又不消耗服务器资源。

7.2 定时备份与日志轮转

上线后有两件事不能拖:备份和日志轮转。

MySQL 备份用 mysqldump,放 crontab:

0 3 * * * mysqldump --single-transaction --defaults-extra-file=/etc/myapp-backup.cnf myapp | gzip > /opt/app/backup/myapp_$(date +\%F).sql.gz

--single-transaction可以避免备份过程中锁表,对 InnoDB 表很重要。--defaults-extra-file后面放的是独立配置文件,里面写备份账号的用户名密码,防止密码出现在 crontab 明文里。

备份文件保留 7 天:

find /opt/app/backup -name "*.sql.gz" -mtime +7 -delete

日志轮转用 logrotate。因为 Spring Boot 项目我前面已经用 logback 做了滚动,这里主要是兜底 Nginx 日志和 systemd journal。在/etc/logrotate.d/myapp写:

/opt/app/logs/*.log /var/log/nginx/*.log { daily rotate 7 compress missingok notifempty copytruncate }

另外限制 journald 日志体积:

mkdir -p /etc/systemd/journald.conf.d echo -e "[Journal]\nSystemMaxUse=100M" > /etc/systemd/journald.conf.d/limit.conf systemctl restart systemd-journald

7.3 从 2G 起步的平滑升级路径

最后聊一聊后续的演进思路,算是经验之谈。

如果你一开始只有 2G 机器,不要急着把所有中间件都塞进来。第一阶段就是 Spring Boot + MySQL + Nginx,最多加一个轻量脚本做监控,这是最稳定的结构。等业务量真的上来了,优先把 MySQL 迁到云厂商的 RDS 或单独一台大内存机器,因为 MySQL 对内存的需求通常比 Java 应用更刚性,应用服务器反而可以继续用 2G。到那时候你的 JVM 内存模型已经调得比较稳定,只需要保证 CPU 够用就行。

Redis 要不要上?2G 机器上要特别谨慎。Redis 本身很省,但它缓存的数据一多,内存就会急剧膨胀。我见过有人把 2G 服务器塞了 Redis 之后,缓存还没放几百MB,JVM 和 MySQL 就开始频繁 swap。如果确实需要 Redis,建议设好maxmemory,只缓存热点数据。

容器化这件事也一样。2G 机器上我真不推荐 Docker,不是技术问题,是资源和管理成本不划算。等业务到了需要容器化的规模,你大概率已经换 4G 或 8G 的实例了,那时候再用容器会更舒服。

最后分享一点个人体会。2G 内存部署 Spring Boot + MySQL 这件事,给我最大的收获不是“我能把内存压得多低”,而是学会了不再把默认配置当免死金牌。默认参数大多是给通用场景准备的,不是给某个 2G 小机器准备的。每一次遇到 OOM、连接池耗尽、重启失败,都要回到内存账本上看看分配逻辑。只要你把线程数、连接池、JVM 堆、MySQL buffer pool 都规划好,2G 机器其实能稳跑很长时间。我到现在还留着一台 2G 的云服务器,专门放一些自用 API 和客户演示环境,已经稳定跑了快一年没出过问题。后面如果业务真的涨上去了,再换更大的机器,那时候因为你对每个组件到底吃多少内存已经有了底,买多大内存也不会再靠猜。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 21:26:12

智慧矿山项目建设整体解决方案怎么读?从五层架构到落地避坑

简介&#xff1a;面向智慧矿山规划者、煤矿企业信息化部门及矿业工程从业人员的整体解决方案资料&#xff0c;聚焦“两化”深度融合下煤矿智能化建设路径。文档共938页&#xff0c;以单个PDF文件打包&#xff0c;压缩包大小12.28MB&#xff0c;内容系统完整&#xff0c;便于按章…

作者头像 李华
网站建设 2026/10/6 21:26:02

基于Simulink自动化建模的MBD模型管理工具探索

做MBD&#xff08;Model-Based Design&#xff09;开发有一阵了&#xff0c;Simulink模型就是我们团队的“代码”和“产品核心”。但前两年我逐渐发现一个让人坐不住的问题&#xff1a;模型变多之后&#xff0c;管理方式还是原来的Excel清单加口头约定&#xff0c;评审时大家一…

作者头像 李华
网站建设 2026/10/6 21:20:38

Claude Code营销技能实战:SEO、CRO与FAQ结构化数据自动化

1. 从"marketingskills"这个标题说起&#xff1a;它到底在解决什么问题 第一次看到"marketingskills"这个词&#xff0c;我脑子里冒出来的不是某个具体工具&#xff0c;而是一类很实际的需求&#xff1a;做独立站、做内容、做增长的人&#xff0c;手里有一…

作者头像 李华
网站建设 2026/10/6 21:19:43

认知几何学:测量并重塑你的意义空间

刚开始接触这套思路的时候&#xff0c;我其实是被一个非常普通的现象逼到墙角的&#xff1a;同一份产品需求文档&#xff0c;交给团队里两位经验差不多的工程师&#xff0c;一个人说“这个需求很清晰&#xff0c;照着拆就行”&#xff0c;另一个人却眉头紧锁&#xff0c;说“这…

作者头像 李华
网站建设 2026/10/6 21:18:55

Claude Code 终端AI助手:安装部署、多模型接入与排错实战

每天泡在终端里的朋友&#xff0c;应该都有过这种体验&#xff1a;改几十个文件名、批量替换配置文件、跑完测试再修报错&#xff0c;这些重复动作明明有规律&#xff0c;却还是要一次次手动敲命令。Claude Code 这类工具的出现&#xff0c;算是把“命令行”和“对话式 AI”真正…

作者头像 李华