一文了解 DataLeap 中的 Notebook

陶然陶然發表於2022-11-01

   一、概述

  Notebook 是一種支援 REPL 模式的開發環境。所謂「REPL」,即「讀取-求值-輸出」迴圈:輸入一段程式碼,立刻得到相應的結果,並繼續等待下一次輸入。它通常使得探索性的開發和除錯更加便捷。在 Notebook 環境,你可以互動式地在其中編寫你的程式碼、執行程式碼、檢視輸出、視覺化資料並檢視結果,使用起來非常靈活。

  在資料開發領域,Notebook 廣泛應用於資料清理和轉換、數值模擬、統計建模、資料視覺化、構建和訓練機器學習模型等方面。

  但是顯然,做資料開發,只有 Notebook 是不夠的。在火山引擎 DataLeap 資料研發平臺,我們提供了任務開發、釋出排程、監控運維等一系列能力。我們將 Notebook 作為一種任務型別,加入了資料研發平臺,使使用者既能擁有 Notebook 互動式的開發體驗,又能享受一站式大資料研發治理套件提供的便利。如果還不夠直觀的話,試想以下場景:

  在互動式執行和視覺化圖表的加持下,你很快就除錯完成了一份 Notebook。簡單整理了下程式碼,根據使用到的資料配置了上游任務依賴,上線了週期排程,並順手掛了報警。之後,基本上就不用管這個任務了:不需要每天手動檢查上游資料是否就緒;不需要每天來點選執行,因為排程系統會自動幫你執行這個 Notebook;執行失敗了有報警,可以直接上平臺來處理;上游資料出錯了,可以請他們發起深度回溯,統一修數。

   二、選型

  2019 年末,在決定要支援 Notebook 任務的時候,我們調研了許多 Notebook 的實現,包括 Jupyter、Polynote、Zeppelin、Deepnote 等。Jupyter Notebook 是 Notebook 的傳統實現,它有著極其豐富的生態以及龐大的使用者群體,相信許多人都用過這個軟體。事實上,在位元組跳動資料平臺發展早期,就有了在物理機叢集上統一部署的 Jupyter(基於多使用者方案 JupyterHub),供內部的使用者使用。考慮到使用者習慣和其強大的生態,Jupyter 最終成為了我們的選擇。  

  Jupyter Notebook 是一個 Web 應用。通常認為其有兩個核心的概念:Notebook 和 Kernel。

  Notebook 指的是程式碼檔案,一般在檔案系統中儲存,字尾名為ipynb。Jupyter Notebook 後端提供了管理這些檔案的能力,使用者可以透過 Jupyter Notebook 的頁面建立、開啟、編輯、儲存 Notebook。在 Notebook 中,使用者以一個一個 Cell 的形式編寫程式碼,並按 Cell 執行程式碼。Notebook 檔案的具體內容格式,可參考 The Notebook file format ()。

  Kernel 是 Notebook 中的程式碼實際的執行環境,它是一個獨立的程式。每一次「執行」動作,產生的效果是單個 Cell 的程式碼被執行。具體來講,「執行」就是把 Cell 內的程式碼片段,透過 Jupyter Notebook 後端以特定格式傳送給 Kernel 程式,再從 Kernel 接受特定格式的返回,並反饋到頁面上。這裡所說的「特定格式」,可參考 Messaging in Jupyter ()。

  在 DataLeap 資料研發平臺,開發過程圍繞的核心是任務。使用者可以在專案下的任務開發目錄建立子目錄和任務,像 IDE 一樣透過目錄樹管理其任務。Notebook 也是一種任務型別,使用者可以啟動一個獨立的任務 Kernel 環境,像開發其他普通任務一樣使用 Notebook。  

   三、技術路線

  在 Jupyter 的生態下,除了 Notebook 本身,我們還注意到了很多其他元件。彼時,JupyterLab 正在逐漸取代傳統的 Jupyter Notebook 介面,成為新的標準。JupyterHub 使用廣泛,是多使用者 Notebook 的版本答案。脫胎於 Jupyter Kernel Gateway(JKG)的 Enterprise Gateway(EG),提供了我們需要的 Remote Kernel(上述的獨立任務 Kernel 環境)能力。2020 上半年,我們基於上面的三大元件,進行二次開發,在位元組跳動資料研發平臺釋出了 Notebook 任務型別。整體架構預覽如圖。  

  JupyterLab

  前端這一側,我們選擇了基於更現代化的 JupyterLab () 進行改造。我們刨去了它的周邊檢視,只留下了中間的 Cell 編輯區,嵌入了 DataLeap 資料研發的頁面中。為了和 DataLeap 的視覺風格更契合,從 2020 下半年到 2021 年初,我們還針對性地改進了 JupyterLab 的 UI。這其中包括將整個 JupyterLab 使用的程式碼編輯器從 CodeMirror 統一到 DataLeap 資料研發使用的 Monaco Editor,同時還接入了 DataLeap 提供的 Python & SQL 程式碼智慧補全功能。

  額外地,我們還開發了定製的視覺化 SDK,使得使用者在 Notebook 上計算得到的 Pandas Dataframe 可以接入 DataLeap 資料研發已經提供的資料結果分析模組,直接在 Notebook 內部做一些簡單的資料探查。

  JupyterHub

  JupyterHub() 提供了可擴充套件的認證鑑權能力和環境建立能力。首先,由於使用者較多,因此為每個使用者提供單獨的 Notebook 例項不太現實。因此我們決定,按 DataLeap 專案來切分 Notebook 例項,同專案下的使用者共享一個例項(即一個專案實際上在 JupyterHub 是一個使用者)。這也與 DataLeap 的專案許可權體系保持了一致。注意這裡的「Notebook 例項」,在我們的配置下,是拉起一個執行 JupyterLab 的環境。另外,由於我們會使用 Remote Kernel,所以在這個環境內,並不提供 Kernel 執行的能力。

  在認證鑑權方面,我們讓 JupyterHub 請求我們業務後端提供的驗證介面,判斷登入態的使用者是否具備請求的對應 DataLeap 專案的許可權,以實現許可權體系對接。

  在環境建立方面,我們透過 OpenAPI 對接了位元組跳動內部的 PaaS 服務,為每一個使用了 Notebook 任務的 DataLeap 專案分配一個 JupyterLab 例項,對應一個 PaaS 服務。由於直接新建一個服務的流程較長,速度較慢,因此我們還額外做了池化,預先啟動一批服務,當有新專案的使用者登入時直接分配。

  Enterprise Gateway

  Jupyter Enterprise Gateway () 提供了在分散式叢集(包括 YARN、Kubernetes 等)內部啟動 Kernel 的能力,併成為了 Notebook 到叢集內 Kernel 的代理。在原生的 Notebook 體系下,Kernel 是 Jupyter Notebook / JupyterLab 中的一個本地程式;對於啟用了 Gateway 功能的 Notebook 例項,所有 Kernel 相關的功能的請求,如獲取 Kernel 型別、啟動 Kernel、執行 Cell、中斷等,都會被代理到指定的 Gateway 上,再由 Gateway 代理到具體叢集內的 Kernel 裡,形成了 Remote Kernel 的模式。

  這樣帶來的好處是,Kernel 和 Notebook 分離,不會相互影響:例如某個 Kernel 執行佔用實體記憶體超限,不會導致其他同時執行的 Kernel 掛掉,即使他們都透過同一個 Notebook 例項來使用。  

  EG 本身提供的 Kernel 型別,和位元組跳動內部系統並不完全相容,需要我們自行修改和新增。我們首先以 Spark Kernel 的形式對接了位元組跳動內部的 YARN 叢集。Kernel 以 PySpark 的形式在 Cluster 模式的 Spark Driver 執行,並提供一個預設的 Spark Session。使用者可以透過在 Driver 上的 Kernel,直接發起執行 Spark 相關程式碼。同時,為了滿足 Spark 使用者的使用習慣,我們額外提供了在同一個 Kernel 內交叉執行 SQL 和 Scala 程式碼的能力。

  2020 下半年,伴隨著雲原生的浪潮,我們還接入了位元組跳動雲原生 K8s 叢集,為使用者提供了 Python on K8s 的 Kernel。我們還擴充套件了很多自定義的能力,例如支援自定義映象,以及針對於 Spark Kernel 的自定義 Spark 引數。

  穩定性方面,在當時的版本,EG 存在非同步不夠徹底的問題,在 YARN 場景下,單個 EG 程式甚至只能跑起來十幾個 Kernel。我們發現了這一問題,並完成了各處所需的 async 邏輯改造,保證了服務的併發能力。另外,我們利用了位元組跳動內部的負載均衡(nginx 七層代理叢集)能力,部署多個 EG 例項,並指定單個 JupyterLab 例項的流量總是打到同一個 EG 例項上,實現了基本的 HA。

   四、架構升級

  當使用 Notebook 的專案日漸增加時,我們發現,執行中的 PaaS 服務實在太多了,之前的架構造成了

  部署麻煩。全量升級 JupyterLab 較為痛苦。儘管有升級指令碼,但是透過 API 操作升級服務,可能由於映象構建失敗等原因,會造成卡單現象,因此每次全量升級後都是人工巡檢檢查升級狀態,卡住的升級單人工點選下一步。同時由於升級不同服務不會複用配置相同的映象,所以有多少服務就要構建多少次映象,當服務數量達到一定量級時,我們的批次升級請求可能把內部映象構建服務壓垮。

  JupyterLab 需要不斷的根據使用者增長(專案增長)進行擴容,一旦預先啟動好的資源池不夠,就會存在新專案裡有使用者開啟 Notebook,需要經歷整個 JupyterLab 服務建立、環境拉起的流程,速度較慢,影響體驗。而且,JupyterLab 數量巨大後,遇到 bad case 的機率增高,有些問題不易復現、非常偶發,重啟/遷移即可解決,但是在遇到的時候,使用者體驗受影響較大。

  運維困難。當使用者 JupyterLab 可能出現問題,為了找到對應的 JupyterLab,我們需要先根據專案對應到 JupyterHub user,然後根據 user 找到 JupyterHub 記錄的服務 id,再去 PaaS 平臺找服務,進 webshell。

  當然,還有資源的浪費。雖然每個例項很小(1c1g),但是數量很多;有些專案並不總是在使用 Notebook,但 JupyterLab 依然執行。

  穩定性存在問題。一方面,JupyterHub 是一個單點,升級需要先起後停,掛了有風險。另一方面,EG 入流量經過特定負載均衡策略,本身是為了使 JupyterLab 固定往一個 EG 請求。在 EG 升級時,JupyterLab 請求的終端會隨之改變,極端情況下有可能造成 Kernel 啟動多次的情況。

  基於簡化運維成本、降低架構複雜性,以及提高使用者體驗的考慮,2021 上半年,我們對整體架構進行了一次改良。在新的架構中,我們主要做了以下改進,大致簡化為下圖:

  移除 JupyterHub,將 JupyterLab 改為多例項無狀態常駐服務,並實現對接 DataLeap 的多使用者鑑權。

  改造原本落在 JupyterLab 本地的資料儲存,包括使用者自定義配置、Session 維護和程式碼檔案讀寫。

  EG 支援持久化 Kernel,將 Kernel 遠端環境元資訊持久化在遠端儲存(MySQL)上,使其重啟時可以重連,且 JupyterLab 可以知道某個 Kernel 需要透過哪個 EG 連線。  

  鑑權 & 安全

  單使用者的 Jupyter Notebook / JupyterLab 的鑑權相對簡單(實際上 JupyterLab 直接複用了 Jupyter Notebook 的這套程式碼)。例如,使用預設命令啟動時,會自動生成一個 token,同時自動拉起瀏覽器。有了 token,就可以任意地訪問這個 Notebook。

  事實上,JupyterHub 也是起到了維護 token 的作用。前端會發起一個獲取 token 的 API 請求,再拿著獲取的 token 請求透過 JupyterHub proxy 到真實的 Notebook 例項。而我們直接為 Jupyter Notebook 增加了 Auth 的功能,實現了在 JupyterLab 單例項上完成這套鑑權(此時,使用了 DataLeap 服務簽發的 Token)。  

  最後,由於所有使用者會共享同一組 JupyterLab,我們還需要禁止一些介面的呼叫,以保證系統的安全。最典型的介面包括關閉服務(Shutdown),以及修改配置等。後續 Notebook 所需的配置,轉由前端儲存在瀏覽器內。

  程式碼 & Session 持久化

  Jupyter Notebook 使用 File Manager() 管理 Contents 相關讀寫(對我們而言主要是 Notebook 程式碼檔案),原生行為是將程式碼儲存在本地,多個服務例項之間無法共享同一份程式碼,而且遷移時可能造成程式碼丟失。

  為了避免程式碼丟失,我們的做法是,把程式碼按專案分別儲存在 OSS 上並直接讀寫,同時解決了一些由於程式碼檔案元資訊丟失,併發編輯導致的其他問題。例如,當多個頁面訪問同一份程式碼檔案時,都會從 OSS 獲取新的 code,當使用者儲存時,前端會獲取新的程式碼檔案,比較該檔案的修改時間同前端儲存的是否一致,如果不同,則說明有其它頁面儲存過,會提示使用者選擇覆蓋或是恢復。  

  Notebook 使用 Session 管理使用者到 Kernel 的連線,例如前端透過 POST /session 介面啟動 Kernel,GET /session 檢視當前執行中的 Kernel。在 Session 處理方面,原生的 Notebook 使用了原生的 sqlite(in memory),見程式碼()。儘管我們並不明白這麼做的意義何在(畢竟原生的 Notebook 重啟,一切都沒了),但我們順著這個原生的表結構繼續前進,引入了 sqlalchemy 對接多種資料庫,將 Session 資料搬到了 MySQL。  

  另一方面,由於我們啟動的 Kernel,有一部分涉及 Spark on YARN,啟動速度並不理想,因此早期我們增加了功能,若某個 path 已有正在啟動的 Kernel,則等其啟動完畢而不是再啟動一個新的。這個功能原先使用記憶體中的 set 實現,現在也移植到了資料庫上,透過 sqlalchemy 來訪問。

  Kernel 持久化 & 訪問

  在 Remote Kernel 的場景下,一個 JupyterLab 需要知道它的某個 Kernel 具體在哪個 EG 上。在之前一個專案一個 JupyterLab 的狀態下,我們透過負載均衡簡單處理這個問題:即一個 Server 總是隻訪問同一個 Gateway。然而當 JupyterLab 成為無狀態服務時,使用者並非固定只訪問一個 JupyterLab,也就不能保證總訪問使用者 Kernel 所在的 EG。

  另一個情況是,當 JupyterLab 或 EG 重啟時,其上的 Kernel 都會關閉。當我們升級相關服務時,總是需要通知使用者準備重啟 Kernel。因此,為了實現升級對使用者無感,我們在 EG 這層開發了持久化 Kernel 的特性。

  Kernel Gateway 在啟動 Kernel 時,記錄了關於 Kernel 的一些元資訊,包括啟動引數、連線 Kernel 使用的 IP/Port 等。有了這些資訊,當一個 Kernel Gateway 重啟且 Remote Kernel 不關閉,就有辦法重新連線上。原本這些資訊預設在記憶體 dict 中維護,開源倉庫中有一套儲存在本地檔案的方案;基於這套方案,我們擴充套件了自研的儲存到 MySQL 的方案。

  在多例項的場景下,每一個 EG 例項依然會接管的各自的一部分 Kernel,並記錄每個 Kernel 由誰接管(探活、Cull Idle、連線使用等)。在其關閉前,需要清除接管資訊,以便下次啟動或其他例項啟動時撈起。

  為了減少 client(正常是 JupyterLab) 任意訪問 EG 的情況,一方面我們沿用了負載均衡的策略,另一方面 JupyterLab 在請求 Kernel 相關操作前,會先請求 EG 一次,由 EG 決定 JupyterLab 具體請求哪一個 EG IP/Port。  

  當 EG 服務本身重啟或者升級時,會在程式退出之前去清除接管資訊。當頁面繼續訪問時,JupyterLab 服務將會隨機分發相應請求,由其它的 EG 服務繼續接管。

  收益

  架構升級簡化後,整套 Notebook 服務的穩定性獲得了極大的提升。由於實現了使用者無感知的升級,不僅提升了使用者的使用體驗,運維的成本也同時降低了。

  部署的成本也極大地降低,包括算力、人力的節省。由於剝離了內部依賴,我們得以將這套架構部署在各種公有云、私有化場景。

   五、排程方案

  在前面,我們重點關注了怎麼將 Jupyter 這套應用嵌入到 DataLeap 資料研發中。這隻覆蓋了我們 Notebook 任務的頁面除錯功能。實際上,同時作為一個排程系統,我們還需要關心怎麼排程一個 Notebook 任務。

  首先,是和所有其他任務型別相同的部分:當 Notebook 任務所配置的上游依賴任務全部執行完畢,開始拉起本次 Notebook 任務的執行。我們會根據任務的版本建立一個任務的快照,我們稱之為任務例項,並將其提交到我們的執行器中。

  對於 Notebook 任務,在例項執行前,我們會根據 Notebook 任務對應的版本,從 OSS 複製一份 Notebook 程式碼檔案,用於執行。在具體的執行流程中,我們使用了 Jupyter 生態中的 nbconvert () 來實現在沒有 Jupyter 應用的前提下在後臺執行這份 Notebook 檔案,並將執行後得到的結果 Notebook 檔案傳回 OSS。nbconvert 的工作原理比較簡單,且複用了 Jupyter 底層的程式碼,具體如下:

  根據指定的 Kernel Manager 或 Notebook 檔案裡的 Kernel 型別建立對應的 Kernel Manager ();

  Kernel Manger 建立 Kernel Client,並啟動一個 Kernel;

  遍歷 Notebook 檔案裡的 Cell,呼叫 Kernel Client 執行 Cell 裡的程式碼;

  獲取輸出結果,按照 nbformat 指定的 schema 填入 NotebookNode,並儲存。

  下圖是排程執行 Notebook 的 Kernel 執行流程和透過除錯走 EG 的 Remote Kernel 執行流程對比。可以看出,它們的鏈路並沒有本質上的區別,只不過是在排程執行時,不需要互動式的 Kernel 通訊,以及 EG 的這些 Kernel Launcher 使用了 embed_kernel 在同程式內啟動 Kernel 而已。走到最底層,它們都是使用了 ipykernel 的(其他語言 kernel 同理)。  

   六、未來工作

  Notebook 任務已成為位元組跳動內部使用較為高頻的任務型別。在火山引擎,我們也可以購買 DataLeap,即一站式大資料研發治理套件,開通互動式分析的版本,使用到 DataLeap 的 Notebook 任務。

  有的時候,我們發現,我們有比 Jupyter 社群快半步的地方:比如基於 asyncio 非同步最佳化的 EG;比如給 Notebook 增加 Auth 能力。但社群的發展也很快:比如社群將 Jupyter 後端相關的程式碼實現,統一收斂到了jupyter_server;比如 EG 作者提出的 Kernel Provider 方案,令jupyter_server可以直接支援 Remote Kernel。

  因此我們並未就此止步。目前,這套 Notebook 服務和 DataLeap 資料研發的其他前後端服務,仍存在著割裂。未來,我們希望精簡架構,實現徹底的整合,使 Notebook 並非以嵌入的形式融合在 DataLeap 的產品中,而是使其原生就在 DataLeap 資料研發中被支援,帶來更好的效能,同時又保留所有 Jupyter 生態帶來的強大功能。另一方面,隨著 DataLeap 資料研發平臺對流式資料開發的支援,我們也希望藉助 Notebook 實現使用者對流式資料的探索、除錯、視覺化等功能的需求。相信不久的將來,Notebook 能夠實現流批一體化,來服務更加廣泛的使用者群體。

來自 “ 位元組跳動技術團隊 ”, 原文作者:DataLeap ccw;原文連結:http://server.it168.com/a2022/1027/6770/000006770604.shtml,如有侵權,請聯絡管理員刪除。

相關文章