DynamicTP动态线程池(四)

一、任务增强

DynamicTP中,给提交的任务,Runnable,也做了一层封装(增强)。

其中有四种增强方式:MdcTaskWrapper、TtlTaskWrapper、SwTraceTaskWrapper、OpenTelemetryWrapper

这四种基本都是用于线程的参数传递的。

这里咱们主要聊一下MdcTaskWrapper的套路,以及他的实现原理。

1.1 MDC介绍

首先,咱们要清楚什么是MDC。本质MDC(Mapped Diagnostic Context)是日志框架提供的功能,作用其实就是在线程上下文进行数据的传递,本质就是ThreadLocal,是基于ThreadLocal实现的。

SpringBoot本质也引入了Slf4j以及LogBack的依赖,可以直接在项目的任何位置,直接基于MDC去操作

MDC.put(key,value);

// 其他位置

String value = MDC.get(key);

简单的看一下MDC是如何基于ThreadLocal去实现的。

1、首先查看MDC类,内部提供了一个 MDCAdapter 的属性,并且在静态代码块中,给这个属性赋值,并且构建的是 MDCAdapter 接口下的Logback的实现

2、查看 LogbackMDCAdapter 类,发现内部提供了一个

inal ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();

在基于MDC去做存取操作时,本质上其实就是对 这个 ThreadLocal 里的Map集合做读写操作。

2.2 MdcTaskWrapper应用

前面说完MDC之后,了解了,他是基于ThreadLocal在一个线程中传递数据。

但是现在是线程池的功能,需要当前线程的MDC存储的数据,可以在提交任务后,由执行的子线程也可以获取到提交任务线程的MDC数据。

先应用看效果:

java 复制代码
/**
 * 测试MDC任务增强
 * @throws IOException
 */
@Test
public void taskWrapperTest() throws IOException {
    // 这个是当前测试线程存储的数据
    MDC.put("traceId","sakjdfhasjkfhsdjkfhsdf");
    MDC.put("xxx","yyy");
    // 提交任务到线程池
    dtpExecutor.execute(new MdcTaskWrapper().wrap(() -> {
        System.out.println("子线程执行任务");
        System.out.println(Thread.currentThread().getName() + ":" + MDC.get("traceId"));
        System.out.println(Thread.currentThread().getName() + ":" + MDC.get("xxx"));
    }));
    System.in.read();
}

发现可以在子线程中获取到父线程里提供的MDC中的数据。

上图中的写法有点麻烦,其实DynamicTP可以在配置指定好任务增强的方式后,提交普通任务即可,在任务基于execute方法投递到线程池时,他会自动将任务做增强

dynamictp:

enabled: true # 是否启用 dynamictp,默认true

线程池配置

executors: # 动态线程池配置,

  • threadPoolName: dtpExecutor # 线程池名称,必填

taskWrapperNames: ["mdc"] # 任务包装器名称,继承TaskWrapper接口,可以追加ttl等

任务投递的方式,直接原生方式即可

java 复制代码
/**
 * 测试MDC任务增强
 * @throws IOException
 */
@Test
public void taskWrapperTest2() throws IOException {
    // 这个是当前测试线程存储的数据
    MDC.put("traceId","sakjdfhasjkfhsdjkfhsdf");
    MDC.put("xxx","yyy");
    // 提交任务到线程池
    dtpExecutor.execute(() -> {
        System.out.println("子线程执行任务");
        System.out.println(Thread.currentThread().getName() + ":" + MDC.get("traceId"));
        System.out.println(Thread.currentThread().getName() + ":" + MDC.get("xxx"));
    });
    System.in.read();
}

1.3 分析MdcWapper的增强以及实现原理

1、查看任务增强过程

在dtpExecutor提交任务时,第一步就在执行

command = getEnhancedTask(command);

获取增强后的任务。

在内部会根据咱们的配置方式,获取到对应的TaskWrapper,比如咱们现在指定了mdc,就可以获取到MdcTaskWrapper对象。

最后直接基于 MdcTaskWrapper对象.wrap(任务) 做增强返回。

2、查看任务内部增强后的实现

本质其实就是将普通的Runnable,封装成了一个MdcRunnable的类型。

第一步就是执行了他提供的 有参构造 ,将Runnable进行封装。

将任务进行保存。

将父线程中的MDC里的ThreadLocal中的Map集合进行保存。(就是父线程的MDC数据)

将父线程进行保存。

第二步就是执行MdcRunnable中的run方法

做了一个健壮性校验,如果不是基于线程池的子线程执行任务,不走MDC传递。

将之前保存的父线程的MDC数据,写入到了子线程的MDC中。在后面执行任务后,就可以基于子线程的MDC拿到对应的父线程中的数据了。

在任务执行完毕之后,会将子线程MDC中的数据清除掉,除了traceId。

3、traceId为何保留以及何时清除

DynamicTP中提供了任务超时的一些报警机制,同时还要记录这种错误信息。

