K8s CoreDNS 定制化:自定义域名解析与上游转发策略
DNS 是 K8s 集群的"电话簿"。书页折了一个角,整个集群都在打错电话。
一、场景痛点
微服务 A 调用微服务 B,通过 b-service.default.svc.cluster.local 访问。某天运维把 B 迁移到了另一个 K8s 集群,在入口网关做了流量转发。但 A 里面的代码写死了 b-service 这个 Service 名——域名解析还是指向了老集群的空 Service,所有调用 5 秒超时。
方案有两个:
这就是 CoreDNS 定制化的价值。K8s 自带的 DNS 只解决"集群内 Service 发现",但当你的架构发展到混合云、多集群、外部服务集成时,你需要 DNS 层具备路由重写、上游转发、条件解析的能力。
CoreDNS 的 Corefile 插件体系就是为这些场景设计的。
二、底层机制与原理剖析
2.1 CoreDNS 的插件链模型
插件链按顺序执行。如果 kubernetes 插件匹配到了 Service,直接返回结果,不经过 rewrite。如果没匹配到,继续走 rewrite → forward。
关键:插件顺序决定了解析优先级。把 rewrite 放在 kubernetes 前面可以实现"优先自定义规则,没命中再走 Service 发现"。
2.2 Corefile 的核心配置段
# CoreDNS 的标准 Corefile 结构
.:53 {
# Zone 定义: "." 表示处理所有域的查询
# 1. 基础插件:日志、缓存、健康检查
errors # 将错误输出到标准输出
health { # 健康检查端点 :8080/health
lameduck 5s # 优雅关闭前等待 5 秒
}
ready # 就绪探测端点 :8181/ready
# 2. 核心解析插件(按优先级排序)
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
# 3. 域名重写(自定义规则)
rewrite stop {
# 规则定义…
}
# 4. 上游转发
forward . /etc/resolv.conf {
max_concurrent 1000
}
# 5. 缓存
cache 30
loop
reload
loadbalance
}
2.3 请求流转的完整路径
Pod 发起 DNS 查询 b-service.default.svc.cluster.local
│
├─ Step 1: 检查 Pod 的 /etc/resolv.conf
│ └─ nameserver 指向 kube-dns (CoreDNS) Service ClusterIP
│
├─ Step 2: 请求到达 CoreDNS
│ └─ 匹配 Corefile 中的插件链
│
├─ Step 3: kubernetes 插件检查
│ ├─ b-service 在 default namespace 的 Service 列表?
│ │ ├─ 是 → 返回 ClusterIP 10.96.1.5
│ │ └─ 否 → fallthrough 到 rewrite 插件
│ │
├─ Step 4: rewrite 插件处理
│ ├─ 匹配 rewrite 规则?
│ │ ├─ 是 → 改写域名,继续下一个插件
│ │ └─ 否 → fallthrough
│
├─ Step 5: forward 插件转发
│ └─ 转发到上游 DNS 服务器 (/etc/resolv.conf)
│
└─ Step 6: 返回 IP 地址给 Pod
三、生产级代码实现
3.1 四大典型场景的 CoreDNS 配置
# ============================================================
# ConfigMap: coredns-custom
# K8s 1.18+ 推荐方式: 在 coredns ConfigMap 同级目录添加自定义配置
# ============================================================
—
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
# ======== 核心 Corefile(主配置文件) ========
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
# K8s 集群内域名解析
# cluster.local 是默认的集群域名后缀
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
# 引入所有 Server Block 文件
# 每个场景一个独立的 Server Block 文件
import custom/*.server
# 自定义域名重写规则
import custom/*.rewrite
# 上游 DNS 转发
forward . /etc/resolv.conf {
max_concurrent 1000
policy sequential # 按顺序尝试上游 DNS
}
cache 30
loop
reload
loadbalance
}
—
# ============================================================
# 场景 1: 域名重写(internal DNS alias)
# 文件: custom/rewrite.rewrite
#
# 问题: 服务从 K8s 内迁移到了外部,但 15 个微服务代码里写死了老域名
# 解决: DNS 层做域名重写,不需要改业务代码
# ============================================================
# 1.1 把集群内域名重写为外部域名(不带 stop 关键字,继续走 forward)
rewrite name b-service.default.svc.cluster.local b-service.prod.example.com
# 1.2 环境感知的重写(根据请求来源 namespace)
rewrite name regex (.*)\\.default\\.svc\\.cluster\\.local {1}.production.example.com
# 1.3 把老系统的域名映射到新的微服务
rewrite name legacy-api.internal.local api-gateway.default.svc.cluster.local
—
# ============================================================
# 场景 2: 上游转发策略(多上游 + 分区解析)
# 文件: custom/forward.server
#
# 问题: 部分外部域名需要走特定的 DNS 服务器(如公司内网 DNS)
# 解决: 按域名分区,不同域走不同的上游
# ============================================================
# 2.1 公司内网域名走内网 DNS
internal.example.com:53 {
errors
cache 30
forward . 10.0.1.53 10.0.1.54 {
max_concurrent 1000
policy round_robin # 轮询上游 DNS 服务器
}
}
# 2.2 外部域名走阿里云公共 DNS(100.100.2.136 加密 DNS)
external-vendor.example.com:53 {
errors
cache 60
forward . 100.100.2.136 100.100.2.138 {
tls_servername dns.aliyun.com
# TLS 加密 DNS 查询,防止中间人劫持
max_concurrent 500
}
}
# 2.3 默认外部域名走上游
.:53 {
errors
cache 30
forward . 223.5.5.5 119.29.29.29 { # 阿里 + 腾讯公共 DNS
max_concurrent 1000
policy sequential
# sequential: 第一个失败才尝试第二个,避免不同 DNS 返回结果不一致
}
}
—
# ============================================================
# 场景 3: 条件解析(不同 namespace 走不同解析路径)
# 文件: custom/conditional.rewrite
#
# 问题: staging 环境的服务需要调用 staging 的外部 API
# 解决: 根据请求来源 namespace,返回不同的解析结果
# ============================================================
# 3.1 staging namespace 的服务调用外部 staging API
rewrite name api.external-service.example.com api.staging.example.com
# 3.2 使用 response rewrite 实现视网段返回
# CoreDNS 不原生支持按来源 IP 路由,通过 template 插件实现:
template IN A {
# 当查询 *.staging.svc.cluster.local 时
match "^(.*)\\.staging\\.svc\\.cluster\\.local\\.$"
# 返回固定的 staging 网关 IP
answer "{{ .Name }} 60 IN A 10.96.100.1"
fallthrough
}
—
# ============================================================
# 场景 4: 自定义 DNS 劫持(测试/调试场景)
# 文件: custom/override.server
#
# 问题: 需要将某个外部 API 的流量劫持到本地的 mock 服务做测试
# 解决: 通过 hosts 插件返回 mock 服务的 ClusterIP
# ============================================================
hosts {
# 将第三方支付网关劫持到集群内的 mock 服务
10.96.50.10 payment-gateway.external-bank.example.com
10.96.50.10 api.payment.external-bank.example.com
# 将外部分析服务劫持到内网测试实例
10.96.50.20 analytics-collector.saas-vendor.example.com
fallthrough # 未匹配的域名继续走下游插件
}
3.2 运维脚本:DNS 解析诊断工具
#!/bin/bash
# ============================================================
# dns-diagnose.sh
# K8s DNS 解析诊断脚本
# 从 Pod 内部测试 DNS 解析链路,定位问题环节
# ============================================================
set -euo pipefail
NAMESPACE="${1:-default}"
TEST_DOMAIN="${2:-b-service.default.svc.cluster.local}"
COREDNS_SVC_IP="${3:-10.96.0.10}" # kube-dns Service ClusterIP
echo "============================================"
echo "K8s DNS 诊断: ${TEST_DOMAIN}"
echo "Namespace: ${NAMESPACE}"
echo "============================================"
# Step 1: 检查 /etc/resolv.conf 配置
echo ""
echo "[1/7] 检查 Pod DNS 配置 (/etc/resolv.conf)"
echo "——————————————-"
cat /etc/resolv.conf
NDOTS=$(grep "options" /etc/resolv.conf | grep -oP "ndots:\\d+" || echo "ndots:5")
echo ""
echo " 注意: ${NDOTS},如果域名中的点号 < ndots,会先尝试 search domain 后缀"
# Step 2: 使用 nslookup 解析
echo ""
echo "[2/7] nslookup 解析 (通过 /etc/resolv.conf)"
echo "——————————————-"
nslookup "${TEST_DOMAIN}" 2>&1 || echo " 解析失败"
# Step 3: 直接查询 CoreDNS
echo ""
echo "[3/7] 直接查询 CoreDNS (${COREDNS_SVC_IP})"
echo "——————————————-"
nslookup "${TEST_DOMAIN}" "${COREDNS_SVC_IP}" 2>&1 || echo " CoreDNS 查询失败"
# Step 4: 查询上游 DNS
UPSTREAM_DNS=$(grep "^nameserver" /etc/resolv.conf | head -1 | awk '{print $2}')
echo ""
echo "[4/7] 查询上游 DNS (${UPSTREAM_DNS})"
echo "——————————————-"
nslookup "${TEST_DOMAIN}" "${UPSTREAM_DNS}" 2>&1 || echo " 上游 DNS 查询失败"
# Step 5: 绕开 search domain 做精确查询
echo ""
echo "[5/7] 精确查询 (加末尾点号,绕开 search domain)"
echo "——————————————-"
nslookup "${TEST_DOMAIN}." "${COREDNS_SVC_IP}" 2>&1 || echo " 精确查询失败"
# Step 6: 使用 dig 查看解析详情
echo ""
echo "[6/7] dig 详情 (查询时间 + 查询链)"
echo "——————————————-"
dig "${TEST_DOMAIN}" @"${COREDNS_SVC_IP}" +noall +answer +stats 2>&1 || echo " dig 查询失败"
# Step 7: CoreDNS metrics 检查(需要从 CoreDNS Pod 或 metrics 端点)
echo ""
echo "[7/7] CoreDNS 指标检查"
echo "——————————————-"
# 检查 CoreDNS 的 Prometheus metrics
echo " CoreDNS metrics 暴露在 :9153/metrics"
echo " 关键指标:"
echo " coredns_dns_requests_total – 总请求数"
echo " coredns_dns_responses_total – 总响应数"
echo " coredns_dns_request_duration_seconds – 请求延迟"
echo " coredns_forward_requests_total – 转发请求数"
echo " coredns_forward_responses_total – 转发响应数"
echo " coredns_cache_hits_total – 缓存命中数"
echo " coredns_cache_misses_total – 缓存未命中数"
echo ""
echo "============================================"
echo "诊断完成"
echo "============================================"
3.3 验证配置
# 使用 test-pod 验证 DNS 解析
apiVersion: v1
kind: Pod
metadata:
name: dns-test
namespace: default
spec:
containers:
– name: test
image: busybox:1.36
command:
– sleep
– "3600"
resources:
limits:
memory: "128Mi"
cpu: "100m"
restartPolicy: Never
—
# 执行验证命令
# kubectl exec -it dns-test — nslookup b-service.default.svc.cluster.local
# kubectl exec -it dns-test — nslookup external-api.example.com
# kubectl exec -it dns-test — nslookup internal-service.corp.local
3.4 Python 客户端:DNS 解析监控
"""
DNS 解析监控工具
定期探测关键域名的解析延迟,异常时告警
监控维度:
1. 解析成功/失败计数
2. 解析延迟分布 (P50/P95/P99)
3. CoreDNS 上游转发延迟
4. 域名解析结果变化检测(IP 漂移)
"""
import asyncio
import socket
import time
from dataclasses import dataclass, field
from collections import defaultdict
@dataclass
class DNSProbeResult:
domain: str
resolved_ips: list[str]
latency_ms: float
success: bool
error: str = ""
nameserver: str = ""
timestamp: float = field(default_factory=time.time)
class DNSMonitor:
"""
K8s DNS 解析监控器
监控目标:
1. 集群内 Service 域名解析
2. 外部域名解析(经 CoreDNS forward)
3. 跨环境域名解析
"""
def __init__(self, nameserver: str = "10.96.0.10"):
self.nameserver = nameserver # CoreDNS ClusterIP
# 关键域名列表(需要持续监控)
self.critical_domains = [
# 集群内 Service
"kubernetes.default.svc.cluster.local",
"kube-dns.kube-system.svc.cluster.local",
# 外部依赖
"api.openai.com",
"api.github.com",
# 内部服务
"redis-master.default.svc.cluster.local",
"postgres-rw.database.svc.cluster.local",
]
# 指标存储
self.metrics: dict[str, list[DNSProbeResult]] = defaultdict(list)
async def probe_domain(self, domain: str) -> DNSProbeResult:
"""
探测域名的 DNS 解析
使用 socket 做解析,测量端到端延迟
注意: 这会使用 /etc/resolv.conf 中的 nameserver,
即 CoreDNS 的 ClusterIP
"""
start = time.perf_counter()
try:
# 使用 asyncio 的 getaddrinfo 做异步 DNS 解析
loop = asyncio.get_event_loop()
addrs = await loop.getaddrinfo(
domain,
None,
family=socket.AF_INET,
proto=socket.IPPROTO_TCP
)
elapsed = time.perf_counter() – start
ips = list(set(addr[4][0] for addr in addrs))
return DNSProbeResult(
domain=domain,
resolved_ips=ips,
latency_ms=elapsed * 1000,
success=True,
nameserver=self.nameserver
)
except Exception as e:
elapsed = time.perf_counter() – start
return DNSProbeResult(
domain=domain,
resolved_ips=[],
latency_ms=elapsed * 1000,
success=False,
error=str(e),
nameserver=self.nameserver
)
async def run_probe_cycle(self) -> dict[str, DNSProbeResult]:
"""执行一轮全量探测"""
tasks = [self.probe_domain(d) for d in self.critical_domains]
results = await asyncio.gather(*tasks)
# 存储结果
for result in results:
self.metrics[result.domain].append(result)
# 只保留最近 100 条记录
if len(self.metrics[result.domain]) > 100:
self.metrics[result.domain] = self.metrics[result.domain][-100:]
return {r.domain: r for r in results}
def check_for_ip_drift(self, domain: str) -> bool:
"""
检测 DNS 解析结果的 IP 是否发生变化
IP 漂移可能是 DNS 劫持或 CoreDNS 配置错误
"""
results = self.metrics.get(domain, [])
if len(results) < 2:
return False
# 获取最近两个解析结果
latest_ips = set(results[-1].resolved_ips)
previous_ips = set(results[-2].resolved_ips)
return latest_ips != previous_ips
def report(self) -> str:
"""生成 DNS 监控报告"""
lines = ["\\n📊 DNS 解析监控报告", "=" * 50]
for domain in self.critical_domains:
results = self.metrics.get(domain, [])
if not results:
lines.append(f"\\n {domain}: 无数据")
continue
success_count = sum(1 for r in results if r.success)
latencies = [r.latency_ms for r in results if r.success]
if latencies:
latencies.sort()
p50 = latencies[len(latencies) // 2]
p95 = latencies[int(len(latencies) * 0.95)]
p99 = latencies[int(len(latencies) * 0.99)]
else:
p50 = p95 = p99 = 0
status = "✅" if success_count / len(results) > 0.99 else "⚠️"
lines.append(
f"\\n{status} {domain}"
f"\\n 成功率: {success_count}/{len(results)} ({success_count/len(results)*100:.1f}%)"
f"\\n 延迟: P50={p50:.1f}ms P95={p95:.1f}ms P99={p99:.1f}ms"
)
if self.check_for_ip_drift(domain):
lines.append(" ⚠️ IP 地址已发生变化,请检查!")
return "\\n".join(lines)
async def main():
monitor = DNSMonitor()
# 持续探测,每 30 秒一次
while True:
await monitor.run_probe_cycle()
print(monitor.report())
await asyncio.sleep(30)
if __name__ == "__main__":
asyncio.run(main())
四、边界分析与架构权衡
4.1 什么时候该用 CoreDNS 定制化,什么时候不该用?
该用的场景:
不该用的场景:
4.2 DNS 缓存导致的问题
CoreDNS 默认缓存 30 秒。如果你的服务切换了 IP 但 Pod 还在用缓存,会有 30 秒的"盲视期"。对于滚动更新来说不成问题(新旧 Pod 并存),但对于紧急切换场景,你需要:
# 关键服务的 DNS 记录使用低 TTL
kubernetes cluster.local {
ttl 5 # 非默认的 5 秒 TTL,而不是 30 秒
}
# 或者在 Service 上使用 annotation 指定 TTL
metadata:
annotations:
external-dns.alpha.kubernetes.io/ttl: "5"
4.3 性能影响
每条 rewrite 规则都会增加约 1-5 微秒的解析时间。如果配置了 50+ 条 rewrite 规则,P99 解析延迟可能从 1ms 涨到 5ms。
建议:
- rewrite 规则总数 < 20 条
- 使用正则匹配而不是逐条精确匹配
- 监控 coredns_dns_request_duration_seconds 指标
4.4 安全注意事项
DNS 重写可能导致中间人攻击风险:如果攻击者能修改 ConfigMap,可以将 payment.example.com 重写为攻击者控制的 IP。
防护措施:
- CoreDNS ConfigMap 的修改必须走 GitOps + PR review
- 对关键域名(支付、认证)使用 DNSSEC 验证
- 启用 CoreDNS 的 acl 插件限制查询来源
五、总结
CoreDNS 定制化的核心思想是在 DNS 层解决问题,而不是在应用层打补丁。DNS 是 K8s 最上层的基础设施——改一次配置,所有 Pod 自动生效。相比于让 15 个微服务改代码、协调发布时间窗口,DNS 层的修改是最高效的。
三个最重要的配置模式:
记住一个原则:DNS 修改是全局生效的,改之前先在测试环境验证,改之后看 CoreDNS metrics 确认生效了。别在凌晨 3 点的生产环境直接改 Corefile——这种事,干一次就够了。



