使用Golang实现简单CDN节点_静态资源缓存与分发

应避免直接使用 http.servefile,因其不支持缓存头与 range 请求;推荐用 http.fileserver 配合自定义中间件设置 cache-control、etag 和正确处理范围请求。

使用Golang实现简单CDN节点_静态资源缓存与分发

http.ServeFile 直接服务静态文件?别急,它不支持缓存头和范围请求

直接调用 http.ServeFile 看似省事,但 CDN 节点必须控制 Cache-Control、支持断点续传(Range 请求),而它默认不设缓存头,也不处理 If-None-MatchETag。真实浏览器反复拉同一个 JS 文件时,会绕过本地缓存重发完整请求——这不是“静态服务”,是“静态裸奔”。

实操建议:

立即学习go语言免费学习笔记(深入)”;

  • 改用 http.FileServer + 自定义 http.Handler 中间件,手动写 Cache-Control: public, max-age=31536000(对 .js/.css/.woff2 等资源)
  • 对图片等可能被频繁更新的资源,用 max-age=86400 + ETag(基于文件内容哈希生成)
  • 务必检查 Content-LengthContent-Range 是否在 Range 请求下正确返回——否则视频/大包下载会卡死

缓存策略:内存缓存 vs 本地文件缓存,选哪个?

CDN 节点不是纯代理,要主动缓存源站响应。Go 原生没有线程安全的 LRU,sync.Map 又不支持容量淘汰,硬上容易 OOM。

实操建议:

立即学习go语言免费学习笔记(深入)”;

  • 小流量场景(QPS github.com/hashicorp/golang-lru/v2 的 lru.Cache,设置 Size: 1000,键为 request.URL.Path + request.Header.Get("Accept-Encoding")
  • 大流量或需持久化:跳过内存缓存,直接写入本地磁盘(如 /var/cache/cdn/<hash>.bin</hash>),用 os.Stat 检查 ModTime 判断是否过期,避免重复读文件
  • 千万别把整个响应体(含 body)塞进 map——HTTP header 和 body 应分离缓存,body 用文件存,header 用内存存,否则 GC 压力陡增

net/http.Transport 配置不当,源站连接池会拖垮整个节点

CDN 节点要频繁回源,如果 http.Transport 没调优,会出现大量 connection refusedtimeout,甚至把源站打挂。

实操建议:

立即学习go语言免费学习笔记(深入)”;

  • 必须设置 MaxIdleConns: 100MaxIdleConnsPerHost: 100IdleConnTimeout: 30 * time.Second
  • 禁用 ExpectContinueTimeout(设为 0),避免小请求卡在 100-continue 等待
  • 加一层 context.WithTimeout 到每个 client.Do,超时设为 3 * time.Second(源站响应慢就快速失败,别堵住连接池)
  • 错误日志里一定要打印出 req.URL.Hosterr.Error(),否则你根本分不清是源站崩了还是 DNS 解析失败

路径安全:别让 ../../../etc/passwd 从 URL 里逃出来

用户请求 /static/..%2f..%2f..%2fetc%2fpasswd,若没做路径规范化,filepath.Join 可能拼出越权路径。

实操建议:

立即学习go语言免费学习笔记(深入)”;

  • 收到请求路径后,先用 path.Clean 归一化,再用 strings.HasPrefix 检查是否以允许前缀(如 /static/)开头
  • 禁止使用 filepath.Join 拼接用户输入路径——改用 filepath.FromSlash + filepath.Clean + 白名单校验
  • 静态资源目录必须用绝对路径初始化(如 /data/www),不要用 ./static,避免相对路径语义漂移
  • Linux 下注意 os.OpenFile0444 权限位,别误开写权限,否则缓存文件可能被恶意覆盖

最麻烦的从来不是怎么缓存,而是缓存失效时要不要删本地文件、删的时候会不会正被另一个 goroutine 读着——这种竞态不会报错,只会返回空内容或 500。动手前先想清楚清理时机和锁粒度。