如果您从事云安全或漏洞扫描工作,您很可能依赖 ProjectDiscovery Nuclei——这是由 YAML 模板驱动的黄金标准开源漏洞扫描器。
虽然 Nuclei 作为独立的命令行界面工具表现卓越,但将其作为底层软件开发工具包引擎嵌入到长期运行的微服务或持续扫描工作进程中时,会引入独特的架构挑战:在长时间执行循环中出现内存膨胀和协程泄漏。
最近,我在 Nuclei 问题 #7503 中调查并解决了这些确切的引擎生命周期泄漏问题,并提交了 拉取请求 #7508。以下是我对底层发现的详细分析,以及我如何在 Go 语言中修复这些问题。
🔍 问题:无界状态与孤立协程
当将 NucleiEngine 嵌入到长期运行的应用程序循环中(其中引擎根据每个扫描目标动态实例化和关闭)时,我注意到内存消耗随时间稳步上升,并且在调用 engine.Close() 后很久,孤立的协程仍然处于活动状态。
在对 Go 语言中的引擎生命周期进行性能剖析后,我确定了三个主要的内存泄漏源:
-
HTTP 到 HTTPS 端口跟踪器中的无界
sync.Map:HTTPToHTTPSPortTracker将主机端口映射状态存储在一个无界的sync.Map中。在经过数千次目标扫描后,该映射无限增长且没有驱逐机制。 -
孤立的每主机速率限制器协程: 全局协议状态维护了每次执行的速率限制池(
PerHostRateLimitPool)。当引擎执行结束时,工作后台例程没有被干净地关闭或清除。 -
缓存的模板解析器: 编译后的模板抽象语法树(
parsedTemplatesCache和compiledTemplatesCache)在引擎实例之间将解析后的表示保留在内存中,而在引擎拆解期间没有明确的缓存清除机制。
🛠️ 解决方案:架构与代码修复
1. 有界可过期最近最少使用缓存
我没有在 sync.Map 中保留无界的主机条目,而是用可过期的最近最少使用缓存替换了存储结构,该缓存配置了严格的容量上限(4,096 个条目)和 24 小时的生存时间:
// 用有界可过期最近最少使用缓存替换无界 sync.Map
type HTTPToHTTPSPortTracker struct {
cache *expirable.LRU[string, struct{}]
}
func NewHTTPToHTTPSPortTracker() *HTTPToHTTPSPortTracker {
return &HTTPToHTTPSPortTracker{
cache: expirable.NewLRU[string,
免责声明:本文内容来自互联网,该文观点不代表本站观点。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请到页面底部单击反馈,一经查实,本站将立刻删除。