共计 5381 个字符,预计需要花费 14 分钟才能阅读完成。
如上图所示,在 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 来确认每一次的数据读取是否命中了缓存。我将在下文对此进行具体介绍。
- Uber 的数据湖是十分大的,咱们每天解决 50PB 的数据,而 Uber 的数据湖相对超过了 EB 级别。同时,咱们有各种各样的表,比方 Hudi 表、Hive ETL 等。咱们的数据大部分是依照日期的进行分区的。在 Hudi 中,因为 Hudi 会对文件的每个分区进行增量更新,因而同一个文件能够有不同的版本;Hive ETL 会生成大量的 Staging 目录,在新的文件分区产生之后,这些 staging 目录就生效了。因而,随着时间推移,会有大量的冗余文件和文件更新,咱们须要对这一状况进行解决。
- 对于缓存命中率。咱们每天都有大于 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 端,咱们对元数据有了更分明的认知。
上文提及的一些工作实际上还在进行中。在此之后,咱们还打算进行以下三方面的工作: