最近在折腾一个内部用的 DNS 监控面板过程挺有意思。一开始想得很简单不就是定时解析几个域名把结果存起来再画个图嘛。结果从选型、写脚本、处理异常到最终让整个流程稳定跑起来中间踩的坑一个接一个。尤其是当你试图用一些“聪明”的工具比如 AI 辅助编程来加速时会发现它确实能帮你跳过一些重复劳动但也可能把你带到一些意想不到的沟里——比如生成一段看起来完美但一遇到网络波动或特殊 DNS 记录就崩掉的代码。这个项目最终的目标是构建一个轻量、可自维护的 DNS 监控系统。它不追求大而全的监控平台功能而是聚焦在一个具体问题上如何低成本、自动化地掌握关键域名的解析健康状况并在出现异常如解析失败、TTL异常、记录变更时能及时感知。整个过程更像是一次从“手动检查”到“脚本自动化”再到“考虑工程化”的典型演进。如果你也在考虑为内部服务、对外 API 或核心业务域名搭建类似的监控或者对如何将 AI 工具融入具体开发流程有疑问那么我这一路的“翻车”经验和最终“上线”的思考或许能给你一些参考。1. 为什么需要自建 DNS 监控从一次“感觉不对”的排查说起很多运维或开发同学可能觉得DNS 有云服务商兜底或者偶尔手动nslookup一下就够了。但在实际生产环境中DNS 问题往往非常隐蔽且影响面广。一个典型的场景某个依赖的外部 API 域名突然响应变慢你排查了自身代码、网络链路、服务器负载一切正常。最后才发现是 DNS 解析出的 IP 地址发生了变化而新 IP 的某个路由节点存在拥塞。如果有一个持续监控该域名解析记录包括 A 记录、CNAME、TTL的面板你就能在问题出现的第一时间甚至提前通过观察 TTL 变更或解析延迟波动发现端倪。另一个场景内部服务迁移需要切换 DNS 记录。操作完成后你怎么确认全球各地、不同网络环境的解析都已生效手动抽查几个公共 DNS如 8.8.8.8, 114.114.114.114是远远不够的。一个分布式的、从多个解析源发起查询的监控能给你更全面的生效视图。所以自建 DNS 监控的核心价值不在于替代专业的监控服务而在于针对性只监控对你业务至关重要的那几个域名。深度可控可以自定义检查频率、解析服务器包括指定公共 DNS、运营商 DNS 甚至自建递归解析器、检查的记录类型。数据自有所有历史解析数据都在自己手里便于回溯分析和定制告警。成本与学习对于中小团队或个人项目这是一个理解 DNS 协议、网络监控和自动化脚本的绝佳实践。市面上当然有成熟的 SaaS 服务但对于内部使用、特定需求或纯粹想“知其所以然”的动手派自己从零搭建一遍收获的远不止一个工具。2. 技术选型在“够用”和“好维护”之间找平衡明确了需求接下来就是技术选型。这往往是最容易“想当然”的一步。我的核心思路是优先选择社区活跃、接口简单、依赖清晰的技术栈避免为了“炫技”引入不必要的复杂性。2.1 核心任务分解一个最简 DNS 监控面板需要完成以下任务数据采集定期向目标 DNS 服务器发起查询获取域名的解析结果。数据存储将每次查询的结果IP、TTL、查询耗时等持久化。数据展示通过 Web 界面以图表或列表形式展示历史解析记录和状态。告警通知当解析失败、IP变更或延迟超阈值时触发通知。2.2 选型决策与考量基于上述任务我做了如下选择并附上背后的思考组件选型理由与考量采集脚本语言Python生态丰富有成熟的 DNS 库如dnspython编写网络请求和数据处理脚本快捷。易于与后续的 Web 框架集成。DNS 查询库dnspythonPython 下最权威的 DNS 库之一支持几乎所有记录类型能精细控制查询参数如指定 DNS 服务器、查询类型。数据存储SQLite (开发) / PostgreSQL (生产)初期用 SQLite 快速原型验证单文件、零配置。如果考虑长期运行、更高并发或分布式采集点可平滑迁移到 PostgreSQL。存储字段至少包括域名、解析IP、记录类型、TTL、查询耗时、来源DNS服务器、时间戳。Web 框架Flask 或 FastAPI轻量级适合快速构建 RESTful API 和简单的管理界面。FastAPI 的自动 API 文档生成对后期维护更友好。前端展示简单 HTML Chart.js目标是一个内部管理面板不需要复杂 SPA。Chart.js 足以绘制解析延迟趋势图和 IP 变更时间线。保持前端轻量降低维护成本。定时任务Systemd Timer (Linux) 或 APScheduler (Python内)如果采集脚本是独立的用系统的systemd timer或cron更可靠。如果希望任务管理与Web服务一体化可以在 Python 内使用APScheduler。部署方式Docker 容器化将采集器、Web 服务、数据库如果不用 SQLite分别容器化用 Docker Compose 编排。这保证了环境一致性也便于未来扩展或迁移。这里的一个关键取舍是“一体化”还是“分离式”。一体化所有功能在一个应用内部署简单但耦合度高。分离式采集器、API 服务、前端独立更清晰适合扩展但初始复杂度高。我建议从一体化开始但代码结构上做好模块分离比如将数据采集、存储、API 接口写在不同的模块文件中为将来拆分留出可能。3. 从翻车到稳定采集脚本的“坑”与“填坑”实录这是整个项目最核心也最容易出问题的部分。翻车往往发生在这里。3.1 第一版脚本天真带来的脆弱最初借助一些代码生成工具我很快得到了一个“能用”的脚本。它大概长这样简化版import dns.resolver def query_dns(domain, dns_server8.8.8.8): resolver dns.resolver.Resolver() resolver.nameservers [dns_server] answer resolver.resolve(domain, A) return [str(r) for r in answer] if __name__ __main__: print(query_dns(example.com))这个脚本在理想环境下工作良好。但一旦投入生产循环问题接踵而至没有超时控制如果目标 DNS 服务器无响应脚本会一直挂起阻塞整个任务队列。没有异常处理域名不存在NXDOMAIN、服务器拒绝REFUSED、网络抖动等都会导致程序崩溃。结果处理简单只取了 A 记录如果域名是 CNAME 别名或者有多个 A 记录处理不完整。缺乏重试机制一次失败就彻底失败。没有日志出错了不知道错在哪里。3.2 加固后的采集脚本经过几次“翻车”脚本被重构成下面这样。关键不在于代码多高级而在于对网络请求脆弱性的充分防御。import dns.resolver import dns.exception import time import logging from typing import List, Dict, Optional logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) def safe_dns_query(domain: str, record_type: str A, dns_server: str 8.8.8.8, timeout: int 5, retries: int 2) - Optional[Dict]: 安全的 DNS 查询函数 返回字典包含ips, ttl, cname, query_time, status, error_msg resolver dns.resolver.Resolver() resolver.nameservers [dns_server] resolver.lifetime timeout # 设置查询超时 for attempt in range(retries 1): try: start_time time.time() # 关键使用 resolve()并捕获可能的各种异常 answer resolver.resolve(domain, record_type, raise_on_no_answerFalse) query_time (time.time() - start_time) * 1000 # 毫秒 result { domain: domain, record_type: record_type, dns_server: dns_server, query_time_ms: round(query_time, 2), status: SUCCESS, error_msg: None, timestamp: time.time() } # 处理答案 if answer.rrset is not None: result[ttl] answer.rrset.ttl # 区分 A 记录和 CNAME 记录 if record_type A: result[ips] [str(r) for r in answer.rrset] elif record_type CNAME: result[cname] str(answer.rrset[0].target) # 可以扩展其他记录类型... else: # 有响应但无答案可能是 CNAME 指向的最终记录需要另外查询 result[status] NO_ANSWER logger.warning(fQuery for {domain} ({record_type}) got no answer from {dns_server}) logger.info(fQuery succeeded: {domain} - {result.get(ips, result.get(cname, N/A))} in {query_time:.2f}ms) return result except dns.resolver.NXDOMAIN: error_msg fDomain {domain} does not exist (NXDOMAIN) logger.warning(error_msg) return {domain: domain, status: NXDOMAIN, error_msg: error_msg, timestamp: time.time()} except dns.resolver.Timeout: error_msg fDNS query to {dns_server} for {domain} timed out after {timeout}s logger.warning(error_msg f (Attempt {attempt 1}/{retries 1})) if attempt retries: return {domain: domain, status: TIMEOUT, error_msg: error_msg, timestamp: time.time()} time.sleep(1) # 重试前稍作等待 except dns.resolver.NoNameservers: error_msg fNo nameservers responded for {domain} logger.error(error_msg) return {domain: domain, status: SERVFAIL, error_msg: error_msg, timestamp: time.time()} except dns.exception.DNSException as e: error_msg fDNS error for {domain}: {e} logger.error(error_msg) return {domain: domain, status: ERROR, error_msg: str(e), timestamp: time.time()} except Exception as e: # 捕获其他非DNS异常如网络问题 error_msg fUnexpected error querying {domain}: {e} logger.error(error_msg) return {domain: domain, status: FAILED, error_msg: str(e), timestamp: time.time()} # 所有重试都失败 return {domain: domain, status: FAILED_AFTER_RETRIES, error_msg: All retries exhausted, timestamp: time.time()} # 示例同时查询多个 DNS 服务器获取更全面的视图 def query_multiple_servers(domain: str, servers: List[str] [8.8.8.8, 114.114.114.114, 1.1.1.1]): results [] for server in servers: result safe_dns_query(domain, dns_serverserver) results.append(result) # 避免对同一服务器短时间高频查询 time.sleep(0.5) return results if __name__ __main__: # 测试 test_domain baidu.com # 使用一个稳定存在的域名测试 data query_multiple_servers(test_domain) for d in data: print(d)这个版本的改进点正是从“翻车”中学到的完备的异常捕获区分了NXDOMAIN域名不存在、Timeout超时、NoNameservers服务器失败等不同异常并给予不同的状态标识便于后续统计和告警。超时与重试通过resolver.lifetime设置单次查询超时并通过循环实现简单重试逻辑提高对临时网络波动的容错性。详尽的日志不同级别INFO, WARNING, ERROR的日志是后期排查问题的生命线。结果结构化返回字典包含了所有关键信息状态、错误信息、查询耗时、TTL而不仅仅是 IP 列表为存储和展示打下基础。多服务器查询query_multiple_servers函数展示了如何从多个源头查询这能帮你发现 DNS 缓存、污染或局部生效不一致的问题。3.3 关于“AI搭档”编程的体会在编写和调试上述脚本的过程中我大量使用了 AI 编程助手。它的价值主要体现在快速生成样板代码比如dnspython的基本查询语法、Flask 的路由模板。解释错误信息当遇到一个不熟悉的 DNS 异常时直接粘贴错误它能给出可能的原因和排查方向。提供优化建议比如建议使用raise_on_no_answerFalse来更精细地控制无答案情况。但它的“坑”也同样明显幻觉与过时信息它可能推荐一个不存在的库方法或者给出已弃用的参数用法。缺乏上下文理解生成的代码往往是“标准情况”下的缺乏对生产环境复杂性如并发、资源限制、异常流程的考虑。安全盲区很少会主动提醒你注意输入验证、防止 DNS 重绑定攻击等安全问题。所以我的经验是把 AI 当作一个反应迅速的“初级研究员”或“代码补全工具”但最终的架构设计、边界条件处理和稳定性保障必须由你自己把关。它帮你节省的是查找文档和编写基础逻辑的时间而不是替代你的系统思维和调试能力。4. 构建监控面板数据流与展示层的工程化思考采集到数据只是第一步。如何让数据流动起来并提供一个清晰的视图是“上线”的关键。4.1 设计数据流与存储我采用了一个简单清晰的单向数据流采集脚本 (定时运行) - 写入数据库 - Web后端 (提供API) - 前端页面 (图表展示)数据库表设计示例 (SQLite)CREATE TABLE dns_check_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT NOT NULL, record_type TEXT NOT NULL, dns_server TEXT NOT NULL, status TEXT NOT NULL, -- SUCCESS, NXDOMAIN, TIMEOUT, etc. resolved_ips TEXT, -- 可能多个IP用逗号分隔存储 resolved_cname TEXT, ttl INTEGER, query_time_ms REAL, error_msg TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_domain ON dns_check_results(domain); CREATE INDEX idx_created_at ON dns_check_results(created_at);resolved_ips字段存储字符串对于简单的查询和展示够用。如果需要进行复杂的 IP 地理信息分析可能需要更规范化的存储。索引对按域名和时间查询历史记录至关重要。采集器与写入逻辑加固后的采集脚本在每次成功运行后将结果字典通过 Web 后端提供的 API 接口如POST /api/result提交或者直接使用 SQLAlchemy 等 ORM 写入数据库。我更推荐通过 API 提交这样采集器可以完全独立部署甚至分布在多个不同网络位置的服务器上。4.2 实现 Web 后端与 API使用 Flask/FastAPI 搭建一个轻量后端POST /api/result接收采集器上报的数据。GET /api/results?domainexample.comhours24获取某个域名最近 N 小时的历史记录用于前端绘图。GET /api/domains获取所有被监控的域名列表。GET /api/overview获取概览信息如最近检查成功率、平均延迟等。后端的关键职责是数据验证、聚合和提供干净的 API。例如在POST /api/result时要校验必填字段防止无效数据入库。4.3 打造前端监控面板前端不需要复杂清晰直观是首要目标。一个典型的面板可能包含概览仪表盘显示所有被监控域名的当前状态成功/失败、最近24小时平均延迟、失败率。域名详情页延迟趋势图使用 Chart.js 绘制查询耗时随时间变化的折线图。一眼就能看出延迟毛刺。解析记录历史表格展示每次查询的解析结果IP、TTL 和状态。当 IP 发生变化时用不同颜色高亮显示。多 DNS 源对比如果从多个 DNS 服务器查询可以并排显示结果快速发现解析不一致。告警历史记录每次触发告警的事件如连续失败、IP变更。前端与后端的交互通过 Fetch API 或 Axios 调用后端提供的 RESTful API 获取 JSON 数据然后动态渲染图表和表格。4.4 告警策略从“有通知”到“有效通知”告警是监控的最终目的但也是最容易造成“告警疲劳”的部分。避免“单点抖动”告警不要一次查询失败就发告警。可以采用“5分钟内失败3次”或“连续2次失败”这样的策略避免因网络瞬时波动产生噪音。区分告警级别警告单个 DNS 源查询超时或失败但其他源正常。可能只是该 DNS 服务器临时问题。严重所有配置的 DNS 源对某个域名均查询失败。很可能域名本身配置有问题或遭遇严重故障。信息域名的解析 IP 发生变更。这对于关注 IP 稳定性的场景很重要。告警渠道根据团队习惯集成邮件、Slack、钉钉、企业微信等。初期可以先用邮件和脚本日志稳定后再接入更及时的渠道。告警内容要包含上下文告警信息里至少应包含域名、故障状态如“所有源查询超时”、最近一次成功的解析IP如有、故障开始时间、监控面板的直达链接。5. 部署、优化与长期维护的 checklist当所有组件开发调试完毕准备部署上线时下面这个 checklist 可以帮助你走完最后一公里并规划长期维护。5.1 部署清单环境隔离使用 Docker 和 Docker Compose 将数据库、后端、前端分别容器化。编写好docker-compose.yml和Dockerfile。配置外置将监控的域名列表、DNS 服务器列表、查询频率、告警阈值等配置项通过环境变量或配置文件管理不要硬编码在脚本中。采集器调度如果采集器是独立脚本使用systemd timer或cron来定时触发。确保在cron中设置正确的PATH和环境变量或者直接调用容器内的命令。日志与监控为采集脚本、Web后端配置日志轮转如logrotate。更重要的是监控这个监控系统本身。可以简单地为采集器进程和 Web 服务端口设置一个存活检查如另一个cron任务或简单的 uptime monitor。数据备份定期备份 SQLite 或 PostgreSQL 数据库文件。历史解析数据对于排查历史问题很有价值。5.2 性能与稳定性优化控制查询频率根据实际需要设置合理的查询间隔如每分钟一次。过于频繁的查询可能对公共 DNS 服务器不友好也可能触发限流。异步与并发如果监控域名很多同步查询会耗时很长。可以考虑使用asyncioaiohttp或concurrent.futures实现并发查询但要注意控制并发度避免对目标 DNS 服务器造成压力。数据库清理历史数据会不断增长。可以定期如每月将超过一定时间如30天的详细记录迁移到备份表或删除只保留每日的聚合统计信息如平均延迟、成功率。前端防抖与缓存前端频繁刷新数据时对 API 调用做防抖处理。对于变化不频繁的数据如域名列表可以使用浏览器本地缓存。5.3 长期维护思考可观测性在面板中增加一个“系统状态”页面显示采集任务最近一次运行时间、成功/失败次数、数据库大小等自身健康指标。可扩展性如果未来需要监控成千上万个域名当前的架构可能遇到瓶颈。那时需要考虑将采集器设计成分布式、任务队列如 Redis Celery的模式并将数据库升级为更强大的 PostgreSQL 或时序数据库。安全加固确保 Web 管理界面有基本的访问控制如简单的 HTTP Basic Auth 或内网 IP 限制防止未授权访问。对采集器上报数据的 API 接口可以考虑增加一个简单的 Token 认证。文档与交接为项目编写清晰的README.md说明部署步骤、配置方法、架构图和常见问题。这对于任何需要接手维护的同事都至关重要。回过头看从“翻车”到“上线”构建这样一个 DNS 监控面板技术难点并不高真正的挑战在于将一个个离散的点查询、存储、展示、告警串联成一个稳定、可靠、可维护的自动化系统。AI 编程工具在这个过程中像一把锋利的锉刀能帮你快速打磨掉一些毛刺语法错误、基础代码但整个器物的结构和承重设计依然依赖于你对问题本身的理解和工程经验的积累。这个项目的价值远不止于墙上多了一个监控图表。它更像是一个触点让你更直观地感知到网络基础服务的脉搏并在一次次迭代中把那些原本需要手动、靠经验的操作沉淀成一套可重复、可观测、可演进的自动化流程。这或许是比工具本身更重要的收获。