拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

二级域名分发系统实战:架构、DNS自动化与避坑指南

二级域名分发系统实战:架构、DNS自动化与避坑指南

简介:这份资源是全新二级域名分发系统网站源码的终极最强版,面向需要搭建二级域名分发平台的开发者与运维人员,可用于研究多租户域名解析、流量调度与商业级防护的实现思路。压缩包共约2000个文件,整体89.05MB,以1228个PHP文件为核心业务代码,辅以259个JSON配置、125张PNG界面素材、55个JS与35个CSS前端资源,另有SQL建表脚本、模板文件与说明文档,结构完整便于二次开发。系统基于PHP8.1与Swoole扩展构建,支持高并发请求,内置智能分发引擎、多租户主域名资源池、独立流量统计、CC防御与SQL注入防护,并提供可视化面板、协程缓存加速、开放API接口及企业版与个人版多套前端主题。目前已有65人学习下载,适合希望研究域名分发架构、接口对接与安全防护的读者参考,需注意仅供研究学习使用。

1. 二级域名分发系统到底在分发什么:从一次踩坑说起

去年帮一个做 SaaS 工具的朋友处理过一个挺尴尬的事故。他们主站app.example.com跑得好好的,某天运营同事为了做一场活动,手动在 DNS 面板加了一批promo-01.example.com到promo-50.example.com的解析记录,结果其中三条 CNAME 指错了目标,用户打开活动页直接跳到空白页,客服电话被打爆。事后复盘,问题根本不在 DNS 本身,而在于「批量、可控、可回收地把二级域名分出去」这件事,他们全程靠人肉操作。

这就是二级域名分发系统要解决的问题。它本质是一套「域名资源池 + 分配规则 + 解析自动化」的组合:用户在前台提交申请或触发某个动作,系统按预设规则从池子里挑一个二级域名,自动写入 DNS 解析,同时把这条记录登记进数据库,支持后续的查询、续期、回收和统计。常见落地场景包括多租户 SaaS 给每个客户分配独立子域、短链服务批量生成跳转域名、建站平台给用户开独立站点、活动页快速铺量。

这套系统适合谁?如果你手上有一个泛域名证书、一台能调 DNS API 的服务器、以及「域名分配」这个反复出现的需求,那它值得做。反过来,如果只是偶尔加两三条记录,用面板点两下就够了,上系统反而是过度设计。接下来我按「先讲清架构和选型,再给可复现的落地步骤,最后说坑」的顺序,把这套东西拆开讲。

2. 分发系统的核心架构与 DNS 选型:为什么不能只写数据库

很多人第一反应是「不就是往数据库插一条记录吗」。真跑起来会发现,数据库里有一条user_a -> sub001.example.com的记录,和用户浏览器能打开sub001.example.com,中间隔着 DNS 解析这一层。系统必须同时维护「业务侧的分配状态」和「DNS 侧的真实解析」,两边任何一边掉链子,用户看到的就是打不开。

2.1 三层结构:申请层、调度层、解析层

我一般把系统拆成三层,职责边界清晰,出问题好定位。

申请层负责接收请求和校验。用户提交想要的域名前缀、用途、有效期,这一层做格式校验(前缀只能是小写字母数字和连字符)、敏感词过滤、频率限制。它不碰 DNS,只产出「一条待分配的申请」。

调度层是核心,负责从域名池里挑一个可用前缀、检查是否冲突、生成分配记录、决定这条记录走哪条解析线路。它要处理并发——两个用户同时申请同一个前缀,必须有锁或者唯一索引兜底。

解析层负责真正调用 DNS 服务商的 API 写入记录,并把 API 返回的 record_id 存回数据库,方便后续修改和删除。这一层要处理 API 限流、超时重试、以及「数据库写成功但 DNS 写失败」这种半成功状态。

三层之间用状态字段串起来,比如申请记录有pending / allocated / resolved / failed几个状态,任何一步失败都能从状态看出卡在哪。

2.2 DNS 服务商怎么选:API 能力比价格重要

选 DNS 服务商时,别只看解析速度和价格,对分发系统来说,API 的完整度和限流策略才是命门。你需要的能力至少包括:创建记录、修改记录、删除记录、按名称查询记录、返回稳定的记录 ID。有些服务商的 API 只能整条覆盖不能单独改,或者删除要靠「值匹配」而不是 ID,批量操作时非常容易误删。

下面这张表是我实际对比过的几个维度,具体服务商名字按你自己的账号体系选,这里只列判断标准:

判断维度合格线踩坑表现
创建记录 API支持指定 name/type/value/ttl只能整域覆盖,改一条动全量
返回记录 ID创建后返回唯一 ID只能靠 name+value 反查,重名就乱
删除方式按 ID 删除按值删除,值相同会误删
限流明确 QPS 且可申请提升突发批量直接 429,无重试就丢记录
TTL 下限支持 60s 或更低最低 600s,回收后长时间残留

提示:TTL 别设太长。分发系统经常要回收和改指向,TTL 设 600s 以上,用户改了配置要等十分钟才生效,排查时你会怀疑人生。

2.3 泛域名与证书:一次配好省掉后续麻烦

二级域名分发几乎必然配泛域名解析和泛域名证书。泛解析*.example.com指向你的入口服务器,这样新增的sub001.example.com不用单独加 A 记录也能被解析到。证书用*.example.com的通配符证书,覆盖所有二级域名,省去每个子域单独签发的麻烦。

要注意泛解析和具体记录的关系:如果你给sub001单独加了 CNAME,它会优先于泛解析生效;删除这条 CNAME 后,又回落到泛解析。这个特性可以用来做「默认兜底页」——没被分配的二级域名全部落到一个提示页,而不是报 NXDOMAIN。

3. 从零跑通最小分发流程:数据库、API 封装与分配逻辑

这一章给能直接抄的代码。我用 Python 写,数据库用 MySQL,DNS 操作封装成一个类,方便你换成任意服务商。整套逻辑跑通后,你就能实现「提交前缀 → 自动分配 → 自动解析 → 可查询」的闭环。

3.1 建表:域名池和分配记录分开存

先把数据结构定下来。域名池存「可用的前缀」,分配记录存「谁在什么时候拿了哪个前缀、解析状态如何」。两张表分开,池子可以批量导入,记录可以追溯。

-- 域名池:预先生成好的可用前缀 CREATE TABLE domain_pool ( id BIGINT PRIMARY KEY AUTO_INCREMENT, prefix VARCHAR(63) NOT NULL UNIQUE COMMENT '二级域名前缀,如 sub001', status TINYINT NOT NULL DEFAULT 0 COMMENT '0可用 1已分配 2禁用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 分配记录:谁拿了哪个前缀 CREATE TABLE allocation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, prefix VARCHAR(63) NOT NULL, full_domain VARCHAR(255) NOT NULL COMMENT '完整域名 sub001.example.com', target VARCHAR(255) NOT NULL COMMENT '解析目标,IP或CNAME', record_type VARCHAR(10) NOT NULL DEFAULT 'A', dns_record_id VARCHAR(64) DEFAULT NULL COMMENT 'DNS服务商返回的记录ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待解析 1已生效 2失败 3已回收', expire_at DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_prefix (prefix), KEY idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

domain_pool.prefix和allocation.prefix都加了唯一约束,这是防并发重复分配的第一道闸。dns_record_id单独存一列,删除和修改时靠它精确定位,不靠值匹配。status字段把「待解析」和「已生效」分开,DNS 调用失败时记录停在 0 或 2,方便重试。

3.2 封装 DNS API:重试和幂等是重点

DNS 调用是最容易翻车的地方。网络抖动、限流、超时都会让一次创建「看起来失败但其实成功了」,重试时又创建一条重复记录。我的做法是:创建前先按名称查一次,存在就复用,不存在才创建。

import time import requests class DnsClient: def __init__(self, api_token, zone): self.api_token = api_token self.zone = zone # 如 example.com self.base = "https://api.your-dns-provider.com/v1" def _headers(self): return {"Authorization": f"Bearer {self.api_token}"} def find_record(self, name): """按名称查询记录,返回 record_id 或 None""" resp = requests.get( f"{self.base}/zones/{self.zone}/records", params={"name": name}, headers=self._headers(), timeout=10, ) resp.raise_for_status() items = resp.json().get("records", []) return items[0]["id"] if items else None def upsert_record(self, name, rtype, value, ttl=60): """存在则更新,不存在则创建,返回 record_id""" rid = self.find_record(name) payload = {"name": name, "type": rtype, "value": value, "ttl": ttl} for attempt in range(3): try: if rid: r = requests.put( f"{self.base}/zones/{self.zone}/records/{rid}", json=payload, headers=self._headers(), timeout=10, ) else: r = requests.post( f"{self.base}/zones/{self.zone}/records", json=payload, headers=self._headers(), timeout=10, ) r.raise_for_status() return r.json()["id"] except requests.HTTPError as e: # 429 限流或 5xx 服务端错误,退避重试 if r.status_code in (429, 500, 502, 503) and attempt < 2: time.sleep(2 ** attempt) continue raise raise RuntimeError("dns upsert failed after retries")

find_record先查后写,保证幂等——重试不会产生重复记录。upsert_record里对 429 和 5xx 做指数退避重试,最多三次。ttl=60是默认值,回收场景下短 TTL 能让变更快速生效。注意raise_for_status之后才判断状态码,顺序别写反,否则异常里拿不到r。

3.3 分配逻辑:用事务和行锁防并发

分配的核心是「从池子里取一个可用前缀并标记为已分配」,这一步必须原子。用SELECT ... FOR UPDATE锁住候选行,再更新状态,避免两个请求拿到同一个前缀。

import pymysql def allocate(user_id, target, rtype="A", expire_days=365): conn = pymysql.connect(host="127.0.0.1", user="app", password="***", database="domain", autocommit=False) try: with conn.cursor() as cur: # 锁住一条可用前缀 cur.execute( "SELECT prefix FROM domain_pool " "WHERE status=0 ORDER BY id LIMIT 1 FOR UPDATE" ) row = cur.fetchone() if not row: raise RuntimeError("域名池已空") prefix = row[0] # 标记池子已用 cur.execute( "UPDATE domain_pool SET status=1 WHERE prefix=%s", (prefix,) ) full_domain = f"{prefix}.example.com" # 写分配记录,状态待解析 cur.execute( "INSERT INTO allocation " "(user_id, prefix, full_domain, target, record_type, status, expire_at) " "VALUES (%s,%s,%s,%s,%s,0, DATE_ADD(NOW(), INTERVAL %s DAY))", (user_id, prefix, full_domain, target, rtype, expire_days), ) alloc_id = cur.lastrowid conn.commit() except Exception: conn.rollback() raise finally: conn.close() # 事务外调用 DNS,避免长事务占用锁 try: client = DnsClient(api_token="***", zone="example.com") rid = client.upsert_record(prefix, rtype, target) _update_dns_status(alloc_id, rid, status=1) except Exception as e: _update_dns_status(alloc_id, None, status=2) raise return full_domain

关键点在「事务外调 DNS」。如果把 DNS 请求放在事务里,一次超时就会长时间持有行锁,池子被锁死。先提交数据库拿到alloc_id,再调 DNS,成功更新状态为 1,失败更新为 2 并抛异常,后续可以用定时任务扫描 status=2 的记录重试。FOR UPDATE配合ORDER BY id LIMIT 1保证并发下每个请求拿到不同的前缀。

3.4 回收与查询:把状态机跑完整

分配只是开始,回收同样重要。用户到期或主动释放时,要把 DNS 记录删掉、池子前缀标记回可用、分配记录置为已回收。

def release(alloc_id): conn = pymysql.connect(host="127.0.0.1", user="app", password="***", database="domain", autocommit=False) with conn.cursor() as cur: cur.execute( "SELECT prefix, dns_record_id FROM allocation WHERE id=%s AND status=1", (alloc_id,), ) row = cur.fetchone() if not row: raise RuntimeError("记录不存在或状态不允许回收") prefix, rid = row # 先删 DNS,再改库;删失败就不动库,保证一致 client = DnsClient(api_token="***", zone="example.com") if rid: client.delete_record(rid) cur.execute("UPDATE allocation SET status=3 WHERE id=%s", (alloc_id,)) cur.execute("UPDATE domain_pool SET status=0 WHERE prefix=%s", (prefix,)) conn.commit() conn.close()

顺序是「先删 DNS 再改库」。如果反过来,库先标记回收、DNS 删除失败,这个前缀就变成「池子里可用但线上还占着」的幽灵记录,下次分配出去会冲突。删 DNS 失败时直接抛异常,库不动,状态保持 1,人工介入或定时重试。

4. 避坑与排查:分发系统最容易翻车的五个地方

这套系统逻辑不复杂,但线上跑起来,坑基本都集中在「数据库和 DNS 不一致」以及「并发和限流」上。下面五条是我和朋友实际踩过的,按现象、原因、解决写清楚。

4.1 现象:用户拿到域名但打不开,数据库显示已生效

原因通常是 DNS 记录写成功了,但泛解析没配,或者记录类型和目标不匹配。比如用户要的是 CNAME 指向另一个域名,系统却写了 A 记录指向服务器 IP,浏览器解析到 IP 后 Host 不匹配,直接 404 或证书报错。

解决:分配前校验record_type和target的匹配关系,A 记录目标必须是合法 IP,CNAME 目标必须是域名。同时确认泛解析*.example.com已生效,用dig sub001.example.com和dig test-not-exist.example.com对比,后者应该落到兜底页而不是 NXDOMAIN。

4.2 现象:批量分配时部分记录丢失,日志里有 429

原因就是 DNS 服务商限流。一次性分配几百个域名,API 直接返回 429,如果代码里没有重试,这些请求就静默失败了,但数据库状态可能已经写成「已生效」。

解决:所有 DNS 调用走统一的重试封装,429 和 5xx 指数退避。批量场景下加一个令牌桶或信号量,把并发压到服务商 QPS 以下。分配任务改成异步队列,前台只返回「排队中」,后台慢慢消费,用户体验反而更好。

4.3 现象:两个用户拿到同一个二级域名

原因是分配逻辑没有加锁,两个请求同时SELECT到同一条可用前缀,都以为自己是第一个。唯一索引能挡住数据库层面的重复插入,但用户会看到「分配失败」而不是「换一个」。

解决:SELECT ... FOR UPDATE锁行,或者用UPDATE domain_pool SET status=1 WHERE prefix=%s AND status=0判断影响行数,为 0 就重新取。唯一索引作为最后一道防线保留,捕获重复键异常后自动重试下一个前缀。

4.4 现象:回收后域名还能访问,等很久才失效

原因是 TTL 设太长,或者回收时只改了数据库没删 DNS 记录。前者是缓存问题,后者是一致性问题。

解决:TTL 统一设 60s 或更低。回收逻辑严格按「先删 DNS 再改库」执行,删 DNS 失败就抛异常不继续。加一个对账定时任务,每天扫描allocation里 status=1 但 DNS 查不到记录、或 status=3 但 DNS 还有记录的条目,自动修复并告警。

4.5 现象:泛域名证书不覆盖新子域,浏览器报证书错误

原因是证书是单域名证书,或者通配符证书只覆盖一级,*.example.com不覆盖a.b.example.com这种多级。

解决:确认证书是*.example.com形式的通配符证书,且分发的前缀都是单级。如果业务需要多级子域,得用*.b.example.com或更复杂的证书方案。上线前用openssl s_client -connect sub001.example.com:443 -servername sub001.example.com验证证书链。

5. 进阶:把分发系统做成可运营的基础设施

跑通最小闭环后,真正决定这套系统能不能长期用的,是运营能力。我一般会加三个东西:配额与审批、解析健康检查、以及数据看板。

配额与审批解决「谁能分多少」。给每个用户设一个quota字段,申请时先扣配额,回收时返还。企业用户走审批流,个人用户直接分配。这一步能挡住大量恶意刷域名前缀的请求。

解析健康检查是定时任务,对 status=1 的记录做dig或 HTTP 探测,连续失败三次就标记异常并通知。我踩过的坑是:DNS 记录明明在,但目标服务器挂了,用户以为是我们系统的问题。有了健康检查,能快速区分「解析问题」和「目标问题」。

数据看板统计池子使用率、每日分配量、回收量、失败率。池子使用率超过 80% 就该补前缀了,失败率突然升高通常是 DNS 服务商限流或 API 变更。

最后说一个我自己的习惯:所有 DNS 操作都记一条操作日志,包含请求参数、响应、耗时、重试次数。分发系统出问题时,日志是唯一的后悔药。我见过太多团队只记「成功/失败」,真出问题连当时调的是哪个 API、返回了什么都不知道,只能靠猜。

这套东西不难,难的是把「数据库状态」和「DNS 状态」当成一个整体来维护,任何一边单独看都是黑匣子。把状态机、重试、对账这三件事做扎实,它就能从一次性脚本变成能扛住运营的基础设施。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表