SpringBoot内嵌Tomcat优化

背景

  • springboot内嵌tomcat学习

系统参数

应用服务优化项

  1. springboot tomcat配置数字,当链接超出后依然会阻塞,不会立即返回链接失败
  2. tomcat配置线程不是轮询分配,相对轮询分配。
  3. dubbo链接超出配置线程数后会立即返回失败

web基础学习和动态编译

web框架加载

过程记录

  • 项目使用了gradle,为springmvc的结构.当前进行调试测试用例,

    1. gradle使用tomcat运行,类打包了到了build的目录,同时idea设置的自动编译目录为out目录.这里就出现一个问题,热部署时候类不在一起,不过发现类会在2个目录存在,估计为同时会产生编译
      1
      maven项目的为target目录.同时idea也会设置成这个.猜想maven自行设置的
    2. springmvc项目进行test用例时候运行,找不到WEB-INF下的资源.这就回到编译目录来看,运行的测试环境使用的是选择的是idea,不是gradle.所以WEB-INF不在范围内.
      1
      gradle中有单独参数设置webapp目录project.webAppDirName
    3. springboot的maven项目如何结合servlet.
      1
      2
      3
      1. Application中添加@ServletComponentScan
      2. 定义一个类贴上注解:@WebServlet(name = "MyServlet",urlPatterns = "/myServlet").这时候可以使用普通的servlet
      3. 普通servlet堆栈信息很简单,这时候如果要用filter需要使用原生的注解,可以百度,这里不在赘述
  • 动态编译

    1. idea有自带动态编译的功能,在tomcat或者springboot类的on aciont中选择:update class and resources
      1
      2
      1. 这里需要注意你的如果是gradle项目,注意你选择对你的项目编译采用的gradle还是idea.
      2. gradle默认的编译目录不一致,有可能会导致热部署失效,注意检查一下.
    2. 使用三方工具arthas.优秀的一款工具,具体使用可以参照说明文档.

springmvc和springboot以及servlet

  1. springmvc只是需要指定webapp路径
  2. springboot已经使用了servlet3.0+ 可以使用注解来进行,不要再配置web.xml
  3. 如果项目需要springboot但同时又需要web.xml的.更改springboot的注册方式(存疑)

SpringMvc启动初始化过程

springmvc

web.xml 配置解释

  1. 放在全局中,格式如下

    1
    2
    3
    4
    5
    <!--全局提前初始化的使用 -->
    <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-mvc.xml</param-value>
    </context-param>
  2. 同spring的dispatchServlet放在一起

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!--configure the setting of springmvcDispatcherServlet and configure the mapping -->
    <!-- 如果把下面注释掉,会默认使用servlet-name的值拼上serlvet。找WEB-INF下的类。-->
    <servlet>
    <servlet-name>spring-mvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    </servlet>
    • spring-mvc下的servlet中init-param可以省略掉。如果省略掉会默认使用:servlet-name的value值拼上servlet.xml。如上文则是:(spring-mvc-servlet.xml).查找路径需在WEB-INF下。和web.xml一致
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      类名:XmlWebApplicationContext
      加载文件方法:
      protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
      String[] configLocations = getConfigLocations();
      if (configLocations != null) {
      for (String configLocation : configLocations) {
      reader.loadBeanDefinitions(configLocation);
      }
      }
      }

      springmvc的差别(谁加载了context-param的参数?)

  3. servelt的init-param的contextConfigLocation是为了加载DispatcherServlet的. 而context-param参数 的applicationContext.xm是为了加载web程序需要加载的数据库等等配置。

