Skip to content

Latest commit

 

History

History
809 lines (625 loc) · 16.1 KB

File metadata and controls

809 lines (625 loc) · 16.1 KB

11.3 IO 优化

📍 导航返回目录 | 上一节:CPU优化 | 下一节:内存优化


IO 瓶颈识别

如何判断是 IO 瓶颈?

# 查看磁盘 IO 统计
iostat -x 1

# 查看 IO 密集进程
iotop

# 查看文件系统缓存
free -h

# 查看某进程的 IO
pidstat -d 1 -p <pid>

IO 瓶颈特征

  • iowait (wa) 百分比高(> 20%)
  • 磁盘 util% 接近 100%
  • 平均响应时间 (await) 高
  • 进程处于 D 状态(不可中断睡眠)

磁盘 IO 优化

1. 顺序 IO vs 随机 IO

性能差异

  • 顺序读写:~500 MB/s(HDD)、~3000 MB/s(SSD)
  • 随机读写:~1 MB/s(HDD)、~300 MB/s(SSD)

优化策略:合并小 IO

import (
    "bufio"
    "os"
)

// ❌ 随机小 IO:每次写 1 字节
func writeSlow(filename string, data []byte) error {
    file, _ := os.Create(filename)
    defer file.Close()
    
    for _, b := range data {
        file.Write([]byte{b})  // 每次系统调用
    }
    return nil
}

// ✅ 批量顺序 IO:使用缓冲
func writeFast(filename string, data []byte) error {
    file, _ := os.Create(filename)
    defer file.Close()
    
    writer := bufio.NewWriterSize(file, 64*1024) // 64KB 缓冲
    defer writer.Flush()
    
    writer.Write(data)  // 一次写入
    return nil
}

性能对比(写入 1MB 数据):

  • 小 IO:5000ms
  • 批量 IO:50ms
  • 性能提升:100x

2. 预读优化

# 查看预读大小
blockdev --getra /dev/sda

# 设置预读(单位:512字节扇区)
blockdev --setra 8192 /dev/sda  # 4MB 预读

应用层预读

import "io"

// ✅ 预读优化
func readWithPrefetch(filename string, offset int64, size int) ([]byte, error) {
    file, _ := os.Open(filename)
    defer file.Close()
    
    // 预读更大范围(2倍)
    prefetchSize := size * 2
    reader := bufio.NewReaderSize(file, prefetchSize)
    
    file.Seek(offset, io.SeekStart)
    data := make([]byte, size)
    _, err := reader.Read(data)
    return data, err
}

3. Direct IO(绕过页缓存)

适用场景

  • 自己管理缓存的数据库
  • 大文件顺序读写
  • 避免污染系统缓存
import (
    "os"
    "syscall"
)

// ✅ Direct IO 打开文件
func openDirectIO(filename string) (*os.File, error) {
    return os.OpenFile(filename, 
        os.O_RDWR|syscall.O_DIRECT, 0666)
}

// 使用示例(需要对齐)
func writeDirectIO(filename string, data []byte) error {
    file, _ := openDirectIO(filename)
    defer file.Close()
    
    // Direct IO 需要扇区对齐(512 字节)
    alignedSize := (len(data) + 511) & ^511
    alignedData := make([]byte, alignedSize)
    copy(alignedData, data)
    
    _, err := file.Write(alignedData)
    return err
}

4. 零拷贝技术

sendfile(文件到 Socket)

import (
    "net"
    "os"
    "syscall"
)

// ✅ 零拷贝发送文件
func sendFileZeroCopy(conn net.Conn, filename string) error {
    file, _ := os.Open(filename)
    defer file.Close()
    
    stat, _ := file.Stat()
    fileSize := stat.Size()
    
    // 获取 socket fd
    tcpConn := conn.(*net.TCPConn)
    connFile, _ := tcpConn.File()
    defer connFile.Close()
    
    // sendfile 系统调用(零拷贝)
    _, err := syscall.Sendfile(
        int(connFile.Fd()),
        int(file.Fd()),
        nil,
        int(fileSize),
    )
    return err
}

性能对比(发送 100MB 文件):

  • 传统方式(read + write):200ms,2 次拷贝
  • sendfile(零拷贝):80ms,0 次拷贝
  • 性能提升:2.5x

mmap(内存映射)

import (
    "os"
    "syscall"
)

// ✅ mmap 读取大文件
func readFileMmap(filename string) ([]byte, error) {
    file, _ := os.Open(filename)
    defer file.Close()
    
    stat, _ := file.Stat()
    size := int(stat.Size())
    
    // mmap 映射文件到内存
    data, err := syscall.Mmap(
        int(file.Fd()),
        0,
        size,
        syscall.PROT_READ,
        syscall.MAP_SHARED,
    )
    if err != nil {
        return nil, err
    }
    
    defer syscall.Munmap(data)
    
    // 直接读取内存
    result := make([]byte, size)
    copy(result, data)
    return result, nil
}

适用场景

  • 随机访问大文件
  • 共享内存通信
  • 需要高性能的文件读写

网络 IO 优化

1. IO 多路复用

epoll(Linux)

import (
    "golang.org/x/sys/unix"
    "syscall"
)

// ✅ epoll 实现高性能网络服务
type EPollServer struct {
    epollFd int
    events  []unix.EpollEvent
}

func NewEPollServer() (*EPollServer, error) {
    epollFd, err := unix.EpollCreate1(0)
    if err != nil {
        return nil, err
    }
    
    return &EPollServer{
        epollFd: epollFd,
        events:  make([]unix.EpollEvent, 128),
    }, nil
}

func (s *EPollServer) AddConn(fd int) error {
    event := unix.EpollEvent{
        Events: unix.EPOLLIN | unix.EPOLLET, // 边缘触发
        Fd:     int32(fd),
    }
    return unix.EpollCtl(s.epollFd, unix.EPOLL_CTL_ADD, fd, &event)
}

func (s *EPollServer) Wait() ([]int, error) {
    n, err := unix.EpollWait(s.epollFd, s.events, -1)
    if err != nil {
        return nil, err
    }
    
    fds := make([]int, n)
    for i := 0; i < n; i++ {
        fds[i] = int(s.events[i].Fd)
    }
    return fds, nil
}

性能对比

  • select:支持 1024 个连接
  • epoll:支持 100000+ 个连接
  • C10K 问题解决方案

2. 批量发送(Nagle 算法禁用)

import "net"

// ✅ 禁用 Nagle 算法,立即发送
func setNoDelay(conn *net.TCPConn) error {
    return conn.SetNoDelay(true)
}

// ✅ 批量发送减少系统调用
func batchWrite(conn net.Conn, messages [][]byte) error {
    // 合并多个消息
    var buffer []byte
    for _, msg := range messages {
        buffer = append(buffer, msg...)
    }
    
    // 一次发送
    _, err := conn.Write(buffer)
    return err
}

3. 连接复用(HTTP Keep-Alive)

import (
    "net/http"
    "time"
)

// ✅ HTTP 客户端连接池
var httpClient = &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,              // 最大空闲连接
        MaxIdleConnsPerHost: 10,               // 每个 host 的空闲连接
        IdleConnTimeout:     90 * time.Second, // 空闲超时
        DisableKeepAlives:   false,            // 启用 Keep-Alive
    },
    Timeout: 30 * time.Second,
}

func makeRequest(url string) (*http.Response, error) {
    return httpClient.Get(url)  // 复用连接
}

性能对比

  • 短连接:建立连接 2ms + 请求 10ms = 12ms/req
  • 长连接:请求 10ms = 10ms/req
  • 性能提升:20%

4. 零拷贝网络传输

import (
    "io"
    "net"
)

// ✅ splice 零拷贝(Linux)
func proxySplice(client, server net.Conn) error {
    clientFile, _ := client.(*net.TCPConn).File()
    serverFile, _ := server.(*net.TCPConn).File()
    defer clientFile.Close()
    defer serverFile.Close()
    
    // pipe 创建内核缓冲区
    r, w, _ := os.Pipe()
    defer r.Close()
    defer w.Close()
    
    go func() {
        // client -> pipe (splice)
        syscall.Splice(
            int(clientFile.Fd()),
            nil,
            int(w.Fd()),
            nil,
            1024*1024,
            0,
        )
    }()
    
    // pipe -> server (splice)
    syscall.Splice(
        int(r.Fd()),
        nil,
        int(serverFile.Fd()),
        nil,
        1024*1024,
        0,
    )
    return nil
}

异步 IO

1. io_uring(Linux 5.1+)

特点

  • 真正的异步 IO
  • 无需线程池
  • 性能极高
// 使用第三方库:github.com/iceber/iouring-go
import "github.com/iceber/iouring-go"

