现代 Rails 对反模式"thread + fork"的弹性如何?



我认为这是一个流行的反模式,发生独立,例如activeJob本地任务与异步,或来自控制器,因为服务器的策略必须考虑在内。

我的问题是,当在线程内分叉时,应该在代码中采取什么注意事项(考虑ActiveJob任务内部),然后甚至线程化它?

我在网上看到的主要担忧是:

  • 需要在fork后丢失并重新打开数据库连接。现在看来,activeRecord照顾它,不是吗?
  • 访问公共记录器可能很复杂。

Concurrent也被认为是有问题的,但是当前的版本已经打了补丁,可以检测到fork已经发生并且线程已经死亡。尽管如此,似乎仍然需要确保在分叉进程结束时,关闭任何可能有活动或挂起作业的Rails::并发池。我认为这就足够了

ActiveJob::Base.queue_adapter.shutdown

,但可能会错过一些尚未启动的任务或其他并发队列下的任务。事实上,如果在puma web服务器管理的控制器中使用Concurrent::Future,我认为它已经发生了。一般我尝试插入

Concurrent::global_io_executor.shutdown
Concurrent::global_io_executor.wait_for_termination

我发现的额外问题与资源有关:默认情况下,postgres服务器还没有准备好管理这么多连接。也许在分叉之前减小连接池的大小是明智的。并且在开发时,inotify watchcher gem也会耗尽资源。生产在这两种情况下都很好。

;-我反对这样做,但我们中的许多人还是这样做了,忽略了这是不安全的事实…这是一个简单的事实,在多线程进程中调用fork可能会导致新的子进程崩溃/死锁/旋转,还可能导致其他(更难隔离的)错误。

这与Ruby无关这与保护关键区和核心进程功能的锁定机制有关,例如打开/关闭文件,分配内存和任何用户创建的互斥锁/自旋锁等。

为什么有风险?

当调用fork时,新进程继承前一个进程的所有状态,但只继承调用fork的线程(所有其他线程不存在于新进程中)。

这意味着如果任何其他线程在临界区内(例如,分配内存,打开文件等),该临界区将在新进程的生命周期内保持锁定,可能导致死锁或意外错误。

为什么我们忽略它?

在实践中,某些东西严重损坏的风险通常非常低,大多数开发人员既没有遇到问题,也没有认识到其原因。打开的文件可以手动(如果不是自动的话)关闭,这就给我们留下了关键区的问题。

我们通常可以重置我们自己的临界区,这使得大多数系统的临界区…

fork影响的系统核心临界区并不多。最主要的是内存分配器,它几乎永远不会崩溃。通常malloc实现有多个"区域",每个"区域"都有自己的关键区域,要达到系统的底层页面分配(即mmap)将是一个长期的尝试。

那么安全吗?

。东西还是会坏,只是很少坏,即使坏了也不总是很明显。此外,父进程有时可以捕获其中一些错误并重试/恢复,还有其他方法可以处理风险。

我应该做吗?

我不建议这么做,但要视情况而定。如果你能处理一个错误,当然,继续。如果没有,那就是没有。

无论如何,使用IPC将消息转发给后台进程通常要好得多,以便进程执行任何所需的fork/任务。

当一个Rails控制器与一个web服务器结合在一起时,这种模式自然会发生。情况是不同的,取决于如果web服务器是线程,分叉或事件,但最终的结论是相同的;这是安全的。

Fork + Fork和thread + Fork应该不会出现多次访问数据库或多次执行相同代码的问题,因为只有当前线程在子线程中是活动的。

如果事件机在已分叉的线程中仍然处于活动状态,那么Event + fork可能会引发问题。幸运的是,大多数设计都会生成一个单独的线程来控制事件循环。

最新更新