问题

  • 问题一:放context-param和init-param区别?
    1. Context-param(上下文参数),在这个元素中可以定义多个组成的键值对,但是要注意这里定义的键值对作用于是application,而且在有些应用中会提前定义自己的键值对,所以可以通过这种方式配置某些技术,同时这里也可以自定义一些参数,然后在业务逻辑中使用。获取键值对的方式如下
      1
      ServletContextEvent .getServletContext().getInitParameter("urlrewrite");
    2. 的作用范围则是当前对应的Servlet,只有对应的Servlet才能够调用到,有些提前定义的Servlet中也会判断是否有某些配置的键值对,如果有则根据配置的键值对处理逻辑,没有则根据默认的逻辑处理,同时也可以自定义键值对在后期自定义的Servlet当中使用。获取键值对的方式如下
      1
      this.getInitParameter("param1")
      备注: 注意以上两者获取键值对的方式的区别,第一个必须获取ServletContext之后才能够获取,因为第一个的键值对属于整个应用,而第二个则是通过this获取,因为这里获取的键值对仅仅属于当前的Servlet。

流程

  1. 启动一个WEB项目的时候,容器(如:Tomcat)会去读它的配置文件web.xml.读两个节点:
  2. 紧接着,容器创建一个ServletContext(上下文),这个WEB项目所有部分都将共享这个上下文.
  3. 容器将转化为键值对,并交给ServletContext.
  4. 容器创建中的类实例,即创建监听.
  5. 在监听中会有contextInitialized(ServletContextEvent args)初始化方法,在这个方法中获得ServletContext =ServletContextEvent.getServletContext();
    context-param的值 =ServletContext.getInitParameter(“context-param的键”);
  6. 得到这个context-param的值之后,你就可以做一些操作了.注意,这个时候你的WEB项目还没有完全启动完成.这个动作会比所有的Servlet都要早.
    换句话说,这个时候,你对中的键值做的操作,将在你的WEB项目完全启动之前被执行.
  7. 举例.你可能想在项目启动之前就打开数据库.那么这里就可以在中设置数据库的连接方式,在监听类中初始化数据库的连接.
  8. 这个监听是自己写的一个类,除了初始化方法,它还有销毁方法.用于关闭应用前释放资源.比如说数据库连接的关闭.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    如:
    <!-- 加载spring的配置文件 -->
    <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml,/WEB-INF/action-servlet.xml,/WEB-

    INF/jason-servlet.xml</param-value>
    </context-param>
    <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

微博热搜的实现

微博

实现原理

  1. 网上没有具体的,看该内容涉及几个词。也进行一个总结记录
  • 抽样检查

    1
    用于计算和存储相关数据
  • 热词排名方法一:贝叶斯平均法(Bayesian average)/热词排名法二:牛顿冷却定律

    1
    已关键字进行求值分数,在用优先级队列进行求TOP

线上JVM运行调试

jvm调试

gc部分

  • jvm的垃圾收集器状态

    1. jmap -heap [pid]
  • jvm dump堆文件

    1. jmap -dump:format=b,file=[路径].hprof [pid]
  • 手动gc

    1. jmap -histo:live [pid]
  • 查看gc的次数和频率

    1. 使用jstat。参考文章:java性能调试工具
    2. 在实际使用中发现如下的问题。后面在一篇文章有部分说明:参考文章
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      1. 新生代为680M。SurvivorRatio为8。也就是新生区544M,2个幸存区各68M。但结合线上的jmap和jstat中,这个使用内存量会变,幸存区实际再用低于68M,新生区会大于544。
      原因:使用的是ps新生代垃圾收集器才会是这个问题。parnew则是按照配置来预先分配
      parallel scavenge新生代中具体分配策略参见:
      备注: Parallel Scavenge 收集器
      特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。
      该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(这就是为啥幸存区不是按照比例的来配置大小的原因)(与ParNew收集器最重要的一个区别)
      GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdaptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
      Parallel Scavenge收集器使用两个参数控制吞吐量:
      XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
      XX:GCRatio 直接设置吞吐量的大小。

