我想使用Dask计算一堆“易于收集”的统计信息。 速度是我的首要关注和目标,因此我希望对这个问题进行广泛的讨论。 理想情况下,我希望在一个小时内完成上述问题。 我希望使用100-1000名工人。 目前,在基准测试中,我正在大型计算机(160核,4 TB RAM)上运行此程序,但计划很快迁移到Kubernetes。
我在数据框中有一些数据(熊猫,dask,csv,镶木地板等) 我还有很多数据的子集(带有任意列过滤器),我想为其计算统计数据。
DataFrame大小:介于5 GB和5 TB之间的数据。 (1亿行,数千列)。预计将来会达到50-100 TB。
统计数据大小:大约5000个唯一过滤器,每个唯一过滤器介于1到500个统计之间。 (5k-5M统计数据)
下面的玩具示例:
requested_statistics = [
{'filters': [{'col': 'col_1', 'op': 'lt', 'value': 0.8},
{'col': 'col_38', 'op': 'lt', 'value': 0.4},
{'col': 'col_224', 'op': 'gt', 'value': 0.12333}],
'output': {'col': 'col_3', 'op': 'sum'},
'name': 'stat_1'},
{'filters': [{'col': 'col_551', 'op': 'lt', 'value': 0.8},
{'col': 'col_112', 'op': 'gt', 'value': '2018-01-13'},
{'col': 'col_1', 'op': 'lt', 'value': 0.8}],
'output': {'col': 'col_2', 'op': 'avg'},
'name': 'stat_2'}
]
我可以编写一个可以在dask或pandas上运行的简单解析器:
def filter_index(df, filter):
filter_ops = {'lt': lambda x, y: x < y, 'gt': lambda x, y: x > y, 'eq': lambda x, y: x == y}
return filter_ops[filter['op']](df[filter['col']], filter['value'])
def get_indexer(df, filters):
if len(filters) == 1:
return filter_index(df, filters[0])
return np.logical_and(filter_index(df, filters[0]), get_indexer(df, filters[1:]))
def get_statistic(df, statistic):
indexer = get_indexer(df, statistic['filters'])
agg_ops = {'sum': np.sum, 'avg': np.mean, 'unique_count': lambda x: x.unique().size}
return agg_ops[statistic['output']['op']](df[statistic['output']['col']][indexer])
all_stats = {x['name']: get_statistic(df, x) for x in requested_statistics}
1)只需依靠一下future_stats = client.compute(all_stats)
。
这没有用,因为优化图形(或仅序列化为调度程序)的计算时间过长。 在小规模测试中,这可以很好地工作,但是当我按比例扩展npartition时,这似乎在时间上比O(N)差很多。
2)对每个统计信息(client.compute(stat, sync=True)
或client.compute(stat).result()
)运行计算。
这增加了与调度程序进行通信的开销,对于我要计算的约100,000个统计信息,将花费太长时间。
3)缓存(通过持久存储)中间结果(索引器),以便我可以重复使用。
鉴于过滤器有时可以共享索引器,我为filter_index
和get_indexer
字段添加了缓存。
具体地说,创建一个哈希和indexer = client.persist(indexer)
,在以后的调用中返回持久化索引器。对于get_indexer
,我还添加了combinations
检查,以尝试查看缓存中是否存在过滤器的任何子集。我还优化了调用统计信息的顺序,以使每个下一组最多最多只需要1个新的变化索引器。
(例如,一次执行所有共享相同过滤器的操作,然后移至下一个)。
不幸的结果是,需要大量的内存来保留所有布尔掩码。
我还没有尝试过滚动缓存(随着计算运行cache.pop(index_id)
,一旦计算不再需要它进行持久化),但这是我的下一步。
上面列出的解决方案(3)是我目前已经实现的,但是它的表现仍然不如我希望的那样。
内存成本非常高(有效地为每个唯一的过滤器创建了一个全新的列)
计划程序/图形序列化似乎很昂贵
在htop
处看到的大多数情况下,只有dask-scheduler
以100%的速度运行,并且工作人员大多闲置。
1)我还可以采用哪些其他方法,或者上面列出的方法是否有明显的遗漏?
2)我考虑过df.query(string)
,但是由于它可以在整个数据帧上运行,因此效率似乎很低(很多数据重复)。这是真的,还是使用内置的语法解析器会有所收获(我注意到,虽然dask图较小,但不确定是否值得)。
3)调度程序和单线程(?)敏捷图形创建者似乎是瓶颈,是否有任何清晰的路径可以并行化这些东西?
4)当我查看分布式bokeh状态监视程序时,我经常注意到它在这些计算过程中也挂起,从而使其难以调试,并让我好奇使用Web服务器是否确实损害了调度程序的性能?这是真的吗?
5)在日志中,我收到很多Event loop was unresponsive in Worker for Xs.
警告。我能做些什么来帮助平衡工作或重新编写分配给工作人员的任务,或使调度程序更具响应性?
6)为了降低复杂图形的复杂度,我有了repartition(npartitions=num_workers*2)
,但是我不确定这是一种很好的启发式方法还是应该使用什么?
这是调度程序正在管理的任务的一个示例(这是〜25个唯一的过滤器,每个过滤器具有〜50个统计信息,总共计算了约1,000个统计信息。
https://i.imgur.com/hRzmXHP.png
感谢您对如何考虑优化这一点的任何帮助或指导。
答案 0 :(得分:3)
我想到了两个一般性建议,但是如果没有实际经验,很难诊断出这样的问题。听起来您已经在看仪表板,这很高兴听到。在这里,我将重点关注两个建议以避免调度开销,因为这是您特别提到的。
dd.read_csv
等操作的默认分区大小足够小,无法在家用笔记本电脑上使用。 (我怀疑它们大约为128MB)。给定节点的大小,您可以将其增加10倍(或更大),并且可以。这样还可将您的调度程序开销减少10倍。
截至2018年12月20日,它仍在开发分支中,但是dask.dataframe开始在表达式级别而不是任务级别融合。这应该可以大大减少您上千个统计信息的开销,从Dask的角度来看,可能会将它们变成一项任务。
您可能要跟踪以下PR:
我也鼓励您提供一个用例的综合示例作为GitHub问题,以便它可以为将来的开发提供信息。我建议使用dask.datasets.timeseries()
来创建一个伪造的数据框,然后使用一些简单的方法来生成大量简单的统计信息。 (如果可能的话,简单会更好,这样维护人员就不必再深入研究了。)