仅单线程使用多处理池执行与PySpark的并行SQL查询

时间:2018-11-21 01:57:03

标签: python multithreading apache-spark pyspark netsuite

在某些情况下,我正在使用PySpark(如果无法使用Python而不是Spark,而需要使用Scala或Java,则使用Spark)从缺少主键的数百个数据库表中提取数据。 (为什么Oracle会创建一个包含具有主键表的ERP产品是一个不同的主题……但是,无论如何,我们都需要能够提取数据并将数据从每个数据库表保存到Parquet文件中。)尝试使用Sqoop代替PySpark,但是由于遇到了许多问题,因此改用PySpark / Spark更为有意义。

理想情况下,我想在计算集群中包含每个任务节点:获取表的名称,从数据库中查询该表,并将该表另存为S3中的Parquet文件(或Parquet文件集) 。我的第一步是使它在独立模式下本地运行。 (如果我对每个给定的表都有一个主键,那么我可以在给定表的不同行集之间划分查询和文件保存过程,并在计算集群中的任务节点之间分配行分区以执行文件保存操作并行,但是由于Oracle的ERP产品缺少关注表的主键,因此不是一种选择。)

我能够使用PySpark成功查询目标数据库,并且能够通过多线程成功将数据保存到Parquet文件中,但是由于某些原因,只有一个线程可以执行任何操作因此,发生的事情是只有一个线程获取一个tableName,查询数据库,并将该文件作为Parquet文件保存到所需的目录中。然后,该作业结束,就好像没有其他线程在执行一样。我猜测可能发生某种类型的锁定问题。 如果我正确理解了这里的评论:How to run multiple jobs in one Sparkcontext from separate threads in PySpark? 除非有与执行并行JDBC SQL查询有关的特定问题,否则我尝试做的应该是可能的。

编辑:我正在专门寻找一种方法,使我可以使用某种类型的线程池,这样就无需为每个要创建的表手动创建线程。我需要在集群中的所有任务节点上进行处理和手动负载均衡。

即使我尝试设置:

--master local[*]

--conf 'spark.scheduler.mode=FAIR'

问题仍然存在。

另外,为了简要解释我的代码,我需要使用自定义的JDBC驱动程序,并且我正在Windows的Jupyter笔记本中运行代码,因此我使用一种变通方法来确保PySpark以正确的参数开头。 (据记录,我对其他操作系统一无所知,但我的Windows计算机是我最快的工作站,所以这就是我使用它的原因。)

这是我的设置:

driverPath = r'C:\src\NetSuiteJDBC\NQjc.jar'
os.environ["PYSPARK_SUBMIT_ARGS"] = (
    "--driver-class-path '{0}' --jars '{0}' --master local[*] --conf 'spark.scheduler.mode=FAIR' --conf 'spark.scheduler.allocation.file=C:\\src\\PySparkConfigs\\fairscheduler.xml' pyspark-shell".format(driverPath)
)

import findspark
findspark.init()
from pyspark import SparkContext, SparkConf
from pyspark.sql import SparkSession, Column, Row, SQLContext
from pyspark.sql.functions import col, split, regexp_replace, when
from pyspark.sql.types import ArrayType, IntegerType, StringType

spark = SparkSession.builder.appName("sparkNetsuite").getOrCreate()
spark.sparkContext.setLogLevel("INFO")
spark.sparkContext.setLocalProperty("spark.scheduler.pool", "production")
sc = SparkContext.getOrCreate()

然后,为了测试多重处理,我在运行Jupyter笔记本的目录中创建了sparkMethods.py文件,并将此方法放入其中:

def testMe(x):
    return x*x

当我跑步时:

from multiprocessing import Pool
import sparkMethods

if __name__ == '__main__':
    pool = Pool(processes=4)              # start 4 worker processes

    # print "[0, 1, 4,..., 81]"
    print(pool.map(sparkMethods.testMe, range(10)))

在我的Jupyter笔记本中,我得到了预期的输出:

  

[0,1,4,9,16,25,36,49,64,81]

现在,在有人反对我编写下一个方法的方式之前,请知道我最初尝试通过闭包传递spark上下文,然后遇到了Pickling错误,如此处记录:I can "pickle local objects" if I use a derived class? 因此,我将所有Spark上下文包含在我放入sparkMethods.py文件的下一个方法中(至少直到找到更好的方法为止)。我将方法放入外部文件(而不是仅将其包含在Jupyter Notebook中)的原因是为了解决此问题:https://bugs.python.org/issue25053 如此处所述: Multiprocessing example giving AttributeError 和这里: python multiprocessing: AttributeError: Can't get attribute "abc"

