Jupyter 在美团民宿的应用实践

阅读数:22 2019 年 11 月 30 日 08:00

Jupyter在美团民宿的应用实践

前言

做算法的同学对于 Kaggle 应该都不陌生,除了举办算法挑战赛以外,它还提供了一个学习、练习数据分析和算法开发的平台。Kaggle 提供了 Kaggle Kernels,方便用户进行数据分析以及经验分享。在 Kaggle Kernels 中,你可以 Fork 别人分享的结果进行复现或者进一步分析,也可以新建一个 Kernel 进行数据分析和算法开发。Kaggle Kernels 还提供了一个配置好的环境,以及比赛的数据集,帮你从配置本地环境中解放出来。Kaggle Kernels 提供给你的是一个运行在浏览器中的 Jupyter,你可以在上面进行交互式的执行代码、探索数据、训练模型等等。更多关于 Kaggle Kernels 的使用方法可以参考 Introduction to Kaggle Kernels ,这里不再多做阐述。

Jupyter在美团民宿的应用实践

对于比赛类的任务,使用 Kaggle Kernels 非常方便,但我们平时的主要任务还是集中在分析、处理业务数据的层面,这些数据通常比较机密并且数量巨大,所以就不能在 Kaggle Kernels 上进行此类分析。因此,大型的互联网公司非常有必要开发并维护集团内部的一套「Kaggle Kernels」服务,从而有效地提升算法同学的日常开发效率。

本文我们将分享美团民宿团队是如何搭建自己的「Kaggle Kernels」—— 一个平台化的 Jupyter,接入了大数据和分布式计算集群,用于业务数据分析和算法开发。希望能为有同样需求的读者带来一些启发。

美团内部数据系统现状

现有系统与问题

算法同学在离线阶段主要包含三类任务:数据分析、数据生产、模型训练。为满足这些任务的要求,美团内部也开发了相应的系统:

  1. 魔数平台:用于执行 SQL 查询,下载结果集的系统。通常在数据分析阶段使用。
  2. 协同平台:用于使用 SQL 开发 ETL 的平台。通常用于数据生产。
  3. 托管平台:用于管理和运行 Spark 任务,用户提供任务的代码仓库,系统管理和运行任务。通常用于逻辑较复杂的 ETL、基于 Spark 的离线模型训练 / 预测任务等。
  4. 调度平台:用于管理任务的依赖关系,周期性按依赖执行调度任务。

这些系统对于确定的任务完成的比较好。例如:当取数任务确定时,适合在魔数平台执行查询;当 Spark 任务开发就绪后,适合在托管平台托管该任务。但对于探索性、分析性的任务没有比较好的工具支持。探索性的任务有程序开发时的调试和对陌生数据的探查,分析性的任务有特征分析、Bad Case 分析等等。以数据探索为例,我们经常需要对数据进行统计与可视化,现有的做法通常是:魔数执行 SQL -> 下载 Excel -> 可视化。这种方式存在的问题是:

  1. 分析和取数工具割裂。
  2. 大数据分析可视化困难。

以 Bad Case 分析为例,现有的做法通常是:

Jupyter在美团民宿的应用实践

这种方式存在的问题是:

  1. 分析与取数割裂,整个过程需要较多的手工操作。
  2. 分析过程不容易复现,对于多人协作式的验证以及进一步分析不利。
  3. 本地 Python 环境可能与分析对象的依赖有冲突,需要付出额外精力管理 Python 环境。

离线数据相关任务的模式通常是取数(小数据 / 大数据)–> Python 处理(单机 / 分布式)–> 查看结果(表格 / 可视化)这样的循环。我们希望支持这一类任务的工具具有如下特质:

  1. 体验流畅:数据任务可以在统一的工具中完成,或者在可组合的工具链中完成。
  2. 体验一致:数据任务所用工具应该是一致的,不需要根据任务切换不同工具。
  3. 使用便捷:工具应是开箱即用,不需要繁琐的前置配置。
  4. 结果可复现:分析过程能够作为可执行代码保存下来,需要复现时执行即可,也应支持修改。

探索和分析类任务往往会带来可以沉淀的结果,如产生新的特征、模型、例行报告,希望可以建立起分析任务和调度任务的桥梁

我们需要怎样的 Jupyter

参考 Kaggle Kernels 的体验和开源 Jupyter 的功能,Notebook 方式进行探索分析具有良好的体验。我们计划定制 Jupyter,使其成为完成数据任务的统一工具。这个定制的 Jupyter 应具备以下功能:

  1. 接入 Spark:取数与分析均在 Jupyter 中完成,达到流畅、一致的体验。
  2. 接入调度系统:方便沉淀分析结果。
  3. 接入学城系统(内部 WiKi):方便分享和复现。
  4. 预配置环境:提供给用户开箱即用的环境。
  5. 用户隔离环境:避免用户间互相污染环境。

