1 背景
随着云原生技术的飞速发展,各大公有云厂商提供的云服务也变得越来越标准、可靠和易用。凭借着云原生技术,用户不仅可以在不同的云上低成本部署自己的业务,而且还可以享受到每一个云厂商在特定技术领域上的优势服务,因此多云架构备受青睐。
知乎目前采用了多云架构,主要是基于以下考虑:
服务多活: 将同一个服务部署到不同的数据中心,防止单一数据中心因不可抗力不能正常提供服务,导致业务被“一锅端”;
容量扩展: 一般而言,在公司的服务器规模达到万台时,单一数据中心就很难支撑业务后续的扩容需求了;
降本增效: 对于同一服务,不同云厂商对同一服务的定价和运维的能力也不尽相同,我们期望能够达到比较理想的状态,在云服务满足我们需求的前提下,尽量享受到低廉的价格。
知乎目前有多个数据中心,主要的机房有以下两个:
在线机房: 主要是部署知乎主站上直接面向用户的服务(如评论、回答等),这部分服务对时延敏感;
离线机房 :主要是部署一些离线存储,计算相关的服务,对时延不敏感,但是对吞吐要求高。
两个数据中心之间通过专线连接,许多重要服务都依赖于专线进行跨机房调用,所以维持专线的稳定十分重要。专线流量是衡量专线是否稳定的重要指标之一,如果专线流量达到专线的额定带宽,就会导致跨专线服务之间的调用出现大量的超时或失败。
一般而言,服务的吞吐都不会特别高,还远远达不到专线带宽的流量上限,甚至连专线带宽的一半都达不到,但是在我们的算法场景中有一些比较特殊的情况:算法模型的训练在离线机房,依赖 HDFS 上的海量数据集,以及 Spark 集群和机器学习平台进行大规模分布式训练,训练的模型结果存储在 HDFS 上,一个模型甚至能达到数十上百 GB;在模型上线时,算法服务会从在线机房跨专线读取离线 HDFS 上的模型文件,而算法服务一般有数十上百个容器,这些容器在并发读取 HDFS 上的文件时,很轻易就能将专线带宽打满,从而影响其他跨专线服务。
2 多 HDFS 集群
在早期,我们解决算法模型跨机房读取的方式非常简单粗暴,部署一套新的 HDFS 集群到在线机房供算法业务使用,业务使用模型的流程如下:
多 HDFS 集群的架构虽然解决了专线流量的问题,但是依然存在一些问题:
基于以上痛点,我们自研了多云缓存服务—UnionStore。
3 自研组件 UnionStore
3.1 简介
UnionStore 顾名思义,就是联合存储的意思,它提供了标准的 S3 协议来访问 HDFS 上的数据,并且以对象存储来作为跨机房缓存。UnionStore 目前在知乎有两种使用场景:
模型上线场景: 部署到在线机房,作为跨机房缓存使用:
用户在向 UnionStore 请求读取文件时,会先检查文件是否已经上传到对象存储上:
模型训练场景: 部署到离线机房,作为 HDFS 代理使用,目的是为业务提供 S3 协议的 HDFS 访问方式,通过,业务就能挂载 HDFS 到本地目录,读取训练数据进行模型的训练。
模型训练场景是我们 UnionStore 上线后的扩展场景,之前我们尝试过很多 HDFS 挂载 POSIX 的方式,但是效果都不太理想,主要体现在重试方面,而 UnionStore 正好提供了 S3 协议,s3fs-fuse 重试做的不错,所以我们最后选择了 UnionStore + s3fs-fuse 对 HDFS 进行本地目录的挂载。
其工作流程如下:
相比于之前多 HDFS 集群方案,UnionStore 的优势如下:
3.2 实现细节
UnionStore 的完整架构图如下:
在使用对象存储作为缓存时,UnionStore 有三个核心组件:
UnionStore Server: 无状态节点,每一个节点都能单独提供服务,一般会部署多个,用于分摊流量;
Object Storage: 对象存储,用于缓存 HDFS 上的数据,一般是在哪个云厂商就使用对应云厂商提供的对象存储,流量费用几乎可忽略;
Task Manager: 任务管理器,用于存储缓存任务,可用 MySQL 和 Redis 实现。
基于这三个组件我们在 UnionStore 上实现了一系列有用的功能。
文件校验: 文件被缓存至对象存储后,如果 HDFS 上的文件做了修改,UnionStore 需要检查到文件的变更,确保用户不会读取到错误的文件。这里我们在将 HDFS 文件上传至对象存储时,会将 HDFS 文件的大小,最后修改时间,checksum 等元信息存储到对象存储文件的 UserMetadata 上,用户在读取文件时,会检查这部分的信息,只有当信息校验通过时,才会返回对象存储上的文件,如果校验未通过,则会重新缓存这个文件,更新对象存储上的缓存。
读写加速: 对象存储的单线程读写速度大约在 30-60MB/sec,远远小于 HDFS 的吞吐,如果不做特殊处理,是很难满足业务的读写需求的。在读方面,我们利用对象存储的 RangeRead 接口,多线程读取对象存储上的数据返回给用户,达到了与 HDFS 相同的读取速度。在写方面,我们利用对象存储的 MultiPartUpload 接口,多线程上传 HDFS 上的文件,也能达到与 HDFS 相同的写入速度。
文件仅缓存一次: 因为 UnionStore Server 被设计成了无状态节点,所以它们之间是无法互相感知的。如果有多个请求同时打到不同的 Server 节点上来请求未缓存的文件,这个文件可能会被不同的 Server 多次缓存,对专线造成较大的压力。我们引入了 Task Manager 这个组件来解决这个问题:
这里所有的状态变更操作都发生在 Server 节点,Task Manager 只负责存储任务信息以及提供队列的原子操作。
3.3 局限
UnionStore 项目在知乎运行了两年,早期并没有出现任何问题,但是随着算法业务规模的不断扩大,出现了以下问题:
另外还有一个关键点,机器学习平台为保证多活,也采用了多云架构,支持了多机房部署,在读取训练数据时,走的是 UnionStore 对 HDFS 的直接代理,没走缓存流程,因为训练数据大部分都是小文件,而且数量特别巨大,小文件都过一遍缓存会导致缓存任务在任务队列里排队时间过长,很难保证读取的时效性,因此我们直接代理了 HDFS。按照这种使用方式,专线带宽在训练数据规模扩大时,依然会成为瓶颈。
以上痛点使我们面临两个选择:一是继续迭代 UnionStore,让 UnionStore 具备高性能缓存能力,比如支持本地 SSD 以及内存缓存;二是寻找合适的开源解决方案,完美替代 UnionStore 的使用场景。基于人力资源的宝贵,我们选择了其二。
4 利用 Alluxio 替代 UnionStore
4.1 调研
我们调研了业内主流的文件系统,发现 Alluxio 比较适合我们的场景,原因有以下几点:
对 Alluxio 的调研让我们非常惊喜,它不仅满足了我们的需求,还给我们“额外赠送”了不少附加功能。
我们在内部对 Alluxio 进行了测试,以 100G 的文件做单线程读取测试,多次测试取平均值,结果如下:
组件 | 调优项 | 缓存速率 | 未命中缓存时读取速率 | 命中缓存时读取速率 | 是否支持边缓存边读取 | 100GB 文件未命中缓存读取时间 | 100GB 文件命中缓存读取时间 |
---|---|---|---|---|---|---|---|
默认配置 HDD 盘 | |||||||
UnionStore | 10 个加速线程 | 否 | |||||
默认配置 NVME 盘 | 1600MB/sec | 是 |
其中 HDFS 因为涉及到 OS 层面的缓存,波动是最大的,从 200MB/sec - 500MB/sec 都有,而 UnionStore 与 Alluxio 在命中缓存时表现十分稳定。
4.2 集群规划
Alluxio 在我们的规划中是每个机房部署一套,利用高性能 NVME 磁盘对 HDFS 和对象存储上的数据进行缓存,为业务提供海量数据的加速服务。
依据业务的使用场景,我们将 Alluxio 集群分为两类。
模型上线加速集群: Alluxio 集群缓存模型本身,利用 S3 Proxy 对外提供只读服务,加速模型的上线;
模型训练加速集群: Alluxio 集群缓存模型训练数据,利用 Alluxio fuse 对 HDFS 上数据与元数据再做本地缓存,加速模型的训练;产出的模型直接通过 Alluxio fuse 写入 HDFS 进行持久化存储。
4.3 模型上线场景适配
4.3.1 场景特点
我们的模型上线场景有以下特点:
针对模型上线场景,我们选择了 S3 Proxy 来为业务提供缓存服务,不使用 Alluxio Client 以及 Alluxio fuse 主要是基于以下考虑:
4.3.2 集群部署
首先是集群的部署方式,在这个场景下,我们的 Alluxio 集群采取了“大集群轻客户端”的方式来部署,也就是提供足够数量的 Worker 与 S3 Proxy 来支撑业务以 S3 协议发起的高并发请求,架构图如下:
我们的集群版本是 2.9.2,在这个版本,S3 Proxy 有 v1 v2 两种实现,可通过配置
alluxio.proxy.s3.v2.version.enabled
进行切换。v2 版本有一个很重要的功能,就是将 IO 操作与元数据操作进行了分类,分别交给不同的线程池去处理。这样做的好处是,让元数据操作能够快速执行,不被 IO 线程卡住,因为一般情况下,元数据请求的 QPS 远远大于读写文件的 QPS。这个功能对我们非常有用,我们 UnionStore 的 QPS 在 25K 左右,其中 90% 的操作都是元数据访问。
整个 Alluxio 集群我们采取了裸金属机部署,Alluxio 也提供了 k8s 的部署方式,但是在我们的权衡之下,还是选择了裸金属机部署,原因如下:
我们除了按照社区文档的推荐将 Master 与 Job Master,Worker 与 Job Worker 部署到同一台机器上,还另外将 S3 Proxy 与 Worker 进行了混布。S3 Proxy 在用户看起来虽然是服务端,但是对 Alluxio 集群来说它还是客户端,而 Alluxio 对于客户端有一个非常重要的优化:
当 Client 与 Worker 在同一节点时,就可以使用短路读的功能,在短路读开启的情况下,Client 将不再利用网络请求调用 Worker 上的 RPC 接口读取数据,而是直接读本地磁盘上的数据,能够极大节省网卡资源。通过 S3 Porxy 访问 Alluxio 时,流量主要分为以下几个部分:
其中 1,2 中的流量远小于 3,4 中的流量,短路读能够将 3 的流量省下,节省约 30%-50% 的流量。
其次是集群的部署规模,在模型读取这个场景,尽管每天的读取总量可达数 PB,但是因为模型文件很快就会过期,所以 Worker 的容量并不需要很大,Worker 网卡的总带宽能够支持读取流量即可。Worker 的数量可按照
流量峰值/(2/3*网卡带宽)
来计算,这里网卡需要预留 1/3 的 buffer 来供 Worker 读取 UFS 以及 Worker 互相同步数据使用。
最后是 Alluxio Master 的 HA 方式,我们选择了 Raft,在我们的测试过程中,在上亿的元数据以及数百 GB 堆的情况下,Master 主从切换基本上在 10 秒以内完成,效率极高,业务近乎无感。
4.3.3 上线与调优
我们的上线过程也是我们调优的一个过程。
在初期,我们只将一个小模型的读取请求从 UnionStore 切换到了 Alluxio S3 Proxy,效果如下:
里面的每一条线段都代表着一个模型的读取请求,线段的长短代表读取数据的花费的时间。
其中阶段一是我们内部的 UnionStore 服务,阶段二是我们直接切换到 S3 Proxy 时的状态,可以很明显的看到换成 S3 Proxy 了以后,模型读取的平均速度有所上升,但是出现了尖刺,也就是偶尔有请求读取的很慢。问题出在模型读取时,总是冷读,也就是模型数据没有经过预热,在文件未预热的情况下,从 Alluxio 读数据最多只能达到与 HDFS 相同的速度,不能充分发挥缓存的能力。而且通过测试,我们发现 Alluxio 在并发请求同一个没有经过预热的文件时,性能会下降的十分严重,甚至达不到直接读 HDFS 的速度。因此我们需要想办法预热文件。
预热文件的手段一般有以下两种:
方式 1 的问题在于需要用户深度参与,有额外的心智负担和开发成本,其次是用户调用 load 命令不可控,如果对一个超大目录进行 load,将会使所有缓存失效。
方式 2 也需要用户提供监听的路径,如果路径是文件比较方便,只需要监听 close 请求即可,但是路径是目录的情况下,涉及到临时文件,rename 等,十分复杂;每次用户新增模型时,都需要我们把路径新加入监控,有额外的沟通成本;另外由于我们这个场景,数据产出与读取的间隔在秒级,监控文件变更链路太长,可能出现一些延迟,从而导致预热方案失效。
基于以上缺点,我们自己设计了一套缓存策略:
冷读文件慢的本质在于通过 Alluxio 读取未缓存文件时,读到哪一个 block 才会去缓存这个 block,没有做到并发缓存 block。因此我们在 S3 Proxy 上添加了一个逻辑,在读取文件时,会将文件按 block 进行分段生成 cache block 任务,平均提交到每一个 Worker 来异步缓存。这样的好处是,客户端在读取前面少量几个未缓存的 block 后,后面的 block 都是已经缓存完毕的,读取速度十分快。此外,由于提前缓存了 block,缓存穿透的问题也能有所缓解,HDFS 流量能够下降 2 倍以上。
此缓存策略需要注意以下几点:
在上线了这个缓存策略后,我们进入了阶段三,可以看到,阶段三的尖刺全部消失了,整体的速度略微有所提升。因为我们是对小文件(1GB 左右)进行的缓存,所以提升效果不明显。经过我们测试,此缓存策略能够提升读取大文件(10GB 及以上)3-5 倍的速度,而且文件越大越明显。
解决了缓存的问题后,我们继续切换更多模型的读取到 S3 Proxy,效果如下:
本次我们另外切换了三个模型的读取请求到 S3 Proxy,其中橙色模型是我们之前已经切换到 S3 Proxy 的模型,本次新增的模型最大达到了 10G,读取流量峰值为 500Gb/sec。
这次我们同样分为三个阶段,阶段一是橙色模型已经切换到 S3 Proxy,其他模型都使用 UnionStore,因为橙色模型的数据量小,并且还用了 Alluxio 加速,所以它的读取速度能够比其他模型的读取速度快上数十倍。
阶段二是我们将其他模型也切换至 S3 Proxy 后的状态,可以看到其他模型读取速度明显变快了,但是橙色模型读取速度受到其他模型的影响反而变慢了,这是一个非常奇怪的现象。最后我们定位到是元数据缓存没有开启的原因,在元数据缓存没有开启的情况下,Alluxio 会将客户端的每一次请求都打到 HDFS 上,加上 S3 Proxy 也会频繁对一些系统目录做检查,这样就导致 Master 同步元数据的负担非常重,性能甚至能下降上千倍。
在这个场景,我们本来是不打算开启元数据缓存的,主要是担心业务对已缓存修改文件进行修改,导致读取到错误的文件,从而影响模型的上线。但是从实践的结果来看,元数据缓存必须要开启来提升 Master 的性能。
与业务方沟通过后,我们制定了元数据一致性的规范:
在开启元数据缓存过后,我们来到了图中的阶段三,可以很明显的看到所有模型数据的读取速度有了飞跃式提升,相比于最开始没有使用 S3 Proxy 读取速度提升了 10+ 倍。这里需要注意的是,10+ 倍是指在 Alluxio 机器数量足够多,网卡足够充足的情况下能达到的效果,我们在实际使用过程中,用了 UnionStore 一半的资源达到了与 UnionStore 同样的效果。
4.3.4 S3 Proxy 限速
我们在模型读取场景上线 Alluxio 的本意是为了提高业务方读取模型的速度,但是因为通过 Alluxio 读数据实在是太快了,反而需要我们给它限速,非常的具有戏剧性。不限速将会面临一个很严重的问题:算法容器在读取模型时,如果文件较大,不仅会影响 S3 Proxy 所在物理机的网卡,也会导致该容器所在的 k8s 宿主机的网卡长时间处于被占满状态,从而影响这一节点上的其他容器。
目前限速的实现主要有以下几种方案:
Worker 端限速: 优点是对所有客户端生效,缺点是对同节点客户端短路读不生效,在我们的场景,S3 Proxy 会走短路读,不能满足我们的需求。
客户端限速: 优点是能够同时对 Alluxio fuse 和 S3 Proxy 生效,缺点是客户端可以自己改配置绕过限制,同时服务端版本和客户端版本可能存在不一致的情况,导致限速失效。
S3 Proxy 限速: 只能对 S3 Proxy 生效,对其他的客户端以及 Worker 都不能生效。
因为我们当前的目标就是替代 UnionStore,业务方访问 Alluxio 的入口只有 S3 Proxy,因此客户端限速和 S3 Proxy 限速都能满足我们的需求,但是从实现的难易角度上考虑,我们最后选择了从 S3 Proxy 层面限速。
我们支持了两种限速策略,一方面是 S3 Proxy 进程全局限速,用于保护 Worker 网卡不被打满;另一方面是单连接限速,用于保护业务容器所在 k8s 节点。限速策略我们已经贡献给了社区,如果感兴趣可以参考:。
4.4 模型训练场景适配
4.4.1 场景特点
我们的模型训练场景有以下特点:
针对模型训练场景,毫无疑问我们应该选择 Alluxio fuse 来提供缓存服务: 1. Alluxio fuse 提供了 POSIX 访问方式; 2. Alluxio fuse 能够利用内存和磁盘做元数据缓存与数据缓存,能够最大程度利用 GPU 机器上闲置的物理资源。
4.4.2 性能测试
在上线前,我们对 fuse 用 fio 进行了压测。
Alluxio fuse 配置:
变量 | 值 |
---|---|
容器镜像 | 社区版本 alluxio/alluxio:2.9.0 |
容器总内存 | |
元数据缓存 | 60 秒内核元数据缓存 |
DirectoryMemory | |
垃圾回收器 | |
JDK 版本 | OpenJDK 11.0.18 |
内核数据缓存 | auto_cache |
测试结果如下:
测试项 | 结果 |
---|---|
本地磁盘顺序读 | 1800MB/sec |
本地磁盘随机读 | 1000MB/sec |
fuse 1G 文件顺序读 | 1700MB/sec |
fuse 10G 文件顺序读 | 1700MB/sec |
fuse 100G 文件顺序读 | |
fuse 随机读 |
以上结果均针对数据已缓存至 fuse 本地磁盘的情况,1G 文件与 10G 文件读取时,速度是 100G 文件的两倍,这是因为容器的内存为 40G,有充足的 pagecache 来缓存 1G 与 10G 的文件,但是 100G 的文件没有充足的 pagecache,所以性能会下降,但是也能达到不错的速度,整体行为符合预期。
4.4.3 集群部署
Alluxio fuse 的部署方式我们选择了以 DaemonSet 部署,通过 host path 进行映射,没有选择 CSI 部署,主要是基于以下考虑:
这里对挂载点恢复做一个说明,一般情况下,如果 Alluxio fuse 容器因为各种异常挂了,哪怕 fuse 进程重新启动起来,将目录重新进行挂载,但是在业务容器里的挂载点也是坏掉的,业务也读不了数据;但是如果做了挂载点恢复,Alluxio fuse 容器启动起来以后,业务容器里的挂载点就会自动恢复,此时如果业务自身有重试逻辑,就能不受影响。Alluxio fuse 进程的挂载点恢复包括两个部分,一部分是挂载点本身的恢复,也就是 fuse 进程每次重启后要挂到同一个挂载点;另一部分是客户端缓存数据的恢复,也就是 fuse 进程每次重启后缓存数据目录要与原先保持一致,避免从 Alluxio 集群重复拉取已经缓存到本地的文件。挂载点恢复在 CSI 里需要做一些额外的开发来支持,但是如果是以 host path 的方式映射,只要在业务容器里配置了 HostToContainer 即可,不需要额外的开发。
我们 fuse 进程的部署架构图如下:
在这个场景下,我们的 Alluxio 集群采取了“小集群重客户端”的方式来部署,即提供一个规模较小的 Alluxio 集群,只用来做数据的分发,性能和缓存由 Alluxio fuse 自身保证。Alluxio 集群只需要提供高配置的 Master 和少量的 Worker 即可,集群整体的部署架构如下:
按照这种部署模式,3 台 Raft HA 的 Master 与 少量 Worker 就可支撑起 fuse 进程大规模的部署。
4.4.4 Alluxio fuse 调优
首先是元数据缓存,Alluxio fuse 可开启元数据缓存,这里容易与 Master 对 UFS 元数据的缓存弄混淆,我们简单做个说明:
所以建议在开启 fuse 元数据缓存后,设置
alluxio.user.file.metadata.sync.interval=0
以便每次 fuse 在本地元数据缓存失效后,都能拿到 UFS 最新的元数据。
另外 fuse 的元数据缓存可以通过一些特殊的命令来更新(需要配置
alluxio.fuse.special.command.enabled=true
):
元数据缓存可通过以下命令进行强制刷新,假设我们的 mount 目录为
/mnt/alluxio
,利用以下命令可以刷新所有元数据缓存:
ls -l /mnt/alluxio/.alluxiocli.metadatacache.dropAll
复制代码
利用以下命令可以刷新指定目录(这里以
/user/test
为例)的元数据缓存:
ls -l /mnt/alluxio/user/test/.alluxiocli.metadatacache.drop
复制代码
在代码中(以 python 为例),可以这样清理元数据:
print(os.path.getsize("/mnt/alluxio/user/test/.alluxiocli.metadatacache.drop"))
复制代码
但是需要注意,内核元数据缓存是清理不掉的,所以这里推荐内核元数据缓存设置一个较小的值,比如一分钟,用户空间元数据缓存设置一个较大的值,比如一小时,在对元数据有一致性要求的时候,手动刷新用户空间元数据缓存后,等待内核元数据缓存失效即可。
元数据缓存和数据缓存同时开启的情况下,清理元数据缓存的命令在使用上会有一些问题,我们进行了修复,参考:。
其次就是数据缓存,我们的 Alluxio fuse 因为是用 DeamonSet 的方式进行的部署,所以数据缓存我们基本上可以用满整台物理机的磁盘,极大降低了 Alluxio Worker 的流量。
最后就是资源配置,因为每个机器只起一个 fuse 进程,所以可以适当给 fuse 进程多分配给一些 CPU 和内存,CPU 可以适当超卖,以处理突然激增的请求。
内存方面,首先是堆内存的配置,如果开启了用户空间元数据缓存,按照
缓存路径量数 * 2KB * 2
来设置 Xmx。另外 DirectoryMemory 可设置大一点,一般 8G 够用。如果开启了内核数据缓存,还需要给容器留存一些空间来存放 pagecache,因为 kubernetes 计算容器内存使用量会包含 pagecache 的使用量。关于 pagecache 是否会引起容器 OOM,我们查找了很多文档都没有得到准确的结论,但是我们用如下配置进行了压测,发现容器并不会 OOM,并且 fuse 的表现十分稳定:
变量 | 值 |
---|---|
数据缓存大小 | |
元数据缓存 | 60 秒内核元数据缓存 |
DirectoryMemory | |
垃圾回收器 | |
Java 版本 | OpenJDK 11.0.18 |
压测 QPS | |
压测流量 |
4.4.5 上线结果
我们的算法模型训练切换至 Alluxio fuse 后,模型训练的效率达到了本地磁盘 90% 的性能,相比于原来 UnionStore 的 s3fs-fuse 的挂载,性能提升了约 250%。
5 S3 Proxy 在大数据场景的应用
回顾模型上线场景,我们不仅为算法业务提供了模型加速读取的能力,还沉淀下来了一个与对象存储协议兼容,但是下载速度远超普通对象存储的组件,那就是 Alluxio S3 Proxy,所以我们现在完全可以做一些”拿着锤子找钉子“的一些事情。
这里介绍一下我们大数据组件的发布与上线流程,流程图大致如下:
下面用文字简单描述:
其中 Kosmos 是我们自研的包管理系统,其诞生的背景可以参考:Flink 实时计算平台在知乎的演进;另外我们的大数据运维平台也有相应的专栏,感兴趣可以查看:Ansible 在知乎大数据的实践。
一方面,这个流程最大的问题在于大规模上线节点时,从对象存储下载二进制包速度过慢。比如我们要对所有的>
另一方面,对象存储在不同的机房使用时,也会面临外网流量的问题,造成比较高的费用;所以这里对 Kosmos 做了多机房改造,支持向不同的对象存储上传二进制包,用户在请求 Kosmos 时,需要在请求上加上机房参数,以便从 Kosmos 获取同机房对象存储的下载链接,如果用户选错了机房,依然会使用外网流量。
上述问题其实可以通过改造大数据运维平台来解决,比如将下载与部署逻辑解耦,在节点上以较高的并发下载二进制包后再进行滚动部署,但是改造起来比较费时费力,更何况我们现在有了更高效下载文件的方式— Alluxio S3 Proxy,所以更没有动力来做这个改造了。
我们将 Kosmos 的对象存储挂载到 Alluxio 上,Kosmos 在被请求下载时,返回 Alluxio S3 Proxy 的只读链接,让用户从 S3 Proxy 读取数据,改造后的流程图如下:
经过我们的改造,Kosmos 几乎所有的下载请求都能在 1-2 秒内完成,相比于从对象存储下载,快了 90% 以上,下图是我们的生产环境中,Kosmos 分别对接对象存储与 Alluxio 的下载速度对比,其中 Alluxio S3 Proxy 被我们限速至 600MB/sec:
此外 Alluxio 我们也进行了多机房部署,支持了 Kosmos 的多机房方案,哪怕是用户选错了机房,也不会造成额外的外网流量,仅仅只是会请求其他机房的 Alluxio 集群,消耗一定的专线带宽。
6 权限相关
Alluxio 在与 HDFS 对接时,会继承 HDFS 的文件权限系统,而 HDFS 与 Alluxio 的用户可能不一致,容易造成权限问题。权限问题比较重要,所以我们单独用一个章节来做介绍。
我们通过研究代码与测试,总结了基于 Alluxio 2.9.2 版本(HDFS 与 Alluxio 的认证方式都是 SIMPLE),用户与权限的映射关系,总览图如下:
首先是 Alluxio Java Client 的用户:Alluxio Java Client 与 Alluxio 交互时,如果配置了
alluxio.security.login.username
,Alluxio 客户端将会以配置的用户访问 Alluxio 集群,否则将会以 Alluxio Java Client 的启动用户访问 Alluxio。
Alluxio Master/Worker 在与 HDFS 交互时,如果 Master/Worker 在启动时配置了环境变量
HADOOP_USER_NAME
(可在
alluxio-env.sh
配置),则 Master/Worker 将会以配置的用户访问 HDFS,否则将会以 Master/Worker 的进程启动用户访问 HDFS。这里需要注意,Master 和 Worker 尽量配置一样的 HDFS 用户,否则一定会造成权限问题。
在向 HDFS 写入文件时,Alluxio 会先以 Master/Worker 配置的 HDFS 用户写入文件,写完以后会调用 HDFS 的 chown 命令,将文件的 owner 修改为 Alluxio Java Client 的用户,这里我们举例说明:假设 Alluxio 启动用户为 alluxio,Alluxio Java Client 用户为 test,在向 HDFS 写入文件时,Alluxio 会先将文件以 alluxio 账号写到 HDFS 上,再将文件 chown 变成 test 用户,这时如果 alluxio 用户不是 HDFS 超级用户,在 chown 时会发生错误(比较坑的一点是这个错误 alluxio 不会抛出给客户端),导致 Alluxio 上看到的文件 owner 是 test,但是 HDFS 上的文件 owner 时 alluxio,造成元数据不一致。
其次是 S3 Proxy 的用户,S3 Proxy 它也是一个比较特殊的 Alluxio Java Client,但同时它也是一个 Server 端,这里主要是用户请求 S3 Proxy 的 AK SK 与 HDFS 用户的映射。S3 Proxy 默认会将用户的 AK 映射成访问 Alluxio 集群的用户,这里也可以自己实现映射关系,比如将 AK 映射成特定的用户,S3 Proxy 里有相关插件。
最后是 Alluxio fuse 的用户,Alluxio fuse 因为涉及到 linux 文件系统,而且有多种与 linux 本地文件系统相关的实现,所以比前面的更加复杂,这里我们只讨论默认情况,也就是
alluxio.fuse.auth.policy.class=alluxio.fuse.auth.LaunchUserGroupAuthPolicy
时的情况。用户在访问挂载目录时,用的是当前 linux 用户,用户看到挂载目录里所有文件的 owner 都是 fuse 进程启动用户;fuse 在写本地缓存目录时,用的是 fuse 进程的启动用户,此外 fuse 进程与 Alluxio 集群交互时又完全遵循 Alluxio Java Client 的逻辑。
综上所述,比较推荐的用户设置方式为:
7 其他问题
在上线过程中,我们遇到了很多问题,其中大部分都跟配置项调优有关。遇到这些问题的原因主要还是因为 Alluxio 是面相通用设计的缓存系统,而用户的场景各式各样,很难通过默认配置完美适配,比如我们有多套 Alluxio 集群,每套集群用来解决不同的问题,所以这些集群的配置都有些许差异。多亏 Alluxio 提供了许多灵活的配置,大部分问题都能通过修改配置解决,所以这里只介绍一些让我们印象深刻的“代表”。
最大副本数: 在模型上线场景,缓存副本数我们不设上限,因为在算法模型在读取时,往往是一个大模型同时几十个甚至上百个容器去读,占用的存储不多,但是读取次数多,并且仅高并发读取这一次,很少有再读第二次的情况。所以这里对每一个缓存文件副本数不做限制,可以让每个 Worker 都缓存一份,这样能够达到最大的吞吐,拥有最好的性能。在模型训练场景,我们将缓存副本数设置为 3,一方面是因为训练数据量很大,需要节省存储,另一方面是 Alluxio fuse 的本地缓存会承担大部分流量,所以对于 Worker 的吞吐要求相对较低。
S3 Proxy ListObjects 问题: 我们发现 S3 Proxy 在实现 ListObjects 请求时,会忽略 maxkeys 参数,列出大量不需要的目录。 比如我们请求的 prefix 是, maxkeys 是 1,S3 Proxy 会递归列出下所有文件,再从所有文件里挑选出满足 prefix的第一条数据,这样不仅性能差,也会导致可能出现 OOM 的情况,我们采用临时方案进行的修复,感兴趣可以参考。这个问题比较复杂,需要 Master 与 S3 Proxy 联合去解决,可以期待的进展。
监控地址冲突: 我们监控采用的是 Prometheus 方案,Alluxio 暴露了一部分指标,但是 JVM 指标需要额外在 Master 或者 Worker 的启动参数中添加 agent 与端口暴露出来,添加 agent 以后,因为 monitor 会继承 Master 与 Worker 的启动参数,所以 monitor 也会尝试使用与 Master 和 Worker 同样的指标端口,这会出现 ”Address already in use“ 的错误,从而导致 monitor 启动失败。具体可查看。
Master 异常加载 UFS 全量元数据:
如果一个路径下有 UFS mount 路径,在对这个路径调用 getStatus 方法时,Alluxio master 会递归同步这个路径下的所有文件的元信息。比如路径下的路径是 UFS 的 mount 路径,在调用
getStatus("/a")
的时候,会导致下面的元数据被全量加载。如果是一个大路径,可能会导致 Master 因为加载了过多的元数据而频繁 GC 甚至卡死。具体可查看。
Master 频繁更新 access time: 我们在使用过程中,发现 Master 偶尔会很卡,通过 Alluxio 社区同学的帮助,定位到问题来自 Master 频繁更新文件的最后访问时间,通过合入,我们解决了这个问题。
8 总结与展望
其实从 2022 年的下半年我们就开始调研 Alluxio 了,但是因为种种原因,中途搁置了一段时间,导致 Alluxio 推迟到今年才上线。在我们调研与上线的过程中,Alluxio 社区是我们最强大的外援,为我们提供了海量的帮助。
本次我们在算法场景对 Alluxio 小试牛刀,取得的结果令人十分惊喜。
从性能上讲,在算法模型上线的场景,我们将 UnionStore 用 Alluxio 替换后,最高能够获得数十倍的性能提升;在模型训练场景,我们配合 Alluxio fuse 的本地数据缓存,能够达到近似本地 NVME 磁盘的速度,相比于 UnionStore + s3fs-fuse 的方案,性能提升了 2-3 倍。
从稳定性上讲,在 HDFS 抖动或者升级切主的时候,因为有数据缓存和元数据缓存,Alluxio 能够在一定时间内不受影响,正常提供服务。
从成本上讲,Alluxio 相比于 UnionStore 每年为我们节省了数十万真金白银,而且性能上还有盈余。
从长远的发展来看,Alluxio 具有强大的可扩展性,尤其是 Alluxio 的新一代架构 Dora ,能够支持我们对海量小文件缓存的需求,这让我们更有信心支撑算法团队,面对即将到来的人工智能浪潮。
最后再次感谢 Alluxio 团队,在我们上线的过程中为我们提供了大量的帮助与建议,也希望我们后续能够在大数据 OLAP 查询加速场景以及分布式数据集编排领域继续深入合作与交流。