![9da26092a376bf8d87e9ce8c17b0476d.png](https://i-blog.csdnimg.cn/blog_migrate/8dd45399617ad3a899bab441ae17cd11.jpeg)
最近我们新上了一个预测模型的解决方案,其中有一步,不同于以往用复杂逻辑对稀疏处理特征的预处理方式,而是通过创建数百个Tensor来做预处理,一个tensor创建时间大概是0.2ms,用for循环串行执行创建数百个tensor,单次请求耗时可达到50ms,加上后面的预测,1并发下耗时就达到了65ms,压测状况如下:
![f879aa3105e735e1e1e0f1c6d6685415.png](https://i-blog.csdnimg.cn/blog_migrate/06fe5e8df2e1f22615950078cce22df0.jpeg)
也就是并发到了6或者7,已经开始出现超时了,这和我们的目标相去甚远。那么很简单的,我们想到了用parallelStream来完成循环处理。
![e8d1458dbcdabf7ac69b2ae61113f9a9.png](https://i-blog.csdnimg.cn/blog_migrate/68b0e2b03b428434cd10842de3e0ff70.png)
果然,单次预处理逻辑耗时下降到了15ms以内,于是我们满心欢喜的把他发上去压测,结果是:
![98a9e7116c6e8200f3aea8c1ac1fde54.png](https://i-blog.csdnimg.cn/blog_migrate/51b8bbe171fbf8586f31dc094d10f07b.jpeg)
耗时并没有好,而且突增的很明显,在并发到了3的时候就已经耗时80ms了。其实这也很好解释,并行流parallelStream会最大地利用并行度,消耗大量的线程和CPU资源,而作为运算密集型的模型服务,CPU被前置逻辑消耗了必然会影响整体性能。
![0f44790e554bd65aaf1465f5beb73b05.png](https://i-blog.csdnimg.cn/blog_migrate/766b6f96e5e834c8922bc5169ceecd6f.jpeg)
当我打开应用监控,果然是这样的:
![5edd39f93acb87f9d18e1eb83a5ba7c2.png](https://i-blog.csdnimg.cn/blog_migrate/bc09a6f49cd8096c8751e83e26ec830a.png)
我第一反应就是要降低并行度,查阅了资料和源码,并行流底层使用的是ForkJoinPool作为线程池,于是我尝试了用System.property("java.util.concurrent.ForkJoinPool.common.parallelism",4)和在JVM参数里-Djava.util.concurrent.ForkJoinPool.common.parallelism=4(这个数据经过后续多次测试调成了5)设置,为了避免spring加载的时候把一些系统参数覆盖以及想让spring入口美观点,就干脆只保留JVM里的配置了,压测结果:
![8e5ccfd1ea6d31d1c6c852aa8f6565c1.png](https://i-blog.csdnimg.cn/blog_migrate/56ddef5a427f51eb8673fd42c0601e1f.jpeg)
发现CPU负载是下来了,也没有突增,但是整体和不开parallelStream区别不大,那真的没有办法了吗?
在我增加JVM参数的时候,发现了几个以前的配置:
-XX:ReservedCodeCacheSize=512m -XX:MaxMetaspaceSize=1g -XX:NativeMemoryTracking=detail
瞬间对这几个参数产生了兴趣
ReservedCodeCacheSize:
用于设置Code Cache大小,JIT编译的代码都放在Code Cache中,若Code Cache空间不足则JIT无法继续编译,并且会去优化,比如编译执行改为解释执行,由此,性能会降低
我是第一次知道原来java也有可能进行解释执行啊,这不成了边吃边拉的python了吗(无恶意 如果空间不足性能必然会大大降低,作为一台12核48G的服务器,不能忍,先改成1G。当然了,这个参数的改动并不会对本次压测产生影响,因为JVM日志里并没有报 ->
CodeCache已满,编译器已被禁用
MaxMetaspaceSize:
最大元空间占用内存大小,这个就是标准的JAVA基础知识了,不赘述(我说的也不好),那么他有必要配1g吗?
![ba8c41cb218d1fde368432b01094cfe4.png](https://i-blog.csdnimg.cn/blog_migrate/097d2421073710aeaba1cb9ea0f66692.png)
看来我并没有那么多的类和类加载器,何必占用JVM的空间?调小调小。
可是调整元空间占用大小也仅仅是释放一些创建线程的压力(可用内存越少,可创建的线程越少),并不能明显解决已减少了并行度的服务的耗时呀。
接下来才是重头戏:
NativeMemoryTracking:
这个参数其实是之前为了排查堆外内存泄露问题时加上的,一直没有关(至于堆外内存泄露如何解决又是另一个故事了),那么这个参数会对JVM带来哪些影响呢?
Native Memory Tracking (NMT) 是Hotspot VM用来分析VM内部内存使用情况的一个功能。我们可以利用jcmd(jdk自带)这个工具来访问NMT的数据。NMT必须先通过VM启动参数中打开,不过要注意的是,打开NMT会带来5%-10%的性能损耗。
配置参数:
-XX:NativeMemoryTracking=[off | summary | detail]
# off: 默认关闭# summary: 只统计各个分类的内存使用情况.# detail: Collect memory usage by individual call sites.
通过jcmd查看NMT报告以及查看对比情况。
jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
# summary: 分类内存使用情况 # detail: 详细内存使用情况,除了summary信息之外还包含了虚拟内存使用情况 # baseline: 创建内存使用快照,方便和后面做对比 # summary.diff: 和上一次baseline的summary对比 # detail.diff: 和上一次baseline的detail对比 # shutdown: 关闭NMT
显然,之前查找问题加上的这个参数影响了我们服务的整体性能,而且可能不少,压测的时候完全可以把这个参数设置为off状态
那么。。。见证奇迹的时候到了
![d06c23f24e41d1fa0694550b382738b5.png](https://i-blog.csdnimg.cn/blog_migrate/fd6af1ee789bb110b37d995d65a0379d.jpeg)
我们成功的把并发压上了10,并且RT完全可接受,整体的吞吐率和QPS均符合线上切小流量的标准!!!
通过对JVM的调整我们成功实现了性能优化1.5倍的目标,而且在这些参数上线之后,可能对整个系统所有接口都有正向的影响,待后续更新。
---> 我来更新了
![fd8cb94b217ad706ae0db0273d678cbb.png](https://i-blog.csdnimg.cn/blog_migrate/0795be3d1298b7f60c6d66083bddc795.png)
![9629044ef685f272b7b9644cbab9fe89.png](https://i-blog.csdnimg.cn/blog_migrate/1a03935095cbb4d37df81e544aa72efb.png)
调参大法好~