关于fork:多进程fork的陷阱

40次阅读

共计 3411 个字符,预计需要花费 9 分钟才能阅读完成。

零、释义

  • milvus:向量数据库
  • langchain:python 提醒工程框架

一、背景

  • 本篇文章基于一个 BUG 的排查和解决过程,试图还原在某些场景下多过程编程的【陷阱】,达到前事不忘; 后事之师的成果。
  • 程序基于 python,但论断和情理实用于所有语言

二、BUG 问题体现

  • 最近的一段提醒工程相干的 python 代码,在不同操作系统的状况下,体现不一样

    • 在 macos 零碎与 linux 零碎的单过程、macos 零碎的多过程状况下均能够失常运行:
    • 在 linux 的多过程状况下会卡在与 milvus 交互的中央,如下图

三、假如

  1. milvus 服务端导致(磁盘满了、内存满了、服务忙碌等)
  2. 网络异样导致
  3. 操作系统导致
  4. 连贯 milvus 所用的底层调用包导致

四、假如验证和 BUG 排查思路

  1. ❌【milvus 服务端导致】

    1. 独自测试了 milvus 的读写,服务自身没有问题,milvus 所在服务器也是衰弱状态,不存在资源不足的状况。排除 1
  2. ❌【网络异样导致】

    1. telnet 端口通,ping 通且稳固,网络是 OK 的。排除 2
  3. ✅【操作系统导致】✅【连贯 milvus 所用的底层调用包导致】

    1. 初步判断为多过程导致的问题,那么为何 macos 中的多过程失常,linux 零碎的多过程就有问题呢?
    2. 排查思维链(Chain-of-Thought)

      1. 于是翻阅 python 对于多过程模块的官网文档,直到看到了这样一段话

      2. python 多过程在不同操作系统,默认启动子过程的形式是不一样的,在 windows 和 macos 上,默认应用【spawn】,而在 linux 上,默认是用【fork】,那么问题很有可能出在这两种不同的启动形式上。
      3. 本着控制变量法的 debug 形式,我在 linux 上将子过程的启动形式指定为了【spawn】,✅问题解决,程序胜利运行
      4. 至此,尽管外表上问题解决了,但我对解决此 BUG 的播种只有:【spawn】大法好,对其余稍深层次的细节无所不知,遗留有一些关键问题:

        1. spawn 是什么
        2. fork 是什么
        3. 为什么针对此 BUG,spawn 能够,fork 不行
        4. 如果咱们偏要用 fork 来做,行不行,怎么做?
      5. 于是,又回过头认真看了官网文档介绍以及 python 官网 issue 讨论区,(如下图)

      6. spawn 与 fork概念如下

        1. spawn:从头构建一个子过程,父过程的数据等拷贝到子过程空间内,领有本人的 Python 解释器,所以须要从新加载一遍父过程的包,因而启动较慢,因为数据都是本人的,安全性较高
        2. fork:除了必要的启动资源外,其余变量,包,数据等都继承自父过程,并且是 copy-on-write 的,也就是共享了父过程的一些内存页,因而启动较快,然而因为大部分都用的父过程数据,所以是不平安的过程
      7. fork 有可能导致不平安的过程,是因为 fork 用到 copy-on-write 技术,会继承父过程的数据和堆栈,由此导致一些不平安的问题。
      8. 那么针对此 BUG,具体是哪个中央导致了不平安呢?

        1. 既然是 milvus 连贯出了错,那先从连贯下手,排查发现,
        2. 首先,主过程所在文件在 import 模块的时候,其中一个模块(文件)发动了一次 milvus 的连贯,如下图

        3. 而后,主过程开始启动子过程(fork),子过程调用 langchain 的 milvus 模块,langchain 中 milvus 连贯初始化的代码是这样写的

        4. 子过程在上图中的步骤 2 的时候卡住,经排查是因为子过程基本没有连上 milvus,然而步骤 1 明明曾经判断过,如果没有连贯,则创立。
        5. 再进一步看看 connections.has_connection(“default”)这个函数,如下图

        6. 函数会判断 self._connected_alias 变量中是否有记录,进一步看看这个变量怎么来的

        7. 在连贯 milvus 时,程序保护一个 self._connected_alias 变量来记录是否存在连贯,connections.has_connection(“default”)函数只是去 self._connected_alias 中查看是否有连贯记录,
        8. 至此发现问题关键所在,父过程在第一次连贯 milvus 的时候,程序在 self._connected_alias 变量中记录了连贯信息,当 fork 子过程的时候,self._connected_alias 变量被一并继承给了子过程,而当子过程应用 connections.has_connection(“default”)函数判断与 milvus 的连贯状态的时候,发现了从父过程继承过去的 self._connected_alias 变量的已连贯信息,于是判断为已有连贯,导致子过程在理论没有连贯 milvus 的状况下间接加载 milvus 的数据,引发谬误。

