努力
加油

在内存为1GB的ECS上部署Spring Boot微服务需要注意什么?

1GB 内存的 ECS 跑 Spring Boot,这属于“极限操作”。在现在的技术环境下,Spring Boot 默认启动往往就要占用 200-400MB 甚至更多,再加上操作系统本身、数据库连接池、日志输出等开销,很容易直接 OOM(Out Of Memory)或者被系统 Swap 机制拖死。

如果你必须在这台机器上部署,或者这是为了压测/学习目的,以下是经过实战验证的优化清单,按优先级排序:

1. JVM 参数是生死线

这是最关键的一步。不要使用默认的 -Xmx 和 -Xms。

  • 设置堆内存上限:
    建议将最大堆内存设置为物理内存的 50%-60%。对于 1GB 机器,建议设为 -Xmx512m 或 -Xms512m。

    • java -jar app.jar -Xms512m -Xmx512m
    • 注意:一定要让 -Xms 和 -Xmx 相等,避免 JVM 动态调整堆大小带来的性能抖动和额外内存开销。
  • 开启 G1 GC:
    Java 8u191+ 或 Java 11+ 默认就是 G1,但如果是老版本 Java 8,务必指定:
    --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED (某些框架需要)
    更关键的是确保使用 G1 垃圾回收器:-XX:+UseG1GC。G1 对低延迟和大堆(相对小堆而言)更友好,且能更好地控制停顿时间。

  • 元空间(Metaspace)限制:
    Spring Boot 依赖较多,类加载多,默认 Metaspace 可能无限增长直到耗尽内存。
    设置 -XX:MaxMetaspaceSize=128m,防止元空间泄漏导致 OOM。

  • 关闭不必要的调试信息:
    去掉 -Xdebug、-Xrunjdwp 等开发期参数,生产环境不需要这些。

2. 应用层面的“瘦身”

  • 排除 Tomcat 容器:
    Spring Boot 默认内嵌 Tomcat,虽然方便,但有一定开销。如果追求极致,可以考虑切换到 Undertow(基于 Netty,内存占用更低,并发能力更强)。

    <!-- pom.xml -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
  • 精简依赖:
    检查 pom.xml / build.gradle,移除所有未使用的 starter。例如,如果不使用 Actuator,就删掉;如果不使用 JPA,就别引入 Hibernate 的大包。

  • 禁用自动配置:
    在 application.yml 或 main 方法中,通过 exclude 排除不需要的 AutoConfiguration 类。比如不用 Redis 就排除 RedisAutoConfiguration,不用 JPA 就排除 DataSourceAutoConfiguration 等。每少一个自动配置,JVM 初始化和运行时内存都省一点。

  • 异步日志 + 本地磁盘缓冲:
    内存紧张时,I/O 也会成为瓶颈。使用 Logback 或 Log4j2 时,配置异步 Appender,并将日志写入本地磁盘而非直接打印到 stdout/stderr(尤其是当 stdout 被重定向到文件时,频繁刷新会消耗大量 CPU 和内存)。

3. 操作系统与中间件协同

  • 关闭 Swap(交换分区):
    非常重要! Linux 的 Swap 机制会导致严重的性能抖动(Thrashing)。当内存不足时,系统会将部分内存页换出到磁盘,而磁盘 I/O 极慢,会导致整个服务响应极慢甚至假死。

    # 查看 swap
    free -h
    # 临时关闭
    sudo swapoff -a
    # 永久关闭需修改 /etc/fstab

    宁愿 OOM Crash 重启,也不要让系统进入 Swap 状态。配合 Kubernetes 或简单的监控脚本,在 OOM 后自动拉起即可。

  • 数据库连接池调优:
    如果你在同一台机器上部署了 MySQL/MongoDB 等,务必限制连接池大小。
    例如 HikariCP:

    spring:
      datasource:
        hikari:
          maximum-pool-size: 5  # 1GB 内存下,5-10 个连接足够,别设成 20+
          minimum-idle: 2

    每个连接都会占用一定内存(线程栈、缓冲区),连接数过多会迅速吃光内存。

  • 考虑嵌入式数据库或轻量级存储:
    如果可能,用 H2 内存数据库代替 MySQL,或用 SQLite 代替 MongoDB。对于 1GB 机器,跑一个完整的 MySQL 实例是非常奢侈且危险的。

4. 监控与兜底策略

  • 健康检查与自动重启:
    由于内存有限,长期运行可能出现内存泄漏或碎片化。建议在 ECS 上使用简单的守护进程(如 systemd 或 supervisor)监控应用进程,一旦崩溃立即重启。

    # /etc/systemd/system/myapp.service
    [Service]
    Restart=always
    RestartSec=5
    Environment="JAVA_OPTS=-Xms512m -Xmx512m -XX:+UseG1GC"
  • 启用 JVM 内置监控:
    开启 -XX:+UnlockDiagnosticVMOptions -XX:+PrintGCDetails 暂时用于排查,但生产环境建议接入 Prometheus + Grafana,重点监控 Heap Used, Non-Heap Used, GC Count, GC Time。

  • 阿里云/腾讯云原生监控:
    利用云厂商提供的云监控插件,设置内存使用率超过 85% 时报警,提前干预。

5. 架构层面的妥协

  • 单体 vs 微服务:
    在 1GB 内存上谈“微服务”是伪命题。真正的微服务意味着多个独立进程,每个都要有 JVM 开销。此时应将其视为单体应用(Monolith)来设计,只是代码结构上保持模块化。不要试图在一台 1GB 机器上同时运行 Eureka/Nacos + Gateway + 两个业务服务。

  • 缓存策略:
    避免在应用中创建过大的本地缓存(如 Caffeine/Guava Cache)。如果必须缓存,考虑使用外部 Redis(如果预算允许加一台便宜的 Redis 实例),否则本地缓存要严格设置 TTL 和最大条目数。

总结 checklist

  1. JVM: -Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxMetaspaceSize=128m
  2. 容器: 优先 Undertow,其次 Tomcat(调小 maxThreads)
  3. OS: 关闭 Swap
  4. DB: 连接池 ≤ 10
  5. 依赖: 剔除无用 Starter,禁用无用 AutoConfiguration
  6. 心态: 接受偶尔 OOM 重启,做好自动化运维准备

最后提醒:1GB 内存适合做 Demo、测试环境、极低流量的内部工具或边缘计算节点。对于面向公众的生产系统,至少升级到 2GB 起步,体验会有质的飞跃。