如上图所示,在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端,咱们对元数据有了更分明的认知。
上文提及的一些工作实际上还在进行中。在此之后,咱们还打算进行以下三方面的工作: