Java服务_并发TP90&TP99性能与可用率监控

Java服务_并发TP90&TP99性能与可用率监控

1.监控原理

1.1基本概念

方法性能就是指方法的调用时间,主要用TP50、TP90、TP99等(top percent)表示。

TP90就是在监控单位时间内的所有调用中,90%调用完成所需要的最短时间。统计1分钟内的所有调用所需时间,从小到大排序,前90%中的最后一个即为1分钟内的TP90。

方法可用率是指方法调用成功率,不发生异常的概率

1.2实现思路

监控某个方法的调用时间和可用率基本步骤:

  • 方法开始前开始记时;
  • 方法发生异常时记录失败;
  • 方法完成时记录总时间。

单机部署应用的监控数据,可以保存在本地内存中的static变量、缓存,也可以保存在本地硬盘中的数据库、日志文件等。

如果要看分布式部署应用总的监控,则必须远程保存到同一个地方,缓存、日志文件、db都可以实现。

2.单机实例

2.1监控方法类

核心属性与方法:

  • 两个Map<String, DelayQueue<T>>类型属性用于分别保存调用时间和调用异常数据;
  • static代码块执行定时删除过期数据逻辑;
  • start()用于起使位置埋点,error()用于异常位置埋点,end()用于结束位置埋点,getAP()用于获取方法可用率数据,getTP()用于获取方法性能数据。
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package jia.zheng.monitor;

import java.util.*;
import java.util.concurrent.*;
import java.util.function.Predicate;

public class Monitor {

//重要:此处最好不要用LinkedList来保存服务调用时间队列,因为LinkedList只能通过index倒序删除数据,存在线程安全问题,在删除数据同时插入数据,导致删错数据。
//costTime中key为服务方法名,DelayQueue<CostTime>中保存了方法调用时间队列;
//errorCount中key为服务方法名,一旦方法异常,则向DelayQueue<ErrorCount>中插入一个ErrorCount对象,直接用Queue的长度来进行调用失败计数。
private static Map<String, DelayQueue<CostTime>> costTime = new HashMap<>();
private static Map<String, DelayQueue<ErrorCount>> errorCount = new HashMap<>();

//创建一个定时轮询线程池,用来定时执行过期的数据删除。
private static ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);

static {
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
for (Map.Entry<String, DelayQueue<CostTime>> entry : costTime.entrySet()) {
//重要:将队列中过期时间小于或等于0的CostTime全都删除。DelayQueue、Delayed、Predicate这一套东西也很有意思。
entry.getValue().removeIf(new Predicate<CostTime>() {
@Override
public boolean test(CostTime costTime) {
return costTime.getDelay(TimeUnit.MILLISECONDS) <= 0;
}
});
}
for (Map.Entry<String, DelayQueue<ErrorCount>> entry : errorCount.entrySet()) {
entry.getValue().removeIf(new Predicate<ErrorCount>() {
@Override
public boolean test(ErrorCount errorCount) {
return errorCount.getDelay(TimeUnit.MILLISECONDS) <= 0;
}
});
}
}
}, 0, 5, TimeUnit.SECONDS);//项目启动时就开始运行,每5s执行一次。
}

public static Info start(String serviceName) {
return new Info(serviceName);
}

public static void error(Info info) {
info.setState(false);
}

public static void end(Info info) {
if (info.isState()) {//服务调用正常
DelayQueue<CostTime> delayQueue = costTime.get(info.getServiceName());
if (delayQueue == null) {
delayQueue = new DelayQueue<>();
}
//重要:设置数据的过期时间,此处设置CostTime对象的过期时间为60s,也就是说计算方法性能和可用率的单位时间为60s。
delayQueue.put(new CostTime(System.currentTimeMillis() - info.getStartTime(), 60 * 1000));
costTime.put(info.getServiceName(), delayQueue);
}else {//服务调用发生错误
DelayQueue<ErrorCount> delayQueue = errorCount.get(info.getServiceName());
if (delayQueue == null) {
delayQueue = new DelayQueue<>();
}
delayQueue.put(new ErrorCount(60 * 1000));
errorCount.put(info.getServiceName(), delayQueue);
}
}

//计算服务失败数和成功数,用于计算可用率,就是 (1-失败数/(失败数+时间数))
public static int[] getAP(String serviceName) {
int[] AP = new int[2];
AP[0] = 0;
AP[1] = 0;

DelayQueue<CostTime> costQueue = costTime.get(serviceName);
DelayQueue<ErrorCount> errorQueue = errorCount.get(serviceName);

if (errorQueue == null) {
if (costQueue == null) {
return AP;
}
AP[1] = costQueue.size();
return AP;
}

AP[0] = errorQueue.size();
if (costQueue == null) {
return AP;
}
AP[1] = costQueue.size();
return AP;
}