此方法包含建立JDBC连接的逻辑:

# In sparkMethods.py file:
def getAndSaveTableInPySpark(tableName):
    import os
    import os.path
    from pyspark.sql import SparkSession, SQLContext
    spark = SparkSession.builder.appName("sparkNetsuite").getOrCreate()
    spark.sparkContext.setLogLevel("INFO")
    spark.sparkContext.setLocalProperty("spark.scheduler.pool", "production")

    jdbcDF = spark.read \
        .format("jdbc") \
        .option("url", "OURCONNECTIONURL;") \
        .option("driver", "com.netsuite.jdbc.openaccess.OpenAccessDriver") \
        .option("dbtable", tableName) \
        .option("user", "USERNAME") \
        .option("password", "PASSWORD") \
        .load()

    filePath = "C:\\src\\NetsuiteSparkProject\\" + tableName + "\\" + tableName + ".parquet"
    jdbcDF.write.parquet(filePath)
    fileExists = os.path.exists(filePath)
    if(fileExists):
        return (filePath + " exists!")
    else:
        return (filePath + " could not be written!")

然后,回到我的Jupyter笔记本中,运行:

import sparkMethods
from multiprocessing import Pool

if __name__ == '__main__':
    with Pool(5) as p:
        p.map(sparkMethods.getAndSaveTableInPySpark, top5Tables)

问题在于似乎只执行一个线程。

执行该命令时,在控制台输出中,我看到它最初包含以下内容:

  

该进程无法访问文件,因为该文件正在被另一个进程使用。   系统找不到文件   C:\ Users \ DEVIN〜1.BOS \ AppData \ Local \ Temp \ spark-class-launcher-output-3662.txt。   。 。 。

这使我怀疑也许正在发生某种类型的锁定。

无论如何,线程之一将始终成功运行以成功完成并成功查询其对应的表并将其保存到所需的Parquet文件中。该过程中存在一些不确定性,因为不同的执行结果将导致不同的线程赢得比赛并因此处理不同的表。 有趣的是,仅执行单个作业,如Spark UI中所示: Spark UI picture that shows that only one Spark Job was executed 但是,此处的文章:https://medium.com/@rbahaguejr/threaded-tasks-in-pyspark-jobs-d5279844dac0 表示如果成功启动,我应该期望在Spark UI中看到多个作业。

现在,如果问题是PySpark实际上不能跨不同的任务节点并行运行多个JDBC查询,那么也许我的解决方案是使用JDBC连接池,甚至只是为每个表打开一个连接(如只要我在线程末尾关闭连接)。 在处理表列表时,我成功通过jaydebeapi库连接到数据库,如下所示:

import jaydebeapi
conn = jaydebeapi.connect("com.netsuite.jdbc.openaccess.OpenAccessDriver",  
                          "OURCONNECTIONURL;", 
                          ["USERNAME", "PASSWORD"], 
                          r"C:\src\NetSuiteJDBC\NQjc.jar")

top5Tables = list(pd.read_sql("SELECT TOP 5 TABLE_NAME FROM OA_TABLES WHERE TABLE_OWNER != 'SYSTEM';", conn)["TABLE_NAME"].values)
conn.close()
top5Tables

输出为:

['SALES_TERRITORY_PLAN_PARTNER',
 'WORK_ORDER_SCHOOLS_TO_INSTALL_MAP',
 'ITEM_ACCOUNT_MAP',
 'PRODUCT_TRIAL_STATUS',
 'ACCOUNT_PERIOD_ACTIVITY']

因此,可以想象,如果问题是无法使用PySpark在这样的任务节点之间分配多个查询,那么也许我可以使用jaydebeapi库进行连接。但是,在那种情况下,我仍然需要一种方法来将JDBC SQL查询的输出写到Parquet文件中(理想情况下将利用Spark的模式推断功能),但是如果出现以下情况,我愿意采用这种方法这是可行的。

那么,如何在主节点不按顺序执行所有查询的情况下成功查询数据库并将输出并行保存(即分布在任务节点中)到Parquet文件中?

2 个答案:

答案 0 :(得分:0)

