从零构建DNS监控系统:Python脚本、Flask面板与工程化实践
2026/8/24 3:44:03 网站建设 项目流程

最近在折腾一个内部用的 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 监控的核心价值,不在于替代专业的监控服务,而在于:

  1. 针对性:只监控对你业务至关重要的那几个域名。
  2. 深度可控:可以自定义检查频率、解析服务器(包括指定公共 DNS、运营商 DNS 甚至自建递归解析器)、检查的记录类型。
  3. 数据自有:所有历史解析数据都在自己手里,便于回溯分析和定制告警。
  4. 成本与学习:对于中小团队或个人项目,这是一个理解 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 timercron更可靠。如果希望任务管理与Web服务一体化,可以在 Python 内使用APScheduler
部署方式Docker 容器化将采集器、Web 服务、数据库(如果不用 SQLite)分别容器化,用 Docker Compose 编排。这保证了环境一致性,也便于未来扩展或迁移。

这里的一个关键取舍是“一体化”还是“分离式”。一体化(所有功能在一个应用内)部署简单,但耦合度高。分离式(采集器、API 服务、前端独立)更清晰,适合扩展,但初始复杂度高。我建议从一体化开始,但代码结构上做好模块分离,比如将数据采集、存储、API 接口写在不同的模块文件中,为将来拆分留出可能。

3. 从翻车到稳定:采集脚本的“坑”与“填坑”实录

这是整个项目最核心也最容易出问题的部分。翻车往往发生在这里。

3.1 第一版脚本:天真带来的脆弱

最初,借助一些代码生成工具,我很快得到了一个“能用”的脚本。它大概长这样(简化版):

import dns.resolver def query_dns(domain, dns_server='8.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'))

这个脚本在理想环境下工作良好。但一旦投入生产循环,问题接踵而至:

  1. 没有超时控制:如果目标 DNS 服务器无响应,脚本会一直挂起,阻塞整个任务队列。
  2. 没有异常处理:域名不存在(NXDOMAIN)、服务器拒绝(REFUSED)、网络抖动等都会导致程序崩溃。
  3. 结果处理简单:只取了 A 记录,如果域名是 CNAME 别名,或者有多个 A 记录,处理不完整。
  4. 缺乏重试机制:一次失败就彻底失败。
  5. 没有日志:出错了不知道错在哪里。

3.2 加固后的采集脚本

经过几次“翻车”,脚本被重构成下面这样。关键不在于代码多高级,而在于对网络请求脆弱性的充分防御。

import dns.resolver import dns.exception import time import logging from typing import List, Dict, Optional logging.basicConfig(level=logging.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_answer=False) 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(f"Query for {domain} ({record_type}) got no answer from {dns_server}") logger.info(f"Query succeeded: {domain} -> {result.get('ips', result.get('cname', 'N/A'))} in {query_time:.2f}ms") return result except dns.resolver.NXDOMAIN: error_msg = f"Domain {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 = f"DNS 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 = f"No 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 = f"DNS 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 = f"Unexpected 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_server=server) 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_answer=False来更精细地控制无答案情况。

但它的“坑”也同样明显:

  • 幻觉与过时信息:它可能推荐一个不存在的库方法,或者给出已弃用的参数用法。
  • 缺乏上下文理解:生成的代码往往是“标准情况”下的,缺乏对生产环境复杂性(如并发、资源限制、异常流程)的考虑。
  • 安全盲区:很少会主动提醒你注意输入验证、防止 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?domain=example.com&hours=24:获取某个域名最近 N 小时的历史记录,用于前端绘图。
  • GET /api/domains:获取所有被监控的域名列表。
  • GET /api/overview:获取概览信息,如最近检查成功率、平均延迟等。

后端的关键职责是数据验证、聚合和提供干净的 API。例如,在POST /api/result时,要校验必填字段,防止无效数据入库。

4.3 打造前端监控面板

前端不需要复杂,清晰直观是首要目标。一个典型的面板可能包含:

  1. 概览仪表盘:显示所有被监控域名的当前状态(成功/失败)、最近24小时平均延迟、失败率。
  2. 域名详情页
    • 延迟趋势图:使用 Chart.js 绘制查询耗时随时间变化的折线图。一眼就能看出延迟毛刺。
    • 解析记录历史:表格展示每次查询的解析结果(IP)、TTL 和状态。当 IP 发生变化时,用不同颜色高亮显示。
    • 多 DNS 源对比:如果从多个 DNS 服务器查询,可以并排显示结果,快速发现解析不一致。
  3. 告警历史:记录每次触发告警的事件(如连续失败、IP变更)。

前端与后端的交互:通过 Fetch API 或 Axios 调用后端提供的 RESTful API 获取 JSON 数据,然后动态渲染图表和表格。