jvm更换垃圾收集器

  1. Serial(串行)收集器
    1. -XX:+UseSerialGC
  2. Parallel(并行)收集器
    1. -XX:+UseParallelGC -XX:+UseParallelOldGC
  3. CMS(并发)收集器
    1. -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
  4. 开启G1收集器的方式
    1. -XX:+UseG1GC
  5. 组合说明
    1
    2
    3
    4
    5
    -XX:+UseParNewGC = ParNew + SerialOld这个组合已经很少用(在某些版本中已经废弃)
    -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
    -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
    -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
    -XX:+UseG1GC = G1

jvm内存管理

  1. Java_JVM参数-XX:MaxDirectMemorySize 与 两种 ByteBuffer: heap,direct ByteBuffer(参考:https://www.cnblogs.com/laoqing/p/10380536.html)
  2. ByteBuffer有两种:
    • heap ByteBuffer -> -XX:Xmx
      • 1.1、一种是heap ByteBuffer,该类对象分配在JVM的堆内存里面,直接由Java虚拟机负责垃圾回收;
    • direct ByteBuffer -> -XX:MaxDirectMemorySize
      • 1.2、一种是direct ByteBuffer是通过jni在虚拟机外内存中分配的。通过jmap无法查看该快内存的使用情况。只能通过top来看它的内存使用情况。
        • 1.2.1、JVM堆内存大小可以通过-Xmx来设置,同样的direct ByteBuffer可以通过-XX:MaxDirectMemorySize来设置,此参数的含义是当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC。注意该值是有上限的,默认是64M,最大为sun.misc.VM.maxDirectMemory(),在程序中中可以获得-XX:MaxDirectMemorySize的设置的值。
        • 1.2.2、没有配置MaxDirectMemorySize的,因此MaxDirectMemorySize的大小即等于-Xmx
        • 1.2.3、Direct Memory的回收机制,Direct Memory是受GC控制的
        • 1.2.4、对于使用Direct Memory较多的场景,需要注意下MaxDirectMemorySize的设置,避免-Xmx + Direct Memory超出物理内存大小的现象

常用参数说明

  • jvm一些常用参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    -server                                             ## 服务器模式
    -Xms2g ## 初始化堆内存大小
    -Xmx2g ## 堆内存最大值
    -Xmn256m ## 年轻代内存大小,整个JVM内存=年轻代 + 年老代 + 持久代
    -Xss256k ## 设置每个线程的堆栈大小
    -XX:PermSize=256m ## 持久代内存大小
    -XX:MetaspaceSize=21m ## 最大持久代内存大小
    -XX:MaxMetaspaceSize=21m ## 最大可分配元空间
    -XX:MaxDirectMemorySize=21m ## 直接内存分配
    -XX:ReservedCodeCacheSize=256m ## 代码缓存,存储已编译方法生成的本地代码
    -XX:+UseCodeCacheFlushing ## 代码缓存满时,让JVM放弃一些编译代码
    -XX:+DisableExplicitGC ## 忽略手动调用GC, System.gc()的调用就会变成一个空调用,完全不触发GC
    -Xnoclassgc ## 禁用类的垃圾回收,性能会高一点
    -XX:+UseConcMarkSweepGC ## 并发标记清除(CMS)收集器
    -XX:+CMSParallelRemarkEnabled ## 启用并行标记,降低标记停顿
    -XX:+UseParNewGC ## 对年轻代采用多线程并行回收,这样收得快
    -XX:+UseCMSCompactAtFullCollection ## 在FULL GC的时候对年老代的压缩,Full GC后会进行内存碎片整理,过程无法并发,空间碎片问题没有了,但提顿时间不得不变长了
    -XX:CMSFullGCsBeforeCompaction=3 ## 多少次Full GC 后压缩old generation一次
    -XX:LargePageSizeInBytes=128m ## 内存页的大小
    -XX:+UseFastAccessorMethods ## 原始类型的快速优化
    -XX:+UseCMSInitiatingOccupancyOnly ## 使用设定的回收阈值(下面指定的70%)开始CMS收集,如果不指定,JVM仅在第一次使用设定值,后续则自动调整
    -XX:CMSInitiatingOccupancyFraction=70 ## 使用cms作为垃圾回收使用70%后开始CMS收集
    -XX:SoftRefLRUPolicyMSPerMB=50 ## Soft reference清除频率,默认存活1s,设置为0就是不用就清除
    -XX:+AlwaysPreTouch ## 强制操作系统把内存真正分配给JVM
    -XX:+PrintClassHistogram ## 按下Ctrl+Break后,打印类的信息
    -XX:+PrintGCDetails ## 输出GC详细日志
    -XX:+PrintGCTimeStamps ## 输出GC的时间戳(以基准时间的形式)
    -XX:+PrintHeapAtGC ## 在进行GC的前后打印出堆的信息
    -XX:+PrintGCApplicationConcurrentTime ## 输出GC之间运行了多少时间
    -XX:+PrintTenuringDistribution ## 参数观察各个Age的对象总大小
    -XX:+ParallelRefProcEnabled ## 默认为 false,并行的处理 Reference 对象,如 WeakReference,除非在 GC log 里出现 Reference 处理时间较长的日志,否则效果不会很明显。
    -XX:+PrintGCApplicationStoppedTime ## GC造成应用暂停的时间
    -Xloggc:../log/gc.log ## 指定GC日志文件的输出路径
    -ea ## 打开断言机制,jvm默认关闭
    -Dsun.io.useCanonCaches=false ## java_home没有配置,或配置错误会报异常
    -Dsun.awt.keepWorkingSetOnMinimize=true ## 可以让IDEA最小化到任务栏时依然保持以占有的内存,当你重新回到IDEA,能够被快速显示,而不是由灰白的界面逐渐显现整个界面,加快回复到原界面的速度
    -Djava.net.preferIPv4Stack=true ## 让tomcat默认使用IPv4
    -Djdk.http.auth.tunneling.disabledSchemes="" ## 等于Basic会禁止proxy使用用户名密码这种鉴权方式,反之空就可以使用
    -Djsse.enablesSNIExtension=false ## SNI支持,默认开启,开启会造成ssl握手警告
    -XX:+HeapDumpOnOutOfMemoryError ## 表示当JVM发生OOM时,自动生成DUMP文件
    -XX:HeapDumpPath=D:/data/log ## 表示生成DUMP文件的路径,也可以指定文件名称,如果不指定文件名,默认为:java_<pid>_<date>_<time>_heapDump.hprof。
    -XX:-OmitStackTraceInFastThrow ## 省略异常栈信息从而快速抛出,这个配置抛出这个异常非常快,不用额外分配内存,也不用爬栈,但是出问题看不到stack trace,不利于排查问题
    -Dfile.encoding=UTF-8
    -Duser.name=qhong

    -XX:NewRatio=3 ## 新生代与年老代的比例。比如为3,则新生代占堆的1/4,年老代占3/4。
    -XX:SurvivorRatio=8 ## 新生代中调整eden区与survivor区的比例,默认为8,即eden区为80%的大小,两个survivor分别为10%的大小。
    -XX:PretenureSizeThreshold=10m ## 晋升年老代的对象大小。默认为0,比如设为10M,则超过10M的对象将不在eden区分配,而直接进入年老代。
    -XX:MaxTenuringThreshold=15 ## 晋升老年代的最大年龄。默认为15,比如设为10,则对象在10次普通GC后将会被放入年老代。
    -XX:MaxTenuringThreshold=0 ## 垃圾最大年龄,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,该参数只有在串行GC时才有效
    -XX:+HeapDumpBeforeFullGC ## 当JVM 执行 FullGC 前执行 dump
    -XX:+HeapDumpAfterFullGC ## 当JVM 执行 FullGC 后执行 dump
    -XX:+HeapDumpOnCtrlBreak ## 交互式获取dump。在控制台按下快捷键Ctrl + Break时,JVM就会转存一下堆快照
    -XX:+PrintGC ## 输出GC日志
    -verbose:gc ## 同PrintGC,输出GC日志
    -XX:+PrintGCDateStamps ## 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
    -XX:+PrintFlagsInitial ## 显示所有可设置参数及默认值
    -enablesystemassertions ## 激活系统类的断言
    -esa ## 同上
    -disablesystemassertions ## 关闭系统类的断言
    -dsa ## 同上
    -XX:+ScavengeBeforeFullGC ## FullGC前回收年轻代内存,默认开启
    -XX:+CMSScavengeBeforeRemark ## CMS remark前回收年轻代内存
    -XX:+CMSIncrementalMode ## 采用增量式的标记方式,减少标记时应用停顿时间
    -XX:+CMSClassUnloadingEnabled ## 相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。如果希望回收,就使用该标志,注意,即使没有设置这个标志,一旦永久代耗尽空间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC
    -XX:+CMSConcurrentMTEnabled ## 当该标志被启用时,并发的CMS阶段将以多线程执行(因此,多个GC线程

问题故障解析

mq故障

  1. 事故现象:服务开始出现请求缓慢,进而系统全面崩溃

    1
    2
    3
    4
    5
    6
    8:00,收到系统故障提醒,并立即进行故障的排查; 
    8:00至16:00,分析问题,从CPU、内存、连接数量、系统日志等进行分析均未发现明显异常,
    尝试各种解决方案,包括限流、熔断、服务降级、服务重启等均无法有效解决;
    16:00, 发现到消息队列服务器有阻塞现象,对消息队列服务器进行重启;
    16:30,完成所有相关的服务器重启,系统恢复正常,并开始验证服务的稳定性;
    17:20,对外恢复服务;
  2. 问题出现原因。

    1
    2
    3
    4
    5
    1. 出现问题原因在于生产者发送消息过快,mq出现消息堆积,同时消费者处理过慢。
    2. 由于消息持续堆积引发了rabbitmq自己的保护机制,开始阻塞生产者写入消息。
    3. 生产者开启的是线程,线程被阻塞无法释放,线程不断开启,重启服务后没过几分钟服务又开始宕机。
    4. 虽然看见mq消费堆积,服务重启后依然在不断消费,所以还未察觉到是mq有问题。
    5. mq的阻塞写入造成一个循环,消费慢,写入快,系统服务级联反应。造成系统持续不可用。
  3. 执行过程。

    1
    2
    3
    4
    1. 线上出现问题,第一要点保证服务尽快可用。
    2. 对于中间件重启,第一要点恢复数据,防止数据丢失。
    3. mq由于本次消费堆积,进行了和相应业务方对接,对mq消息进行区分。哪些可以直接删除(需要参照业务)。哪些重新导出写脚本进行补偿。
    4. mq重启后服务一切正常,系统恢复可用状态

rabbitmq阻塞说明:https://cloud.tencent.com/developer/article/1454194

1
2
3
4
5
6
7
8
9
1. 因为RabbitMQ服务器在启动时会计算系统内存总大小。然后会根据vm_memory_high_watermark参数指定的百分比,进行控制
2. 当RabbitMQ的磁盘空闲空间小于50M(默认),生产者将被BLOCK,并且阻塞信息发布前,会尝试把内存中的信息输出到磁盘上
```

# redis故障

1. 事故现象:个别服务大量redis timeout。导致涉及缓存接口不可用。
```$xslt
1. 重启redis,数据抓紧恢复,影响1小时
  1. 问题出现原因。本次问题出现为3个原因进行叠加。
    1
    2
    3
    1. redis存在大量的大key,导致redis服务的压力过大。
    2. 中间件的不合理,由于开发者不了解jedis,对jedis在外面在包装一个线程池,导致线程池套用线程池。redis服务的tcp连接非常多。
    3. redis目前为共享一个,线上服务多。最终问题出现压垮了redis。

java线程池状态

  1. Java线程不同状态下中断机制的效果
    状态 中断效果 描述
    NEW    无
    RUNNABLE    设置中断标志位    用户自己判断是否中断,以及如何处理
    BLOCKED    设置中断标志位    用户自己判断是否中断,以及如何处理
    WAITING    抛InterruptedException异常,并清空中断标志位
    TIMED_WAITING    抛InterruptedException异常,并清空中断标志位
    TERMINATED    死亡状态

SpringCloud系统性能优化

背景

  • 需要对系统进行流程性测试,查找系统瓶颈点所在,当大流量来临能有效应对。现将优化处理方法进行梳理总结,供大家参考学习

系统参数

系统服务名 服务器数量 CPU(核) 内存(G)
nginx 2 4 8
gateway 4 8 16
course(不同服务器类型) 2 2 4
course(不同服务器类型) 2 8 64
user 2 2 4
login 2 2 4
数据库1 1 8 16
数据库2 1 8 16
redis 1 2 2
  • 服务器说明:在测试中有直连进行测试,实际测试某一个服务中,未全部使用服务器,会根据测试内容临时下线机器,具体方案会在后续说到。
  • 启动参数中:gateway使用内存配置为4g。 其他应用服务启动参数内存为2g

压测优化点内容

gateway优化项

  • 网关核心功能

    • 路由转发
    • 流量计算/限流
    • 统一登录验证
  • spring cloud gateway文档:https://docs.spring.io/spring-cloud-gateway/docs/2.2.6.RELEASE/reference/html/#gateway-starter

  • -Dreactor.netty.ioWorkerCount=64

    问题1:线程数量设置过小。

    1. 被压测接口:返回当前系统时间。接口响应平均响应时间在1毫秒不到
    2. 压测方法:直连tomcat压测,和通过gateway在连接对比压测
    3. 压测QPS:tomcat直连:3W+ 。 gateway链接:1500+
    4. 说明:gateway有进行redis的登录验证操作,耗时在2、3毫秒左右,redis的瓶颈在1W左右
    5. 压测过程中,对比后发现gateway服务器的cpu利用率很低,对比发现属于redis验证阻塞了主线程,导致请求无法及时转发。
    6. gateway使用reactor netty进行作为转发框架。默认设置为cpu数量同等线程数,但只适合cpu密集型任务,对于路由转发任务需要调高线程数量,以便于提高cpu利用率
      参考文章:https://blog.csdn.net/trecn001/article/details/107286396

    问题2:登录验证redis存在大key

    1. 被压测接口:同问题1一致
    2. 压测方法:同问题1一致
    3. 压测QPS:同问题1一致
    4. 说明:同问题1一致
    5. 经过解决问题1后,QPS依然维持在1W左右,通过计算,用户登录后存储在redis中字节数为1388个字节,redis带宽为128Mbit/s。换算后redis的带宽瓶颈为QPS:1W+。去掉中间程序因素,只能维持在1w左右
    6. 追踪程序后,用户登录使用的为jwt验证。会将用户所有数据进行加密存储为accessToken和一个refreshToken。
      参考文章:https://www.cnblogs.com/ruoruchujian/p/11271285.html
    7. redis存储信息包含:用户id,用户类型,accessToken,refreshToken,deviceId,jti。同时:根据jwt的规则accessToken加密串已经包含了所有的信息,所以不需要在单独存储。同时查看目前系统登录逻辑refreshToken暂时并没有使用,只是用于一个扩展项。
    8. 对登录用户信息进行优化,redis不在存储refreshToken,同时对加密token进行字段缩减。只放入userId,deviceId必要字段,加密串大大减少。缩减后剩余388个字节。redis带宽可同步增长3~4倍

应用服务优化项

  1. spring actuator至性能衰减

    1
    2
    3
    1. https://www.shuzhiduo.com/A/rV57PbN9dP/
    2. 由于actuator会注入很多*号的url。这时候程序使用PathVariable ,会造成很多次的无效匹配
    3. 解决:actuator声明一些需要使用的端点,不要使用*号进行
  2. 华为云redis查询QPS过低排查

    1
    2
    1. redis带宽不足
    2. 程序的并发高,同时用户存储了所有数据,占用大量带宽
  3. spring mvc transactional导致性能瓶颈

  4. 应用服务tomcat连接数配置,此项不作说明,需要根据业务系统来定

    1
    2
    3
    4
    server.tomcat.max-threads=300   // springboot默认是200
    server.tomcat.accept-count=200 // springboot默认是100
    server.tomcat.max-connections=8192 // springboot默认是8192 . 1024*8。
    server.tomcat.min-spare-threads=50 // springboot默认是10
  5. hystrix使用信号量配置减少CPU上下文切换,此项不作说明,需要根据业务系统来定

    1
    2
    1. 参考文章:https://blog.csdn.net/dap769815768/article/details/94630276
    2. 系统使用了okhttp,本身有配置相应线程池,不需要在使用hystrix进行线程池。以减少cpu争用

优化总结

  1. 在分布式系统中,需要定位问题点,问题点对应了才能进行解决。上面解决方案非常简单,但难点是定位到各个问题点。同时有可能是多不同问题点叠加产生瓶颈。
  2. 定位问题中需要进行分解目标点,参考附录大致图中。以下是定位gateway网关的思路。其他接口也可以参照以下思路。使用排除法一步步测试
    • 系统经过第一次的压测,将应用服务进行了优化。随后进行第二轮压测,定位至网关redis瓶颈和线程数。定位过程如下
      • 在前面压测中,由于应用服务存在瓶颈点,一直未打满gateway,导致无法压测出gateway有瓶颈。本次进行全流程的压测,其中有一个响应在几毫秒内的接口。对比tomcat和网关后发现有巨大差异。
      • 压测中需要进行一步步排除差异,第一步:先进行了tomcat压测,排除nginx和gateway,第二步:直连其中一台gateway进行压测,排除nginx,第三步:外网域名压测。
      • 结果对比后第二步有巨大差异,而且波动很大。第三步由于nginx有2台,gateway有4台。在物理设备中有增加,但同时也同第一步的结果差异较大
      • 通过第二步对比tomcat和经过网关在转发的QPS数后,准备了一个不进行登录验证空接口进行测试(下文该接口记录为A),同时网关有进行登录验证,在准备一个需要登录验证的空接口(下文该接口记录为B)。
      • 直接进行tomcat压测,确定应用服务是否存在瓶颈点。对tomcat链接瓶颈疑问直连进行压测。确定tomcat参数正常,不存在相应瓶颈
      • 进行第二步操作细化,先使用了A接口进行压测。对比发现QPS相差较小,由于经过网关一层有相比较有下降,稍QPS少一点为正常现象。
      • 确定了A接口不存在问题。这时候在使用B接口进行测试,运行效果相差特别大。QPS有10的倍数下降。这时候可以确定为网关登录验证出现问题
      • 当前问题进行了细化,网关的登录验证产生瓶颈。这时候定位出2个问题。
      • 首先查看了redis瓶颈,通过监控发现redis没有瓶颈,带宽使用量也不高,那么就确定为redis的客户端也就是gateway存在其他瓶颈。
      • 首先对gateway的redis连接数进行优化,调高参数,进行再次压测。但调高参数发现并没有效果,同时调高的参数并没有被使用上。在细化后瓶颈点不在redis连接获取数据上
      • 对gateway的流程进行梳理,查看开启的线程数量为8个。同时经过跟踪后,redis的操作是在主线程进行。主线程数量不足导致的并发数无法提高
      • 提高主线程并发数量,QPS响应开始以倍数提高。最终测试通过

参考文章:https://www.cnblogs.com/binyue/p/6141088.html

性能优化

  • spring mvc transactional导致性能瓶颈

  • 华为云redis带宽限制

  • spring actuator至性能瓶颈(https://www.shuzhiduo.com/A/rV57PbN9dP/)

  • 优化项:tomcat进行并发配置

  • hystrix信号量配置减少CPU上下文切换

img.png