背景
最近入手了一台 2核 2G (2H2G) 的云服务器,计划搭建基于 Java Spring Boot 的博客系统 Halo。作为 Java 开发者,我们都知道 JVM 是个"内存吞噬者"。在 2G 内存的物理机上,既要跑操作系统,又要跑 Docker 守护进程,还要跑数据库(PostgreSQL)和应用本身,如果不做精细化配置,OOM (Out of Memory) 几乎是必然的。
本文将从架构师视角,复盘如何通过分层优化,让 Java 应用在低配环境稳定运行。
一、 操作系统层:Swap 是一道防线
在生产环境的高性能集群中,我们通常会关闭 Swap 以避免磁盘 I/O 引起的性能抖动(Stop-the-world)。但在 2G 内存的小机器上,策略必须改变。
此时,Swap 不是为了扩容,而是为了防崩溃。当 JVM 或数据库偶尔出现内存峰值时,Swap 可以防止 Linux 的 OOM Killer 直接杀掉我们的主进程。
Bash
# 检查当前 Swap
free -h
# 如果没有,建议创建 2G 的 Swap 文件(与物理内存 1:1)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
架构思考:资源受限场景下,可用性(Availability)优先于极致的延迟(Latency)。
二、 容器层:资源限制 (Cgroups)
使用 Docker 部署时,必须限制容器的资源使用,防止某个容器(比如数据库)吃光所有内存导致宿主机死机。
在 docker-compose.yaml 中,我为应用和数据库都划定了界限:
YAML
services:
halo:
image: halohub/halo:2.20
deploy:
resources:
limits:
memory: 800M # 硬限制,超过即被 Kill
cpus: '1.0'
halodb:
image: postgres:15
deploy:
resources:
limits:
memory: 512M
三、 JVM 层:让 Java "感知" 容器
这是最关键的一步。在 JDK 8u191 之前,JVM 无法感知自己运行在容器内,它会读取宿主机的物理内存(比如宿主机 32G,容器限制 1G,JVM 依然觉得有 32G 可用),默认设置的 Heap 大小会直接撑爆容器。
虽然现在 JDK 版本大多已支持容器感知,但在 2G 这种极限环境下,依靠默认比例(通常是 1/4)依然不够精准。我们需要手动接管堆内存管理。
方案 A:硬编码 -Xms 和 -Xmx (传统做法)
YAML
environment:
- JVM_OPTS=-Xmx512m -Xms512m -Xss256k
优点:精准,绝不超标。
缺点:不灵活,升级配置需修改参数。
方案 B:使用 -XX:MaxRAMPercentage (云原生做法)
推荐使用百分比控制,让 JVM 自动计算。
YAML
environment:
- JVM_OPTS=-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=75.0
这意味着如果容器限制了 800M,堆内存最大为 600M,留 200M 给非堆内存(Metaspace, Code Cache, 线程栈等)。
注意:在小内存(<4G)场景下,建议预留至少 25-30% 给非堆内存。
四、 数据库调优:PostgreSQL 的克制
PostgreSQL 默认配置较为保守,但在 2G 机器上依然可能偏大。我们需要调整 shared_buffers。对于 Halo 这种读多写少的博客系统,数据库压力并不大。
建议在 Postgres 容器启动命令中加入: -c shared_buffers=128MB -c max_connections=50
五、 最终效果
经过上述调优,整套系统(Nginx + Halo + Postgres)在启动后内存占用稳定在 1.4G 左右,预留了约 600M 的缓冲空间。配合 Swap 机制,即使面对突发的爬虫流量,系统也表现出了良好的韧性。
总结
在有限的资源下做架构,本质上是在做Trade-off(权衡)。
OS层:开启 Swap 保底。
Docker层:设置 Limits 防止雪崩。
应用层:精准计算 Heap 与 Non-Heap 的比例。
这不仅仅是一次部署,更是一次对微服务资源隔离的微缩演练。