netty压测与调优经验分享

  |   0 评论   |   0 浏览

分享目的

1、带大家过一下压测中碰到的问题

2、实际演示压测的过程

3、分享JVM调优的相关知识与性能调优的常用工具

1、压测前需要列出必要的监控指标

分为业务指标和机器指标,必要时添加业务监控逻辑,知道关键指标有哪些,遗漏后影响压测结果的判断。

压测netty的机器指标类似,需要关注以下指标:

负载、cpu、内存、堆外内存、GC频率与速度、压测客户端和服务端TCP缓冲池大小,压测netty特别要注意对外内存

业务指标添加了客户端ping值的输出与port轮训执行时间的统计

2、压测前统计出所有可能影响服务性能的变量

压测的目的不光是找到服务的最大承载量,更需要根据压测调整不合理的逻辑和参数,找到最优配置,

需要列出所有可以调整的参数,压测时一次调整一个参数

比如本次压测识别到的变量:

堆内存大小、日志打印量、日志框架、垃圾回收器、服务的线程数、硬件参数配置

3、压测机器与环境的调优

1)压测的服务端机器尽量使用单独的服务器,排除其他服务影响,配置和线上机器越接近越好,如果资源允许,直接在线上环境压测

2)确保线上与测试的压测环境参数统一

3)cpu不仅要关注核数,也需要关注cpu的类型、主频高低,线上测试32核2.5GHz的机器表现不如16核3.2GHz

4)长连接压测客户端建议2-3倍于服务机器

4、压测过程中同步做优化

压测和优化是不分家的,压测不光是看服务的承压情况,也要根据监控的指标去做代码优化

压测流程回顾

压测轮次人数备注
15000killer团队压测,使用Java-WebSocket
---
25000改成netty
39000购买新的测试机 16核3.2hz
412000调整线程数配置
514000修改了垃圾回收器配置
618000去掉了序列化,反序列化代码

问题

1、docker虚拟机环境获取cpu核数异常

