更新web
This commit is contained in:
319
dns/cache.go
319
dns/cache.go
@@ -3,6 +3,7 @@ package dns
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -55,6 +56,12 @@ type DNSCache struct {
|
||||
// 双向链表头和尾指针,用于LRU淘汰
|
||||
head *LRUNode // 头指针,指向最久未使用的节点
|
||||
tail *LRUNode // 尾指针,指向最近使用的节点
|
||||
// 缓存变化跟踪,用于智能保存
|
||||
changeCount int // 缓存变化次数
|
||||
lastSaveCacheSize int64 // 上次保存时的缓存大小
|
||||
lastSaveItemCount int // 上次保存时的缓存项数量
|
||||
lastSaveTime time.Time // 上次保存时间
|
||||
minSaveInterval time.Duration // 最小保存间隔,避免过于频繁的保存
|
||||
}
|
||||
|
||||
// LRUNode 双向链表节点,用于LRU缓存
|
||||
@@ -71,20 +78,25 @@ func NewDNSCache(defaultTTL time.Duration, cacheMode string, cacheSizeMB int, ca
|
||||
maxCacheSize := int64(cacheSizeMB) * 1024 * 1024
|
||||
|
||||
cache := &DNSCache{
|
||||
cache: make(map[string]*LRUNode),
|
||||
ttl: defaultTTL,
|
||||
maxSize: 10000, // 默认最大缓存10000条记录
|
||||
cacheSize: 0,
|
||||
maxCacheSize: maxCacheSize,
|
||||
cacheMode: cacheMode,
|
||||
cacheFilePath: cacheFilePath,
|
||||
saveInterval: saveInterval,
|
||||
maxCacheTTL: maxCacheTTL,
|
||||
minCacheTTL: minCacheTTL,
|
||||
saveStopCh: make(chan struct{}),
|
||||
saveRunning: false,
|
||||
head: nil,
|
||||
tail: nil,
|
||||
cache: make(map[string]*LRUNode),
|
||||
ttl: defaultTTL,
|
||||
maxSize: 10000, // 默认最大缓存10000条记录
|
||||
cacheSize: 0,
|
||||
maxCacheSize: maxCacheSize,
|
||||
cacheMode: cacheMode,
|
||||
cacheFilePath: cacheFilePath,
|
||||
saveInterval: saveInterval,
|
||||
maxCacheTTL: maxCacheTTL,
|
||||
minCacheTTL: minCacheTTL,
|
||||
saveStopCh: make(chan struct{}),
|
||||
saveRunning: false,
|
||||
head: nil,
|
||||
tail: nil,
|
||||
changeCount: 0,
|
||||
lastSaveCacheSize: 0,
|
||||
lastSaveItemCount: 0,
|
||||
lastSaveTime: time.Now(),
|
||||
minSaveInterval: 30 * time.Second, // 最小保存间隔为30秒,避免过于频繁的保存
|
||||
}
|
||||
|
||||
// 加载现有缓存(如果存在)
|
||||
@@ -158,42 +170,118 @@ func cacheKey(qName string, qType uint16) string {
|
||||
|
||||
// calculateItemSize 计算缓存项大小
|
||||
func calculateItemSize(item *DNSCacheItem) int {
|
||||
// 序列化响应以计算大小
|
||||
data, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return 0
|
||||
// 使用更高效的方式估算缓存项大小
|
||||
// 避免使用json.Marshal和rr.String(),因为它们在高频调用时会消耗大量CPU资源
|
||||
size := 0
|
||||
|
||||
// 估算Response大小
|
||||
if item.Response != nil {
|
||||
// 粗略估算DNS消息大小
|
||||
// 头部大小约12字节
|
||||
size += 12
|
||||
|
||||
// 问题部分
|
||||
for _, q := range item.Response.Question {
|
||||
size += len(q.Name) + 4 // 域名长度 + 类型(2) + 类(2)
|
||||
}
|
||||
|
||||
// 高效估算资源记录大小,避免调用rr.String()
|
||||
estimateRRSize := func(rr dns.RR) int {
|
||||
rrSize := len(rr.Header().Name) + 10 // 域名 + 类型(2) + 类(2) + TTL(4) + 长度(2)
|
||||
|
||||
switch rr.Header().Rrtype {
|
||||
case dns.TypeA:
|
||||
rrSize += 4 // IPv4地址
|
||||
case dns.TypeAAAA:
|
||||
rrSize += 16 // IPv6地址
|
||||
case dns.TypeCNAME, dns.TypePTR, dns.TypeNS:
|
||||
// 对于CNAME、PTR、NS记录,需要估算目标域名长度
|
||||
if cname, ok := rr.(*dns.CNAME); ok {
|
||||
rrSize += len(cname.Target)
|
||||
} else if ptr, ok := rr.(*dns.PTR); ok {
|
||||
rrSize += len(ptr.Ptr)
|
||||
} else if ns, ok := rr.(*dns.NS); ok {
|
||||
rrSize += len(ns.Ns)
|
||||
} else {
|
||||
// 默认估算
|
||||
rrSize += 30
|
||||
}
|
||||
case dns.TypeMX:
|
||||
// MX记录:优先级(2) + 目标域名
|
||||
rrSize += 2 + 30 // 默认30字节目标域名
|
||||
case dns.TypeTXT:
|
||||
// TXT记录:文本长度
|
||||
rrSize += 50 // 默认50字节文本
|
||||
case dns.TypeSRV:
|
||||
// SRV记录:优先级(2) + 权重(2) + 端口(2) + 目标域名
|
||||
rrSize += 6 + 30 // 默认30字节目标域名
|
||||
case dns.TypeSOA:
|
||||
// SOA记录:主NS + 管理员邮箱 + 序列号(4) + 刷新时间(4) + 重试时间(4) + 过期时间(4) + 最小TTL(4)
|
||||
rrSize += 100 // 默认100字节
|
||||
default:
|
||||
// 其他类型记录,使用默认估算
|
||||
rrSize += 50
|
||||
}
|
||||
|
||||
return rrSize
|
||||
}
|
||||
|
||||
// 回答部分
|
||||
for _, rr := range item.Response.Answer {
|
||||
size += estimateRRSize(rr)
|
||||
}
|
||||
|
||||
// 授权部分
|
||||
for _, rr := range item.Response.Ns {
|
||||
size += estimateRRSize(rr)
|
||||
}
|
||||
|
||||
// 附加部分
|
||||
for _, rr := range item.Response.Extra {
|
||||
if rr.Header().Rrtype == dns.TypeOPT {
|
||||
// OPT记录大小约为40字节(EDNS0)
|
||||
size += 40
|
||||
} else {
|
||||
size += estimateRRSize(rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(data)
|
||||
|
||||
// 其他字段大小
|
||||
size += 8 // Expiry
|
||||
size += 1 // HasDNSSEC
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
// hasDNSSECRecords 检查响应是否包含DNSSEC记录
|
||||
func hasDNSSECRecords(response *dns.Msg) bool {
|
||||
// 定义检查单个RR是否为DNSSEC记录的辅助函数
|
||||
isDNSSECRecord := func(rr dns.RR) bool {
|
||||
// 直接在循环中检查RR类型,避免创建匿名函数的开销
|
||||
|
||||
// 检查回答部分
|
||||
for _, rr := range response.Answer {
|
||||
switch rr.(type) {
|
||||
case *dns.DNSKEY, *dns.RRSIG, *dns.DS, *dns.NSEC, *dns.NSEC3:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查响应中是否包含DNSSEC相关记录
|
||||
for _, rr := range response.Answer {
|
||||
if isDNSSECRecord(rr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 检查授权部分
|
||||
for _, rr := range response.Ns {
|
||||
if isDNSSECRecord(rr) {
|
||||
switch rr.(type) {
|
||||
case *dns.DNSKEY, *dns.RRSIG, *dns.DS, *dns.NSEC, *dns.NSEC3:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查附加部分
|
||||
for _, rr := range response.Extra {
|
||||
if isDNSSECRecord(rr) {
|
||||
switch rr.(type) {
|
||||
case *dns.DNSKEY, *dns.RRSIG, *dns.DS, *dns.NSEC, *dns.NSEC3:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -260,6 +348,9 @@ func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.D
|
||||
delete(c.cache, oldestKey)
|
||||
c.removeNode(c.head)
|
||||
}
|
||||
|
||||
// 更新缓存变化计数
|
||||
c.changeCount++
|
||||
}
|
||||
|
||||
// Get 获取缓存项
|
||||
@@ -334,11 +425,23 @@ func (c *DNSCache) Size() int {
|
||||
|
||||
// startCleanupLoop 启动定期清理过期缓存的协程
|
||||
func (c *DNSCache) startCleanupLoop() {
|
||||
ticker := time.NewTicker(time.Minute * 1) // 每1分钟清理一次,减少内存占用
|
||||
// 初始清理间隔为1分钟
|
||||
cleanupInterval := time.Minute * 1
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
c.cleanupExpired()
|
||||
cleanupInterval = c.cleanupExpired()
|
||||
|
||||
// 调整下次清理间隔,范围:15秒到5分钟
|
||||
if cleanupInterval < 15*time.Second {
|
||||
cleanupInterval = 15 * time.Second
|
||||
} else if cleanupInterval > 5*time.Minute {
|
||||
cleanupInterval = 5 * time.Minute
|
||||
}
|
||||
|
||||
// 更新清理间隔
|
||||
ticker.Reset(cleanupInterval)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,9 +488,35 @@ func (c *DNSCache) saveCacheToFile() {
|
||||
c.saveMutex.Lock()
|
||||
defer c.saveMutex.Unlock()
|
||||
|
||||
// 智能保存策略
|
||||
// 1. 如果缓存变化次数少于10次,跳过保存
|
||||
// 2. 如果距离上次保存时间不足最小保存间隔,跳过保存
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
changeCount := c.changeCount
|
||||
lastSaveTime := c.lastSaveTime
|
||||
lastSaveCacheSize := c.lastSaveCacheSize
|
||||
lastSaveItemCount := c.lastSaveItemCount
|
||||
currentCacheSize := c.cacheSize
|
||||
currentItemCount := len(c.cache)
|
||||
c.mutex.RUnlock()
|
||||
|
||||
// 检查是否需要保存
|
||||
if changeCount < 10 {
|
||||
return
|
||||
}
|
||||
if time.Since(lastSaveTime) < c.minSaveInterval {
|
||||
return
|
||||
}
|
||||
if currentItemCount > 0 {
|
||||
cacheSizeChange := float64(currentCacheSize-lastSaveCacheSize) / float64(lastSaveCacheSize+1) // +1避免除以零
|
||||
itemCountChange := float64(currentItemCount-lastSaveItemCount) / float64(lastSaveItemCount+1) // +1避免除以零
|
||||
if math.Abs(cacheSizeChange) < 0.1 && math.Abs(itemCountChange) < 0.1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 开始保存缓存
|
||||
c.mutex.RLock()
|
||||
// 收集有效的缓存项
|
||||
validItems := make(map[string]*SerializableDNSCacheItem)
|
||||
now := time.Now()
|
||||
@@ -419,6 +548,7 @@ func (c *DNSCache) saveCacheToFile() {
|
||||
CacheMode: c.cacheMode,
|
||||
CacheFilePath: c.cacheFilePath,
|
||||
}
|
||||
c.mutex.RUnlock()
|
||||
|
||||
// 序列化到JSON
|
||||
data, err := json.MarshalIndent(serializableCache, "", " ")
|
||||
@@ -434,6 +564,14 @@ func (c *DNSCache) saveCacheToFile() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新保存状态
|
||||
c.mutex.Lock()
|
||||
c.changeCount = 0
|
||||
c.lastSaveTime = time.Now()
|
||||
c.lastSaveCacheSize = currentCacheSize
|
||||
c.lastSaveItemCount = currentItemCount
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SaveToFile 保存缓存到文件
|
||||
@@ -566,8 +704,8 @@ func (c *DNSCache) SetMaxCacheSize(size int64) {
|
||||
c.maxCacheSize = size
|
||||
}
|
||||
|
||||
// cleanupExpired 清理过期的缓存项
|
||||
func (c *DNSCache) cleanupExpired() {
|
||||
// cleanupExpired 清理过期的缓存项,并返回下一次清理间隔的建议值
|
||||
func (c *DNSCache) cleanupExpired() time.Duration {
|
||||
now := time.Now()
|
||||
|
||||
c.mutex.Lock()
|
||||
@@ -575,17 +713,126 @@ func (c *DNSCache) cleanupExpired() {
|
||||
|
||||
// 收集所有过期的键
|
||||
var expiredKeys []string
|
||||
totalItems := len(c.cache)
|
||||
|
||||
// 遍历缓存,收集过期项
|
||||
for key, node := range c.cache {
|
||||
if now.After(node.value.Expiry) {
|
||||
expiredKeys = append(expiredKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
expiredCount := len(expiredKeys)
|
||||
|
||||
// 智能清理策略
|
||||
// 1. 如果过期项比例超过50%,立即清理
|
||||
// 2. 如果缓存大小超过最大缓存大小的80%,清理过期项
|
||||
// 3. 如果缓存项数量超过最大条目数的80%,清理过期项
|
||||
needCleanup := false
|
||||
if totalItems > 0 {
|
||||
if float64(expiredCount)/float64(totalItems) > 0.5 {
|
||||
needCleanup = true
|
||||
} else if c.cacheSize > c.maxCacheSize*8/10 {
|
||||
needCleanup = true
|
||||
} else if totalItems > c.maxSize*8/10 {
|
||||
needCleanup = true
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有过期项或不需要清理,根据过期项比例返回建议的清理间隔
|
||||
if expiredCount == 0 || !needCleanup {
|
||||
// 计算下一次清理间隔
|
||||
var nextInterval time.Duration
|
||||
if totalItems == 0 {
|
||||
// 空缓存,下一次清理间隔可以长一些
|
||||
nextInterval = 5 * time.Minute
|
||||
} else {
|
||||
expireRatio := float64(expiredCount) / float64(totalItems)
|
||||
// 过期项比例越高,清理间隔越短
|
||||
if expireRatio < 0.1 {
|
||||
nextInterval = 5 * time.Minute
|
||||
} else if expireRatio < 0.3 {
|
||||
nextInterval = 2 * time.Minute
|
||||
} else {
|
||||
nextInterval = 1 * time.Minute
|
||||
}
|
||||
}
|
||||
return nextInterval
|
||||
}
|
||||
|
||||
// 删除过期的缓存项
|
||||
for _, key := range expiredKeys {
|
||||
if node, found := c.cache[key]; found {
|
||||
// 减去缓存项大小
|
||||
c.cacheSize -= int64(node.value.Size)
|
||||
delete(c.cache, key)
|
||||
c.removeNode(node)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理后,如果缓存大小仍然超过最大缓存大小,继续清理最久未使用的项
|
||||
if c.cacheSize > c.maxCacheSize {
|
||||
// 计算需要清理的额外大小
|
||||
overflow := c.cacheSize - c.maxCacheSize
|
||||
cleanedSize := int64(0)
|
||||
|
||||
// 从链表头开始清理(最久未使用的项)
|
||||
current := c.head
|
||||
for current != nil && cleanedSize < overflow {
|
||||
nextNode := current.next
|
||||
cleanedSize += int64(current.value.Size)
|
||||
|
||||
// 删除节点
|
||||
delete(c.cache, current.key)
|
||||
c.removeNode(current)
|
||||
|
||||
current = nextNode
|
||||
}
|
||||
}
|
||||
|
||||
// 清理后,如果缓存项数量仍然超过最大条目数,继续清理最久未使用的项
|
||||
if len(c.cache) > c.maxSize {
|
||||
// 计算需要清理的额外数量
|
||||
overflowCount := len(c.cache) - c.maxSize
|
||||
|
||||
// 从链表头开始清理(最久未使用的项)
|
||||
current := c.head
|
||||
for i := 0; i < overflowCount && current != nil; i++ {
|
||||
nextNode := current.next
|
||||
|
||||
// 删除节点
|
||||
c.cacheSize -= int64(current.value.Size)
|
||||
delete(c.cache, current.key)
|
||||
c.removeNode(current)
|
||||
|
||||
current = nextNode
|
||||
}
|
||||
}
|
||||
|
||||
// 清理后,根据剩余过期项比例返回建议的清理间隔
|
||||
// 重新计算剩余过期项
|
||||
var remainingExpired int
|
||||
for _, node := range c.cache {
|
||||
if now.After(node.value.Expiry) {
|
||||
remainingExpired++
|
||||
}
|
||||
}
|
||||
|
||||
remainingItems := len(c.cache)
|
||||
var nextInterval time.Duration
|
||||
if remainingItems == 0 {
|
||||
nextInterval = 5 * time.Minute
|
||||
} else {
|
||||
remainingRatio := float64(remainingExpired) / float64(remainingItems)
|
||||
// 剩余过期项比例越高,清理间隔越短
|
||||
if remainingRatio < 0.1 {
|
||||
nextInterval = 5 * time.Minute
|
||||
} else if remainingRatio < 0.3 {
|
||||
nextInterval = 2 * time.Minute
|
||||
} else {
|
||||
nextInterval = 1 * time.Minute
|
||||
}
|
||||
}
|
||||
|
||||
return nextInterval
|
||||
}
|
||||
|
||||
1006
dns/server.go
1006
dns/server.go
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user