1.用户中心现状

1.1 查询方式

用户中心查询操作使用 【CompletableFuture】 + 【自定义线程池】 的方式实现,所有查询均为单表操作, 将查询结果在应用层组合后返回给调用方;

1.2 强一致性

用户中心使用读写分离方案,对于强一致性的请求,读请求也需要强制读取主库,但很多操作使用【自定义线程池】,实现方案为:filter拦截需要读取主库的url,需要将上下文透传到【自定义线程池】,配置Mybatis拦截器(aop亦可),根据透传的上下文,使用 Sharding jdbc 设置强制操作主库:HintManagerHolder.isMasterRouteOnly()

1.3 上下文透传

父子线程透传:

父线程透传给子线程可以使用java提供的 InheritableThreadLocal

提交线程与线程池透传:

但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 【任务提交给线程池时】的ThreadLocal值传递到 【任务执行时】。

(a)使用TtlRunnable TtlCallable 修饰 Runnable Callable

(b)使用TtlExecutors 修饰线程池

(c)参考文档:https://github.com/alibaba/transmittable-thread-local

1.4 线程池配置

aysncTaskExecutor(原有线程池,上下文透传、强一致性线程池,用于有查询需求的写操作,保证读取请求读取主库、强一致性)

CorePoolSize = 80 MaxPoolSize = 500 QueueCapacity = 2000

TaskExecutorConfig implements AsyncConfigurer(原有线程池,@Async时使用,用于给第三方异步发送消息)

CorePoolSize = 80 MaxPoolSize = 500 QueueCapacity = 2000

cacheThreadPoolExecutor(C端老师增加读取缓存线程池)

CorePoolSize = 80 MaxPoolSize = 500 QueueCapacity = 2000

transactionThreadTaskPool(C端老师增加缓存后、写操作异步刷新缓存需要等数据库提交后refresh)

CorePoolSize = 80 MaxPoolSize = 500 QueueCapacity = 2000

2.根据学员号查询接口异常

/v2/students/search/code

2.1 批量查询学员

2.1.1 问题

当参数学员号较多、且请求量稍大时,会出现TaskRejectedException异常

2.1.2 分析

C端加缓存改造后,将批量学员号批量查询改为遍历查询缓存、缓存无查询数据库refresh,但此时学员号较多,每个学员号都会启动一个线程查询、直到cacheThreadPoolExecutor队列和线程达到上限采用默认抛弃(拒绝)策略。

同一时刻930批量请求将线程池打满

跟踪单个请求

2.2.3 方案

伟华老师已经将根据学员号查询改为批量查询

2.2 单表查询后数据组合方式

2.2.1 问题

service层组合学员、学员号信息,使用CompletableFuture默认的ForkJoin框架提供的线程池,未使用自定义线程池,ForkJoinPool是典型的异步任务线程池,当大量请求从tomcat线程池打过来时,会将请求打到队列、异步处理,请求方可能会超时,不适合做实时api服务

ForkJoinPool默认线程池配置为:

队列数量:WorkQueue MAXIMUM_QUEUE_CAPACITY = 1 << 26; // 64M

线程数量:cpu可用core - 1

2.2.2 方案

(a)使用自定义线程池,改造简单、但使用CompletableFuture方式适合做任务计算、而非实时api服务。

(b)直接使用tomcat主线程,串行请求单表数据组合可能会导致RT升高,好处是不用切换线程,有问题会直接抛出,而不是自定义线程池内部消化、且易维护。

3.全链路压测服务不可用

3.1 问题

