Skip to main content

问题排查案例

这里收录常见问题的具体排查步骤。

挂载失败 / 挂载时卡住

如果挂载 JuiceFS 时报错失败,或者卡住并不断重试,请首先详读报错信息,JuiceFS 客户端日志中会暴露出绝大部分错误,比如 AK/SK 填写错误导致调用对象存储 API 创建 bucket 失败。

如果发现问题并不简单,需要深入排查,那么为了隔离旧版本问题,我们推荐将客户端版本升级到最新:

sudo juicefs version --upgrade

升级完毕重新挂载,如果问题复现,请在挂载命令后面加上 --foreground --verbose 参数使挂载在前台运行并输出调试信息来排查问题:

sudo juicefs mount $VOL_NAME /jfs --foreground --verbose

比方说,调试信息显示是解析域名失败,无法连接元数据服务,那么请确认上游 DNS 服务器正常运作并刷新本地 DNS 缓存。

如果能够成功解析域名但是无法与元数据服务器建立连接,很可能是由于防火墙限制了端口访问。请确保挂载的机器能够访问外网 9300 - 9500 TCP 端口使其能与我们的元数据服务器通信。

如果您是使用本地 iptables 做防火墙,可以执行以下命令进行允许 9300 - 9500 范围端口:

sudo iptables -A OUTPUT -p tcp --match multiport --dports 9300:9500 -j ACCEPT

如果你使用的是公有云平台的安全组,请登录公有云的控制台进行相应调整。

读写失败

JuiceFS 的读写失败可能由多种多样的原因构成,无法一一列举。不过在发生读写错误时,你总能在客户端日志(默认 /var/log/juicefs.log,详见 mount 参数)里找到报错原因,在这里介绍一些常见故障和排查步骤。

无法与元数据服务通信

JuiceFS 客户端需要时刻与元数据集群保持畅通网络连接,才能高速访问元数据服务。如果运行途中网络访问中断,一般会看见大量类似下方日志:

<ERROR>: request 402 (40) timeout after 1.000603709s
<ERROR>: request 428 (3283870) failed after tried 30 times

以上日志中的 request 402request 428 都是客户端与元数据服务通信的 RPC 命令代号,大部分属于文件系统的元数据操作。如果日志中大量出现此类 RPC 请求超时的错误,可以用下方命令进行排查:

# 在 TCP 连接里定位到客户端连接的元数据服务地址
lsof -iTCP -sTCP:ESTABLISHED | grep jfsmount
# 如果客户端已经卸载,找不到 TCP 连接,也可以直接在配置文件里找到元数据服务地址
grep master ~/.juicefs/$VOL_NAME.conf

# 确保对元数据服务端口访问畅通(假设元数据服务地址为 aliyun-bj-1.meta.juicefs.com)
telnet aliyun-bj-1.meta.juicefs.com 9402
# 如果没安装 telnet,也可用 curl / wget,但由于元数据服务并不是 HTTP 协议,只能给出空响应
curl aliyun-bj-1.meta.juicefs.com:9402
# curl: (52) Empty reply from server

# 如果域名无法解析,则需要检查 master_ip 是否能正常访问,master_ip 同样在配置文件中可获得
grep master_ip -A 3 ~/.juicefs/$VOL_NAME.conf
# 用上方的网络排查命令确认 IP 能正常访问

# 如果域名能正常解析,确认结果与配置文件中 master_ip 匹配
dig aliyun-bj-1.meta.juicefs.com

经历上方排查,可能出现的问题以及处理手段如下:

域名无法解析,在云服务中,Metadata 服务域名在公网就能顺利解析,如果客户端无法解析该域名,多半表明宿主机的 DNS 配置有问题,需要予以排查。

不过事实上,就算域名无法解析,只要客户端能顺利访问 Metadata 节点的 IP(配置文件中 master_ip),JuiceFS Client 就能正常工作。因此在私有部署中,并不强求域名一定可以解析,域名的存在主要是为了灾备,如果 Metadata 节点不会发生且不打算迁移,可以不设置域名解析(会产生一些 WARNING 日志)。但如果设置了,一定要设置正确,不要为了避免 WARNING 日志而“乱加域名解析”,否则客户端万一需要重连 Metadata 节点,通过错误的域名解析结果拿到了无法连接的地址,也会直接报错导致挂载无法使用。

master_ip 无法正常访问,在云服务中,客户端一般通过公网 IP 连接 Metadata 服务,如果 IP 无法正常访问,作如下排查:

  • 如果 curlping 等探测命令发生 timeout,往往是安全组、防火墙设置有误,导致丢包,应予以排查
  • 如果对 Metadata 服务端口(默认 9402)探测结果为 Connection Refused,则可以认为访问畅通,但 Metadata 没有在正常运行(通常发生在私有部署)
  • 在私有部署中,可能需要自行控制 Metadata 域名解析,这时候可以按照上方排查示范命令,检查域名解析的结果与配置文件中的 master_ip 匹配,如果二者不相同,根据实际情况修复域名解析,或者重新挂载

与对象存储通信不畅(网速慢)

