用户体验的谋求是有限的,而老本是无限的,如何均衡?
用户体验很重要,降本也很重要。做技术的都晓得,加机器堆资源能够解决绝大多数的用户感觉慢的问题,但要加钱。没什么用户体验是开发不了的,但要排期,实质也要钱。在老本无限,包含机器资源和开发人力都无限的状况下,如何晋升用户体验呢?
对于大数据查问引擎来说,用户体验的第一优先级是快,天下文治唯快不破。而缓存技术是很好的抉择,能够无效达到咱们的目标。
硬件上,咱们能够开掘服务器闲置资源的后劲。因为在 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_AFFINITY
node-scheduler.node-selection-hash-strategy=CONSISTENT_HASHING
node-scheduler.max-splits-per-node=400
node-scheduler.max-pending-splits-per-task=40
后续咱们会针对 k8s 容器化环境,进行专门的调度策略优化,确保新的 worker 容器会优先应用宿主机上已存在,且无其余 worker 应用的缓存目录空间,并确保 master 将相应的数据处理申请发到该 worker 上。
想要理解更多对于 Alluxio 的干货文章、热门流动、专家分享,可点击进入【Alluxio 智库】: