
一台 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 到 300MBMySQL 8.0 正常提供服务大约 400 到 600MB其中 InnoDB 缓冲池是大头Spring Boot 应用如果设置-Xmx512m堆最多占 512MB但 JVM 实际远不止这些元空间、线程栈、JIT 编译器、GC 结构都要占额外内存整体可能要 700 到 900MB。再加上 Nginx 几十MBPage 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_schemaSpring Boot JVM700 到 900MB-Xms256m -Xmx512m线程池压到 50Nginx20 到 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 端口用来 SSH80 和 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.swappiness10 /etc/sysctl.conf sysctl -p为什么用 swappiness10因为我不想让系统积极使用 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 javaSpring 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 -pALTER USER rootlocalhost IDENTIFIED BY Root123456; 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 myapplocalhost IDENTIFIED BY App123456; GRANT ALL PRIVILEGES ON myapp.* TO myapplocalhost; FLUSH PRIVILEGES;这里我坚持用 utf8mb4因为 utf8 在 MySQL 里存不了 emoji 和生僻字。还有应用程序账号不要用 rootroot 是留给运维管理的应用账号只给某个库的权限就够了。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 用的低流量应用开着问题不大但日志文件要记得轮转不然又会变成磁盘杀手。改完配置重启 MySQLsystemctl 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 线程数上限是 200HikariCP 数据库连接池也有自己的默认值。这些默认设置在 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因为每个线程默认栈是 1MB200 个线程光栈内存就是 200MB再加上线程调度开销还没处理业务内存就先没了一半。50 个线程对低并发项目来说足够配合 accept-count 100 做等待队列即使有突发请求也不会一下子把内存打穿。如果你担心高峰期不够可以先观察监控再加不要一开始留太多余量。HikariCP 为什么最大连接数设 20连接池里的每个连接在 MySQL 端都是实实在在的内存占用连接越多 MySQL 压力越大。20 个连接、5 个最小空闲对单机应用完全够用。真正的并发瓶颈通常不在数据库连接数而在慢 SQL 本身连接池设再大也救不了全表扫描。JVM 参数这样设置JAVA_OPTS-Xms256m -Xmx512m -XX:MaxMetaspaceSize256m -XX:UseG1GC -XX:MaxGCPauseMillis200 -Dfile.encodingUTF-8-Xms256m是让 JVM 启动时就预留 256M 堆免得刚启动时频繁扩充堆内存-Xmx512m是为堆上限封顶。-XX:MaxMetaspaceSize256m是限制类元数据区防止类加载多了失控。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 └── backupconf放外部配置文件lib放 jar 包logs放日志backup放数据库备份和旧版本 jar。上传 jar 用 rsync 比 scp 友好rsync -avzP target/myapp-1.0.0.jar deployserver:/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启动时明确指定 profilejava -jar /opt/app/lib/current.jar --spring.profiles.activeprod数据库密码尤其不要写死在代码仓库里。我习惯用 environment 变量注入例如application-prod.yml里spring: datasource: url: jdbc:mysql://127.0.0.1:3306/myapp?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue 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] DescriptionMy Spring Boot Application Afternetwork.target mysqld.service Requiresmysqld.service [Service] Typesimple Userdeploy Groupdeploy WorkingDirectory/opt/app EnvironmentFile/etc/myapp.env ExecStart/usr/bin/java -Xms256m -Xmx512m -XX:MaxMetaspaceSize256m -XX:UseG1GC -jar /opt/app/lib/current.jar --spring.profiles.activeprod SuccessExitStatus143 Restarton-failure RestartSec10 [Install] WantedBymulti-user.target说明几个关键点Userdeploy让进程以普通用户身份运行而不是 root。这是生产环境的基本安全意识。EnvironmentFile/etc/myapp.env会在启动前加载环境变量文件Spring Boot 的${DB_PASSWORD}就从这里取。SuccessExitStatus143这个很多人不知道。systemd 停止服务时默认发 SIGTERM 信号给 JVMSpring Boot 会执行优雅停机然后进程退出。Java 进程收到 SIGTERM 后的退出码通常是 143systemd 默认把非 0 退出码视为失败所以你如果不加这一行可能会看到“stop 一个服务结果又被 systemd 自动拉起来”的诡异现象。配置好之后systemctl daemon-reload systemctl enable --now myapp journalctl -u myapp -f首次启动因为 JVM 需要加载大量类慢一点是正常的。看到 “Started MyApplication” 或者端口监听出现后再用ss -lntp | grep 8080确认服务真的起来了。5.3 Nginx 反向代理与 HTTPSSpring 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 证书现在主流做法是 Lets 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.这表示应用从连接池拿不到连接了。登录 MySQLmysql -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 的Requiresmysqld.service只是说“应用要跟着 MySQL 启动”并不能保证 MySQL 内部初始化完成。针对这个问题可以写一个启动前等待 MySQL 的脚本或者在ExecStartPre里用mysqladmin ping探活。第二查看应用自己的日志journalctl -u myapp -e --no-pager如果是端口被占用你会看到Port 8080 was already in use。如果是密码环境变量没加载会看到Access denied for user myapplocalhost。第三检查软链接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.1SSL 连接错误本地连接时 useSSL 未设置JDBC 报 SSL 警告本地加useSSLfalseallowPublicKeyRetrievaltrue生产按需配置证书日志磁盘占满日志没做滚动du -sh /opt/app/logs配置 logback 滚动策略或 logrotate重启后无法访问端口被占用或 jar 软链接失效ss -lntp; ls -l current.jar清理占端口的进程重建软链接时间差 8 小时JVM/MySQL 时区不一致date; show variables like %time%统一 Asia/ShanghaiJDBC 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放 crontab0 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]\nSystemMaxUse100M /etc/systemd/journald.conf.d/limit.conf systemctl restart systemd-journald7.3 从 2G 起步的平滑升级路径最后聊一聊后续的演进思路算是经验之谈。如果你一开始只有 2G 机器不要急着把所有中间件都塞进来。第一阶段就是 Spring Boot MySQL Nginx最多加一个轻量脚本做监控这是最稳定的结构。等业务量真的上来了优先把 MySQL 迁到云厂商的 RDS 或单独一台大内存机器因为 MySQL 对内存的需求通常比 Java 应用更刚性应用服务器反而可以继续用 2G。到那时候你的 JVM 内存模型已经调得比较稳定只需要保证 CPU 够用就行。Redis 要不要上2G 机器上要特别谨慎。Redis 本身很省但它缓存的数据一多内存就会急剧膨胀。我见过有人把 2G 服务器塞了 Redis 之后缓存还没放几百MBJVM 和 MySQL 就开始频繁 swap。如果确实需要 Redis建议设好maxmemory只缓存热点数据。容器化这件事也一样。2G 机器上我真不推荐 Docker不是技术问题是资源和管理成本不划算。等业务到了需要容器化的规模你大概率已经换 4G 或 8G 的实例了那时候再用容器会更舒服。最后分享一点个人体会。2G 内存部署 Spring Boot MySQL 这件事给我最大的收获不是“我能把内存压得多低”而是学会了不再把默认配置当免死金牌。默认参数大多是给通用场景准备的不是给某个 2G 小机器准备的。每一次遇到 OOM、连接池耗尽、重启失败都要回到内存账本上看看分配逻辑。只要你把线程数、连接池、JVM 堆、MySQL buffer pool 都规划好2G 机器其实能稳跑很长时间。我到现在还留着一台 2G 的云服务器专门放一些自用 API 和客户演示环境已经稳定跑了快一年没出过问题。后面如果业务真的涨上去了再换更大的机器那时候因为你对每个组件到底吃多少内存已经有了底买多大内存也不会再靠猜。