文件系统健康检查不能在/healthz handler中实时stat或write,而应由后台ticker定期采样关键路径的可读写性、inode余量和磁盘可用率,并原子缓存结果供handler快照读取。

文件系统健康检查不是“能不能读/proc/mounts”,而是“当前业务路径是否可读写、inode 是否耗尽、磁盘是否即将满载”。直接在 HTTP handler 里 os.Stat 或 os.WriteFile 一次,既慢又危险——它会阻塞请求、触发 probe 超时、甚至因权限/挂载点异常 panic。
为什么不能在 /healthz handler 里实时 stat 或 write
很多团队把文件系统检查写进 handler,结果 Kubernetes livenessProbe 频繁失败。根本原因有三:
-
os.Stat在 NFS 挂载点卡住几秒是常态,而 K8s 默认 timeout 是 1 秒 -
os.WriteFile尝试写临时文件,可能因 no space left、no inodes left、只读挂载等返回不同错误,但 handler 无法区分是瞬时满载还是永久故障 - 每次请求都执行 I/O,短时间高并发探测(比如每 5 秒 10 个 probe)会压垮底层存储或触发限流
用后台 ticker 定期采样,缓存结果供 handler 读取
正确做法是启动一个独立 goroutine,用 time.Ticker 每 10–30 秒检查一次关键路径,把结果原子写入内存。handler 只做快照读取。
- 检查项必须精简:只盯业务核心路径(如
/var/lib/myapp/data),不扫全盘 - 每个检查加 context.WithTimeout(
ctx, 3 * time.Second),超时即判失败,不等 syscall 返回 - 状态变量用
sync/atomic.Value存储结构体(含ok bool、error string、timestamp int64),避免锁竞争 - 服务 shutdown 时必须调
ticker.Stop(),否则热重启后多个 ticker 并发写同一变量
关键指标怎么查才可靠
文件系统健康不是“路径存在”,而是“能干活”。以下检查必须组合使用:
立即学习“go语言免费学习笔记(深入)”;
-
os.Stat(dir)确认路径可访问且是目录(不是符号链接断裂或权限拒绝) -
unix.Statfs(dir)获取Bavail(可用 block 数)和IFree(可用 inode 数),比df -h更准;当Bavail < 5% * Blocks或IFree < 1000时标记为 warn - 尝试
os.OpenFile(tmpfile, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0600)写一个 1KB 临时文件再os.Remove,验证可写性 —— 这步不能省,只看df会漏掉 no space left on device 的 false positive - 所有 syscall 错误需分类:对
ENOSPC、ENOSPC、EROFS返回明确提示;对ETIMEDOUT、EHOSTUNREACH(NFS 场景)按瞬时故障降级,不直接标 down
HTTP handler 必须绕过中间件,且只返回状态码
Kubernetes readinessProbe 只看 HTTP 状态码,不解析 body。写 handler 时务必注意:
- 注册路径用
/readyz(就绪)而非/healthz(存活),因为文件系统不可写 = 不能收新请求 - 用独立
http.ServeMux或框架的 skip middleware 功能(如 Gin 的r.GET("/readyz", handler).SkipMiddleware = true),确保没被日志、鉴权中间件拦截 - 成功时只写
w.WriteHeader(http.StatusOK),失败时只写w.WriteHeader(http.StatusServiceUnavailable);body 可选,但别写大 JSON —— 探针不需要详情,详情留作 debug 接口(如/debug/fs) - 禁止在 handler 里调
runtime.GC()、debug.ReadGCStats()等可能 STW 的操作,哪怕只是读取
最易被忽略的是:NFS 或 CephFS 等网络文件系统,stat 和 write 的错误语义和本地 ext4 完全不同,必须单独适配错误码;而容器内 mount namespace 隔离导致 /proc/mounts 看不到宿主机真实挂载点,所以一切检查必须基于业务实际使用的路径,而非“理论应该挂在哪”。


















