前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一起 MINIO(Go) 响应慢故障实录分析

一起 MINIO(Go) 响应慢故障实录分析

作者头像
挖坑的张师傅
发布2024-06-19 20:25:16
2090
发布2024-06-19 20:25:16
举报
文章被收录于专栏:张师傅的博客

前段时间我们边缘部署的 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 协议读取一个文件的典型过程如下:

代码语言:javascript
复制

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 的数据

代码语言:javascript
复制
$ ./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 相关事件。

代码语言:javascript
复制
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 为例:

代码语言:javascript
复制
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 来观测文件删除的过程。

代码语言:javascript
复制
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/

代码语言:javascript
复制
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 协议相关的内容。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 张师傅的博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题概述
  • 部署架构
  • NFS 协议简介
  • 初步排查接口慢
  • 文件删除为什么慢
  • 小结
  • 后记
相关产品与服务
云点播
面向音视频、图片等媒体,提供制作上传、存储、转码、媒体处理、媒体 AI、加速分发播放、版权保护等一体化的高品质媒体服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档