后端接口性能优化分析-问题发现&问题定义(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 后端接口性能优化分析-问题发现&问题定义

后端接口性能优化分析-问题发现&问题定义(上):https://developer.aliyun.com/article/1413664


3.链路追踪


有时候某个接口涉及的逻辑很多,比如:查数据库、查redis、远程调用接口,发mq消息,执行业务代码等等。


该接口一次请求的链路很长,如果逐一排查,需要花费大量的时间,这时候,我们已经没法用传统的办法定位问题了。


有没有办法解决这问题呢?


用分布式链路跟踪系统:skywalking


通过skywalking定位性能问题:

在skywalking中可以通过traceId(全局唯一的id),串联一个接口请求的完整链路。可以看到整个接口的耗时,调用的远程服务的耗时,访问数据库或者redis的耗时等等,功能非常强大。


之前没有这个功能的时候,为了定位线上接口性能问题,我们还需要在代码中加日志,手动打印出链路中各个环节的耗时情况,然后再逐一排查。


问题排查


1.个别接口响应慢


链路追踪


使用SkyWalking,展示出每一个与网络有关的耗时。比如:读写数据库、读写Redis、SpringCloud调用、Dubbo调用等。这样就能立马定位是哪次操作耗时了。


同时,SkyWalking可以记录每一个SQL语句,可以帮助定位。


例如:(如箭头所指处,最上边一个是总耗时,下边的线段是单个操作的耗时)


在链路上打印日志


在相应的链路上打印日志,然后查看日志,看是哪个地方耗时。


死锁问题


在这里重点讲一下出现死锁问题的排查经验吧


有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁


t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

Object A = new Object();
    Object B = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (A) {
            log.debug("lock A");
            sleep(1);
            synchronized (B) {
                log.debug("lock B");
                log.debug("操作...");
            }
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        synchronized (B) {
            log.debug("lock B");
            sleep(0.5);
            synchronized (A) {
                log.debug("lock A");
                log.debug("操作...");
            }
        }
    }, "t2");
    t1.start();
    t2.start();
12:22:06.962 [t2] c.TestDeadLock - lock B 
12:22:06.962 [t1] c.TestDeadLock - lock A 


定位死锁


检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:


jps

cmd > jps
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
12320 Jps
22816 KotlinCompileDaemon
33200 TestDeadLock // JVM 进程
11508 Main
28468 Launcher
cmd > jstack 33200
        Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
        2018-12-29 05:51:40
        Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode):
        "DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition
        [0x0000000000000000]
        java.lang.Thread.State: RUNNABLE
        "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry
        [0x000000001f54f000]
        java.lang.Thread.State: BLOCKED (on object monitor)
        at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
        - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
        - locked <0x000000076b5bf1d0> (a java.lang.Object)
        at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
        "Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry
        [0x000000001f44f000]
        java.lang.Thread.State: BLOCKED (on object monitor)
        at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
        - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
        - locked <0x000000076b5bf1c0> (a java.lang.Object)
        at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
// 略去部分输出
        Found one Java-level deadlock:
        =============================
        "Thread-1":
        waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
        which is held by "Thread-0"
        "Thread-0":
        waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
        which is held by "Thread-1"
        Java stack information for the threads listed above:
        ===================================================
        "Thread-1":
        at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
        - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
        - locked <0x000000076b5bf1d0> (a java.lang.Object)
        at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
        "Thread-0":
        at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
        - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
        - locked <0x000000076b5bf1c0> (a java.lang.Object)
        at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
        Found 1 deadlock

这里是两个线程 “Thread-0” 和 “Thread-1” 在互相等待对方释放锁,从而导致了死锁。


线程 “Thread-1” 试图获取对象0x000000076b5bf1c0的监视器锁(即synchronized关键字保护的那个对象),但是这个锁已经被线程 “Thread-0” 持有。因此,“Thread-1” 进入了等待状态,等待 “Thread-0” 释放这个锁。


同时,线程 “Thread-0” 试图获取对象0x000000076b5bf1d0的监视器锁,但是这个锁已经被线程 “Thread-1” 持有。因此,“Thread-0” 进入了等待状态,等待 “Thread-1” 释放这个锁。


jconsole


2.所有接口响应慢


如果线上出了问题,首先判断是业务问题还是整个系统的问题。如果是业务问题,就去看应用的日志等进行排查。如果出现了如下问题,就可能是整个系统的问题


  1. 大量接口都很慢
  2. 页面打不开


如何得知系统出问题了?


系统出问题时,我们需要进行详细排查,一般情况下,有以下场景我们可以得知线上出问题了:


  1. 用户反馈功能不能正常使用
  2. 监控系统的邮件或者短信提醒


系统问题排查步骤


以下按顺序进行


  1. 是否CPU占用过高
  2. 是否内存占用过高
  3. 是否磁盘占用过高
  4. 是否网络故障
  5. 查看后台日志
  6. 是否是数据库问题(比如:索引失效、死锁)
  7. 是否是垃圾回收导致


CPU占用过高


什么场景需要排查CPU占用?


  1. 访问接口的响应速度很慢。
  2. 系统崩溃无响应
  3. 压测时要查看CPU、内存、load、rt、qps等指标


步骤简述


  1. 定位进程 (命令:top)
  2. 定位线程 (命令:top -Hp 进程号)
  3. 定位代码位置 (命令:jstack)


排查方法详述


  1. 找到占CPU最高的进程。
  1. top命令,记下进程号(PID)。假设最高的是:1893
  1. 通过进程找到对应的线程。
  1. top -Hp 1893。假设最高的是:4519
  2. (因为Java是单进程多线程的)
  1. 通过线程定位到代码大概的位置信息。
  1. printf %x 4519。结果为:11a7
  2. jstack 1893 | grep 11a7 -A 30 –color
    结果大概是这样的:

可能导致CPU使用率飙升的操作


  • 无限循环的while
  • 经常使用Young GC
  • 频繁的GC


如果访问量很高,可能会导致频繁的GC甚至Full GC。当调用量很大时,内存分配将如此之快以至于GC线程将连续执行,这将导致CPU飙升。


  • 序列化和反序列化

序列化和反序列化本身不会导致CPU使用率飙升。序列化是将对象转换为字节流的过程,而反序列化则是将字节流转换回对象的过程。这两个过程通常在内存中完成,并不直接导致CPU使用率的飙升。


然而,在某些情况下,序列化和反序列化可能会导致CPU使用率增加。例如,当需要大量的数据进行序列化或反序列化时,可能会消耗较多的CPU资源。此外,如果序列化和反序列化的操作频繁且耗时较长,也可能对CPU使用率产生一定影响。


  • 正则表达式

正则表达式本身不会导致CPU飙升,但是使用不当或者复杂的正则表达式可能会导致CPU负载增加。

正则表达式在处理大量文本时,特别是复杂的表达式或者需要进行大量回溯的情况下,可能会消耗大量的 CPU 资源。这是因为正则表达式引擎需要在文本中进行搜索和匹配,并且有些复杂的规则可能需要大量的计算才能找到匹配的结果。


因此,在编写正则表达式时,需要注意避免过于复杂或低效的表达式,尤其是在处理大量文本的场景下。另外,对于一些需要频繁执行的操作,可以考虑在代码中进行优化,以减少对 CPU 的负载影响。


  • 线程上下文切换


内存占用过高


步骤简述


定位Java程序内存使用过高或者内存泄漏的问题跟CPU也类似,可分为以下步骤:


  1. 定位进程 (命令:top)
  2. 定位线程 (命令:top -Hp 进程号)
  3. 定位代码位置 (命令:jmap生成快照,MAT / jprofiler / VisualVM / jhat 查看快照数据)


1.定位进程


  1. top
  2. 按下M(按内存占用由大到小排序)
  1. // 假设定位到的进程ID为14279。


2.定位线程


top -Hp 14279;


按下M(按内存占用由大到小排序)

