Jupyter在美團民宿的應用實踐

前言

做算法的同學對於Kaggle應該都不陌生,除了舉辦算法挑戰賽以外,它還提供了一個學習、練習數據分析和算法開發的平臺。Kaggle提供了Kaggle Kernels,方便用戶進行數據分析以及經驗分享。在Kaggle Kernels中,你可以Fork別人分享的結果進行復現或者進一步分析,也可以新建一個Kernel進行數據分析和算法開發。Kaggle Kernels還提供了一個配置好的環境,以及比賽的數據集,幫你從配置本地環境中解放出來。Kaggle Kernels提供給你的是一個運行在瀏覽器中的Jupyter,你可以在上面進行交互式的執行代碼、探索數據、訓練模型等等。更多關於Kaggle Kernels的使用方法可以參考 Introduction to Kaggle Kernels,這裏不再多做闡述。

對於比賽類的任務,使用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分析爲例,現有的做法通常是:

這種方式存在的問題是:

  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應用是一個Web服務,我們可以從這個維度來看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

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

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. 調度執行。

幾個關鍵組件介紹:

  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)。一個用戶在登錄後新建容器實例的過程中,這幾個模塊的交互如下圖所示:

可以看到,新建容器實例後,用戶的交互都是經過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執行代碼時序圖

這裏,需要在IPython的exec階段支持PySpark。

PySpark原理

啓動PySpark有兩種方式:

  1. 方案一:PySpark命令啓動,內部執行了spark-submit命令。
  2. 方案二:任意Python shell(Python、IPython)中執行Spark會話創建語句。

這兩種啓動方式有什麼區別呢?

看一下PySpark架構圖:

PySpark架構圖,來自SlideShare

與Spark的區別是,多了一個Python進程,通過Py4J與Driver JVM進行通信。

PySpark方案啓動流程

PySpark啓動時序圖
IPython方案啓動流程

實際的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的。

nbconvert執行時序圖

問題1從原理上看,是可以正常執行的。實際測試也是如此。對於問題2,答案似乎並不明顯。結合“PySpark啓動時序圖”、“實際的IPython中啓動Spark時序圖”與“nbconvert執行時序圖”:

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數據可視化庫。下圖是一個數據分析和可視化的例子:

數據分析與可視化

Notebook分享

Notebook不僅支持交互式的執行代碼,對於文檔編輯也有不錯的支持。數據分析過程中的數據、表格、圖表加上文字描述就是一個很好的報告。Jupyter服務還支持用戶一鍵將Notebook分享到美團內部的學城中。一鍵分享:

一鍵分享

上述數據分析分享到內部學城的效果如下圖所示:

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語句以及修改數據集路徑。

LightGBM on Spark Demo

排序策略調試

通過開發ipywidgets實現了一個線上排序策略的調試工具,可以用於查看排序結果以及排序原因(通過查看變量值)。

總結與展望

通過平臺化Jupyter的定製與部署,我們實現了數據分析、數據生產、模型訓練的統一開發環境。在此基礎上,還集成了內部公共服務和業務服務,從而實現了從數據分析到策略上線到結果分析的全鏈路支持。

我們對這個項目未來的定位是數據科學的雲端集成開發環境,而Jupyter項目所具有的極強擴展性,也能夠支持我們朝着這個方向不斷進行演進。

作者介紹

文龍,美團民宿研發團隊工程師。

穎藝,美團民宿研發團隊工程師。

本文轉載自公衆號美團技術團隊(ID:meituantech)。

原文鏈接

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章