用户体验的谋求是有限的,而老本是无限的,如何均衡?

用户体验很重要,降本也很重要。做技术的都晓得,加机器堆资源能够解决绝大多数的用户感觉慢的问题,但要加钱。没什么用户体验是开发不了的,但要排期,实质也要钱。在老本无限,包含机器资源和开发人力都无限的状况下,如何晋升用户体验呢?

对于大数据查问引擎来说,用户体验的第一优先级是快,天下文治唯快不破。而缓存技术是很好的抉择,能够无效达到咱们的目标。

硬件上,咱们能够开掘服务器闲置资源的后劲。因为在cpu利用率的评估体系下,服务器的内存,本地磁盘可能有闲暇资源,可能挖掘出一些可用资源为咱们所用。

技术上,抉择开源社区大规模实际过的、有胜利案例的,收费的计划,能够升高开发成本,事倍功半。

PrestoDB社区的缓存计划就是一个很好的抉择。曾经在Meta公司(原Facebook)大规模落地实际过了,Uber也有落地;有源码和技术分享的材料;Alluxio社区也提供了很多反对。基于种种原因,咱们抉择应用该技术进行查问减速,官网Blog链接:https://prestodb.io/blog/2021/02/04/raptorx。

但即便有胜利案例,在外部落地时也会遇到各种问题,在不减少机器资源的前提下,查问工夫tp95提速超过1倍,其余查问速度指标也有50%到1倍的晋升。具体成果会在《从PrestoSQL到PrestoDB-Presto计算引擎版本升级小结》一文中具体介绍,详见链接:https://mp.weixin.qq.com/s/bPn8ncT_AXcPXbfAy6bAQw

PrestoDB的查问机制

PrestoDB查问数据的大略流程如图所示。缓存计划实质是将从内部服务获取的数据缓存在内存和本地硬盘中,缩小和内部零碎的交互,以提供更好的查问体验。

因为把有限的内部数据拉到了本地,缓存要思考数据有效性,容量管制,以及如何监控缓存的成果,即统计命中率。

利用缓存

Hive MetaStore缓存

查问一个一般的hive分区表,至多会有以下的读取元数据操作:获取表信息,获取满足过滤条件的分区名列表,依据分区的数量拆成几个并发线程,每个线程通过批量接口获取10到100个分区的分区信息。

当离线集群规模达到几千上万台,hive表会十分多,查问量也十分大。即便物理上拆分了若干个mysql数据库,若干个metastore服务,在拜访高峰期查问hive metastore也是一个较为耗时的操作。

下图是某集群获取表操作的均匀响应工夫(绿线)和p99响应工夫(橙线),能够看到即便是根本的获取表信息操作,在拜访顶峰时延时也会很高(Presto拜访元数据的默认超时工夫是10秒,超过10秒会重试三次,所以指标下限就是10秒)。

应用元数据缓存能够进步查问速度,缩小metastore的交互,升高meastore的拜访压力,但须要思考时效性问题,即如何感知元数据变动。

元数据缓存是PrestoDB晚期版本就有的性能,之前曾经在线上应用了。实现基于guava cache,将hive metastore的表,分区等元数据信息缓存在内存中,通过刷新工夫,过期工夫和缓存实体的下限数的配置来控制数据的有效性和容量下限。

对一个元数据实体来说,第一次查问会先从近程获取,之后从缓存中读取。当元数据被缓存的工夫达到刷新工夫,再次申请还是会从缓存中读取,但会启动异步线程从近程获取并更新缓存,这样能够兼顾查问性能和数据有效性。当数据被缓存的工夫超过过期工夫,再次申请会梗塞,直到从近程获取并更新缓存。当缓存实体达到下限(按实体类型各自计算),再次写入缓存会删掉最旧的。以前应用PrestoSql的时候,遇到过同步缓存的线程死锁,起因是同步元数据的代码里有获取其余元数据实体缓存的逻辑,比方loadPartitionByName会先调用getTable办法,如果表缓存过期了且同步线程用满了就可能产生死锁。PrestoDB新版本不会在同步代码里获取其余实体的缓存,所以没有这个问题。

在PrestoDB新版本中,新加了两个参数,设置只缓存分区信息,和查看分区版本性能。

前者是进步缓存的有效性,不缓存库,表这些轻量级的元数据信息,只缓存分区信息。后者是在获取分区名列表时,会获取带版本的分区名信息,应用分区缓存前先比拟分区的版本,版本统一才应用缓存。

但查看分区版本须要hive metastore有对应的接口,该性能并没有奉献给hive社区,PrestoDB社区版是用不了的。而只缓存分区信息,其实设计目标是配合查看分区版本来应用,独自应用仍然存在数据不统一的问题。思考到业务高峰期的申请延时,所以咱们决定缓存包含表信息在内的所有元数据。那元数据有变动时如何保障有效性呢?上面具体分析一下:

当元数据的变动是Presto引擎引起的,Presto能够主动清理掉发生变化元数据的缓存。还能够通过jconsole调用jmx接口清理掉缓存。除此之外,元数据过期只能等刷新和过期工夫,以及容量下限主动清理掉最旧的。

