概念定义
TTL 是每条 DNS 记录的过期字段。递归解析器在 TTL 耗尽前一直提供缓存答案,之后才回源查询权威服务器。它决定了一个刚性折中:长 TTL 降低查询量与延迟但拖慢故障切换,短 TTL 带来敏捷却增加解析流量。
工作机制
解析器从取得记录那刻起倒计时,因此临近过期的记录在另一个解析器那里可能只剩几秒生命。负缓存有自己的 TTL(来自 SOA 记录)。部分浏览器与操作系统会施加最小 TTL,桩解析器也会忽略过小的值。
使用场景
计划 IP 迁移、故障切换、CDN 供应商更换或基于 DNS 的负载均衡时必须先审视 TTL。切换前至少提前一个旧 TTL 降下来,变更窗口才可实现。故障切换系统按业务容忍度匹配 TTL(常见 30 到 300 秒),稳定记录可设数小时以分摊权威查询压力。
常见误区
TTL 本身也是被缓存的数据:修改 TTL 后要等旧 TTL 过期才生效,这个两步传播常让运维措手不及。客户端缓存与连接池的寿命超过 DNS TTL,30 秒记录也杀不掉既有连接。监控应在迁移期间对比权威答案与解析器可见答案,把 TTL 当作变更预算而非保证。
代码示例
package main
import (
"context"
"fmt"
"net"
"sort"
"time"
)
func Lookup(ctx context.Context, host string) ([]string, error) {
addresses, err := net.DefaultResolver.LookupHost(ctx, host)
if err != nil {
return nil, err
}
sort.Strings(addresses)
return addresses, nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
addresses, err := Lookup(ctx, "localhost")
if err != nil {
fmt.Println("lookup failed:", err)
return
}
for _, address := range addresses {
fmt.Println(address)
}
}
package main
import (
"container/list"
"fmt"
"sync"
"time"
)
type Stats struct {
Hits int
Misses int
Evicts int
}
type entry[K comparable, V any] struct {
key K
value V
expiresAt time.Time
}
type LRU[K comparable, V any] struct {
mu sync.Mutex
capacity int
ttl time.Duration
now func() time.Time
items map[K]*list.Element
order *list.List
stats Stats
}
func NewLRU[K comparable, V any](capacity int, ttl time.Duration) (*LRU[K, V], error) {
if capacity < 1 {
return nil, fmt.Errorf("capacity must be positive")
}
if ttl <= 0 {
return nil, fmt.Errorf("ttl must be positive")
}
return &LRU[K, V]{
capacity: capacity,
ttl: ttl,
now: time.Now,
items: make(map[K]*list.Element),
order: list.New(),
}, nil
}
func (c *LRU[K, V]) Get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
element, ok := c.items[key]
if !ok {
c.stats.Misses++
var zero V
return zero, false
}
item := element.Value.(*entry[K, V])
if !c.now().Before(item.expiresAt) {
c.removeElement(element)
c.stats.Misses++
var zero V
return zero, false
}
c.order.MoveToFront(element)
c.stats.Hits++
return item.value, true
}
func (c *LRU[K, V]) Put(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
if element, ok := c.items[key]; ok {
item := element.Value.(*entry[K, V])
item.value = value
item.expiresAt = c.now().Add(c.ttl)
c.order.MoveToFront(element)
return
}
element := c.order.PushFront(&entry[K, V]{
key: key,
value: value,
expiresAt: c.now().Add(c.ttl),
})
c.items[key] = element
for len(c.items) > c.capacity {
oldest := c.order.Back()
if oldest == nil {
break
}
c.removeElement(oldest)
c.stats.Evicts++
}
}
func (c *LRU[K, V]) removeElement(element *list.Element) {
item := element.Value.(*entry[K, V])
c.order.Remove(element)
delete(c.items, item.key)
}
func (c *LRU[K, V]) Stats() Stats {
c.mu.Lock()
defer c.mu.Unlock()
return c.stats
}
func (c *LRU[K, V]) Len() int {
c.mu.Lock()
defer c.mu.Unlock()
return len(c.items)
}
func main() {
cache, err := NewLRU[string, int](2, 50*time.Millisecond)
if err != nil {
fmt.Println(err)
return
}
cache.Put("a", 1)
cache.Put("b", 2)
if value, ok := cache.Get("a"); ok {
fmt.Println("hit a:", value)
}
cache.Put("c", 3)
if _, ok := cache.Get("b"); !ok {
fmt.Println("b evicted")
}
time.Sleep(60 * time.Millisecond)
if _, ok := cache.Get("a"); !ok {
fmt.Println("a expired")
}
fmt.Printf("len=%d stats=%+v\n", cache.Len(), cache.Stats())
}