@庞子运 智工匹配推送这个定时任务,并发太高了,要降低点:现在每批查出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),那我就保证同时只发出去这么多请求。
// 定义一个全局的信号量,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 个一批太快,那就拆小一点,或者慢一点。
- 减小批大小:将
100改为20或30。 - 增加批次间隔:在
@Scheduled注解中,或者在任务循环中加入Thread.sleep(),拉长拉取间隔。 - 使用 RateLimiter:引入 Guava 的
RateLimiter,平滑地控制每秒发出的请求数(QPS)。
方案三:优化 Parallel Stream 的使用
parallelStream 对于 IO 密集型任务(如 HTTP 调用)其实不好控制。建议改为显式的线程池管理。
// 使用虚拟线程工厂创建 ExecutorService
ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
// 遍历订单,提交任务
for (Order order : orders) {
virtualThreadExecutor.submit(() -> {
// 结合方案一的 Semaphore 使用
pushSingleMaster(order);
});
}
// 注意:如果是定时任务,不需要 shutdown 这个 executor
注意:虚拟线程本身非常轻量,但它们内部的阻塞操作(如等待 HTTP 连接)依然会占用底层资源(连接池)。所以限制并发的核心还是在于限制外部请求的数量,而不是限制虚拟线程的数量。
方案四:OpenFeign 连接池参数精细化调整
除了 maxPerRoute,还有几个关键参数需要调整:
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
方案五:消息队列削峰(架构级优化)
如果数据量持续增大,最优雅的方式是将“查订单”和“推送师傅”解耦。
- 定时任务只负责扫描订单,将订单 ID 发送到 Kafka/RocketMQ。
- 启动一个消费者组,设置合理的
concurrency(并发数),慢慢消费队列中的消息去调用messagepush。 这样,数据库压力、连接池压力都会变得非常平滑。
3. 给你的行动建议清单
- 立即止血:
- 将
maxPerRoute调整为200。 - 在 OpenFeign 配置中,将
connection-acquire-timeout调小(如 2秒),让线程别等太久,快速失败,避免大量线程挂起。
- 将
- 核心修复(代码改动):
- 引入 Semaphore,限制并发请求数为
180左右。 - 将批处理大小从
100降至30或50。
- 引入 Semaphore,限制并发请求数为
- 中长期优化:
- 考虑引入消息队列。
- 监控
messagepush服务的实际处理能力(TPS),反向推导你这边合适的并发数和批大小。
总结一句话:不要指望无限创建虚拟线程就能提高吞吐量,系统的瓶颈在下游的 messagepush 服务和连接池。用 Semaphore 做“闸门”,控制住流入下游的流量,才是解决问题的正道。
需要我帮你写具体的代码改造示例吗?