跳转至

文件系统与 IO

学习目标

学完本章后,学习者应该能够:

  1. 理解文件描述符、inode、buffered IO、direct IO 和 fsync。
  2. 理解 select、poll、epoll 与高并发网络 IO 的关系。
  3. 能解释 Reactor 模型在后端服务中的意义。
  4. 能排查常见文件描述符泄漏和磁盘 IO 问题。

文件描述符

在 Unix-like 系统中,文件、socket、管道等资源都可以通过文件描述符访问。

文件描述符是进程级资源,有上限。打开文件、接受连接、连接数据库都会消耗描述符。

当描述符耗尽时,常见报错是:

too many open files

这可能导致新连接无法建立、日志无法写入、配置无法读取。

inode

inode 保存文件元信息,比如权限、所有者、大小、时间戳和数据块位置。文件名只是目录项中的名字,指向 inode。

磁盘没有满但无法创建文件,可能是 inode 耗尽。大量小文件系统尤其需要注意。

buffered IO、direct IO 与 fsync

默认文件写入通常先进入操作系统缓存,并不一定立刻落盘。

  • buffered IO:经过内核页缓存,常见默认方式。
  • direct IO:绕过页缓存,常见于数据库等系统。
  • fsync:要求把文件数据和元数据刷到磁盘。

fsync 能提高持久性,但调用频繁会明显影响性能。

select、poll、epoll

高并发网络服务不能为每个连接都阻塞等待。IO 多路复用让一个线程可以管理大量连接。

简化区别:

机制 特点
select 早期机制,描述符数量受限,扫描成本高
poll 解决部分数量限制,但仍要扫描
epoll Linux 常用,高并发场景效率更好

Go netpoller 会利用操作系统提供的 IO 多路复用能力,让 goroutine 等待网络 IO 时不会占住 OS thread。

Reactor 模型

Reactor 模型的核心是事件驱动:

  1. 监听 IO 事件。
  2. 事件就绪后分发给处理逻辑。
  3. 处理逻辑尽量不要长期阻塞事件循环。

很多网络框架、Redis、Nginx、Go runtime netpoller 都能看到事件驱动思想。

Go 后端实际应用例子

例子一:安全写日志文件

打开文件后要及时关闭,长生命周期日志文件则应集中管理:

func AppendLine(path string, line string) error {
    f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
    if err != nil {
        return err
    }
    defer f.Close()

    _, err = fmt.Fprintln(f, line)
    return err
}

高频日志不建议每条都打开关闭文件,应使用成熟日志库或后台批量写。

例子二:流式复制文件

大文件不要一次性读入内存:

func CopyFile(dst io.Writer, src io.Reader) error {
    _, err := io.Copy(dst, src)
    return err
}

io.Copy 会分块复制,更适合文件下载、上传转存和代理转发。

常见误区

  • 误区一:文件写入返回成功就一定落盘。

buffered IO 可能只是写入内核缓存。需要强持久性时要考虑 fsync,但也要接受性能代价。

  • 误区二:磁盘空间没满就不会写失败。

inode 可能耗尽,权限可能错误,目录可能不存在,文件描述符也可能耗尽。

  • 误区三:epoll 是业务代码直接必须掌握的 API。

大多数 Go 后端不直接写 epoll,但要理解 Go 网络并发为什么能支撑大量连接,以及阻塞操作会如何影响服务。

线上问题案例

某服务把每个请求的完整响应体写入单独文件,短时间创建了海量小文件。磁盘空间还没满,但 inode 耗尽,导致日志和临时文件创建失败。

修复方式包括按天或按小时合并文件、限制采样、定期清理、把大对象放对象存储,并监控磁盘空间和 inode 使用率。

实战任务

设计一个“文件上传转存”接口:

  1. 限制上传文件大小。
  2. 不把整个文件读进内存。
  3. 正确关闭文件和请求体。
  4. 说明如果磁盘满或描述符耗尽会有什么表现。
参考答案

可以用 http.MaxBytesReader 限制请求体大小,用 r.FormFile 或流式读取获取上传内容,然后用 io.Copy 分块写入目标文件或对象存储。请求体、上传文件句柄和目标文件都要关闭。

磁盘满时可能返回 no space left on device,inode 耗尽也会导致创建文件失败。文件描述符耗尽时可能出现 too many open files,新连接、文件、日志句柄都可能无法创建。

面试题

1. 什么是文件描述符?为什么会耗尽?

参考答案

文件描述符是进程访问文件、socket、管道等资源的句柄。打开文件、建立网络连接、接受客户端连接都会占用文件描述符。

如果程序忘记关闭文件或连接,或者并发连接数超过限制,就可能耗尽描述符,导致新文件或新连接无法创建。

2. fsync 的作用是什么?

参考答案

fsync 用于要求操作系统把文件相关数据刷新到磁盘,降低系统崩溃时数据丢失风险。

它能提高持久性,但调用成本较高。数据库、消息队列等系统会谨慎设计 fsync 策略,在性能和可靠性之间取舍。

3. epoll 解决了什么问题?

参考答案

epoll 是 Linux 的 IO 多路复用机制,适合管理大量连接。它能让程序高效等待多个文件描述符的 IO 就绪事件,避免为每个连接都阻塞一个线程。

Go runtime 的网络轮询器会使用类似机制,让大量 goroutine 等待网络 IO 时不必占用大量 OS thread。