如何搭建 Jupyter 平台

Jupyter 项目架构

Project Jupyter 由多个子项目组成,通过这些子项目可以自由组合出不同的应用。子项目的依赖关系如下图所示:

Jupyter在美团民宿的应用实践

这个案例中,Jupyter 应用是一个 Web 服务,我们可以从这个维度来看 Jupyter 架构:

Jupyter在美团民宿的应用实践

Jupyter 扩展方式

整个 Jupyter 项目的模块化和扩展性上都非常优秀。上图中的 JupyterLab、Notebook Server、IPython、JupyterHub 都是可扩展的。

JupyterLab 扩展(labextension)

JupyterLab 是 Jupyter 全新的前端项目,这个项目有非常明确的扩展规范以及丰富的扩展方式。通过开发 JupyterLab 扩展,可以为前端界面增加新功能,例如新的文件类型打开 / 编辑支持、Notebook 工具栏增加新的按钮、菜单栏增加新的菜单项等等。JupyterLab 上的前端模块具有非常清楚的定义和文档,每个模块都可以通过插件获取,进行方法调用,获取必要的信息以及执行必要的动作。我们在提供分享功能、调度功能时,均开发了 JupyterLab 扩展。JupyterLab 扩展通常采用 TypeScript 开发,开发文档可参考: https://jupyterlab.readthedocs.io/en/stable/developer/extension_dev.html

Jupyter在美团民宿的应用实践

JupyterLab 核心组件依赖图

Notebook Server 扩展(serverextension)

Notebook Server 是用 Python 写的一个基于 Tornado 的 Web 服务。通过 Notebook Server 扩展,可以为这个 Web 服务增加新的 Handler。增加新的 Handler 通常有两种用途:

  1. 为 JupyterLab 扩展提供对应的后端接口,用于响应一些需要由服务端处理的事件。例如调度任务的注册需要通过 JupyterLab 扩展发起请求,由 Notebook Server 扩展执行。
  2. 提供一个前端界面以及对应的后端处理服务。例如 jupyter-rsession-proxy,用于在 JupyterHub 中使用 RStudio。

Notebook Server 扩展开发文档可参考:

https://jupyter-notebook.readthedocs.io/en/stable/extending/handlers.html

Jupyter Kernels

Jupyter 用于执行代码的模块叫 Kernel,除了默认的 ipykernel 以外,还可以有其他的 Kernel 用于支持其他编程语言。例如支持 Scala 语言的 almond 、支持 R 语言的 irkernel ,更多详见语言支持列表

IPython Magics

IPython Magics 就是那些 %、%% 开头的命令。常见的 Magics 有 %matplotlib inline,设置 Notebook 中调用 matplotlib 的绘图函数时,直接展示图表在 Notebook 中。执行 Magics 时,事实上是调用了该 Magics 定义的一个函数。对于 Line Magics(一个 %),传入函数的是当前行的代码;对于 Cell Magics(两个 %),传入的是整个 Cell 的内容。定义一个新的 IPython Magics 仅需定义一个函数,这个函数的入参有两个,一个是当前会话实例,可以用来遍历当前会话的所有变量,可以为当前会话增加新的变量;另一个是用户输入,对于 Line Magics 是当前行,对于 Cell Magcis 是当前 Cell。

IPython Magics 在简化代码方面非常有效,我们开发了 %%spark、%%sql 用于创建 Spark 会话以及 SQL 查询。另外很多第三方的 Magics 可以用来提高我们的开发效率,例如在开发 Word2Vec 变种时,使用 %%cython 来进行 Cython 和 Python 混合编程,省去编译加载模块的工作。

IPython Magics 开发文档可参考: https://ipython.readthedocs.io/en/stable/config/custommagics.html

IPython Widgets(ipywidgets)

IPython Widgets 是一种基于 Jupyter Notebook 和 IPython 的可交互控件。与普通可视化不同的是,在控件上的交互会触发和 Python 的通信并执行相应的代码,Python 上相应的动作也会触发界面实时变化。

IPython Widgets 在提供工具类型的功能增强上非常有用,基于它,我们实现了一个线上排序服务的调试和复现工具,用于展示排序结果以及指定房源在排序过程中的各种特征以及中间变量的值。IPython Widgets 的开发可以通过组合现有的 Widgets 实现,也可以完全自定义一个,IPython Widgets 开发文档可参考: https://ipywidgets.readthedocs.io/en/stable/examples/Widget Custom.html

