缓存过滤

在某些情况下,数据集的总大小可能超过分配给 Alluxio 缓存的总磁盘空间。 为了解决这个问题,Alluxio 提供了缓存过滤器(cache filter)功能,允许管理员制定规则,根据文件路径仅缓存特定的文件。 管理员还可制定规则,让 Alluxio 自动从 UFS 重新加载缓存,确保同步最新的文件内容。

配置

要启用缓存过滤器功能,请将以下配置添加到 alluxio-site.properties 文件中:

# 缓存过滤器并非默认启用
alluxio.user.client.cache.filter.enabled=true
alluxio.user.client.cache.filter.type=RULE_SET
alluxio.user.client.cache.filter.config.file=${ALLUXIO_HOME}/conf/cache_filter.json
alluxio.user.client.cache.filter.config.check.interval=5min

Alluxio 提供了缓存过滤规则的模板 JSON 文件。 该文件位于 ${ALLUXIO_HOME}/conf/cache_filter.json.template。 Alluxio 组件会每隔 alluxio.user.client.cache.filter.config.check.interval定义的时间间隔动态重新加载缓存过滤规则文件,因此文件的更改会被自动同步,但会有一定延迟。 如果希望更新的频次更高,可以将检查间隔设置为较小的值,例如 30s

缓存过滤器规则

Alluxio 根据文件的可变性,定义了 3 种不同的缓存过滤模式。

  • Immutable: 用户声明路径上的元数据/数据不会发生变化。Alluxio 缓存这些文件的元数据/数据,但不会访问 UFS 来查看文件是否发生了变化。

  • Skip Cache: 用户声明路径上的元数据/数据不应缓存在 Alluxio 中。这通常适用于更改频繁且因此缓存价值较小的文件/目录。也适用于读取频次极低的文件。

  • Max Age: 用户声明路径上的元数据/数据可能会发生变化。如果设置 MaxAge=10min,则意味着已缓存到 Alluxio 中10分钟内的元数据/数据缓存可以被使用(可以被认为是最新的内容)。 如果已缓存的时间超过了 10 分钟,Alluxio 会再次查看UFS,以确定 UFS中的该文件是否发生了变化。

示例

下面通过一个 JSON 示例文件来说明如何设置规则。

{
  "apiVersion": 1,
  "metadata": {
    "immutable": [".*/immutable_tables/.*"],
    "skipCache": [".*/skip_cache_tables/.*"],
    "maxAge": {".*/mutable_tables/.*":"10s"},
    "defaultType": "immutable"
  },
  "data": {
    "immutable": [".*/immutable_tables/.*"],
    "skipCache": [".*/skip_cache_tables/.*"],
    "maxAge": {".*/mutable_tables/.*":"10s"},
    "defaultType": "immutable"
  }
}

配置文件允许管理员为元数据和数据设置不同的缓存规则。 现在我们只看元数据部分。metadata section 包含 4 个部分:

  1. immutable: 一组正则表达式(regex pattern)。路径符合这里定义的文件将被视为 Immutable 类型。

  2. skipCache: 一组正则表达式。路径符合这里定义的文件将被视为 Skip Cache 类型。

  3. maxAge: 一组正则表达式,定义了路径和每个路径应该使用的 Max Age 时限。例如,匹配 ".*/mutable_tables/.*" 的文件将具有 10 秒的最大缓存时限。

  4. defaultType: 如果文件不匹配任何正则表达式,则将被视为指定的默认类型。

注意:理论上,规则名称是驼峰式(camel case)的,例如 immutable, skipCache, maxAge。 但为了容错性更高,规则名称不区分大小写——诸如 SkipcacheskipCAche 这样的配置都能被正确解析为 skipCache

下面将进一步解释不同使用场景下的最佳缓存规则配置。

不同使用场景下的推荐缓存过滤规则

管理不变文件