在记录信息或者报警的时候,可能需要用到traceId。

如果在任务执行完毕后,直接将traceId清楚,后续的日志记录无法找到traceId。

DynamicTP重写了线程池的 afterExecute 方法,在这个方法里需要记录一些日志,采集一些数据,这些过程涉及到了traceId,同时在 afterExecute 方法的最后,也会清除掉MDC里的traceId

Ps:ThreadPoolExecutor 线程池在执行任务时,提供了任务前后的扩展方法

Ps:类似TTL等增强,先不聊。比如提供的TtlTaskWrapper,用的阿里提供的TransmittableThreadLocal,后面单独的分析一下TransmittableThreadLocal也就知道怎么回事了。

二、报警通知

功能的目的就是在线程池出现了

参数调整

活跃线程数阈值

队列任务阈值

任务超时

排队超时

拒绝策略

上述6种情况时,都可以给咱们做一个通知。这里咱们直接基于DingDing的方式实现一下通知效果。

很多配置即便没写,他也有默认值。

2.1 设置平台报警

平台报警的配置模板,官方有提供,直接照葫芦画瓢~~~

只需要额外的在DingDing或者其他的通知平台上获取到对应的 ​access_token​ 即可。

dynamictp:

告警渠道

platforms: # 通知报警平台配置

https://oapi.dingtalk.com/robot/send?access_token=6d2a71da0266a8e4aac6e26c87fbec4604a49f2f768455c19e18afa2061315a9

  • platform: ding

platformId: 1 # 平台id,自定义,随便写数值

urlKey: 6d2a71da0266a8e4aac6e26c87fbec4604a49f2f768455c19e18afa2061315a9 # webhook 中的 access_token

secret: # 安全设置在验签模式下才的秘钥,非验签模式没有此值

receivers: 18888888888 # 钉钉账号手机号,这里给通知后,会@手机号的用户,主负责人。

配置完毕后,算是有DingDing报警机器人了。

2.2 指定报警触发方式

基于官方提供的配置方式,保留两种报警触发的方式

线程池参数改变

线程池队列任务达到阈值

dynamictp:

全局配置

globalExecutorProps:

notifyItems:

  • type: change # 线程池核心参数变更通知

silencePeriod: 1 # 通知静默时间(单位:s),默认值1,0表示不静默

  • type: capacity # 队列容量使用率,报警项类型,查看源码 NotifyTypeEnum枚举类

threshold: 80 # 报警阈值,意思是队列使用率达到70%告警;默认值=70

count: 2 # 在一个统计周期内,如果触发阈值的数量达到 count,则触发报警;默认值=1

period: 30 # 报警统计周期(单位:s),默认值=120

silencePeriod: 0 # 报警静默时间(单位:s),0表示不静默,默认值=120

上面是全局的配置,如果需要单独的针对某一个线程池指定也是可以的。

dynamictp:

线程池配置

executors: # 动态线程池配置,都有默认值,采用默认值的可以不配置该项,减少配置量

  • threadPoolName: dtpExecutor # 线程池名称,必填

单独针对当前线程池指定报警触发方式

notifyItems:

  • type: change # 线程池核心参数变更通知

silencePeriod: 1 # 通知静默时间(单位:s),默认值1,0表示不静默

  • type: capacity # 队列容量使用率,报警项类型,查看源码 NotifyTypeEnum枚举类

threshold: 80 # 报警阈值,意思是队列使用率达到70%告警;默认值=70

count: 2 # 在一个统计周期内,如果触发阈值的数量达到 count,则触发报警;默认值=1

period: 30 # 报警统计周期(单位:s),默认值=120

silencePeriod: 0 # 报警静默时间(单位:s),0表示不静默,默认值=120

2.3 将报警方式设置到具体的线程池

其实就是将配置好的DingDing里指定的标识 ,1 ,设置到具体的线程池,或者指定到全局配置即可

dynamictp:

线程池配置

executors: # 动态线程池配置,都有默认值,采用默认值的可以不配置该项,减少配置量

  • threadPoolName: dtpExecutor # 线程池名称,必填

platformIds: [1] # 这个 1 ,就是前面配置的DingDing

上述是指针某个线程池的配置,也可以设置全局配置。

问题:单个服务的通知次数,咱们可以通过配置文件中提供的静默时间去限制,但是如果后期服务是集群的方式部署,并且节点还不少,通知次数就无法通过单个服务的配置项来解决频繁通知的问题了。

2.4 频繁报警通知问题

前面说道了,单个服务基于静默时间配置即可,但是集群,无法处理。

So,在1.0.8 版本开始支持集群限流,基于 Redis 实现的滑动窗口限流,会限制实际进行推送的节点个数,使用引入以下依赖。

<dependency>

<groupId>org.dromara.dynamictp</groupId>

<artifactId>dynamic-tp-spring-boot-starter-extension-limiter-redis</artifactId>

<version>1.2.2</version>

</dependency>

