〇、经验总结:

  • 如果在压测过程中,压力始终上不去,可以考虑是施压机器并发上不去,或者被压机器请求处理不过来。
  • 施压上不去或者被压机器请求处理不过来,是因为机器CPU瓶颈?内存瓶颈?端口数量瓶颈?逐步排查定位。
  • 类似于Grpc这类需要建立rpc连接的请求,可扩展端口的数量会影响并发时连接建立数量。
  • 长链路的压测链,在定位问题时可以先从短链开始逐步排查扩展到长链,最终完成整个链路的压力测试。
  • 关注压测过程中可能出现的异常现象,哪怕是很不明显的地方,都可能会存在一个BUG。

一、背景说明
这周我们对项目里新增的几个接口进行了压力测试,期间遇到了一些之前没有遇到过的坑,走了一些弯路,在这里对这次压力测试经历进行总结复盘,同时也希望能给看到这篇文章的诸位提供一些浅显的思路。
首先介绍一下我们项目的结构。服务入口是一个网关模块,提供一个Grpc类型的接口,数据传输模式是一元数据模式。网关模块与其他业务模块之间通过Dubbo接口进行交互。
服务的架构概况图如下:
记一次Grpc接口压力测试&性能调优

该业务接口部署的服务器配置和部署MySQL组件的服务器配置一致,都是4核8G,50G普通硬盘,并且处于同一个内网网段,我们预估的性能指标要达到300并发,600TPS。
在压力测试过程中,我们重点关注TPS、GC次数、CPU占用率和接口响应时间等指标。

二、测试过程
完成项目部署后,我们开始编辑jemeter测试脚本,设置压力测试的标准为300个并发线程,在10秒内全部启动,持续压测时间15分钟,接着开始启动jemeter脚本进行测试。
1、第一次压力测试
垃圾收集策略包括:老年代启用CMS垃圾收集算法,新生代启用ParNew垃圾收集算法,新生代最大存活周期为15次minorGC,FullGC时使用CMS算法,并开启CMS中的并行标记。
根据前几次的压力测试经验,我们将初始堆内存设置为2048MB,因为偏小的堆内存设置容易在压力测试时被撑爆。
JVM内存分配:最大/最小堆内存为2048MB,Eden和Survivor比例为8:2,新生代和老年代的比例为1:2。由于服务器安装的是JDK8版本,废弃了永久代的配置。
JVM配置参数如下:

-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-Xloggc:/var/log/$MODULE/gc.log 
-XX:+UseConcMarkSweepGC 
-XX:+UseParNewGC 
-XX:MaxTenuringThreshold=15 
-XX:+ExplicitGCInvokesConcurrent 
-XX:+CMSParallelRemarkEnabled 
-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=/var/log/$MODULE 
-Xmx2048m -Xms2048m 
-XX:SurvivorRatio=8

(2)性能指标监控
top命令观察java线程的CPU占用率(us表示用户进程,sy表示系统进程),并使用jstac -gcutil Pid 1000 命令,定期查看虚拟机的GC情况。
一切准备就绪后,我们开始跑压测脚本,并查看性能监控指标。
但是我们没有看到预期来临的压力,而是并发到达一定值时,好像突然并发压力中断了,然后间隔1-2秒后压力又重新出现,这期间接口服务器各项指标均没有异常。很显然,并发存在问题!
记一次Grpc接口压力测试&性能调优

(3)问题排查与解决
上述压测过程中出现的现象,可以细分为两类:

  • 并发数量没有达到预期值,而且徘徊在一个较低水平,压测2分钟,接口侧只收到了3W多个请求,实际在施压服务器尝试发起了7W次请求。
  • 并发过程中出现类似压力中断的现象,而且并非偶然现象,压力停顿之后压测脚本中的断言开始抛出错误信息:连接异常。
    异常信息如下:
    记一次Grpc接口压力测试&性能调优

由于Grpc接口需要在客户端与服务端建立RPC连接,那么两端都需要同时指定各自的一个端口进行数据通信。基于这一点,我们判断出现第一种现象的原因可能有两个:

  • 施压机器端口受限,没有启动足够的线程发起请求;
  • 被压机器端口受限,不能接收施压机器发过来的所有请求连接,导致连接请求丢失。
    而出现第二种现象的原因,可能正是由于端口数量受限,并发线程不能正常发起,需要等待连接端口释放后才能继续发起新的连接线程。
    于是我们去查看两端机器的扩展端口列表,命令:
    cat  /proc/sys/net/ipv4/ip_local_port_range

    结果是:
    记一次Grpc接口压力测试&性能调优

即仅放开了 32768——60999 之间的端口,数量大致也是3W左右。
然后我们重新设置了被压机器的端口扩展列表,命令:

echo  "10000    65535" >  /proc/sys/net/ipv4/ip_local_port_range

结果是:
记一次Grpc接口压力测试&性能调优

2、第二次压力测试
经过第一次压力测试调整后,我们开始对调整效果进行测试验证。
(1)JVM配置
JVM配置没有改变
(2)性能指标监控
查看被压机器的GC情况和CPU使用情况,依然没有太大变化,但请求数量稍微有所提升,说明端口扩展有一定的效果,但是不明显。
继续查看施压机器的信息,接口调用的结果断言依然存在错误,虽然错误率有所降低(跟被压机器接收请求的数量上升有关系),错误信息还是连接异常。

记一次Grpc接口压力测试&性能调优

从这个结果来看,解决问题的方向是对的,于是继续把施压机器的端口扩展放开,开始第三轮测试验证。
3、第三次压力测试
(1)JVM配置
JVM配置没有改变
(2)性能指标监控
施压机器和被压机器的端口列表都放开后,grpc连接请求都正常了。在100并发和300并发两种情况下,持续2分钟发起的请求数相差不大,说明已经接近了两端服务器的处理极限了。
记一次Grpc接口压力测试&性能调优

(3)新问题暴露
本来以为这样就OK了,然而此时突然发现被压机器的GC开始出现异常,即YGC次数开始不变动,但是FGC频繁。
压测一段时间后,FGC开始出现,频率为平均每秒2-3次。
记一次Grpc接口压力测试&性能调优

通常出现FGC的原因,无非就是老年代被占满了,于是查看线程的老年代堆内存情况:

jstat -gcold  PID

记一次Grpc接口压力测试&性能调优
记一次Grpc接口压力测试&性能调优

我的天!老年代才64KB!这是把新生代内存塞满后,开始往可怜巴巴的老年代塞了,于是频繁触发FGC。
于是查看JVM参数配置,发现少了老年代的内存配置了。
老年代的堆内存配置可以通过 -XX:NewRatio=3 来设置,表示老生代:新生代的比值。即如果2GB堆内存的话,那么老年代是1.5GB,新生代是0.5GB。
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/$MODULE/gc.log -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:MaxTenuringThreshold=15 -XX:+ExplicitGCInvokesConcurrent -XX:+CMSParallelRemarkEnabled -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/$MODULE -Xmx2048m -Xms2048m -XX:SurvivorRatio=8 -XX:NewRatio=3
配置好之后,重启被压测模块,查看进程的堆内存分配情况:
记一次Grpc接口压力测试&性能调优