Presto 在字节跳动的实践

时间:2021-06-25 01:01:01

来源:DataFunTalk


导读 本文主要内容包括:

1. 云平台上统一 UDF 的实现

2. 字节内部平台的实现

3. 贡献到开源社区的相关内容

4. 未来工作


分享嘉宾|张砚炳 字节跳动 软件开发工程师

编辑整理|陈业利 英祐科技

出品社区|DataFun



01

云平台上统一 UDF 的实现
Lakehouse Analytics Service 是火山引擎上对外提供的支持多引擎多租户的湖仓一体服务。

Presto 在字节跳动的实践

如果用户希望在云平台上使用自己的 UDF,会特别关注以下功能:
(1)是否兼容 Hive 的 UDF,比如 SQL UDF 是比较好的兼容手段,但对于小公司,在实际业务使用中,上云面临的最大困难是如何降低成本,希望能直接把 Hive UDF 迁移到云平台上使用。
(2)能否支持 UDF Jar 包的热更新。从比较小的角度来讲,像 Presto 这种常驻服务,如果实现一个 UDF Jar 包的动态加载,现在是必须要重启 Presto 服务,整个成本会比较高,对用户体验和 SLA 会有比较大的影响。从更高的产品角度而言,如果想要实现 UDF Jar 包更新,更好的方式是提供类似 CURD 功能的产品,用户可以进行多版本 UDF Jar 包的管理,可以选择相应的版本快速应用,快速生效。
(3)隔离相关,包括作业隔离和资源隔离。UDF 本质上都是用户自己写的代码,用户的代码是否安全,是否存在隐藏漏洞,可以被外部利用攻击用户数据,这些是无法事先得知的。所以必须要在内核和网络级别上对用户作业进行隔离,从而保证用户数据安全。同时还引申出另一个问题,即资源隔离问题。比如一个 Presto UDF 太复杂了,占了整个 Presto 资源,导致其它作业执行很慢。这种情况就不再是安全问题了,而是资源规划问题。所以在产品中能够有效的限制资源是很有必要的。
基于以上思路,我们完善了一套 Remote UDF 方案。

Presto 在字节跳动的实践

在远端通过 FAAS 服务,提供了一套通过镜像进行多版本管理 UDF Jar 包 Function 的管理能力,这个 FAAS 本身是个 Serverless 服务,能够提供理论上无限可拓展的计算资源,这样可以帮助用户进行复杂算子的更高并行度计算,同时因为在不同的机器上,天然实现了资源隔离,本身在 FAAS 层面也会做到足够的内核隔离和网络隔离。 
在客户端,提供了基于 Grpc 方式进行相应接口的调用。如上图所示, 整个 Remote 方案本质上能够把 UDF 的调用接口抽象出来,在实际使用过程中可以做到一套 UDF 能够在多个引擎适配,只需适配相应的 RPC 接口即可。
这种架构具有四大特点和优势:
(1)卓越的安全性
远端变成独立的沙箱,能够很好地实现内核和网络的完全隔离。同时因为这是一个远端资源,可以通过控制每一个容器的数量,限制资源使用,防止用户 Expensive Operator 的过多使用,影响 Presto 集群的整体稳定性。

Presto 在字节跳动的实践

(2)卓越的可扩展性
提供水平可扩展的资源能力,基于这个能力,用户的算子在相对复杂的情况下,可以利用远端可扩展的资源为复杂算子提供更好的支持, 这样某个在单机节点上因为性能损耗的原因难以快速跑出来的复杂算子,在远端因为计算资源可以水平扩缩容,就可以保证其执行。
Presto 在字节跳动的实践
(3)支持 UDF Jar 包热更新
对应的管理方式是把不同 UDF Jar 包的版本映射到不同的实际镜像当中。关于 UDF Server 是一个 Server 跑函数还是多个函数, 这完全取决于 Remote 端的实现。在我们的产品中,因为要做到隔离性,一个 UDF Jar 包会对应一套镜像,这套镜像就是一个实际的容器服务,不同租户对应不同的服务,基于这个镜像进行水平的扩缩容。这本身不是由 Presto Engine Side 决定,针对不同情形会有不同的实现。

