Rails在线程中执行业务代码无日志挂起问题排查求助
核心可能成因
1. Rails开发环境常量自动加载死锁
Rails 5.2默认使用的常量自动加载机制不是线程安全的。当子线程中首次引用未预加载的常量(你场景里的AggregationReport、Report)时,自动加载逻辑会申请全局类加载锁,如果此时主线程也持有该锁,就会触发死锁,表现为程序永久卡住。单线程环境下不存在锁竞争,所以运行正常。
2. 类初始化逻辑的锁竞争
如果AggregationReport或其父类Report的initialize方法、类回调(如self.inherited、ActiveRecord的after_initialize等)中存在不可重入的互斥锁、全局资源抢占逻辑(Redis锁、文件锁、独占式全局变量修改等),多线程场景下会出现多线程抢锁失败永久等待的问题。
3. 数据库连接池等待
如果AggregationReport是ActiveRecord子类,且初始化逻辑中存在数据库操作,Rails默认的数据库连接池(默认大小为5)如果被占满,子线程会一直阻塞等待空闲连接,表现为卡住无输出。
4. 子线程异常静默
Ruby默认Thread.abort_on_exception为false,子线程中抛出的未捕获异常不会向上抛到主线程,只会导致子线程挂死,join操作会永久等待。
验证&解决步骤
- 优先验证自动加载问题
在创建线程之前的主线程中提前预加载所有用到的常量,确认问题是否复现:
# 提前预加载常量,避免自动加载锁竞争 AggregationReport Report SomeService # 原有多线程逻辑 threads = [] threads << Thread.new { data_array.each do |data| service = SomeService.new(data) data['answers'] = service.answer_data end } threads.each(&:join)
如果是开发环境出现该问题、生产环境正常,基本可以确定是自动加载导致的,生产环境开启eager loading后不会触发该问题。
2. 优化数据库连接使用
用Rails连接池方法包裹子线程逻辑,避免连接耗尽等待:
threads << Thread.new { ActiveRecord::Base.connection_pool.with_connection do data_array.each do |data| service = SomeService.new(data) data['answers'] = service.answer_data end end }
也可以适当调大database.yml中的pool配置值,适配多线程的连接需求。
3. 开启异常抛出&死锁排查
在启动线程前增加配置,让子线程异常可以抛出到主线程:
Thread.abort_on_exception = true
如果程序依然卡住,可以给进程发送SIGQUIT信号(执行kill -QUIT 进程ID),Ruby会打印所有线程的调用栈,直接定位到阻塞的具体代码行。
额外优化建议
你当前的多线程实现只是把整个遍历逻辑放到了单个子线程运行,没有实现并行处理,如果要利用多线程提速,可以按单条数据/数据分批的方式创建多个线程处理。
内容的提问来源于stack exchange,提问作者Yu Yan

