[AI生成] go实现dns缓存
基于net.DefaultResolver的自定义拨号器,默认优先走dns解析(超时时间1s),dns解析失败时走缓存,缓存有效期是24小时;支持指定哪些域名提前预解析。

options.go
// Package dnsplugin 提供基于 DNS 缓存的域名解析服务
// 参考自 volcengine/dns-stale-cache 的设计思想
package dnsplugin
import "time"
// staleUpTo 是缓存的统一过期时间(与官方一致,24 小时)
// 所有缓存条目在存储 24 小时后会被自动清理
const staleUpTo = 24 * time.Hour
// Options 定义 DNS 解析器的配置项(与 volcengine/dns-stale-cache 对齐)
type Options struct {
CacheFirst bool // 是否优先使用缓存:true=先查缓存再异步刷新,false=每次都查 DNS 然后刷新缓存
DnsTimeout time.Duration // DNS 查询超时时间(默认 1 秒)
Addr []string // 需要解析的域名或 host:port 地址列表(可选)
}
// Option 是函数式选项类型,用于灵活配置 Options
type Option func(*Options)
// WithCacheFirst 设置是否优先使用缓存(与 volcengine/dns-stale-cache 语义一致)
// - false(默认):每次先发起 DNS 请求,拿到结果后刷新缓存(保证最新)
// - true:每次先返回缓存内容,异步发起 DNS 请求刷新缓存(性能优先)
func WithCacheFirst(preferUse bool) Option {
return func(o *Options) {
o.CacheFirst = preferUse
}
}
// WithDnsTimeout 设置 DNS 查询的超时时间(与 volcengine/dns-stale-cache 语义一致)
// 默认值为 1 秒
func WithDnsTimeout(timeout time.Duration) Option {
return func(o *Options) {
o.DnsTimeout = timeout
}
}
// WithAddr 设置需要解析的域名或 host:port 地址列表(与 volcengine/dns-stale-cache 语义一致)
// 适用于预先知道要解析哪些域名的场景
func WithAddr(addr []string) Option {
return func(o *Options) {
o.Addr = addr
}
}
// defaultOptions 返回默认配置(与 volcengine/dns-stale-cache 保持一致)
// - CacheFirst: false(默认先查 DNS)
// - DnsTimeout: 1 second(默认 1 秒超时)
// - 缓存统一 24 小时过期(staleUpTo 常量)
func defaultOptions() *Options {
return &Options{
CacheFirst: false, // 默认 false
DnsTimeout: 1 * time.Second, // 默认 1 秒
Addr: nil, // 默认无地址
}
}
dialer.go
package dnsplugin
import (
"context"
"fmt"
"net"
"time"
)
// Dialer 是带 DNS 缓存的自定义拨号器
// 让上层的 HTTP Client、gRPC Client 等自动使用 DNS 缓存
//
// 使用方式(以标准库 HTTP 为例):
//
// dialer := NewDialer(WithCacheFirst(true))
// transport := &http.Transport{
// DialContext: dialer.DialContext,
// }
// client := &http.Client{Transport: transport}
//
// 完整调用链:
//
// client.Get("https://github.com")
// → http.Transport 建立 TCP 连接
// → dialer.DialContext("tcp", "github.com:443")
// → resolver.ResolveFirst("github.com") 解析为 IP(优先查缓存)
// → 用解析后的 IP 和 net.Dialer 建立 TCP 连接
type Dialer struct {
resolver *Resolver
netDialer *net.Dialer
}
// NewDialer 创建一个默认的带 DNS 缓存的拨号器
// 接受与 NewResolver 相同的 Option 配置
func NewDialer(opts ...Option) *Dialer {
return &Dialer{
resolver: NewResolver(opts...),
netDialer: &net.Dialer{Timeout: 30 * time.Second},
}
}
// NewDialerWithBase 基于用户提供的 net.Dialer 创建拨号器
// 适用场景:已有自定义的 net.Dialer(设置了 KeepAlive、LocalAddr 等)
func NewDialerWithBase(baseDialer *net.Dialer, opts ...Option) *Dialer {
if baseDialer == nil {
baseDialer = &net.Dialer{Timeout: 30 * time.Second}
}
return &Dialer{
resolver: NewResolver(opts...),
netDialer: baseDialer,
}
}
// Dial 不带 context 的拨号方法(兼容旧代码)
// 内部转发到 DialContext,使用 Background context
func (d *Dialer) Dial(network, address string) (net.Conn, error) {
return d.DialContext(context.Background(), network, address)
}
// DialContext 带 context 的拨号方法(http.Transport 会调用它)
// 执行流程:
// 1. 拆分 address 为 host 和 port
// 2. 如果 host 是纯 IP,直接用底层 netDialer 拨号
// 3. 否则用带缓存的 Resolver 解析 host,得到一个 IP
// 4. 用解析后的 IP 和 port 拼接成新地址,调用底层 netDialer 建立 TCP 连接
func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
// 步骤 1:拆分 host 和 port
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, fmt.Errorf("invalid address %s: %w", address, err)
}
// 步骤 2:纯 IP 地址直接拨号(无需 DNS 解析)
if net.ParseIP(host) != nil {
return d.netDialer.DialContext(ctx, network, address)
}
// 步骤 3:用带缓存的 Resolver 解析域名,得到第一个 IP
resolvedIP, err := d.resolver.ResolveFirst(host)
if err != nil {
return nil, fmt.Errorf("DNS resolve failed for %s: %w", host, err)
}
// 步骤 4:拼接成 IP:port,用底层 netDialer 建立 TCP 连接
resolvedAddr := net.JoinHostPort(resolvedIP, port)
return d.netDialer.DialContext(ctx, network, resolvedAddr)
}
// GetResolver 返回内部的 Resolver(用于调试或直接调用解析方法)
func (d *Dialer) GetResolver() *Resolver {
return d.resolver
}
// ClearCache 清空 DNS 缓存(透传到底层 Resolver)
func (d *Dialer) ClearCache() {
d.resolver.ClearCache()
}
resolver.go
package dnsplugin
import (
"context"
"fmt"
"net"
"strings"
"time"
)
// Resolver 是 DNS 域名解析器(与 volcengine/dns-stale-cache 对齐)
// 在系统默认 DNS 解析器之上封装了一层 24 小时缓存
// 核心功能:
// 1. 将域名解析为 IP 列表
// 2. 对解析结果进行缓存(统一 24 小时过期)
// 3. 对 IP 地址直接返回,不做 DNS 查询
// 4. 支持 CacheFirst 模式:先查缓存再异步刷新
type Resolver struct {
cache *dnsCache // DNS 缓存(24 小时过期)
opts *Options // 配置选项
}
// NewResolver 创建一个新的 DNS 解析器
// 接受可变数量的 Option 函数,用于覆盖默认配置
// 用法:
//
// resolver := NewResolver(
// WithCacheFirst(true),
// WithDnsTimeout(2*time.Second),
// )
func NewResolver(opts ...Option) *Resolver {
options := defaultOptions()
for _, opt := range opts {
opt(options)
}
return &Resolver{
cache: newDNSCache(),
opts: options,
}
}
// Resolve 解析单个域名(或 host:port 字符串)为 IP 列表
// 这是最基础的解析方法,流程:
// 1. 如果输入包含端口号,先拆分出纯域名
// 2. 如果输入本身是纯 IP,直接返回
// 3. CacheFirst=true: 先查缓存 → 命中则异步刷新 DNS 后返回缓存
// 4. CacheFirst=false: 先查 DNS → 成功则刷新缓存后返回
// 5. DNS 失败:如果缓存中有,则返回缓存(降级使用)
func (r *Resolver) Resolve(hostPort string) ([]string, error) {
// 步骤 1:拆分 host:port
host, port, err := net.SplitHostPort(hostPort)
if err != nil {
// 拆分失败,说明是纯域名(无端口)
host = hostPort
}
// 步骤 2:纯 IP 直接返回
if net.ParseIP(host) != nil {
// 如果有 port,则返回 IP:port 格式
if port != "" {
return []string{net.JoinHostPort(host, port)}, nil
}
return []string{host}, nil
}
// 步骤 3:CacheFirst=true → 先查缓存
if r.opts.CacheFirst {
if ips, ok := r.cache.get(host); ok {
// 命中缓存:先返回,再异步发起 DNS 查询刷新缓存
go r.requestAndRefresh(host) // 异步刷新,不阻塞当前调用
// 如果有 port,则给每个 IP 都加上 port
if port != "" {
result := make([]string, len(ips))
for i, ip := range ips {
result[i] = net.JoinHostPort(ip, port)
}
return result, nil
}
return ips, nil
}
}
// 步骤 4:发起 DNS 查询(带超时)
ips, err := r.lookupDNS(host)
if err != nil {
// DNS 失败:如果缓存中有,降级使用缓存
if cachedIPs, ok := r.cache.get(host); ok {
if port != "" {
result := make([]string, len(cachedIPs))
for i, ip := range cachedIPs {
result[i] = net.JoinHostPort(ip, port)
}
return result, nil
}
return cachedIPs, nil
}
return nil, err // 没有缓存,返回 DNS 错误
}
// 步骤 5:DNS 成功,写入缓存并返回
r.cache.set(host, ips)
if port != "" {
result := make([]string, len(ips))
for i, ip := range ips {
result[i] = net.JoinHostPort(ip, port)
}
return result, nil
}
return ips, nil
}
// LookupHost 解析配置的地址列表(与 volcengine/dns-stale-cache 对齐)
// 遍历 opts.Addr 中的每个地址,依次解析并返回结果列表
// 每个结果以逗号分隔的 IP(或 IP:port)字符串形式返回
func (r *Resolver) LookupHost() ([]string, error) {
var result []string
for _, addr := range r.opts.Addr {
// 处理 URL 格式(例如 "https://github.com")
if strings.Contains(addr, "//") {
// 简单处理:去掉 scheme 部分
if idx := strings.Index(addr, "//"); idx >= 0 {
addr = addr[idx+2:]
}
}
// 解析该地址
ips, err := r.Resolve(addr)
if err != nil {
continue // 解析失败的跳过(与官方行为一致)
}
// 将 IP 列表以逗号拼接,加入结果
result = append(result, strings.Join(ips, ","))
}
return result, nil
}
// ResolveFirst 解析域名并返回第一个 IP(适用于只需一个 IP 的场景)
func (r *Resolver) ResolveFirst(hostPort string) (string, error) {
ips, err := r.Resolve(hostPort)
if err != nil {
return "", err
}
if len(ips) == 0 {
return "", fmt.Errorf("no IPs resolved for %s", hostPort)
}
return ips[0], nil // 返回第一个 IP(与官方一致,无轮询)
}
// requestAndRefresh 异步发起 DNS 查询并刷新缓存
// 用于 CacheFirst=true 模式下:已命中缓存但需要后台刷新
func (r *Resolver) requestAndRefresh(host string) {
ips, err := r.lookupDNS(host)
if err != nil {
return // DNS 查询失败,保持原有缓存不变
}
r.cache.set(host, ips) // 刷新缓存
}
// lookupDNS 执行实际的 DNS 查询(内部方法)
// 使用系统默认的 Resolver,带 context 超时控制
func (r *Resolver) lookupDNS(host string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), r.opts.DnsTimeout)
defer cancel()
ips, err := net.DefaultResolver.LookupHost(ctx, host)
if err != nil {
return nil, err
}
if len(ips) == 0 {
return nil, fmt.Errorf("no IPs found for %s", host)
}
return ips, nil
}
// ClearCache 清空所有缓存
func (r *Resolver) ClearCache() {
r.cache.clear()
}
// CleanExpired 清理过期缓存(超过 24 小时未刷新的条目)
func (r *Resolver) CleanExpired() {
r.cache.cleanExpired()
}
// Stats 返回当前缓存的统计信息(用于调试和监控)
func (r *Resolver) Stats() string {
r.cache.mu.RLock()
defer r.cache.mu.RUnlock()
var sb strings.Builder
sb.WriteString(fmt.Sprintf("DNS Cache Stats: %d entries\n", len(r.cache.items)))
for host, entry := range r.cache.items {
elapsed := time.Since(entry.Stored)
remaining := staleUpTo - elapsed
if remaining < 0 {
remaining = 0
}
sb.WriteString(fmt.Sprintf(" %s -> %v (stored %v ago, expires in %v)\n",
host, entry.IPs, elapsed.Round(time.Second), remaining.Round(time.Second)))
}
return sb.String()
}
cache.go
package dnsplugin
import (
"sync"
"time"
)
// cacheEntry 表示单个域名的缓存条目(与 volcengine 官方一致的简化结构)
// 只存储 IP 列表和存储时间戳,缓存统一 24 小时过期
type cacheEntry struct {
IPs []string // 解析得到的 IP 列表
Stored time.Time // 缓存存储时间(用于判断是否超过 24 小时)
}
// dnsCache 是线程安全的 DNS 缓存结构(与 volcengine 官方一致的简化实现)
// 使用 map 存储,sync.RWMutex 保证并发安全
type dnsCache struct {
mu sync.RWMutex
items map[string]*cacheEntry
}
// newDNSCache 创建并初始化一个新的 DNS 缓存
func newDNSCache() *dnsCache {
return &dnsCache{
items: make(map[string]*cacheEntry),
}
}
// get 从缓存中查询域名对应的 IP 列表
// 缓存有效期为 24 小时(staleUpTo),超过则视为失效
// 返回值:IP 列表, 是否命中缓存
func (c *dnsCache) get(host string) ([]string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.items[host]
if !ok {
return nil, false // 未命中缓存
}
// 判断是否已过 24 小时有效期
// time.Since(entry.Stored) 返回从存储到现在经过的时间
// 超过 staleUpTo(24 小时)则视为缓存失效
if time.Since(entry.Stored) >= staleUpTo {
return nil, false // 缓存已过期
}
return entry.IPs, true // 命中有效缓存
}
// set 将域名解析结果写入缓存
// 记录存储时间,24 小时后自动失效
func (c *dnsCache) set(host string, ips []string) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[host] = &cacheEntry{
IPs: ips,
Stored: time.Now(), // 记录当前时间为存储时间
}
}
// clear 清空所有缓存
func (c *dnsCache) clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.items = make(map[string]*cacheEntry)
}
// cleanExpired 清理所有过期的缓存条目(超过 24 小时)
// 类似官方的 cleanFileCache,但仅清理内存缓存
func (c *dnsCache) cleanExpired() {
c.mu.Lock()
defer c.mu.Unlock()
for host, entry := range c.items {
if time.Since(entry.Stored) >= staleUpTo {
delete(c.items, host) // 移除过期条目
}
}
}
// size 返回当前缓存中的有效条目数
func (c *dnsCache) size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.items)
}
main.go
// Package main 是 dns-plugin 的演示程序(使用 default 配置,无自定义)
// 按顺序展示 3 个核心功能:
// 1. 基础 DNS 解析(域名 → IP,含 LookupHost 批量解析)
// 2. 缓存命中对比(第一次 DNS 查询 vs 第二次缓存读取)
// 3. 自定义 Dialer 集成到 HTTP Client
package main
import (
"fmt"
"io"
"net/http"
"time"
dnsplugin "test/dns-plugin"
)
func main() {
fmt.Println("========================================")
fmt.Println(" DNS Plugin Demo - 使用 Default 配置")
fmt.Println("========================================")
fmt.Println()
// 演示 1:基础 DNS 解析
demoBasicResolve()
fmt.Println()
// 演示 2:缓存命中对比
demoCacheHit()
fmt.Println()
// 演示 3:自定义 Dialer + HTTP Client
demoDialerHTTP()
fmt.Println()
fmt.Println("========================================")
fmt.Println(" Demo Complete - 所有演示完成")
fmt.Println("========================================")
}
// ============================================================================
// 演示 1:基础 DNS 解析(default 配置)
// ============================================================================
// 使用 NewResolver() 无参数(即 default 配置):
// - CacheFirst: false(每次先发起 DNS 查询,然后刷新缓存)
// - DnsTimeout: 1 秒
// - 缓存 24 小时过期
func demoBasicResolve() {
fmt.Println("--- 演示 1:基础 DNS 解析 (default 配置) ---")
// 使用 default 配置创建 Resolver(无需传任何 Option)
resolver := dnsplugin.NewResolver()
// 测试 3 个主机名:两个国内真实域名 + 一个纯 IP
hosts := []string{"www.baidu.com", "www.aliyun.com", "192.168.1.1"}
for _, host := range hosts {
ips, err := resolver.Resolve(host)
if err != nil {
fmt.Printf(" %s -> 解析失败: %v\n", host, err)
} else {
fmt.Printf(" %s -> %v\n", host, ips)
}
}
// 演示 LookupHost 批量解析:需要预配置 Addr(这是唯一需要传 Option 的场景)
fmt.Println()
fmt.Println(" --- 批量解析 (LookupHost) ---")
resolver2 := dnsplugin.NewResolver(
dnsplugin.WithAddr([]string{"www.baidu.com:443", "www.aliyun.com:443"}),
)
results, err := resolver2.LookupHost()
if err != nil {
fmt.Printf(" 批量解析失败: %v\n", err)
} else {
for _, r := range results {
fmt.Printf(" -> %s\n", r)
}
}
// 打印缓存统计
fmt.Println()
fmt.Println(resolver2.Stats())
}
// ============================================================================
// 演示 2:缓存命中演示(CacheFirst=true)
// ============================================================================
// 演示 CacheFirst=true 下的缓存行为:
// - 第一次:缓存为空 → 发起 DNS 查询 → 写入缓存 → 返回
// - 第二次:先查缓存 → 命中 → 直接返回(同时后台异步刷新)
// - DNS 失败时:降级使用缓存
//
// 核心价值:
// 1. 加速:后续请求直接命中缓存(微秒级 vs 毫秒级)
// 2. 高可用:DNS 服务器故障时仍可用缓存返回
// 3. 新鲜度:命中缓存后异步发起 DNS 查询刷新缓存,保证数据不过时
func demoCacheHit() {
fmt.Println("--- 演示 2:缓存命中对比 (CacheFirst=true) ---")
// 显式设置 CacheFirst=true:优先查缓存
resolver := dnsplugin.NewResolver(
dnsplugin.WithCacheFirst(true),
)
host := "www.baidu.com"
// 第一次解析:缓存为空 → 必须发起真实 DNS 查询
fmt.Printf(" 第一次解析 %s(缓存为空,发起 DNS 查询)...\n", host)
start := time.Now()
ips1, err := resolver.Resolve(host)
elapsed1 := time.Since(start)
if err != nil {
fmt.Printf(" 错误: %v\n", err)
return
}
fmt.Printf(" 结果: %v, 耗时: %v\n", ips1, elapsed1)
// 第二次解析:命中我们自己的缓存 → 直接返回
// 同时后台会异步发起 DNS 查询刷新缓存
fmt.Printf(" 第二次解析(命中缓存,异步刷新)...\n")
start = time.Now()
ips2, err := resolver.Resolve(host)
elapsed2 := time.Since(start)
if err != nil {
fmt.Printf(" 错误: %v\n", err)
return
}
fmt.Printf(" 结果: %v, 耗时: %v\n", ips2, elapsed2)
// 计算加速倍数(分母加 1 避免除以 0)
speedup := float64(elapsed1) / float64(elapsed2+1)
fmt.Printf(" 缓存加速: ~%.1fx 倍\n", speedup)
fmt.Println()
fmt.Println(" 说明:CacheFirst=true 时,第二次从我们的缓存直接返回。")
fmt.Println(" 注:第1次如果也很快(<1ms),可能命中了操作系统 DNS 缓存。")
fmt.Println(" 缓存统一 24 小时过期,可通过 CleanExpired() 主动清理。")
fmt.Println()
fmt.Println(resolver.Stats())
}
// ============================================================================
// 演示 3:自定义 Dialer + HTTP Client(default 配置)
// ============================================================================
// 展示如何将 DNS Plugin 集成到标准库 net/http 中
// 使用 NewDialer() 无参数(default 配置)
func demoDialerHTTP() {
fmt.Println("--- 演示 3:自定义 Dialer + HTTP Client (default 配置) ---")
// 使用 default 配置创建 Dialer(无需传任何 Option)
dialer := dnsplugin.NewDialer()
// 创建 http.Transport,替换默认的 DialContext
transport := &http.Transport{
DialContext: dialer.DialContext,
}
// 创建 HTTP Client
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
// 发送 HTTP 请求,内部会自动使用 DNS 缓存
urls := []string{"https://www.baidu.com"}
for _, url := range urls {
fmt.Printf(" GET %s...\n", url)
start := time.Now()
resp, err := client.Get(url)
elapsed := time.Since(start)
if err != nil {
fmt.Printf(" 错误: %v\n", err)
continue
}
bodySize, _ := io.Copy(io.Discard, resp.Body)
resp.Body.Close()
fmt.Printf(" 状态: %s, 响应大小: %d bytes, 耗时: %v\n",
resp.Status, bodySize, elapsed)
}
// 打印 DNS 缓存统计
fmt.Println()
fmt.Println(dialer.GetResolver().Stats())
}
运行结果
$ go run main.go
========================================
DNS Plugin Demo - 使用 Default 配置
========================================
--- 演示 1:基础 DNS 解析 (default 配置) ---
www.baidu.com -> [180.101.49.44 180.101.51.73]
www.aliyun.com -> [122.228.195.21 122.225.209.41 115.238.197.52 122.228.79.44 122.228.15.36 61.164.147.227 60.188.126.94 61.164.115.95]
192.168.1.1 -> [192.168.1.1]
--- 批量解析 (LookupHost) ---
-> 180.101.49.44:443,180.101.51.73:443
-> 122.228.195.21:443,122.225.209.41:443,115.238.197.52:443,122.228.79.44:443,122.228.15.36:443,61.164.147.227:443,60.188.126.94:443,61.164.115.95:443
DNS Cache Stats: 2 entries
www.baidu.com -> [180.101.49.44 180.101.51.73] (stored 0s ago, expires in 24h0m0s)
www.aliyun.com -> [122.228.195.21 122.225.209.41 115.238.197.52 122.228.79.44 122.228.15.36 61.164.147.227 60.188.126.94 61.164.115.95] (stored 0s ago, expires in 24h0m0s)
--- 演示 2:缓存命中对比 (CacheFirst=true) ---
第一次解析 www.baidu.com(缓存为空,发起 DNS 查询)...
结果: [180.101.49.44 180.101.51.73], 耗时: 540.7µs
第二次解析(命中缓存,异步刷新)...
结果: [180.101.49.44 180.101.51.73], 耗时: 0s
缓存加速: ~540700.0x 倍
说明:CacheFirst=true 时,第二次从我们的缓存直接返回。
注:第1次如果也很快(<1ms),可能命中了操作系统 DNS 缓存。
缓存统一 24 小时过期,可通过 CleanExpired() 主动清理。
DNS Cache Stats: 1 entries
www.baidu.com -> [180.101.49.44 180.101.51.73] (stored 0s ago, expires in 24h0m0s)
--- 演示 3:自定义 Dialer + HTTP Client (default 配置) ---
GET https://www.baidu.com...
状态: 200 OK, 响应大小: 2443 bytes, 耗时: 118.2591ms
DNS Cache Stats: 1 entries
www.baidu.com -> [180.101.49.44 180.101.51.73] (stored 0s ago, expires in 24h0m0s)
========================================
Demo Complete - 所有演示完成
========================================
浙公网安备 33010602011771号