top - 18:33:07 up 25 days,  7:48,  1 user,  load average: 0.17, 0.27, 0.23
Threads:  54 total,   1 running,  53 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.5 us,  0.7 sy,  0.0 ni, 98.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  8168236 total,   231696 free,  3660496 used,  4276044 buff/cache
KiB Swap:   969964 total,   969964 free,        0 used.  4197860 avail Mem
  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
14293 weiping   20   0 4508772  97036  18112 S  10    12 152:35.42 java
14279 weiping   20   0 4508772  97036  18112 S  5.0  1.2   0:00.00 java
14282 weiping   20   0 4508772  97036  18112 S  0.0  1.2   0:00.37 java

注意观察两点:


  1. 线程的数量
  1. 可以看到,它线程数是54(左上角的Threads项),属于正常。
  2. 若比较大(比如大于1000),要考虑是不是代码有问题:

是不是代码里手动起了多个线程。比如:使用OkHttpClient时每次都创建了连接池(ConnectionPool);应该是只创建一个连接池的。

是不是自己创建的线程池最大个数太大了。

  1. 占内存大的线程的PID
  1. 如果线程数量正常,就要dump内存的快照信息来查看。


3.定位代码位置


如果是线上环境,注意dump之前必须先将流量切走,否则大内存dump是直接卡死服务。


dump当前快照:jmap -dump:format=b,file=dump.hprof 14279


查找实例数比较多的业务相关的实例,然后找到相应代码查看。(使用工具查看dump.hprof。比如:MAT、VisualVM、jhat)


磁盘问题


步骤简介


  1. 是否磁盘快用完了(命令:df、du)
  2. TPS是否正常(命令:iostat、vmstat、lsof)


这个命令是用来检查系统的磁盘性能是否正常。具体来说:


  • iostat 命令用于报告 CPU 使用情况和磁盘I/O统计信息。
  • vmstat 命令用于报告虚拟内存统计信息。
  • lsof 命令用于列出打开文件。


通过使用这些命令,可以获得关于磁盘 I/O 活动、CPU 使用率、内存使用等方面的统计数据。其中,磁盘 I/O 统计信息中的 TPS(每秒传输的 I/O 操作数)可以用来评估磁盘的性能是否正常。如果 TPS 过高或者过低,可能意味着磁盘存在性能问题,需要进一步分析和解决。


网络问题


垃圾回收


什么情况下,GC会对程序产生影响?


不管Minor GC还是FGC,都会造成一定程度的程序卡顿(即Stop The World:GC线程开始工作,其他工作线程被挂起),即使采用ParNew、CMS或者G1这些更先进的垃圾回收算法,也只是在减少卡顿时间,而并不能完全消除卡顿。


那到底什么情况下,GC会对程序产生影响呢?根据严重程度从高到底,包括以下4种情况:


  1. FGC过于频繁
  1. FGC通常比较慢,少则几百毫秒,多则几秒,正常情况FGC每隔几个小时甚至几天才执行一次,对系统的影响还能接受。
  2. 一旦出现FGC频繁(比如几十分钟执行一次),是存在问题的,会导致工作线程频繁被停止,让系统看起来一直有卡顿现象,也会使得程序的整体性能变差。


  1. YGC耗时过长

       a. 一般来说,YGC的总耗时在几十或者上百毫秒是比较正常的,虽然会引起系统卡顿几毫秒或者几十毫秒,这种情况几乎对用户无感知,对程序的影响可以忽略不计。

       b. 如果YGC耗时达到了1秒甚至几秒(都快赶上FGC的耗时了),那卡顿时间就会增大,加上YGC本身比较频繁,就会导致比较多的服务超时问题。


  1. FGC耗时过长
  1. FGC耗时增加,卡顿时间也会随之增加,尤其对于高并发服务,可能导致FGC期间比较多的超时问题,可用性降低,这种也需要关注。


  1. YGC过于频繁
  1. 即使YGC不会引起服务超时,但是YGC过于频繁也会降低服务的整体性能,对于高并发服务也是需要关注的。