//计算服务性能
public static long[] getTP(String serviceName) {
long[] TP = new long[2];
TP[0] = -1;
TP[1] = -1;

DelayQueue<CostTime> costQueue = costTime.get(serviceName);
if (costQueue == null || costQueue.size() == 0) {
return TP;
}
CostTime[] costTimes = costQueue.toArray(new CostTime[]{});
List<CostTime> costTimeList = Arrays.asList(costTimes);

//将调用时间进行排序
Collections.sort(costTimeList, new Comparator<CostTime>() {
@Override
public int compare(CostTime o1, CostTime o2) {
return (int) (o1.getCostTime() - o2.getCostTime());
}
});
int size = costTimeList.size();
//重要:注意要向下取整,向上取整有可能导致ArrayIndexOutOfBoundsException。
TP[0] = costTimeList.get((int) Math.floor(0.9d * size)).getCostTime();
TP[1] = costTimeList.get((int) Math.floor(0.99d * size)).getCostTime();
return TP;
}
}
  • 最好不要用LinkedList来保存服务调用时间队列,因为LinkedList只能通过index倒序删除数据,存在线程安全问题,在删除数据同时插入数据,导致删错数据。此处使用DelayQueue来保存相关数据,并创建定时轮询线程池,借助Delayed和Predicate定时删除过期数据。
  • 我们可以自定义过期时间,过期时间实际上就是监控单位时间。
  • 取TP90或TP99时要向下取整,向上取整有可能超出数组范围,导致ArrayIndexOutOfBoundsException。

2.2属性类

埋点属性类

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
package jia.zheng.monitor;

//方法调用属性类
public class Info {
//方法key
private String serviceName;
//方法开始时间
private long startTime;
//方法调用成功与否
private boolean state;

public Info(String serviceName) {
this.serviceName = serviceName;
this.startTime = System.currentTimeMillis();
this.state = true;
}

public String getServiceName() {
return serviceName;
}

public long getStartTime() {
return startTime;
}

public boolean isState() {
return state;
}

public void setState(boolean state) {
this.state = state;
}
}

调用时间数据类

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
package jia.zheng.monitor;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class CostTime implements Delayed {
private final long costTime;
private final long expireTime;

public CostTime(long costTime, long delay) {
this.costTime = costTime;
this.expireTime = System.currentTimeMillis() + delay;
}

public long getCostTime() {
return costTime;
}

//重要:重写getDelay()方法,这是进行数据删除时的关键类,返回负值即为过期。
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}

@Override
public int compareTo(Delayed o) {
final CostTime c1 = (CostTime) o;
return (int) (this.costTime - c1.costTime);
}
}
  • 过期数据删除时就是调用getDelay()方法,通过返回的long类型值判断数据是否过期。

调用失败计数数据类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package jia.zheng.monitor;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class ErrorCount implements Delayed {
private final long expireTime;

public ErrorCount(long delay) {
this.expireTime = System.currentTimeMillis() + delay;
}

@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}

@Override
public int compareTo(Delayed o) {
return 0;
}
}
  • 直接通过ErrorCount对象的个数来进行调用失败计数,所以该类中不需要额外属性,只需要expireTime用来计算过期时间即可。

2.3实验类

模拟被调用方法类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package jia.zheng.service;

import org.springframework.stereotype.Service;

@Service
public class ServiceMethod {
public int service() throws InterruptedException {
//使用随机数来进行模拟,小于0.1假设为调用失败。
double random = Math.random();
if (random < 0.1) {
return 0;
}else {//调用成功,则用该随机数模拟服务调用时间。
Thread.sleep(Math.round(random * 1000));
return 1;
}
}
}

模拟调用埋点类

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
67
68
69
70
71
72
73
74
75
76
77
package jia.zheng.controller;

import jia.zheng.monitor.Info;
import jia.zheng.monitor.Monitor;
import jia.zheng.service.ServiceMethod;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.*;

@RestController
public class ThreadPoolVisit {

@Autowired
ServiceMethod serviceMethod;

//重要:用线程池辅助模拟多线程并发调用
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 100, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(50));

@GetMapping("visit")
public String visit() throws InterruptedException {
long startTime = System.currentTimeMillis();
//循环调用,每次执行一个的调用任务。
while (true) {
//每次调用适当休息一会,防止线程池不够用,会拒绝服务。
Thread.sleep(5);
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
try {
Info info = Monitor.start("testService");
int state = serviceMethod.service();
if (state == 0) {
Monitor.error(info);
}
Monitor.end(info);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//执行两分钟
if (System.currentTimeMillis() - startTime > 2 * 60 * 1000) break;
}
return "执行完毕!";
}

//重要:用一个定时器线程池来定时在计算并输出服务性能和可用率,每5s计算并输出一次。
private static ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
static {
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
//非常重要:线程池中runner的run()报错日志不会输出到控制台上,一定要用try-catch块抓住并打印异常信息。
try {
int[] AP = Monitor.getAP("testService");
long[] TP = Monitor.getTP("testService");
int total = AP[0] + AP[1];
double app;
if (AP[0] != 0 && AP[1] == 0) {//全部失败
app = 0d;
}else if (AP[1] == 0) {//还没有调用过
app = 100d;
}else {
app = (double) AP[1] /total;
}

System.out.println("单位时间内总调用次数:" + total + "次------------可用率:" + app + "%------------性能TP90:" + TP[0] + "ms------------性能TP99:" + TP[1] +"ms");

}catch (Exception e) {
e.printStackTrace();
}
}
}, 10, 5, TimeUnit.SECONDS);//项目启动后延迟10s启动。
}
}
  • 此处用线程池辅助模拟多线程并发调用,这是非常巧妙的。
  • 此处用一个定时器线程池来定时在计算并输出服务性能和可用率,每5s计算并输出一次。如果要添加告警机制也就是在此逻辑上添加的,计算完之后将性能值和可用率值与设定阈值进行比较,超出设定范围则执行报警逻辑。
  • 线程池中任务的run()方法的报错日志不会输出到控制台上,一定要用try-catch块抓住异常并打印,才会将异常信息输出到控制台和日志文件中。

