使用html.Render函数遭遇内存泄漏,请求触发OOM问题排查
问题分析
- 重复渲染父节点:每个文本节点触发一次父节点的
html.Render操作,若一个父节点包含多个文本节点,会重复生成多次相同的HTML字符串,造成大量冗余内存分配。 - Builder/Buffer容量无法回收:
strings.Builder.Reset()仅重置长度,底层byte切片的容量保持不变;处理过大HTML后,Builder会长期占用大内存。即使使用sync.Pool,大尺寸的Buffer/Builder会持续占用池内存,无法有效释放。 - 大字符串累积:提取的
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
相关产品推荐
相关产品推荐