Jupyter在美团民宿的应用实践

ipyleaflet

扩展 JupyterHub

Authenticators

JupyterHub 是一个多用户系统,登录模块可替换,通过实现新的 Authenticator 类并在配置文件中指定即可。通过这个扩展点,我们实现了使用内部 SSO 系统登录 JupyterHub。Authenticator 开发文档可参考: https://jupyterhub.readthedocs.io/en/stable/reference/authenticators.html

Spawners

当用户登录时,JupyterHub 需要为用户启动一个用户专用 Notebook Server。启动这个 Notebook Server 有多种方式:本机新的 Notebook Server 进程、本机启动 Docker 实例、K8s 系统中启动新的 Pod、YARN 中启动新的实例等等。每一种启动方式都对应一个 Spawner,官方提供了多种 Spawner 的实现,这些实现本身是可配置的。如果不符合需求,也可以自己开发全新的 Spawner。由于我们需要实现 Spark 接入,对 K8s 的 Pod 有新的要求,所以基于 KubeSpawner 定制了一个 Spawner 来解决 Spark 连接集群的网络问题。Spawner 开发文档可参考: https://jupyterhub.readthedocs.io/en/stable/reference/spawners.html

我们的定制

回顾我们的需求,这个定制的 Jupyter 应具备以下功能:

  1. 接入 Spark:可以通过配置容器环境以及 Spawner 完成。
  2. 接入调度系统:需要开发 JupyterLab 扩展以及 Notebook Server 扩展。
  3. 接入学城系统:需要开发 JupyterLab 扩展以及 Notebook Server 扩展。
  4. 预配置环境:镜像配置。
  5. 用户隔离环境:通过定制 Authenticators + K8s Spawner 实现容器级别环境隔离。

我们的方案是基于 JupyterHub on K8s。下图是平台化 Jupyter 的架构图,从上到下可以看到三条主线:1. 分享复现、2. 探索执行、3. 调度执行。

Jupyter在美团民宿的应用实践

几个关键组件介绍:

  1. JupyterLab:交互式执行的前端,开源项目。
  2. Jupyter Server:交互式执行的后端,开源项目。
  3. Commuter:浏览 Notebook 的工具,开源项目。
  4. K8s:容器编排系统,开源项目。
  5. Cantor:美团调度系统,同类开源项目有 AirFlow。
  6. 托管平台:美团离线任务托管平台,给定代码仓库和任务参数,为我们执行 Spark-Submit 的平台。
  7. 学城:美团文档系统。
  8. MSS:美团对象存储。
  9. NB-Runner:Notebook Runner,在 nbconvert 的基础上增加了参数化和 Spark 支持。

在定制 Jupyter 中,最为关键的两个是接入 Spark 以及接入调度系统,下文中将详细介绍这两部分的原理。JupyterHub on K8s 包括几个重要组成部分:Proxy、Hub、Kubernetes、用户容器(Jupyter Server Pod)、单点登录系统(SSO)。一个用户在登录后新建容器实例的过程中,这几个模块的交互如下图所示:

Jupyter在美团民宿的应用实践

可以看到,新建容器实例后,用户的交互都是经过 Proxy 后与 Jupyter Server Pod 进行通信。因此,扩展功能的工作主要是定制 Jupyter Server Pod 对应的容器镜像。

让 Jupyter 支持 Spark

Jupyter 平台化后,我们得到一个接近 Kaggle Kernel 的环境,但是还不能够使用大数据集群。接下来,就是让 Jupyter 支持 Spark,Jupyter 支持 Spark 的方案有 Toree ,出于灵活性考虑,我们没有使用。我们希望让普通的 Python Kernel 能支持 PySpark。为了能让 Jupyter 支持 Spark,我们需要了解两方面原理:Jupyter 代码执行原理和 PySpark 原理。

Jupyter 代码执行原理

所用到的 Jupyter 分三部分:前端 JupyterLab、服务端 Jupyter Server、语言 Kernel IPython。这三个模块的通信如下图所示:

Jupyter在美团民宿的应用实践

Jupyter 执行代码时序图

这里,需要在 IPython 的 exec 阶段支持 PySpark。

PySpark 原理

启动 PySpark 有两种方式:

  1. 方案一:PySpark 命令启动,内部执行了 spark-submit 命令。
  2. 方案二:任意 Python shell(Python、IPython)中执行 Spark 会话创建语句。

这两种启动方式有什么区别呢?

看一下 PySpark 架构图:

Jupyter在美团民宿的应用实践

PySpark 架构图,来自 SlideShare

