Python Dask 了解及简单使用

502 阅读10分钟

Dask

介绍

Dask 是一个灵活的开源库,适用于 Python 中的并行和分布式计算。

什么是 Dask?

Dask 是一个开源库,旨在为现有 Python 堆栈提供并行性。Dask 与 Python 库(如 NumPy 数组、Pandas DataFrame 和 scikit-learn)集成,无需学习新的库或语言,即可跨多个核心、处理器和计算机实现并行执行。

  • 用于并行列表、数组和 DataFrame 的 API 集合,可原生扩展 Numpy、NumPy、Pandas 和 scikit-learn,以在大于内存环境或分布式环境中运行。  Dask 集合是底层库的并行集合(例如,Dask 数组由 Numpy 数组组成)并运行在任务调度程序之上。
  • 一个任务调度程序,用于构建任务图形,协调、调度和监控针对跨 CPU 核心和计算机的交互式工作负载优化的任务。

Dask 包含三个并行集合,即 DataFrame、Bag 和数组,每个均可自动使用在 RAM 和磁盘之间分区的数据,以及根据资源可用性分布在集群中多个节点之间的数据。对于可并行但不适合 Dask 数组或 DataFrame 等高级抽象的问题,有一个“延迟”函数使用 Python 装饰器修改函数,以便它们延迟运行。这意味着执行被延迟,并且函数及其参数被放置到任务图形中。

Dask 的任务调度程序可以扩展至拥有数千个节点的集群,其算法已在一些全球最大的超级计算机上进行测试。其任务调度界面可针对特定作业进行定制。Dask 可提供低用度、低延迟和极简的序列化,从而加快速度。

在分布式场景中,一个调度程序负责协调许多工作人员,将计算移动到正确的工作人员,以保持连续、无阻塞的对话。多个用户可能共享同一系统。此方法适用于 Hadoop HDFS 文件系统以及云对象存储(例如 Amazon 的 S3 存储)。

该单机调度程序针对大于内存的使用量进行了优化,并跨多个线程和处理器划分任务。它采用低用度方法,每个任务大约占用 50 微秒。

为何选择 Dask?

Python 的用户友好型高级编程语言和 Python 库(如 NumPy、Pandas 和 scikit-learn)已经得到数据科学家的广泛采用。

这些库是在大数据用例变得如此普遍之前开发的,没有强大的并行解决方案。Python 是单核计算的首选,但用户不得不为多核心或多计算机并行寻找其他解决方案。这会中断用户体验,还会让用户感到非常沮丧。

过去五年里,对 Python 工作负载扩展的需求不断增加,这导致了 Dask 的自然增长。Dask 是一种易于安装、快速配置的方法,可以加速 Python 中的数据分析,无需开发者升级其硬件基础设施或切换到其他编程语言。启动 Dask 作业所使用的语法与其他 Python 操作相同,因此可将其集成,几乎不需要重新写代码。

Demo

需要用到的依赖

pip install dask
pip install bokeh
pip install distributed
pip install dask[dataframe]

DataFrames

docs.dask.org/en/stable/d…

摘自官网 Dask DataFrames

你的数据太大了
你的计算太慢了,其他技术也不起作用

Pandas

您的数据很小
你的计算速度很快(亚秒)
有更简单的方法来加速你的计算,比如避免.application或Python for循环,并使用内置的pandas方法。

可以将一个DataFrame转换为一个Dask.DataFrame

import numpy as np
import pandas as pd
from dask.dataframe import dd

index = pd.date_range("2021-09-01", periods=2400, freq="1h")
df = pd.DataFrame({"a": np.arange(2400), "b": list("abcaddbe" * 300)}, index=index)
print(df)
ddf = dd.from_pandas(df, npartitions=10)

total = ddf["2021-10-01": "2021-10-09 5:00"].compute()

print(ddf)
print(total)
求和
import time

import dask.dataframe as dd
import pandas as pd
import tqdm
from dask.distributed import Client

from jy_common.jy_data import StockData

pd_1k = StockData.get_stock_trade_only_k_1m("2024-08-07", ['3'])

pd_1k_list = []
for x in tqdm.tqdm(range(10)):
    pd_1k_list.append(pd_1k.copy())


#combined_df len 12857350 一千万数据

combined_df = pd.concat(pd_1k_list, ignore_index=True)


def pd_t():
    print(f"combined_df len {len(combined_df)}")
    ts = time.time()

    # result = combined_df.groupby('wind_codes')['open'].sum()
    result = combined_df.groupby('wind_codes')['open'].mean()

    print(reult)
    print('cost time :%s' % (time.time() - ts))
    # cost time :1.0329430103302002


def dask_t():
    # 可以指定集群 去掉则为本地模式
    client = Client('192.168.10.5:8786',timeout=60)

    # pip install dask[dataframe]
    print(f"combined_df len {len(combined_df)}")
    # npartitions 分区
    ddf = dd.from_pandas(combined_df, npartitions=100)
    ts = time.time()
    # result = combined_df.groupby('wind_codes')['open'].sum()
    result = ddf.groupby('wind_codes')['open'].mean().compute()
    print(result)
    print('cost time :%s' % (time.time() - ts))

    # 本地
    # 2 Work cost time :5.209286451339722
    # 集群 
    # 2 Work npartitions=100 cost time :41.783215284347534
    # 4 Work npartitions=100 cost time :21.400845766067505
    # 6 Work npartitions=100 cost time :17.400845766067505
    # 6 Work npartitions=1   cost time :9.316875219345093


if __name__ == '__main__':
    dask_t()
Partitions

分区, 可以用npartitions指定分区个数,Dask DataFrame 被拆分为许多分区,其中每个分区是一个 Pandas DataFrame。这些 DataFrame 沿索引垂直拆分

ddf = dd.from_pandas(combined_df, npartitions=100)

在做一些特定操作时,合适的分区会影响操作的效率.分区也会再聚合时对需要做聚合的操作分成几分执行.

使用索引

以选择性地沿单个索引列对 Dask DataFrame 进行排序。针对此列的某些操作可能非常快。例如,如果数据集按时间排序,则可以快速选择特定日期的数据、执行时间序列联接等。您可以通过查看 df.known_divisions 属性来检查您的数据是否已排序。您可以使用 .set_index(column_name) 方法设置索引列。

避免全数据洗牌

某些操作(如 set_index 和 merge/join )在并行或分布式设置中比在单台计算机的内存中执行更难。特别是,重新排列数据的洗牌操作变得更加通信密集。例如,如果您的数据是按客户 ID 排列的,但现在您想按时间排列数据,则所有分区都必须相互通信以交换数据分片。这可能是一个密集的过程,尤其是在集群上。

结论

经测试在一千万数据,pandas做数据求均值, pandas更快,dask在本地模式和集群 2 4 6个Work时表现不同,但均慢于Pandas.其实大部分时间耗费在网络传输和集群交互.简单操作感觉不适用

Delayed

dask.delayed 接口并行化自定义算法

import time

from dask import delayed


def inc(x):
    time.sleep(1)
    return x + 1


def add(x, y):
    time.sleep(1)
    return x + y


def no_delayed():
    """
    普通代码 
    time
    no_delayed: 3.001702070236206
    # 因为我们一个接一个地按顺序调用每个函数
    @return:
    """
    ts = time.time()
    x = inc(1)
    y = inc(2)
    z = add(x, y)
    print(f"no_delayed: {time.time() - ts}")


def dask_delayed():
    """
    dask
    time
    dask_delayed: 2.5537471771240234
    @return:
    """
    ts = time.time()
    x = delayed(inc)(1)
    y = delayed(inc)(2)
    z = delayed(add)(x, y)
    # 这会立即运行,它所做的只是构建一个图

  
    # 这里才事实上地使用本地线程池运行我们的计算
    z.compute()
    print(f"dask_delayed: {time.time() - ts}")


if __name__ == '__main__':
    no_delayed()
    dask_delayed()

image.png

分布式

本地模式

所有未指定集群的方式运行Dask代码都是本地模式运行

集群方式

注: 需要准备几台python环境相同,并且Dask包相同的电脑

简单环境搭建

主服务器执行

dask-scheduler


'''
2024-08-09 13:29:47,133 - distributed.scheduler - INFO - -----------------------------------------------
2024-08-09 13:29:47,759 - distributed.http.proxy - INFO - To route to workers diagnostics web server please install jupyter-server-proxy: python -m pip install jupyter-server-proxy
2024-08-09 13:29:47,791 - distributed.scheduler - INFO - State start
2024-08-09 13:29:47,818 - distributed.scheduler - INFO - -----------------------------------------------
2024-08-09 13:29:47,819 - distributed.scheduler - INFO -   Scheduler at:   tcp://192.168.10.5:8786
2024-08-09 13:29:47,819 - distributed.scheduler - INFO -   dashboard at:  http://192.168.10.5:8787/status
2024-08-09 13:29:47,819 - distributed.scheduler - INFO - Registering Worker plugin shuffle

'''

访问上边给出的Status地址可以看到监控页面,这里面可以看的运行的信息 image.png 其他电脑执行

# dask-worker 地址   上边执行后会有地址
dask-worker 192.168.10.211:8786


