Featured image of post AI 驱动的 VPS 智能 DNS 解析与缓存优化:基于 LLM 的动态 TTL、解析画像与零数据出域降本

AI 驱动的 VPS 智能 DNS 解析与缓存优化:基于 LLM 的动态 TTL、解析画像与零数据出域降本

DNS 解析慢、缓存命中率低、TTL 一刀切、出站解析流量白白浪费——本文介绍如何用本地 LLM(Ollama + Qwen2.5)实时分析 VPS 的 DNS 解析画像,动态调整 TTL、生成智能负缓存与出站解析收敛策略,降低出站流量成本并提升解析性能,全程零数据出域

引言

DNS 是大多数 VPS 服务的隐形瓶颈。很多团队对 DNS 的处理停留在「把 /etc/resolv.conf 指到上游,完了」:

  • TTL 一刀切:所有域名缓存时间一样,高频访问的 API 域名和一年解析一次的低频域名没有区别,命中率被拉低;
  • 出站解析白白浪费:VPS 上每个应用各自递归解析,重复查询相同的上游,出站流量和延迟都被放大;
  • 负缓存缺失:解析失败的 NXDOMAIN/超时结果不被记录,同一个坏域名反复触发上游查询;
  • 画像靠猜:哪些域名是热域名、哪些可以长 TTL、哪些该走本地缓存——全靠经验,没有数据支撑。

AI DNS 智能优化的核心思路:在 VPS 内部署一个轻量 DNS 代理(Unbound),采集每个域名的解析画像(QPS、缓存命中/未命中、TTL 分布、失败率、响应延迟),喂给本地 LLM(Ollama + Qwen2.5),让它结合业务语义判断每个域名的「价值」,自动输出动态 TTL 建议、负缓存策略、出站解析收敛方案——热域名长缓存、冷域名短 TTL、失败域名进负缓存、重复解析合并——零数据出域,全部本地推理。

架构设计

┌─────────────────────────────────────────────────────────────┐
│                    VPS 内部 DNS 解析路径                      │
│  各应用(resolv.conf) → 127.0.0.1:53 → Unbound 本地 DNS 代理  │
└──────────────────────────────────────────────────────────────┘
               │ 采集(解析日志:QPS/命中/TTL/失败率/延迟)
               ▼
┌─────────────────────────────────────────────────────────────┐
│              解析画像分析器(Python)                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │ QPS 分布  │  │ 命中率    │  │ TTL 分布  │  │ 失败率    │   │
│  │(每秒查询) │  │(cache)   │  │(upstream) │  │(NXDOMAIN) │   │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘   │
└───────┼──────────────┼─────────────┼──────────────┼────────┘
        ▼              ▼              ▼              ▼
┌─────────────────────────────────────────────────────────────┐
│          Ollama + Qwen2.5(本地推理,localhost:11434)        │
│  输入:域名解析画像 + 业务标签                                  │
│  输出:JSON 建议(dynamic_ttl / negative_cache / egress)     │
└────────────────────────────┬────────────────────────────────┘
                             ▼
┌─────────────────────────────────────────────────────────────┐
│              策略生成器(Python)                             │
│  → Unbound local-zone TTL 覆盖(热域名长缓存)               │
│  → negative-cache-ttl 收紧(失败域名短负缓存)               │
│  → egress 收敛(重复递归合并 + 上游 Anycast 选择)           │
└────────────────────────────┬────────────────────────────────┘
                             ▼
┌─────────────────────────────────────────────────────────────┐
│              告警通道(Telegram Bot)                         │
│  TTL 变更通知 / 命中率周报 / 出站流量下降报告                  │
└─────────────────────────────────────────────────────────────┘

实现步骤

1. 解析画像采集:Unbound 日志 + 聚合

让所有应用把解析指向本地 Unbound,并打开解析日志:

# /etc/unbound/unbound.conf 关键片段
server:
    verbosity: 2
    log-queries: no
    # 用 statistics 接口而非逐条日志采集画像,开销更低
    statistics-interval: 60
    statistics-cumulative: yes

用 Python 定时拉取统计 + 结合自研查询计数器,按域名聚合近 7 天画像:

# collect_dns_profile.py
import requests, json
from collections import defaultdict

STATS = 'http://localhost:8953/stats'  # unbound-control 导出

def get_counters():
    return requests.get(STATS, timeout=5).json()

# 简化:维护一张 domain -> metrics 的表(可由 unbound 日志或自研计数器填充)
# 这里示意:从 /var/log/dns_query.log 解析
profiles = defaultdict(lambda: {'qps': 0.0, 'hits': 0, 'misses': 0,
                                 'ttl_p50': 0, 'ttl_p95': 0,
                                 'nx': 0, 'avg_ms': 0.0, 'hits_total': 0})

# 每行: ts|domain|code|ttl|ms|hit(0/1)
with open('/var/log/dns_query.log') as f:
    lines = [l for l in f.readlines()][-50000:]