与 Spark 的区别是,多了一个 Python 进程,通过 Py4J 与 Driver JVM 进行通信。

PySpark 方案启动流程

Jupyter在美团民宿的应用实践

PySpark 启动时序图
IPython 方案启动流程

Jupyter在美团民宿的应用实践

实际的 IPython 中启动 Spark 时序图

Toree 采用的是类似方案一的方式,脚本中调用 spark-submit 执行特殊版本的 Shell,内置了 Spark 会话。我们不希望这么做,是因为如果这样做的话就会:

  1. 多了一个 PySpark 专供的 Kernel,我们希望 Kernel 应该是统一的 IPython。
  2. PySpark 启动参数是固定的,配置在 kernel.json 里。希望 PySpark 任务是可以按需启动,可以灵活配置所需的参数,如 Queue、Memory、Cores。

因此我们采用方案二,只需要一些环境配置,就能顺利启动 PySpark。另外为了简化 Spark 启动工作,我们还开发了 IPython 的 Magics,%spark 和 %sql。

环境配置

为了让 IPython 中能够顺利启动起 Spark 会话,需要正确配置如下环境变量:

  • JAVA_HOME:Java 安装路径,如 /usr/local/jdk1.8.0_201。
  • HADOOP_HOME:Hadoop 安装路径,如 /opt/hadoop。
  • SPARK_HOME:Spark 安装路径,如 /opt/spark-2.2。
  • PYTHONPATH:额外的 Python 库路径,如 $SPARK_HOME/python:$SPARK_HOME/python/lib/py4j-0.10.4-src.zip。
  • PYSPARK_PYTHON:集群中使用的 Python 路径,如./ARCHIVE/notebook/bin/python。集群中使用 Python 通常需要虚拟环境,通过 spark.yarn.dist.archives 带上去。
  • PYSPARK_DRIVER_PYTHON:Spark Driver 所用的 Python 路径,如果你用 Conda 管理 Python 环境,那这个变量应为类似 /opt/conda/envs/notebook/bin/python 的路径。

为了方便,建议设置各 bin 路径到 PATH 环境变量中:$SPARK_HOME/sbin:$SPARK_HOME/bin:$HADOOP_HOME/sbin:$HADOOP_HOME/bin:$JAVA_HOME/bin:$PATH。完成这些之后,可以在 IPython 中执行创建 Spark 会话代码验证:

复制代码
import pyspark
spark = pyspark.sql.SparkSession.builder.appName("MyApp").getOrCreate()

在 Spark 任务中执行 Notebook

执行 Notebook 的方案目前有 nbconvert ,Python API 方式执行样例如下所示,暂时称这段代码为 NB-Runner.py:

复制代码
# Import:首先我们 import nbconvert 和 ExecutePreprocessor 类:
import nbformat
from nbconvert.preprocessors import ExecutePreprocessor
# 加载:假设 notebook_filename 是 notebook 的路径,我们可以这样加载:
with open(notebook_filename) as f:
nb = nbformat.read(f, as_version=4)
# 配置:接下来,我们配置 notebook 执行模式:
ep = ExecutePreprocessor(timeout=600, kernel_name='python')
# 执行 (preprocess):真正执行 notebook 的地方是调用函数 preprocess:
ep.preprocess(nb, {'metadata': {'path': 'notebooks/'}})
#保存:最后,我们保存 notebook 执行结果:
with open('executed_notebook.ipynb', 'w', encoding='utf-8') as f:
nbformat.write(nb, f)

现在有两个问题需要确认:

  1. 当 Notebook 中存在 Spark 相关代码时,Python NB-Runner.py 能否正常执行?
  2. 当 Notebook 中存在 Spark 相关代码时,Spark-Submit NB-Runner.py 能否正常执行?

之所以会出现问题 2,是因为我们的调度系统只能调度 Spark 任务,所以必须使用 Spark-Submit 的方式来启动 NB-Runner.py。为了回答这两个问题,需要了解 nbconvert 是如何执行 Notebook 的。

Jupyter在美团民宿的应用实践

nbconvert 执行时序图

问题 1 从原理上看,是可以正常执行的。实际测试也是如此。对于问题 2,答案似乎并不明显。结合“PySpark 启动时序图”、“实际的 IPython 中启动 Spark 时序图”与“nbconvert 执行时序图”:

Jupyter在美团民宿的应用实践

Spark-Submit NB-Runner.py 的方式存在问题的点可能在于,IPython 中执行 Spark.builder.getOrCreate 时,Driver JVM 已经启动并且 Py4J Gateway Server 已经实例化完成。如何让 Spark.builder.getOrCreate 执行时跳过上图“实际的 IPython 中启动 Spark 时序图”的 Popen(spark-submit) 以及后续的启动 Py4J Gateway Server 部分,直接与 Py4J Gateway Server 建立连接?

