零、释义
- 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_socketif 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...