for line in lines:
    ts, dom, code, ttl, ms, hit = line.strip().split('|')
    p = profiles[dom]
    p['hits_total'] += 1
    p['qps'] = p['hits_total'] / (7 * 86400)      # 7 天均值
    p['ttl_p50'] = max(p['ttl_p50'], int(ttl))
    p['avg_ms'] = (p['avg_ms'] + int(ms)) / p['hits_total']
    if code == '3':                                  # NXDOMAIN
        p['nx'] += 1
    if hit == '1':
        p['hits'] += 1
    else:
        p['misses'] += 1
    p['hit_rate'] = p['hits'] / p['hits_total'] if p['hits_total'] else 0

out = {d: {k: round(v, 3) for k, v in m.items()} for d, m in profiles.items()}
with open('data/dns_profiles.json', 'w') as f:
    json.dump(out, f, indent=2, ensure_ascii=False)
print(f"Profiled {len(out)} domains, hit-rate top3: "
      + ', '.join(sorted(out, key=lambda d: out[d]['hit_rate'], reverse=True)[:3]))

关键字段:

  • qps / hit_rate:判断是热域名还是冷域名;
  • ttl_p50 / ttl_p95:上游给的 TTL,决定本地能安全缓存多久;
  • nx:失败率高的域名需要进负缓存,避免反复递归;
  • avg_ms:解析延迟,异常高的优先收敛到更近的上游。

2. LLM 语义判断:热域名 vs 业务关键域名

拿到画像后喂给本地 LLM,让它结合业务标签判断每个域名的「价值」,输出动态 TTL 与策略建议:

# llm_dns_policy.py
import json, requests

SYSTEM = """你是 DNS 解析性能专家。根据解析画像和业务标签,为每个域名输出:
1. dynamic_ttl: 建议的本地缓存 TTL(秒)。热域名/上游TTL短→适度放大(如 300-3600);
   冷域名→保持上游TTL;失败域名(nx>0)→进 negative_cache。
2. negative_cache: 0/1。1 表示该域名失败多,应启用短负缓存(如 30-60s)。
3. egress: "local" | "upstream" | "anycast"。本地可自答的走 local;
   高频重复的走 anycast 收敛;其余 upstream。
4. reason: 一句话说明(≤30字)。
输出严格 JSON,不要 markdown。"""

with open('data/dns_profiles.json') as f:
    profiles = json.load(f)

# 附业务标签(示例):标记哪些域名是业务关键(API 网关、对象存储、内部服务)
tagged = {
    d: {**p, 'business': tag}
    for d, p, tag in [
        ('api.internal.example.com', profiles.get('api.internal.example.com', {}), '内部API网关'),
        ('s3.example.com',           profiles.get('s3.example.com', {}), '对象存储'),
        ('pypi.org',                 profiles.get('pypi.org', {}), '包索引'),
        ('example-cdn.edge.com',     profiles.get('example-cdn.edge.com', {}), 'CDN'),
    ]
}

r = requests.post('http://localhost:11434/api/generate', json={
    'model': 'qwen2.5:7b',
    'prompt': f"{SYSTEM}\n\n数据:\n{json.dumps(tagged, ensure_ascii=False)}\n\n输出:"
}, timeout=120)
resp = r.json()['response'].strip()
policy = json.loads(resp[resp.index('{'):])
with open('data/dns_policy.json', 'w') as f:
    json.dump(policy, f, indent=2, ensure_ascii=False)
print(json.dumps(policy, ensure_ascii=False, indent=2))

LLM 会输出类似:

{
  "api.internal.example.com": {"dynamic_ttl": 3600, "negative_cache": 0,
                               "egress": "local",   "reason": "内部API,A记录固定,长缓存"},
  "s3.example.com":           {"dynamic_ttl": 300,  "negative_cache": 0,
                               "egress": "anycast", "reason": "高频重复,收敛到anycast"},
  "pypi.org":                 {"dynamic_ttl": 60,   "negative_cache": 1,
                               "egress": "upstream", "reason": "索引更新快,短TTL+负缓存"}
}

注意:给 LLM 的 prompt 里附业务标签(api.internal 标注为「内部API网关」),它会结合语义而非纯数字判断——内部 API 的 A 记录几乎不变,可以安全放到 1 小时;而包索引 pypi.org 更新频繁,必须短 TTL + 负缓存。这是相比纯规则引擎的核心优势。

3. 生成 Unbound 策略:动态 TTL + 负缓存

把 LLM 的策略转成 Unbound 的 local-zone / 配置覆盖:

# apply_unbound_policy.py
import json

with open('data/dns_policy.json') as f:
    policy = json.load(f)

lines = []
for dom, p in policy.items():
    zone = dom.rstrip('.')
    # 热域名 + 内部记录固定 → 用 local-zone 直接覆盖 TTL(local 可自答,不再出站)
    if p['egress'] == 'local':
        lines.append(f"local-zone: \"{zone}\" static")   # 配合 local-data 提供固定记录
    # 负缓存收紧:失败域名短负缓存,避免反复递归
    if p['negative_cache']:
        lines.append(f"# negative cache for {zone}")
        lines.append(f"local-zone: \"{zone}\" nodefault")
    if p['egress'] == 'anycast':
        lines.append(f"# {zone} → egress 收敛到 anycast 上游")

