You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用html.Render函数遭遇内存泄漏,请求触发OOM问题排查

问题分析
  1. 重复渲染父节点:每个文本节点触发一次父节点的html.Render操作,若一个父节点包含多个文本节点,会重复生成多次相同的HTML字符串,造成大量冗余内存分配。
  2. Builder/Buffer容量无法回收:strings.Builder.Reset()仅重置长度,底层byte切片的容量保持不变;处理过大HTML后,Builder会长期占用大内存。即使使用sync.Pool,大尺寸的Buffer/Builder会持续占用池内存,无法有效释放。
  3. 大字符串累积:提取的TagInfo数组保存了所有文本节点对应的父节点HTML,当页面文本节点多、父节点HTML体积大时,这些字符串会快速耗尽内存。
解决方案

1. 缓存父节点渲染结果

每个父节点仅渲染一次,用节点指针作为key缓存渲染后的HTML,彻底避免重复计算:

func Extract(doc *html.Node) []TagInfo {
    var (
        tags     = make([]TagInfo, 0, 100)
        tagCache = make(map[*html.Node]string)
        f        func(*html.Node)
    )

    f = func(n *html.Node) {
        if n.Type == html.TextNode {
            text := strings.TrimSpace(n.Data)
            if text != "" {
                parent := n.Parent
                tagHTML, ok := tagCache[parent]
                if !ok {
                    var buf strings.Builder
                    if err := html.Render(&buf, parent); err != nil {
                        log.Println(err)
                        tagHTML = ""
                    } else {
                        tagHTML = buf.String()
                    }
                    tagCache[parent] = tagHTML
                }
                tags = append(tags, TagInfo{
                    Tag:  tagHTML,
                    Name: parent.Data,
                    Text: n.Data,
                })
            }
        }
        for child := n.FirstChild; child != nil; child = child.NextSibling {
            f(child)
        }
    }
    f(doc)
    return tags
}

同步修改GetData函数,无需再传递strings.Builder:

func GetData(w http.ResponseWriter, r *http.Request) {
    u := r.URL.Query().Get("url")
    doc, err := GetDoc(u)
    if err != nil {
        log.Println(err)
        w.WriteHeader(500)
        return
    }
    data := Extract(doc)
    csvw := csv.NewWriter(io.Discard)
    for _, d := range data {
        csvw.Write([]string{d.Name, d.Tag, d.Text})
    }
    csvw.Flush()
}

2. 优化sync.Pool内存占用(可选)

若仍需复用Buffer,在归还到池时重置其容量,避免大内存对象长期滞留:

var bufPool = sync.Pool{
    New: func() interface{} {
        return bytes.NewBuffer(make([]byte, 0, 1024))
    },
}

func getRenderedHTML(n *html.Node) string {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        // 超过阈值则替换为小容量Buffer,避免大内存占用
        if cap(buf.Bytes()) > 4096 {
            bufPool.Put(bytes.NewBuffer(make([]byte, 0, 1024)))
        } else {
            bufPool.Put(buf)
        }
    }()
    if err := html.Render(buf, n); err != nil {
        log.Println(err)
        return ""
    }
    return buf.String()
}

3. 按需生成HTML(可选)

如果不需要精确还原原始HTML,可手动拼接标签名和属性,完全规避html.Render的内存开销:

func getTagHTML(n *html.Node) string {
    if n.Type != html.ElementNode {
        return ""
    }
    var buf strings.Builder
    buf.WriteByte('<')
    buf.WriteString(n.Data)
    for _, attr := range n.Attr {
        buf.WriteByte(' ')
        buf.WriteString(attr.Key)
        if attr.Val != "" {
            buf.WriteString(`="`)
            buf.WriteString(html.EscapeString(attr.Val))
            buf.WriteByte('"')
        }
    }
    buf.WriteByte('>')
    // 如需支持闭合标签/自闭合标签,可添加额外判断逻辑
    return buf.String()
}
效果验证

修改后,每个父节点仅渲染一次,内存分配量会降至原来的1/N(N为父节点下的文本节点数),同时避免了大内存Builder/Buffer的长期占用,200次请求的内存占用会大幅降低,不会出现OOM问题。

内容的提问来源于stack exchange,提问作者Jayesh Kale

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 00:15:05