Presto 在字节跳动的实践

(4)提供统一的接口描述
Remote UDF 能够对外提供统一的接口描述,这样的好处是:
① 可以同时支持多引擎,在调用端不管是 Spark 还是 Presto,甚至像现在比较火的 Native Engine(C++ 语言实现),在调用 UDF 时也不用考虑是否需要用 JNI 来 wrap 一套  UDF,只需远端实现同一个 Grpc 接口即可。同样在 UDF server 端也可以用任意语言实现,比如 Java 甚至是 Python 等,只需有相应的容器支持即可。
② 基于这种方便的迁移性和便利性,Remote UDF 能够提供很好的复用业务逻辑的能力。

Presto 在字节跳动的实践

与 Local 相比,Remote UDF 的优点如上文所述,主要包括:
(1)远端资源可拓展
(2)独立的环境
(3)热更新

Presto 在字节跳动的实践

同时,它也会有一些额外的代价:
(1)Network 的 overhead;
(2)远端启用 UDF server 时需要基于镜像启动,会产生镜像启动时间。
基于这两个问题的解决方案:
(1)关于网络的开销,本质上是计算量和网络量的 Cost 的平衡,在本地可以通过 Merge 配置的大小来降低单次请求量所消耗的网络开销。
(2)同时可以进行镜像的预热,事先预留好一定的资源, 保证服务的快速响应。
通过这些手段,能够做到 Remote 性能与 Local 相当,同时在某些复杂算子的情况下具有更好的优势,因为它在远端资源可以水平扩缩容。
02
字节内部平台的实现

Presto 在字节跳动的实践

接下来介绍一个内部平台的实现,支持了 Local 的 Hive UDF 和 UDAF。
能够在 Presto 支持 Hive UDF/UDAF,会有很多好处:
(1)对于开发人员和用户,可以复用 Hive UDF,以很低的成本,直接切换引擎,在 Presto 执行 SQL。
(2)对于数据平台的同学们,可以方便地管理 UDF 的 Jar 包,减少不必要的成本。

Presto 在字节跳动的实践

对于很多公司,最初引进 Presto 的时候可能主要考虑其作为优秀的交互式查询引擎,在 ad-hoc 场景有比较大的优势。如果能够对外提供完整的 SQL 语义,后端可以自己选择在 ETL 场景跑 Spark, 在 ad-hoc 场景跑 Presto。但是实际在推广过程中会遇到下面两个比较明显的问题:
(1)如何保证语义一致;
(2)如何保证 UDF 的接口一致。
对于第一个问题,如何保证语义一致,有很多 SQL 改写平台,能够逐步做到语法层面的一致。但是 UDF 的一致性影响比较大,不太可能让用户重新写一套。我们推广 SQL 的自动路由的初衷就是想让用户无感知,并且推动一套 SQL 可以在多个引擎运行。如果不能做到一致性兼容的话,即使推广了 Remote UDF 功能, 用户还是直连 Presto 或者 Spark。基于这些考虑,我们在 Presto 上支持了在 Local 模式下执行 Hive 的 UDF/UDAF。

Presto 在字节跳动的实践

现在可以保证在 ad-hoc 场景下超过 90% 的 SQL 都是跑在 Presto 上,而且没有用户是直连任何引擎来执行 SQL,完全由统一的 SQL 语言来保证这件事情,每天达到 100w+ 的 ad-hoc 的查询量。
03
贡献到开源社区的相关内容
接下来介绍一些我们向社区做的贡献。

Presto 在字节跳动的实践

不仅是云产品还有公司内部的技术都有很多贡献到了开源社区,主要包括两部分:
(1)在 PrestoDB 代码库下支持 Hive UDF/UDAF;
(2)在 Remote UDF 框架下面支持通过 Grpc 协议的调用。

Presto 在字节跳动的实践

介绍一下接口层面的设计。实现了 RemoteScalarFunctionImplemention,在此基础上,为了支持 Hive UDF/UDAF,还引入了一个新接口 JavaScalarFunctionImplementation。这样我们把 Presto 自带的 UDF 和 Hive 的 UDF 做为两个子实现,映射到了 Java 的 UDF 下面,在接口层面保证了 Presto 能够支持 Hive UDF/UDAF。

Presto 在字节跳动的实践

从实现架构来看,整体如上图所示,构建了一套基于 Hive 的 FunctionNamespaceManager, 会在 Resolve Function 阶段加载相应的 Hive UDF 类,并且把 Hive UDF 的数据类型跟 Presto 的数据类型进行映射和 wrap, 最后执行。

Presto 在字节跳动的实践

当前已经提交到 PrestoDB 开源社区的部分:
支持了 Hive 内置的 UDF/UDAF,既支持 Generic 的 UDF/UDAF,也支持 Simple 的 UDF/UDAF。之前有些 UDF 社区方案对 UDF 的支持有很多限制,我们贡献到社区的这套方案对 built-in 是比较完整的支持。
现在还不支持基于 Metastore 的 UDF 和 UDAF,包括临时 UDF/UDAF 和永久 UDF/UDAF ,会在后续的向社区贡献的规划中推动。

Presto 在字节跳动的实践

这里简单介绍一下,在 PrestoDB 里如何使用 Hive 的 UDF 和 UDAF。 
(1)注册UDF/UDAF:因为当前只支持 built-in 的 UDF,因此还需要到代码里(FunctionRegistry)里进行注册;如果注册 GenericUDF,则调用相关 registryGenericUDF 的 function;如果是注册 SimpleUdf,则要调用 registryUDF 的类,就可以完成需要的 UDF/UDAF 的注册。
(2)启用方面:整体启用是比较简单的。可以参考下面的代码:
Presto 在字节跳动的实践
Presto 在字节跳动的实践

Presto 在字节跳动的实践

04
未来工作

Presto 在字节跳动的实践

(1)支持基于 Metastore 的 UDF/UDAF,主要包括两部分:
① 支持从 Metastore 拉取元数据;
② 支持 UDF Jar 包的动态更新,热加载机制。
(2)优化 Remote UDF 性能,在 Presto Site Engine 做一些优化,包括:
① Merge Small Pages
② Dictionary Encoding 
③ 结果缓存
(3)支持 Remote UDAF。
05
问答环节

Q1:贡献的 PrestoDB 的 Hive UDF 的一部分,Hive 的支持函数?

A1:参考上文如何注册,大家把需要的函数直接在 functionRegistry 上添加即可。
Q2:Spark 支持 Remote UDF 是开源实现的吗?
A2:没有开源实现,因为 Presto 和 Spark 共同支持 Remote UDF 的 function 完全是 Facebook 内部的实现, 但是其实 Spark 要支持 Remote UDF 是非常容易的,完全可以在 plugins 时候,访问 Metadata source,Presto Remote UDF 如何实现的话,Spark 可以直接把 Jar 包拿下来,因为也是 scala implementions,如果是 Java 的 UDF 的话是比较容易实现的。主要难点是在怎么做好 preload,predistribute。这样避免在冷启动的时候导致启动比较慢。
Q3:内部使用 Remote 的场景多吗?
A3:相对来讲,内部使用 Remote 的场景会偏少一点, 某些用户的 UDF 计算比较复杂的话,使用 Remote 场景是比较有效,可以解决本地资源不足。在 Facebook 的内部使用 Remote 场景会比较多,降低用户写函数的门槛,另外很多用户想要的业务逻辑需要访问自己的 Backend Service,或者是 Fetch 安全相关的 function 也会做成 Remote,基本上在 Presto Engine 内部的话,需要 Access Remote Service 基本是不可操作的,跑到 Remote 上即可。