在 PySpark 代码中,看到如下这段代码:

复制代码
def launch_gateway(conf=None):
"""
launch jvm gateway
:param conf: spark configuration passed to spark-submit
:return:
"""
if "PYSPARK_GATEWAY_PORT" in os.environ:
gateway_port = int(os.environ["PYSPARK_GATEWAY_PORT"])
else:
SPARK_HOME = _find_spark_home()
# Launch the Py4j gateway using Spark's run command so that we pick up the
# proper classpath and settings from spark-env.sh
on_windows = platform.system() == "Windows"
script = "./bin/spark-submit.cmd" if on_windows else "./bin/spark-submit"
...

如果我们能在 IPython 进程中设置环境变量 PYSPARK_GATEWAY_PORT 为真实的 Py4J Gateway Server 监听的端口,就会跳过 Spark-Submit 以及启动 Py4J Gateway Server 部分。那么 PYSPARK_GATEWAY_PORT 从哪来呢?我们发现在 Python 进程中存在这个环境变量,只需要通过 ExecutorPreprocessor 将它传递给 IPython 进程即可。

使用案例

数据分析与可视化

数据探查和数据分析在这里都是同样的流程。用户要分析的数据通常存储在 MySQL 和 Hive 中。为了方便用户在 Notebook 中交互式的执行 SQL,我们开发了 IPython Magics %%sql 用来执行 SQL。

SQL Magics 的用法如下:

复制代码
%%sql <var> [--preview] [--cache] [--quiet]
SELECT field1, field2
FROM table1
WHERE field3 == field4

SQL 查询的结果暂存在指定的变量名 <var> 中,对于 MySQL 数据源 <var> 的类型是 Pandas DataFrame,对于 Hive 数据源 <var> 的类型是 Spark DataFrame。<var> 可用于需要对结果集进行操作的场合,如多维分析、数据可视化。目前,我们支持几乎所有的 Python 数据可视化库。下图是一个数据分析和可视化的例子:

Jupyter在美团民宿的应用实践

数据分析与可视化

Notebook 分享

Notebook 不仅支持交互式的执行代码,对于文档编辑也有不错的支持。数据分析过程中的数据、表格、图表加上文字描述就是一个很好的报告。Jupyter 服务还支持用户一键将 Notebook 分享到美团内部的学城中。一键分享:

Jupyter在美团民宿的应用实践

一键分享

上述数据分析分享到内部学城的效果如下图所示:

Jupyter在美团民宿的应用实践

Notebook 分享效果

模型训练

基于大数据的模型训练通常使用 PySpark 来完成。除了 Spark 内置的 Spark ML 可以使用以外,Jupyter 服务上还支持使用第三方 X-on-Spark 的算法,如 XGBoost-on-Spark、LightGBM-on-Spark。我们开发了 IPython Magics %%spark 来简化这个过程。

Spark Magics 的用法如下:

复制代码
%%spark
[--conf <property-name>=<property-value>]
[--conf <property-name>=<property-value>]
...

执行 %%spark 后,会启动 Spark 会话,启动后 Notebook 会话中会新建两个变量 spark 和 sc,分别对应当前 Spark 会话的 SparkSession 和 SparkContext。

下图是一个使用 LightGBM-on-Yarn 训练模型的例子,基于 Azure/mmlspark 官方 Notebook 例子,仅需添加启动 Spark 语句以及修改数据集路径。

Jupyter在美团民宿的应用实践

LightGBM on Spark Demo

排序策略调试

通过开发 ipywidgets 实现了一个线上排序策略的调试工具,可以用于查看排序结果以及排序原因(通过查看变量值)。

Jupyter在美团民宿的应用实践

总结与展望

通过平台化 Jupyter 的定制与部署,我们实现了数据分析、数据生产、模型训练的统一开发环境。在此基础上,还集成了内部公共服务和业务服务,从而实现了从数据分析到策略上线到结果分析的全链路支持。

我们对这个项目未来的定位是数据科学的云端集成开发环境,而 Jupyter 项目所具有的极强扩展性,也能够支持我们朝着这个方向不断进行演进。

作者介绍

文龙,美团民宿研发团队工程师。

颖艺,美团民宿研发团队工程师。

本文转载自公众号美团技术团队(ID:meituantech)。

原文链接

https://mp.weixin.qq.com/s/x9SlEvQj4DdYGKtQOKuslg

评论

发布