背景
即使熟悉了开发中的各项技术和优化技巧,但在真正的性能优化场景下,自己依旧很难开展优化任务。所以需要总结一些优化方案,方便以后查阅和学习。
如何找到优化目标?
应用性能低,有很多方面的因素,比如业务需求层面、架构设计层面、硬件/软件层面等。
通常,关注一个硬件资源(比如 CPU),我们主要关注以下基本要素。
利用率: 一般是瞬时值,属于采样范围,用来判断有没有峰值,比如 CPU 使用率。
饱和度: 一般指资源是否被合理利用,能否用分担更多的工作。比如,饱和度过高,新请求在特定 queue 里排队;再比如,内存利用率过低、CPU 利用率过高,就可以考虑空间换时间。
错误信息: 错误一般发生在问题严重的情况下,需要特别关注。
联想信息: 对引起的原因进行猜测,并用更多的工具验证猜想,猜测影响因素并不一定是准确的,只是帮助我们分析问题,比如系统响应慢很可能是大量使用了 SWAP 导致的。
CPU
查看 CPU 使用可以使用 top 命令,尤其注意它的负载(load)和使用率,vmstat 命令也可以看到系统的一些运行状况。
内存
内存可以使用 free 命令查看,尤其关注剩余内存的大小(free)。对于 Linux 系统来说,启动之后由于各种缓存和缓冲区的原因,系统内存会被迅速占满,所以我们更加关注的是 JVM 的内存。
top 命令的 RES 列,显示的就是进程实际占用的物理内存,这个值通常比 jmap 命令获取的堆内存要大,因为它还包含大量的堆外内存空间。
网络
iotop 可以看到占用网络流量最高的进程;通过 netstat 命令或者 ss 命令,能够看到当前机器上的网络连接汇总。在一些较底层的优化中,会涉及针对 mtu 的网络优化。
I/O
通过 iostat 命令,可以查看磁盘 I/O 的使用情况,如果利用率过高,就需要从使用源头找原因;类似 iftop,iotop 可以查看占用 I/O 最多的进程,很容易可以找到优化目标。
其他
lsof 命令可以查看当前进程所关联的所有资源;sysctl 命令可以查看当前系统内核的配置参数; dmesg 命令可以显示系统级别的一些信息,比如被操作系统的 oom-killer 杀掉的进程就可以在这里找到。
参考:
解决
找到了具体的性能瓶颈点,就可以针对性地进行优化。
1.CPU 问题
CPU 是系统的核心资源,如果 CPU 有瓶颈,很多任务和线程就获取不到时间片,便会运行缓慢。如果此时系统的内存充足,就要考虑是否可以空间换时间,通过数据冗余和更优的算法来减少 CPU 的使用。
在 Linux 系统上,通过 top-Hp 便能容易地获取占用 CPU 最高的线程,进行针对性的优化。
资源的使用要细分,才能够进行专项优化。
我曾经碰见一个棘手的性能问题,线程都阻塞在 ForkJoin 线程池上,经过仔细排查才分析出,代码在等待耗时的 I/O 时,采用了并行流(parallelStrea)处理,但是 Java 默认的方式是所有使用并行流的地方,公用了一个通用的线程池,这个线程池的并行度只有 CPU 的两倍。所以请求量一增加,任务就会排队,造成积压。
2.内存问题
内存问题通常是 OOM 问题,可以参考“19 | 高级进阶:JVM 常见优化参数”进行优化。如果内存资源很紧张,CPU 利用率低,则可以考虑时间换空间的方式。
SWAP 分区使用硬盘来扩展可用内存的大小,但它的速度非常慢。一般在高并发的应用中,会把 SWAP 关掉,因为它很容易会引起卡顿。
3.I/O 问题
我们通常开发的业务系统,磁盘 I/O 负载都比较小,但网络 I/O 都比较繁忙。
当遇到磁盘 I/O 占用高的情况,就要考虑是否是日志打印得太多导致的。通过调整日志级别,或者清理无用的日志代码,便可缓解磁盘 I/O 的压力。
业务系统还会有大量的网络 I/O 操作,比如通过 RPC 调用一个远程的服务,我们期望使用 NIO 来减少一些无效的等待,或者使用并行来加快信息的获取。
还有一种情况,是类似于 ES 这样的数据库应用,数据写入本身,就会造成繁重的磁盘 I/O。这个时候,可以增加硬件的配置,比如换成 SSD 磁盘,或者增加新的磁盘。
数据库服务本身,也会提供非常多的参数,用来调优性能。根据“06 | 案例分析:缓冲区如何让代码加速”和“07 | 案例分析:无处不在的缓存,高并发系统的法宝”的描述,这部分的配置参数,主要影响缓冲和缓存的行为。
比如 ES 的 segment 块大小,translog 的刷新速度等,都可以被微调。举个例子,大量日志写入 ES 的时候,就可以通过增大 translog 写盘的间隔,来获得较大的性能提升。
4.网络问题
数据包在网络上传输,影响的主要因素就是结果集的大小。通过去除无用的信息,启用合理的压缩,可以获得较大的性能提升。
值得注意的是,这里的网络传输值得不仅仅是针对浏览器的,在服务间调用中也有着同样的情况。
比如,在 SpringBoot 的配置文件中,通过配置下面的参数,就可以开启 gzip。
server:
compression:
enabled: true
min-response-size: 1024
mime-types: ["text/html","application/json","application/octet-stream"]
但是,这个 SpringBoot 服务,通过 Feign 接口从另外一个服务获取信息,这个结果集并没有被压缩。
可以通过替换 Feign 的底层网络工具为 OkHTTP,使用 OkHTTP 的透明压缩(默认开启 gzip),即可完成服务间调用的信息压缩,但很多同学容易忘掉这一环。我曾经调优果一个项目,将返回的数据包从9MB 压缩到300KB 左右,极大地减少了网络传输,节省了大约 500ms 的时间。
网络 I/O 的另外一个问题就是频繁的网络交互,通过将结果集合并,使用批量的方式,可以显著增加性能,但这种方式的使用场景有限,比较适合异步的任务处理。
使用 netstat 命令,或者 lsof 命令,可以获取进程所关联的,TIME_WAIT 和 CLOSE_WAIT 网络状态的数量,前者可以通过调整内核参数来解决,但后者多是应用程序的 BUG。
参考:
以上内容学习自拉勾教育,为方便后续问题排查,简单记录下~