这个是jdk的问题,Jdku131之前的版本在使用`Runtime.getRuntime().availableProcessors();获取的是宿主机的cpu核数,之后的版本是容器限制的核数

2、测试环境负载高,TCP缓冲堆积

3、如何在netty中定时去发消息

这么写肯定不行

 @Override
      public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
          Channel ch = ctx.channel();
          //省略业务逻辑
          while(true){
              Thread.sleep(100L);
              reportPosition(ch);
        }
    }

对准确性要求不高可以使用HashedWheelTimer时间轮

4、线程数的配置并不是和cpu核数相关

5、jvm版本问题

6、gc问题

7、代码优化

性能监控工具

1、查看gc

jstat -gc pid 1000

2、查看class内存、实例占用

jmap -histo:live pid

3、查看TCP缓冲区排名

netstat -antp |grep 8080 |sort -k 3 -rn|head -20 -k 3是发队列排名 -k 2是收队列排名

4、查看线程cpu占用和线程堆栈信息

建议使用arthas

5、java线程、内存、gc监控

VisualVm

JVM调优

JVM调优有必要吗?什么时候做?

实际上GC调优并不是必须做的,默认的GC参数基本已经是最优的了,有时候调完参数可能性能更低了 有一句话是这么说的:"GC tuning is the last task to be done."


线上的jvm问题

1.8.0_171的内存分配eden区与survior比例为1:1

1.8.0_171的堆内存分布:

2692160479f86f053c0294a800d15c6b.png

1.8.0_222的堆内存分布:

8aeacd3f87acb9e3e5356f9c725a0ac3.png

JDK1.8是默认开启 -XX:+UseAdaptiveSizePolicy,每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量

查看进程使用的垃圾回收器

线上的垃圾回收器并是不默认的Parallel Scavenge回收器

如何确定的?

jinfo -flags 1
Attaching to process ID 1, please wait...
 Debugger attached successfully.
 Server compiler detected.
 JVM version is 25.171-b11
 Non-default VM flags: -XX:CICompilerCount=15 -XX:InitialHeapSize=65011712 -XX:MaxHeapSize=1025507328 -XX:MaxNewSize=341835776 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=21495808 -XX:OldSize=43515904 -XX:-OmitStackTraceInFastThrow -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC

但是openjdk1.8.0_222并不显示使用了什么垃圾回收器

这就需要看垃圾回收日志了

截一小段日志

 OpenJDK 64-Bit Server VM (25.222-b10) for linux-amd64 JRE (1.8.0_222-8u222-b10-1~deb9u1-b10), built on Jul 19 2019 16:57:48 by "pbuilder" with gcc 6.3.0 20170516
 Memory: 4k page, physical 20093748k(20091412k free), swap 0k(0k free)
 CommandLine flags: -XX:InitialHeapSize=6442450944 -XX:+ManagementServer -XX:MaxHeapSize=6442450944 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
 2020-12-16T11:48:03.312+0800: 1.215: [Full GC (Metadata GC Threshold) 2020-12-16T11:48:03.312+0800: 1.215: [Tenured: 0K->16178K(4194304K), 0.0701119 secs] 369122K->16178K(6081792K), [Metaspace: 20767K->20767K(1069056K)], 0.0702013 secs] [Times: user=0.07 sys=0.01, real=0.07 secs]
 2020-12-16T11:48:04.797+0800: 2.701: [Full GC (Metadata GC Threshold) 2020-12-16T11:48:04.798+0800: 2.701: [Tenured: 16178K->30815K(4194304K), 0.0845959 secs] 569863K->30815K(6081792K), [Metaspace: 34729K->34729K(1081344K)], 0.0846848 secs] [Times: user=0.08 sys=0.01, real=0.09 secs]
 2020-12-16T11:50:23.669+0800: 141.572: [GC (Allocation Failure) 2020-12-16T11:50:23.669+0800: 141.572: [DefNew: 1677824K->37838K(1887488K), 0.0742408 secs] 1708639K->68654K(6081792K), 0.0743923 secs] [Times: user=0.09 sys=0.03, real=0.08 secs]
 2020-12-16T11:50:29.821+0800: 147.724: [GC (Allocation Failure) 2020-12-16T11:50:29.821+0800: 147.724: [DefNew: 1715662K->35657K(1887488K), 0.0643268 secs] 1746478K->66473K(6081792K), 0.0645106 secs] [Times: user=0.04 sys=0.03, real=0.07 secs]

其中DefNew表示年轻代使用的是Serial回收器

修改成Parallel Scavenge后

2020-12-16T11:56:40.688+0800: 220.533: [GC (Allocation Failure) [PSYoungGen: 2069312K->3968K(2080768K)] 2150367K->85031K(6275072K), 0.0216595 secs] [Times: user=0.02 sys=0.01, real=0.02 secs]
2020-12-16T11:56:42.369+0800: 222.213: [GC (Allocation Failure) [PSYoungGen: 2069376K->3840K(2083328K)] 2150439K->84903K(6277632K), 0.0236630 secs] [Times: user=0.02 sys=0.00, real=0.03 secs]
2020-12-16T11:56:44.049+0800: 223.893: [GC (Allocation Failure) [PSYoungGen: 2072320K->3904K(2082304K)] 2153383K->84967K(6276608K), 0.0216463 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
2020-12-16T11:56:45.726+0800: 225.570: [GC (Allocation Failure) [PSYoungGen: 2072384K->4064K(2084864K)] 2153447K->85135K(6279168K), 0.0213808 secs] [Times: user=0.02 sys=0.01, real=0.02 secs]
2020-12-16T11:56:47.408+0800: 227.253: [GC (Allocation Failure) [PSYoungGen: 2075616K->3648K(2083840K)] 2156687K->84719K(6278144K), 0.0213834 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2020-12-16T11:56:49.086+0800: 228.930: [GC (Allocation Failure) [PSYoungGen: 2075200K->3712K(2085888K)] 2156271K->84791K(6280192K), 0.0211097 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

PSYoungGen表示使用的Parallel Scavenge

线程个数受容器环境限制

gc1.png

在加上参数-XX:ParallelGCThreads=8

如何判断需要做JVM参数优化了呢

GC性能的衡量指标

吞吐量: 用户代码时间 /(用户代码执行时间 + 垃圾回收时间)GC耗时2分钟,系统运行100分钟,则吞吐量为98%,一般来说不能低于95%

停顿时间: 指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。

如果GC执行时间满足下列所有条件,就没有必要进行GC优化了: + Minor GC执行非常迅速(50ms以内) + Minor GC没有频繁执行(大约10s执行一次) + Full GC执行非常迅速(1s以内) + Full GC没有频繁执行(大约10min执行一次)

垃圾回收频率: 通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。

监控GC状态

1、打印GC日志

通过GCViewer分析GC日志|通过 网站:https://gceasy.io/gc-index.jsp 分析GC日志

2、通过jstat统计gc频率和时间

ps -p 1 -o pid,start_time,etime

jstat -gc 1 1000

再计算吞吐量

GC优化怎么做

GC优化的策略:

降低MInor GC频率

Minor GC的频率和Eden区大小有关,可以通过增大新生代空间来降低Minor GC的频率,但增大Eden区会不会造成Minor GC的时间增加呢,会增加,但如果MinorGC的频率过高导致能在年轻代销毁的对象晋升到老年代对性能的影响更大

将进入老年代的对象数量降到最低

老年代GC相对来说会比新生代GC更耗时,因此,减少进入老年代的对象数量可以显著降低Full GC的频率

减少Full GC的执行时间

Full GC的时间过长会造成系统的卡顿,对响应优先的系统影响很大,检查查询数据库对象的条数,较少查询的字段,避免大对象的分配

选择合适的垃圾回收算法

响应速度优先 选择CMS和G1,吞吐量优先选择Parallel Scavenge

垃圾收集器跟内存大小的关系

  1. Serial 几十兆
  2. PS 上百兆 - 几个G
  3. CMS - 20G
  4. G1 - 上百G
  5. ZGC - 4T - 16T(JDK13)

如何设置内存大小

内存大小、GC运行次数和GC运行时间之间的关系:

大内存空间 + 减少了GC的次数 + 提高了GC的运行时间

小内存空间 + 增多了GC的次数 + 降低了GC的运行时间

常用的GC优化参数

常用的GC优化参数

类型参数描述建议值
堆内存大小-Xms启动JVM时堆内存的大小3-4倍Full GC后的老年代占用量
----

-Xmx堆内存最大限制3-4倍Full GC后的老年代占用量
新生代空间大小-XX:NewRatio新生代和老年代的内存比默认2

-XX:NewSize新生代内存大小

-XX:SurvivorRatioEden区和Survivor区的内存比默认8

标题:netty压测与调优经验分享
作者:不断努力的青春
地址:http://songaw.com/articles/2022/01/25/1643100582165.html