不变文件是缓存的最佳适用场景。 如果文件是不变的,在像 Alluxio 这样的分布式缓存系统中不会有任何一致性问题。 如果能确保文件无更改,用户从 Alluxio 或 UFS 访问到的内容将会是相同的。 默认的 cache_filter.json.template 文件会将 Alluxio 中所有文件的元数据和数据定义为不变。

{
  "apiVersion": "1",
  "metadata": {
    "defaultType": "immutable"
  },
  "data": {
    "defaultType": "immutable"
  }
}

也可以使用 Alluxio 仅缓存元数据而不缓存数据。

{
  "apiVersion": "1",
  "metadata": {
    "defaultType": "immutable"
  },
  "data": {
    "defaultType": "skipCache"
  }
}

如果知道某些文件不适合缓存,可以将它们添加到 skipCache 列表中。 在这样的配置下,Alluxio 实际上被配置为了访问缓存和直接访问UFS之间的路由。

{
  "apiVersion": "1",
  "metadata": {
    "skipCache": [".*/never_cache/.*"],
    "defaultType": "immutable"
  },
  "data": {
    "skipCache": [".*/never_cache/.*"],
    "defaultType": "immutable"
  }
}

管理可变文件

可变文件对于缓存系统来说是个难题。 Alluxio 和其他任何分布式缓存系统一样,也会在处理变化的文件时遇到问题。 针对可变文件,Alluxio 提供了三个选项。

选项1:可变文件不经过缓存

最简单的方法是直接访问 UFS 读取这些可变文件。 UFS 通常提供强一致性,可以被视作数据的真实来源。 如果所有的元数据和数据请求都直接发送至 UFS,那么 Alluxio 的用户就不会遇到数据一致性问题。

一般来说,用户应当挑选出这些可变目录的路径并将其标记为skipCache。 例如,用户在 NAS 上有一些用于存储脚本的目录。 用户希望在这些目录上实现强一致性,能接受缓存性能的损耗,那么就可以按如下所示配置缓存过滤器:

{
  "apiVersion": "1",
  "metadata": {
    "skipCache": ["file://dev/scripts/.*", "file://data/dev/scripts/.*"],
    "defaultType": "immutable"
  },
  "data": {
    "skipCache": ["file://dev/scripts/.*", "file://data/dev/scripts/.*"],
    "defaultType": "immutable"
  }
}

适用场景 该选项适用于能够牺牲性能换取强一致性的使用场景。但是,如果文件被频繁更新,缓存带来的性能提升很快就会被额外的管理开销或缓存刷新成本所抵消。

选项 2:缓存可变文件并手动刷新缓存

在某些使用场景中,高性能是必须的。这意味着 skipCache 模式不再是一个选项。 但是,这些文件可能只是偶尔发生更改。 在这种情况下,用户仍然可以为这些文件配置 immutable 模式,但需要在 UFS 更新这些文件之后,对 Alluxio 中的缓存进行手动刷新/失效处理。

缓存过滤器配置与之前一样,将这些文件视作不变。

{
  "apiVersion": "1",
  "metadata": {
    "defaultType": "immutable"
  },
  "data": {
    "defaultType": "immutable"
  }
}

由于这些路径被配置为immutable,Alluxio 不会主动刷新其缓存。 因此,管理员或数据工程师需要进行手动刷新。

假设一个典型的 ELT(提取-加载-转换)场景工作流,如下:

  1. 从 OLTP 数据库的 SALES 表中加载原始数据。过滤条件是过去一天的所有交易记录。

  2. 所有记录经过 ELT 流程处理,并在 s3://datalake/tables/pipeline/sales/下创建一个物化表,如s3://datalake/tables/pipeline/sales/data_20240102/xxx.parquet

  3. Alluxio 挂载 s3://datalake/tables/pipeline/,通过其北向 S3 接口将数据提供给数据分析师。

在该示例场景中,数据工程师发现 s3://datalake/tables/pipeline/sales/data_20240102/ 中的数据不正确,需要通过再次触发 ELT 流程来重新加载表。 因此,该目录中的文件将被更改(可变性)。

ELT 流程触发并完成后,数据工程师需要手动重新加载 Alluxio 中的元数据。

# 命令为
$ alluxio job load --metadata-only --path <ufs path> --submit

# 在此情况下,数据工程师需触发
$ alluxio job load --metadata-only \
  --path s3://datalake/tables/pipeline/sales/data_20240102/ --submit

上述命令依赖于 Alluxio 3.x 中的 Scheduler(调度器)服务。 该命令将强制重新加载 Alluxio worker 上指定目录的元数据。 如果缓存内容已更改,缓存将被失效处理(因此下一次读取将从 UFS 加载最新的缓存数据)。

当最佳数据获取模式是访问缓存时,也是触发数据预加载的最佳时机。

$ alluxio job load --path s3://datalake/tables/pipeline/sales/data_20240102/ --submit

示例展示了如何更新和重新加载目录。 如果某个操作未经 Alluxio 在 UFS 中修改了内容,那么该操作的人员或作业也应手动刷新 Alluxio 中的元数据,以反映出数据的变化。

适用场景 这种方法最适合文件变更极少且为手动触发,大多数文件都不变的情况。 它也适用于数据由工作流生成/更新的情况,我们可以在工作流中额外添加一个步骤来通知 Alluxio。

选项3:基于时间的缓存刷新

在某些使用场景中,性能是一个硬性要求。 同时,确保文件变更触发 Alluxio 更新可能很困难或无法实现。 在这些情况下,用户可能希望 Alluxio 能够自动检测这些文件的变化并进行更新。

MaxAge 类型可以满足这一需求。 如果 s3://datalake/tables/pipeline/sales/s3://datalake/tables/pipeline/inventory/ 中的数据每隔几个小时就会发生变化, 管理员可以将缓存过滤器进行如下配置:

{
  "apiVersion": "1",
  "metadata": {
    "maxAge": {"s3://datalake/tables/pipeline/sales/.*": "1h","s3://datalake/tables/pipeline/inventory/.*": "1h"},
    "defaultType": "immutable"
  },
  "data": {
    "defaultType": "immutable"
  }
}

因为 maxAge 被设置为 1 小时,如果现有的元数据缓存时间已经超过 1 小时,Alluxio worker 将执行与 UFS 的元数据同步,以获取最新的文件元数据。

用户可能会问,为什么 maxAge 只对元数据而不是对数据设置?在 Alluxio 中,元数据定义了数据状态并控制数据生命周期。 当元数据被刷新(从 UFS 重新加载且状态发生变化)时,Alluxio 也会删除数据(但不会将数据重新加载到缓存中)。 因此,只需在元数据上设置 maxAge 规则以触发元数据的重新加载即可。这种配置既有效又具有良好的性能。

三个选项对比

下面,我们通过表格来比较上述三个选项。

缓存最适用于不变文件。 如果底层文件不发生变化,Client 可以始终从 Alluxio client 缓存、集群缓存或直接从 UFS 访问到相同的内容。 Alluxio 也可以进行复制、驱逐或重新加载缓存的数据,不会出现一致性问题。 因此,相较选项 3, 我们推荐选项 1 和选项 2。 选项 1 和选项 2 更好地保证了 Alluxio 缓存的不变性,最大程度地避免一致性问题。

如果必须采用选项 3 来实现自动缓存刷新,maxAge 的设置应平衡性能和数据一致性。 与 UFS 的元数据同步可能会很慢且成本高昂。 如果数据每隔几个小时更新一次,将maxAge 设为 1小时 显然比 10秒 更好。 但是,如果数据频繁变化且maxAge必须低于 1分钟,请尝试使用 skipCache, 这是因为当底层文件更新如此频繁时,缓存可能已经毫无价值且具有误导性(因为 client 可能会访问陈旧的数据版本)。

当文件被配置为 immutable 时,Alluxio 不会同步 UFS 来获取文件更新。 因此,如果将文件配置为immutable,但该文件在 UFS 中已发生更改,我们会从 Alluxio 缓存中看到陈旧的版本。 更糟糕的情况是,如果 Alluxio worker A 加载了旧版本而 worker B 加载了新版本, 我们可能会从 Alluxio 缓存中看到两个不同的版本,而 worker A 永远不会尝试将缓存刷新到最新版本。