'''
2024-08-09 13:53:47,813 - distributed.nanny - INFO -         Start Nanny at: 'tcp://192.168.10.212:37249'
2024-08-09 13:53:48,698 - distributed.diskutils - INFO - Found stale lock file and directory '/tmp/dask-scratch-space/worker-bo7nhiy2', purging
2024-08-09 13:53:48,698 - distributed.diskutils - INFO - Found stale lock file and directory '/tmp/dask-scratch-space/worker-8ykf0m9y', purging
2024-08-09 13:53:49,810 - distributed.worker - INFO -       Start worker at: tcp://192.168.10.212:46488
2024-08-09 13:53:49,810 - distributed.worker - INFO -          Listening to: tcp://192.168.10.212:46488
2024-08-09 13:53:49,811 - distributed.worker - INFO -          dashboard at:       192.168.10.212:39052
2024-08-09 13:53:49,811 - distributed.worker - INFO - Waiting to connect to:  tcp://192.168.10.211:8786
2024-08-09 13:53:49,811 - distributed.worker - INFO - -------------------------------------------------
2024-08-09 13:53:49,811 - distributed.worker - INFO -               Threads:                          8
2024-08-09 13:53:49,811 - distributed.worker - INFO -                Memory:                   7.62 GiB
2024-08-09 13:53:49,811 - distributed.worker - INFO -       Local Directory: /tmp/dask-scratch-space/worker-v9srcqjw
2024-08-09 13:53:49,811 - distributed.worker - INFO - -------------------------------------------------
2024-08-09 13:53:54,624 - distributed.worker - WARNING - Mismatched versions found

+---------+---------------------------------------------+-----------+---------+
| Package | Worker-0a4056b4-36cd-4477-8f6d-8b45014cad76 | Scheduler | Workers |
+---------+---------------------------------------------+-----------+---------+
| lz4     | 4.3.3                                       | None      | 4.3.3   |
+---------+---------------------------------------------+-----------+---------+
2024-08-09 13:53:54,625 - distributed.worker - INFO - Starting Worker plugin shuffle
2024-08-09 13:53:54,626 - distributed.worker - INFO -         Registered to:  tcp://192.168.10.211:8786
2024-08-09 13:53:54,626 - distributed.worker - INFO - -------------------------------------------------
2024-08-09 13:53:54,627 - distributed.core - INFO - Starting established connection to tcp://192.168.10.211:8786


'''


使用

任何dask代码前加上以下代码,运行代码时就会自动把Dask任务放到集群运行

from dask.distributed import Client
 # 连接集群
 client = Client('192.168.10.211:8786')

Future

Dask 支持一个实时任务框架,该框架扩展了 Python 的 concurrent.futures 接口。

集群

虚拟机2: 8处理器 8G内存 1 Work

虚拟机3: 8处理器 8G内存 2 Work

虚拟机4: 8处理器 8G内存 2 Work

import time

from dask.distributed import Client


def square(x):
    # 让程序暂停5秒
    time.sleep(5)
    print(f'square--{x}')
    return x ** 2


def neg(x):
    # 让程序暂停5秒
    time.sleep(5)
    print(f'neg--{x}')
    return -x


def no_dask():
    """
    不用dask 单线程
    #2个任务每个任务运行完需要5秒 10万次循环  cost time :1000000
    """
    ts = time.time()
    add_list1 = []
    for e in range(100000):
        add_list1.append(square(e))
    add_list2 = []
    for e2 in add_list1:
        add_list2.append(neg(e2))

    total = sum(add_list2)
    print(total)  # 输出: 15

    print('cost time :%s' % (time.time() - ts))


def dask_cluster():
    """
    集群
     #5 Work  2个任务每个任务运行完需要5秒 10万次循环  cost time :25066.37527489662
    """
    client = Client('192.168.10.211:8786')
    ts = time.time()
    # 执行 square
    A = client.map(square, range(100000))
    # 执行 neg
    B = client.map(neg, A)
    # 汇总结果
    total = client.submit(sum, B)
    print(total.result())
    print('cost time :%s' % (time.time() - ts))
    client.close()


def add(x):
    print(x)
    return x


def dask_single():
    """
    单机dask
    """
    client = Client()
    ts = time.time()
    A = client.map(square, range(100000))
    B = client.map(neg, A)
    # 汇总结果 可以传一个自定义函数对结果进行处理
    total = client.submit(add, B)
    print(total.result())
    print('cost time :%s' % (time.time() - ts))


if __name__ == '__main__':
    dask_single()

Other

中文翻译文档 www.heywhale.com/home/column…

Apache Parquet 是一种列式二进制格式。它是存储大量表格数据的事实标准,也是我们推荐的基本表格数据存储解决方案。

 Kubernetes 是一个开源的容器编排引擎,用来对容器化应用进行自动化部署、扩缩和管理。该项目托管在 CNCF

kubernetes.io/zh-cn/docs/…

分布式计算哪家强:Spark、Dask、Ray大比拼-CSDN博客

集群方案