第7章 DNS、CDN 与负载均衡
元数据卡
| 项目 | 内容 |
|---|---|
| 难度 | (中等) |
| 前置 | 第1章 网络分层模型;第4章 HTTP |
| 关键词 | DNS、A/AAAA/CNAME 记录、递归/迭代查询、Anycast、CDN、边缘节点、L4/L7 负载均衡、一致性哈希、Round Robin |
| Python | 3.8+ |
| 代码量 | ~100 行 |
你在哪
"你发现驿道上有一层隐形的导航系统——当你念出 'google.com' 时,DNS(域名解析法阵)把它翻译成驿道坐标。CDN(内容分发驿道)把法术卷轴复制到离你最近的信标塔。负载均衡把魔力流量分配到后端的多个信标塔上。"
上一章我们钻进 HTTP 和 TLS 的世界——学会了怎么发法术请求、怎么法力握手、怎么建一条安全的加密驿道。但有一个问题我们一直假装不存在:
法师的观测镜怎么知道
api.example.com的信标塔在哪座山上?
你不可能把全世界所有驿道坐标背下来。就算背下来了,坐标也可能随时变。所以我们需要一个驿道地址簿——或者说,一个分布在全球的、每秒处理数亿次法术查询的、有史以来最大的分布式驿道地址簿。
这就是 DNS(Domain Name System)。
但这章我们不止聊 DNS。单个服务器扛不住全世界的流量——哪怕你的代码再完美、算法再高效,一台机器每秒能处理的请求有物理上限。于是有了 CDN 把内容推到离用户最近的地方,有了负载均衡把流量分摊到一群服务器上。
这三者(DNS + CDN + 负载均衡)构成了现代互联网的入口骨架。你访问任何一个大型网站,第一秒内这三样东西全在工作。
本章分层
- 必读:域名如何变成 IP(DNS 递归/迭代查询)、为什么缓存导致 DNS 变更延迟生效(TTL)、CDN 为什么快(边缘节点 + Anycast)、负载均衡怎么分流(L4 vs L7、常见算法)
- 选读:DNS 记录类型(A/AAAA/CNAME/MX/NS/TXT)、GeoDNS 智能解析、一致性哈希的原理
- 深水区:BGP Anycast 的路由层原理、迷你 DNS 解析器实现
本章不会要求你掌握
- DNSSEC 签名链
- BGP 的具体路由配置
- 一致性哈希的虚拟节点算法(属于分布式缓存章节)
你的任务
学完本章,你应当能做到:
- 解释浏览器从输入 URL 到拿到 IP 地址的全过程——递归查询、缓存、迭代
- 看懂 DNS 记录类型:A、AAAA、CNAME、MX、NS、TXT
- 用
dig和nslookup诊断 DNS 问题 - 解释 CDN 的核心原理(边缘节点 + 回源 + Anycast)——理解 Anycast 只需要直觉,不要求 BGP 细节
- 对比 L4 和 L7 负载均衡的适用场景
- 理解一致性哈希的价值(具体实现属于分布式缓存章节)
遭遇战 → 获得技能
第1战:DNS 分层——全世界最大的分布式 K/V 存储
问题: 你在法师观测镜输入 www.example.com 回车。观测镜需要知道这个法术域名对应的驿道坐标。它去问谁?
答案不是一个服务器,而是一整棵树。
┌───────────────────────┐
│ 根 DNS 服务器 │ (13 组逻辑根,任播)
│ (.) │
└────────┬──────────────┘
│
┌────────┴──────────────┐
│ 顶级域 DNS 服务器 │ .com / .org / .cn / .io ...
│ (TLD) │
└────────┬──────────────┘
│
┌────────┴──────────────┐
│ 权威 DNS 服务器 │ example.com 的权威记录
│ (Authoritative) │
└───────────────────────┘DNS 层次结构(从根到叶):
| 层级 | 角色 | 例子 |
|---|---|---|
| 根服务器 | 13 组逻辑服务器(物理节点数百个,靠 Anycast 分布全球),知道所有 TLD 服务器的地址 | a.root-servers.net |
| TLD 服务器 | 管理顶级域(如 .com、.org、.cn、.io),知道该域下所有权威服务器的地址 | Verisign 运营 .com |
| 权威服务器 | 管理具体域名,持有最终的 DNS 记录 | 你买域名时配置的 NS 记录指向它 |
| 递归解析器 | 替客户端做全部查询工作,一般由 ISP 或公共服务商运营 | 8.8.8.8 (Google), 1.1.1.1 (Cloudflare) |
关键区分:递归 vs 迭代
客户端 → 递归解析器(帮你跑完全程)
↓
递归解析器 → 根服务器(告诉我 .com 的服务器在哪)
← 返回 .com TLD 的 IP
↓
递归解析器 → .com TLD(告诉我 example.com 的权威服务器在哪)
← 返回权威服务器的 IP
↓
递归解析器 → 权威服务器(www.example.com 的 IP 是多少?)
← 返回 93.184.216.34
↓
递归解析器 → 客户端(93.184.216.34)- 递归查询(Recursive): 客户端对递归解析器说「我要
www.example.com,你查完告诉我」——自己什么也不用做,等人给结果。 - 迭代查询(Iterative): 递归解析器对各级服务器说「你不知道?那告诉我去问谁」——多次往返,每次拿到下一站的地址。
实际中,递归解析器会缓存中间结果,不需要每次从头查。下一战细聊。
用 Python 模拟一次 DNS 查询(实际调用系统解析器):
import socket
def dns_lookup(hostname: str) -> str:
try:
ip = socket.gethostbyname(hostname)
return ip
except socket.gaierror as e:
return f"解析失败: {e}"
# socket.gethostbyname 底层调用 getaddrinfo(3),
# 这个函数会走系统配置的解析流程(/etc/resolv.conf → 递归解析器 → 缓存)
print(dns_lookup("www.example.com")) # 93.184.216.34
print(dns_lookup("google.com")) # 142.250.80.46(可能因区域不同)** 实际经验:**
gethostbyname()是旧 API,不支持 IPv6(AAAA 记录)。现代代码用getaddrinfo()更稳妥。Python 的socket.getaddrinfo()是跨平台的封装。
用 dig 命令看 DNS 查询全貌(这是排查网络问题时最常用的工具之一):
# +trace 模拟递归解析器一步一步查
dig www.example.com +trace
# 只看特定记录类型
dig www.example.com A # IPv4
dig www.example.com AAAA # IPv6
dig example.com MX # 邮件交换记录
dig example.com NS # 域名服务器记录
# 使用特定递归解析器
dig @8.8.8.8 www.example.com # 用 Google 的 DNS 查第2战:DNS 记录类型与缓存——TTL 是一切取舍的核心
问题: 你改了域名解析法阵记录(比如把 www.example.com 指向新信标塔),为什么等了 24 小时才生效?
因为你改的是权威服务器上的记录。但全世界的递归解析器和浏览器都在缓存旧记录。缓存的有效期由 TTL(Time To Live) 决定——单位是秒。
常见 DNS 记录类型:
| 类型 | 全称 | 作用 | 例子 |
|---|---|---|---|
| A | Address | 域名 → IPv4 | example.com → 93.184.216.34 |
| AAAA | IPv6 Address | 域名 → IPv6 | example.com → 2606:2800:220:1:248:1893:25c8:1946 |
| CNAME | Canonical Name | 域名 → 另一个域名(别名) | www.example.com → example.com |
| MX | Mail Exchange | 域名 → 邮件服务器 | example.com → mail.example.com (优先级 10) |
| NS | Name Server | 域名 → 权威 DNS 服务器 | example.com → ns1.example.com |
| TXT | Text | 存储任意文本 | SPF 记录、域名验证 |
| SOA | Start of Authority | 域的权威信息(主服务器、管理员邮箱、序列号、刷新间隔) | — |
CNAME 的特殊之处: CNAME 指向另一个域名,解析器需要额外一次查询才能拿到最终 IP。这意味着:
- CNAME 记录不能和其他记录共存(一个域名要么是 CNAME,要么有 A/AAAA 记录,二选一)
- CNAME 的目标域名可以跨域(如
cdn.example.com → cdn.cloudflare.net)
TTL 的博弈论:
短 TTL(60 秒): 变更快速生效,但缓存命中率低,权威服务器压力大
长 TTL(86400 秒=1天): 缓存命中率高,但变更要等一天
运维操作的核心原则:修改前降低 TTL → 等旧缓存过期 → 修改记录 → 确认生效后再恢复短 TTLimport socket
import time
def resolve_and_time(hostname: str, use_cache: bool = True) -> tuple[str, float]:
"""测量 DNS 解析耗时"""
start = time.perf_counter()
# 清空缓存(仅在调试场景)
if not use_cache:
socket.setdefaulttimeout(5)
ip = socket.gethostbyname(hostname)
elapsed = (time.perf_counter() - start) * 1000
return ip, elapsed
# 第一次查询(可能需要走完整递归)
ip, ms = resolve_and_time("www.github.com")
print(f"[首次] {ip} — {ms:.1f}ms")
# 第二次查询(大概率命中本地 DNS 缓存)
ip, ms = resolve_and_time("www.github.com")
print(f"[缓存] {ip} — {ms:.1f}ms")
# 第二次通常比首次快几十倍;如果 TTL 短或缓存过期,差异不明显第3战:CDN——把世界放在你家门口
问题: 一座法术影音塔在加州有一座信标塔。上海的法师想看 4K 法术影像。魔力流横跨太平洋,延迟至少 120ms,驿道容量成本天价。
怎么办?把法术内容复制到离用户近的地方。这就是 CDN(法术内容分发驿道)。
CDN 的核心架构:
┌──────────────────────┐
┌────┤ 源服务器 (Origin) ├────┐
│ └──────────────────────┘ │
│ │
┌─────┴──────┐ ┌──────┴─────┐
│ 边缘节点 1 │ │ 边缘节点 2 │
│ (上海) │ ... │ (纽约) │
└─────┬──────┘ └──────┬─────┘
│ │
┌─────┴──────┐ ┌──────┴─────┐
│ 用户 A │ │ 用户 B │
└────────────┘ └────────────┘三大要素:
边缘节点(Edge/POP Node): 部署在全球各地的缓存服务器。用户请求的静态资源(图片、CSS/JS、视频片段)存储在边缘节点上,用户从最近的边缘节点获取,而不是跨越半个地球去源服务器。
回源(Origin Pull): 如果边缘节点没有用户请求的资源(缓存未命中),它会向源服务器请求一份副本,缓存到本地,然后返回给用户。写入缓存后,同一资源的下一个请求就直接命中边缘了。
Anycast 路由: 多个边缘节点共享同一个 IP 地址。用户的请求自动被路由到「最近」的节点(BGP 层面的最短路径)。这种技术的优势是不需要用户做任何配置——DNS 解析结果和用户地理位置无关,路由由互联网的自治系统自动决定。
Anycast 的与众不同之处(对比 Unicast):
Unicast(传统的单播):
同一 IP 只在同一台机器上
你可能离那台机器很远
Anycast(任播):
同一 IP 分布在全球多台机器上
BGP 路由协议会把你的请求送到「离你最近」的那台CDN 的 DNS 层面整合:
用户输入 www.example.com
↓
DNS 解析 → 权威 DNS 返回的其实是 CDN 的边缘节点 IP
(而不是源服务器的真实 IP)
↓
用户向该 IP 请求资源
↓
命中了边缘缓存? → 直接返回
没命中? → 回源拉取 + 缓存用 Python 检测 CDN 的存在(通过查看响应头):
import requests
def check_cdn(url: str) -> dict:
resp = requests.get(url, timeout=5)
headers = dict(resp.headers)
cdns = {
"Cloudflare": "cf-ray",
"Akamai": "x-akamai-",
"Fastly": "x-served-by",
"CloudFront": "x-amz-cf-",
"阿里云 CDN": "x-cdn-",
"腾讯云 CDN": "x-cache-from",
}
detected = []
for name, pattern in cdns.items():
for k in headers:
if pattern.lower() in k.lower():
detected.append(name)
break
return {
"url": url,
"status": resp.status_code,
"detected_cdn": detected or ["未检测到 CDN"],
"cache_headers": {
"cache-control": headers.get("Cache-Control"),
"age": headers.get("Age"), # 资源在 CDN 中缓存的秒数
"x-cache": headers.get("X-Cache"), # HIT/MISS
}
}
# 测试一个典型的 CDN 加速站点
result = check_cdn("https://www.example.com")
print(f"CDN 检测: {result['detected_cdn']}")
print(f"缓存状态: {result['cache_headers']}")** Java / C++ 差异:** 网络请求库在各语言中 API 不同,但核心流程一致。Java 用
java.net.HttpURLConnection或 Apache HttpClient,C++ 用 cpprestsdk 或 libcurl。DNS 解析过程对用户层透明——Python 的requests、Java 的URL、C++ 的asio底层都调用了系统的getaddrinfo(3)。你不需要为每一层重写协议栈。
第4战:DNS 在 CDN 中的角色——智能解析与 GeoDNS
CDN 的最后一公里依赖 DNS 做流量调度。实现方式叫 GeoDNS——根据发起查询的 IP 地址所在地,返回不同的结果。
用户 A(北京)查询 www.cdn-example.com
↓
GeoDNS → IP 归属查询 → 北京电信
↓
返回最近边缘节点:123.123.123.1(北京边缘)
用户 B(纽约)查询同一域名
↓
GeoDNS → IP 归属查询 → 纽约 Verizon
↓
返回最近边缘节点:98.98.98.1(纽约边缘)GeoDNS 的局限与 Anycast 的补充:
GeoDNS 在域名解析法阵层面做了一次地理路由。但 DNS 解析结果会被缓存(受 TTL 限制),缓存期间法师如果移动了怎么办?以及,GeoDNS 只能粗粒度调度(法师城/法术灵脉服务商级别),不能做到请求级别的负载感知。
所以大型 CDN 一般两层配合:
- GeoDNS → 把用户引导到最近的集群(比如华东 vs 北美)
- Anycast → 集群内部自动路由到具体边缘节点
第5战:负载均衡——把流量驯服成一只温顺的羊
问题: 你的驿道信标塔每秒收到 10 万法术请求。一座信标塔每秒只能处理 1 万。怎么办?十座信标塔。问题变成了:法术请求怎么分配到十座信标塔上?
这就是负载均衡器(Load Balancer) 的工作。
L4 vs L7 负载均衡:
| 特性 | L4(传输层) | L7(应用层) |
|---|---|---|
| 工作层级 | 基于 IP + 端口 | 基于 HTTP 内容(URL、Header、Cookie) |
| 性能 | 极高(纯内核转发) | 稍低(需要解析应用层协议) |
| 灵活性 | 低——只根据 IP/PORT 分发 | 高——根据 URL 路径、用户身份、内容类型分发 |
| 例子 | LVS (Linux Virtual Server)、IPVS | Nginx、HAProxy、Envoy、Traefik |
| 适用场景 | TCP/UDP 协议的任何服务,或不需要应用层信息 | HTTP/HTTPS/gRPC,需要内容感知路由 |
L4 的工作原理: 本质上是一个 NAT 路由器。收到 Client → LB:80 的包,把目标地址改写为 Backend-3:8080,转发。响应回来再改回去。
L7 的额外能力(以 Nginx 为例):
# L7 负载均衡可以根据 URL 路径分发到不同后端集群
upstream api_servers {
least_conn;
server api1.example.com:8080 weight=3;
server api2.example.com:8080 weight=2;
server api3.example.com:8080 backup; # 备用节点
}
upstream static_servers {
ip_hash; # 同一 IP 始终去同一后端(session 保持)
server static1.example.com:8080;
server static2.example.com:8080;
}
server {
listen 80;
location /api/ {
proxy_pass http://api_servers;
}
location /static/ {
proxy_pass http://static_servers;
}
}常见负载均衡算法:
| 算法 | 原理 | 适用场景 | 缺陷 |
|---|---|---|---|
| Round Robin | 轮流分配。请求 1 → 服务器 A,请求 2 → B,请求 3 → C,然后循环 | 服务器配置相同,请求处理时间相近 | 不感知负载差异 |
| Weighted Round Robin | 带权重的轮流,权重高的服务器收到更多请求 | 服务器配置不同(比如 4C8G vs 8C16G) | 权重只能粗调 |
| Least Connections | 发给当前活跃连接最少的服务器 | 请求处理时间差异大(长连接场景尤佳) | 需要维护连接计数 |
| IP Hash | 对客户端 IP 哈希,同一 IP 始终去同一后端 | 需要 session 保持(sticky session) | 哈希可能不均;后端变化时大量连接偏移 |
| 一致性哈希 | 在哈希环上分配,减少增删节点时的重映射 | 缓存集群、分布式存储 | 实现略复杂 |
深水区:一致性哈希的核心应用场景是分布式缓存(如 Redis 集群),本章只做原理介绍。它的完整算法、虚拟节点优化和运维实践属于专门的分布式缓存章节。
一致性哈希的精髓(也是最大难点):
传统哈希取模:hash("key1") % N,如果 N(服务器数量)变了,绝大部分 key 会映射到不同服务器——缓存失效风暴。
一致性哈希的做法:
把 N 台服务器映射到一个虚拟的环形空间(0 ~ 2^32-1)
1. 计算服务器节点的哈希值,放到环上
2. 每个 key 也计算哈希值,放到环上
3. 顺时针找到第一个服务器节点,就是该 key 的归属
4. 添加/删除一台服务器时,只影响它的邻居节点,
其他节点的映射不受影响 —— 只有 K/N 的 key 需要迁移 ┌───┐
┌──────┤ 0 ├──────┐
│ └───┘ │
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Node C │ │ key1 → A │
│ │ │ │
└──────┬──────┘ └──────┬──────┘
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ key2 → A │ │ Node A │
│ │ │ │
└──────┬──────┘ └──────┬──────┘
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Node B │ │ key3 → C │
└─────────────┘ └─────────────┘Python 实现一致性哈希(基础版):
import hashlib
import bisect
from typing import Any
class ConsistentHash:
"""一致性哈希环(带虚拟节点)"""
def __init__(self, nodes: list[Any], replicas: int = 3):
self.replicas = replicas # 每个物理节点的虚拟节点数
self.ring: list[int] = [] # 环上的哈希值(有序)
self.map: dict[int, Any] = {} # 哈希值 → 物理节点
self.nodes = set()
for node in nodes:
self.add_node(node)
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest()[:8], 16)
def add_node(self, node: Any):
self.nodes.add(node)
for i in range(self.replicas):
h = self._hash(f"{node}:{i}")
bisect.insort(self.ring, h)
self.map[h] = node
def remove_node(self, node: Any):
self.nodes.discard(node)
for i in range(self.replicas):
h = self._hash(f"{node}:{i}")
self.ring.remove(h)
del self.map[h]
def get_node(self, key: str) -> Any:
if not self.ring:
return None
h = self._hash(key)
# 二分查找:顺时针找第一个 ≥ h 的位置
idx = bisect.bisect_left(self.ring, h)
if idx == len(self.ring):
idx = 0 # 绕回环起点
return self.map[self.ring[idx]]
def stats(self) -> dict:
"""统计各节点负责的 key 数量(用于测试分布均匀性)"""
counts = {n: 0 for n in self.nodes}
if not self.ring:
return counts
# 每个哈希段属于顺时针遇到的下一个节点
for i, h in enumerate(self.ring):
nxt = self.ring[(i + 1) % len(self.ring)]
node = self.map[h]
counts[node] += 1
return counts
# 测试一致性哈希
nodes = ["server-a", "server-b", "server-c"]
ch = ConsistentHash(nodes, replicas=100) # 增加虚拟节点改善均匀性
# 模拟 1000 个 key 的分布
keys = [f"user:{i}" for i in range(1000)]
distribution: dict[str, int] = {n: 0 for n in nodes}
for k in keys:
n = ch.get_node(k)
distribution[n] += 1
print("初始分布:", distribution)
# 移除 server-b
ch.remove_node("server-b")
new_dist: dict[str, int] = {n: 0 for n in ["server-a", "server-c"]}
for k in keys:
n = ch.get_node(k)
new_dist[n] += 1
print("移除 server-b 后:", new_dist)
# 只有原本在 server-b 上的 key 迁移到了 a 和 c
# server-a 和 server-c 上原本的 key 不受影响(约 2/3 保持稳定)虚拟节点(Virtual Node / Replica)的作用:
没有虚拟节点时,环上的少量服务器节点分布可能不均匀——某段环上的 key 太多。加虚拟节点相当于把每个物理节点复制成 N 份随机分布在环上,统计学上大幅改善均匀性。
常见陷阱
项目:迷你 DNS 解析器 + 本地缓存
实现一个命令行工具 py-dns,模拟递归解析器行为:检查本地缓存,命中则返回;未命中则用系统 DNS 解析后缓存结果。
#!/usr/bin/env python3
"""
py-dns —— 迷你 DNS 解析器 + 缓存
用法:
python py-dns.py www.example.com
"""
import sys
import socket
import time
import json
import os
CACHE_FILE = os.path.expanduser("~/.py-dns-cache.json")
def load_cache() -> dict:
try:
with open(CACHE_FILE) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def save_cache(cache: dict):
with open(CACHE_FILE, "w") as f:
json.dump(cache, f, indent=2)
def lookup(hostname: str, ttl_default: int = 300) -> dict:
"""模拟递归解析器(带 TTL 缓存)"""
cache = load_cache()
now = time.time()
hostname = hostname.lower().strip()
# 检查缓存
if hostname in cache:
entry = cache[hostname]
if now < entry["expires"]:
remaining = int(entry["expires"] - now)
return {
"hostname": hostname,
"ip": entry["ip"],
"source": "cache",
"ttl_remaining": remaining,
}
else:
del cache[hostname]
# 未命中 → 调用系统解析器
try:
ip = socket.gethostbyname(hostname)
except socket.gaierror as e:
return {"hostname": hostname, "error": str(e)}
# 存入缓存(TTL 硬编码为 5 分钟,权威 TTL 信息从 socket 无法获取)
cache[hostname] = {
"ip": ip,
"expires": now + ttl_default,
}
save_cache(cache)
return {
"hostname": hostname,
"ip": ip,
"source": "resolver",
"ttl": ttl_default,
}
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: py-dns.py <hostname>")
sys.exit(1)
result = lookup(sys.argv[1])
if "error" in result:
print(f"✗ {result['hostname']}: {result['error']}")
else:
source = "📦 缓存" if result["source"] == "cache" else "🌐 系统解析"
extra = f" | TTL 剩余 {result['ttl_remaining']}s" if "ttl_remaining" in result else f" | TTL {result['ttl']}s"
print(f"{result['hostname']} → {result['ip']} ({source}{extra})")运行示例:
$ python py-dns.py www.example.com
www.example.com → 93.184.216.34 (🌐 系统解析 | TTL 300s)
$ python py-dns.py www.example.com
www.example.com → 93.184.216.34 (📦 缓存 | TTL 剩余 294s)这个简陋的缓存系统就是真实 DNS 递归解析器的核心逻辑简化版。当然,真实的 BIND 或 Unbound 要复杂得多——处理并发、EDNS、DNSSEC、缓存淘汰策略等等——但灵魂是一样的。
通关挑战
问题 1(DNS 解析): 假设你修改了 example.com 的 A 记录从 IP_A 改成 IP_B,但你的用户仍然访问到 IP_A。列出所有可能的缓存层级和对应的 TTL 控制策略。
问题 2(CDN): 你的服务需要向全球用户分发静态文件(JS/CSS/图片),文件更新频率每周一次。CDN 的 TTL 应该设多久?如果更新了文件但 CDN 还在返回旧版本,有哪些方法强制刷新?
问题 3(一致性哈希): 5 台服务器,一致性哈希环上均匀分布。移除 1 台后,大约有多少比例的 key 需要迁移?
问题 4(L4 vs L7): 你的 HTTP API 需要根据 X-User-Region 请求头发送不同响应。应该用 L4 还是 L7 负载均衡?为什么?
问题 5(实战调试): 你的同事说「api.example.com 解析出来的 IP 不对」。你用 dig 查正常,但用浏览器访问就是旧 IP。可能是什么原因?怎么排查?
验收标准
| 技能 | 自检方法 |
|---|---|
| DNS 解析流程 | 能手绘递归解析器到根/TLD/权威服务器的完整查询链 |
| DNS 记录类型 | 能说出 A、AAAA、CNAME、MX、NS 的用途和查询方式 |
| 缓存与 TTL | 能解释 DNS 变更需要等多久生效,以及如何加速 |
| CDN 原理 | 能说清边缘节点、回源、Anycast、GeoDNS 的配合关系 |
| 负载均衡算法 | 能对比 Round Robin、最少连接、一致性哈希的适用场景 |
| 一致性哈希 | 能手算新增/减少节点时 key 的迁移比例 |
常见卡点
把 DNS 缓存和 HTTP 缓存搞混。 DNS 缓存管的是「域名 → IP」的映射,在操作系统或路由器层面;HTTP 缓存管的是「URL → 资源」的映射,在浏览器或 CDN 层面。TTL 是各自独立的。
以为 TTL 是 DNS 解析的「生效延迟」。 TTL 是缓存有效期,不是 DNS 记录传播时间。权威服务器上的记录一改就生效 —— 问题是缓存里的旧记录要等到 TTL 到期才会去刷新。
认为
dig +trace是实际查询路径。+trace模拟递归解析过程,但用的是你的本地 DNS 工具而不是递归解析器。真实查询中,递归解析器有缓存、有转发策略,路径可能完全不同。一致性哈希的「均匀分布」不是自动的。 少量节点在环上的分布天然不均匀,必须加虚拟节点(replicas)或使用
ketama算法改进。把 Anycast 当作「自动负载均衡」。 Anycast 只在网络层做路由选择,它不知道后端负载状态。如果某个节点过载,Anycast 不会自动绕开它——需要在应用层有健康检查和故障剔除机制。
现在不需要理解
- DNSSEC 的签名链和 RRSIG 记录细节——知道它存在就行,真实部署场景才会用到
- EDNS 扩展机制——解决 DNS 报文长度限制的扩展,知道有这个东西即可
- Envoy、Traefik、Kong 等云原生网关的详细配置——偏运维的负载均衡器配置,是下一层级的技能
- BGP Anycast 的具体路由配置——那是网络工程师的工作,程序员的掌握程度是「理解原理」足够
- DNS 的 DoH/DoT(DNS over HTTPS/TLS)——隐私增强协议,概念简单,需要时查文档即可
旅人笔记
- DNS 是互联网的隐式基础设施——你每次请求都要过它,但平时根本感觉不到它的存在。所有系统瓶颈排查,DNS 都是第一检查项。
- CDN 的核心哲学是「把内容推到离用户最近处」——延迟是物理定律(光速),不能消除但能缩短。边缘计算的逻辑也一样。
- 一致性哈希是分布式系统中最重要的设计模式之一。你会在缓存、数据库分片、消息队列分区中反复遇到它。
- 负载均衡器不是万能的——健康检查、熔断、重试、超时设置每个都要想清楚。一个过载的负载均衡器比一个挂掉的服务器更可怕。
- TTL 是一切缓存策略的命门:太短⇒压力大,太长⇒更新慢。调 TTL 是分布式系统的日常手艺活。
→ 下一站预告
DNS 帮我们把域名变成了 IP,负载均衡把请求送到具体服务器,CDN 把内容推到边缘。但所有这些都建立在一个假设上:数据能在两个端点之间可靠地传输。下一章,我们下降到数据链路层——看看以太网帧长什么样、MAC 地址怎么工作、交换机怎么阻止冲突域扩散,以及为什么说「网线不通」的诊断要从物理层开始。