SpringBoot处理1 次 Http请求消耗多少堆内存?数据可能令你惊讶!

2,034 阅读8分钟

在实际工作中,经常会需要进行在全链路压测,优化 GC参数,优化 JVM 内存分配。

当知道 1 次 RPC 请求和 Http 请求需要的堆内存大小后,你可以精确地计算:指定的并发量之下,系统需申请多少堆内存。同时结合 JVM 新生代堆大小,就能推算出 1 分钟发生多少次 GC,这个 GC频率是否过于频繁?从而针对性的优化。

我们希望 1 次 Rpc、Http 请求申请堆内存足够少,这样可减少 GC 导致的系统停顿,提高系统性能,单机可以支撑更高的并发量。

1次 Http 请求,申请多少堆内存?

1 次 RPC 请求,申请多少堆内存?

如果不亲自实验,无法得出结论。

1. 实验思路

关键动作

  1. 创建SpringBoot新应用(版本2.5.4)。
  2. 新增 Post 接口,供 JMeter 调用。
  3. JMeter(开源压测工具)新建测试计划。每个线程执行2000 次Http接口调用,共10 个线程,总调用 20000 次。
  4. SpringBoot 打印 GC 详细日志,记录GC 前后,新生代申请了多少内存。

Jmeter 调用 20000 次 Http 接口以后,通过手动 GC 的方式触发 GC,通过 GC 详细日志计算压测期间新生代堆内存增长量。(对象基本分配在新生代)

2. SpringBoot 声明 Http 接口

如下代码声明了一个 Post接口 create; 创建了 Get 接口,用于触发GC。

@Slf4j
@RestController
public class TestController {

   private AtomicLong count = new AtomicLong(0);

   @ResponseBody
   @RequestMapping(value = "create", method = RequestMethod.POST)
   public String create(@RequestBody Order order) {
      //log.warn("收到提单请求 cnt{}:{}", count.getAndIncrement(), order);
      return "ok";
   }

   @ResponseBody
   @RequestMapping(value = "gc", method = RequestMethod.GET)
   public String gc() {
      System.gc();
      return "ok";
   }
}

3. JMeter 新建测试计划

3.1 新增线程组

新建线程组,选择 10 个线程,每个线程循环 2000次。 image.png

image.png

3.2 新建 Http 默认值

image.png

3.3 新建请求头

由于请求体是 JSON,所以新增请求头 Content-Type image.png

3.4 新建 Http 请求

请求中指定 Url 和 请求体 image.png

4. 实验过程

4.1 启动 SpringBoot应用

堆内存大小 4G,其中新生代内存 2G。SurivivorRadio=8,即每个 Surivivor 占比新生代 1/10。

指定GC日志位置:-Xloggc:/Users/testUser/log/gc.log

 java -server 
 -Xmx4g -Xms4g -XX:SurvivorRatio=8 -Xmn2g
 -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1g -XX:MaxDirectMemorySize=1g 
 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCCause -XX:+PrintGCDetails 
 -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution 
 -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=32768 
 -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
 -XX:ParallelCMSThreads=6 -XX:+CMSClassUnloadingEnabled 
 -XX:+UseCMSCompactAtFullCollection -XX:+CMSParallelInitialMarkEnabled 
 -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+PrintHeapAtGC 
 -XX:CMSFullGCsBeforeCompaction=1 -XX:CMSInitiatingOccupancyFraction=70 
 -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintReferenceGC  
 -XX:+ParallelRefProcEnabled -XX:ReservedCodeCacheSize=256M 
 -Xloggc:/Users/testUser/log/gc.log
 -jar target/activiti-0.0.1-SNAPSHOT.jar

4.2 多次手动 GC

由于JVM 启动过程中,需要加载大量对象,所以我们在压测之前先手动 GC,清理一下存量对象。

curl http://localhost:8080/gc

4.3 Jmeter 启动压测

执行 JMeter压测计划,每次执行会调用 20000 次, image.png

4.4 GC 日志解读

GC 以后,新生代 Eden 区已使用内存为 0。 GC 前 Eden区大小就是 20000 次 Http 调用所申请的内存总和! image.png

5. 实验结果

SpringBoot 在处理 Http 请求时,即使请求体相对较小,平均每次 Http 调用仍会申请约 34 K 的堆内存。这一点显得尤为突出,因为请求体仅包含 50 个字符,远远未达到 1K 大小。然而,每次 Http 请求所消耗的内存却依然高达 34K。这可能是由于在 SpringBoot 的内部处理流程中需要创建多个对象,这些对象的总内存占用显著高于请求体本身。

{"userId": 32898493, "productId":39043, "detail": ""}

在调整 Http 请求后,如将 detail 字段设置为 1200 个字符时,每次 Http 调用平均占用堆内存为 36K。两次实验结果间的差异为 2K,这与 1200 个字符占用的内存大小基本持平(需考虑一定的误差)。

