gitbook/高楼的性能工程实战课/docs/368125.md
2022-09-03 22:05:03 +08:00

34 KiB
Raw Permalink Blame History

17 | 查询购物车:为什么铺底参数一定要符合真实业务特性?

你好,我是高楼。

今天我们来看看查询购物车接口。

到现在为止,这是我们分析的第六个接口了。不过,我希望你能明白,我们分析每个接口,并不是为了搞清楚这个接口本身的逻辑,而是通过不同接口的基准测试,来分析不同的性能问题,争取给你带来更多的分析案例。

现在很多人在性能场景执行过程中,仍然会问出“当铺底数据不符合生产环境时,该怎么办”这样的疑问,其实答案也挺简单,那就是模拟不出生产环境中的问题。

所以在这节课中你将看到当铺底数据不合理时会对TPS产生什么样具体的影响。由此你会进一步理解为什么我一直在跟你强调铺底数据要符合生产环境逻辑

此外,我们还会分析另一个问题,这个问题可能会让你看着比较郁闷,你会发现我们分析了很久,逻辑看似非常合理,但是结果并不如人意。面对这样的情况,那我们该怎么处理呢?这里留个悬念,我们直接开始今天的分析。

压力数据

对于查询购物车这个接口,还是一样,我们先来看第一次运行的性能场景结果。这是一个一开始就足以让人心碎的性能数据:

你看线程数在增加的过程中TPS只达到40而响应时间从一开始就不断地增加。

这可怎么办呢根据我们RESAR性能分析逻辑第一步仍然是看架构图接着是拆分响应时间。因为响应时间在不断增加所以我们想要拆分响应时间非常容易。

架构图

在拆分响应时间之前,我们看一下架构图。在这一步,你只需要把架构图记个大概就行了。因为后面还要反复回来看多次。

第一阶段分析

拆分响应时间

我们反反复复在讲,做性能分析的时候,首先就是拆分时间

别人在问我问题的时候经常会这样描述TPS不高响应时间长瓶颈在哪呢一看到这种问题我通常会反问响应时间长在哪呢然后经典的对话结束语就出现了——我不知道呀。我也很想帮助对方解决问题但是对于这样的描述我根本无从下手。

一个做性能的人,怎么能只描述响应时间长呢?你至少要告诉别人慢在哪里。这就是为什么我一直在强调要画架构图。因为有了图,才有拆分时间的手段,这样一来,我们自然就不会盲目,除非你啥都没有。

在拆分时间的时候,你还要注意一点,要找准时间段。根据我的经验,一般是看响应时间的趋势,如果一直都长的话,倒是简单,看哪一段响应时间都行。要是有的时候长,有的时候短,那你就要注意了,在拆分响应时间的时候,要注意把监控工具中的时间段选择好。

在这里我们选择SkyWalking时间段2021-01-02 13:53:00 - 2021-01-02 13:54:00。具体拆分时间如下:

  • User - Gateway

  • Gateway

  • Gateway - Cart

  • Cart

  • Cart - MySQL

通过上面抓取的数据你明显可以看到是购物车服务Cart那一段的响应时间长。

我们要注意有些数据抓取工具由于工具本身的问题会存在不小的数据偏差比如说对于上面的SkyWalking时间段我们看到Gateway - Cart之间的服务端平均响应时间是829.25。但是在Cart上却是984.50。同样的一段时间,这里就出现了一些偏差。

在每一个监控工具上都或多或少存在性能数据偏差就比如docker stats我简直是不想看。所以我们有时候要结合多个工具来对比数据。

定向监控分析

拆分完响应时间后我们不再从全局分析开始而是直接跳到了定向监控。因为对于查询购物车这个接口我们已经知道Cart服务是慢的所以我们就直接进去查看对应的慢的方法在哪里。

这个接口的调用方法如下所示:

/**
 * 根据会员id查询购物车数据
 *
 * @param memberId 会员id
 * @return
 */
@Override
public List<OmsCartItem> list(Long memberId) {
    if (memberId == null) {
        return null;
    }
    OmsCartItemExample example = new OmsCartItemExample();
    example.createCriteria().andDeleteStatusEqualTo(0).andMemberIdEqualTo(memberId);
    return cartItemMapper.selectByExample(example);
}

通过上面的代码我们知道了方法名那我们直接用Arthas来Trace这个接口就好了命令如下

trace com.dunshan.mall.cart.service.imp.CartItemServiceImpl list -v -n 5 --skipJDKMethod false '1==1'

于是,我们得到了如下的信息:

[arthas@1]$ trace com.dunshan.mall.cart.service.imp.CartItemServiceImpl list -v -n 5 --skipJDKMethod false '1==1'
Condition express: 1==1 , result: true
`---ts=2021-01-02 14:59:53;thread_name=http-nio-8086-exec-556;id=10808;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@18c26588
    `---[999.018045ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$e110d1ef:list()
        `---[998.970849ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #57


Condition express: 1==1 , result: true
`---ts=2021-01-02 14:59:54;thread_name=http-nio-8086-exec-513;id=107d3;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@18c26588
    `---[1095.593933ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$e110d1ef:list()
        `---[1095.502983ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #57


Condition express: 1==1 , result: true
`---ts=2021-01-02 14:59:53;thread_name=http-nio-8086-exec-505;id=1078b;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@18c26588
    `---[2059.097767ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$e110d1ef:list()
        `---[2059.013275ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #57


Condition express: 1==1 , result: true
`---ts=2021-01-02 14:59:54;thread_name=http-nio-8086-exec-541;id=107f6;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@18c26588
    `---[1499.559298ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$e110d1ef:list()
        `---[1499.498896ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #

通过上面的数据可以看到list()的响应时间确实是长了但是这个接口并不复杂就是一个select语句而已。对应的select语句的Mapper内容如下

<select id="selectByExample" parameterType="com.dunshan.mall.model.OmsCartItemExample" resultMap="BaseResultMap">
    select
    <if test="distinct">
      distinct
    </if>
    <include refid="Base_Column_List" />
    from oms_cart_item
    <if test="_parameter != null">
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClause != null">
      order by ${orderByClause}
    </if>
  </select>

这个Mapper对应到数据库中具体的SQL就是

SELECT id, product_id, product_sku_id, member_id, quantity, price, product_pic, product_name, product_sub_title, product_sku_code, member_nickname, create_date, modify_date, delete_status, product_category_id, product_brand, product_sn, product_attr FROM oms_cart_item WHERE (  delete_status = 0  AND member_id = 597427  )

既然是一个select语句消耗的时间长那我们就到数据库里根据相应的SQL来看对应表的数据直方图。命令如下

select member_id,count(*) from oms_cart_item_202101021530 GROUP BY 1 ORDER BY 2 DESC;

结果如下,我们截取了直方图的部分数据:

从上述数据库中的数据来看一个会员ID的下面已经加了不少数据。虽然select是通过会员ID查的但是没做分页处理。这是最简单直接的SQL问题了分析过程也非常简单。当我们一看到SQL时间长的时候就要查看一下执行计划

既然上面的type值是ALL说明走的是全表扫描那我们就要根据SQL中的where条件来确定一下要创建什么索引如果where条件中的查询结果是多条结果并且数据较多那就需要做分页。分析到这里其实也比较容易想到对应的解决方案有两个动作要做

  1. 创建索引:创建索引是为了查询的时候可以精准查询。
  2. 做分页:是为了避免返回到前端的数据太多。

优化效果

我们虽然讲的是“优化效果”准确来说只是“验证效果”。因为上面的两个动作都是为了提高SQL的查询效果确切来说就为了减少查询出来的数据。那我们现在就直接把数据给降下来来验证我们的判断是不是正确。

为了验证我们的分析过程是正确的这里我先直接把表给TRUNCATE掉先看看响应时间能不能上来。如果能上来那就是这里的问题了。

可如果不是呢?那我们只能回到角落默默流泪了。这么简单的问题都找不到,我不是一个合格的性能分析人员。

不管怎么说,来,我们看下结果:

可以看到TPS一下子上升了很多在场景不间断的情况下这个比对如此喜人。看来我还能继续干这一行。

不过,我们的分析到这里并没有结束,屋漏偏逢连夜雨,我在接着做压力的过程中,又出现了状况,这让我们不得不进入第二个阶段的分析。

第二阶段分析

到底又出现了什么问题呢?具体情况如下:

What? 那是TPS曲线吗那是掉下来了吗掉的还这么多吗同样是场景不间断啊。我的职业生涯难道要就此断送了吗

这个问题有点复杂。但是从响应时间曲线上看明显是响应时间增加了TPS下来了。既然这样仍然走我们拆分响应时间的思路就好了这里不再赘述。

通过拆分时间我们知道响应时间长的问题出在了Gateway上。下面我们就根据RESAR性能分析逻辑老老实实开始分析。

全局监控分析

我们从系统级的资源上可以明显看到所有的worker节点都无压力。

我们再从Pod角度来看一下

你看有些Pod消耗的CPU已经达到了100%。我把所有的Pod排个序结果如下

虽然我们看到了像node_exporter、ES相关的Pod资源都用得不低但是这些CPU使用率高的节点Pod的资源也都限制了。同时你要注意这个资源占用率高的Pod中并没有我们的应用节点也就是说我们应用节点的CPU资源并没有用完。

我本来想去看一下在这段时间内应用所在的worker上的内存消耗具体是怎样的。但是在这段时间内却没了数据

你看中间的数据已经断掉了node_exporter已经不传数了。没有办法我们只有放弃看 worker上的内存消耗了。

既然如此那我们先查一下Gateway在哪个worker上同时也来看一下这个worker上有多少Pod。走这一步是因为在整个Kubernetes中所有的namespace都用worker主机的资源。所以从资源使用的角度来看我们要考虑到所有命名空间中的Pod。

所有namespace在应用节点上的所有Pod如下

- 先查询gateway所在的worker节点名

  [root@k8s-master-2 ~]# kubectl get pods --all-namespaces -o wide | grep gateway
    default                gateway-mall-gateway-6567c8b49c-pc7rf        1/1     Running   0          15h     10.100.140.2     k8s-worker-2   <none>           <none>

- 再查询对应worker上的所有POD
[root@k8s-master-2 ~]# kubectl get pods --all-namespaces -o wide | grep k8s-worker-2
default                elasticsearch-client-1                       1/1     Running   4          20d     10.100.140.28    k8s-worker-2   <none>           <none>
default                elasticsearch-data-2                         1/1     Running   0          4d2h    10.100.140.35    k8s-worker-2   <none>           <none>
default                elasticsearch-master-2                       1/1     Running   4          20d     10.100.140.30    k8s-worker-2   <none>           <none>
default                gateway-mall-gateway-6567c8b49c-pc7rf        1/1     Running   0          15h     10.100.140.2     k8s-worker-2   <none>           <none>
kube-system            calico-node-rlhcc                            1/1     Running   0          2d5h    172.16.106.149   k8s-worker-2   <none>           <none>
kube-system            coredns-59c898cd69-sfd9w                     1/1     Running   4          36d     10.100.140.31    k8s-worker-2   <none>           <none>
kube-system            kube-proxy-l8xf9                             1/1     Running   6          36d     172.16.106.149   k8s-worker-2   <none>           <none>
monitoring             node-exporter-mjsmp                          2/2     Running   0          4d17h   172.16.106.149   k8s-worker-2   <none>           <none>
nginx-ingress          nginx-ingress-nbhqc                          1/1     Running   0          5d19h   10.100.140.34    k8s-worker-2   <none>           <none>
[root@k8s-master-2 ~]# 

从上面的结果可以看到我们的worker节点上有9个Pod。

不过我们一开始看全局资源信息的时候并没有发现整个worker节点的资源使用率很高。这是因为我们已经在Pod里限制了资源。所以我们列一下每个Pod的资源限制

对于那些其他资源占用不高的Pod我们就不看了。

既然资源有限制那我们还要把目光转回到Gateway上面来。

定向监控分析

通过查看链路时间我们也能知道是Gateway上消耗的时间较长

但是这个sendRequest是干嘛的不知道。

那我们就做一个试验看看跳过Gateway之后的TPS是多少。

可见走GatewayTPS只能有400多不走GatewayTPS能达到800多。所以问题确实出在了Gateway上。

看到这里有一个环节我们是缺失的那就是查看Kubernetes容器里的Java进程的健康状态。因为我们在前面查了worker也查了worker上的Pod所以现在就到了第三层也就是Pod中的Java应用。

对此你也不用有负担你想想对于一个Java应用来说能有个啥无非就是堆、栈一顿看。来我们打印个Gateway的栈看一下。

从栈上,啥也没看出来,整个状态似乎都挺合理的。 注意在这里我不是只看一个截图哦我已经把整个栈都撸了一遍。由于CPU也不高我们在分析栈的时候主要看一下有没有锁等待。从上图可以看到并没有锁等待也都合理。

看完栈之后接下来该看堆了。我们得想尽办法把Kubernetes的Java进程堆拿出来看看

看到没如此规则的关联关系TPS和Gateway的GC趋势是完全一致的。

不过这样看还是不够具体我们还需要更细的数据。所以我们进去看一下GC状态

[root@gateway-mall-gateway-6567c8b49c-pc7rf /]# jstat -gcutil 1 1000 1000
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00  55.45  45.33  52.96  94.74  92.77  38427 1953.428    94  113.940 2067.368
 57.16   0.00  26.86  53.24  94.74  92.77  38428 1954.006    94  113.940 2067.946
  0.00  54.30  15.07  53.65  94.74  92.77  38429 1954.110    94  113.940 2068.050
 39.28   0.00  18.39  53.84  94.74  92.77  38430 1954.495    94  113.940 2068.435
 39.28   0.00  81.36  53.84  94.74  92.77  38430 1954.495    94  113.940 2068.435
  0.00  26.13  68.79  53.84  94.74  92.77  38431 1954.597    94  113.940 2068.537
 39.18   0.00  59.75  53.84  94.74  92.77  38432 1954.683    94  113.940 2068.624
  0.00  24.70  76.28  53.84  94.74  92.77  38433 1954.794    94  113.940 2068.734

你看一次YGC大概需要100ms一秒一次YGC这样YGC就占了10%左右,这个时间有点多了。

既然YGC消耗CPU较高那我们就考虑优化Java参数。先来看一下Java参数

[root@gateway-mall-gateway-6567c8b49c-pc7rf /]# jinfo -flags 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.242-b08
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=262144000 -XX:+ManagementServer -XX:MaxHeapSize=4164943872 -XX:MaxNewSize=1388314624 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=87359488 -XX:OldSize=174784512 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops 
Command line:  -Dapp.id=svc-mall-gateway -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=svc-mall-gateway -Dskywalking.collector.backend_service=skywalking-oap:11800 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=1100 -Dcom.sun.management.jmxremote.rmi.port=1100 -Djava.rmi.server.hostname=localhost -Dspring.profiles.active=prod -Djava.security.egd=file:/dev/./urandom
[root@gateway-mall-gateway-6567c8b49c-pc7rf /]# 

从上面的参数中就可以看到我在Kubernetes的Java进程中并没有配置GC回收相关的参数。所以这里我们加上相关的参数。

在下面的参数中我加了PrintGC相关的参数以及ParNew参数

[root@gateway-mall-gateway-6c6f486786-mnd6j /]# jinfo -flags 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12
Non-default VM flags: -XX:CICompilerCount=2 -XX:CompressedClassSpaceSize=1065353216 -XX:+HeapDumpOnOutOfMemoryError -XX:InitialHeapSize=2147483648 -XX:+ManagementServer -XX:MaxHeapSize=2147483648 -XX:MaxMetaspaceSize=1073741824 -XX:MaxNewSize=1073741824 -XX:MetaspaceSize=1073741824 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=1073741824 -XX:OldSize=1073741824 -XX:ParallelGCThreads=6 -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParNewGC 
Command line:  -Dapp.id=svc-mall-gateway -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=svc-mall-gateway -Dskywalking.collector.backend_service=skywalking-oap:11800 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=1100 -Dcom.sun.management.jmxremote.rmi.port=1100 -Djava.rmi.server.hostname=localhost -Xms2g -Xmx2g -XX:MetaspaceSize=1g -XX:MaxMetaspaceSize=1g -Xmn1g -XX:+UseParNewGC -XX:ParallelGCThreads=6 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCDetails -Xloggc:gc.log -Dspring.profiles.active=prod -Djava.security.egd=file:/dev/./urandom
[root@gateway-mall-gateway-6c6f486786-mnd6j /]# 

本来指望ParNew能有啥用然而并没有什么用。

既然加参数不是能很快见效的那我们就得看一下YGC的时候回收了什么然后再来决定从哪里下手收拾Java进程内存的消耗问题。所以我们打印一下jmap histo信息来看一下对象消耗内存的变化如下所示

[root@gateway-mall-gateway-6c6f486786-mnd6j /]# jmap -histo 1 | head -20


 num     #instances         #bytes  class name
----------------------------------------------
   1:       2010270      124874960  [C
   2:        787127       91014984  [I
   3:        601333       42467920  [Ljava.lang.Object;
   4:       1534551       36829224  java.lang.String
   5:        420603       31107504  [B
   6:         21891       21972896  [Ljava.util.concurrent.ConcurrentHashMap$Node;
   7:        186170       11914880  java.util.regex.Matcher
   8:        228807       10982736  java.util.StringTokenizer
   9:        291025        9312800  java.util.concurrent.ConcurrentHashMap$Node
  10:        274253        8804936  [Ljava.lang.String;
  11:        179524        8617152  org.springframework.web.util.pattern.PathPattern$MatchingContext
  12:        210473        8418920  java.util.LinkedHashMap$Entry
  13:        154562        6182480  io.netty.handler.codec.DefaultHeaders$HeaderEntry
  14:        191349        6123168  java.util.LinkedList
  15:        126218        6058464  java.util.TreeMap
  16:         68528        6030464  java.lang.reflect.Method
  17:         98411        5363408  [Ljava.util.HashMap$Node;
[root@gateway-mall-gateway-6c6f486786-mnd6j /]# 

在这里我们需要把这个命令多执行几次看看对象消耗内存的变化。前面我们看到YGC过于频繁但是从内存上来看对象的内存回收得挺好。

所以对于这种YGC很高但从对象内存的消耗又看不出什么有效信息的问题只有一种可能那就是对象创建得快销毁也快。那么我们只有一个地方可以准确查找对象内存的消耗了那就是对象的delta。我们连上JvisualVM看下内存对象delta变量

这张图上的字之所以这么小是因为我连的是远程Windows桌面分辨率不高实在没有办法。不过你要是仔细看的话还是能看到最上面那个HashMap。

我比较喜欢用这种视图来看delta值。从这里可以看到增加和销毁都很快。

在前面我们加了打印GC log的参数所以我们把GC log拿出来分析一下得到结果如下

从上面的分析来看主要是YGC在消耗响应时间。这与我们前面的分析吻合但是我们仍旧没有找到具体的问题点。

在这个问题的分析过程中,我不断在做应用的修改、重启等动作。结果,没想到性能问题没解决,又遇到了两个其他问题,特地记录在这里。

之所以记录这样的问题,是想告诉你:在我们的分析过程中,什么样的问题都有可能存在。而我们虽说是做性能分析的人,但也不是只分析性能问题,而是见到问题就要去解决,要不然,你就走不下去

支线问题一

我在查找宿主机日志时发现如下信息:

[3594300.447892] ACPI Exception: AE_AML_BUFFER_LIMIT, Evaluating _PMM (20130517/power_meter-339)
[3594360.439864] ACPI Error: SMBus/IPMI/GenericSerialBus write requires Buffer of length 66, found length 32 (20130517/exfield-389)
[3594360.439874] ACPI Error: Method parse/execution failed [\_SB_.PMI0._PMM] (Node ffff8801749b05f0), AE_AML_BUFFER_LIMIT (20130517/psparse-536)

从错误信息来看这是一个ACPI缓存区大小的问题。这个缓存大小在BIOS和内核之间没有协商一致也就是说请求的缓存区大小是66字节而给的却是32字节。所以电源监控的管理模块就报了异常。

这是缺少内核模块引起的,因为这个内核模块在我所用的这个内核版本中不会自动更新。对应的解决方法倒也简单:

echo "blacklist acpi_power_meter" >> /etc/modprobe.d/hwmon.conf
modprobe ipmi_si
modprobe acpi_ipmi

其中,第一条命令是为了不让这个错误再次出现。当然了,这不是解决问题,只是不想看到这个报错而心里烦燥。后面两条命令是手动加载模块,但前提是你要更新内核版本。

支线问题二

再回到我们分析的主线上前面提到一个Java的YGC消耗的CPU比较高但是业务逻辑又没有什么问题。所以我尝试换一个最简单的Demo程序先来测试一下整个集体是不是正常的。这个Demo程序没有任何业务逻辑只返回247B的示例程序。

我简单说明一下,我之所以把这个测试过程放在这个支线问题中来描述,是想让我的行为更加有条理。

在这个测试过程中我执行了两次。上图的前半部分走了Ingress后面没有走Ingress可是后面TPS并没有掉下来。这时问题就基本清楚了。

我这里列个表格梳理一下到现在看到的信息,理理思路。

从以上数据可以判断出TPS掉下来和Ingress有绝对的关系。那我们就来看看Ingress的日志

root@nginx-ingress-m9htx:/var/log/nginx# ls -lrt
total 0
lrwxrwxrwx 1 root root 12 Sep 10  2019 stream-access.log -> /proc/1/fd/1
lrwxrwxrwx 1 root root 12 Sep 10  2019 error.log -> /proc/1/fd/2
lrwxrwxrwx 1 root root 12 Sep 10  2019 access.log -> /proc/1/fd/1
root@nginx-ingress-m9htx:/proc/1/fd# ls -lrt
total 0
lrwx------ 1 root root 64 Jan  7 18:00 7 -> 'socket:[211552647]'
lrwx------ 1 root root 64 Jan  7 18:00 4 -> 'anon_inode:[eventpoll]'
lrwx------ 1 root root 64 Jan  7 18:00 3 -> 'socket:[211552615]'
l-wx------ 1 root root 64 Jan  7 18:00 2 -> 'pipe:[211548854]'
l-wx------ 1 root root 64 Jan  7 18:00 1 -> 'pipe:[211548853]'
lrwx------ 1 root root 64 Jan  7 18:00 0 -> /dev/null
root@nginx-ingress-m9htx:/proc/1/fd# find ./ -inum 212815739
root@nginx-ingress-m9htx:/proc/1/fd# find ./ -inum 212815740

悲怆的感觉你看日志直接重定向到标准输出和标准错误了而标准输出和标准错误默认都是屏幕。那我们就到Kubernetes管理工具中去追踪日志。可是结果是啥也没有。唉这可怎么办呢

从下面这张图我们也可以看到当压力经过这个Ingress时报错是必然的压力越大报错越多。

可是分析到这里我们再没有其他可以分析的日志了。没什么办法只能查一下Ingress的版本了结果发现当前的Ingress已经有了新的版本。

为了避免去踩Ingress本身存在的一些坑我把它的版本从1.5.5换到1.9.1之后,得到如下结果:

你看图中没有报错了看来那些错误是Ingress版本导致的。

然而即便如此我们还是没有解决TPS会掉的问题。你可能会说上面这张图里的TPS不是没有掉吗其实这只是假象。在上面的场景中我们只是为了验证Ingress的问题所以执行时间并不长。

请你注意我们到这里并没有解决前面所说的TPS会掉的问题。应该说我们这里可能有两个问题一个是Ingress而另一个可能是在其他地方但是我们还没有去验证。因此我们要回到主线上继续来分析它。

回到主线

经过一翻折腾,你是不是感觉脑袋已经晕了?当我们被一些技术细节深深拖进去的时候,一定要保持清醒。

根据我的经验,这个时候我们可以在纸上画一下架构图。并不是说前面已经有架构图,我们就不用画了。画架构图是为了帮我们梳理思路。并且我们还要画得再细一点:

经过梳理我采用分段测法来判断问题与哪一层相关因为Cart服务需要通过外部调用走网关那我在这里直接调用Cart服务不走网关。并且我也跳过Ingress直接用NodePort来提供服务看看TPS有没有调下来。

首先我直接从cart服务的NodePort压进去得到这样的结果

也就是说Cart服务本身就会导致TPS降下来看起来也并不规律。

那我们就修改Tomcat参数把线程数、连接数调大再来一次。你可能奇怪为什么要这样调呢这是因为在查看应用线程健康状态的过程中我注意到Spring Boot里的Tomcat线程很忙。

在我这样反复验证了几次之后发现TPS并没有掉下去。

为了进一步验证TPS和上面的线程数、连接数等参数有关我又特意把配置改回去再看是不是Tomcat参数的问题。

结果TPS掉下去的情况没有复现

气得我不得不吃份麻辣烫发泄一下。本来我已经看到了TPS掉下来和GC有关。并且我们在GC中经过一顿分析发现Servlet的hashmap$node在快速地创建和回收说明YGC消耗资源多和压力大小有关所以调了Tomcat相关的参数。可是现在在同样的压力下问题竟然不能复现也真是醉了。

像这种随机的问题是比较难整的。不知道TPS稳定的假象是不是和中间有过重启有关。话说重启大法在技术领域中真是绝对的大招。

既然这个问题没有复现,现场也没有了,我们也只能放弃。

虽然针对这个问题,我们从前到后的分析逻辑都非常合理,但是仍然没有找到问题点在哪里。如果它是一个随机的问题,那就是我们没有在合适的时机抓到问题的原因。

对于一个项目来说,如果出现的随机问题对业务造成的影响是不能接受的,那我们就必须花大精力去解决。如果影响不大,那也可以先放一放。但是每一个问题都有出现的必然性,也就是说,那些看似随机的问题,其实在技术中都有着绝对的必然性。

那这个问题到底是什么呢?在这里,我先留一个悬念,因为再继续分析下去,我们这节课就太长了,你看着也很累。下节课我们接着分析。

总结

在这节课中,我们讲了两个阶段的性能分析。

第一个阶段比较简单,就是一个查询的问题。对于查询来说,在实时交易的过程中,最好能够精准查找。如果出现范围查询,那就必须要有分页。

不过,如果是大范围的查询,那不仅会对网络造成压力,同时还会对应用、数据库等各层都产生非常明显的压力。所以,在出现范围查询时,我们必须做好技术选型。当业务必须做这样的范围查询时,你可以考虑换组件,像大数据这样的思路就可以用起来了。

第二个阶段有点麻烦,虽然我们花了很多时间精力,但是到最后没有找到根本原因。不过,我们分析的方向和思路都是没有问题的。

对于这种看似很随机的问题,在实际的项目中也经常出现。我们分析到最后可能会发现这是一个非常简单的问题,让人气得直跺脚。至于这个问题的根本原因是什么,我们下节课再做说明。

无论如何,在这节课中,我们仍然把分析的逻辑描述完整了,希望能给到你一些完整的思路。

课后作业

最后,请你思考一下:

  1. 在实时交易中,如何快速判断数据库的数据量所引发的性能问题?定向分析的证据链是什么?
  2. 如何从CPU使用高定位到GC效率引发的性能问题

记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。

如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下这节课再见!