如何使用dask有效地计算许多简单的统计信息

时间:2018-12-19 03:29:54

标签: python pandas distributed dask dask-distributed

问题

我想使用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_indexget_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

感谢您对如何考虑优化这一点的任何帮助或指导。

1 个答案:

答案 0 :(得分:3)

我想到了两个一般性建议,但是如果没有实际经验,很难诊断出这样的问题。听起来您已经在看仪表板,这很高兴听到。在这里,我将重点关注两个建议以避免调度开销,因为这是您特别提到的。

使用更大的分区

dd.read_csv等操作的默认分区大小足够小,无法在家用笔记本电脑上使用。 (我怀疑它们大约为128MB)。给定节点的大小,您可以将其增加10倍(或更大),并且可以。这样还可将您的调度程序开销减少10倍。

使用高级图形融合

截至2018年12月20日,它仍在开发分支中,但是dask.dataframe开始在表达式级别而不是任务级别融合。这应该可以大大减少您上千个统计信息的开销,从Dask的角度来看,可能会将它们变成一项任务。

您可能要跟踪以下PR:

我也鼓励您提供一个用例的综合示例作为GitHub问题,以便它可以为将来的开发提供信息。我建议使用dask.datasets.timeseries()来创建一个伪造的数据框,然后使用一些简单的方法来生成大量简单的统计信息。 (如果可能的话,简单会更好,这样维护人员就不必再深入研究了。)