这表明 SpringBoot 内部未进行多次请求体拷贝。

5.1 添加日志打印

log.warn("收到提单请求 cnt{}:{}", count.getAndIncrement(), order);

在打印请求日志后,单次 Http 请求的平均内存使用量达到了 56 KB,比之前增加了整整 20 KB。

然而,当我移除 detail 字段后,单次请求的内存使用骤降至 35.7 KB。

这表明,当日志量较小时,打印日志对内存占用的影响较小。但随着日志大小的增加,内存占用显著上升,这可能触发更频繁地GC,最终导致系统性能明显下降。

因此,建议各位严格控制单条日志的大小,以优化内存使用和系统性能。

Case内存消耗
小请求34K
小请求 + 打日志35.7 K
大请求36K
大请求 + 打日志56K

6. 真实的数据

根据以上实验结果可得出结论:单次 HTTP 请求消耗约34KB内存,这并不意味着所有SpringBoot应用的内存消耗都是如此。由于实验所用的代码相对简单,因此34KB可能是内存消耗的最小值。

举例来说,在我司的线上环境中,单次RPC请求的内存消耗在 0.5MB 到 1MB 之间,内存占用量相对较大。

这是因为复杂的基础架构、复杂的业务逻辑、复杂的流程、多次下游调用、多次SQL 调用、多次缓存调用、日志打印等等 均需要消耗大量的内存!

在此之前,我一直对新生代 Eden 区高达 5G 的情况下,仍每分钟进行 1-2 次 young GC 感到困惑。

经过粗略计算后发现,如果每次请求消耗 0.5M 内存,当单台服务器每秒并发度达到 500 次时,每分钟需要分配的内存高达 15G。因此,至少需要进行3次 young GC 才能满足需求。

我是五阳,关注我,追踪更多我在大厂的工作经历和思考!

我的开源项目

最后夹带一点私货,五阳最近花了3个月的时间完成一个开源项目。

开源3周以来,已有近 230 多个关注和Fork

Gitee:gitee.com/juejinwuyan…

GitHub github.com/juejin-wuya…

开源平台上有很多在线商城系统,功能很全,很完善,关注者众多,然而实际业务场景非常复杂和多样化,开源的在线商城系统很难完全匹配实际业务,广泛的痛点是

  • 功能堆砌,大部分功能用不上,需要大量裁剪;
  • 逻辑差异点较多,需要大量修改;
  • 功能之间耦合,难以独立替换某个功能。

由于技术中间件功能诉求较为一致,使用者无需过多定制化,技术中间件开源项目以上的痛点不明显,然而电商交易等业务系统虽然通用性较多,但各行业各产品的业务差异化极大,所以导致以上痛点比较明显

所以我在思考,有没有一个开源系统,能提供电商交易的基础能力,能让开发者搭积木的方式,快速搭建一个完全契合自己业务的新系统呢?

  • 他们可以通过编排和配置选择自己需要的功能,而无需在一个现成的开源系统上进行裁剪
  • 他们可以轻松的新增扩展业务的差异化逻辑,不需要阅读然后修改原有的系统代码!
  • 他们可以轻松的替换掉他们认为垃圾的、多余的系统组件,而不需要考虑其他功能是否会收到影响

开发者们,可以择需选择需要的能力组件,组件中差异化的部分有插件扩展点能轻松扩展。或者能支持开发者快速的重新写一个完全适合自己的新组件然后编排注册到系统中?

memberclub 就是基于这样的想法而设计的。 它的定位是电商类交易系统工具箱, 以SDK方式对外提供通用的交易能力,能让开发者像搭积木方式,从0到1,快速构建一个新的电商交易系统!

image.png

具体介绍可参见

Gitee开源地址gitee.com/juejinwuyan…

GitHub开源地址 : github.com/juejin-wuya…

在这个项目中你可以学习到 SpringBoot 集成 以下框架或组件。

  1. Mybatis、Mybatis-plus 集成多数据源
  2. Sharding-jdbc 多数据源分库分表
  3. redis/redisson 缓存
  4. Apollo 分布式配置中心
  5. Spring Cloud 微服务全家桶
  6. RabbitMq 消息队列
  7. H2 内存数据库
  8. Swagger + Lombok + MapStruct

同时你也可以学习到以下组件的实现原理

  1. 流程引擎的实现原理
  2. 扩展点引擎实现原理
  3. 分布式重试组件实现原理
  4. 通用日志组件实现原理 参考:juejin.cn/post/740727…
  5. 商品库存实现原理: 参考:juejin.cn/post/731377…
  6. 分布式锁组件: 参考:
  7. Redis Lua的使用
  8. Spring 上下文工具类 参考: juejin.cn/post/746927…