4.4 告警策略:从“有通知”到“有效通知”

告警是监控的最终目的,但也是最容易造成“告警疲劳”的部分。

  • 避免“单点抖动”告警:不要一次查询失败就发告警。可以采用“5分钟内失败3次”或“连续2次失败”这样的策略,避免因网络瞬时波动产生噪音。
  • 区分告警级别
    • 警告:单个 DNS 源查询超时或失败,但其他源正常。可能只是该 DNS 服务器临时问题。
    • 严重:所有配置的 DNS 源对某个域名均查询失败。很可能域名本身配置有问题或遭遇严重故障。
    • 信息:域名的解析 IP 发生变更。这对于关注 IP 稳定性的场景很重要。
  • 告警渠道:根据团队习惯,集成邮件、Slack、钉钉、企业微信等。初期可以先用邮件和脚本日志,稳定后再接入更及时的渠道。
  • 告警内容要包含上下文:告警信息里至少应包含:域名、故障状态(如“所有源查询超时”)、最近一次成功的解析IP(如有)、故障开始时间、监控面板的直达链接。

5. 部署、优化与长期维护的 checklist

当所有组件开发调试完毕,准备部署上线时,下面这个 checklist 可以帮助你走完最后一公里,并规划长期维护。

5.1 部署清单

  1. 环境隔离:使用 Docker 和 Docker Compose 将数据库、后端、前端分别容器化。编写好docker-compose.ymlDockerfile
  2. 配置外置:将监控的域名列表、DNS 服务器列表、查询频率、告警阈值等配置项,通过环境变量或配置文件管理,不要硬编码在脚本中。
  3. 采集器调度:如果采集器是独立脚本,使用systemd timercron来定时触发。确保在cron中设置正确的PATH和环境变量,或者直接调用容器内的命令。
  4. 日志与监控:为采集脚本、Web后端配置日志轮转(如logrotate)。更重要的是,监控这个监控系统本身。可以简单地为采集器进程和 Web 服务端口设置一个存活检查(如另一个cron任务或简单的 uptime monitor)。
  5. 数据备份:定期备份 SQLite 或 PostgreSQL 数据库文件。历史解析数据对于排查历史问题很有价值。

5.2 性能与稳定性优化

  • 控制查询频率:根据实际需要设置合理的查询间隔(如每分钟一次)。过于频繁的查询可能对公共 DNS 服务器不友好,也可能触发限流。
  • 异步与并发:如果监控域名很多,同步查询会耗时很长。可以考虑使用asyncio+aiohttpconcurrent.futures实现并发查询,但要注意控制并发度,避免对目标 DNS 服务器造成压力。
  • 数据库清理:历史数据会不断增长。可以定期(如每月)将超过一定时间(如30天)的详细记录迁移到备份表或删除,只保留每日的聚合统计信息(如平均延迟、成功率)。
  • 前端防抖与缓存:前端频繁刷新数据时,对 API 调用做防抖处理。对于变化不频繁的数据(如域名列表),可以使用浏览器本地缓存。

5.3 长期维护思考

  • 可观测性:在面板中增加一个“系统状态”页面,显示采集任务最近一次运行时间、成功/失败次数、数据库大小等自身健康指标。
  • 可扩展性:如果未来需要监控成千上万个域名,当前的架构可能遇到瓶颈。那时需要考虑将采集器设计成分布式、任务队列(如 Redis + Celery)的模式,并将数据库升级为更强大的 PostgreSQL 或时序数据库。
  • 安全加固:确保 Web 管理界面有基本的访问控制(如简单的 HTTP Basic Auth 或内网 IP 限制),防止未授权访问。对采集器上报数据的 API 接口,可以考虑增加一个简单的 Token 认证。
  • 文档与交接:为项目编写清晰的README.md,说明部署步骤、配置方法、架构图和常见问题。这对于任何需要接手维护的同事都至关重要。

回过头看,从“翻车”到“上线”,构建这样一个 DNS 监控面板,技术难点并不高,真正的挑战在于将一个个离散的点(查询、存储、展示、告警)串联成一个稳定、可靠、可维护的自动化系统。AI 编程工具在这个过程中,像一把锋利的锉刀,能帮你快速打磨掉一些毛刺(语法错误、基础代码),但整个器物的结构和承重设计,依然依赖于你对问题本身的理解和工程经验的积累。

这个项目的价值,远不止于墙上多了一个监控图表。它更像是一个触点,让你更直观地感知到网络基础服务的脉搏,并在一次次迭代中,把那些原本需要手动、靠经验的操作,沉淀成一套可重复、可观测、可演进的自动化流程。这,或许是比工具本身更重要的收获。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询