降级

当访问量剧增,服务出现问题(响应时间长或不响应),非核心服务影响到核心流程的性能时,将这些服务降级,以保证核心服务的可用性(弃卒保帅)

主要分为以下几种情况:

  • 页面降级,页面片段降级,页面异步请求降级:动态渲染页面边为静态默认页面
  • 服务功能降级:一些不重要的服务出现问题或影响性能,返回兜底数据
  • 读降级:从读数据库降级为只读缓存
  • 写降级:不对缓存更新,然后峰值过去后异步更新数据库,保证最终一致性(同步转异步)
  • 爬虫降级:对爬虫访问返回空数据或静态数据
  • 风控降级:拒绝高风险用户

何时需要降级:

  • 响应超时
  • 失败次数大于阈值
  • 出现故障
  • 限流降级:当访问量达到限流阈值时,后续的请求会被降级,处理方案可以是:排队,无货,错误页(活动太火爆,请稍后重试)
  • 人为关闭某项服务时,如灰度测试

如何降级

通过修改配置文件

例如:当检测到需要降级时,发送一个指令修改properties配置文件,借住JDK 7 WatchService 实现配置文件变更监听

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
public static void main(String[] args) throws Exception {
File file = new File("C:\\Users\\zjm\\IdeaProjects\\LimitTraffic\\src\\main\\java\\com\\heu\\zjm\\utils\\application.properties"); // 模拟配置文件
WatchService watchService = FileSystems.getDefault().newWatchService();
Paths.get(file.getParent()).register(watchService,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
// 启动一个线程监听内容变化
Thread watchThread = new Thread(()->{
try {
while (true) {
WatchKey watchKey = watchService.take();
for (WatchEvent<?> event : watchKey.pollEvents()) {
if (Objects.equal(event.context().toString(), file.getName())) {
System.out.println("文件发生了变化:" + event.kind() + ",重新载入配置文件");
break;
}
}
watchKey.reset();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
watchThread.setDaemon(true); // 守护线程
watchThread.start();
watchThread.join(); // 防止主线程结束将守护线程停止
Runtime.getRuntime().addShutdownHook(new Thread(()->{
try {
watchService.close();
} catch (IOException e) {
e.printStackTrace();
}
}));
}

注:用idea修改配置文件内容不会观察到modify现象的提示,用文本编辑器修改后保存可以观察到modify操作的提示

用户线程和守护线程:主线程结束,如果有用户线程,用户线程继续执行;主线程结束,没有用户线程,守护线程结束。默认创建的都是用户线程,守护线程需要在startsetDaemon(true)

​ 在分布式系统中,要同时修改很多台服务器上的配置文件,会很麻烦,可以使用分布式配置中心统一管理

通过配置中心(注册中心)

可选择的开源项目:ZooKeeperDiamondDisconfEtcd 3Consul

熔断

降级的开关:某个服务的熔断打开,不处理该服务的请求,直接做降级处理;熔断关闭,重新处理该服务的请求

个人理解:熔断是为了防止反复请求已经降级的服务

熔断的状态
  • 闭合:如果配置了熔断开关强制闭合,或者当前失败率低于阈值,不启动熔断,不进行降级处理
  • 打开:如果配置了熔断开关强制打开,或者当前失败率超过阈值,熔断开关打开,调用降级处理方法getFallback
  • 半打开:熔断处于打开状态,一段时间后重试请求,如果重试成功闭合熔断,否则熔断开关还处于打开状态

失败请求的统计

  • 计数统计:记录一定时间窗内(N秒)的失败、超时、线程池拒绝、信号量拒绝数,记录N组,写入数据时写到第N组,统计时计算前N-1组,因为第N组刚开始统计时是随时变化的,然后基于时间滚转统计分组
  • 最大并发数统计
  • 延时百分比统计

下面模拟一个长度为10的时间窗的计数统计,与上述实现有所不同的是:写入时写到当前时间所处位置nowIndex上,统计时计算nowIndex以外的位置数字之和,每秒将nowIndex循环后移一位

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
import java.util.Random;
public class BucketedCounter {
private int windowSize = 10;
private int[] windows;
private int nowIndex = 0;
public void start(int windowSize) {
this.windowSize = windowSize;
this.start();
}
public void start() {
this.windows = new int[windowSize];
// 创建一个线程控制时间窗的移动,每秒操作的数组下标向后推一个格
Thread t = new Thread(()->{
while (true) {
try {
Thread.sleep(1000L);
// 打印windowSize秒内接受到的请求数
System.out.println(statistic());
// 显示时间窗数组内容
showBucketed();
this.nowIndex += 1;
this.nowIndex %= this.windowSize;
this.windows[nowIndex] = 0;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
private int statistic() {
int total = 0;
for (int i=0; i<this.windowSize; i++) {
// 计算除了当前操作位置以外的数组之和
if (i == this.nowIndex) {
continue;
}
total += this.windows[i];
}
return total;
}
// 显示时间窗各个时间点接受的请求数,'[]'为当前操作的位置
private void showBucketed() {
for (int i=0; i<this.windowSize; i++) {
if (i == this.nowIndex) {
System.out.print('[');
}
System.out.print(this.windows[i]);
if (i == this.nowIndex) {
System.out.print(']');
}
System.out.print(' ');
}
System.out.println();
}
public void sendRequest() {
// 只对当前操作位置增加
this.windows[this.nowIndex] += 1;
}
public static void main(String[] args) throws InterruptedException {
BucketedCounter counter = new BucketedCounter();
// 启动计算器
counter.start();
Random random = new Random();
// 模拟发送请求
while (true) {
counter.sendRequest();
// 模拟请求间隔时间
Thread.sleep(10 + random.nextInt(20));
}
}
}
您的支持鼓励我继续创作!