perf(resolver): replace slice-based LRU with container/list for O(1) operations

DNS resolver LRU cache used []string slice for access ordering:
- moveToFrontLocked: O(n) linear scan + slice重组 per cache update
- storeCache held exclusive write lock during entire O(n) operation,
  blocking all concurrent reads

Replace with container/list + map[string]*list.Element:
- storeCache: O(1) MoveToFront via list.Element pointer
- evictLRULocked: O(1) Back() + Remove()
- DeleteCacheEntry: O(1) instead of O(n) scan

This matches the LRU pattern already used by FileCache and
FileInfoCache. Write lock duration reduced from O(n) to O(1),
significantly reducing read contention under mixed workloads.
This commit is contained in:
xfy 2026-06-04 00:13:10 +08:00
parent ba8c746a2e
commit 12caed5d4e
3 changed files with 36 additions and 38 deletions

View File

@ -6,6 +6,7 @@
package resolver
import (
"container/list"
"time"
)
@ -59,23 +60,21 @@ func (r *DNSResolver) GetCacheEntry(host string) (*DNSCacheEntry, bool) {
// DeleteCacheEntry 删除指定主机的缓存条目。
func (r *DNSResolver) DeleteCacheEntry(host string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.cache, host)
// 从 LRU 链表中移除
for i, h := range r.lruOrder {
if h == host {
r.lruOrder = append(r.lruOrder[:i], r.lruOrder[i+1:]...)
break
}
if elem, ok := r.lruIndex[host]; ok {
r.lruList.Remove(elem)
delete(r.lruIndex, host)
}
delete(r.refreshHosts, host)
r.mu.Unlock()
}
// ClearCache 清空所有缓存。
func (r *DNSResolver) ClearCache() {
r.mu.Lock()
r.cache = make(map[string]*DNSCacheEntry)
r.lruOrder = make([]string, 0, r.config.CacheSize)
r.lruList = list.New()
r.lruIndex = make(map[string]*list.Element)
r.refreshHosts = make(map[string]struct{})
r.mu.Unlock()
}

View File

@ -421,22 +421,28 @@ func TestMockDNSMoveToFrontLocked(t *testing.T) {
resolver := New(cfg).(*DNSResolver)
// 设置缓存
resolver.cache = map[string]*DNSCacheEntry{
"a": {},
"b": {},
"c": {},
}
resolver.lruOrder = []string{"a", "b", "c"}
elemA := resolver.lruList.PushFront("a")
elemB := resolver.lruList.PushFront("b")
elemC := resolver.lruList.PushFront("c")
resolver.lruIndex["a"] = elemA
resolver.lruIndex["b"] = elemB
resolver.lruIndex["c"] = elemC
// 移动 "a" 到前端
resolver.moveToFrontLocked("a")
resolver.lruList.MoveToFront(resolver.lruIndex["a"])
// 验证顺序
order := []string{}
for e := resolver.lruList.Back(); e != nil; e = e.Prev() {
order = append(order, e.Value.(string))
}
expected := []string{"b", "c", "a"}
for i, v := range expected {
if resolver.lruOrder[i] != v {
t.Errorf("LRU order[%d] = %s, want %s", i, resolver.lruOrder[i], v)
if order[i] != v {
t.Errorf("LRU order[%d] = %s, want %s", i, order[i], v)
}
}
}

View File

@ -16,6 +16,7 @@
package resolver
import (
"container/list"
"context"
"fmt"
"net"
@ -61,41 +62,31 @@ func (r *DNSResolver) storeCache(host string, entry *DNSCacheEntry) {
r.mu.Lock()
defer r.mu.Unlock()
// 已存在则更新并移到头部
if _, ok := r.cache[host]; ok {
if elem, ok := r.lruIndex[host]; ok {
r.cache[host] = entry
r.moveToFrontLocked(host)
r.lruList.MoveToFront(elem)
return
}
// 检查是否需要淘汰
if r.config.CacheSize > 0 && len(r.cache) >= r.config.CacheSize {
r.evictLRULocked()
}
r.cache[host] = entry
r.lruOrder = append(r.lruOrder, host)
elem := r.lruList.PushFront(host)
r.lruIndex[host] = elem
}
// evictLRULocked 淘汰最久未使用的条目(需持有锁)。
func (r *DNSResolver) evictLRULocked() {
if len(r.lruOrder) == 0 {
oldest := r.lruList.Back()
if oldest == nil {
return
}
oldest := r.lruOrder[0]
delete(r.cache, oldest)
r.lruOrder = r.lruOrder[1:]
}
// moveToFrontLocked 将条目移到 LRU 链表尾部(最新)(需持有锁)。
func (r *DNSResolver) moveToFrontLocked(host string) {
for i, h := range r.lruOrder {
if h == host {
r.lruOrder = append(r.lruOrder[:i], r.lruOrder[i+1:]...)
r.lruOrder = append(r.lruOrder, host)
return
}
}
host := oldest.Value.(string)
delete(r.cache, host)
delete(r.lruIndex, host)
r.lruList.Remove(oldest)
}
// DNSResolver 实现 Resolver 接口的 DNS 解析器。
@ -103,8 +94,9 @@ type DNSResolver struct {
config *config.ResolverConfig
stopCh chan struct{}
refreshHosts map[string]struct{}
cache map[string]*DNSCacheEntry // DNS 缓存
lruOrder []string // LRU 访问顺序(最旧在前)
cache map[string]*DNSCacheEntry
lruList *list.List
lruIndex map[string]*list.Element
hits atomic.Int64
misses atomic.Int64
errors atomic.Int64
@ -166,7 +158,8 @@ func New(cfg *config.ResolverConfig) Resolver {
stopCh: make(chan struct{}),
refreshHosts: make(map[string]struct{}),
cache: make(map[string]*DNSCacheEntry),
lruOrder: make([]string, 0, cfg.CacheSize),
lruList: list.New(),
lruIndex: make(map[string]*list.Element),
}
}