共计 3411 个字符,预计需要花费 9 分钟才能阅读完成。
零、释义
- milvus:向量数据库
- langchain:python 提醒工程框架
一、背景
- 本篇文章基于一个 BUG 的排查和解决过程,试图还原在某些场景下多过程编程的【陷阱】,达到前事不忘; 后事之师的成果。
- 程序基于 python,但论断和情理实用于所有语言
二、BUG 问题体现
-
最近的一段提醒工程相干的 python 代码,在不同操作系统的状况下,体现不一样
- 在 macos 零碎与 linux 零碎的单过程、macos 零碎的多过程状况下均能够失常运行:
-
在 linux 的多过程状况下会卡在与 milvus 交互的中央,如下图
三、假如
- milvus 服务端导致(磁盘满了、内存满了、服务忙碌等)
- 网络异样导致
- 操作系统导致
- 连贯 milvus 所用的底层调用包导致
四、假如验证和 BUG 排查思路
-
❌【milvus 服务端导致】
- 独自测试了 milvus 的读写,服务自身没有问题,milvus 所在服务器也是衰弱状态,不存在资源不足的状况。排除 1
-
❌【网络异样导致】
- telnet 端口通,ping 通且稳固,网络是 OK 的。排除 2
-
✅【操作系统导致】✅【连贯 milvus 所用的底层调用包导致】
- 初步判断为多过程导致的问题,那么为何 macos 中的多过程失常,linux 零碎的多过程就有问题呢?
-
排查思维链(Chain-of-Thought)
-
于是翻阅 python 对于多过程模块的官网文档,直到看到了这样一段话
- python 多过程在不同操作系统,默认启动子过程的形式是不一样的,在 windows 和 macos 上,默认应用【spawn】,而在 linux 上,默认是用【fork】,那么问题很有可能出在这两种不同的启动形式上。
- 本着控制变量法的 debug 形式,我在 linux 上将子过程的启动形式指定为了【spawn】,✅问题解决,程序胜利运行
-
至此,尽管外表上问题解决了,但我对解决此 BUG 的播种只有:【spawn】大法好,对其余稍深层次的细节无所不知,遗留有一些关键问题:
- spawn 是什么
- fork 是什么
- 为什么针对此 BUG,spawn 能够,fork 不行
- 如果咱们偏要用 fork 来做,行不行,怎么做?
-
于是,又回过头认真看了官网文档介绍以及 python 官网 issue 讨论区,(如下图)
-
spawn 与 fork概念如下
- spawn:从头构建一个子过程,父过程的数据等拷贝到子过程空间内,领有本人的 Python 解释器,所以须要从新加载一遍父过程的包,因而启动较慢,因为数据都是本人的,安全性较高
- fork:除了必要的启动资源外,其余变量,包,数据等都继承自父过程,并且是 copy-on-write 的,也就是共享了父过程的一些内存页,因而启动较快,然而因为大部分都用的父过程数据,所以是不平安的过程
- fork 有可能导致不平安的过程,是因为 fork 用到 copy-on-write 技术,会继承父过程的数据和堆栈,由此导致一些不平安的问题。
-
那么针对此 BUG,具体是哪个中央导致了不平安呢?
- 既然是 milvus 连贯出了错,那先从连贯下手,排查发现,
-
首先,主过程所在文件在 import 模块的时候,其中一个模块(文件)发动了一次 milvus 的连贯,如下图
-
而后,主过程开始启动子过程(fork),子过程调用 langchain 的 milvus 模块,langchain 中 milvus 连贯初始化的代码是这样写的
- 子过程在上图中的步骤 2 的时候卡住,经排查是因为子过程基本没有连上 milvus,然而步骤 1 明明曾经判断过,如果没有连贯,则创立。
-
再进一步看看 connections.has_connection(“default”)这个函数,如下图
-
函数会判断 self._connected_alias 变量中是否有记录,进一步看看这个变量怎么来的
- 在连贯 milvus 时,程序保护一个 self._connected_alias 变量来记录是否存在连贯,connections.has_connection(“default”)函数只是去 self._connected_alias 中查看是否有连贯记录,
- 至此发现问题关键所在,父过程在第一次连贯 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
- Python crashes on macOS after fork with no exec
- multiprocessing’s default posix start method of ‘fork’ is broken: change to ‘spawn’
- Multiprocessing causes Python to crash and gives an error may have been in progress in another thread when fork() was called
- 机器学习模型 API 多过程内存共享
- 写时复制
- https://docs.python.org/3/library/multiprocessing.html
- https://discuss.python.org/t/switching-default-multiprocessin…