zhs007 / pyspark.demo

pyspark demo

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

pyspark.demo

这是我用来测试Spark的例子,0基础开始。
不会花时间在接口和语法上,自己去官网查文档,那个是最靠谱的。
希望能从这些例子上,你能对spark有足够的认识。

之所以写这个项目,是因为不管是google还是百度,很多能搜到的关于这方面的文章都不太完善(特别是中文的)。
当然,最终还是要靠大家自己多动手尝试,我也只能保证在当下的版本,这些例子里的代码、结论、实现方案,都是验证过的。

语言选型

选择的python,是因为都是接口调用,具体运算逻辑被封装到底层去了,语言层面效率差别其实不大。
但如果你有非常多的复杂运算放在python层,其实还是会有影响的。
因此,技术选型还是得看整体项目规划。

python和java、scale有一些差别,在部署上,最主要的是python不支持cluster模式,只能用默认的client方式。

运行环境搭建

我不太喜欢污染本地环境,所以提交了docker项目,建议使用docker,后面单机开多节点也方便些。

之所以没有直接用bde2020的源,是因为我需要一些通用依赖,譬如mysql、numpy、pandas等,每个app都自己装依赖太麻烦了(numpy、pandas的安装非常非常慢......)。
如果你的需求不一样,做法应该也会有少许差别。

注意,按 bde2020 官方脚本,实际上使用的是 python3 (虽然系统默认的还是 python2.7 )。
个人觉得这个方案比网上很多改系统默认python版本号要好。

下面是基本的使用步骤:

  1. 进入docker\pythonapplocal目录,执行builddocker.sh文件。
sh builddocker.sh
  1. 只启动master,使用startdocker.sh即可。
sh startdocker.sh
  1. 进入容器bash。

  2. 用下面的脚本来启动脚本,才能在webui里看到数据。

PYSPARK_PYTHON=python3 /spark/bin/spark-submit --master spark://spark-master:7077 main.py
  1. 这时,应该可以通过webui来看到进展了。
    注意,我在启动脚本里将8080映射到了3722。

最后,如果不是经常改Dockerfile,就不需要重启docker,那样只需要启动脚本即可。

注意:

  • python脚本里,如果要提到到master运行,创建的时候不能填local。
# 这样是不行的
sc = SparkContext("local", "mysql app")

# 这样才是正确的
sc = SparkContext(appName="retention rate app")
  • 也可以使用docker里的master、worker、pythonapp这样开启3个容器来使用。
    一个个的build、start即可。
    也可以用 docker-compose

(此处应该有张结构图)

  • python依赖可以不用每个容器都装,submit的装好即可(因为python没有cluster模式)。

  • 在docker下,如果有类似这样的报错 java.io.IOException: Failed to connect to b924c66ac091:34708
    本质上是因为worker需要和submit通信,但由于submit也是docker的,hostname默认给的是容器ID。

解决方案很多,和部署关系很大。
我提一个思路,submit节点其实是知道spark-master的,而和spark-master节点通信的ip地址,应该是可以被worker访问到的,所以通过spark-master获得ip地址,写入spark.driver.host即可。

import socket


def getHostIP():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('spark-master', 8080))
        ip = s.getsockname()[0]
    finally:
        s.close()

    return ip


myip = getHostIP()

spark = SparkSession.builder.appName("rdd basic").config(
    "spark.driver.host", myip).getOrCreate()
  • 如果executor报错,docker logs是查不到日志的,需要到 /spark/work/ 里查日志。
    这个日志其实在webui上也能看到,但前提是那些端口要映射出来。

单机快速测试

单机模式下,其实submit时不传入remote参数即可,但 bde2020 库里没有给这种方案,如果我们不映射 master ,会报错。
所以我单独写了个 local 的底包。

步骤如下:

  1. 进入docker\pythonapplocal目录,执行builddocker.sh文件。
sh builddocker.sh
  1. 参照startdockerrr.sh,执行具体的app即可。

注意:

如果这样启动,基本上就只能查日志了,因为结束后,webui是会被关闭掉的。

熟悉环境和基本操作 -- rddbasic

这个项目很简单,最基本的rdd使用,把docker架设好,就可以运行python脚本。

主要是用来熟悉环境和基本操作的。

基本的SQL -- mysqlbasic

这里多了一个mysql操作,如果用我提供的docker,环境应该就是正常的,只需要配置一个mysql实例即可,建议用mysql5(我们线上环境还是mysql5......)。
如果是mysql8的话,mysql connector需要换成8.x版。可以自行下载,然后放到docker目录下,重新build即可。

