高并发定时任务优化与连接池配置

小豆丁 17小时前 ⋅ 4 阅读
聊天记录
头像

@庞子运 智工匹配推送这个定时任务,并发太高了,要降低点:现在每批查出100个订单-->parallelStream遍历-->每个订单使用虚拟线程推送单个师傅-->openfeign调用messagepush服务。openfeign现在maxPerRoute连接数只有100(准备改成200),请求量太大了短时间消费速度远低于生产速度,导致有17398个请求在等待获取连接。告警的异常就是等了5秒还拿不到连接所以异常了。你安排优化下这个定时任务吧 ,maxPerRoute是啥

头像

你好,我是元宝。这个问题很典型,是典型的下游连接池被上游并发洪峰冲垮的场景。我来帮你理清概念并给出一套组合优化方案。

1. maxPerRoute 是什么?

在 OpenFeign(底层通常是 Apache HttpClient 或 OkHttp)中,maxPerRoute 指的是 HTTP 连接池中,针对同一个目标服务地址(Route,比如 http://messagepush-service:8080)允许存在的最大并发连接数

  • 你现在的情况maxPerRoute = 100
  • 发生了什么:你的定时任务每批 100 个订单,使用 parallelStream(默认使用的是 ForkJoinPool.commonPool(),并发度通常等于 CPU 核数,但在虚拟线程加持下,这 100 个任务会极快地尝试发送 HTTP 请求)。这 100 个请求瞬间打向 messagepush 服务,连接池最多只能提供 100 个连接。
  • 结果:第 101 个及以后的请求必须等待,直到有连接被释放。你提到等待队列积压了 17398 个请求,等待超过 5 秒就触发了超时异常。

简单比喻maxPerRoute 是通往 messagepush 服务的高速公路的车道数(100条)。你的定时任务是突然放行 100 辆车,虽然路宽了,但后面的车(下一批任务或其他请求)还在源源不断涌来,导致收费站(连接池)外堵成长龙。


2. 优化方案(建议组合使用)

单纯把 maxPerRoute 从 100 调到 200 只是增加了车道,治标不治本,因为你的生产速度远快于消费速度,调大后很快又会被填满。必须从限流(控制生产速度)削峰填谷入手。

方案一:使用 Semaphore(信号量)限制并发请求数(最推荐)

这是最直接的解决方式。既然连接池只有 100(或 200),那我就保证同时只发出去这么多请求。

JAVA
// 定义一个全局的信号量,permits 设置为比 maxPerRoute 稍小的值,留点余量
// 例如 connectionMaxPerRoute 是 200,这里设 180
private final Semaphore pushSemaphore = new Semaphore(180);

public void processBatch(List<Order> orders) {
    orders.forEach(order -> {
        // 异步执行,但受信号量控制
        CompletableFuture.runAsync(() -> {
            try {
                // 获取许可,如果获取不到则阻塞在这里,不会疯狂创建连接
                pushSemaphore.acquire();
                try {
                    // 你的虚拟线程推送逻辑
                    pushSingleMaster(order);
                } finally {
                    // 务必在 finally 中释放,防止死锁
                    pushSemaphore.release();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, virtualThreadExecutor); // 这里传入你的虚拟线程执行器
    });
}

优点:强制限制了正在飞行的请求数量,完美保护下游连接池。

方案二:调整批量大小与速率(简单有效)

既然 100 个一批太快,那就拆小一点,或者慢一点。

  1. 减小批大小:将 100 改为 2030
  2. 增加批次间隔:在 @Scheduled 注解中,或者在任务循环中加入 Thread.sleep(),拉长拉取间隔。
  3. 使用 RateLimiter:引入 Guava 的 RateLimiter,平滑地控制每秒发出的请求数(QPS)。

方案三:优化 Parallel Stream 的使用

parallelStream 对于 IO 密集型任务(如 HTTP 调用)其实不好控制。建议改为显式的线程池管理。

JAVA
// 使用虚拟线程工厂创建 ExecutorService
ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();

// 遍历订单,提交任务
for (Order order : orders) {
    virtualThreadExecutor.submit(() -> {
        // 结合方案一的 Semaphore 使用
        pushSingleMaster(order);
    });
}
// 注意:如果是定时任务,不需要 shutdown 这个 executor

注意:虚拟线程本身非常轻量,但它们内部的阻塞操作(如等待 HTTP 连接)依然会占用底层资源(连接池)。所以限制并发的核心还是在于限制外部请求的数量,而不是限制虚拟线程的数量。

方案四:OpenFeign 连接池参数精细化调整

除了 maxPerRoute,还有几个关键参数需要调整:

YAML
feign:
  httpclient:
    # 启用 httpclient(如果还没启用的话)
    enabled: true
    # 连接池最大连接数(总连接数,>= maxPerRoute)
    max-connections: 400
    # 每个路由的最大连接数(你关心的参数)
    max-connections-per-route: 200
    # 连接存活时间
    time-to-live: 60
    # 连接超时
    connection-timeout: 2000
    # 等待可用连接的超时时间(非常重要!)
    # 建议适当调小,比如 1-2秒,快速失败,避免长时间阻塞线程
    connection-acquire-timeout: 2000

方案五:消息队列削峰(架构级优化)

如果数据量持续增大,最优雅的方式是将“查订单”和“推送师傅”解耦。

  1. 定时任务只负责扫描订单,将订单 ID 发送到 Kafka/RocketMQ
  2. 启动一个消费者组,设置合理的 concurrency(并发数),慢慢消费队列中的消息去调用 messagepush。 这样,数据库压力、连接池压力都会变得非常平滑。

3. 给你的行动建议清单

  1. 立即止血
    • maxPerRoute 调整为 200
    • 在 OpenFeign 配置中,将 connection-acquire-timeout 调小(如 2秒),让线程别等太久,快速失败,避免大量线程挂起。
  2. 核心修复(代码改动):
    • 引入 Semaphore,限制并发请求数为 180 左右。
    • 将批处理大小从 100 降至 3050
  3. 中长期优化
    • 考虑引入消息队列。
    • 监控 messagepush 服务的实际处理能力(TPS),反向推导你这边合适的并发数和批大小。

总结一句话:不要指望无限创建虚拟线程就能提高吞吐量,系统的瓶颈在下游的 messagepush 服务和连接池。用 Semaphore 做“闸门”,控制住流入下游的流量,才是解决问题的正道。

需要我帮你写具体的代码改造示例吗?

全部评论: 0

    我有话说: