简介:派小星DNS二级域名分发V2.0重构版面向域名服务商、IDC站长及需要批量开通二级域名给终端用户的平台运营者,帮助完成域名资源统一接入、在线分发、会员套餐与合规审核等环节。本次重构重写了核心代码,在接口层、业务层和管理端均有明显升级,适合直接部署或二次开发。压缩包内共1371个文件,以681个JavaScript、400个CSS、67个PHP及SQL数据库脚本为主,覆盖前端交互、后台界面、业务逻辑与数据表结构,整体约16.93MB。功能上集成腾讯云、阿里云、Cloudflare、华为云、西部数码等主流DNS厂商,支持多级会员等级及专属折扣,可自由组合基础版、企业版、定制版等域名套餐;内置身份证、手机号、人脸三要素实名认证和违规关键词自动拦截,帮助规避监管风险。后台UI全新改版,支持域名批量操作、数据统计与模块化插件市场。已有191人学习下载。
1. 派小星DNS二级域名分发V2.0重构版:让每个用户都在几十秒内拿到独立子域名
做SaaS平台、容器云、设备接入服务的团队,迟早会遇到一个需求:给每个注册用户分配一个独立二级域名,比如 zhangsan.example.com、lisi.example.com,用户拿到就能直接用,后台改了配置域名要跟着变,用户注销了域名还得能回收。这个需求听起来不复杂,真正做起来却经常在 DNS 这一层翻车——改一条记录要等缓存过期、Zone 文件并发写入丢更新、日志里出现大量解析失败还定位不到原因。「派小星DNS二级域名分发V2.0重构版」这个项目,就是在解决这个具体问题:自建一套能动态分发二级域名的 DNS 解析服务,把「分配域名」从手动改记录变成调用一次接口就能完成。这篇笔记会从模型讲起,落到 CoreDNS 与控制面的具体实现,再把上线前后的坑逐条拆开。
2. 二级域名分发的核心模型:权威应答、泛解析与分发策略怎么选
2.1 在动手之前先分清楚:权威DNS、递归DNS和防缓存穿透
二级域名分发必须有一个自建的权威 DNS 服务器,这是整个方案的基石。公共 DNS(比如常见的 114.114.114.114 或 8.8.8.8)只是递归服务器,它们会替客户端去查询权威服务器,然后把结果缓存在自己那里。我们要做的是把 example.com 这个域的权威解析权拿到自己手里,让递归服务器在收到 someone.example.com 的查询时,最终回到我们的服务器上来取答案。
在 Linux 上自建 DNS 服务器,最常见的做法是 BIND 9 或者 CoreDNS。V2.0 重构版我选的是 CoreDNS,理由后面展开讲。但无论用哪个,都要先理解一条关键链路:递归服务器找权威服务器的顺序是从根域往下走的,example.com 的 NS 记录指向哪里,决定了解析权归谁。所以第一步不是写 Zone 文件,而是去域名注册商那里把 example.com 的 NS 记录改成我们自建 DNS 服务器的地址。如果这一步没做,后面所有配置都是白搭——自己服务器上解析得再对,公网上永远拿不到结果。
防缓存穿透是分发场景里必须提前设计的问题。普通网站解析量小,缓存穿透最多慢一点;但二级域名分发的场景下,每个用户都可能有自己的解析请求,如果每个二级域名都是第一次出现、没有缓存,权威服务器会瞬间被打满。常见做法是给泛解析记录设置一个合理的 TTL,让递归服务器缓存住大部分查询结果,同时控制面在更新记录时主动压低 TTL 值,等待缓存自然过期。
2.2 泛解析与精确解析的优先级:这是分发器最容易翻车的地方
很多人一开始会想到用泛解析来偷懒:在 Zone 里写一条*.example.com. IN A 1.2.3.4,所有二级域名都解析到同一个 IP。这在早期确实能用,但很快就会发现两个问题。第一,所有用户都指向同一个入口,业务层还得靠 Host 头来区分用户,分发就变成了名字分发,不是解析分发;第二,泛解析记录没法针对不同用户返回不同 IP,也没法做下线回收,用户注销后域名照样能解析。
DNS 协议有一个必须记住的优先级规则:精确匹配优先于泛匹配。也就是说,如果 Zone 里同时存在zhangsan.example.com. IN A 5.6.7.8和*.example.com. IN A 1.2.3.4,查询 zhangsan.example.com 时永远返回 5.6.7.8,不会去匹配泛解析。这条规则本身是好事,但它要求分发器在写入时小心处理“删除”逻辑——用户注销后,如果只删了精确记录而忘了保留泛解析,这个二级域名并不会变成不存在,而是会“掉进”泛解析里继续可用。这是一个非常隐蔽的安全漏洞。
所以在实际设计里,我一般会区分两种模式。纯分发模式:Zone 里不放泛解析,每个二级域名都显式写入精确记录,回收时直接删除。兜底模式:Zone 里保留一条泛解析指向默认网关,但控制面在删除精确记录时会把对应的用户标记为“已禁用”,网关层再做一次校验。生产环境我更推荐后者,因为纯分发模式下如果控制面写库失败,会出现一个域名既没写在 Zone 里也没被兜底,解析直接 NXDOMAIN,用户侧看到的就是“域名不存在”。
2.3 常见分发策略对比:A记录轮询、CNAME到业务网关、按区域调度
确定了精确加泛兜底的模型之后,接下来要决定每条解析记录返回什么。这里有三条路线,各有明确的适用边界。
A 记录轮询是最直观的:把一批后端 IP 放在 IP 池里,分发器按负载或哈希选择一个 IP,直接写入zhangsan.example.com. IN A 1.2.3.4。优点是解析链路短,客户端拿到 IP 直连;缺点是后端扩容时已经分出去的二级域名不会自动漂移,除非做健康检查后强制更新 TTL。适合后端是一组无状态网关的场景。
CNAME 到业务网关是第二种:每个二级域名解析为zhangsan.example.com. IN CNAME edge.example.com,业务网关根据域名做路由。这种做法的好处是后端 IP 变化时只需要改 edge.example.com 一条记录,所有二级域名自动跟随;坏处是 CNAME 链条增加了递归服务器的查询次数,且有些场景要求根域不能是 CNAME(但二级域名用 CNAME 没有这个限制)。V2.0 重构版我最终采纳的就是这种模式,后文会详细展开。
按区域调度是第三种:根据用户来源 IP 段返回不同区域的接入 IP。写着很美好,但二级域名分发场景里用得不多,因为大多数平台的用户量级根本到不了区域调度的规模。真需要的话,CoreDNS 里有 geoip 插件可以做,但我不建议一上来就上这个复杂度,先把前两种跑通再说。
| 策略 | 运维成本 | 后端扩容表现 | 适合场景 |
|---|---|---|---|
| A记录轮询 | 中:需要管理IP池 | 新域名走新IP,老域名不漂移 | 后端是固定网关池 |
| CNAME到网关 | 低:只改一条记录 | 自动跟随网关记录 | SaaS多租户平台 |
| 区域调度 | 高:需要region映射 | 视策略而定 | 全国性低延迟业务 |
3. 自建DNS + 控制面API:用CoreDNS跑通二级域名分发的落地方案
3.1 选型结论:为什么V2.0用CoreDNS而不是BIND9
BIND 9 是传统权威 DNS 的事实标准,配置语法成熟,出问题能找到大量文档。但它在二级域名分发场景里有一个致命短板:动态更新要么通过 nsupdate 工具向服务器发 UPDATE 报文,要么直接改 Zone 文件再 reload。nsupdate 协议写起来啰嗦,Zone 文件 reload 在记录量超过几万条时会有明显延迟,而且并发场景下两个进程同时写一个文件,容易出现半个文件写坏的情况。
CoreDNS 是 CNCF 出品的 Go 语言 DNS 服务器,配置简洁,插件化设计,其中 etcd 插件可以直接把解析数据存在 etcd 里,控制面只要写 etcd 的 key,DNS 数据面立即感知,不需要 reload 任何文件。这对分发场景来说是本质性的改变:分发器从「写文件等生效」变成了「写 KV 即生效」。
当然,CoreDNS 也不是没有坑。它默认不保存权威 Zone 的 SOA 记录细节,需要自己配置;它的泛解析插件写法也和 BIND 的 Zone 文件语法不同。但这些都可以接受。选型结论就是:如果团队已经决定要上一套控制面 API,直接选 CoreDNS + etcd;如果只想手工维护几百条记录、不想写任何代码,那老老实实用 BIND 9 加 Zone 文件反而更稳。
3.2 最小可运行的Corefile配置与启动命令
安装 CoreDNS 很简单,下载二进制解压就能跑。核心在 Corefile,这是整个服务器的唯一配置入口。下面这个配置是 V2.0 分发服务最小可运行版本:
example.com:53 { etcd { path /skydns endpoint http://127.0.0.1:2379 ttl 60 } errors log health :8091 prometheus :9153 }这段配置的每一行都有讲究。example.com:53表示监听 53 端口,作为 example.com 域的权威服务器。etcd插件指定解析数据从 etcd 读取,path 是前缀/skydns,endpoint 是本机 etcd 服务地址,ttl 是当 etcd 里的记录没有显式 TTL 时使用的默认值,这里设 60 秒,意味着递归服务器最多缓存一分钟。errors 和 log 开启错误和访问日志,health 开启健康检查端口供负载均衡器探活,prometheus 暴露指标。
把这段保存在/etc/coredns/Corefile后,启动命令是:
coturn不对,这里写错了,CoreDNS 的启动命令非常简单:
./coredns -conf /etc/coredns/Corefile启动后终端会打印监听的端口和加载的插件。如果出现Failed to start server: listen tcp :53: bind: permission denied,是因为 53 端口需要 root 权限,用sudo运行或者给二进制加cap_net_bind_service权限。
这里提一下在 Linux 修改 DNS 配置时常见的系统层问题:如果你是在本机用 CoreDNS 做测试,务必确认/etc/resolv.conf里指向的是 127.0.0.1 而不是外部 DNS。Ubuntu 22.04 这类系统里,NetworkManager 和 systemd-resolved 会自动改写这个文件,你改了之后重启网络就被还原。后面避坑章会专门讲这个。
3.3 把分发逻辑做成带状态的控制面:etcd键设计与API代码片段
CoreDNS 的 etcd 插件有一套自己的键路径规则。对于zhangsan.example.com.这条解析记录,它会去 etcd 里查找键/skydns/com/example/www/zhangsan。注意顺序:键根路径从顶级域开始写,域名是反着拼的,而且 www 这个中间段是example.com拆出来的。这套规则刚接触很容易搞反,写错键名的话,解析结果永远为空,排查起来十分痛苦。
我给出的通用做法是:不手写键路径,而是在控制面里写一个域名转键函数。下面是 Go 语言片段,完成从完整域名到 etcd key 的转换:
func domainToKey(domain string) (string, error) { domain = strings.TrimSuffix(domain, ".") parts := strings.Split(domain, ".") // 反转域名段: zhangsan.example.com -> [com example zhangsan] for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 { parts[i], parts[j] = parts[j], parts[i] } // 只保留 example.com 二级域名下的内容, 去掉前两段 if len(parts) < 3 { return "", fmt.Errorf("非法二级域名: %s", domain) } sub := parts[2:] return "/skydns/" + strings.Join(sub, "/"), nil }这个函数的核心逻辑是反转域名段并拼接成 CoreDNS etcd 插件期望的格式。参数说明:传入的 domain 必须是完整二级域名,比如zhangsan.example.com,函数先把末尾的点去掉,再按点分割、反转顺序,最后去掉前两段顶级域和主域,只保留二级域名字段。返回值就是 etcd key。
写入解析数据时,控制面需要同时设置 value 和 TTL。以 CNAME 分发为例,向 etcd 写入一条记录:
curl -X PUT http://127.0.0.1:2379/v2/keys/skydns/com/example/www/zhangsan \ -d value='{"host":"edge.example.com","ttl":60}' \ -d ttl=300这里 value 是一个 JSON 结构体,host字段表示 CNAME 的目标,ttl字段覆盖默认 TTL。curl 命令里那个ttl=300是 etcd 键本身的租约时间,到时间后键会自动删除,相当于给了分发服务一个“自动回收”能力——如果控制面崩了,旧记录最多存活 300 秒就消失,不会一直错误解析。
4. V2.0重构版做了什么:从zone文件手动维护到动态状态分发
4.1 V1版的三个让运维头疼的问题:改记录靠sleep、状态黑匣子、并发下丢更新
先说说 V1 版为什么需要重构,这能帮你判断自己是否也在走同样的弯路。V1 版的做法是 BIND 9 加 Zone 文件:控制面生成一份完整 Zone 文件,替换到 BIND 的配置目录,然后执行rndc reload。看起来简单,跑起来全是问题。
第一个问题是改记录靠 sleep。rndc reload是异步的,命令返回不代表新 Zone 已经生效。为了确认记录真的可解析,控制面必须在 reload 之后主动查询一次,确认通过再返回成功。很多团队图省事,直接 sleep 两秒就算完事,结果记录量大的时候两秒根本不够,用户端拿到的是旧解析。
第二个问题是状态黑匣子。Zone 文件是文本,控制面每次全量生成,一旦某个用户的数据在内存里被误删,生成的 Zone 文件里就少了一条目录,但你没有 diff 工具的话完全看不出来。V1 时代我们经常遇到用户投诉“我的域名解析不了”,查半天发现是某个后台任务把整段 Zone 记录覆盖了。
第三个问题是并发丢更新。两个控制面实例同时生成 Zone 文件,后写的会覆盖先写的,一个用户的更新操作直接弄丢另一个用户的。加锁能缓解,但 BIND reload 本身不是事务性的,写到一半断电,文件就坏了,服务直接不可用。
4.2 V2.0重构版的分发流程:控制面写状态、数据面只应答
V2.0 重构版的核心理念是“控制面和数据面彻底分离”。数据面保持无状态,只是 CoreDNS 进程在读取 etcd 中的记录并应答查询;控制面负责把接收到的业务请求翻译成对 etcd 的操作。这个架构最直接的好处是:没有文件、没有锁、没有 reload,所有状态都在 etcd 里,天然具备事务性和一致性。
分发流程可以概括为以下几步。第一步,业务方调用控制面 API,携带用户名、期望的二级域名前缀、目标 CNAME 或 IP;第二步,控制面做格式校验和权限校验,包括检查前缀是否被占用、域名是否包含非法字符;第三步,控制面把解析记录写入 etcd,路径用前面说的 domainToKey 拼接,同时写入一份独立的业务元数据键,记录这个二级域名属于哪个用户、什么时间分配的;第四步,客户端发起 DNS 查询,CoreDNS 从 etcd 读出记录返回应答。
这套流程下,一个用户的完整生命周期变得可控:创建域名就是一个 PUT,变更域名就是一个 PUT 覆盖,回收域名就是一个 DELETE。没有中间状态,也没有文件残留。
4.3 重构版关键参数:TTL、更新间隔、健康检查与灰度切换
重构后官方文档里看起来参数变少了,但有几个参数直接决定线上表现,我需要单独说明。
TTL 是第一个要调优的参数。V2.0 默认把解析记录 TTL 设置为 60 秒,这是刻意选择的。太短(比如 5 秒)会让递归服务器频频回源,权威服务器的请求量暴涨;太长(比如 3600 秒)会导致用户修改配置后要等一小时才能生效,这在 SaaS 分发场景是不能接受的。60 秒是一个让失败恢复时间和回源压力都能接受的中间值。如果你对故障恢复速度有更高要求,可以降到 30 秒,但一定要给 CoreDNS 加一层视图缓存做对冲。
etcd 的压缩间隔也要关注。分发场景下记录变更频繁,etcd 的历史版本如果不压缩会不断膨胀。我一般在控制面加一个后台任务,每隔一小时调用一次 etcd 的 compaction API,保留最近几十分钟的变更历史即可。
灰度切换是一个容易忽略的点。当你要调整分发策略时,不要直接改 Corefile 里的插件配置然后重启 CoreDNS——重启期间所有解析都会超时。正确做法是启动一个新版本的 CoreDNS 实例监听 5353 端口,先用 dig 手工验证新实例的解析结果,确认无误后通过 DNS 层把部分查询流量切过去,最后再平滑替换线上实例。
5. 避坑:二级域名分发上线前后的5个真实坑点
5.1 改完域名不生效:上游递归缓存、本地DNS缓存和TTL三层的联合作案
现象:在控制面更新了解析记录,dig 到自己的权威服务器查询显示新 IP,但在客户机上nslookup得到的还是旧 IP。
原因:这不是权威服务器的问题,而是缓存链路的三层叠加。第一层是客户机本机的 DNS 缓存;第二层是运营商递归服务器(比如 114.114.114.114)的缓存;第三层才是权威服务器。你 dig 权威服务器绕过了前两层,所以看到新记录,但普通用户走的是完整链路,旧记录还在缓存里。
解决:更新记录后的 60 到 120 秒内,控制面返回值只代表权威侧已生效,不代表全平台生效。如果业务要求“立即生效”,可以在控制面实现一个主动通知:调用公共递归服务器的缓存刷新接口,但这类接口不是所有运营商都提供。更务实的做法是把 TTL 默认值设短,并如实告知业务方“最长 60 秒生效”。
排查时一定要会用 dig 指定不同服务器对比:
dig @127.0.0.1 zhangsan.example.com A +noall +answer dig @114.114.114.114 zhangsan.example.com A +noall +answer第一条命令查本地权威服务器,第二条查公共递归服务器。如果前者新、后者旧,基本可以断定是递归缓存,等待 TTL 过期即可,不要去重启权威服务。
5.2 Ubuntu/Debian系统里/etc/resolv.conf总被重置:NetworkManager与resolvconf的冲突
现象:在 Ubuntu 22.04 服务器上手工修改/etc/resolv.conf把 nameserver 指到本地 CoreDNS,过一段时间或者重启网络后发现文件被还原成 127.0.0.53 或者运营商地址,自建 DNS 服务变得无人问津。
原因:这是 Ubuntu 等发行版系统再正常不过的行为。/etc/resolv.conf是软链接,指向 systemd-resolved 或 NetworkManager 生成的动态文件。系统每次网络事件触发都会重新生成这个文件,手工修改必然被覆盖。
解决:如果 CoreDNS 跑在本机且要承接回环查询,正确做法不是改 resolv.conf,而是把 systemd-resolved 的 DNS 配置指向本机,让 resolved 作为转发层。修改/etc/systemd/resolved.conf,把 DNS 设置为 127.0.0.1,然后执行systemctl restart systemd-resolved。如果不想用 systemd-resolved,也可以直接 mask 掉它并让 NetworkManager 管理 DNS,但那个配置更绕,我的习惯是承认 resolved 的存在,让它成为 CoreDNS 的上游。
5.3 泛解析和下划线:下划线与特殊字符导致的子域名分发失败
现象:控制面给用户分配了一个叫my_domain_1.example.com的二级域名,写入 etcd 后查询一切正常,但用户在自己系统里配置这个域名时提示非法,浏览器访问直接报错。
原因:DNS 协议本身允许下划线等字符出现在域名标签里,但公网证书签发机构、主流 Web 服务器和浏览器对域名有更严格的限制。RFC 1034 规定域名标签只能包含字母、数字和连字符,且连字符不能出现在开头或结尾;下划线只在某些特殊服务记录里合法,普通主机名用下划线是给自己找麻烦。
解决:控制面在分配前做一次校验,用正则过滤非[a-zA-Z0-9-]字符,并限定段长不超过 63 个字符,整个域名不超过 253 个字符。同时检查连续点、开头点、结尾点这些边界情况。校验不通过直接拒绝,而不是写进 DNS 之后再让用户发现不能用。
5.4 日志泄漏与错误记录导致的DNS查询风暴
现象:一个不存在的二级域名被客户端反复查询,访问日志里每分钟刷出几百条 NXDAMIN。进一步查发现是某个客户端应用写死了这个域名地址,在等待超时后无限重试。
原因:这是一次典型的 DNS 查询风暴。分发服务面对的查询者不只有浏览器,还有各种 SDK、爬虫和写死的客户端。它们对同一个不存在的域名高频重试,qps 瞬间升高,打满 CoreDNS 的查询处理线程。
解决:两个防线。第一,在 CoreDNS 前面加一层缓存,让 NXDOMAIN 响应也被缓存。第二,在 CoreDNS 中配置限流插件,对同一源 IP 设置每秒最大查询次数,超限直接丢弃。同时必须处理日志问题:CoreDNS 的 log 插件在 qps 高的时候本身就是磁盘杀手,生产环境建议把 log 关掉,只保留 errors,或者接采集器异步落盘。刚上线时我喜欢开着 log 看解析是否成功,后来发现高流量时每条日志都是纯损耗,就学乖了。
5.5 恶意域名与黑产团伙的绕过:黑名单过滤、日志溯源与告警联动
现象:有人批量注册了一堆与平台内某个品牌相似的三级域名,通过分发服务指向恶意页面,并且每天换一个新域名来躲避安全团队的封禁。
原因:DNS 分发服务天然会被黑产盯上。如果一个平台开放二级域名自助分配,攻击者可以自动注册大量前缀,把它们指向钓鱼页面或恶意下载站,然后用这类域名去做推广。
解决:这已经超出 DNS 本身的范围,需要和其他系统联动。我在控制面里维护一份黑名单前缀表,包含常见恶意词根、竞品品牌词根,以及高危后缀组合;分配前做一次过滤。同时在数据面把解析日志全量接入日志系统,用脚本定期扫描量异常的短期域名,发现可疑的自动下线。最后把高德图这种思路放到一边,追踪恶意记录只是第一步,真正需要的是控制面加上“最近一小时内新分配域名解析量超过阈值就自动冻结”的规则,这套告警联动机制比手动拉黑高效得多。
6. 验证与进阶玩法:用dig和dnspython完成分发正确性巡检
整套服务上线后,不能只靠人工 dig 验证,必须有一套自动化巡检脚本。dig 适合单次排查,批量巡检我推荐用 dnspython 写一个小脚本,覆盖四类断言:域名能正确解析、解析结果是预期目标、解析 TTL 在合理范围、已回收的域名返回 NXDOMAIN。
一个简单的巡检脚本骨架如下:
import dns.resolver resolver = dns.resolver.Resolver() resolver.nameservers = ["127.0.0.1"] def check_record(domain, expected_cname): try: ans = resolver.resolve(domain, "CNAME") if str(ans[0].target).rstrip(".") != expected_cname: print(f"FAIL: {domain} CNAME 期望 {expected_cname}, 实际 {ans[0].target}") else: print(f"OK: {domain}") except dns.resolver.NXDOMAIN: print(f"FAIL: {domain} 解析为 NXDOMAIN") check_record("zhangsan.example.com", "edge.example.com")这段脚本的逻辑是:构造一个指向本地权威服务器的 resolver,然后对每个已分配域名执行 CNAME 查询,把返回目标和期望值做比对。这里的参数说明:check_record的第二个参数是期望的 CNAME 目标;查询失败时捕获 NXDOMAIN 异常并标记 FAIL。脚本本身很薄,但接入到 CI/CD 里,每次控制面发布后自动跑一遍,能有效拦截“写错键路径”“覆盖了别家记录”这类低级错误。
最后说一个我的个人习惯:每次调整 TTL 或更新 CoreDNS 配置前,我都会先用dig +trace完整走一遍链路,看从根域到权威域的每一跳。这样做不是多此一举,而是能立刻暴露出是不是有递归服务器在中间劫持、NS 记录有没有被误改。只要你和我一样经历过缓存风暴和配置回滚的折磨,就会明白这套动作比任何监控面板都来得直接。希望帮到你。
本文还有配套的精品资源,点击获取