码农戏码

新生代农民工的自我修养

0%

LiteFlow线程池饥饿死锁

最近又收到同事反馈 flow 卡住跑不动了

上一次遇到这类反馈,排查下来是【多线程饥饿死锁】导致的

本着同类问题不能再出现的原则,这次需要彻底排查根因,避免重蹈覆辙

整个flow是基于LiteFlow框架实现,为了性能,每个Flow创建一个线程池,每个节点 NodeComponent 内部使用多线程执行

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
Flow {
ThreadPoolExecutor nodeExecutor = new ThreadPoolExecutor

}

NodeComponent {
List<Task> taskList = new List();

taskList.add(task);

List<Future<TaskResult>> futures = nodeExecutor.invokeAll(taskList);
}

第一次卡住

某个flow中,有个特殊的形态,Node 与 Node 之间会有逻辑依赖关系
如图,NodeD 会依赖 NodeC
当 NodeD 任务数量超过线程池大小时,并且所有线程被分配给 TaskD 时,被依赖的 TaskC 处于资源饥饿
造成整个 flow 卡住

这儿卡住的两个条件:
1、任务数量超过了线程数量
2、所有线程都被分配给了 TaskD

解决:

对于 NodeD ,使用独立线程池,不再使用flow全局线程池,线程池隔离
这样 NodeC 和 NodeD 的线程池竞争关系就没有了

第二次卡住

这次卡住,对照 flow 分析,底层道理是一样的,只是表现形式不一样

运行 flow 本身有一根主线程,当有 when 条件时,会使用内置的 when 线程池

如果不配置,默认 when 线程池 是 16个线程
并且所有 flow 的 when 条件都使用同一个 when 线程池

按上图,如果是一个线程池 =3 ,执行 flow 多次时,按图中分配,所有线程都在等待 TaskC 的完成

可当 TaskC 完成后,when 线程 都在其它地方,等待完成的任务 已经在线程线队列中

此时,只能等待 线程的 timeout , 而 默认 timeout = 3600s

解决

原因同理第一次卡住,只是第一次是因为自定义的线程池NodeExecutor,线程池任务里阻塞等待同池任务

而这一次,是 LiteFlow 底层的 when 线程池

解决的办法还是独立线程池,LiteFlow 提供了配置

在LiteFlow 的源码中,有一段注释:

// 如果设置了线程池隔离,则每个 when 都会有对应的线程池,这是为了避免多层嵌套时如果线程池数量不够时出现单个线程池死锁。用线程池隔离的方式会更加好
// 如果 when 没有超多层的嵌套,还是用默认的比较好。
// 默认设置不隔离。也就是说,默认情况是一个线程池类一个实例,如果什么都不配置,那也就是在 when 的情况下,全局一个线程池。

其实也应征了上述分析。

目前liteflow设计里when线程池,如果你不单独设置自定义线程池,那么就会用默认的线程池。而这个线程池,是所有的when共同一个。

LiteFlow从2.11.1开始,提供一个liteflow.when-thread-pool-isolate参数,默认为false,如果设为true,则会开启WHEN的线程池隔离机制,这意味着每一个when都会有单独的线程池。这个特性对于运行复杂的嵌套when时是可以提升运行速度的且规避掉一些锁的问题。

可以如下配置来开启:

1
liteflow.when-thread-pool-isolate=true

不过当前使用Liteflow 版本 还有线程池泄漏的bug,翻看源码:

在 并行 编排 时, 每个 WHEN 条件去获取 Executor 有bug。
当有 condition 时,这个bug就会被触发。

排查

怎么定位这种问题

1、在 grafana 监控面板看查看 thread 数量,是否在增长,一直增长那可能是线程池泄漏,有时会以 OOM 形式暴露

2、统计线程状态,查看大量线程处于BLOCKED/WAITING,确认线程耗尽

比如上面卡住的情况,通过线程状态,看到大量的 WAITING 线程