前段时间我们边缘部署的 minio 出现下载和删除文件都很慢的问题,严重影响了相关业务功能,因此进行了分析和解决。本文记录了完整的分析过程, 涉及了以下几个方面:
使用 strace 分析系统调用
使用 trace-cmd 观测内核函数堆栈和事件
NFS 协议及 noac 选项介绍
minio 删除文件的流程分析
问题概述
我们遇到的主要问题有两个:
下载 minio 中存储的文件时, 概率性地会长时间无响应, 导致相关页面的视频点播失败
存储服务器的 2PB 容量已达 97%, 触发了写保护, 无法继续写入。为释放空间, 需要先删除旧数据, 但删除 minio 中的文件异常缓慢, 导致删除进度不达预期
第一个问题是最先暴露的,出问题的时候,curl 文件长时间无返回:
引发了如下视频点播失败:
第二个问题是晚一点暴露的,存储服务器的容量是 2PB,在前段时间已经达到了 97%,触发了存储服务器的写保护,导致所有的写入删除都失败了。不得不停止写入,关闭写保护,想办法先删除数据,腾出容量。在删除数据的过程中,发现删除接口非常慢,导致我们没法在短时间内释放容量,开放上传功能。
这两个问题,都是指向了 minio 接口慢,于是进行了一系列的分析,过程记录如下。
部署架构
部署架构如下,客户端通过 NFS 协议挂载了由存储供应商提供的 4 节点 2PB 总容量存储集群, 在其上运行 minio 服务, 存储大量 1MB~4MB 的小文件。
为更好理解后续分析, 我们先简单介绍一下 NFS 协议原理。
NFS 协议简介
通过 tcpdump 抓包, 可以观察到使用 NFS 协议读取一个文件的典型过程如下:
bash
复制代码
cat /mnt/ya/file.mb > /dev/null
读取/mnt/ya/file.mb 文件时, 涉及到 3 个文件/文件夹的 fileHandle:
0x7867b7e8: /mnt
0x3b936eb4: /mnt/ya
0x2977a4cf: /mnt/ya/file.mb
可以看到读取 /mnt/ya/file.mb 经历了下面这几步:
获取 /mnt 的文件夹属性
判断是否有 /mnt 文件夹的访问权限
在 /mnt 目录查找 ya 文件夹的 filehandle,查到是 0x3b936eb4
判断是否有 /mnt/ya 文件夹的访问权限
获取 /mnt/ya 文件夹属性
在 /mnt/ya 目录查找 file.mb 文件的 filehandle,查到是 0x2977a4cf
判断是否有 /mnt/ya/file.mb 的访问权限
获取 /mnt/ya/file.mb 的文件属性
读取 /mnt/ya/file.mb 文件
这么来看 NFS 协议是一个低效的协议,读取一个文件的过程就是逐层判断是否有权限。为了优化减少网络请求,默认情况下,NFS 客户端会缓存文件和目录的属性(如权限、大小和时间戳),以减少对 NFS 服务器的远程过程调用的需求。
初步排查接口慢
通过观察 minio 进程的线程状态, 发现绝大部分线程都处于不可中断的 D 状态, 推测它们可能被阻塞在文件 IO 操作上。
minio 是 go 语言开发的,得益于 go 方便的 profile 机制,可以非常方便看到 goroutine 的堆栈信息。MinIO 的 mc 工具(MinIO Client)是一个现代化的命令行工具,提供了类似于 UNIX 命令(如 ls、cat、cp、mirror、admin)的功能。我们可以通过它提供的 admin profile 功能来触发 Go 的 profile。
使用下面的命令采集 goroutine 的数据
shell
复制代码
$ ./mc admin profile start --type=goroutines minio/ # 等待一段时间 # 停止 profile,会生成 profile.zip 文件 $ ./mc admin profile stop minio/ $ unzip profile.zip $ ls -l -rw------- 1 root root 35K 6月 7 18:57 profile-127.0.0.1:9000-goroutines.txt -rw------- 1 root root 36K 6月 7 18:57 profile-127.0.0.1:9000-goroutines-before.txt -rw------- 1 root root 1.5M 6月 7 18:57 profile-127.0.0.1:9000-goroutines-before,debug=2.txt -rw-r--r-- 1 root root 512K 6月 7 18:57 profile.zip
查看生成的 profile 文件,我们发现大量的接口阻塞在系统调用上,根据不同的文件操作,有些阻塞在 .syscallopenat(打开文件)、syscall.fstatat(查看文件信息)、syscall.unlinkat(删除文件)等。
通过这个 profile 我们可以确定是 minio 发起了系统调用,到了内核 nfs 模块,但 nfs 模块迟迟未返回响应,导致 minio 长时间阻塞在系统调用上。
至此已经不是 minio 这个 go 程序能处理的了。出问题的时候,通过 ls 命令直接去查看 nfs 的文件,一样会卡住无返回。
为进一步分析 NFS 内核行为, 我们使用 trace-cmd 工具查看所有 NFS 相关事件。
arduino
复制代码
trace-cmd record -e "nfs:*"
发现 minio 频繁执行 nfs_refresh_inode 操作, fhandle 为 0x463b99f0
经过分析,频繁触发的 fhandle 是一个文件夹,路径是 /mnt/minio107054/data1/store-pub,除此之外还有一个文件夹 inode 更新非常频繁 /mnt/minio107054/data1/.minio.sys。
不仅是 minio 表现是这样,当我直接执行 ls 的时候,trace-cmd 的输出也是如此。
这两个文件夹的 refresh inode 操作都返回了 invalid data 错误, 提示 inode 缓存数据无效。
为深入追踪内核行为, 我们使用 trace-cmd 的 function_graph 功能分析内核函数调用栈。以 stat /mnt/minio107054/data1/store-pub/xxx.ts 为例:
bash
复制代码
trace-cmd record -p function_graph -F stat /mnt/minio107054/data1/store-pub/xxx.ts
会生成一个非常长的内核函数调用栈文件,我们先来确定哪个函数耗时较长。
通过这个调用栈,我们可以清晰的看到,是 fstatat 这个系统调用执行了近 260s(4 分钟+)之久。中间执行的函数有 70 多万行。我们来具体看下这 70 多万行到底发生了什么。
到这里我们基本上清楚了,系统调用慢的原因是,由于大目录属性频繁变更, 导致 inode 缓存数据失效, NFS 客户端需要不断从 NFS 服务器获取最新的 inode 数据。为了解决这个问题,存储原厂的工程师提议我们启用 noac 挂载选项来禁用客户端的属性缓存来临时规避这个问题。
使用 noac 选项可以禁用文件和目录属性的缓存。这样每次客户端访问文件属性时,都会直接从 NFS 服务器获取最新的数据,而不是使用本地缓存的数据。这样可以临时绕开上面这类 /mnt/minio107054/data1/store-pub 大文件夹的属性变更的影响。但是会大大增加网络通信的次数,但是这明显会好过长时间卡在属性更新上。
启用 noac 以后,删除依然非常慢,大并发下需要 20 多秒才能删除一个文件,接下来我们来解决删除慢的问题。
文件删除为什么慢
我们接下来接续分析为什么删除文件会慢。因为删除文件会触发系统调用,我们可以用 strace 来观测文件删除的过程。
r
复制代码
strace -tt -T -f -p `pidof minio` -o strace.out
通过 strace 追踪, 发现 minio 删除一个文件如 store-pub/xxx.ts 实际会删除以下四个文件:
数据文件 store-pub/xxx.ts
元数据文件 .minio.sys/buckets/store-pub/xxx.ts/fs.json (文件)
空文件夹 .minio.sys/buckets/store-pub/xxx.ts
非空文件夹 .minio.sys/buckets/store-pub(删除会失败)
strace 可以显示系统调用的时间和返回值。通过看 strace 日志我们发现删除最后一个非空文件夹 .minio.sys/buckets/store-pub 时,一定会失败,返回 ENOTEMPTY(文件夹非空),耗时长达 10 几秒到 20 几秒。
实际上, 这一删除操作是多余的。这个元数据目录是 bucket 的根目录,除非 bucket 下所有文件都被删完,否则不可能是空的。
minio 这一部分删除的逻辑可谓简单粗暴,比如你要删除 /a/b/c/d.txt,你可以指定 base_path,比如 /a,它的删除逻辑是尝试递归所有上层目录 ,直到遇到 basepath 或者删除失败。
删除 /a/b/c/d.txt
删除 /a/b/c/
删除 /a/b
尝试删除 /a,发现与 base_path 相等,退出
这种实现方式实现比较简单,删除文件的同时,可以删除这个文件路径上所有的空目录。
这里删除元数据文件时 .minio.sys/buckets/store-pub/xxx.ts/fs.json,它传入的 base_path 是 .minio.sys/buckets/
go
复制代码
const minioMetaBucket = ".minio.sys"; minioMetaBucketDir := pathJoin(fs.fsPath, minioMetaBucket) if bucket != minioMetaBucket { // Delete the metadata object. // fsMetaPath = minio.sys/buckets/store-pub/xxx.ts/fs.json // minioMetaBucketDir = err = fsDeleteFile(ctx, minioMetaBucketDir, fsMetaPath) if err != nil && err != errFileNotFound { return objInfo, toObjectErr(err, bucket, object) } } // basePath:要往上删到哪一级路径 // deletePath:要删除文件的路径 func fsDeleteFile(ctx context.Context, basePath, deletePath string) error { }
于是修改 minio 源码,增加 basePath 的层级到 minio.sys/buckets/store-pub
这样就不会触发这个超级大目录 minio.sys/buckets/store-pub 的删除行为。
修改上面重新构建镜像部署,发现删除从 20s+ 左右讲到了 10s+ 级别。还是不够快,于是继续分析。
minio 现在会调用四次 unlinkat 删除,其中前两次是删除真实的文件
数据文件 store-pub/xxx.ts
元数据文件 .minio.sys/buckets/store-pub/xxx.ts/fs.json (文件)
后两次第一次是尝试删除元数据文件 .minio.sys/buckets/store-pub/xxx.ts,但是因为是文件夹,删除会失败,第二次以删目录的方式去删除。
后两次删除删除 .minio.sys/buckets/store-pub/xxx.ts 这个空目录非常慢,为什么慢原因还不知道。
但是这个空目录几乎不影响我们释放磁盘空间,该删的数据文件 xxx.ts 以及元数据文件 fs.json 都已经删除成功了。于是我们再次大胆的先跳过这个空目录的删除。把 basePath 层级加大到 .minio.sys/buckets/store-pub/xxx.ts
这样删除操作就只会真正删除下面这两个文件
数据文件 store-pub/xxx.ts
元数据文件 .minio.sys/buckets/store-pub/xxx.ts/fs.json (文件)
删除这两个普通文件是非常快的:
可以看到,我们现在可以在 100ms 以内就完成删除了文件。接口整体的耗时在大并发下也可以到秒级。
继续分析 strace 日志,可以看到 minio 在删除文件前会先对元数据文件加锁,因为我们不会并发删除同一个文件,这一步的时间消耗也可以省掉。
于是继续改代码,去掉对元数据文件加锁,高并发下接口总耗时降低到大概在 500ms 左右。
删除接口的函数从之前的 20s+ 降低到 500ms,有了明显的改善。经过这些改动以后,经过两天的删除,存储容量有了明显回落。
小结
因为大目录频繁更新,导致 nfs 客户端缓存频繁失效,导致 nfs 客户端忙于获取最新的 inode,导致很多请求阻塞,启用 noac 可以临时缓解
因为 NFS 的性能问题,导致删除非空大目录非常慢,minio 恰好因为递归删除触发到了删除大目录这个问题,导致删除非常慢
minio 删除文件的过程会触发删除空目录、元数据加锁,大并发下非常慢,我们可以临时去掉增快删除速度。
后记
其实 MINIO + NFS 的组合是强烈不推荐的,坑太多了,去 minio 的 github issue 查找相关的问题,会看到开发者回复不会处理 NFS 相关的 issue。强烈不建议大家用 MINIO+NFS 这个坑人组合。
这俩组合要想稳定运行下去,需要再对 NFS 和 minio 的源码有更深入的理解。
希望通过这篇文章,你可以了解到 trace-cmd、strace、go profile 相关工具的使用,以及 NFS 协议相关的内容。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。