如果无法访问对象存储,或者仅仅是网速太慢,JuiceFS 客户端也会发生读写错误。你也可以在日志中找到相应的报错。

# 上传块的速度不符合预期
<INFO>: slow request: PUT chunks/1986/1986377/1986377131_11_4194304 (%!s(<nil>), 20.512s)

# 上传失败还可能伴随着打印堆栈信息,阅读出错的函数名,可推测是对象存储上传失败
<ERROR>: flush 9902558 timeout after waited 8m0s [writedata.go:604]
<ERROR>: pending slice 9902558-80: {chd:0xc0007125c0 off:0 chunkid:1986377183 cleng:12128803 soff:0 slen:12128803 writer:0xc010a92240 freezed:true done:false status:0 notify:0xc010e57d70 started:{wall:13891666526241100901 ext:5140404761832 loc:0x35177c0} lastMod:{wall:13891666526250536970 ext:5140414197911 loc:0x35177c0}} [writedata.go:607]
<WARNING>: All goroutines (718):
goroutine 14275 [running]:
jfs/mount/fs.(*inodewdata).flush(0xc004ec6fc0, {0x7fbc06385918, 0xc00a08c140}, 0x0?)
/p8s/root/jfs/mount/fs/writedata.go:611 +0x545
jfs/mount/fs.(*inodewdata).Flush(0xc0007ba1e0?, {0x7fbc06385918?, 0xc00a08c140?})
/p8s/root/jfs/mount/fs/writedata.go:632 +0x25
jfs/mount/vfs.Flush({0x2487e98?, 0xc00a08c140}, 0x9719de, 0x8, 0x488c0e?)
/p8s/root/jfs/mount/vfs/vfs.go:1099 +0x2c3
jfs/mount/fuse.(*JFS).Flush(0x1901a65?, 0xc0020cc1b0?, 0xc00834e3d8)
/p8s/root/jfs/mount/fuse/fuse.go:348 +0x8e
...
goroutine 26277 [chan send, 9 minutes]:
jfs/mount/chunk.(*wChunk).asyncUpload(0xc005f5a600, {0x0?, 0x0?}, {0xc00f849f50, 0x28}, 0xc0101e55e0, {0xc010caac80, 0x4d})
/p8s/root/jfs/mount/chunk/cached_store.go:531 +0x2e5
created by jfs/mount/chunk.(*wChunk).upload.func1
/p8s/root/jfs/mount/chunk/cached_store.go:615 +0x30c
...

如果是网络异常导致无法访问,或者对象存储本身服务异常,问题排查相对简单。但在如果是在低带宽场景下希望优化 JuiceFS 的使用体验,需要留意的事情就稍微多一些。

首先,在网速慢的时候,JuiceFS 客户端上传/下载文件容易超时(类似上方的错误日志),这种情况下可以考虑:

  • 降低上传并发度,比如 --max-uploads=1,避免上传超时。
  • 降低读写缓冲区大小,比如 --buffer-size=64 或者更小。当带宽充裕时,增大读写缓冲区能提升并发性能。但在低带宽场景下使用过大的读写缓冲区,flush 的上传时间会很长,因此容易超时。
  • 默认 GET / PUT 请求超时时间为 60 秒,因此增大 --get-timeout 以及 --put-timeout,可以改善读写超时的情况。

此外,低带宽环境下需要慎用「客户端写缓存」特性。先简单介绍一下 JuiceFS 的后台任务设计:每个 JuiceFS 客户端默认都启用后台任务,后台任务中会执行碎片合并(Compaction)、异步删除等工作,而如果节点网络状况太差,则会降低系统整体性能。更糟的是如果该节点还启用了客户端写缓存,则容易出现碎片合并后上传缓慢,导致其他节点无法读取该文件的危险情况:

# 由于 writeback,碎片合并后的结果迟迟上传不成功,导致其他节点读取文件报错
<WARNING>: readworker: unexpected data block size (requested: 4194304 / received: 0)
<ERROR>: read for inode 0:14029704 failed after tried 30 times
<ERROR>: read file 14029704: input/output error
<INFO>: slow operation: read (14029704,131072,0): input/output error (0) <74.147891>
<WARNING>: fail to read chunkid 1771585458 (off:4194304, size:4194304, clen: 37746372): get chunks/1771/1771585/1771585458_1_4194304: oss: service returned error: StatusCode=404, ErrorCode=NoSuchKey, ErrorMessage="The specified key does not exist.", RequestId=62E8FB058C0B5C3134CB80B6
<WARNING>: readworker: unexpected data block size (requested: 4194304 / received: 0)
<WARNING>: fail to read chunkid 1771585458 (off:0, size:4194304, clen: 37746372): get chunks/1771/1771585/1771585458_0_4194304: oss: service returned error: StatusCode=404, ErrorCode=NoSuchKey, ErrorMessage="The specified key does not exist.", RequestId=62E8FB05AC30323537AD735D

为了避免此类问题,我们推荐在低带宽节点上禁用后台任务。你需要在控制台“访问控制”页面,创建新的“客户端访问令牌”,并取消勾选“允许后台任务”,最后用新生成的 Token 来执行客户端挂载。

