通知配置变更通知属性
场景配置添加调用链超时时间设置
更新README
This commit is contained in:
byteblogs168 2023-01-14 20:48:04 +08:00
parent 69ef41cbaf
commit 347bc9a616
16 changed files with 195 additions and 18 deletions

143
README.md
View File

@ -25,7 +25,7 @@ https://www.byteblogs.com/chat
<dependency>
<groupId>com.x.retry</groupId>
<artifactId>x-retry-client-starter</artifactId>
<version>0.0.2.0</version>
<version>0.0.4.0</version>
</dependency>
```
@ -291,3 +291,144 @@ http://localhost:8080
- 管理员: 管理所有的 组谦虚
- 权限: 需要管理的组
![user_add.png](doc/images/user_add.png)
### 系统剖析
#### 客户端与服务端数据交互图
![client_server_data_flow.jpg](doc/images/client_server_data_flow.jpg)
> 客户端核心能力
- 负责发现异常,标记事故现场
- 根据不同阶段进行本地重试和远程重试
- 失败上报和执行服务端下发的重试指令
- 避免服务间调用产生重试放大风险
- 重试流量管控
- 单机多注解循环引用问题
- 标记重试流量
- 调用链超时控制(Deadline Request)
- 特殊的 status code 限制链路重试
> 服务端核心能力
- 收集上报信息,统一预警
- 通过组协调器为不同的POD分配需要调度的Group信息
- 管理死信队列和重试数据状态以及触发时间
- 支持配置中心可视化
#### 系统架构图
![系统架构图-v1.0.jpg](doc/images/系统架构图-v1.0.jpg)
### 客户端剖析
#### 重试流量管控
> 单机重试管控
单机多注解嵌套方法,通过标记重试现场入口,发生异常重试只重试现场入口,防止每个方法都重试, 从而避免了重试风暴
> 链路重试管控
- 特殊的status code限制链路重试: 让被调用方有反抗的权利(统一约定一个特殊的 status code
它表示:调用失败,但别重试)
- 调用链超时控制(Deadline Request): 当剩余时间不够时不再发起重试请求
- 特殊的retry flag 保障重试请求不重试: 通请求头传递retry flag 保障即使发生异常也不重试
> 重试流速管控
通过路由策略和限流措施对每个组的集群进行流量控制
#### 支持多种退避策略
- 线性退避: 每次等待固定时间后重试
- 随机退避: 在一定范围内随机等待一个时间后重试
- 延迟等级退避: 依据延迟等级, 等待每个延迟等级设置的时间, 延迟等级枚举 `DelayLevelEnum`
- Cron表达式退避: 使用Cron表达式计算重试触发时间
#### 客户端功能模块图
![客户端功能模块-v1.0.jpg](doc/images/客户端功能模块-v1.0.jpg)
- 启动模块
- 滑动窗口模块: 监听需要上报的数据
- Netty启动器: 启动客户端Netty组件建立与服务端心跳机制
- 远程配置获取器: 获取最新版本的配置信息
- 注解扫描模块: 负责扫描添加到方法上的@Retryable注解获取参数信息、类信息、方法路径等解析注解元数据信息构建执行器
- 重试阶段
- 重试模式
- 本地重试: 当发生异常时候, 若注解上的配置`RetryType.ONLY_LOCAL`或者`RetryType.LOCAL_REMOTE`, 则会触发本地内存重试
- 远程重试: 本地重试没有成功,若注解上的配置`RetryType.ONLY_REMOTE`或者`RetryType.LOCAL_REMOTE`, 则会触发上报服务端重试
- 执行器
- 重试组件: 对guava retry 的深度封装
- 重试执行器
- 类反射执行器: 即重试执行原方法
- 自定义方法执行器: 用户通过实现`RetryMethod`接口, 即可实现自定义重试, 发生重试时直接重试自定义方法执行器
- 重试流量管控
- 单机多注解循环引用问题
> 标记重试入口,触发重试时只从标记的重试入口进入
![单机多注解循环引用问题.jpg](doc/images/单机多注解循环引用问题.jpg)
- 标记重试流量
> 对于重试的请求,我们在请求头中下发一个特殊的标识(xRetry:boolean),
在 Service A ->Service B ->Service C 的调用链路中当Service B 收到Service A 的请求时会先读取这个 xRetry 判断这个请求是不是重试请求,
如果是那它调用Service C 即使失败也不会重试;否则将触发重试 。
同时Service B 也会把这个 xRetry 下传,它发出的请求也会有这个标志,它的下游也不会再对这个请求重试
![重试流量标识.jpg](doc/images/重试流量标识.jpg)
- 调用链超时控制(Deadline Request)
> DDL 是“ Deadline Request 调用链超时”的简称,我们知道 TCP/IP 协议中的 TTL 用于判断数据包在网络中的时间是否太长而应被丢弃DDL 与之类似,
它是一种全链路式的调用超时,可以用来判断当前的 RPC 请求是否还需要继续下去。如下图,在 RPC 请求调用链中会带上超时时间,
并且每经过一层就减去该层处理的时间,如果剩下的时间已经小于等于 0 ,则可以不需要再请求下游,直接返回失败即可。
![DDL.jpg](doc/images/DDL.jpg)
- 特殊的 status code 限制链路重试
> 如果每层都配置重试可能导致调用量指数级扩大,这样对底层服务来说压力是非常之大的, 通过对流量的标记
,用户可以判断是否是重试的流量来判断是否继续处理,我们使用 Google SRE 中提出的内部使用特殊错误码的方式来实现:
1 统一约定一个特殊的 status code ,它表示:调用失败,但别重试。
2 任何一级重试失败后,生成该 status code 并返回给上层。
3 上层收到该 status code 后停止对这个下游的重试,并将错误码再传给自己的上层。
> 这种方式理想情况下只有最下一层发生重试,它的上游收到错误码后都不会重试,但是这种策略依赖于业务方传递错误码,
对业务代码有一定入侵,而且通常业务方的代码差异很大, 调用 RPC 的方式和场景也各不相同,需要业务方配合进行大量改造,
很可能因为漏改等原因导致没有把从下游拿到的错误码传递给上游。
![重试特殊的statuscode.jpg](doc/images/重试特殊的statuscode.jpg)
### 服务端剖析
#### 分布式调度模块
- master thread
1. 扫描所有启用的组,通过客户端协调算法,分配当前节点需要重试的组
2. 生成Actor,并扫描当前分配组下面所有待重试的数据
- 重试分发组件
- ScanGroupActor
1. 通过配置的时间范围扫描待重试数据
2. 通过条件过滤器,过滤出满足条件的重试数据
- ExecUnitActor
1. 通过远程调用下发重试指令,
2. 标记重试流量
- 结果处理
1. FailureActor: 处理重试失败数据,累加重试次数
2. FinishActor: 处理重试成功数据并更新状态为重试成功
- 算法
- 客户端轮询算法
1. 一致性Hash算法
2. LRU算法
3. 随机算法
- 服务端Rebalance算法
1. 一致性hash 算法

BIN
doc/images/DDL.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -19,7 +19,7 @@ CREATE TABLE `notify_config`
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`group_name` varchar(64) NOT NULL COMMENT '组名称',
`notify_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '通知类型 1、钉钉 2、邮件 3、企业微信',
`notify_attribute` varchar(512) NOT NULL COMMENT '通知地址',
`notify_attribute` varchar(512) NOT NULL COMMENT '配置属性',
`notify_threshold` int(11) NOT NULL DEFAULT '0' COMMENT '通知阈值',
`notify_scene` tinyint(4) NOT NULL DEFAULT '0' COMMENT '通知场景',
`description` varchar(256) NOT NULL DEFAULT '' COMMENT '描述',
@ -101,6 +101,7 @@ CREATE TABLE `scene_config`
`max_retry_count` int(11) NOT NULL DEFAULT '5' COMMENT '最大重试次数',
`back_off` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1、默认等级 2、固定间隔时间 3、CRON 表达式',
`trigger_interval` varchar(16) NOT NULL DEFAULT '' COMMENT '间隔时长',
`deadline_request` bigint(20) unsigned NOT NULL DEFAULT '60000' COMMENT 'Deadline Request 调用链超时 单位毫秒',
`description` varchar(256) NOT NULL DEFAULT '' COMMENT '描述',
`create_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
@ -151,7 +152,8 @@ CREATE TABLE `system_user`
UNIQUE KEY `uk_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
INSERT INTO x_retry.system_user (username, password, role) VALUES ('admin', 'cdf4a007e2b02a0c49fc9b7ccfbb8a10c644f635e1765dcf2a7ab794ddc7edac', 2);
INSERT INTO x_retry.system_user (username, password, role)
VALUES ('admin', 'cdf4a007e2b02a0c49fc9b7ccfbb8a10c644f635e1765dcf2a7ab794ddc7edac', 2);
CREATE TABLE `system_user_permission`
(

View File

@ -75,6 +75,17 @@
@change="value => handleChange(value, record.key, 'maxRetryCount')"/>
<template v-else>{{ text }}</template>
</template>
<template slot="deadlineRequest" slot-scope="text, record">
<a-input-number
v-if="record.editable"
:min="100"
:max="60000"
style="width: 100%;"
:value="text"
placeholder="调用链超时时间(毫秒)"
@change="value => handleChange(value, record.key, 'deadlineRequest')"/>
<template v-else>{{ text }}(毫秒)</template>
</template>
<template slot="triggerInterval" slot-scope="text, record">
<a-input
v-if="record.editable"
@ -84,7 +95,7 @@
:disabled="data.find(item => item.key === record.key).backOff === '1'"
@change="e => handleChange(e.target.value, record.key, 'triggerInterval')"
/>
<template v-else>{{ text }}</template>
<template v-else>{{ text }}()</template>
</template>
<template slot="operation" slot-scope="text, record">
<template v-if="record.editable">
@ -138,7 +149,7 @@ export default {
title: '场景状态',
dataIndex: 'sceneStatus',
key: 'sceneStatus',
width: '12%',
width: '10%',
scopedSlots: { customRender: 'sceneStatus' }
},
{
@ -155,18 +166,25 @@ export default {
width: '12%',
scopedSlots: { customRender: 'maxRetryCount' }
},
{
title: '调用链超时时间',
dataIndex: 'deadlineRequest',
key: 'deadlineRequest',
width: '15%',
scopedSlots: { customRender: 'deadlineRequest' }
},
{
title: '间隔时间',
dataIndex: 'triggerInterval',
key: 'triggerInterval',
width: '12%',
width: '10%',
scopedSlots: { customRender: 'triggerInterval' }
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: '25%',
width: '15%',
scopedSlots: { customRender: 'description' }
},
{
@ -237,7 +255,7 @@ export default {
this.data = []
res.data.map(record => {
this.loading = false
const { id, sceneName, sceneStatus, maxRetryCount, backOff, triggerInterval, description } = record
const { id, sceneName, sceneStatus, maxRetryCount, backOff, triggerInterval, description, deadlineRequest } = record
this.data.push({
key: id,
sceneName: sceneName,
@ -246,6 +264,7 @@ export default {
backOff: backOff.toString(),
triggerInterval: triggerInterval,
description: description,
deadlineRequest: deadlineRequest,
editable: false,
isNew: false
})
@ -262,7 +281,7 @@ export default {
},
remove (delKey) {
const delData = this.data.find(item => item.key === delKey)
const { key, sceneName, sceneStatus, maxRetryCount, backOff, triggerInterval, description } = delData
const { key, sceneName, sceneStatus, maxRetryCount, backOff, triggerInterval, description, deadlineRequest } = delData
this.formData.push({
key: key,
sceneName: sceneName,
@ -270,6 +289,7 @@ export default {
maxRetryCount: maxRetryCount,
backOff: backOff,
triggerInterval: triggerInterval,
deadlineRequest: deadlineRequest,
description: description,
isDeleted: 1
})
@ -279,7 +299,7 @@ export default {
},
saveRow (record) {
this.memberLoading = true
const { key, sceneName, sceneStatus, maxRetryCount, backOff, triggerInterval, description } = record
const { key, sceneName, sceneStatus, maxRetryCount, backOff, triggerInterval, description, deadlineRequest } = record
if (!sceneName || !sceneStatus || !maxRetryCount || !backOff || (backOff === '1' ? false : !triggerInterval)) {
this.memberLoading = false
this.$message.error('请填写完整成员信息。')
@ -296,6 +316,7 @@ export default {
backOff: backOff,
triggerInterval: triggerInterval,
description: description,
deadlineRequest: deadlineRequest,
isDeleted: 0
})
}
@ -354,6 +375,7 @@ export default {
maxRetryCount: null,
backOff: '1',
triggerInterval: '',
deadlineRequest: '60000',
description: '',
editable: true,
isNew: true

View File

@ -24,5 +24,5 @@ public class XRetryHeaders {
/**
* 调用链超时时间 单位毫秒(ms)
*/
private long ddl = 60 * 10 * 1000;
private long ddl = 60000;
}

View File

@ -1,11 +1,10 @@
package com.x.retry.server.persistence.mybatis.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.x.retry.server.persistence.mybatis.po.SceneConfig;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@InterceptorIgnore(tenantLine = "false")
public interface SceneConfigMapper extends BaseMapper<SceneConfig> {
}
}

View File

@ -27,6 +27,8 @@ public class SceneConfig implements Serializable {
private String description;
private Long deadlineRequest;
private LocalDateTime createDt;
private LocalDateTime updateDt;
@ -34,4 +36,4 @@ public class SceneConfig implements Serializable {
private static final long serialVersionUID = 1L;
}
}

View File

@ -102,6 +102,14 @@ public class GroupConfigRequestVO {
*/
private String triggerInterval;
/**
* Deadline Request 调用链超时 单位毫秒
* 默认值为 60*10*1000
*/
@Max(message = "最大60000毫秒", value = 60000)
@Min(message = "最小100ms", value = 100)
private Long deadlineRequest;
/**
* 是否删除
*/

View File

@ -28,6 +28,8 @@ public class SceneConfigResponseVO {
private String description;
private Long deadlineRequest;
private LocalDateTime createDt;
private LocalDateTime updateDt;

View File

@ -9,11 +9,12 @@
<result column="max_retry_count" jdbcType="TINYINT" property="maxRetryCount" />
<result column="back_off" jdbcType="TINYINT" property="backOff" />
<result column="trigger_interval" jdbcType="TINYINT" property="triggerInterval" />
<result column="deadline_request" jdbcType="BIGINT" property="deadlineRequest" />
<result column="description" jdbcType="VARCHAR" property="description" />
<result column="create_dt" jdbcType="TIMESTAMP" property="createDt" />
<result column="update_dt" jdbcType="TIMESTAMP" property="updateDt" />
</resultMap>
<sql id="Base_Column_List">
id, scene_name, group_name, scene_status, max_retry_count, back_off, `trigger_interval`, description, create_dt, update_dt
id, scene_name, group_name, scene_status, max_retry_count, back_off, `trigger_interval`, deadline_request, description, create_dt, update_dt
</sql>
</mapper>
</mapper>