——在“小马拉大车”的现实里榨干每一 MB 性能

作者:magicxie

场景:IoT 远程打印

环境:8G 服务器 + 4G 消息中间件

目标:高并发、低延迟、不 OOM、不重复打印

前言

在上一篇文章中,我们聊了接口级故障的降级、熔断、限流和排队。但很多同学私信问我一个问题:

**“8G 内存真的够用吗?JVM 和 MQ 到底怎么配?”

说实话,在理想世界里,我会给你 32G、64G;但在现实项目里,我见过太多团队就是要在 8G 机器上硬扛几十万打印请求

这篇文章,不讲虚的,只讲我们在生产环境验证过的 JVM 和 MQ 参数调优实录


一、整体资源分配原则(非常重要)

在 8G 服务器上,绝对不能“All In”。

✅ 资源拆分(经验值)

组件

内存分配

说明

OS + 内核

1G

PageCache、TCP 缓冲区

JVM(应用)

3G

核心业务

MQ(Broker)

2.5G

消息堆积、索引

预留

1.5G

突发、GC、OS Cache

⚠️ 核心原则:

JVM 永远不要用到 4G 以上,否则 MQ 和 OS 会一起“窒息”。


二、JVM 调优实录(IoT 打印场景)

1️⃣ 基础 JVM 参数(生产可用)

-Xms3g
-Xmx3g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2

2️⃣ 为什么选 G1?

GC 类型

是否推荐

原因

CMS

已废弃,碎片严重

Parallel GC

⚠️

停顿时间长

G1 GC

停顿可控,适合 3G 堆

IoT 打印系统最怕:

  • Full GC 停顿 5s+

  • 打印机回调超时

  • 重复投递任务

G1 能把 STW 控制在 200ms 以内


3️⃣ 关键 JVM 参数解析

-XX:MaxGCPauseMillis=200

告诉 GC:你可以牺牲一点吞吐,但不能卡住接口

-XX:G1HeapRegionSize=16m
  • 默认是 1~32m

  • 打印任务对象不大,但数量多

  • 16m 是一个平衡点

-XX:ParallelGCThreads=4
  • 8G 机器,CPU 通常是 4C 或 8C

  • 不要打满 CPU,留资源给 MQ 和网络


4️⃣ OOM 防护(非常关键)

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump.hprof
-XX:OnOutOfMemoryError="kill -9 %p"

📌 原因:

  • 一旦发生 OOM,JVM 处于“半死不活”状态

  • 不如直接 Kill,由 Supervisor / Docker 重启


5️⃣ 元空间 & 直接内存(IoT 容易忽视)

-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m

MQ Client、Netty、NIO 都在用 Direct Memory

如果这里不设上限,OOM 会“假装在堆外”。


三、MQ(4G 环境)调优实录

假设我们使用的是 RocketMQ / Kafka 类中间件(RabbitMQ 思路类似)。


1️⃣ Broker 内存分配

-Xms2g
-Xmx2g

⚠️ 不要给 MQ 3G、4G

消息最终是靠 PageCache 读的,不是全放堆里。


2️⃣ 页缓存与刷盘策略(IoT 打印的关键)

✅ 异步刷盘(推荐)
flushDiskType=ASYNC_FLUSH

原因:

  • 打印任务允许毫秒级延迟

  • 同步刷盘会直接把 TPS 打残


3️⃣ 文件与内存映射

mapedFileSizeCommitLog=1G
mapedFileSizeConsumeQueue=5M
  • CommitLog 不宜过大

  • ConsumeQueue 小一点,减少内存占用


4️⃣ 消息堆积保护(非常重要)

maxMessageSize=65536
diskMaxUsedSpaceRatio=85

配置

作用

maxMessageSize

防止大任务撑爆内存

diskMaxUsedSpaceRatio

磁盘快满时拒绝写入

IoT 打印消息体一定要精简:

{
  "orderId": "...",
  "deviceId": "...",
  "fileId": "...",
  "copies": 1
}

文件本身 不要进 MQ,只存文件 ID。


5️⃣ Consumer 端背压(4G 环境必做)

pullBatchSize=8
consumeThreadMin=10
consumeThreadMax=20

❌ 不要开 64 / 128 线程

✅ 小批量 + 慢消费 = 稳定


四、线程池 & 连接池调优(常被忽略)

1️⃣ Tomcat 线程池

server:
  tomcat:
    max-threads: 200
    min-spare-threads: 20
    accept-count: 100
  • 200 是上限

  • 超过就排队,而不是扩容


2️⃣ DB 连接池(HikariCP)

hikari:
  maximum-pool-size: 30
  minimum-idle: 5
  connection-timeout: 2000

IoT 打印系统:

  • 大部分压力在 MQ

  • DB 只是记账,不需要大连接池


五、操作系统层调优(8G 机器必做)

1️⃣ 文件句柄

ulimit -n 65535

MQ + Netty + 打印机长连接,句柄消耗极快。


2️⃣ TCP 参数

net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_tw_reuse = 1

防止大量 TIME_WAIT 占满端口。


3️⃣ 交换分区(Swap)

vm.swappiness = 10
  • 完全关闭 Swap 风险高

  • 设为 10,只允许“最后关头”使用


六、监控与告警(调优闭环)

✅ JVM 监控指标

  • Old Gen 使用率 > 70%

  • Full GC 次数 > 0

  • GC 停顿时间 > 500ms

✅ MQ 监控指标

  • Consumer Lag > 1000

  • Broker Disk Usage > 80%

  • Send Latency > 200ms

✅ 业务指标(最关键)

  • 重复打印率

  • 订单状态不一致数

  • 打印机离线率


七、小结:小资源系统的生存法则

✅ 1. 不要迷信“大厂参数”

大厂 32G、64G 的参数,直接搬过来就是灾难


✅ 2. 资源分配优先级

OS
 ↓
MQ
 ↓
JVM
 ↓
业务线程

✅ 3. IoT 场景的特殊性

通用系统

IoT 打印

允许重试

禁止随意重试

可回滚

不可回滚

内存换吞吐

稳定压倒一切


✅ 4. 一句话总结

在 8G 内存的 IoT 系统里,调优不是“压榨性能”,而是“控制欲望”。


后记

这套 JVM + MQ 参数,在我们生产环境中稳定运行了 一年多,支撑了日均 20 万+ 打印订单,未发生一次因内存问题导致的资损事故。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