// ✅ io_uring 异步读取
func readAsyncIOUring(filename string) ([]byte, error) {
    ring, _ := iouring.New(128)
    defer ring.Close()
    
    file, _ := os.Open(filename)
    defer file.Close()
    
    buf := make([]byte, 4096)
    
    // 提交异步读请求
    request := iouring.Read(int(file.Fd()), buf)
    ring.QueueSQE(request)
    
    // 等待完成
    _, err := ring.WaitCQEvents(1)
    return buf, err
}

性能对比(随机读取 1000 个 4KB 文件):

  • 同步 IO:500ms
  • AIO(线程池):250ms
  • io_uring:100ms
  • 性能提升:5x

2. Go 异步 IO 模式

import (
    "context"
    "sync"
)

// ✅ Goroutine 实现异步 IO
type AsyncReader struct {
    pool *sync.Pool
}

func NewAsyncReader() *AsyncReader {
    return &AsyncReader{
        pool: &sync.Pool{
            New: func() interface{} {
                return make([]byte, 4096)
            },
        },
    }
}

func (r *AsyncReader) ReadAsync(filename string) <-chan Result {
    resultCh := make(chan Result, 1)
    
    go func() {
        buf := r.pool.Get().([]byte)
        defer r.pool.Put(buf)
        
        data, err := os.ReadFile(filename)
        resultCh <- Result{Data: data, Err: err}
        close(resultCh)
    }()
    
    return resultCh
}

type Result struct {
    Data []byte
    Err  error
}

// 使用示例
func example() {
    reader := NewAsyncReader()
    
    // 并发读取多个文件
    results := make([]<-chan Result, 0)
    for _, filename := range filenames {
        results = append(results, reader.ReadAsync(filename))
    }
    
    // 等待所有结果
    for _, resultCh := range results {
        result := <-resultCh
        // 处理结果...
    }
}

IO 缓冲优化

1. 合适的缓冲区大小

import "bufio"

// ❌ 默认缓冲(4KB)
func readDefault(filename string) ([]byte, error) {
    file, _ := os.Open(filename)
    defer file.Close()
    
    reader := bufio.NewReader(file)  // 默认 4KB
    return io.ReadAll(reader)
}

// ✅ 大缓冲(1MB)
func readLargeBuffer(filename string) ([]byte, error) {
    file, _ := os.Open(filename)
    defer file.Close()
    
    reader := bufio.NewReaderSize(file, 1024*1024)  // 1MB
    return io.ReadAll(reader)
}

性能对比(读取 100MB 文件):

  • 4KB 缓冲:500ms
  • 1MB 缓冲:200ms
  • 性能提升:2.5x

2. 双缓冲机制

// ✅ 双缓冲:边读边处理
func processLargeFile(filename string) error {
    file, _ := os.Open(filename)
    defer file.Close()
    
    const bufferSize = 64 * 1024
    buffers := [2][]byte{
        make([]byte, bufferSize),
        make([]byte, bufferSize),
    }
    
    current := 0
    resultCh := make(chan []byte, 1)
    
    go func() {
        for {
            n, err := file.Read(buffers[current])
            if err != nil {
                close(resultCh)
                return
            }
            
            // 发送当前缓冲
            data := make([]byte, n)
            copy(data, buffers[current][:n])
            resultCh <- data
            
            // 切换缓冲
            current = 1 - current
        }
    }()
    
    // 并行处理
    for data := range resultCh {
        processData(data)  // 处理数据
    }
    
    return nil
}

IO 调度优化

1. 调度器选择

# 查看当前调度器
cat /sys/block/sda/queue/scheduler

# 设置调度器
echo "deadline" > /sys/block/sda/queue/scheduler

# 调度器对比:
# - noop:适合 SSD(无需排序)
# - deadline:低延迟优先
# - cfq:公平调度(默认)
# - bfq:低延迟 + 公平

2. IO 优先级

# 设置 IO 优先级(ionice)
ionice -c 2 -n 0 ./my_app  # 最高优先级

# IO 类别:
# 0 - 未设置
# 1 - 实时(RT)
# 2 - 尽力而为(BE,默认)
# 3 - 空闲(Idle)

Go 代码设置 IO 优先级

import "syscall"

// ✅ 设置进程 IO 优先级
func setIOPriority(priority int) error {
    const IOPRIO_CLASS_BE = 2
    const IOPRIO_WHO_PROCESS = 1
    
    ioprio := (IOPRIO_CLASS_BE << 13) | priority
    
    _, _, err := syscall.Syscall(
        syscall.SYS_IOPRIO_SET,
        IOPRIO_WHO_PROCESS,
        0,  // 当前进程
        uintptr(ioprio),
    )
    
    if err != 0 {
        return err
    }
    return nil
}

IO 优化实战案例

案例 1:日志系统优化

场景:日志写入 QPS 从 1000 提升到 100000

优化步骤

1. 问题分析

# iostat 发现大量随机写
iostat -x 1

# 发现每条日志都单独写文件
strace -p <pid> 2>&1 | grep write

2. 优化方案

import (
    "bufio"
    "sync"
    "time"
)

// ✅ 批量写入日志
type BatchLogger struct {
    file   *os.File
    writer *bufio.Writer
    mu     sync.Mutex
    buffer []string
    ticker *time.Ticker
}

func NewBatchLogger(filename string) *BatchLogger {
    file, _ := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    
    logger := &BatchLogger{
        file:   file,
        writer: bufio.NewWriterSize(file, 1024*1024), // 1MB 缓冲
        buffer: make([]string, 0, 1000),
        ticker: time.NewTicker(100 * time.Millisecond),
    }
    
    // 定时刷新
    go logger.flushLoop()
    
    return logger
}

func (l *BatchLogger) Log(msg string) {
    l.mu.Lock()
    l.buffer = append(l.buffer, msg)
    
    // 满 1000 条立即刷新
    if len(l.buffer) >= 1000 {
        l.flushLocked()
    }
    l.mu.Unlock()
}

func (l *BatchLogger) flushLoop() {
    for range l.ticker.C {
        l.mu.Lock()
        l.flushLocked()
        l.mu.Unlock()
    }
}

func (l *BatchLogger) flushLocked() {
    if len(l.buffer) == 0 {
        return
    }
    
    for _, msg := range l.buffer {
        l.writer.WriteString(msg)
        l.writer.WriteByte('\n')
    }
    l.writer.Flush()
    l.buffer = l.buffer[:0]
}

3. 效果验证

性能对比

  • 优化前:1000 QPS,iowait 50%
  • 优化后:100000 QPS,iowait 5%
  • 性能提升:100x

案例 2:文件传输优化

场景:大文件传输从 100 MB/s 提升到 1 GB/s

// ✅ 零拷贝 + 并行传输
func transferFileFast(src, dst string) error {
    srcFile, _ := os.Open(src)
    defer srcFile.Close()
    
    dstFile, _ := os.Create(dst)
    defer dstFile.Close()
    
    stat, _ := srcFile.Stat()
    fileSize := stat.Size()
    
    // 并行传输多个块
    const chunkSize = 64 * 1024 * 1024  // 64MB
    numChunks := (fileSize + chunkSize - 1) / chunkSize
    
    var wg sync.WaitGroup
    for i := int64(0); i < numChunks; i++ {
        wg.Add(1)
        go func(chunkID int64) {
            defer wg.Done()
            
            offset := chunkID * chunkSize
            size := chunkSize
            if offset+size > fileSize {
                size = fileSize - offset
            }
            
            // 使用 sendfile 零拷贝
            syscall.Sendfile(
                int(dstFile.Fd()),
                int(srcFile.Fd()),
                &offset,
                int(size),
            )
        }(i)
    }
    
    wg.Wait()
    return nil
}

IO 优化检查清单

磁盘 IO

  • 是否合并小 IO 为大 IO?
  • 是否使用了合适的缓冲区大小?
  • 是否优化了顺序访问模式?
  • 是否使用了零拷贝技术?
  • 是否使用了 Direct IO(适用场景)?
  • 是否使用了 mmap(大文件随机访问)?

网络 IO

  • 是否使用了 IO 多路复用?(epoll)
  • 是否使用了连接池?
  • 是否启用了 Keep-Alive?
  • 是否批量发送数据?
  • 是否使用了零拷贝?(sendfile)
  • 是否优化了 TCP 参数?

异步 IO

  • 是否使用了异步 IO?(io_uring)
  • 是否使用了 Goroutine 并发?
  • 是否使用了双缓冲机制?

本章小结

核心要点

  1. 批量操作:合并小 IO,减少系统调用
  2. 零拷贝:sendfile、mmap、splice 减少内存拷贝
  3. 异步 IO:io_uring、Goroutine 提升并发能力
  4. 缓冲优化:使用合适的缓冲区大小
  5. 顺序访问:优化访问模式,利用预读

优化优先级

批量IO > 零拷贝 > 异步IO > 缓冲优化 > 调度优化

⏮️ 上一节:CPU优化 | ⏭️ 下一节:内存优化