6万并发用户中心服务不可用:response:{"State":0,"Error":"【用户中心系统】异常,请稍后再试。此时用户中心无法接收处理后续连接请求,重启应用才能恢复,需要用户中心相关老师协助配合排查优化;用 户中心异常时压测时间点:(7月22日 00:06/00:20/00:33/00:51)

3.2 用户、订单中心对比分析

吞吐率、响应时间对比:用户中心(6台)、订单中心(3台)

订单单台机器请求量、RT分位图,每分钟请求量6k、最大RT0.2秒、平均5ms

用户中心单台机器请求量、RT分位图,每分钟请求量7.5k、最大RT 9秒左右、平均500ms

3.3 中间件、数据库分析

3.3.1 事务、DB、缓存负载分析

(a)事务分析

前三名分别是:根据studentId查询学员、根据studentCode查询学员、查询真实手机号;最大响应时间长达16.733秒;

(b)数据库分析

吞吐率、响应时间:压测期间、数据库吞吐率没有明显升高,应该是开启缓存的效果、且RT也都在5毫秒左右

慢SQL追踪:次数、响应时间都不大

(c)Redis分析

吞吐率、响应时间:压测期间吞吐率升高,说明流量都打到了redis;响应时间最大值373毫秒、平均RT极小

3.3.2 结论

数据库、Redis非此次压测瓶颈,可以断定程序代码、线程池、JVM参数、垃圾回收器等多方面有调优的可能性

3.4 代码、线程池、虚拟机层分析

3.4.1 异常统计分析

(a) 异常统计

压测期间异常信息总量:6840

压测期间异常信息总量(6840) = RejectedExecutionException(6484) + Redis command timed out(212) + 其他异常(空指针):144

java.util.concurrent.RejectedExecutionException:6484条

Redis command timed out:212条

其他异常(空指针):144

(b)异常分析:

大部分异常为 java.util.concurrent.RejectedExecutionException,缓存改造中C端老师使用了【CompletableFuture】 + 【自定义线程池】方式,单台机器最大并发量为queueSize2000 + maxSize500 = 2500,原来徐文老师仅仅使用了【CompletableFuture】方案,CompletableFuture最大队里为64M、线程数量为cpu core - 1,最多会造成超时,但不会造成RejectedExecutionException异常,因为队列足够大64M。

少部分异常为Redis command timed out,可能是瞬间流量过大导致请求redis超时,也可能仅仅是自己程序内部线程竞争。

下面将从垃圾收集器选型、JVM常用参数、线程数量、GC耗时等几个方面分析

3.4.2 垃圾收集器

订单中心:CMS,适合做web应用等实时api服务,STW时间短

用户中心:Java默认的Parallel,适合做任务,吞吐量优先,STW时间长

3.4.3 JVM参数

订单中心:

-server -Xmx4g -Xms4g -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:CMSInitiatingOccupancyFraction=70

用户中心:

-Xmx8g -Xms8g

内存设置过大,使用默认配置old/new=2,老年代2*8/3、年轻代内存8/3g,两者都过大,不利于内存回收,分布式应用应该采取小内存、多节点模式

3.4.5 线程数量

用户中心是订单中心线程数量的5-7倍

订单中心:

用户中心:

3.4.6 GC耗时

观察用户中心6台机器,在23:30-01:00之间出现几次Full gc、数十次Yong gc,STW时间长达600ms,两方面原因:线程数量过多、跟gc线程抢资源;堆内存设置过大导致gc时间过长

观察订单中心3台机器,在23:30-01:00之间只有Yong gc,无Full gc,且垃圾收集时间在20ms以内

订单中心

用户中心:

3.5 解决方案

3.5.1 线程池

原有方案:【CompletableFuture】+【自定义线程池】并发请求多张表组合数据

当流量较小时,1个tomcat线程对应N个自定义线程请求组合,RT较低;

当流量较大时,假如此时1000 tomcat 线程,那内部需要 N*1000 个线程对应处理任务,当没有这么多线程时,需要进入队列等待,类似于异步处理任务,导致调用方等待超时;当达到自定义线程池上限时(队列满了,线程数量达到maxSize),就会抛出RejectedException

如果依然使用原有方案,因读写操作比例10:1,则应该将写操作线程池aysncTaskExecutor、发送异步消息线程池TaskExecutorConfig,提交事务后刷新缓存线程池 transactionThreadTaskPool 都等写操作线程调小;将读操作改为缓存线程池 Executors.newCachedThreadPool()读取(队列容量为0)此线程池队列SynchronousQueue,缓存60秒,线程最大为数量为Integer.MAX_VALUE

A [blocking queue](eclipse-javadoc:☂=extend-core/D:\/Soft\/jdk1.8.0_92\/jre\/lib\/rt.jar in which each insert operation must wait for a corresponding remove operation by another thread, and vice versa. A synchronous queue does not have any internal capacity, not even a capacity of one.

依然会有1个请求对应N个线程的问题

建议方案:建议不再使用【CompletableFuture】 + 【自定义线程池】方式查询,直接使用tomcat主线程,串行请求单表数据(数据库或缓存)组合。

当流量较小时,此方案可能会导致单条RT升高,好处是不用切换线程,能够节省部分资源;

当流量较大时,也不会有明显的RT升高、有问题会直接抛出,而不是自定义线程池内部消化、且易维护。

3.5.2 垃圾收集器选型、JVM参数调整

建议使用CMS垃圾收集器,小内存(尤其是年轻代,因大部分都是很快消亡的,Yong gc频繁点远远好于Full gc)、多节点方案

-server -Xmx4g -Xms4g -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:CMSInitiatingOccupancyFraction=70