只需要再配置文件中追加cluster-limit配置即可。(局部也可以设置)

dynamictp:

全局配置

globalExecutorProps:

notifyItems:

  • type: change # 线程池核心参数变更通知

silencePeriod: 120 # 通知静默时间(单位:s),默认值1,0表示不静默

clusterLimit: 1 # 集群限流

  • type: capacity # 队列容量使用率,报警项类型,查看源码 NotifyTypeEnum枚举类

threshold: 80 # 报警阈值,意思是队列使用率达到80%告警;默认值=80

count: 2 # 在一个统计周期内,如果触发阈值的数量达到 count,则触发报警;默认值=1

period: 30 # 报警统计周期(单位:s),默认值=120

silencePeriod: 120 # 报警静默时间(单位:s),0表示不静默,默认值=120

clusterLimit: 1 # 集群限流

记得启动Redis服务,查看Redis的数据的变化。

2.5 滑动时间创建时间原理

利用是的ZSet的结构,ZSet可以让score存储时间戳,并且ZSet还提供了zrangebyscore的查询方式。利用ZSet实现滑动时间创建的方式特别多。

直接通过源码的方式,查看到一个Lua脚本一步一步分析。

1、查看他执行脚本前需要赋值的5个属性

2、判断key是否存在,获取key对应的元素个数

key的个数,声明为0

local accepted = 0

查看key是否存在

local exists_key = redis.call('exists', key)

如果key存在

if (exists_key == 1) then

或者这个zset结构中member的个数

accepted = redis.call('zcard', key)

end

3、判断窗口内元素个数是否超过阈值

利用前面获取到的元素个数,比较limit限制的元素个数

if (accepted < limit) then

没有达到窗口的阈值,直接zadd,写入当前元素

redis.call('zadd', key, timestamp, member)

end

最后查看一下执行Lua脚本的代码

java 复制代码
public List<Object> isAllowed(String key, long windowSize, int limit) {
    // 获取Lua脚本文件
    RedisScript<?> script = this.getScript();
    // 生成ZSet的key,服务名:前缀:key          key=具体线程池别名 + 报警触发方式
    List<String> keys = this.getKeys(key);
    // 获取另外四个参数。  时间窗口长度、限制个数、当前系统时间、写入的member
    String[] values = this.getArgs(key, windowSize, limit);
    // 执行Lua脚本
    return Collections.unmodifiableList((List) Objects.requireNonNull(stringRedisTemplate.execute(script, keys,
            values)));
}
执行之后,利用返回结果判断,是否能够通知

@Override
public boolean tryPass(String name, long interval, int limit) {
    try {
        // 调用前面的执行脚本的操作
        val res = isAllowed(name, interval, limit);
        // 判断结果是否为空,为空,那就发。
        if (CollectionUtils.isEmpty(res)) {
            return true;
        }
        // 基于返回结果的remain,如过remain为null或者小于等于0,不能发送通知
        if (Objects.isNull(res.get(LUA_RES_REMAIN_INDEX)) || (long) res.get(LUA_RES_REMAIN_INDEX) <= 0) {
            if (log.isDebugEnabled()) {
                log.debug("DynamicTp notify, trigger redis rate limit, limitKey:{}, res:{}", name, res);
            }
            return false;
        }
        // remain有剩余发送次数,返回true
        return true;
    } catch (Exception e) {
        log.error("DynamicTp notify, redis rate limit check failed, limitKey:{}", name, e);
        return true;
    }
}

这个实现的方式,可以套用到短信平台中,但是当前Lua脚本的实现,有一些缺点,时间窗口不严谨,存在明明可以发送,但是因为他的zremrangbyscore的太晚,导致无法发送,So,脚本可以优化。

But,当前只是通知报警,所以不严谨,对于当前的DynamicTP无伤大雅。。。。。。

套到短信平台的话,一定要优化一下,比如zremrangbyscore提前,获取元素个数滞后。

相关推荐
野犬寒鸦2 小时前
从零起步学习并发编程 || 第九章:Future 类详解及CompletableFuture 类在项目实战中的应用
java·开发语言·jvm·数据库·后端·学习
uzong2 小时前
软件工程师应该尽量改掉的坏习惯
后端
前路不黑暗@2 小时前
Java项目:Java脚手架项目的统一模块的封装(四)
java·开发语言·spring boot·笔记·学习·spring cloud·maven
喵呜嘻嘻嘻2 小时前
Gurobi求解器参数
java·数据结构·算法
消失的旧时光-19432 小时前
第二十四课:从 Java 后端到系统架构——后端能力体系的最终总结
java·开发语言·系统架构
卓怡学长3 小时前
m225在线房屋租赁和电子签约系统的设计与实现
java·数据库·spring·tomcat·maven·intellij-idea
高山上有一只小老虎3 小时前
SpringBoot项目单元测试
spring boot·后端·单元测试
Sylvia33.3 小时前
火星数据:棒球数据API
java·前端·人工智能