每天 100 万次登陆请求,8G 内存该如何设置 JVM 参数,大概可以分为以下 8 个步骤 。
第一步、新系统上线如何规划容量?
1. 套路总结
任何新的业务系统在上线以前都需要去估算服务器配置和 JVM 的内存参数,这个容量与资源规划并不仅仅是系统架构师的随意估算的,需要根据系统所在业务场景去估算,推断出来一个系统运行模型,评估 JVM 性能和 GC 频率等等指标。以下是我结合大牛经验以及自身实践来总结出来的一个建模步骤:
-
计算业务系统每秒钟创建的对象会佔用多大的内存空间,然后计算集群下的每个系统每秒的内存占用空间(对象创建速度);
-
设置一个机器配置,估算新生代的空间,比较不同新生代大小之下,多久触发一次 MinorGC;
-
为了避免频繁 GC,就可以重新估算需要多少机器配置,部署多少台机器,给 JVM 多大内存空间,新生代多大空间;
-
根据这套配置,基本可以推算出整个系统的运行模型,每秒创建多少对象,1 秒以后成为垃圾,系统运行多久新生代会触发一次 GC,频率多高。
2. 套路实战:以登录系统为例
有些同学看到这些步骤还是发憷,说的好像是那么回事,一到实际项目中到底怎么做我还是不知道。光说不练假把式,以登录系统为例模拟一下推演过程:
-
假设每天 100 万次登陆请求,登陆峰值在早上,预估峰值时期每秒 100 次登陆请求;
-
假设部署 3 台服务器,每台机器每秒处理 30 次登陆请求。假设一个登陆请求需要处理 1 秒钟,JVM 新生代里每秒就要生成 30 个登陆对象,1 秒之后请求完毕这些对象成为了垃圾;
-
一个登陆请求对象假设 20 个字段,一个对象估算 500 字节,30 个登陆佔用大约 15kb。考虑到 RPC 和 DB 操作,网络通信、写库、写缓存一顿操作下来,可以扩大到 20-50 倍,大约 1 秒产生几百 K~1M 数据;
-
假设 2C4G 机器部署,分配 2G 堆内存,新生代则只有几百 M,按照 1M/s 的垃圾产生速度,几百秒就会触发一次 MinorGC 了;
-
假设 4C8G 机器部署,分配 4G 堆内存,新生代分配 2G,如此需要几个小时才会触发一次 MinorGC。
所以,可以粗略的推断出来一个每天 100 万次请求的登录系统,按照 4C8G 的 3 实例集群配置,分配 4G 堆内存、2G 新生代的 JVM,可以保障系统的一个正常负载。
基本上把一个新系统的资源评估了出来,所以搭建新系统要每个实例需要多少容量多少配置,集群配置多少个实例等等这些,并不是拍拍脑袋和胸脯就可以决定的下来的。
第二步、如何进行垃圾回收器的选择
吞吐量还是响应时间?
首先引入两个概念——吞吐量和低延迟。
吞吐量 = CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)
响应时间 = 平均每次的GC的耗时
通常,吞吐优先还是响应优先这个在JVM中是一个两难之选。
堆内存增大,gc一次能处理的数量变大,吞吐量大;但是gc一次的时间会变长,导致后面排队的线程等待时间变长;相反,如果堆内存小,gc一次时间短,排队等待的线程等待时间变短,延迟减少,但一次请求的数量变小(并不绝对符合)。
无法同时兼顾,是吞吐优先还是响应优先,这是一个需要权衡的问题。
CMS和G1
目前主流的垃圾回收器配置是新生代采用ParNew,老年代采用CMS组合的方式,或者是完全采用G1回收器,
从未来的趋势来看,G1是官方维护和更为推崇的垃圾回收器。
业务系统:
-
延迟敏感的推荐CMS;
-
大内存服务,要求高吞吐的,采用G1回收器!
CMS垃圾回收器的工作机制
CMS主要是针对老年代的回收器,老年代是标记-清除,默认会在一次FullGC算法后做整理算法,清理内存碎片。
-
优点:并发收集、主打“低延时” 。在最耗时的两个阶段都没有发生STW,而需要STW的阶段都以很快速度完成。
-
缺点:1、消耗CPU;2、浮动垃圾;3、内存碎片
-
适用场景:重视服务器响应速度,要求系统停顿时间最短。
总之:
业务系统,延迟敏感的推荐CMS;
大内存服务,要求高吞吐的,采用G1回收器!
第三步、如何对各个分区的比例、大小进行规划
首先,JVM最重要最核心的参数是去评估内存和分配,第一步需要指定堆内存的大小。
- -Xms 初始堆大小
- -Xmx 最大堆大小
其次,需要指定-Xmn新生代的大小,这个参数非常关键,灵活度很大。
- -Xmn新生代的大小
sun官方推荐为3/8大小,但是要根据业务场景来定,针对于无状态或者轻状态服务(现在最常见的业务系统如Web应用)来说,一般新生代甚至可以给到堆内存的3/4大小;
而对于有状态服务(常见如IM服务、网关接入层等系统)新生代可以按照默认比例1/3来设置。服务有状态,则意味著会有更多的本地缓存和会话状态信息常驻内存,应为要给老年代设置更大的空间来存放这些对象。
最后,是设置-Xss栈内存大小,设置单个线程栈大小。
默认值和JDK版本、系统有关,一般默认512~1024kb。一个后台服务如果常驻线程有几百个,那么栈内存这边也会占用了几百M的大小。
一份通用的JVM参数模板
基于4C8G系统的ParNew+CMS回收器模板(响应优先),新生代大小根据业务灵活调整!
-Xms4g
-Xmx4g
-Xmn2g
-Xss1m
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=10
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+AlwaysPreTouch
-XX:+HeapDumpOnOutOfMemoryError
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:gc.log
如果是GC的吞吐优先,推荐使用G1,基于8C16G系统的G1回收器模板:
G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,
即调整-XX:MaxGCPauseMillis=N
参数,这也符合G1的目的——让GC调优尽量简单!
同时也不要自己显式设置新生代的大小(用-Xmn或-XX:NewRatio参数),
如果人为干预新生代的大小,会导致目标时间这个参数失效。
-Xms8g
-Xmx8g
-Xss1m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=150
-XX:InitiatingHeapOccupancyPercent=40
-XX:+HeapDumpOnOutOfMemoryError
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:gc.log
针对-XX:MaxGCPauseMillis
来说,参数的设置带有明显的倾向性:调低↓:延迟更低,但MinorGC频繁,MixGC回收老年代区减少,增大Full GC的风险。调高↑:单次回收更多的对象,但系统整体响应时间也会被拉长。
针对InitiatingHeapOccupancyPercent
来说,调参大小的效果也不一样:调低↓:更早触发MixGC,浪费cpu。调高↑:堆积过多代回收region,增大FullGC的风险。
调优总结
系统在上线前的综合调优思路:
1、业务预估:根据预期的并发量、平均每个任务的内存需求大小,然后评估需要几台机器来承载,每台机器需要什么样的配置。
2、容量预估:根据系统的任务处理速度,然后合理分配Eden、Surivior区大小,老年代的内存大小。
3、回收器选型:响应优先的系统,建议采用ParNew+CMS回收器;吞吐优先、多核大内存(heap size≥8G)服务,建议采用G1回收器。
4、优化思路:让短命对象在MinorGC阶段就被回收(同时回收后的存活对象<Survivor区域50%,可控制保留在新生代),长命对象尽早进入老年代,不要在新生代来回复制;尽量减少Full GC的频率,避免FGC系统的影响。
5、到目前为止,总结到的调优的过程主要基于上线前的测试验证阶段,所以我们尽量在上线之前,就将机器的JVM参数设置到最优!
JVM调优只是一个手段,但并不一定所有问题都可以通过JVM进行调优解决,大多数的Java应用不需要进行JVM优化,我们可以遵循以下的一些原则:
-
上线之前,应先考虑将机器的JVM参数设置到最优;
-
减少创建对象的数量(代码层面);
-
减少使用全局变量和大对象(代码层面);
-
优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
-
分析GC情况优化代码比优化JVM参数更好(代码层面);