使用Apache Arrow助力PySpark数据处理——本质上是在内存中按照列式存储组织数据格式,以提升性能
使用Apache Arrow助力PySpark数据处理
开源大数据EMR 2019-05-30 1802浏览量
Apache Arrow从Spark 2.3版本开始被引入,通过列式存储,zero copy等技术,JVM 与Python 之间的数据传输效率得到了大量的提升。本文主要介绍一下Apache Arrow以及Spark中的使用方法。
列式存储简介
在介绍Spark中使用Apache Arrow之前,先简单的介绍一下Apache Arrow以及他背后的一些技术背景。
在大数据时代之前,大部分的存储引擎使用的是按行存储的形式,很多早期的系统,如交易系统、ERP系统等每次处理的是增、删、改、查某一个实体的所有信息,按行存储的话能够快速的定位到单个实体并进行处理。如果使用列存储,对某一个实体的不同属性的操作就需要进行多次随机读写,效率将会是非常差的。
随着大数据时代的到来,尤其是数据分析的不断发展,任务不需要一次读取实体的所有属性,而只关心特定的某些属性,并对这些属性进行aggregate等复杂的操作等。这种情况下行存储将需要读取额外的数据,形成瓶颈。而选择列存储将会减少额外数据的读取,对相同属性的数据还可以进行压缩,大大的加快了处理速度。
以下是行存储和列存储的对比说明,摘自Apache Arrow 官网,上面是一个二维表,由三个属性组成,分别是session_id, timestamp和source_ip。左侧为行存储在内存中的表示,数据按行依次存储,每一行按照列的顺序存储。右侧为列存储在内存中的表示,每一列单独存放,根据batch size等属性来控制一次写入的列簇大小。这样当查询语句只涉及少数列的时候,比如图中SQL查询,只需要过滤session_id列,避免读取所有数据列,减少了大量的I/O损耗,同时考虑到CPU pipeline以及使用CPU SIMD技术等等,将大大的提升查询速度。
Apache Arrow
在大数据领域,列式存储的灵感来自Google于2010年发表的Dremel论文,文中介绍了一种支持嵌套结构的存储格式,并且使用了列式存储的方式提升查询性能,在Dremel论文中还介绍了Google如何使用这种存储格式实现并行查询的。这篇论文影响了Hadoop生态系统发展,之后的Apache Parquet和Apache ORC作为列式存储格式已经被广大的Hadoop生态系统使用,如Spark、Hive、Impala等等。
Apache Arrow在官网上是这样定义的,Apache Arrow是一个跨语言、跨平台的内存数据结构。从这个定义中我们可以看到Apache Arrow与Apache Parquet以及Apache ORC的区别。Parquet与ORC设计的目的针对磁盘数据,在列存储的基础上使用了高效率的压缩算法进行压缩,比如使用snappy、gzip和zlib等算法对列数据进行压缩。所以大部分情况下在数据读取的时候需要首先对数据进行反压缩,并有一定的cpu使用损耗。而Arrow,作为在内存中的数据,并不支持压缩(当然写入磁盘是支持压缩的),Arrow使用dictionary-encoded来进行类似索引的操作。
除了列存储外,Arrow在数据在跨语言的数据传输上具有相当大的威力,Arrow的跨语言特性表示在Arrow的规范中,作者指定了不同数据类型的layout,包括不同原始数据类型在内存中占的比特数,Array数据的组成以及Null值的表示等等。根据这些定义后,在不同的平台和不同的语言中使用Arrow将会采用完全相同的内存结构,因此在不同平台间和不同语言间进行高效数据传输成为了可能。在Arrow之前如果要对不同语言数据进行传输必须要使用序列化与反序列化技术来完成,耗费了大量的CPU资源和时间,而Arrow由于根据规范在内存中的数据结构一致,可以通过共享内存, 内存映射文件等技术来共享Arrow内存结构,省去了序列化与反序列过程。
Spark与Apache Arrow
介绍完Arrow的背景后,来看一下Apache Spark如何使用Arrow来加速PySpark处理的。一直以来,使用PySpark的客户都在抱怨python的效率太低,导致了很多用户转向了使用Scala进行开发。这主要是由于Spark使用Scala语言开发,底层启动的是JVM,而PySpark是Scala中PythonRDD对象启动的一个Python子进程,Python与JVM的通信使用了Py4J, 通过Py4J Python程序能够动态的访问JVM中的Java对象,这一过程使用了linux pipe,在底层JVM需要对RDD进行序列化,在Python端需要对RDD进行反序列化,当数据量较大的时候效率远不如直接使用Scala。流程如下图。
很多数据科学家以及分析人员习惯使用python来进行处理,尤其是使用Pandas和Numpy库来对数据进行后续处理,Spark 2.3以后引入的Arrow将会大大的提升这一效率。我们从代码角度来看一下实现,在Spark 2.4版本的dataframe.py代码中,toPandas的实现为:
if use_arrow:
try:
from pyspark.sql.types import _check_dataframe_convert_date, \
_check_dataframe_localize_timestamps
import pyarrow
batches = self._collectAsArrow()
if len(batches) > 0:
table = pyarrow.Table.from_batches(batches)
pdf = table.to_pandas()
pdf = _check_dataframe_convert_date(pdf, self.schema)
return _check_dataframe_localize_timestamps(pdf, timezone)
else:
return pd.DataFrame.from_records([], columns=self.columns)
except Exception as e:
# We might have to allow fallback here as well but multiple Spark jobs can
# be executed. So, simply fail in this case for now.
msg = (
"toPandas attempted Arrow optimization because "
"'spark.sql.execution.arrow.enabled' is set to true, but has reached "
"the error below and can not continue. Note that "
"'spark.sql.execution.arrow.fallback.enabled' does not have an effect "
"on failures in the middle of computation.\n %s" % _exception_message(e))
warnings.warn(msg)
raise
如果使用了Arrow(Spark 2.4默认使用),比较重要的一行是_collectAsArrow(),_collectAsArrow()实现为:
def _collectAsArrow(self):
"""
Returns all records as a list of ArrowRecordBatches, pyarrow must be installed
and available on driver and worker Python environments.
.. note:: Experimental.
"""
with SCCallSiteSync(self._sc) as css:
sock_info = self._jdf.collectAsArrowToPython()
return list(_load_from_socket(sock_info, ArrowStreamSerializer()))
这里面使用了ArrowStreamSerializer(),而ArrowStreamSerializer定义为
class ArrowStreamSerializer(Serializer):
"""
Serializes Arrow record batches as a stream.
"""
def dump_stream(self, iterator, stream):
import pyarrow as pa
writer = None
try:
for batch in iterator:
if writer is None:
writer = pa.RecordBatchStreamWriter(stream, batch.schema)
writer.write_batch(batch)
finally:
if writer is not None:
writer.close()
def load_stream(self, stream):
import pyarrow as pa
reader = pa.open_stream(stream)
for batch in reader:
yield batch
def __repr__(self):
return "ArrowStreamSerializer"
可以看出在这里面,jvm对数据根据Arrow规范设置好内存数据结构进行列式转化后,Python层面并不需要任何的反序列过程,而是直接读取,这也是Arrow高效的原因之一。
对比看那一下如果不使用Arrow方法为:
def collect(self):
"""Returns all the records as a list of :class:`Row`.
>>> df.collect()
[Row(age=2, name=u'Alice'), Row(age=5, name=u'Bob')]
"""
with SCCallSiteSync(self._sc) as css:
sock_info = self._jdf.collectToPython()
return list(_load_from_socket(sock_info, BatchedSerializer(PickleSerializer())))
序列化方法为PickleSerializer,需要对每一条数据使用PickleSerializer进行反序列化。
那如何通过这一特性来进行我们的开发呢,Spark提供了Pandas UDFs功能,即向量化UDF,Pandas UDF主要是通过Arrow将JVM里面的Spark DataFrame传输给Python生成pandas DataFrame,并执行用于定义的UDF。目前有两种类型,一种是Scalar,一种是Grouped Map。
这里主要介绍一下Scalar Python UDFs的使用,以及可能的场景。 Scalar Python UDFs可以在select和withColumn中使用,他的输入参数为pandas.Series类型,输出参数为相同长度的pandas.Series。Spark内部会通过Arrow将列式数据根据batch size获取后,批量的将数据转化为pandas.Series类型,并在每个batch都执行用户定义的function。最后将不同batch的结果进行整合,获取最后的数据结果。
以下是官网的一个例子:
import pandas as pd
from pyspark.sql.functions import col, pandas_udf
from pyspark.sql.types import LongType
# Declare the function and create the UDF
def multiply_func(a, b):
return a * b
multiply = pandas_udf(multiply_func, returnType=LongType())
# The function for a pandas_udf should be able to execute with local Pandas data
x = pd.Series([1, 2, 3])
print(multiply_func(x, x))
# 0 1
# 1 4
# 2 9
# dtype: int64
# Create a Spark DataFrame, 'spark' is an existing SparkSession
df = spark.createDataFrame(pd.DataFrame(x, columns=["x"]))
# Execute function as a Spark vectorized UDF
df.select(multiply(col("x"), col("x"))).show()
# +-------------------+
# |multiply_func(x, x)|
# +-------------------+
# | 1|
# | 4|
# | 9|
# +-------------------+
首先定义udf,multiply_func,主要功能就是将a、b两列的数据对应行数据相乘获取结果。然后通过pandas_udf装饰器生成Pandas UDF。最后使用df.selecct方法调用Pandas UDF获取结果。这里面要注意的是pandas_udf的输入输出数据是向量化数据,包含了多行,可以根据spark.sql.execution.arrow.maxRecordsPerBatch来设置。
可以看出Pandas UDF使用非常简单,只需要定义好Pandas UDF就可以了。有了Pandas UDF后我们可以很容易的将深度学习框架和Spark进行结合,比如在UDF中使用一些深度学习框架,比如scikit-learn,我们可以对批量的数据分别进行训练。下面是一个简单的例子,利用Pandas UDF来进行训练:
# Load necessary libraries
from pyspark.sql.functions import pandas_udf, PandasUDFType
from pyspark.sql.types import *
import pandas as pd
from scipy.optimize import leastsq
import numpy as np
# Create the schema for the resulting data frame
schema = StructType([StructField('ID', LongType(), True),
StructField('p0', DoubleType(), True),
StructField('p1', DoubleType(), True)])
# Define the UDF, input and outputs are Pandas DFs
@pandas_udf(schema, PandasUDFType.GROUPED_MAP)
def analyze_player(sample_pd):
# return empty params in not enough data
if (len(sample_pd.shots) <= 1):
return pd.DataFrame({'ID': [sample_pd.player_id[0]],
'p0': [ 0 ], 'p1': [ 0 ]})
# Perform curve fitting
result = leastsq(fit, [1, 0], args=(sample_pd.shots,
sample_pd.hits))
# Return the parameters as a Pandas DF
return pd.DataFrame({'ID': [sample_pd.player_id[0]],
'p0': [result[0][0]], 'p1': [result[0][1]]})
# perform the UDF and show the results
player_df = df.groupby('player_id').apply(analyze_player)
display(player_df)
除此之外还可以使用TensorFlow和MXNet等与Spark进行融合,近期阿里云EMR Data Science集群将会推出相应的功能,整合EMR Spark与深度学习框架之间调度与数据交换功能,希望大家关注。
https://www.codercto.com/a/68911.html
用ApacheArrow加速PySpark
内容简介:Pandas、Numpy是做数据分析最常使用的Python包,如果数据存在Hadoop又想用Pandas做一些数据处理,通常会使用PySpark的可以看到,近500w数据的toPandas操作,开启arrow后,粗略耗时统计从39s降低为2s。如何开启arrow,就是spark.sql.execution.arrow.enabled=true这个配置了,spark2.3开始支持。
本文转载自:http://tech.dianwoda.com/2019/04/01/yong-apachearrowjia-su-pyspark/,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有。
用ApacheArrow加速PySpark
https://www.codercto.com/a/68911.html
Pandas、Numpy是做数据分析最常使用的 Python 包,如果数据存在Hadoop又想用Pandas做一些数据处理,通常会使用PySpark的 DataFrame.toPandas() 这个方法。让人不爽的是,这个方法执行很慢,数据量越大越慢。
做个测试
Using Python version 2.7.14 (default, Oct 5 2017 02:28:52)
SparkSession available as 'spark'.
>>> def test():
... from pyspark.sql.functions import rand
... from better_utils import TimeUtil
... start = TimeUtil.now_unix()
... df = spark.range(1 << 22).toDF('id').withColumn("x", rand())
... df.toPandas()
... cost = TimeUtil.now_unix() - start
... print "耗时:{}s".format(cost)
...
>>> test()
耗时:39s
>>> spark.conf.set("spark.sql.execution.arrow.enabled", "true")
>>> test()
/Users/yulian/anaconda3/envs/python2/lib/python2.7/site-packages/pyarrow/__init__.py:152: UserWarning: pyarrow.open_stream is deprecated, please use pyarrow.ipc.open_stream
warnings.warn("pyarrow.open_stream is deprecated, please use "
耗时:2s
>>>
可以看到,近500w数据的toPandas操作,开启arrow后,粗略耗时统计从39s降低为2s。
如何开启arrow,就是spark.sql.execution.arrow.enabled=true这个配置了,spark2.3开始支持。
另外需要安装pip install pyarrow。
是什么
Arrow是一种跨语言的基于内存的列式数据结构。
在分布式系统内部,每个系统都有自己的内存格式,大量的 CPU 资源被消耗在序列化和反序列化过程中,并且由于每个项目都有自己的实现,没有一个明确的标准,造成各个系统都在重复着复制、转换工作,这种问题在微服务系统架构出现之后更加明显,Arrow 的出现就是为了解决这一问题。作为一个跨平台的数据层,我们可以使用 Arrow 加快大数据分析项目的运行速度。
需要明确的是,Apache Arrow 不是一个引擎,也不是一个存储系统,它是用来处理分层的列式内存数据的一系列格式和算法。
为什么
PySpark中使用DataFrame.toPandas()将数据从Spark DataFrame转换到Pandas中是非常低效的。
Spark和Python基于Socket通信,使用serializers/deserializers交换数据。
Python的反序列化pyspark.serializers.PickleSerializer使用cPickle模块的标准pickle格式。
Spark先把所有的行汇聚到driver上,然后通过初始转换,以消除Scala和 Java 之间的任何不兼容性,使用Pyrolite库的org.apache.spark.api.python.SerDeUtil.AutoBatchedPickler去把Java对象序列化成pickle格式。
然后序列化后的数据分批发送个Python的worker子进程,这个子进程会反序列化每一行,拼成一个大list;最后利用 pandas.DataFrame.from_records() 从这个list来创建一个Pandas DataFrame。
上面的过程有两个明显问题:
1)即使使用CPickle,Python的序列化也是一个很慢的过程。
2)利用 from_records 来创建一个 pandas.DataFrame 需要遍历Python list,将每个value转换成Pandas格式。
Arrow可以优化这几个步骤:
1)一旦数据变成了Arrow的内存格式,就不再有序列化的需要,因为Arrow数据可以直接发送到Python进程。
2)当在Python里接收到Arrow数据后,pyarrow可以利用zero-copy技术,一次性的从整片数据来创建 pandas.DataFrame,而不需要轮询去处理每一行记录。另外转换成Arrow数据的过程可以在JVM里并行完成,这样可以显著降低driver的压力。
Arrow有点可以总结为:
* 序列化友好
* 向量化
序列化友好指的是,Arrow提供了一个内存格式,该格式本身是跨应用的,无论你放到哪,都是这个格式,中间如果需要网络传输这个格式,那么也是序列化友好的,只要做下格式调整(不是序列化)就可以将数据发送到另外一个应用里。这样就大大的降低了序列化开销。
向量化指的是,首先Arrow是将数据按block进行传输的,其次是可以对立面的数据按列进行处理的。这样就极大的加快了处理速度。
感兴趣的话可以看下Python各种序列化方案的对比:
http://satoru.rocks/2018/08/fastest-way-to-serialize-array/