带池工作者的Python多进程-内存使用优化
我有一个模糊的字符串匹配脚本,在400万个公司名称的大海捞针中寻找大约3万根针。虽然脚本工作正常,但由于内存不足,我尝试在AWS h1.xlarge上通过并行处理来加快速度的尝试失败了 我想了解如何优化工作流程,而不是像在回复中解释的那样尝试获得更多内存——我对这方面还比较陌生,所以应该有足够的空间。顺便说一句,我已经尝试过了(也成功了,但遇到了相同的带池工作者的Python多进程-内存使用优化,python,performance,memory-management,multiprocessing,string-matching,Python,Performance,Memory Management,Multiprocessing,String Matching,我有一个模糊的字符串匹配脚本,在400万个公司名称的大海捞针中寻找大约3万根针。虽然脚本工作正常,但由于内存不足,我尝试在AWS h1.xlarge上通过并行处理来加快速度的尝试失败了 我想了解如何优化工作流程,而不是像在回复中解释的那样尝试获得更多内存——我对这方面还比较陌生,所以应该有足够的空间。顺便说一句,我已经尝试过了(也成功了,但遇到了相同的MemoryError),此外,我还查看了一些非常有用的SO贡献,但还不是很到位 以下是代码中最相关的部分。我希望它能充分阐明逻辑-乐意提供更多需
MemoryError
),此外,我还查看了一些非常有用的SO贡献,但还不是很到位
以下是代码中最相关的部分。我希望它能充分阐明逻辑-乐意提供更多需要的信息:
def getHayStack():
## loads a few million company names into id: name dict
return hayCompanies
def getNeedles(*args):
## loads subset of 30K companies into id: name dict (for allocation to workers)
return needleCompanies
def findNeedle(needle, haystack):
""" Identify best match and return results with score """
results = {}
for hayID, hayCompany in haystack.iteritems():
if not isnull(haystack[hayID]):
results[hayID] = levi.setratio(needle.split(' '),
hayCompany.split(' '))
scores = list(results.values())
resultIDs = list(results.keys())
needleID = resultIDs[scores.index(max(scores))]
return [needleID, haystack[needleID], max(scores)]
def runMatch(args):
""" Execute findNeedle and process results for poolWorker batch"""
batch, first = args
last = first + batch
hayCompanies = getHayStack()
needleCompanies = getTargets(first, last)
needles = defaultdict(list)
current = first
for needleID, needleCompany in needleCompanies.iteritems():
current += 1
needles[targetID] = findNeedle(needleCompany, hayCompanies)
## Then store results
if __name__ == '__main__':
pool = Pool(processes = numProcesses)
totalTargets = len(getTargets('all'))
targetsPerBatch = totalTargets / numProcesses
pool.map_async(runMatch,
itertools.izip(itertools.repeat(targetsPerBatch),
xrange(0,
totalTargets,
targetsPerBatch))).get(99999999)
pool.close()
pool.join()
因此,我想问题是:如何避免为所有工人加载草堆?例如,通过共享数据或采取不同的方法,如将更大的草堆划分给工人而不是针?如何通过避免或消除混乱来提高内存使用率?您的设计有点混乱。您正在使用g一个由N个工作进程组成的池,然后将M个作业分解为N个大小为M/N的任务。换句话说,如果你完全正确,你就是在一个构建在工作进程之上的池上模拟工作进程。为什么要这么做呢?如果你想使用进程,就直接使用它们。或者,将一个池用作一个池,将每个作业作为它自己的任务,并使用批处理功能以某种适当(可调整)的方式对它们进行批处理 这意味着
runMatch
只需要一个needleID和needleCompany,它所做的就是调用findNeedle
,然后执行#然后存储结果
部分。然后主程序变得简单得多:
if __name__ == '__main__':
with Pool(processes=numProcesses) as pool:
results = pool.map_async(runMatch, needleCompanies.iteritems(),
chunkSize=NUMBER_TWEAKED_IN_TESTING).get()
或者,如果结果很小,而不是让所有进程(可能)为某个共享的结果存储内容而争斗,只需返回它们即可。这样,您根本不需要runMatch
,只需:
if __name__ == '__main__':
with Pool(processes=numProcesses) as pool:
for result in pool.imap_unordered(findNeedle, needleCompanies.iteritems(),
chunkSize=NUMBER_TWEAKED_IN_TESTING):
# Store result
或者,如果您确实希望执行N个批次,只需为每个批次创建一个流程:
if __name__ == '__main__':
totalTargets = len(getTargets('all'))
targetsPerBatch = totalTargets / numProcesses
processes = [Process(target=runMatch,
args=(targetsPerBatch,
xrange(0,
totalTargets,
targetsPerBatch)))
for _ in range(numProcesses)]
for p in processes:
p.start()
for p in processes:
p.join()
此外,您似乎为每个任务调用了一次
getHayStack()
(以及getniners
)。我不确定同时使用多个live副本会有多容易,但考虑到这是迄今为止您拥有的最大数据结构,这将是我尝试排除的第一件事。事实上,即使这不是内存使用问题,getHayStack
也很容易成为性能上的一大打击,除非您经常这样做ady正在进行某种缓存(例如,第一次显式地将其存储在全局或可变的默认参数值中,然后仅使用它),因此无论如何都值得修复
一次解决这两个潜在问题的一种方法是在构造函数中使用初始值设定项:
def initPool():
global _haystack
_haystack = getHayStack()
def runMatch(args):
global _haystack
# ...
hayCompanies = _haystack
# ...
if __name__ == '__main__':
pool = Pool(processes=numProcesses, initializer=initPool)
# ...
接下来,我注意到您在多个实际不需要的位置显式生成列表。例如:
scores = list(results.values())
resultIDs = list(results.keys())
needleID = resultIDs[scores.index(max(scores))]
return [needleID, haystack[needleID], max(scores)]
如果结果超过一小部分,这是浪费;只需直接使用results.values()
iterable即可。(事实上,看起来您使用的是Python 2.x,在这种情况下,键和值已经是列表,所以您只是在毫无理由地制作额外副本。)
但在这种情况下,您可以进一步简化整个过程。您只需查找得分最高的键(resultID)和值(score),对吗?因此:
needleID, score = max(results.items(), key=operator.itemgetter(1))
return [needleID, haystack[needleID], score]
这也消除了对分数
的所有重复搜索,这将节省一些CPU
这可能不会直接解决内存问题,但有望使调试和/或调整更容易
首先要尝试的是使用更小的批,而不是输入大小/cpu计数,尝试1。内存使用率下降了吗?如果没有,我们已经排除了这一部分
接下来,尝试sys.getsizeof(_haystack)
,看看它是怎么说的。如果它是,比如说,1.6GB,那么你就可以很好地将其他东西压缩到0.4GB,所以这是攻击它的方法——例如,使用数据库而不是普通的dict
还可以尝试转储内存使用情况(使用模块,getrusage(RUSAGE\u SELF)
)在初始值设定项函数的开始和结束处。如果最后的haystack只有,比如说,0.3GB,但您分配了另外1.3GB来构建它,这就是要解决的问题。例如,您可以派生一个子进程来构建和pickle dict,然后让池初始值设定项打开并取消pickle。或者将两个构建结合起来de>搁置第一个子项中的
db,并在初始值设定项中以只读方式打开它。无论哪种方式,这也意味着您只进行一次CSV解析/dict构建工作,而不是8次
另一方面,如果在第一个任务运行时,虚拟机的总使用率仍然很低(请注意,getrusage
无法直接查看虚拟机的总大小-ru\u maxrss
通常是一个有用的近似值,特别是当ru\u nswap
为0时),问题在于任务本身
首先,getsizeof
任务函数的参数和返回的值。如果它们很大,特别是如果它们在每个任务中不断变大或变量很大,则可能只是数据占用了太多内存,最终它们中的8个加在一起就足够大,可以达到极限
否则,问题最有可能出现在任务函数本身。要么是内存泄漏(只有使用有缺陷的C扩展模块或ctypes
才能出现真正的泄漏,但如果在调用之间保留任何引用(例如,在全局调用中),则可能永远不必要地保留某些内容),或者某些任务本身占用了太多内存。无论哪种方法,这都应该是您可以更轻松地进行测试的方法