如何排查


  1. 公司的监控系统:大部分公司都会有,可全方位监控JVM的各项指标。


  1. JDK自带工具:jmap、jstat
  1. 查看堆内存各区域的使用率以及GC情况:jstat -gcutil pid 1000 (重点关注结果中的YGC、YGCT、FGC、FGCT、GCT)

       b. 查看堆内存中的存活对象,并按空间排序:jmap -histo pid | head -n20

       c. dump堆内存文件:jmap -dump:format=b,file=heap pid

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2天前
|
JSON 前端开发 Java
一文读Web开发 之接口后端接口、类与前端请求、拦截器编写
一文读Web开发 之接口后端接口、类与前端请求、拦截器编写
18 6
|
8天前
|
中间件 Go
go语言后端开发学习(三)——基于validator包实现接口校验
go语言后端开发学习(三)——基于validator包实现接口校验
|
2天前
|
前端开发 Java 程序员
牛皮的程序猿后端返回值怎么定义
在后端接口封装中,通常会统一返回数据格式,确保稳定性和可预测性。常见的模式包括状态码(如`code`或`ret`)、状态信息(`message`或`msg`)、核心数据(`data`)。`success`字段提供了一种直观判断接口是否成功的标志。例如:
7 0
|
1月前
|
缓存 Java 数据库
后端性能优化的实践与经验分享
【5月更文挑战第15天】在互联网环境中,后端性能优化对提供卓越用户体验至关重要。关键领域包括:数据库优化(查询优化、索引优化、表结构优化、数据库维护)、缓存策略(内存缓存、CDN内容分发、HTTP缓存)、服务器配置优化(硬件升级、网络优化、操作系统调整)和代码优化(算法与数据结构、懒加载与异步处理、减少冗余计算、多线程与并发)。通过这些方法,可以提升响应速度,增强用户满意度,促进业务增长。
71 3
|
1月前
|
缓存 JSON 安全
【Uniapp 专栏】Uniapp 与后端接口对接的实战要点
【5月更文挑战第12天】在 Uniapp 项目开发中,成功对接后端接口至关重要。要点包括:深入理解后端提供的接口文档,确保数据格式(如 JSON)正确处理,选择合适的请求方式(如 GET、POST),设置正确的请求头,做好错误处理和数据缓存策略,确保安全性(如使用 HTTPS 和令牌验证)并进行全面测试。同时,进行版本管理和团队协作,与后端开发人员保持良好沟通,以实现高效、稳定的接口对接。
|
1月前
|
XML JSON 前端开发
获取后端接口请求中的参数(@PathVariable,@RequestParam,@RequestBody区别,使用postman请求
获取后端接口请求中的参数(@PathVariable,@RequestParam,@RequestBody区别,使用postman请求
|
1月前
|
前端开发 小程序 测试技术
前端后端测试接口mork神器,Apifox使用一分钟入门
前端后端测试接口mork神器,Apifox使用一分钟入门
59 0
|
1天前
|
消息中间件 负载均衡 持续交付
探索后端开发:微服务架构的演进与实践
【6月更文挑战第25天】本文深入探讨了微服务架构的概念、发展以及在现代后端开发中的应用。我们将通过一个虚构案例,展示如何将传统的单体应用重构为基于微服务的架构,并讨论在此过程中遇到的挑战和解决方案。文章旨在为读者提供从理论到实践的全面指导,帮助理解微服务架构的优势及其在企业级系统中的应用。
|
2天前
|
SQL XML Java
快速入门Web开发(中)后端开发(有重点)(3)
快速入门Web开发(中)后端开发(有重点)(3)
8 1
|
2天前
|
前端开发 数据库 开发者
构建高效后端:Django框架在Web开发中的深度解析
**Django框架深度解析摘要** Django,Python的高级Web框架,以其快速开发和简洁设计备受青睐。核心特性包括Model-Template-View架构、ORM、模板引擎和URL路由。通过创建博客应用示例,展示从初始化项目、定义模型、创建视图和URL配置到使用模板的流程,体现Django如何简化开发,提高效率。其强大功能如用户认证、表单处理等,使Django成为复杂Web应用开发的首选。学习Django,提升Web开发效率。【6月更文挑战第24天】
30 1