Tubi 内部统一使用 Jupyter Notebook 作为数据分析和数据科学工作的平台。不论是探索用户行为,时间序列预测公司 API,还是深度目标检测模型,我们都是在一个基于 JupyterHub [1]且深度定制的平台上完成的,我们称之为 Tubi Data Runtime (TDR)。
在前一篇文章中我们提到了 TDR,此文将提供更多细节,包括优化的数据连接、自定义 Jupyter cell magic 和 JupyterLab 扩展 [2],以及领域特有的可视化工具。接下来的文章,我们会介绍 JupyterHub 在 Kubernetes 上的一些经验。
优化过的数据连接器
对于大部分 Jupyter 用户来说,他们打开 notebook 的第一步操作往往是获取数据。Tubi 的大部分数据源来自 AWS Redshift 和 S3。在分析完我们使用 Redshift 的查询习惯和历史 [3]之后,我们发现 95% 的查询返回的数据大小在 1GB 以内,而这正适合放在内存操作。而在 TDR 之前,即使获取同样大小的数据也需要花费更多时间。主要原因是我们需要启动 Spark 集群,接着执行 Redshift 语句,然后从 Spark DataFrame 转换成 Pandas DataFrame,再之后的操作都是在 Pandas 内。这个过程极其浪费而且很慢。
df = query_redshift(sc, sql).toPandas() # 95% of Redshift queries
我们不得不经历这样流程的主要原因是原生 pandas.read_sql[4]其实只对小数据量友好,而且 Pandas API 相比 PySpark 更被广泛熟知、更轻便。Koalas[5]可能对于这种情况有所帮助,但是它还比较早期并且不能解决资源浪费问题。”鱼和熊掌何以兼得”?我们开发的 Redshift connector 既充分利用了 Redshift UNLOAD[6]命令(其把任务切分为多个小任务并分配到不同节点,并发地生成 CSV 格式的结果再存在 S3 上),而且利用了 Python multiprocessing[7]模块并发读取这些小文件,最后用 pandas.concat[8]合并到一起。
def to\_df(self, nproc=None, \*\*kwargs):
parser\_options = self.\_get\_parser\_options(\*\*kwargs)
s3 = S3FileSystem(key=self.temp\_credentials\[0\], secret=self.temp\_credentials\[1\],
token=self.temp\_credentials\[2\])
args = \[(s3.open(f, 'rb'), self.manifest, True, parser\_options, self.temp\_credentials)
for f in self.manifest.iter\_files()\]
with util.create\_executor(nproc) as executor:
parts = executor(\_read\_part, args)
return pd.concat(parts, ignore\_index=True).reset\_index(drop=True)
并发读取分片结果,再用 pd.concat 合并。围绕 UNLOAD manifest 组织的代码
上面的 create_executor 方法调用了 multiprocessing.Pool 里的 starmap 方法。_read_part 函数(实现未给出)使用了 pd.read_csv 读取子任务生成的 CSV 文件。
@contextmanager
def create\_executor(nproc):
pool = mp.Pool(nproc)
try:
yield pool.starmap
finally:
pool.close()
pool.join()
starmap 和 multiprocessing.Pool 读取部分 CSV 结果
读 CSV 文件容易抛异常,因为内容里可能有不匹配的引用字符,字段为空和分隔符异常等等。但我们的情况是 Redshift UNLOAD 命令生成 CSV,pd.read_csv 函数读取 CSV,没有人为的数据改动,可以保证二者之间格式的一致性,避免读取 CSV 时的异常。我们还利用了 Redshift 生成的 manifest 文件校验行数和协助转换数据类型。
Pandas-redshift connector 端到端工作流
最终,当用户执行这么简单的一句 ”df = tdr.query_redshift(query).to_df()” 时,其背后执行了如上图所示的流程。这个方法耗时与 PySpark 相当甚至略快些,而且不需要 Spark 环境、计算资源远远低于 Spark。
PySpark 和 TDR 粗略性能对比。同样的查询语句(20列)不同的返回条数分别在 Spark 集群(8 个节点)和 TDR(2 CPU + 8 GB)的对比情况。计算刨除了集群启动时间
UNLOAD 命令有两个小问题。首先,UNLOAD 不支持最里面的语句带有 LIMIT 条件。这个问题可以通过 WITH 语句 (common table expression [9]) 解决。
WITH result\_table AS (SELECT \* FROM raw\_table LIMIT 100)
SELECT \* FROM result\_table
其次,UNLOAD 不支持 ORDER BY 语句。这个可以通过拿到数据后再在 Pandas 内排序。
更少的代码,更多的探索
数据科学家已经对 Jupyter 驾轻就熟,但是我们仍希望那些熟悉 SQL 但是 Python 刚入门的人也能使用 TDR。为此,我们开发了 cell magic 和 JupyterLab 扩展。
SQL cell magic
对于有经验的工程师,他们可能已经忘了当初学习编程时遇到的一些基本概念,但对于初学者,变量、函数以及字符串操作等等这些基本概念并不是天生就会的。为了帮助初学者使用 SQL 进行数据分析,我们开发了自有 cell magic [10] 叫做 “%%sql”。这个 cell magic 核心逻辑其实相当简单。
from IPython.core.magic import cell\_magic, Magics, magics\_class
@magics\_class
class TubiDataRuntimeMagics(Magics):
@cell\_magic
def sql(self, line='', cell=None):
import tubi\_data\_runtime as tdr
self.shell.user\_ns\['df'\] = tdr.query\_redshift(cell).to\_df()
使用 query_redshift 执行 cell 内容并赋值给变量 df
如上所示,用户可以直接在 cell 内执行 SQL,其背后调用的正是 query_redshift 函数。
更多的选项和输出用于调试
你可能注意到上面的语句有个 ”{{ott_app}}“ 条件,它其实不是个合法 SQL。这个代码片段是 SQL magic 的相伴相生的特征,我们称之为,嗯...,代码片段。通过 Jinja2 [11]我们可以用别名指代常用 SQL 代码片段,而且可以直接在 SQL 里使用别名。这节省了很多重复代码,避免了一些无意造成的错误,还统一了代码定义和风格。比如,”{{ott_app}}” 代码段可用于 WHERE 条件表示 app 是否属于我们的 OTT 平台。
app in ('tubitv-amazon',
'tubitv-chromecast',
'tubitv-comcast',
'tubitv-ps4',
'tubitv-roku',
'tubitv-samsung',
'tubitv-sony',
'tubitv-tvos',
'tubitv-xboxone')
使用 Jinja2 嵌入可复用代码段到 SQL 语句中
整合 nteract data explorer
一旦获取到数据后,我们很可能希望可视化这些数据以更好地进行探索性数据分析。Netflix 开源的 nteract data explorer [12] 能够自动可视化 dataframe,但是它属于另外一个 notebook 项目(非 Jupyter Notebook),而 JupyterLab 自带的 data explorer 需要单独打开一个 tab 运行,这些都带来糟糕的用户体验。因此,我们决定开发一个 JupyerLab 扩展,它可以直接在 cell 内渲染 data explorer。另外,nteract data explorer 不存储状态,这意味着每当用户刷新 notebook,它就会回到初始状态,这让分析和协同稍有不便。这个扩展中,我们把 data explorer 状态存在了 notebook 的元数据中,所以当你分享一个链接给其他人时,你的状态被保留,其他人能看到你所期望的样式。
renderModel(model: IRenderMime.IMimeModel): Promise<void> {
const data = model.data\[this.\_mimeType\] as JSONObject; // data explorer data
const metadata = model.metadata.dataExplorer as JSONObject; // data explorer metadata
const onMetadataChange = (data: object) => {
model.setData({ metadata: { ...model.metadata, dataExplorer: data } }); // update notebook metadata
const notebookPanel = findNotebookPanel(this).context.save(); // save
};
return new Promise<void>((resolve) => {
ReactDOM.render(<DataExplorer data={data} metadata={metadata} onMetadataChange={onMetadataChange} />, this.node, resolve);
});
}
简化版 data explorer 状态存储
结合 SQL cell magic 和这个扩展,我们需要做的只需执行一句 SQL 和 一句 “display(df)” 就能可视化数据了。这大大降低了 Python 初学者的入门门槛。
有了 SQL cell magic 和 JupyterLab 扩展,初学者只需要写 SQL
团队协作
链接分享
默认情况下,JupyterHub 中的 URL 都是登录用户相关的链接,比如 https:///user/cshe/。这意味着 JupyterLab 中默认的”可分享“链接功能并不能分享。JupyterHub 官方文档推荐使用 /user-redirect/ [13] 路由来分享 notebook,该路由会重定向 URL 到当前用户特定的链接。我们开发了一个 JupyterLab 扩展来改写成带 /user-redirect/ 的 URL,也期望能回馈 JupyterHub 社区(如果还没有好心人已经做了的话)。
目录操作
我们还发现一个小问题,JupyterLab 复制和粘贴功能无法操作目录。基于此,我们开发了另外一个扩展——Deep Copy——来操作目录。
扩展修改或新增的选项
在此,我们很高兴地宣布:上文提到的所有扩展都已经开源了!请移步 JupyterLab extensions [14] 查看。希望我们的一点点努力能丰富 JupyterHub 和 JupyterLab 特性。
更多特性
TDR 特性太多,我们无法一一详述,下面将列举几个其他特性。
TDR 预装了 pandas_profiling [15],调用 profile_report(该方法已经集成到了 pandas dataframe 中) 即可方便检查数据质量。
TDR 中 pandas_profiling 易于数据质量检测
TDR 还含有数据目录,它结合了来自 Redshift 的元数据和来自数仓的用户元数据(如描述等)。通过数据目录,我们可以很容易地浏览数据对象、通过关键字或正则搜索数据,以及 tab 补全探索数据而无需离开当前 notebook。
“键”不离手探索数据目录(还支持 tab 补全)
TDR 还基于 plotly [16] 开发了一个交互式的 3D 可视化工具帮助我们探索成千上万部电影电视剧,叫做 “TDR 内容探索器”。
探索发掘内容之间的距离和关系
对于机器学习,我们也特地开发了可视化工具用来比较不同排序算法结果的异同,这样即使对于非算法专家他们也能很直观地感受和理解算法带来的效果。同时,这也大幅降低了 bug 发生的概率,带来了更强的模型可解释性。
直观比较个性化推荐算法迭代前后的推荐结果异同
未来之路
Tubi 数据科学家是 TDR 最早的用户,之后慢慢推广到产品、开发、测试和运营团队。TDR 早已运行在 Kubernetes 之上,我们会持续升级、开发和维护,后续文章会介绍这方面的一些经验。我们还推出了一门公司内部的培训课程“Tubi 数据认证”,它使用 TDR 作为交互式培训环境,以帮助 SQL 初学者学会如何操作、探索和发掘数据。
我们正在引入和整合包括 TensorBoard 在内的深度学习工具到 TDR 中。未来,我们也计划把 TDR 连接到远程集群中,比如 Spark 集群和 TensorFlow 集群,这样可以使得 TDR 拥有真正的大规模数据处理能力。构建数据工具以使得人人都能做数据分析和数据科学工作,如果你也想从事如此振奋人心的工作,那就加入我们的数据团队吧。
原文:Chang She, Tubi VP of Engineering
译者:Huihua Zhang, Tubi Data Engineer
点击 “阅读原文” 查看英文版