如何在 Go 语言 HTTP 服务中限制文件上传与下载速率

本文介绍使用 `juju/ratelimit` 库结合令牌桶算法,为 go 编写的 http 文件服务(上传/下载)添加可配置的带宽限速功能,支持精确控制如 1mb/s 的读写速率。

在构建高可用文件传输服务时,不限速的 I/O 可能导致带宽打满、响应延迟升高甚至影响其他请求。Go 标准库本身不提供内置速率限制器,但借助成熟的第三方限速库(如 juju/ratelimit),我们可通过令牌桶(Token Bucket) 算法优雅实现平滑、可控的上传/下载限速。

该算法核心思想是:以恒定速率向“桶”中注入令牌,每次读/写操作需消耗对应字节数的令牌;若令牌不足则阻塞等待,从而自然达成平均速率上限。ratelimit.Bucket 支持毫秒级精度,且线程安全,非常适合 HTTP 并发场景。

✅ 下载限速(服务端响应流)

对 http.ResponseWriter 的写入进行限速,需包装 http.ResponseWriter 的底层 io.Writer。推荐方式是创建一个限速的 io.Writer 包装器:

func downloadFile(w http.ResponseWriter, r *http.Request) {
    f, err := os.Open(`e:\test\test.mpg`)
    if err != nil {
        http.Error(w, "file not found", http.StatusNotFound)
        return
    }
    defer f.Close()

    // 限速:1 MB/s = 1_048_576 bytes/sec
    bucket := ratelimit.NewBucketWithRate(1_048_576, 1_048_576)

    // 设置响应头
    w.Header().Set("Content-Type", "application/octet-stream")
    w.Header().Set("Content-Disposition", `attachment; filename="test.mpg"`)

    // 使用限速 Writer 包装 ResponseWriter 的 Write 方法
    limitedWriter := ratelimit.Writer(w, bucket)
    _, err = io.Copy(limitedWriter, f)
    if err != nil && err != io.ErrClosedPipe {
        log.Printf("download error: %v", err)
    }
}
⚠️ 注意:io.ErrClosedPipe 是客户端主动断连的常见错误,建议忽略以避免日志噪音。

✅ 上传限速(服务端请求体读取)

对 http.Request.Body 或 multipart.File 的读取限速,只需用 ratelimit.Reader 包装原始 io.Reader:

func uploadFile(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(32 << 20) // 32MB 内存缓冲
    file, _, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "invalid file field", http.StatusBadRequest)
        return
    }
    defer file.Close()

    os.MkdirAll(`e:\test`, 0755)
    out, err := os.Create(`e:\test\test.mpg`)
    if err != nil {
        http.Error(w, "failed to create file", http.StatusInternalServerError)
        return
    }
    defer out.Close()

    // 限速:1 MB/s(可替换为配置项)
    bucket := ratelimit.NewBucketWithRate(1_048_576, 1_048_576)
    limitedReader := ratelimit.Reader(file, bucket)

    _, err = io.Copy(out, limitedReader)
    if err != nil {
        http.Error(w, "upload failed: "+err.Error(), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte("upload success"))
}

? 配置化与最佳实践

  • 动态速率:将 1_048_576 替换为从配置(如 flag.IntVar 或 viper)读取的变量,实现运行时灵活调整。
  • 多用户隔离:若需按用户/IP 限速,应为每个会话/连接创建独立 Bucket(例如基于 r.RemoteAddr 做 map 缓存 + TTL 清理),避免全局速率被单个大文件霸占。
  • 内存友好:NewBucketWithRate 的 capacity 参数(第二参数)建议设为单次最大读写量(如 64KB–1MB),过大会增加内存占用,过小可能导致突发流量抖动。
  • 监控集成:bucket.Available() 可实时获取剩余令牌数,配合 Prometheus 暴露 rate_limit_remaining_tokens 指标,便于运维观测。

通过上述方式,你无需修改业务逻辑主干,仅需两行包装代码即可为任意 io.Reader/io.Writer 添加精准、低开销的速率控制——让大文件传输更可控、更公平、更健壮。