mosdns自建智能DNS:缓存、内网域名与广告拦截配置实践 DNS 这东西平时没人想起它一旦出问题全家所有设备一起遭殃网页转圈、视频卡顿、智能音箱喊半天不应。我家网络最早就是运营商默认下发的 DNS用了两年某天开始某些域名解析结果怪得很同一台设备重启光猫后解析到的地址就变了排查半天才定位到是本地链路的解析环节被改写。后来索性自建了一台 DNS 服务用 mosdns 做统一入口把缓存、内网域名、广告拦截、上游选择全部收拢到一份配置文件里。这套东西跑了一年多最直观的收益是三件事常用域名的解析几乎全部命中本地缓存首包时间从 30~60ms 降到 1ms 以内内网的 NAS、打印机、开发机统一用*.lan域名访问再也不用记 IP家里小孩的设备上那堆乱七八糟的追踪域名直接在解析层被掐掉页面加载反而更快。mosdns 是一个用 Go 写的开源 DNS 转发与处理工具核心卖点是插件化的处理链你可以把缓存匹配域名集合转发到指定上游改写响应这些动作像积木一样拼起来拼出完全符合自己网络情况的解析逻辑。它体积小、单文件就能跑、内存占用低扔在路由器、迷你主机、甚至一台常年开机的旧笔记本上都不心疼。这篇内容面向三类人一是家里网络设备多、想让解析行为可控可查的折腾型用户二是小型办公网络里需要维护内网域名和统一出口的管理员三是想学习 DNS 处理链设计、拿它当练手项目的开发者。下面我按为什么这么设计—配置怎么写—怎么验证—坑在哪的顺序把这一年的实践完整摊开。1. 先想清楚为什么要在自己网络里放一台智能 DNS1.1 三个真实痛点决定了这个方案的必要性很多人第一次听说自建 DNS反应是运营商给的 DNS 不是能用吗。能用但只是能用。我把自己踩过的三类问题列出来你对照一下有没有中招。第一类是解析结果不理想。公共 DNS 服务为了覆盖全国用户通常按递归出口的位置返回结果。你家宽带的实际链路和这个递归出口不一定一致尤其是自建了多线接入、或者所在区域网络结构比较特殊的场景拿到的解析地址可能绕了一大圈。表现出来就是明明带宽很足某些站点打开就是慢一拍。第二类是内网域名缺失。家里的 NAS、监控、开发机、打印机默认只能靠 IP 访问。IP 会变DHCP 一续租就乱套家里其他人根本记不住。路由器的 DHCP 有时能配本地域名但功能往往很弱不支持通配、不支持多级后缀、更不支持按域名决定走哪个上游。第三类是无法观测。出问题时你完全不知道是解析慢、连接慢还是服务端慢。浏览器开发者工具只能看到一个总耗时看不到 DNS 查询走了谁、花了多久、有没有命中缓存。一台自己的 DNS 服务天然带日志和统计出问题第一时间能定位到环节。mosdns 恰好把这三件事都覆盖了它自带缓存插件、支持域名集合匹配、支持多上游并发、能输出结构化日志。这就是我选它而不是直接用路由器自带 DNS 转发的根本原因。1.2 几种常见做法的横向对比以及我为什么落在这一个在决定用 mosdns 之前我把能想到的方案都试了一遍。这里做个对照方便你按自己的实际情况选。方案优点明显短板适合谁直接用上游公共 DNS零配置改个 IP 就行无缓存、无分流、无日志、结果不可控只求能上网的用户路由器自带 DNS 转发不用额外设备功能极简多数不支持域名集合匹配日志几乎没有单线路、需求简单的家庭通用 DNS 服务软件传统权威/递归一体功能全、生态成熟配置重、资源占用高、插件扩展不灵活有一定规模的正规网络环境mosdns单文件、插件链灵活、资源占用低、日志清晰配置是声明式的需要理解执行顺序文档需要对照版本读家庭折腾、小型办公、学习者最后一项的短板我特意写清楚mosdns 的配置是 YAML 声明式的它的执行模型是一条链从上往下走命中即执行对应动作。如果你按面向过程的思路去理解很容易写出看起来对但不生效的配置。这也是我后面要花大篇幅讲sequence的原因。另外补一句选型经验别一上来就追求完美分流。我最早的配置写了 200 多行规则集一堆结果维护成本高到自己都不想改。现在这份配置只有 80 行左右覆盖了 95% 的日常场景改起来一目了然。配置文件是给自己看的可维护性比功能完备更重要。1.3 部署位置的选择路由器、软路由还是独立小主机这一条我建议你在动手前就想清楚因为装完再搬家挺烦的。把 mosdns 跑在路由器上好处是不增加设备、网络路径最短、所有下游设备自动受益。前提是路由器能跑第三方固件或者支持容器且剩余存储和内存够用。我试过在一台内存 256MB 的老路由器上跑日常没问题但开日志加上大体积域名集合之后内存会紧张偶尔触发 OOM。跑在独立小主机或者一台常年开机的设备上好处是资源充裕、升级方便、日志能长期留存。代价是多一个设备要维护而且必须保证它的 IP 固定否则整个网络的 DNS 就跟着漂。我现在用的是第二种一台低功耗小主机固定内网 IP用容器方式运行。选容器不选裸二进制的原因很实在——升级时换镜像标签就行回滚也是一条命令配置文件用卷挂载容器重建不影响数据。如果你手上只有一台设备裸二进制也完全可以它没有外部依赖解压即用。注意不管部署在哪这台设备的 IP 必须固定。DHCP 保留、静态配置都行但千万别让它飘。DNS 服务地址一变整个网络所有设备都会解析失败那种全屋断网的体感比宽带故障还吓人。2. 环境准备从零到能跑通的最小闭环2.1 系统、端口与权限这几个前置条件mosdns 对系统要求极低主流的 Linux 发行版、容器环境都能跑。需要确认的只有三件事。一是53 端口必须能拿到。DNS 默认走 UDP 53 和 TCP 53。如果在 Linux 上直接跑需要绑定特权端口容器方式则要给相应权限。这里有个特别常见的坑很多发行版预装了本地解析服务它已经占着 53 端口你启动 mosdns 会直接报地址被占用。解决办法是先停掉并禁用它把 53 端口完整让出来。二是监听地址要写对。如果只监听127.0.0.1:53那只有本机能用局域网里其他设备访问不到。要让整个网络使用必须监听0.0.0.0:53或者指定内网网卡地址。我第一次配置就犯了这个错本机测试一切正常换到手机上就解析失败查了半小时才反应过来。三是上游连通性。mosdns 本身不做递归解析它靠上游。所以要保证这台设备能正常访问你选定的上游地址UDP 53 和 TCP 53 都要通——有些上游在响应包较大时会要求走 TCP 重查只开 UDP 会出现大记录查不到的诡异现象。准备工作清单我整理成这样检查项命令 / 做法通过标准53 端口是否被占用ss -lunp | grep :53无输出或输出中只有你自己的服务本地解析服务是否已停systemctl status相关服务处于 inactive / disabled上游 UDP 可达用dig 上游地址 域名有正常 ANSWER上游 TCP 可达同上加tcp参数有正常 ANSWER设备 IP 是否固定查看当前地址与租约重启后地址不变2.2 二进制与容器两条路我为什么推荐容器二进制方式最直接下载对应架构的压缩包解压出可执行文件和示例配置改完配置直接跑。优点是零依赖、启动快、排查问题时中间层少。缺点是升级要手动替换文件配置文件路径、日志路径、规则文件路径都得自己维护好。容器方式多了一层但换来的是可复现。我的做法是把配置文件、规则文件、日志目录分别用卷挂载进容器容器本身只当运行环境。这样升级时改一下镜像标签配置文件完全不动配置写坏了想回滚把旧配置目录换回来重启即可。启动命令大致是这样docker run -d \ --name mosdns \ --restart unless-stopped \ --network host \ -v /opt/mosdns/config.yaml:/etc/mosdns/config.yaml:ro \ -v /opt/mosdns/rules:/etc/mosdns/rules:ro \ -v /opt/mosdns/logs:/var/log/mosdns \ mosdns/mosdns:latest \ start -c /etc/mosdns/config.yaml这里用--network host是为了让容器直接占用宿主机的 53 端口省去端口映射在 UDP 上的麻烦。如果你更希望隔离网络改成端口映射-p 53:53/udp -p 53:53/tcp也能用但要确认宿主机没有其他服务抢这个端口。提示镜像标签建议锁定到具体版本号而不是latest。DNS 是整个网络的基础设施某天自动拉到一个行为有变化的新版本全家断网再去查日志代价太大。2.3 把 DNS 从别处切过来切换顺序很重要配置写好了别急着让全网用给自己留条退路。我的切换顺序是这样的先在 mosdns 里监听一个非 53 的高位端口比如 5353本机用dig -p 5353验证配置正确配置正确后改成 53 端口重启让它在局域网可用先手动改一台设备我用自己的手机的 DNS 指向它观察一两天确认稳定后再改路由器的 DHCP 下发设置让新设备自动拿到这个地址老设备重启或者续租之后才会生效别指望立刻全网切换。第 3 步那一两天很关键。DNS 的问题往往是特定域名、特定时段才暴露比如某个 CDN 域名在你所在区域解析到了一个很远的节点白天正常晚上慢。观察期能把这些抓出来。3. 吃透核心概念插件、插件链和执行顺序3.1 插件化模型一切皆插件配置就是声明实例mosdns 的配置模型很干净全是插件实例。你在配置里声明一堆插件每个插件有一个tag唯一标识和一个type插件类型再加args参数。然后是各种入口插件——UDP 服务、TCP 服务、HTTP 服务——它们把收到的请求交给某条链处理。理解这一点之后配置文件就不再是神秘的一坨而是一张依赖关系图入口插件指向处理链处理链里的步骤引用其他插件。我脑子里画的示意大致是udp_server(入口) - sequence(主链) - cache - 匹配器 - forward(上游)这里有个细节值得强调插件之间靠tag引用写配置时用$tag的形式。写错一个字母启动时就会报找不到插件好在它报错很直接不会静默失败。这也是我喜欢它的原因——配置错误在启动阶段就暴露而不是运行中悄悄走了错误的分支。另外插件声明顺序不等于执行顺序。很多人包括我一开始误以为写在前面就先执行。实际执行顺序完全由sequence里的步骤顺序决定声明顺序只影响初始化。记住这句话能省下大量调试时间。3.2 sequence 的执行逻辑以及命中即终止这个隐含规则sequence是整份配置的心脏。它接收一串步骤args是一个列表从上往下执行。每个步骤有两种形态- exec: $某插件无条件执行这个插件的动作- matches: 条件加exec: $某插件条件成立才执行。关键在于中止语义。链的执行不是全部跑一遍然后汇总而是遇到能产生最终响应的动作就结束。具体来说如果某个步骤里的转发插件拿到了响应或者缓存命中了链就到此为止后面的步骤不再执行。这个设计非常合理——它让缓存优先、命中即返回变成自然结果而不是需要额外判断的逻辑。我最初的配置犯过一个典型错误把缓存插件放在链的末尾想着最后兜底。结果缓存形同虚设因为前面的转发步骤已经拿到响应并终止了链缓存插件根本没机会执行。这个坑值得单独拎出来说注意缓存插件必须放在链的最前面。它的设计是先查命中就短路返回没命中就放行等后续步骤产生响应后由它自己写入。放错位置的后果是缓存永远不命中但日志里看不出任何异常。3.3 匹配器怎么选qname、qtype、响应侧判断匹配器决定这个请求该不该走这条分支是分流逻辑的基础。常用的几类我按使用频率列一下。qname匹配域名集合最常用配合domain_set插件加载的域名列表使用。qtype匹配查询类型比如你想对 AAAA 查询做特殊处理。has_resp判断当前是否已拿到响应用于在响应侧做加工。响应侧的resp_ip匹配响应里的 IP 集合这个在按解析结果决定是否接受的场景里非常有用比如上游返回的地址质量不佳时改走另一个上游重查。domain_set的亮点是它的匹配规则表达能力。它支持完整匹配、后缀匹配、关键字匹配、正则表达式还支持从文件加载。我在内网域名集合里用了后缀匹配一条lan的后缀规则就覆盖了所有*.lan不用逐个列。对比一下如果用的是只能精确匹配的方案光内网域名就得维护几十行。这里说一个实操经验域名集合文件用独立文件维护不要内联在配置里。原因很简单域名列表会频繁变动尤其广告拦截列表而配置结构基本不动。分开之后更新列表只需替换文件、重载配置主配置一个字符都不用碰出错概率低得多。4. 手写一份能跑的配置逐段拆解4.1 最小可用骨架先跑通再加功能我一直建议先跑通最小的再叠功能。下面这份配置是能直接用的最小骨架包含缓存、内网域名、广告拦截、公网上游四块逻辑清晰到能当模板。log: level: info file: /var/log/mosdns/mosdns.log api: http: 127.0.0.1:9091 plugins: # 1) 缓存必须放链首 - tag: cache_main type: cache args: size: 8192 lazy_cache_ttl: 86400 cache_everything: false # 2) 域名集合内网 - tag: set_intranet type: domain_set args: files: - /etc/mosdns/rules/intranet.txt # 3) 域名集合广告与追踪 - tag: set_adblock type: domain_set args: files: - /etc/mosdns/rules/adblock.txt # 4) 广告域名统一拒绝 - tag: block_ads type: black_hole args: - 127.0.0.1 # 5) 内网上游指向局域网内既有解析服务 - tag: fwd_intranet type: forward args: concurrent: 1 upstreams: - addr: 192.168.10.1 # 6) 公网上游两个地址并发 - tag: fwd_public type: forward args: concurrent: 2 upstreams: - addr: 223.5.5.5 - addr: 119.29.29.29 # 7) 主链 - tag: main type: sequence args: - exec: $cache_main - matches: qname $set_adblock exec: $block_ads - matches: qname $set_intranet exec: $fwd_intranet - exec: $fwd_public # 8) 入口 - tag: udp_in type: udp_server args: entry: main listen: 0.0.0.0:53 - tag: tcp_in type: tcp_server args: entry: main listen: 0.0.0.0:53上游地址用的是两个公开解析服务的地址这只是示例请替换成你所在网络实测最优的。测法很简单拿一堆你常访问的域名分别对不同上游dig对比解析耗时和返回地址选最快的两个。别照抄别人的推荐网络环境差异比想象中大。主链的执行顺序读起来就是一句话先查缓存命中广告集合就拒绝命中内网集合就走内网上游其余走公网上游。四行配置逻辑自洽这也是我最终收敛到的形态。4.2 缓存参数怎么定size 和 lazy_cache_ttl 的算法这块是新手最容易拍脑袋填数字的地方。我说说自己的计算方法。size是缓存条目数。一个家庭网络日常活跃域名有多少我的实测数据一台路由器 8 台设备连续跑 7 天去重后的查询域名约 2500 个。所以 8192 条已经相当宽裕能覆盖几天的活跃集合还能容纳长尾。如果你管理的是小型办公网络几十台设备那 16384 到 32768 比较稳妥。别一上来就填 100 万——缓存条目是要占内存的每条记录包含域名、类型、响应记录、时间戳粗算每条 200~500 字节10 万条就是几十 MB对一台 256MB 内存的设备是实打实的压力。lazy_cache_ttl是惰性缓存的存活时间。它的作用是某个域名缓存过期之后如果还有请求进来先返回这份过期记录同时后台再去上游更新。这叫stale-while-revalidate用一点点数据陈旧换取极低的响应延迟。我设的是 86400 秒一天。这个值设大设小有什么影响设太小热门域名过期后要等上游延迟抖动明显设太大某些变更频繁的域名比如负载均衡后面经常换 IP 的服务可能长时间返回旧地址。一天的折中我觉得合适毕竟绝大多数域名的解析地址在一天内的变化对你没有实际影响。如果你的网络里有那种必须实时准确的内部服务域名把它放进内网集合走单独上游别让它进这个惰性缓存。cache_everything这个开关要谨慎。它会让所有响应都进缓存包括本该不缓存的类型。我没开因为默认策略已经足够开了反而可能缓存住一些不该缓存的记录导致排查问题时被误导。4.3 内网域名集合怎么写才能覆盖得干净内网集合文件是我维护频率最低但价值最高的一个文件。内容大致两类本地后缀以及少量必须本地解析的特殊域名。# 本地后缀覆盖 *.lan 及所有子域 lan local home.arpa # 特定设备别名 nas.lan printer.lan dev-box.landomain_set的匹配规则里一行裸域名默认按后缀匹配理解所以写lan就能命中nas.lan、a.b.lan这类。这个行为比很多人预期的宽松正是我们要的。不过有个坑得提醒别把过于宽泛的后缀写进内网集合。我曾经顺手加了一行local本意是覆盖xxx.local的局域网设备发现结果某些公网域名也带这个后缀片段被误判走内网上游解析直接失败。排查时看着日志里走了错误分支的记录才恍然大悟。原则是只写你确实拥有的后缀公共域名后缀一个都别碰。另一个实用技巧是用hosts插件给固定设备做直答比走内网上游快一个数量级- tag: my_hosts type: hosts args: entries: - nas.lan 192.168.10.20 - printer.lan 192.168.10.31hosts的响应是本地的、零延迟的对于那几台 IP 固定的核心设备直接写死最省事。设备 IP 变了就改一行配置重载比重启整个 DHCP 服务友好得多。5. 智能分流的落地从能解析到解析得准5.1 分流不是玄学先明确你要分什么一说智能 DNS很多人脑子里浮现的是复杂的地理位置判断。实际落地时分流的目标通常只有三类想清楚就好办。第一类是范围分流内网域名走内网上游公网域名走公网上游。这个前面已经实现了是最基础也最有价值的一层。第二类是质量分流同一个公网域名不同上游返回的地址质量差异很大。做法是把上游分成两组主组两个地址并发谁先回用谁如果主组超时或者返回结果明显不合理比如返回了私有网段地址那多半是被中间设备改写了自动切备用组。第三类是拦截分流广告、统计、追踪类域名直接返回一个无效地址让请求快速失败而不是等它超时。这类域名有个特点——请求量巨大且高度重复拦掉之后收益立竿见影。我实测过家里几台设备开着网页拦截生效后页面整体加载时间的改善是能感觉到的因为那些追踪脚本不再拖着主线程。这里必须强调一个实操原则用响应侧判断补足请求侧判断。只看域名决定走向有时候不够。比如某个域名被你的规则判给了主组但主组返回的地址在本网络下不可达这时候响应侧的resp_ip判断就能派上用场——发现响应地址落在质量不佳集合里就改走备用组重查一次。这套机制比单纯堆域名列表有效得多。5.2 上游并发与超时两个参数决定体感concurrent是并发查询几个上游。设为 2 表示同时向两个上游发请求谁先回就用谁。这个机制对体感的改善非常明显因为它把最慢的上游变成了最快的那两个里更快的那个。但并发不是越多越好。设成 5你会发现上游的请求量翻了 2.5 倍某些公共解析服务对高频请求有限制可能直接给你限速或者返回错误。我的建议是 2最多 3。两个上游的故障域已经足够分散——同时挂两个的概率低到可以忽略。超时方面默认值一般能用但如果你的网络有链路抖动可以适当放宽。判断依据看日志里的耗时分布如果大量请求耗时集中在 800ms 以上说明上游响应慢先换上游如果大量请求直接是超时记录那可能是网络路径问题或者上游限制了你的请求频率。先看日志再调参数别凭感觉改。还有一个容易被忽略的点上游要选不同类型的地址。比如一个走明文 UDP一个走加密查询DoH/DoT。加密查询的好处是查询内容在网络路径上不明文暴露避免被中间设备看到甚至改写。配置写法是用 URL 形式- tag: fwd_secure type: forward args: concurrent: 2 upstreams: - addr: https://dns.example-provider.com/dns-query - addr: 223.5.5.5示例域名请替换为你信任的服务商地址。这样即使加密那条超时明文那条也能兜住可用性和隐私性兼顾。5.3 ECS 客户端子网什么时候该用什么时候是灾难ECSEDNS Client Subnet的作用是告诉上游我的客户端大概在哪个网段让 CDN 类服务返回地理上更近的节点。听起来很美好实际用起来要看情况。该用的场景你所在网络的上游递归出口离你很远导致解析到的 CDN 节点绕路。开启 ECS 后上游会按你声明的网段返回就近地址实测下载速度和首包延迟都有改善。不该用的场景你的网络对隐私敏感不想把内网网段信息透露给上游或者你已经在用多个上游并发不同上游对 ECS 的处理策略不一致反而带来了结果抖动。我现在的做法是只在特定域名集合上开启。给视频类、下载类这些对节点位置敏感的域名走一个带 ECS 的上游组其他域名走不带 ECS 的普通组。这样既拿到了就近解析的收益又不会把全部查询都带上位置信息。配置上就是再建一个 forward 组在链里用qname匹配器把对应域名集合指过去逻辑和前面完全一样只是多了一条分支。这个分组的维护成本很低域名集合文件里加几行的事。但它带来的收益是实打实的同样是看视频开启前后缓冲频率的差别家里人是能直接感觉出来的。6. 观测与验证别猜去看日志6.1 日志分级和查询日志怎么开才不淹死mosdns 的日志分级很有必要认真设置。日常跑info就够排查具体某个域名时临时调到debug问题定位完立刻调回来。为什么强调这个因为debug级别的日志会把每一个查询的完整决策过程都打出来一个家庭网络一天几十万条查询日志文件几小时就能涨到几百 MB磁盘吃不消日志读起来也费劲。我的日志策略是分层的场景日志级别保留策略用途日常运行info按天切分保留 7 天看错误和异常排查单域名debug临时开启用完即关看完整决策链长期统计info 查询接口配合接口拉指标看缓存命中率、上游耗时配合配置里的 HTTP 接口api.http可以拉取运行指标。我的习惯是每周瞄一眼缓存命中率健康状态下家庭网络的命中率应该在 70% 以上。如果掉到 50% 以下通常说明缓存被频繁刷新或者lazy_cache_ttl设得太小或者有异常的大量随机域名查询那可能是某台设备中了招在疯狂请求随机域名。6.2 用 dig 做三层验证一次到位配置改完必须验证而且不能只验证能不能解析。我做三层验证一层比一层细。第一层基础连通dig 192.168.10.20 example.com short有正常应答就说明服务在跑、上游通。这一层不过别往下看先解决基础问题。第二层分支走向。验证某个域名是否真的走了你期望的分支。做法是查一个内网域名看返回的地址是不是内网地址dig 192.168.10.20 nas.lan short如果返回的是192.168.10.20说明内网分支生效。如果返回超时或者返回了公网地址那说明你的域名集合没匹配上去检查集合文件的后缀写法和文件路径。第三层延迟与缓存。连续查两次同一个域名看耗时dig 192.168.10.20 example.com | grep Query time dig 192.168.10.20 example.com | grep Query time第二次应该明显更快1ms 以内。如果两次都差不多说明缓存没生效回头看缓存插件是不是放在了链首。这一层是最容易被忽略的但恰恰是最有价值的——它验证的是你优化工作的实际成果。还有一个进阶验证查一个不存在的域名看返回码。正常应该是 NXDOMAIN。如果返回 NOERROR 但答案是空的或者干脆超时说明上游行为异常或者链路上有设备在改写响应。这种情况我遇到过换成加密上游后立刻正常。6.3 一台设备一个策略灰度上线的具体操作前面提过灰度这里给个更具体的做法。我按设备重要性分了三批第一批我自己的手机和笔记本。改 DNS 指向新服务用三天。这三天我会特意访问各种类型的站点把异常记下来。第二批家里的电视盒子、平板。这些设备使用场景单一问题暴露慢但影响直观视频卡不卡一眼就知道。用一周。第三批其余所有设备通过 DHCP 统一下发。这时候前两批已经覆盖了大概率的坑。灰度期间建议保留一份快速回退方案把路由器的 DHCP DNS 设置改回原来的值然后让设备重新连一次 WiFi 就能恢复。这个动作我演练过一次确认 2 分钟内能恢复全网心里才踏实。7. 常见问题与排查实录这一节是我一年里真实遇到的问题汇总按现象—可能原因—验证方法—处理整理成表方便你直接对号入座。现象可能原因验证方法处理启动报地址被占用53 端口被本地解析服务占用ss -lunp | grep :53停用并禁用占用服务或先换高位端口验证本机能查其他设备查不了监听地址写成了127.0.0.1查看配置中listen字段改成0.0.0.0:53或内网网卡地址缓存完全不命中缓存插件不在链首连续查询同一域名比耗时把缓存插件移到sequence第一步内网域名解析失败集合文件路径错或后缀匹配没生效dig查内网域名看返回核对文件挂载路径用裸后缀而不是通配符写法某些域名解析到奇怪地址上游结果被中间设备改写换加密上游对比结果启用 DoH/DoT 上游或更换上游大记录查不到如长 TXT只开了 UDP截断后未走 TCPdig tcp对比确保 TCP 53 入口也配置并可用内存持续增长缓存条目过多或日志级别为 debug看进程内存与日志文件大小下调size日志改回 info升级后行为变化新版本插件参数或默认值调整对比官方文档与版本变更说明锁定镜像版本先在非 53 端口验证广告拦截误伤正常站点拦截列表过于激进临时移除该域名从列表验证换更保守的列表或加白名单全网间歇性解析慢上游响应慢或并发过高被限速看日志耗时分布降concurrent到 2换更近的上游表格之外还有几条不容易归类但很值得说的经验。第一条不要在同一条链里放两个 forward 且都用exec无条件执行。我最初想让主组失败自动走备用组写了两行无条件exec结果第一个 forward 拿到响应就终止了链第二个永远不会执行如果第一个超时第二个才开始体感上就是要么快要么等两倍超时。正确做法是用matches加has_resp之类的条件判断或者干脆把两个地址放进同一个 forward 的upstreams列表里用并发解决。后者简单得多我现在就是后者。第二条规则文件要能一键更新但不要自动更新。广告拦截列表这类文件更新频率高我用一个脚本手动触发更新更新前先备份旧文件。为什么不自动因为某次上游列表更新引入了一条过宽的规则把某个常用域名干掉了自动更新的话你连什么时候变的都不知道。手动更新加备份出问题 30 秒回滚。第三条给配置文件上版本管理。用 Git 管理/opt/mosdns目录每次改配置提交一次写清楚改了什么、为什么改。这条听起来小题大做但当你半年后想不起为什么这行规则在这儿的时候git log里的提交信息能救你。我配置里有一条奇怪的例外规则翻提交记录才想起来是当时某台设备的固件有个解析怪癖。第四条DNS 服务本身的可用性要做兜底。万一 mosdns 挂了全网解析就断了。我的兜底是路由器 DHCP 下发的第二个 DNS 地址指向一个公共解析服务。这样主服务挂掉时设备会在一段时间后自动切到备用虽然失去了缓存和拦截的收益但至少不断网。这个细节在稳定的环境里感受不到价值出一次故障你就知道有多重要。第五条别在高峰期改配置。这条听着像废话但我真干过——晚上八点全家在看视频的时候改配置重载失败导致全网断了几分钟被投诉了一整晚。现在的规矩是改配置先在备用端口起一份验证验证通过再动正在跑的动之前跟家里打个招呼。最后分享一个我在长期维护中形成的小习惯配置里每一条规则后面都写一行注释说明这条是为了解决什么。不是为了别人看是为了三个月后的自己。DNS 配置最麻烦的地方不是写是半年后回头改的时候完全不记得某条奇怪规则的存在理由。注释写得越直白越好比如某品牌电视固件会硬编码查询这个域名必须走公网上游否则开机卡 30 秒——这种信息除了你自己没有第二个人能补上。