因为咱们hive表次要是Spark批工作写入的,所以Presto引擎无奈感知到元数据的变动。所以对表信息缓存来说,如果批改了表字段,如果存在缓存,可能要过段时间能力感知。对于分区名列表缓存,如果增加了新分区,也可能要过段时间能力感知。而如果分区产生了数据回溯,因为咱们的批工作没有写入分区的具体统计信息,并且咱们未开启应用分区元数据统计信息预过滤性能,所以分区元数据缓存不受影响。

综上所示,缓存了的表,分区名列表元数据要等缓存过期能力刷新。所以咱们须要严格保障元数据有效性的集群,比方做批工作数据品质校验的,就不开启元数据缓存。心愿进步查问性能承受短时间有效性延时的,开启缓存且只缓存10分钟。

在实践中,咱们遇到了业务方本人也缓存查问后果,引起缓存工夫放大的问题。当新增一个分区后,有时会较长时间能力查问到。为了解决该问题,咱们提供了清理指定表分区缓存的http接口,业务零碎本人晓得新增了,在清理本人查问后果缓存的同时也会调用接口清理Presto的分区缓存。这样就不会有缓存放大的问题了。

另外,在应用中还发现一个表分区太多引起的缓存刷新问题。

获取分区信息个别调用partitionCache的getAll办法一次获取一批partitons,但达到refreshAfterWrite工夫后,再次获取分区信息,partitionCache的getAll会触发线程池异步的批量调用load。如果分区很多,会产生大量申请单个分区的getPartition申请,给hive metastore造成了较大负载压力。有些业务查问的分区会越来越多,甚至一次要查7,8千个分区。因为由一次100个分区的批量接口变成了调用100次1个分区接口,这种表的分区缓存刷新会极大影响metastore的性能,造成所有拜访metastore的申请都变慢。见下图大量获取分区引起的查问毛刺。

这个问题实质是,google guava的refreshAfterWrite机制和loadAll办法有抵触,即后盾刷新机制和全量加载是有抵触的,为反对后盾刷新,全量加载进化为批量的load一个。

详见guava社区探讨:https://github.com/google/guava/issues/1975,guava社区至今未解决。

所以咱们在推动用户做数据治理,缩小查问分区数的同时,敞开了refreshAfterWrite性能,这样就不会有大量的getPartition申请了。

要留神的是一旦配置了超时工夫ttl,refresh-interval就不可为空了。能够将两者配置的一样来敞开后盾刷新性能。优化后的成果见下图,能够看到查问工夫大幅升高了,毛刺也缩小了。

将来咱们会持续优化,将获取分区列表和其余元数据的配置离开,分区列表是批操作不开启后盾刷新,其余元数据缓存开启。

数据文件列表缓存

hive.file-status-cache前缀的配置,能够依据目录key缓存目录下的数据文件信息列表,反对配置作用于哪些表,对s3这种对象存储提速会更显著。

只有确定分区不会回溯重写数据的表能力配置这个,否则查问可能会报错。实际中发现咱们无奈做这个假如,所以未配置这个缓存。

本地数据缓存

应用alluxio缓存HDFS数据。技术介绍能够浏览上面的链接:

https://mp.weixin.qq.com/s/2txWX40aOZVcyfxRL8KLKA

应用时要留神几点:
√ 首先,应用社区的PrestoDB版本,开启本地缓存性能,读数据会报错。起因是援用的社区版alluxio在schema中定义未定义file类型,而本地缓存的文件是file类型,须要在alluxio的源码里加上file类型,从新打包。
√ 其次,PrestoDB为实现hdfs本地缓存,用反射形式批改了FileSystem cache的实现,所以配置里禁用FileSystem cache,运行时会报错。而对于har类型的归档文件,是必须要敞开cache,设置fs.har.impl.disable.cache=true的,否则har文件的读取会报错。须要批改PrestoDB获取FileSystem的代码。
√ 再就是要思考调度的一致性,同一个数据块的查问尽量调用同一个worker节点,否则会占用太多本地存储,命中率还不高。监控发现在高峰期缓存命中率只有30%,须要优化调度参数。在调度章节会具体介绍。本地缓存个别放在ssd或更快的本地存储里。因为咱们服务器的ssd磁盘存储小的只有120GB,所以alluxio本地存储只配置了60G,但成果仍然很可观,单机的命中率有84%左右:

5分钟命中率图上面介绍一些有用的alluxio缓存实践经验:

√ 指标名后缀对立
指标项的名字相似,com.facebook.alluxio:name=Client.CacheShadowCacheBytesHit.presto-test-001_docker,type=counters,带着hostName后缀。而咱们的采集和展现规定须要指标名统一。所以设置presto启动参数alluxio.user.app.id=presto,来对立指标名。

√ 应用ShadowCache
简略来说,这个性能是假如有有限的本地存储时,缓存命中率能达到多少。

Alluxio提供了一个Shadow Cache性能,能够应用布隆过滤器记录计算引擎拜访过哪些数据以及总数据量的大小。每次计算引擎拜访数据,都统计一次是否命中shadow cache中的数据。联合shadow cache和Alluxio的命中率能够检测业务是否适宜应用缓存。

统计shadow cache的命中率,如果较低,则阐明业务场景中极少会拜访反复数据,那么是不适宜应用缓存的。

如果shadow cache的命中率高,但Alluxio缓存的命中率低,阐明业务场景适宜应用缓存,然而以后Alluxio缓存的空间过小了。因为在数据文件被反复拜访时,之前存入Alluxio的缓存,曾经因为缓存满了而被淘汰了,所以反复拜访时将得不到任何减速。那么此时加大Alluxio缓存空间,即换个大硬盘,会获得更好的减速收益。

shadow cache命中率统计见下图:

shadow cache五分钟粒度命中率

咱们shadow均匀数据命中率87% 左右,和目前理论的命中率84%差不多。阐明以后缓存成果曾经不错了。

√ 未统计到的不走缓存

worker忙碌时,master调度会随机选闲暇的worker,即cacheable为false,这时不走alluxio的LocalCacheFileSystem,间接读底层文件系统。而当初的命中率统计,都是基于LocalCacheFileSystem的。但在业务顶峰,很多查问没有走alluxio。那么如何统计计算读alluxio的数据量和读的总数据量(读alluxio+间接读底层文件系统之和)的比率呢?

能够用
(Client.CacheBytesReadCache.presto,type=meters:FiveMinuteRate + Client.CacheBytesRequestedExternal.presto,type=meters:FiveMinuteRate) * 300作为5分钟读alluxio的数据量,除以com.facebook.presto.hive:type=FileFormatDataSourceStats,name=hive:ReadBytes.FiveMinutes.Total ,五分钟hive读的总数据量指标,来计算最近5分钟的实在命中率比率。

√ put失败率高

presto日志里有很多alluxio的异样日志,监控也看到put缓存的失败率在业务高峰期比拟高,见下图。

问题的实质是在高并发场景下,写入要先删除旧数据,并发删除同一个文件的不同块,尽可能递归删除父目录的策略,在删除父目录时遇到了并发抵触,中断写缓存。实际上,在异样的上下文里,父目录不存在只能阐明被其余线程删了,持续操作就好。

做了个简略优化,删除旧缓存时,如果呈现父目录不存在的异样,持续操作。社区已接管,见https://github.com/Alluxio/alluxio/pull/16252

√ 数据有效性保障

alluxio的配置没有超时工夫,只有容量。看代码在openfile时传入了文件的批改工夫。然而,实现为了性能,并没有用传入的文件批改工夫来判断缓存是否生效。所以只会容量满了被替换,不会因为工夫变动过期。

这对于spark/hive sql写入的数据是没问题的,文件名自带jobid前缀,不会批改原有文件。但对于hudi,iceberg这些新技术,文件内容可能扭转,就会有问题。须要做额定的优化退出版本的判断,并保障旧版本的缓存数据生效后能被清理掉。

√ 异步还是同步写

默认配置是不开启alluxio异步写缓存的,而同步写缓存会升高读操作的速度。那么异步写缓存会显著进步查问性能吗?

看alluxio实现,异步写是提交到16个线程的池中异步写,线程不够了间接写失败。所以异步线程配的少了会影响缓存写入,但异步线程多了又会影响worker本身的线程模型。在一个小集群做了测试,异步写并没有显著进步查问性能。所以目前在实践中未开启异步写性能。

将来咱们会增加一种异步写策略,默认异步线程写缓存,当异步线程池满了就降级到以后线程写入。

数据文件索引块缓存

在内存中缓存orc和parquet文件的索引块。这个缓存有提速成果但不如alluxio数据缓存显著,依照官网示例配置就行,留神内存容量。

监控看,该缓存的命中率在87%到93%之间。

分片后果缓存

实质是按文件的数据块粒度,缓存一组计算的计算结果到本地存储里。比方某个文件块通过过滤,sum后的计算结果。因为业务方本人也做了查问后果缓存,所以该缓存的命中率不高,只有14%。

这个性能的实现上还有很多优化空间,将来能够继续优化。

调度策略

如上文所说,调度要思考数据处理的一致性,同一个数据块的查问尽量调用同一个worker节点。所以须要配置调度策略为SOFT_AFFINITY,数据块一致性优先。

思考到节点的高低线,hash策略须要设置为一致性hash,缩小节点高低线对调度的影响。

另外,业务高峰期有的节点会太忙碌,调度会放弃一致性,随机选个节点。而在业务顶峰时,所有节点都会忙碌,随机抉择的意义不大,还是应该优先数据一致性。所以要把忙碌判断的阈值调大。具体配置如下

hive.node-selection-strategy=SOFT_AFFINITYnode-scheduler.node-selection-hash-strategy=CONSISTENT_HASHINGnode-scheduler.max-splits-per-node=400node-scheduler.max-pending-splits-per-task=40

后续咱们会针对k8s容器化环境,进行专门的调度策略优化,确保新的worker容器会优先应用宿主机上已存在,且无其余worker应用的缓存目录空间,并确保master将相应的数据处理申请发到该worker上。

想要理解更多对于Alluxio的干货文章、热门流动、专家分享,可点击进入【Alluxio智库】: