1. 从一次数据读取瓶颈说起为什么我们要引入分布式缓存层大数据平台跑得久了总会遇到一些让人头疼的“老毛病”。其中最典型的一类就是存算分离架构下的数据读取效率问题。早些年我们的集群还是存算一体的计算节点本地就有磁盘读数据走的是本地IO虽然扩展性差一点但胜在快。后来业务量上来了存储和计算不得不拆开计算集群和存储集群各自独立扩展灵活性确实好了很多但新的问题也随之而来——远程读取带来的网络开销和延迟成了很多任务的新瓶颈。具体来说我们的离线计算任务和部分近实时任务底层数据大多放在分布式文件系统或者对象存储上。每次跑任务计算节点都要通过网络去远端拉数据。如果数据量小还好一旦遇到大宽表关联、反复扫描同一份维表、或者多个任务共享同一批热数据的场景网络IO就会被打满任务耗时直线上升。更麻烦的是存储集群那边的压力也很大同一份数据被不同任务反复读取存储侧的带宽被大量重复请求消耗掉。我们试过一些常规手段比如调整任务并行度、优化SQL减少扫描量、把部分热数据提前落到本地磁盘等等。这些方法有一定效果但都是“治标不治本”——要么改动业务逻辑成本太高要么本地磁盘容量有限缓存命中率上不去。后来我们把目光投向了分布式缓存层这个方向核心思路是在计算集群和远端存储之间加一层缓存把热数据缓存在计算节点附近让后续的重复读取直接命中缓存不再走远程网络。在这个方向上Alluxio是一个比较成熟的选择。它本身定位就是虚拟分布式存储系统能够把底层多种存储系统统一挂载进来然后在计算侧提供一层分布式缓存。我们当时评估了几个关键点一是它能不能和现有的计算引擎无缝对接二是缓存策略是否足够灵活三是运维复杂度能不能接受。经过一段时间的调研和测试我们最终决定在部分业务场景中落地Alluxio作为大数据平台的缓存加速层。这篇文章就是把这套方案从设计到落地的过程整理出来包括我们为什么这么选、怎么部署、遇到哪些坑、最后效果如何。如果你所在团队也在为存算分离后的读取效率发愁或者正在评估类似的缓存方案希望这些经验能帮你少走一些弯路。2. 整体架构设计与方案选型思路2.1 为什么是Alluxio而不是其他方案在决定引入缓存层之前我们其实对比过几种不同的思路。第一种是在计算引擎层面做缓存比如利用计算框架自带的缓存机制把热数据缓存在Executor内存或者本地磁盘里。这种方式的好处是改动小但问题也很明显——缓存的生命周期和任务绑定任务结束缓存就没了跨任务共享基本做不到。而且内存缓存受限于单机资源数据量一大就容易OOM。第二种是在存储侧做缓存比如在存储集群前面加一层缓存代理。这个思路的问题在于缓存代理本身可能成为新的瓶颈而且它离计算节点还是太远网络延迟并没有真正消除。第三种就是在计算集群侧引入独立的分布式缓存层Alluxio就是这类方案的代表。它的核心优势在于缓存是独立于计算任务的可以跨任务、跨引擎共享缓存介质可以是内存也可以是SSD/HDD容量扩展灵活而且它对上层计算引擎基本透明不需要业务代码做大量改造。我们当时重点考察了Alluxio的几个能力。首先是挂载能力它支持把多种底层存储统一挂载到同一个命名空间下这样不同存储系统的数据可以通过统一路径访问对上层来说屏蔽了底层差异。其次是缓存策略它支持多种缓存模式比如把数据完全缓存在Alluxio里、只缓存元数据、或者按需缓存我们可以根据数据的热度灵活配置。再就是与计算引擎的集成我们主要用Spark和HiveAlluxio对这两个引擎的支持都比较成熟基本只需要改一下数据路径就能接入。还有一个比较实际的考量是社区活跃度和运维成本。Alluxio的社区版本功能已经比较完整文档也相对齐全遇到问题比较容易找到参考。而且它的架构不算特别复杂Master负责元数据管理Worker负责数据缓存运维团队上手难度可控。2.2 我们最终的部署架构经过几轮讨论和测试我们最终确定的架构大致是这样的在计算集群的每个节点上部署一个Alluxio Worker负责缓存该节点上任务需要的数据。同时选几台节点部署Alluxio Master负责管理整个集群的元数据和缓存索引。底层挂载了我们的分布式文件系统和对象存储上层通过Alluxio的客户端接入Spark、Hive等计算引擎。数据流向是这样的当计算任务需要读取某个路径的数据时请求先到Alluxio客户端客户端根据路径判断是否命中缓存。如果命中直接从本地Worker的内存或磁盘读取如果没有命中Worker会从底层存储拉取数据同时按照配置的缓存策略决定是否缓存下来。后续其他任务再读同一份数据时就可以直接命中缓存了。这个架构的关键点在于缓存命中率的控制。如果缓存命中率太低那Alluxio就变成了一个纯转发层反而增加了延迟。所以我们花了比较多精力在缓存策略的调优上比如哪些数据值得缓存、缓存多久、缓存介质怎么分配等等。后面会详细展开这部分。2.3 缓存介质与容量规划缓存介质的选择直接影响到成本和效果。我们当时的策略是内存SSD混合内存用来缓存最热的小数据集比如维表、配置表这类频繁访问但体积不大的数据SSD用来缓存体量较大但访问频率稍低的数据比如中间结果表、历史分区数据等。容量规划上我们遵循一个原则缓存容量不超过单节点可用存储资源的30%。这个比例是拍脑袋定的但背后有逻辑——留足空间给计算任务本身的shuffle和临时文件避免因为缓存占满磁盘导致任务失败。具体到每个节点我们给Alluxio分配了大约200GB的SSD空间和50GB的内存空间实际使用下来基本够用后续根据监控数据再做调整。还有一个细节是缓存淘汰策略。Alluxio支持LRU最近最少使用等淘汰算法我们用的是默认的LRU但针对不同路径设置了不同的TTL。比如维表数据TTL设得比较长因为更新频率低而一些临时中间结果TTL设得短一些避免长期占用缓存空间。3. 核心细节解析与实操要点3.1 Alluxio的读写流程拆解要理解Alluxio怎么加速数据读取得先搞清楚它的读写流程。我尽量用通俗的方式讲清楚。读流程是这样的计算任务发起读请求请求先到Alluxio客户端。客户端会先问Master“这个数据在缓存里吗在哪个Worker上”Master查一下元数据如果发现数据已经被缓存了就告诉客户端去对应的Worker读。客户端直接连那个Worker从内存或SSD里把数据读出来。如果Master发现数据没缓存就告诉客户端去底层存储读同时Worker会异步地把数据拉过来缓存上。这里有个关键点Alluxio的缓存是按块Block管理的。一个大文件会被切成多个Block分散缓存在不同Worker上。这样设计的好处是读取可以并行多个Worker同时提供数据带宽叠加。但坏处是如果某个Block所在的Worker负载高可能会拖慢整体读取速度。我们实际使用中通过调整Block大小和Worker分布策略尽量让热点数据均匀分散。写流程相对简单一些。计算任务写数据时可以选择写穿Write-Through或者写回Write-Back模式。写穿就是数据同时写到Alluxio和底层存储保证一致性但延迟高写回是先写到Alluxio后续再异步同步到底层存储延迟低但有一致性风险。我们大部分场景用的是写穿因为大数据场景下数据一致性比写入延迟更重要。3.2 缓存策略的配置与调优缓存策略是Alluxio调优的核心。我们主要调整了以下几个参数alluxio.user.file.cache.partially.read.block这个参数控制是否缓存部分读取的Block。默认是false意思是只有完整读取的Block才会被缓存。我们改成true之后即使任务只读了Block的一部分也会把整个Block缓存下来。这样做的好处是后续任务如果读同一个Block的其他部分可以直接命中缓存。代价是可能缓存了一些实际上不会被再次访问的数据。实测下来对于宽表扫描类任务开启这个参数后缓存命中率提升了大约15%。alluxio.user.file.readtype.default这个参数控制默认的读取类型。我们设成了CACHE意思是读取时优先走缓存缓存没有就从底层拉并缓存。另外还有CACHE_PROMOTE选项会把数据提升到更快的存储介质比如从SSD提到内存我们只在少数极热数据上用了这个选项。alluxio.worker.file.persist.pool.size这个参数控制异步持久化线程池的大小。如果写回模式用得多这个值可以适当调大避免持久化任务积压。我们设成了默认值的两倍观察下来持久化延迟明显降低。除了这些参数我们还用了Alluxio的路径级别TTL功能。比如对于/warehouse/dim/路径下的维表数据设置TTL为7天对于/tmp/intermediate/下的中间结果设置TTL为12小时。这样不同热度的数据有不同的缓存生命周期避免冷数据长期占用空间。3.3 与Spark和Hive的集成方式集成这块我们主要做了两件事一是改数据路径二是调客户端参数。Spark集成比较简单。原来Spark读数据写的是底层存储的路径比如hdfs://cluster/warehouse/table现在改成Alluxio的路径alluxio://alluxio-master:19998/warehouse/table。Alluxio会自动把请求转发到底层存储同时做缓存。需要注意的是Spark的spark.sql.hive.metastore配置不需要改因为元数据还是存在原来的Hive Metastore里只是数据路径变了。Hive集成稍微麻烦一点。Hive表的位置Location需要指向Alluxio路径这样Hive查询时才会走Alluxio。但改表位置会影响所有依赖这张表的任务所以我们是分批做的——先拿几张非核心表试水确认没问题后再逐步扩大范围。另外Hive的hive.exec.input.listing.max.threads参数可以适当调大因为Alluxio的元数据操作比底层存储快可以支撑更高的并发列表操作。还有一个坑是路径映射。我们底层有多个存储系统比如老数据在分布式文件系统上新数据在对象存储上。Alluxio支持把不同底层路径挂载到同一个命名空间下但需要提前规划好挂载点。我们当时没规划好后来调整挂载点的时候费了不少功夫。建议一开始就把命名空间设计清楚比如/data/hdfs/对应分布式文件系统/data/object/对应对象存储避免后期迁移。3.4 高可用与故障处理Alluxio Master是单点吗默认部署模式下Master确实存在单点风险。我们用的是Master HA模式部署了多个Master节点通过ZooKeeper做选主。这样即使一个Master挂了另一个能快速接管不影响整体服务。Worker的故障处理相对简单。如果某个Worker挂了Master会把它标记为不可用后续读请求会绕过这个Worker直接去底层存储或者其他Worker读。但已经缓存在这个Worker上的数据就丢了需要重新缓存。为了减少这种损失我们给Worker配置了多级存储内存SSDHDD即使SSD坏了HDD上的缓存还能用。还有一个实际遇到的问题缓存雪崩。有一次我们重启了一个Worker结果大量请求同时涌向底层存储把存储集群带宽打满了。后来我们做了两件事一是Worker重启时逐步上线不要一次性把所有缓存清空二是给底层存储读取加了限流避免突发流量打垮存储。4. 实操过程与核心环节实现4.1 集群部署与初始化配置部署这块我们用的是手动部署配置管理工具的方式。虽然Alluxio也支持容器化部署但我们考虑到缓存需要持久化存储容器化后存储管理比较麻烦所以还是用了物理机部署。具体步骤大致如下第一步规划节点角色。我们选了3台节点作为MasterHA模式所有计算节点作为Worker。Master节点配置相对高一些因为要处理元数据请求Worker节点配置比较均衡主要看缓存介质容量。第二步安装JDK和Alluxio。Alluxio依赖Java环境我们统一装了JDK 8。Alluxio版本选的是当时比较稳定的2.x版本。安装包解压到统一目录比如/opt/alluxio/。第三步配置alluxio-site.properties。这是最关键的配置文件我们主要配了这些# Master配置 alluxio.master.hostnamealluxio-master-1 alluxio.master.embedded.journal.addressesalluxio-master-1:19200,alluxio-master-2:19200,alluxio-master-3:19200 alluxio.zookeeper.enabledtrue alluxio.zookeeper.addresszk-1:2181,zk-2:2181,zk-3:2181 # Worker配置 alluxio.worker.memory.size50GB alluxio.worker.tieredstore.levels2 alluxio.worker.tieredstore.level0.aliasMEM alluxio.worker.tieredstore.level0.dirs.path/mnt/ramdisk alluxio.worker.tieredstore.level0.dirs.quota50GB alluxio.worker.tieredstore.level1.aliasSSD alluxio.worker.tieredstore.level1.dirs.path/mnt/ssd/alluxio alluxio.worker.tieredstore.level1.dirs.quota200GB # 底层存储挂载 alluxio.master.mount.table.root.ufshdfs://cluster/warehouse第四步格式化并启动。Master节点执行alluxio format然后启动Master和Worker进程。启动后通过Web UI默认19999端口检查集群状态。第五步挂载底层存储。除了根挂载点我们还挂载了对象存储alluxio fs mount /data/object s3a://bucket/warehouse这样/data/object/路径下的数据就会走对象存储而根路径下的数据走分布式文件系统。4.2 缓存预热与任务接入集群跑起来之后缓存是空的第一批任务读数据时还是走底层存储。为了尽快让缓存发挥作用我们做了缓存预热。预热的方式有两种一是用alluxio fs load命令手动加载指定路径的数据二是跑一批预热任务把常用维表和热数据先读一遍。我们两种方式都用了手动加载主要针对维表预热任务主要针对最近7天的热分区数据。预热完成后开始接入计算任务。我们采取的是灰度接入策略先选了几个非核心的报表任务把数据路径改成Alluxio路径观察一周。确认稳定后逐步扩大到核心报表和部分ETL任务。整个灰度过程持续了大约一个月期间不断调整缓存策略和参数。接入过程中有个小技巧在任务配置里加一个开关可以随时切换走Alluxio还是走底层存储。这样万一Alluxio出问题可以快速回滚不影响业务。4.3 监控指标与效果评估上线之后我们重点监控了几个指标指标名称含义我们的目标值缓存命中率读请求命中缓存的比率70%缓存容量使用率已用缓存/总缓存容量60%-80%读延迟P9999%读请求的延迟100msWorker CPU使用率Worker节点CPU负载60%底层存储读带宽从底层存储读取的带宽下降50%以上实际跑下来缓存命中率稳定在75%左右读延迟P99从原来的300ms降到了80ms左右底层存储的读带宽下降了约60%。效果还是比较明显的。不过也有几个任务因为数据访问模式比较随机命中率只有30%左右这类任务我们后来就没走Alluxio还是走底层存储。4.4 参数调优的实际记录调优过程中我们记录了一些关键参数的调整过程和效果调整一Block大小。默认Block是64MB我们改成了128MB。原因是我们的数据文件普遍比较大大Block可以减少元数据开销提高顺序读效率。改完之后大文件读取的吞吐量提升了约20%。调整二Worker线程数。alluxio.worker.network.netty.worker.threads默认是CPU核数我们调成了CPU核数的2倍。因为Worker要同时处理多个客户端的读请求线程多一点可以支撑更高并发。调整后高并发场景下的读延迟明显降低。调整三客户端重试策略。alluxio.user.network.netty.timeout默认是30秒我们改成了60秒。因为有些底层存储的读取比较慢30秒容易超时。改完之后偶发的超时错误基本消失了。这些参数调整都不是一蹴而就的我们每次只改一个参数观察一周再决定是否保留。建议你也这么做避免多个参数同时改导致问题难以定位。5. 常见问题与排查技巧实录5.1 缓存命中率低怎么办缓存命中率低是最常见的问题。我们遇到过几种情况情况一数据访问太分散。有些任务每次读的数据都不一样比如按时间分区扫描全量数据这种场景缓存基本没用。解决办法是识别出这类任务不让它们走Alluxio或者只缓存最近几个热分区。情况二缓存容量不够。如果热数据总量超过了缓存容量就会频繁淘汰命中率自然上不去。解决办法是扩容缓存介质或者调整TTL让冷数据更快淘汰。情况三缓存策略配置不当。比如没开启部分读取缓存导致只读了一部分的数据没被缓存。解决办法是检查alluxio.user.file.cache.partially.read.block等参数。排查的时候可以先用alluxio fs stat命令查看某个路径的缓存情况或者通过Web UI看缓存命中率的趋势图。如果发现某个路径命中率特别低就重点分析那个路径的访问模式。5.2 Worker频繁Full GC怎么处理Worker是Java进程内存管理不当容易Full GC。我们遇到过一次Worker每隔几分钟就Full GC一次导致读请求大量超时。排查下来原因是Worker的JVM堆内存设得太小而缓存元数据又比较多导致频繁GC。解决办法是调大Worker的堆内存我们最终设成了16GB。同时把alluxio.worker.memory.size调小了一点给JVM堆留更多空间。另外如果用的是内存缓存要注意内存缓存和JVM堆是两回事。内存缓存用的是堆外内存Off-Heap不受JVM堆大小限制但需要配置alluxio.worker.memory.size来指定用量。如果堆外内存设得太大可能导致系统OOM所以要根据节点总内存合理分配。5.3 底层存储读取超时怎么排查底层存储读取超时通常有几个原因一是底层存储本身负载高响应慢二是网络抖动三是Alluxio客户端超时设置太短。我们遇到过一次底层存储集群在做扩容部分节点响应变慢导致Alluxio读取超时。解决办法是临时调大客户端超时时间等存储集群扩容完成后再调回来。另外我们给底层存储读取加了重试机制alluxio.user.ufs.retry.timeout设成了5分钟这样偶发的超时可以自动重试。如果超时频繁发生建议先检查底层存储的健康状态确认不是存储侧的问题。然后再检查Alluxio的日志看是否有明显的错误信息。5.4 常见问题速查表问题现象可能原因排查方法解决方案缓存命中率低数据访问分散/容量不足/策略不当查看访问模式、缓存使用率调整TTL、扩容、优化策略Worker Full GC频繁JVM堆太小/元数据太多查看GC日志、堆内存使用调大堆内存、减少缓存元数据读请求超时底层存储慢/网络抖动/超时太短检查存储状态、网络延迟调大超时、加重试、限流Worker启动慢缓存数据多/磁盘IO慢查看启动日志、磁盘IO逐步上线、换SSDMaster响应慢元数据操作多/HA切换查看Master日志、ZK状态优化元数据、检查HA配置5.5 几个踩过的坑和避坑建议坑一路径映射没规划好。前面提过我们一开始没规划好挂载点后来调整的时候发现有些任务已经硬编码了路径改起来很麻烦。建议一开始就把命名空间设计清楚并且用统一的路径规范。坑二缓存预热过度。我们有一次预热了太多数据把缓存占满了导致新数据进来时频繁淘汰反而降低了命中率。后来我们控制了预热的数据量只预热最热的那部分。坑三忽略Worker磁盘IO。Worker缓存数据到SSD时如果磁盘IO打满会影响读性能。我们后来给Worker的SSD做了RAID提高了IO吞吐。另外监控磁盘IO使用率也很重要超过80%就要注意了。坑四没做限流。Alluxio从底层存储读数据时如果并发太高可能把底层存储打垮。我们后来加了限流alluxio.user.ufs.read.rate.limit设成了合理值避免突发流量。坑五版本升级太激进。我们有一次升级Alluxio版本没仔细看Release Notes结果新版本改了某个默认参数导致缓存行为变化命中率下降。建议升级前先在测试环境验证确认没问题再上生产。6. 后续扩展方向与个人体会这套方案跑了大半年整体效果是符合预期的。后面我们还在考虑几个扩展方向一是多集群缓存共享让不同计算集群的Alluxio Worker可以互相访问缓存进一步提高命中率二是缓存数据压缩把冷数据压缩后缓存节省空间三是与调度系统联动根据任务的数据依赖关系提前预热缓存做到“任务还没跑缓存已就绪”。我个人在实际操作中的体会是Alluxio这类缓存层不是银弹它最适合的场景是热数据重复读取、存算分离、对读延迟敏感。如果数据访问本身就很分散或者数据量小到本地磁盘就能放下那引入Alluxio反而增加复杂度。另外缓存策略的调优是个持续过程不是配一次就完事需要根据业务变化不断调整。最后再分享一个小技巧给Alluxio的Web UI加个监控告警当缓存命中率低于阈值或者Worker异常时自动通知这样能第一时间发现问题避免影响业务。
阅读完成 · 觉得有帮助?