乐趣区

关于uber:Uber实战案例基于Alluxio实现Presto缓存




如上图所示,在 Uber,所有的决策都与数据无关。Presto 以及其余各种查问引擎在 Uber 是被宽泛应用的。例如,经营团队在 Dashboard 等服务中大量应用了 Presto,而 UberEats 和市场团队也依赖于这些查问后果来确定价格。此外,Presto 也在 Uber 的合规部、增长营销部门、ad-hoc 数据分析等场景下应用。

上图展现了 Uber 外部的一些重要数据。总的来说,目前 Presto 在 Uber 外部有 12K 的月沉闷用户,每天要解决 400K 的查问并且要解决超过 50PB 的数据。在基础设施方面,Uber 有 2 个数据中心,部署 Presto 在大概 6 千个节点和 14 个集群。

Uber 的 Presto 架构如上图所示。

最上层是 UI/Client 层,这其中包含了一些外部的 Dashboard、Google Data Studio、Tableau 等工具。此外,咱们还有一些后盾服务,应用了 JDBC 或其余的外部查问引擎来与 Presto 进行通信。

Proxy 层,这一层负责从每一个 Presto 集群的 coordinator 拉取数据,以获取 query 数量、task 数量、CPU 和 Memory 使用率等信息,咱们也基于此判断应该将每个 query 调度到哪一个集群,来提供 load-balancing 和 query-gating 服务。

在底层,就是多个 Presto 集群,它们与底层 Hive、HDFS、Pinot 等进行通信。不同 plugin 或不同的数据集之间能够进行 join 操作。

此外,如图中的左右两侧所示,对上述架构中的每一层,咱们还有:

外部的监控服务

基于 Kerberos Security 提供的 Security broker

咱们的 workloads 能够大抵分为两类:

Interactive:由数据科学家和工程师发送的 query

Scheduled:次要是 Batch query,包含 Dashboard 查问、ETL 查问等

接下来,咱们简略介绍一下咱们对于业务上云的思考。

在过来的几年中,Uber 的团队始终在思考如何上云和何时上云的问题,以及应该以怎么的布局(layout)来与云进行交互。这里以“what-how-why”模型列出了一些咱们认为值得探讨的点。

● What:咱们有多种多样的利用布局,比方,利用方面,咱们有 BI 等利用;计算引擎方面,咱们不仅有 Spark、Presto 等。在云这个场景下,咱们还有许多云原生的抉择;存储方面,有 gcs、S3、甚至是 HDFS 等多种抉择。

● Why:对咱们来说,上云最重要的一个动机是,咱们心愿进步 cost efficiency,更好地帮忙硬件设施来实现资源弹性扩大。同时,咱们也心愿能提供高可用、可扩展性、可靠性。

● How:如何上云,这其中也有很多要点值得探讨。1. 云能提供很多原生的个性,那么咱们就须要思考,这些个性如何能力与开源组件放弃互相统一;2. 不同规模下的性能如何,Uber 保护了一个十分大型的数据湖,因而性能数据对咱们和客户来说都是十分重要的;3. 咱们也很器重云上的 Security 和 Compliance 问题;4. 咱们也能够应用云提供的一些原生的个性,来补救咱们本人的“技术债”(Tech Debt)。

上图展现了咱们对于 Presto 上云的布局。

基于这样的架构,咱们的解决方案能够扩大到不同云服务厂商中。这个目前只是作为咱们的长期布局和愿景,还处在十分初期的实现阶段。

总体上,咱们有 Cloud 集群和 PROD 集群。如图中右下角所示,咱们心愿大部分的数据还是在 onPrem HDFS 上。图中偏左的蓝色箭头代表了咱们做的一些事后测试,咱们在 HDFS 之上不加任何的缓存进行了一些试验,结果表明,网络流量十分高,从而带来了微小的开销。因而,咱们构想能应用云提供的服务,比方 GCS 或 S3,来提供相似于 L2 Cache 的性能。咱们心愿能将数据集中的一些高拜访频率的重要数据放在这些“云上的 L2 Cache”中,而对于云上的每个 Presto 集群,咱们打算利用本地的 SSD 来缓存一些数据,以晋升性能。

总体来说,Alluxio 的 Cache Library 与 Alluxio Service 的运行形式很不同。Alluxio Cache Library 是在 Presto worker 外部的运行的本地缓存。咱们在默认的 HDFS client 的实现之上封装了一层,当读取 HDFS 的 API 被调用时,零碎首先会查看缓存空间,以通晓这是否为一次缓存命中(cache hit or miss)。如果缓存命中,则从本地的 SSD 中读取数据;否则,将从远端的 HDFS 中读取并将数据缓存在本地,这样在下一次读取时,咱们就能从本地读取。具体实现时,咱们将文件在 HDFS 中的门路作为 key。这一过程中,缓存命中率对于整体的性能也有着十分重要的影响。

此外,为了判断缓存是否在 Presto worker 中,咱们还利用了 Soft Affinity Scheduling。简而言之,这一性能能够确保将同样的 key 散发到同一个 worker 上,这样咱们就能够利用本地的 library 来确认每一次的数据读取是否命中了缓存。我将在下文对此进行具体介绍。

  1. Uber 的数据湖是十分大的,咱们每天解决 50PB 的数据,而 Uber 的数据湖相对超过了 EB 级别。同时,咱们有各种各样的表,比方 Hudi 表、Hive ETL 等。咱们的数据大部分是依照日期的进行分区的。在 Hudi 中,因为 Hudi 会对文件的每个分区进行增量更新,因而同一个文件能够有不同的版本;Hive ETL 会生成大量的 Staging 目录,在新的文件分区产生之后,这些 staging 目录就生效了。因而,随着时间推移,会有大量的冗余文件和文件更新,咱们须要对这一状况进行解决。
  1. 对于缓存命中率。咱们每天都有大于 3PB 的非反复数据(distinct data)拜访,并且有约 10% 的频繁拜访数据 (frequently accessed data) 和约 3% 的热拜访数据(hot accessed data)。针对频繁拜访数据和热拜访数据,咱们心愿能构建一个图,来反映有多少不反复的表拜访 (distinct table access) 和联结表拜访(joint table access)。

针对超大数据量所带来的挑战,咱们尝试构建一个过滤器布局 (filter layout)——只缓存咱们须要的数据。一开始,咱们只会将热拜访数据放入缓存,在此之后,咱们逐渐扩充缓存空间。

下面的图表展现了,在 Presto 的一次扫描或投影的查问中,因为有些 partition/split/task 可能十分大,因而有些 HDFS 节点的提早甚至达到了 4 至 5 秒。在生产环境中这一提早数据还会更大。这就使得数据节点提早产生了随机性(random latency)。这会给咱们生产环境中的查问带来重大的影响。

如果咱们应用了缓存,HDFS 缓存的数据就如上图所示。如果缓存可能命中,咱们就能获得更低的、更稳固的提早数据。试验中,咱们尝试应用本地缓存来获得百分百的缓存命中,来获得一个十分稳固的提早性能。另外,在上述的试验过程中,咱们也修复了一个 Namenode listing 相干的 bug。

目前在 Presto 中,Soft Affinity Scheduling 是基于一个简略的“取模”算法来实现的,这一做法的害处是,如果呈现了节点的增删,那么整个缓存的键空间都须要进行更新。针对这一问题,当初曾经有了一种开源的解决方案,如果预约义的节点很多而出问题的节点很少的话,这一计划能够很好地解决该问题。

然而在 Uber,咱们遇到了新的问题:在咱们集群中,节点的数量不是固定的。所以咱们引入了基于一致性哈希的调度策略。咱们一共有 400 个节点,并为每个节点调配 10 个虚构节点。因而所有缓存的键就散布在一个有 4000 个节点的一致性哈希环上。对于这一改良机制,咱们曾经向开源社区提交了一个 pull request,感兴趣的敌人能够试用此性能。

初期的测试曾经实现,而且数据很不错。结果表明,如果数据在缓存中,对于那些依赖于 SFP 性能的查问而言,它们的性能能够失去大幅度晋升。咱们目前正在进行的工作包含:

工作一:在 TPCDS benchmark 上进行 sf10k 测试;

工作二:尝试从历史数据中剖析表或分区的拜访模式,找出最热的数据,从而能更好地设置缓存过滤器;

工作三:集成一些 Dashboard 和监控性能。

其一,是对于所谓的“stale cache”的问题,即缓存空间中可能存储了旧的数据。只管咱们将缓存数据放在了 Presto 的本地 SSD 中,但真正的数据实际上寄存在 GCS 或者 HDFS 等,这些数据可能被其他人批改。比方,在应用 Hudi table 状况下,咱们常常能够看到,数据文件的“最初批改工夫戳”始终在变动。因而,如果咱们缓存了旧的数据,这就会导致查问的后果不精确。更坏状况下,因为新旧的 page 被混合在一起,当 Presto 传送 Parquet 或者 ORC 文件时,Presto 中可能出现异常(Exception)。

其二,每天从 HDFS 中读取的不反复数据可能很大,但咱们没有足够的缓存空间来缓存所有数据。这就意味着在 Uber 的缓存过滤器之外,Alluxio 须要 eviction 或 allevation 策略。此外,咱们还能够引入 quota 治理。咱们为每个 table 设置一个 quota,对于那些热拜访的表,咱们设置一个更大的 quota,而对于冷拜访的、不须要缓存的表,咱们能够设置一个十分小的甚至是 0 的 quota。

其三,只管咱们曾经在本地缓存中寄存了元数据,但它仅仅在内存中而不在磁盘中。这就导致,当服务器启动时,无奈复原元数据。比方,Presto 的服务器宕机重启后,服务器能够从新获取到数据,然而其中没有元数据,这可能导致 quota 治理被毁坏。

因而,咱们提出了文件级元数据(File Level Metadata),其中保留了最初批改工夫和每个数据文件的 scope。咱们须要将这些数据保留在磁盘中,使得 Presto worker 能在启动时获取这些数据。

引入这种元数据后,数据就会有多个版本(multiple versions of the data)。也就是说,数据被更新后,就会生成一个新的工夫戳,对应于一个新的版本。因而咱们将创立一个新的文件夹来与这个新的工夫戳绝对应,这个文件夹中保留了新的 page,与此同时,咱们会尝试将旧的工夫戳删除。

如上图左侧所示,咱们有两个文件夹,对应于两个工夫戳:timestamp1 和 timestamp2。通常来说,在零碎运行时,不会同时有两个工夫戳,因为咱们会间接将旧的 timestamp1 删除而只保留 timestamp2。然而,在服务器忙碌或高并发的状况下,咱们可能无奈准时将工夫戳删除,这种状况下,咱们就可能会同时有两个工夫戳。除此之外,咱们还保护一个元数据文件,其中以 protobuf 格局的保留了文件信息,而且保留了最新的工夫戳。这样就能保障,Alluxio 的本地缓存只会从最新的工夫戳中读取数据,服务器重启时,也从元数据文件中读取到工夫戳信息,从而能正确地治理 quota 和最初批改工夫等信息。

因为 Alluxio 是一个通用的缓存解决方案,所以它仍然须要计算引擎(即 Presto)来将元数据传递给它。因而,在 Presto 一侧,咱们利用了 HiveFileContext。每一个 Hive 表或 Hudi 表都有一个数据文件,而 Presto 会为每个数据文件创建一个 HiveFileContext。Alluxio 在关上 Presto 文件时,就会利用这一信息。在调用 openFile 时,Alluxio 创立一个新的 PrestoCacheContext 实例,其中保留了 HiveFileContext,也有 scope(蕴含四个等级:database、schema、table、partition)、quota、cache identifier(即文件门路的 md5 值)等信息。咱们会将这个 cache context 传递到咱们的本地文件系统中。这样,咱们就能够在 Alluxio 中进行元数据管理和指标收集等工作。

除了从 Presto 传递数据到 Alluxio,咱们也能够向 Presto 调用一些回调函数(callback)。这样,在执行查问操作时,咱们就能够晓得一些内部消息,比方多少字节数据读取命中了缓存 / 多少字节的数据是从内部 HDFS 存储中读取的。

如上图,咱们将含有 PrestoCacheContext 的 HiveFileContext 传递给本地的缓存文件系统(LocalCacheFileSystem),之后本地的缓存文件系统会向 CacheContext 调用一些回调函数(IncremetCounter),而后这个回调的调用链会持续进行,到 HiveFileContext 中,再到 RuntimeStats。在 Presto 中,执行查问时就是利用 RuntimeStats 来收集指标信息,因而咱们就能够在这里进行一些聚合操作。

在此之后,咱们就能够在 Presto 的 UI 或是 JSON 文件中看到这些本地缓存文件系统相干的信息。有了上述流程,咱们就能让 Alluxio 和 Presto 紧密联系并工作在一起。在 Presto 端,咱们有了更好的统计数据;在 Alluxio 端,咱们对元数据有了更分明的认知。

上文提及的一些工作实际上还在进行中。在此之后,咱们还打算进行以下三方面的工作:

退出移动版