评论针对我的问题以及此处的答案提供了一些提示:How to run independent transformations in parallel using PySpark? 我调查了使用线程而不是多处理的情况。 我仔细研究了以下答案之一:How to run multiple jobs in one Sparkcontext from separate threads in PySpark? 并注意到以下用途:

from multiprocessing.pool import ThreadPool

我能够做到这一点,

from multiprocessing.pool import ThreadPool
pool = ThreadPool(5)
results = pool.map(sparkMethods.getAndSaveTableInPySpark, top5Tables)
pool.close() 
pool.join() 
print(*results, sep='\n')

打印:

C:\src\NetsuiteSparkProject\SALES_TERRITORY_PLAN_PARTNER\SALES_TERRITORY_PLAN_PARTNER.parquet exists!
C:\src\NetsuiteSparkProject\WORK_ORDER_SCHOOLS_TO_INSTALL_MAP\WORK_ORDER_SCHOOLS_TO_INSTALL_MAP.parquet exists!
C:\src\NetsuiteSparkProject\ITEM_ACCOUNT_MAP\ITEM_ACCOUNT_MAP.parquet exists!
C:\src\NetsuiteSparkProject\PRODUCT_TRIAL_STATUS\PRODUCT_TRIAL_STATUS.parquet exists!
C:\src\NetsuiteSparkProject\ACCOUNT_PERIOD_ACTIVITY\ACCOUNT_PERIOD_ACTIVITY.parquet exists!

答案 1 :(得分:0)

基本上,Spark会在后台进行并行化,并且不需要使用multiprocessing包,实际上,它可能会干扰Spark,并且完全没有必要。但是必须做一些事情才能利用这一优势。关键是先构建查询和转换,但不要执行任何Actions。另外,请确保您的Spark集群设置有多个工作节点,并将工作分配到该工作节点。一种简单的方法是使用DataBricks笔记本电脑或大型云供应商提供的其他服务,为您进行所有设置。

火花有两种模式。转换(不执行任何操作,而只是像SQL那样简单地设置查询和转换)。 ACTIONS实际上执行查询并对结果采取行动。 count()是一个动作。 show()是一个动作。查询是一个转换,表添加是一个转换。

要使用Spark内置的固有并行性,请在Spark中将几个查询和转换写入不同的表,但不要collect()count()show()的结果(不要执行此时的任何操作,仅转换)。这将在内部安排查询,但不会执行查询(这是延迟模式)。

然后在代码的稍后部分,当您运行操作(例如计数或显示或收集)时,它将自动将工作并行地分配到所有可用节点。那就是整个火花之美。您的本地设备上不需要特殊的多重处理,所有这些都由Spark处理。

这是一个pySpark示例:

    # First build the queries but don't collect any data.
        part1_sdf = spark.sql(
          "SELECT UtcTime, uSecDelay, sender, Recipient, date , ID "
          "FROM Delay_table "
          "WHERE date between DATE_ADD(now(), - 60) AND DATE_ADD(now(), -59) "
          "AND ID = 'my_id' "
          "ORDER BY UtcTime DESC "
        )
        part2_sdf = spark.sql(
          "SELECT UtcTime, uSecDelay, sender, Recipient, date, ID "
          "FROM Delay_table "
          "WHERE date between DATE_ADD(now(), -58) AND DATE_ADD(now(), -57) "
          "AND ID = 'my_id' "
          "ORDER BY UtcTime DESC "
        )
        # Peform a Transformation on the 2 queries.  No data is pulled up to this point
        transformed_df = part1_sdf.union(part2_sdf)
        # Finally when an action is called, the data is pulled in parallel:
        transformed_df.show(10)
        ### Output
        +--------------------------+--------------------------------------+-----------------+--------------------+----------+--------+
        |UtcTime|                   uSecDelay|                              
 sender|Recipient|                    date|                                 ID|
        +--------------------------+--------------------------------------+-----------------+--------------------+----------+--------+
        |      2020-01-05 01:39:...|                                    69|                4|                  28|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                    65|                4|                  26|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                    62|                4|                   0|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                   108|                4|                  16|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                    68|                4|                  27|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                    71|                4|                  53|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                    68|                4|                   7|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                    65|                4|                  57|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                    64|                4|                  56|2020-01-05|  my_id|
        |      2020-01-05 01:39:...|                                    66|                4|                  44|2020-01-05|  my_id|
        +--------------------------+--------------------------------------+-----------------+--------------------+----------+--------+
        only showing top 10 rows