读放大

在 JuiceFS 中,一个典型的读放大现象是:对象存储的下行流量,远大于实际读文件的速度。比方说 JuiceFS 客户端的读吞吐为 200MiB/s,但是在 S3 观察到了 2GB/s 的下行流量。

JuiceFS 中内置了预读(prefetch)机制:随机读 block 的某一段,会触发整个 block 下载,这个默认开启的读优化策略,在某些场景下会带来读放大。了解这个设计以后,我们就可以开始排查了。

结合先前问题排查方法一章中介绍的访问日志知识,我们可以采集一些访问日志来分析程序的读模式,然后针对性地调整配置。下面是一个实际生产环境案例的排查过程:

# 收集一段时间的访问日志,比如 30 秒:
cat /jfs/.oplog | grep -v "^#$" >> op.log

# 用 wc、grep 等工具简单统计发现,访问日志中大多都是 read 请求:
wc -l op.log
grep "read (" op.log | wc -l

# 选取一个文件,通过 inode 追踪其访问模式,read 的输入参数里,第一个就是 inode:
grep "read (148153116," op.log

采集到该文件的访问日志如下:

2022.09.22 08:55:21.013121 [uid:0,gid:0,pid:0] read (148153116,131072,28668010496,19235): OK (131072) <1.309992>
2022.09.22 08:55:21.577944 [uid:0,gid:0,pid:0] read (148153116,131072,14342746112,19235): OK (131072) <1.385073>
2022.09.22 08:55:22.098133 [uid:0,gid:0,pid:0] read (148153116,131072,35781816320,19235): OK (131072) <1.301371>
2022.09.22 08:55:22.883285 [uid:0,gid:0,pid:0] read (148153116,131072,3570397184,19235): OK (131072) <1.305064>
2022.09.22 08:55:23.362654 [uid:0,gid:0,pid:0] read (148153116,131072,100420673536,19235): OK (131072) <1.264290>
2022.09.22 08:55:24.068733 [uid:0,gid:0,pid:0] read (148153116,131072,48602152960,19235): OK (131072) <1.185206>
2022.09.22 08:55:25.351035 [uid:0,gid:0,pid:0] read (148153116,131072,60529270784,19235): OK (131072) <1.282066>
2022.09.22 08:55:26.631518 [uid:0,gid:0,pid:0] read (148153116,131072,4255297536,19235): OK (131072) <1.280236>
2022.09.22 08:55:27.724882 [uid:0,gid:0,pid:0] read (148153116,131072,715698176,19235): OK (131072) <1.093108>
2022.09.22 08:55:31.049944 [uid:0,gid:0,pid:0] read (148153116,131072,8233349120,19233): OK (131072) <1.020763>
2022.09.22 08:55:32.055613 [uid:0,gid:0,pid:0] read (148153116,131072,119523176448,19233): OK (131072) <1.005430>
2022.09.22 08:55:32.056935 [uid:0,gid:0,pid:0] read (148153116,131072,44287774720,19233): OK (131072) <0.001099>
2022.09.22 08:55:33.045164 [uid:0,gid:0,pid:0] read (148153116,131072,1323794432,19233): OK (131072) <0.988074>
2022.09.22 08:55:36.502687 [uid:0,gid:0,pid:0] read (148153116,131072,47760637952,19235): OK (131072) <1.184290>
2022.09.22 08:55:38.525879 [uid:0,gid:0,pid:0] read (148153116,131072,53434183680,19203): OK (131072) <0.096732>

对着日志观察下来,发现读文件的行为大体上是「频繁随机小读」。我们尤其注意到 offset(也就是 read 的第三个参数)跳跃巨大,说明相邻的读操作之间跨度很大,难以利用到预读提前下载下来的数据(默认的块大小是 4MiB,换算为 4194304 字节的 offset)。也正因此,我们建议将 --prefetch 调整为 0(让预读并发度为 0,也就是禁用该行为),并重新挂载。这样一来,在该场景下的读放大问题得到很好的改善。

内存占用过高

如果 JuiceFS 客户端内存占用过高,考虑按照以下方向进行排查调优,但也请注意,内存优化势必不是免费的,每一项设置调整都将带来相应的开销,请在调整前做好充分的测试与验证。

  • 读写缓冲区(也就是 --buffer-size)的大小,直接与 JuiceFS 客户端内存占用相关,因此可以通过降低读写缓冲区大小来减少内存占用,但请注意降低以后可能同时也会对读写性能造成影响。更多详见「读写缓冲区」。
  • JuiceFS 挂载客户端是一个 Go 程序,因此也可以通过降低 GOGC(默认 100)来令 Go 在运行时执行更为激进的垃圾回收(将带来更多 CPU 消耗,甚至直接影响性能)。详见「Go Runtime」。
  • 如果你使用自建的 Ceph RADOS 作为 JuiceFS 的数据存储,可以考虑将 glibc 替换为 TCMalloc,后者有着更高效的内存管理实现,能在该场景下有效降低堆外内存占用。