2.4运行结果

启动springboot项目,10s观察控制台发现性能和可用率计算任务已经开始执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.5.RELEASE)

2022-06-12 22:03:38.967 INFO 20628 --- [ main] jia.zheng.Application : Starting Application on LAPTOP-1D612HIP with PID 20628 (E:\files\IntelliJIDEA\TPMonitor\target\classes started by jiayue in E:\files\IntelliJIDEA\TPMonitor)
2022-06-12 22:03:38.970 INFO 20628 --- [ main] jia.zheng.Application : No active profile set, falling back to default profiles: default
2022-06-12 22:03:39.672 INFO 20628 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-06-12 22:03:39.686 INFO 20628 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-06-12 22:03:39.686 INFO 20628 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.19]
2022-06-12 22:03:39.755 INFO 20628 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-06-12 22:03:39.755 INFO 20628 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 756 ms
2022-06-12 22:03:39.873 INFO 20628 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2022-06-12 22:03:39.985 INFO 20628 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-06-12 22:03:39.987 INFO 20628 --- [ main] jia.zheng.Application : Started Application in 1.307 seconds (JVM running for 2.227)
单位时间内总调用次数:0次------------可用率:100.0%------------性能TP90:-1ms------------性能TP99:-1ms
单位时间内总调用次数:0次------------可用率:100.0%------------性能TP90:-1ms------------性能TP99:-1ms
单位时间内总调用次数:0次------------可用率:100.0%------------性能TP90:-1ms------------性能TP99:-1ms
单位时间内总调用次数:0次------------可用率:100.0%------------性能TP90:-1ms------------性能TP99:-1ms

浏览器访问,调用visit()方法,控制开可以看到监控数据中第1分钟内调用次数逐渐增加

1
2
3
4
5
6
7
2022-06-12 22:05:21.468  INFO 20628 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-06-12 22:05:21.469 INFO 20628 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-06-12 22:05:21.473 INFO 20628 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
单位时间内总调用次数:493次------------可用率:0.9148073022312373%------------性能TP90:899ms------------性能TP99:990ms
单位时间内总调用次数:1374次------------可用率:0.9017467248908297%------------性能TP90:902ms------------性能TP99:990ms
单位时间内总调用次数:2249次------------可用率:0.9066251667407736%------------性能TP90:907ms------------性能TP99:990ms
单位时间内总调用次数:3137次------------可用率:0.9034109021357986%------------性能TP90:910ms------------性能TP99:989ms

因为监控单元为1分钟,可以看到第2分钟内调用次数基本稳定

1
2
3
4
单位时间内总调用次数:11440次------------可用率:0.9009615384615385%------------性能TP90:913ms------------性能TP99:992ms
单位时间内总调用次数:11452次------------可用率:0.9000174641983933%------------性能TP90:913ms------------性能TP99:992ms
单位时间内总调用次数:11445次------------可用率:0.9010048055919616%------------性能TP90:910ms------------性能TP99:992ms
单位时间内总调用次数:11453次------------可用率:0.9002008207456561%------------性能TP90:909ms------------性能TP99:992ms

因为我们模拟的调用多线程调用的总时间为2分钟,所以2分钟后没有调用了,可以看到第3分钟内调用次数逐渐下降

1
2
3
4
单位时间内总调用次数:7438次------------可用率:0.9005108900242%------------性能TP90:911ms------------性能TP99:991ms
单位时间内总调用次数:6513次------------可用率:0.8988177491171503%------------性能TP90:915ms------------性能TP99:991ms
单位时间内总调用次数:5630次------------可用率:0.9007104795737123%------------性能TP90:915ms------------性能TP99:992ms
单位时间内总调用次数:4802次------------可用率:0.9014993752603082%------------性能

第四分钟后就没有调用了

1
2
3
4
单位时间内总调用次数:0次------------可用率:100.0%------------性能TP90:-1ms------------性能TP99:-1ms
单位时间内总调用次数:0次------------可用率:100.0%------------性能TP90:-1ms------------性能TP99:-1ms
单位时间内总调用次数:0次------------可用率:100.0%------------性能TP90:-1ms------------性能TP99:-1ms
单位时间内总调用次数:0次------------可用率:100.0%------------性能TP90:-1ms------------性能TP99:-1ms

此时再次浏览其访问或者前一次访问还没完成就再一次访问都可以看到预期中的并发效果,其中可用率和性能计算都符合预期。