五、解决方案

解决方案 1

计划

  • 采纳 spawn 形式启动子过程

长处

  • 简略粗犷,子过程和父过程独立,数据隔离,过程平安
  • 拓展和保护绝对不便,不必放心相似的 BUG

有余

  • spawn 形式,会老老实实地 copy 父过程的数据(即便不须要),比拟占内存空间,启动会慢一些

解决方案 2

计划

  • 采纳 fork 形式启动子过程,须要对代码做如下批改

    • 如果能够删除主过程中连贯 milvus 的代码

      • 将 milvus 连贯工作都放到子过程中做
    • 如果不能删除主过程中连贯 milvus 的代码

      • 在子过程判断与 milvus 是否已连贯的时候,不采纳 connections.has_connection(“default”) 函数,而是查看本过程本身的套接字连贯,防止来自父过程继承脏数据的净化,须要新增 have_socket 函数,做法如下
    def have_socket():
      have_socket = False
      process_netstat = psutil.Process(os.getpid())
      for _socket in process_netstat.connections():
          if _socket.raddr.port == MILVUS_PORT:
              have_socket = True
      return have_socket
    
    if not have_socket():
      connections.connect(**connection_args)

长处

  • 采纳 fork,子过程启动快,通过优化代码逻辑,防止过程不平安的状况

有余

  • 后续的代码拓展和保护都要留神代码逻辑,防止相似 BUG

六、总结

  • 写多线程 / 多过程代码的时候,须要留神具体代码逻辑,防止 继承的脏数据 导致线程 / 过程不平安
  • 对于资源束缚不大,性能要求不高的场景,多过程一律用 spawn

七、号外

  • 【python 开发组音讯】将 spawn 在所有平台上设置为默认选项曾经提上日程,打算 3.14 版本正式上线

    • https://discuss.python.org/t/switching-default-multiprocessin…
  • 【fork 的长处和利用场景】fork 也不是一无是处,对于只读数据须要共享的状况,还是十分省内存资源,

    • 比方编写模型预测的并发服务,fork 只加载 1 份模型到内存,而 spawn 会加载 N 份,gunicorn 的 -preload 参数就是基于 fork 的 copy-on-write 技术,达到模型只加载一次的目标

In general, fork is bad, but it’s also convenient and people rely on it to prepare data in a main process and then “duplicate” the process to inherit cooked data. -Victor Stinner


版本信息

  • python3.11.4
  • langchain==0.0.146

References

  1. Python crashes on macOS after fork with no exec
  2. multiprocessing’s default posix start method of ‘fork’ is broken: change to ‘spawn’
  3. Multiprocessing causes Python to crash and gives an error may have been in progress in another thread when fork() was called
  4. 机器学习模型 API 多过程内存共享
  5. 写时复制
  6. https://docs.python.org/3/library/multiprocessing.html
  7. https://discuss.python.org/t/switching-default-multiprocessin…

正文完
 0