spark的load其实并没有把数据全部读出来,就算这个例子实际的表是一张5m的表,如果我们仅仅只是count(),其实并不会很慢,大概50s左右(我们直接sql语句里count,只要0.01s),但如果我们要做一些复杂操作,譬如select某一列,再distinct,大概需要8分钟(而这种操作,放在sql语句里,只需要6s)。

(此处应该有张表格)

因此,spark会做一定的优化,但没有那么智能,所以,能自己做的优化,还是得自己做。

原则上,不需要读的数据不要读出来。

最后,关于写回mysql的一些问题:

  • 如果是overwrite模式,spark应该是会删除表的,而不会行覆盖
  • 如果是append模式,且目标表已建立,有自增长字段,那么只要dataframe里没有这个字段,其实一切正常的。
  • 如果是append模式,目标表已建立,只要字段不冲突,表里面一些自动值的列,譬如插入时间等,也都是正常的。

这一部分,建议多改下代码,自己多试试,网上很多关于自增长ID的处理是错误的(实践出真知)。

用户留存统计 -- retentionrate

统计用户留存率。
每天一张mysql表,但由于服务器之间时间没有绝对同步,所以跨天时,少量数据会存到错误的表里去,这时需要读取3张表才能确定最终的数据。

    yesterday = daytime - timedelta(days=1)
    tomorrow = daytime + timedelta(days=1)

    sqlstr1 = "(SELECT distinct(uid) as uid FROM gamelog6_api_%s WHERE curtime >= '%s') tmp" % (
        daytime.strftime("%y%m%d"), daytime.strftime("%Y-%m-%d"))
    df1 = ctx.read.format("jdbc").options(url=cfg['mysql']['host'],
                                          driver="com.mysql.jdbc.Driver",
                                          dbtable=sqlstr1,
                                          user=cfg['mysql']['user'],
                                          password=cfg['mysql']['password']).load()

    sqlstr2 = "(SELECT distinct(uid) as uid FROM gamelog6_api_%s WHERE curtime >= '%s') tmp" % (
        yesterday.strftime("%y%m%d"), daytime.strftime("%Y-%m-%d"))
    df2 = ctx.read.format("jdbc").options(url=cfg['mysql']['host'],
                                          driver="com.mysql.jdbc.Driver",
                                          dbtable=sqlstr2,
                                          user=cfg['mysql']['user'],
                                          password=cfg['mysql']['password']).load()

    sqlstr3 = "(SELECT distinct(uid) as uid FROM gamelog6_api_%s WHERE curtime < '%s') tmp" % (
        tomorrow.strftime("%y%m%d"), tomorrow.strftime("%Y-%m-%d"))
    df3 = ctx.read.format("jdbc").options(url=cfg['mysql']['host'],
                                          driver="com.mysql.jdbc.Driver",
                                          dbtable=sqlstr3,
                                          user=cfg['mysql']['user'],
                                          password=cfg['mysql']['password']).load()

在这个例子里,有4种不同的写法,结果如下:

    # 无cache,耗时23分30秒
    df1 = df1.union(df2)
    df1 = df1.union(df3)
    df1 = df1.distinct()

    # 结果cache,耗时3分
    df1 = df1.union(df2)
    df1 = df1.union(df3)
    df1 = df1.distinct()
    df1.cache()

    # 加cache且一行写完,耗时3分
    df1 = df1.union(df2).union(df3).distinct().cache()

    # 前面df1、df2、df3都在load以后cache,结束后unpersist
    # 这里属于过度cache,其实效率也没区别
    df1e = df1.union(df2).union(df3).distinct().cache()
    df1.unpersist()
    df2.unpersist()
    df3.unpersist()

这里之所以最后cache,是因为最后的结果后面还有用,而最初的3个dataframe其实后面就没用处了。
结论是 cache 非常重要,而具体调用写法没影响。
至于何时需要cache,第4种写法是个反例,建议自己多尝试体会一下。

因为spark是一个分布式运算框架,出于任务分派的考虑,实际上是在python层构建一个控制链,然后提交到不同的worker里去运行,如果没有cache,其实每个任务都是从头到尾顺序执行的,加了cache,可以由开发者来决策哪些步骤是可缓存的,整个实现方案会简单很多。

(此处应该有张结构图)

About

pyspark demo

License:MIT License


Languages

Language:Python 84.2%Language:Shell 11.5%Language:Dockerfile 4.3%