HDFS 中的集中式缓存管理是一种显式缓存机制,允许用户指定要由 HDFS 缓存的路径。NameNode 将与磁盘上具有所需块的数据节点通信,并指示它们将块缓存到堆外缓存中。
HDFS 中的集中式缓存管理具有许多显著优势。
显式固定可防止频繁使用的数据从内存中驱逐。当工作集的大小超过主内存的大小时,这一点尤其重要,这对于许多 HDFS 工作负载来说很常见。
由于数据节点缓存由 NameNode 管理,因此应用程序可以在做出任务放置决策时查询已缓存块位置的集合。将任务与已缓存块副本并置可提高读取性能。
当块已被数据节点缓存时,客户端可以使用新的、更高效的零拷贝读取 API。由于缓存数据的校验和由数据节点完成一次,因此客户端在使用此新 API 时几乎不会产生开销。
集中式缓存可以提高整体集群内存利用率。当依赖于每个数据节点上的操作系统缓冲区缓存时,重复读取块将导致块的所有n个副本都被拉入缓冲区缓存。通过集中式缓存管理,用户可以显式固定n个副本中的m个,从而节省n-m内存。
HDFS 支持 Linux 平台上的非易失性存储类内存 (SCM,也称为持久性内存) 缓存。用户可以为 DataNode 启用内存缓存或 SCM 缓存。内存缓存和 SCM 缓存可以在 DataNode 之间共存。在当前实现中,DataNode 重新启动时将清除 SCM 中的缓存数据。未来将考虑在 SCM 上提供持久性 HDFS 缓存支持。
对于重复访问的文件,集中式缓存管理非常有用。例如,通常用于联接的 Hive 中的小型“事实表”是缓存的良好候选对象。另一方面,缓存“一年报告查询”的输入可能不太有用,因为历史数据可能只读取一次。
集中式缓存管理对于具有性能 SLA 的混合工作负载也很有用。缓存高优先级工作负载的工作集可确保它不会与低优先级工作负载争夺磁盘 I/O。
在此架构中,NameNode 负责协调集群中所有 DataNode 的堆外缓存。NameNode 定期从每个 DataNode 接收“缓存报告”,其中描述了在给定 DN 上缓存的所有块。NameNode 通过在 DataNode 心跳上附加缓存和取消缓存命令来管理 DataNode 缓存。
NameNode 查询其“缓存指令”集以确定应缓存哪些路径。缓存指令持久存储在 fsimage 和编辑日志中,并且可以通过 Java 和命令行 API 添加、删除和修改。NameNode 还存储一组“缓存池”,这些池是用于将缓存指令分组在一起以进行资源管理和强制执行权限的管理实体。
NameNode 定期重新扫描命名空间和活动缓存指令,以确定需要缓存或取消缓存哪些块,并将缓存工作分配给 DataNode。重新扫描也可以由用户操作触发,例如添加或删除缓存指令或删除缓存池。
我们目前不缓存正在构建、损坏或不完整的块。如果缓存指令涵盖符号链接,则不会缓存符号链接目标。
目前在文件或目录级别执行缓存。块和子块缓存是未来工作的项目。
缓存指令定义应缓存的路径。路径可以是目录或文件。目录以非递归方式缓存,这意味着只缓存目录中第一级列出的文件。
指令还指定其他参数,例如缓存复制因子和过期时间。复制因子指定要缓存的块副本数。如果多个缓存指令引用同一文件,则应用最大缓存复制因子。
过期时间在命令行中指定为生存时间 (TTL),即未来的相对过期时间。缓存指令过期后,NameNode 在做出缓存决策时不再考虑它。
缓存池是一个管理缓存指令组的管理实体。缓存池具有类 UNIX 的权限,用于限制哪些用户和组可以访问该池。写权限允许用户向池中添加和删除缓存指令。读权限允许用户列出池中的缓存指令以及其他元数据。执行权限未使用。
缓存池还用于资源管理。池可以强制执行最大限制,该限制限制了池中的指令可以总共缓存的字节数。通常,池限制的总和大约等于群集上为 HDFS 缓存保留的总内存量。缓存池还会跟踪一些统计信息,以帮助群集用户确定什么内容已缓存以及应该缓存什么内容。
池还可以强制执行最大生存时间。这限制了添加到池中的指令的最大过期时间。
cacheadmin
命令行界面在命令行上,管理员和用户可以通过 hdfs cacheadmin
子命令与缓存池和指令进行交互。
缓存指令由唯一的、不可重复的 64 位整数 ID 标识。即使稍后删除了缓存指令,也不会重复使用 ID。
缓存池由唯一的字符串名称标识。
用法:hdfs cacheadmin -addDirective -path <path> -pool <pool-name> [-force] [-replication <replication>] [-ttl <time-to-live>]
添加新的缓存指令。
<path> | 缓存路径。路径可以是目录或文件。 |
<pool-name> | 将指令添加到其中的池。您必须具有缓存池的写权限才能添加新指令。 |
-force | 跳过缓存池资源限制检查。 |
<replication> | 要使用的缓存复制因子。默认为 1。 |
<time-to-live> | 指令有效的时间。可以用分钟、小时和天来指定,例如 30m、4h、2d。有效的单位是 [smhd]。“never”表示永不过期的指令。如果未指定,则指令永不过期。 |
用法:hdfs cacheadmin -removeDirective <id>
移除缓存指令。
<id> | 要移除的缓存指令的 ID。您必须具有指令所属池的写权限才能移除它。要查看缓存指令 ID 列表,请使用 -listDirectives 命令。 |
用法:hdfs cacheadmin -removeDirectives <path>
移除具有指定路径的每个缓存指令。
<path> | 要移除的缓存指令的路径。您必须具有指令所属池的写权限才能移除它。要查看缓存指令列表,请使用 -listDirectives 命令。 |
用法:hdfs cacheadmin -listDirectives [-stats] [-path <path>] [-pool <pool>]
列出缓存指令。
<path> | 仅列出具有此路径的缓存指令。请注意,如果我们没有对 path 所在缓存池的读取权限,则不会列出该指令。 |
<pool> | 仅列出该池中的路径缓存指令。 |
-stats | 列出基于路径的缓存指令统计信息。 |
用法:hdfs cacheadmin -addPool <name> [-owner <owner>] [-group <group>] [-mode <mode>] [-limit <limit>] [-maxTtl <maxTtl>]
添加新的缓存池。
<name> | 新池的名称。 |
<owner> | 池所有者的用户名。默认为当前用户。 |
<group> | 池所属组。默认为当前用户的主要组名。 |
<mode> | 池的 UNIX 风格权限。权限以八进制指定,例如 0755。默认情况下,此项设置为 0755。 |
<limit> | 此池中指令缓存的最大字节数(总计)。默认情况下,不设置限制。 |
<maxTtl> | 添加到池中的指令允许的最大生存时间。此时间可以用秒、分、时和天指定,例如 120s、30m、4h、2d。有效单位为 [smhd]。默认情况下,不设置最大值。值“never”指定没有限制。 |
用法:hdfs cacheadmin -modifyPool <name> [-owner <owner>] [-group <group>] [-mode <mode>] [-limit <limit>] [-maxTtl <maxTtl>]
修改现有缓存池的元数据。
<name> | 要修改的池的名称。 |
<owner> | 池所有者的用户名。 |
<group> | 池所属组的组名。 |
<mode> | 池的 Unix 风格权限(八进制)。 |
<limit> | 此池可以缓存的最大字节数。 |
<maxTtl> | 添加到池中的指令允许的最大生存时间。 |
用法:hdfs cacheadmin -removePool <name>
移除一个缓存池。此操作还会取消与该池关联的路径的缓存。
<name> | 要移除的缓存池的名称。 |
用法:hdfs cacheadmin -listPools [-stats] [<name>]
显示一个或多个缓存池的信息,例如名称、所有者、组、权限等。
-stats | 显示其他缓存池统计信息。 |
<name> | 如果指定,则仅列出命名的缓存池。 |
用法:hdfs cacheadmin -help <command-name>
获取有关命令的详细帮助。
<command-name> | 要获取其详细帮助的命令。如果未指定命令,则打印所有命令的详细帮助。 |
为了将块文件锁定到内存中,DataNode 依赖于 Windows 上的 libhadoop.so
或 hadoop.dll
中的本机 JNI 代码。如果您使用 HDFS 集中式缓存管理,请务必启用 JNI。
目前,持久性内存缓存有两种实现方式。默认方式是基于纯 Java 的实现,另一种是本机实现,它利用 PMDK 库来提高缓存写入和缓存读取的性能。
要启用基于 PMDK 的实现,请执行以下步骤。
安装 PMDK 库。有关详细信息,请参阅官方网站 http://pmem.io/。
使用 PMDK 支持构建 Hadoop。请参阅源代码中 BUILDING.txt
中的“PMDK 库构建选项”部分。
要验证 Hadoop 是否正确检测到 PMDK,请运行 hadoop checknative
命令。
务必为 DRAM 缓存或持久性内存缓存配置以下属性之一。请注意,DRAM 缓存和持久性缓存不能在 DataNode 上共存。
dfs.datanode.max.locked.memory
这决定了 DataNode 将用于缓存的最大内存量。在类 Unix 系统上,DataNode 用户的“锁定内存大小”ulimit(ulimit -l
)也需要增加以匹配此参数(请参阅 操作系统限制 部分)。设置此值时,请记住,您还需要内存空间来存储其他内容,例如 DataNode 和应用程序 JVM 堆以及操作系统页面缓存。
此设置与 延迟持久化写入功能共享。Data Node 将确保延迟持久化写入和集中式缓存管理使用的组合内存不超过 dfs.datanode.max.locked.memory
中配置的数量。
dfs.datanode.pmem.cache.dirs
此属性指定持久性内存的缓存卷。对于多个卷,它们应以“,”分隔,例如“/mnt/pmem0, /mnt/pmem1”。默认值为空。如果配置此属性,将检测卷容量。并且无需配置 dfs.datanode.max.locked.memory
。
以下属性不是必需的,但可以指定用于调整
dfs.namenode.path.based.cache.refresh.interval.ms
NameNode 将使用此值作为后续路径缓存重新扫描之间的时间量(以毫秒为单位)。这会计算要缓存的块以及包含该块副本的每个 DataNode,该副本应缓存该块。
默认情况下,此参数设置为 30000,即 30 秒。
dfs.datanode.fsdatasetcache.max.threads.per.volume
DataNode 将使用此值作为每个卷用于缓存新数据的最大线程数。
默认情况下,此参数设置为 4。
dfs.cachereport.intervalMsec
DataNode 将使用此值作为向 NameNode 发送其缓存状态完整报告之间的时间量(以毫秒为单位)。
默认情况下,此参数设置为 10000,即 10 秒。
dfs.namenode.path.based.cache.block.map.allocation.percent
我们将分配给缓存块映射的 Java 堆百分比。缓存块映射是一个使用链式哈希的哈希映射。如果缓存块数量较大,则较小的映射访问速度可能会较慢;较大的映射会消耗更多内存。默认值为 0.25%。
dfs.namenode.caching.enabled
此参数可用于启用/禁用 NameNode 中的集中式缓存。当禁用集中式缓存时,NameNode 将不会处理缓存报告或存储有关群集中块缓存位置的信息。请注意,即使 NameNode 不会根据此信息采取任何操作,它仍会继续在文件系统元数据中存储基于路径的缓存位置。此参数的默认值为 true(即启用集中式缓存)。
dfs.datanode.pmem.cache.recovery
此参数用于确定是否在 DataNode 启动期间恢复持久内存上先前缓存的状态。如果启用,DataNode 将恢复持久内存上先前缓存数据的状态。因此,将避免重新缓存数据。如果未启用此属性,DataNode 将清理持久内存上的先前缓存(如果有)。此属性仅在启用持久内存时才起作用,即配置了 dfs.datanode.pmem.cache.dirs
。
如果您收到错误“无法启动数据节点,因为配置的最大锁定内存大小……大于数据节点可用的 RLIMIT_MEMLOCK ulimit”,这意味着操作系统对您可以锁定的内存量施加的限制低于您配置的限制。要解决此问题,您必须调整 DataNode 运行的 ulimit -l 值。通常,此值在 /etc/security/limits.conf
中配置。但是,它会根据您使用的操作系统和发行版而有所不同。
当您能够从 shell 运行 ulimit -l
并获得比使用 dfs.datanode.max.locked.memory
配置的值更高的值或表示没有限制的字符串“unlimited”时,您将知道自己已正确配置此值。请注意,ulimit -l
通常会以 KB 为单位输出内存锁定限制,但必须以字节为单位指定 dfs.datanode.max.locked.memory。
此信息不适用于 Windows 上的部署。Windows 没有 ulimit -l
的直接等效项。