常见问题

当未启用缓存过滤器时,缓存刷新的原理是什么?

Alluxio 之前通过 alluxio.user.file.metadata.sync.interval 配置来控制当前缓存数据是否应被视为陈旧并进行刷新。

缓存过滤规则类型如何映射到旧的alluxio.user.file.metadata.sync.interval 语义如下所示:

  • Immutable: alluxio.user.file.metadata.sync.interval=-1

  • Skip Cache: alluxio.user.file.metadata.sync.interval 指定是否应刷新现有缓存。 skipCache 模式下完全不经过缓存。因此,没有与

  • Max Age(k): alluxio.user.file.metadata.sync.interval=k

  • Max Age(0): alluxio.user.file.metadata.sync.interval=0

如果未启用缓存过滤器功能,Alluxio 将回退到解析 alluxio.user.file.metadata.sync.interval配置以确定是否应刷新缓存 。默认值为-1,意味着 Alluxio 在第一次同步之后将不会尝试与 UFS 进行第二次元数据同步并更新缓存内容。

# 默认配置
alluxio.user.file.metadata.sync.interval=-1

当对元数据和数据配置不同的缓存过滤规则时,有哪些注意事项?

通常应为元数据和数据配置相同的缓存过滤规则,因为当元数据和数据有不同规则时,推理行为或故障排查可能会非常困难。

例如,将元数据缓存设置为immutable,而将所有数据缓存设置为skipCache。 假设文件 s3://bucket/table/data.txt 在 UFS 中被覆盖,文件大小从 length=1GB 变为length=512MB。 之后,如果尝试从头到尾扫描该文件,Alluxio 将依赖已缓存的陈旧元数据(length=1GB),并尝试从 UFS 读取数据。 于是,我们可能会收到来自 UFS 的异常,提示超出 512MB的偏移量(offset)未找到。 上述示例显示为元数据和数据定义不同的缓存过滤规则时存在的风险。 如果从不同的数据源读取这些数据(一个从 Alluxio 缓存,另一个从 UFS),可能会有数据不一致的风险。

本文档的前面章节列出了可能考虑对元数据和数据使用不同缓存过滤规则的主要场景。

为什么在使用maxAge 规则时,我的应用程序会访问到不一致的数据?

当配置 maxAge 时,Alluxio 提供的是"有界陈旧性"级别的一致性。 "有界陈旧性"指的是 client 可能观察到一个比 UFS 中最新版本更旧的版本,但这里的陈旧性是有时间上限的。 这个时限就是 maxAge 定义的值。 该一致性级别比最终一致性(eventual consistency)强,但比强一致性(strong consistency)或顺序一致性(sequential consistency)弱。 在这个时限内,client可能访问到文件的任一陈旧版本,因此可能会出现不一致。

从底层来看,Alluxio 是一个分布式系统,能够提供多个层级的缓存(client 缓存和集群缓存)。 如果client 由不同的缓存提供数据服务,这些缓存可能会在不同的时间从 UFS 加载数据,因此可能会访问到文件的不同版本。

当超过maxAge后 ,Alluxio 组件将会将现有缓存视为过期。然后,Alluxio 会尝试与 UFS 进行元数据同步,以获取最新版本。

为什么即使在将文件配置为 immutable 后,我的应用程序也会访问到不一致的数据?

与上一问题类似,不同的 Alluxio 组件可能在不同时间从 UFS 加载数据。 如果 UFS 中的文件发生了更改,不同的 Alluxio 组件可能会缓存文件的不同版本。 由于缓存过滤规则设置为 immutable,Alluxio 组件将永远不会尝试再次检查 UFS 中的文件。 因此,现有缓存可能会永远保持不一致。

当出现此问题时,请参考管理可变文件中的选项 1 或选项 2。

Last updated