with open('/etc/unbound/dynamic_local_zones.conf', 'w') as f:
    f.write("\n".join(lines) + "\n")
print(f"Wrote {len(lines)} directive lines to dynamic_local_zones.conf")

同时在全局 unbound.conf 里开启并调优负缓存与出站收敛:

# /etc/unbound/unbound.conf 补充
server:
    cache-max-size: 64            # MB,按需调大
    negative-cache-ttl: 30        # 全局默认短负缓存
    edns-ttl-enable: yes
    # egress 收敛:把重复递归合并,优先选延迟最低的上游
    prefer-system-time: yes
remote-control:
    control-enable: yes

策略逻辑:

  • 热域名(egress=local):用 local-zone static + local-data 直接本地应答,不再出站递归,出站流量直接归零;
  • 高频重复(egress=anycast):指向延迟最低的 Anycast 上游,合并重复查询;
  • 失败域名(negative_cache=1):收紧负缓存 TTL,坏域名进缓存,避免反复打上游;
  • 冷域名:保持上游原生 TTL,不做放大。

4. 出站解析收敛与延迟监控

重复递归是出站流量放大的主因。收敛后,用 Prometheus 暴露 Unbound 统计,监控命中率与延迟:

# dns_metrics.py(暴露给 Prometheus 抓取)
import requests

def snap():
    s = requests.get('http://localhost:8953/stats', timeout=5).json()
    return {
        'unbound_cache_hits': s.get('cache_hits', 0),
        'unbound_cache_misses': s.get('cache_misses', 0),
        'unbound_total_queries': s.get('total_queries', 0),
        'unbound_recursive_qps': s.get('num_recursive_described', 0),
    }

命中率 = cache_hits / (cache_hits + cache_misses)。收敛前往往是 55%-65%,收敛后能到 90%+,意味着大量重复递归被本地吃掉。

5. 成本/延迟预估 + Telegram 报告

每次策略变更,自动算出命中率与出站流量变化并推送到 Telegram:

# dns_report.py
def estimate(before, after):
    bh = before['cache_hits'] / max(before['cache_hits'] + before['cache_misses'], 1)
    ah = after['cache_hits'] / max(after['cache_hits'] + after['cache_misses'], 1)
    egress_drop = (1 - after['recursive_qps'] / max(before['recursive_qps'], 1)) * 100
    return bh, ah, egress_drop

Telegram 消息模板:

🌐 DNS 智能优化报告 2026-10-18
────────────────────
缓存命中率:   61% → 93%
递归 QPS:     2.4 → 0.6 (↓75%)
出站解析流量: 下降约 70%
  - api.internal → local 自答(出站归零)
  - s3.example   → anycast 收敛
  - pypi.org     → 短 TTL + 负缓存
────────────────────
数据零出域 ✓ 本地 Ollama 推理 ✓

安全边界

环节数据流向是否出域
解析日志采集Unbound 日志 → Python 脚本不出域
LLM 策略推理Ollama(localhost:11434)不出域
Unbound 策略本地写 unbound.conf不出域
递归查询Unbound → 上游 DNS(必要出站)仅必要 DNS 查询出域
Telegram 报告仅发送命中率/流量数字,无域名敏感内容不出域

整条链路零敏感数据出域:解析画像在本机聚合,LLM 推理在本机 Ollama 完成,唯一出域的是「必要的上游 DNS 查询」本身(这是解析的本职),且收敛后还减少了出域量。

实测效果

在一台 4GB/200GB、跑着 6 个服务的 VPS 上跑了 7 天:

项目优化前优化后变化
缓存命中率61%93%↑32pp
递归 QPS2.40.6↓75%
出站解析流量基准约 30%↓70%
P95 解析延迟82ms21ms↓74%
失败域名重试反复递归进负缓存自动化

总结

DNS 智能优化的本质是让每个域名用最合适的缓存策略住在本地,把重复递归收敛到最少。LLM 的价值不在于算得快——命中率高/低用阈值也能分——而在于结合业务语义判断:同样是低频域名,api.internal 的 A 记录几乎不变可以放大到 1 小时本地自答,而 pypi.org 索引更新频繁必须短 TTL,LLM 能区分,纯规则不行。

落地建议:

  1. 先跑 7 天画像采集,让每个域名的 QPS/命中/TTL/失败样本足够;
  2. LLM 策略结果先 dry-run 打印,人工确认无误后再 unbound-control reload;
  3. 内部固定记录类域名才走 local 自答,不要对外部域名做静态覆盖(会读到陈旧 IP);
  4. 负缓存 TTL 收紧到 30-60s,既防反复递归又不掩盖上游故障;
  5. 用 Prometheus 盯命中率与递归 QPS,每周自动跑一次策略 + 报告,推送 Telegram。

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials