
做运维久了就会发现“服务没挂”和“用户能访问”完全是两码事。我之前就吃过这个亏后台服务进程跑得稳稳当当CPU、内存、连接数全部正常前端页面就是刷不出来。排查到最后问题出在服务依赖的一个资源 URL 上——那个地址在某个中间环节已经不通了但从进程视角看一切指标都正常。这类问题最能逼出一个需求你需要一个能主动、持续、按自己的规则去探测资源 URL 连通性的工具。市面上现成的监控平台不少但真要满足内部环境、私有服务、自定义探测频率和判定标准这些场景还是得自己动手写一个心里才踏实。这篇文章就聊聊我自建这个 URL 连通性探测工具的全过程从需求拆解、协议选择到参数设计、告警降噪再到部署后踩过的坑。内容偏实操适合被“服务正常但客户说连不上”困扰过的朋友也适合准备从零搭一套轻量可用性监控的人。1. 为什么我要自己写这个 URL 连通性探测工具1.1 外部监控平台覆盖不到的“网络死角”很多团队一开始都在用第三方监控平台定时去检查公网入口、首页、登录接口这些“门面地址”这套方案对纯公网业务还算凑合。但我们当时面临的情况更复杂一部分服务跑在企业内网另一部分在开发测试环境还有一些服务依赖的是别人提供的内部资源地址比如对象存储网关、消息队列管理接口、数据平台的调度入口。这些地址从公网探针根本访问不到外部监控平台自然给不出任何有价值的结果。就算把探针部署到内网第三方平台的调度粒度和判定逻辑也很难贴合实际。监控平台通常是分钟级轮询自定义探针部署又得在他们的客户端里做二次开发限制不少。最尴尬的是测试环境地址经常变、端口临时起临时关想调整监控项还得等平台侧配置生效运气不好一次变更要等半天。对运维来说这种“监控死角”比没有监控还难受——你以为有人盯着其实那里什么保护都没有。1.2 从一次线上事故说起监控有但告警晚了半拍让我把时间拨回项目最难熬的那一周。有个面向内部员工的门户系统频繁出现“页面加载一半就转圈”的投诉。服务端日志翻遍了没发现异常数据库连接池、Redis 命中率、网关转发耗时都在正常范围。最后靠抓包才发现门户页面里引用的一个静态资源地址指向了迁移后的新域名而旧域名在内网 DNS 里的解析记录已经失效。浏览器一直请求这个域名每次都等超时页面自然就卡住了。这个资源地址恰好不在任何监控项里。第三方平台监控的是首页 URL 本身首页 HTML 确实能返回 200但里面引用的子资源已经死了很久。更讽刺的是旧域名的 DNS 解析其实是间歇性失败属于偶发问题分钟级监控根本抓不住这种“时好时坏”的状态。那次事故后我下决心自己写一个工具要求很朴素想探测什么 URL 就探测什么 URL想多久探一次就多久探一次判定失败的标准也能自己定。不做成大而全的监控平台就做一个“指哪打哪”的连通性探测工具能持续跑、能出历史数据、能告警就够。2. 动手前的三个关键决策协议栈、探测频率与失败判定2.1 协议栈怎么选HTTP/HTTPS、TCP 还是 ICMP很多初学者写探测工具第一反应就是“用 ping”但实际做下来会发现ICMP 在很多网络环境下会被防火墙直接丢弃而且 ping 只能验证主机层面的连通性完全看不出来 80 端口和 443 端口是否正常。真正在业务场景里我更常用的是这几种探测协议HTTP/HTTPS 请求最贴合业务视角能检查 URL 是否能正常响应能拿到状态码、响应时间、响应体内容适合探测 API、页面、静态资源地址。TCP 端口连通适合非 HTTP 服务比如数据库端口、消息队列端口、Redis、内部 RPC 服务。只需要确认“端口能连上”不关心具体协议内容。TCP 自定义协议握手这是 HTTP 探测的超集。比如探测一个自研 RPC 端口连接后需要发一段固定的握手包能收到预期响应才算通过。这个能力扩展起来很方便我在工具里预留了“发送字节”和“期望响应字节”的配置位。这里有个容易踩的误区不是所有 HTTP 服务都返回 200 才算健康。有些内部服务为了不暴露内部信息对健康检查路径故意返回 403有些管理接口返回 302 跳转到登录页反而是正常状态。所以探测工具不能硬编码“2xx 就是成功”必须支持自定义期望状态码。2.2 频率、超时与重试的参数取舍探测频率这块我一开始想得比较简单既然要监控那就 10 秒探一次越快发现问题越好。结果上线第一天就被现实教育了。某个老系统接口扛不住高频访问监控跑了一个小时直接把对方接口触发限流对方运维气势汹汹找过来问是不是在打他们接口。所以频率设置必须尊重两个约束一是探测目标自身能承受的请求压力二是问题发现速度的真实需求。我的经验是对公网资源一般 30 秒到 60 秒探一次足够内网核心服务可以激进一点到 10 秒到 15 秒但需要提前确认目标服务的承受能力。如果是对外网第三方服务做探测频率务必要保守有些服务商会把高频率请求视为攻击行为。超时参数同样需要单独设计。很多人的做法是requests.get(url, timeout5)把所有超时混在一起。但更正确的做法是区分连接超时和读取超时连接超时太短跨地域访问会大量误报读取超时太短处理慢的接口很容易被误判。我一般把连接超时设为 3 秒到 5 秒读取超时设为 10 秒到 30 秒不同目标可以在配置里单独覆盖。重试逻辑也不是越重试越好。过度重试会让故障恢复时间被拉长本来 5 秒能发现的问题重试三次之后可能拖到 30 秒。我的设计是第一次失败立即重试一次如果仍失败间隔 5 秒再重试一次再失败才标记为探测失败。这样能过滤掉绝大多数网络抖动又不会把告警拖得太迟钝。2.3 判定“失败”的完整链路仅仅“发起请求”和“收到响应”并不足以定义探测成功。我把一次完整探测拆成了五个环节每个环节都有明确的失败类型环节判定标准典型失败类型DNS 解析域名能解析出 IPdns_errorTCP 建连目标端口能完成握手connection_refused / timeoutTLS 握手证书有效且能完成加密握手tls_error / certificate_expiredHTTP 响应状态码符合期望http_5xx / unexpected_status业务语义响应体包含期望关键字可选body_mismatch这个拆解特别重要因为它能直接决定后续怎么处理。DNS 解析失败和 TCP 连接超时代表网络链路有问题值得立即重试和告警HTTP 500 说明链路是通的但服务本身在处理请求时出错了这同样值得告警但排查方向和前者完全不同。有了失败类型告警信息里就不再只是一句干巴巴的“连接失败”而是“探测到 https://xxx 域名解析失败重试两次后仍失败”定位效率翻倍。3. 核心实现从单探针到并发可观测3.1 同步请求的短板与并发方案的选择第一版工具我写得很直接用一个循环逐个探测 URL顺序执行。跑起来才发现一旦探测列表里有那么一两个慢接口整个周期就被拖慢了。比如一个接口 20 秒超时另一个接口本来 1 秒就能探完结果都得等这 20 秒走完才开始下一轮。探测周期被无限拉长监控的及时性大打折扣。解决办法自然是并发但并发方案有讲究。Python 生态里有三种选择多线程、多进程、协程。对探测这种以网络 IO 为主的任务多线程和协程都够用差别主要在资源的简洁性上。协程写起来更轻但很多现成的 HTTP 库在协程环境里需要配套生态支持多线程更成熟代码也更容易理解。最终我选择了线程池方案并发数设置成 20。这个数字不是拍脑袋定的而是根据目标服务承受能力和单轮探测任务量算出来的。当时大概有三十多个 URL 需要探测20 个并发意味着单轮最多两批跑完而大多数目标响应都在 1 秒内整体周期能控制在 2 秒以内。测试环境网络抖动比较常见所以单线程那种“一个慢请求拖一轮”的问题必须避免。3.2 代码骨架与关键函数核心探测逻辑其实不复杂关键在结构清晰。我抽象了一个ProbeTask对象发起探测的入口长这样import socket import ssl import time from dataclasses import dataclass, field from concurrent.futures import ThreadPoolExecutor, as_completed dataclass class ProbeResult: target: str protocol: str status_code: int time_total: float fail_type: str message: str detected_at: float def probe_http(url: str, expected_code: int, timeout: tuple) - ProbeResult: start time.time() try: resp session.get(url, timeouttimeout, allow_redirectsTrue) elapsed round(time.time() - start, 3) if resp.status_code expected_code: return ProbeResult(url, http, resp.status_code, elapsed, , ) return ProbeResult(url, http, resp.status_code, elapsed, unexpected_status, f期望 {expected_code}实际 {resp.status_code}) except Exception as exc: elapsed round(time.time() - start, 3) fail_type classify_exception(exc) return ProbeResult(url, http, 0, elapsed, fail_type, str(exc))有人可能觉得这个代码看起来有点“薄”但探测工具本来就不该塞太多业务逻辑。关键在几个容易被忽略的细节session.get用的是预创建的连接池长连接能省去反复握手的开销异常分类classify_exception把socket.timeout、ConnectionError、ssl.SSLError分别映射到明确类型为后续统计和告警铺路。3.3 结果数据结构与错误分类探测结果统一用 JSON 行格式落盘一行一个记录。这个格式选它不是图省事而是因为日志类工具和命令行分析工具都天然支持grep、awk、jq 随手就能处理。每行包含目标地址、协议、状态码、耗时、失败类型、探测时间这几个核心字段排查问题时会明显感觉到方便。classify_exception的错误分类要尽量细。我见过很多人把异常全部打包成“连接失败”导致告警永远只显示一句话根本没法派单。我自己维护了这样一张映射表异常类型映射结果告警级别建议socket.timeouttimeout高ConnectionRefusedErrorconnection_refused高socket.gaierrordns_error高ssl.SSLCertVerificationErrorcertificate_error中需提前续期SSLEOFErrortls_error高HTTP 5xxhttp_5xx高HTTP 4xxhttp_4xx低需人工判断不同类型的失败后续重试和告警策略也可以不同。比如证书错误通常不会因为重试而恢复重试只会浪费时间但http_5xx有可能是服务临时抖动重试成功的概率很高。这个区分在告警降噪里非常重要。4. 聚合、告警与持续观测让数据说话4.1 状态聚合用滑动窗口代替单点判断最开始看探测结果我习惯看最近一轮的状态这一轮通了就是绿灯这一轮挂了就告警。后来复盘发现这种方法问题很大——单轮探测受网络抖动影响太明显经常出现上一轮失败、下一轮成功的情况告警发得毫无价值。后来我引入了滑动窗口聚合。简单说就是统计最近 5 分钟内总探测次数和成功次数用成功率来代表当前健康状态。成功率低于一个阈值才认为异常否则最多算“抖动”或“降级”。窗口大小可以配置对敏感业务可以缩短到 1 分钟对普通资源用 5 分钟更合理。这样做还有个额外好处可以更容易地画趋势图而不是只看一个孤立的“通/不通”状态。4.2 告警降噪连续失败阈值与恢复确认告警降噪是这套工具里性价比最高的优化。我的规则很简单单次失败不立刻告警连续失败三次才触发恢复通知也不是恢复成功一次就发而是连续成功三次才解除告警。这个“三次”是拍脑袋定的吗不是。我统计过前一个月的探测数据网络抖动导致的瞬时失败几乎不会连续出现三次以上而真实故障大概率会持续超过一分钟。告警消息里一定要带上前因后果。我有一条固定的告警模板包含目标 URL、连续失败次数、最近一次失败类型、最近三次探测结果时间线、DNS 解析耗时和 TLS 证书剩余天数。这些信息能让收到告警的人不用再登服务器查日志直接判断是找网络组还是找应用组。4.3 历史数据的价值趋势与回归很多人把连通性探测工具当成“告警触发器”用完就扔我觉得这是浪费。工具每天会沉淀大量可观测数据这些数据的价值比告警本身大得多。举个例子某个外部服务的响应时间从 200ms 逐渐涨到 800ms单日的探测结果看起来都“正常”没有触发任何告警。但拉出一个月的趋势折线你会发现这条线斜率很明显说明对方服务在缓慢劣化。等真的哪一天超时阈值被击穿就完全不是突发问题而是趋势恶化的必然结果。把历史数据落成 CSV 或者导入时序数据库后这类“缓慢劣化”基本都能提前发现。5. 实际部署三个月后踩过的那些坑5.1 环境变量里的代理陷阱这是最隐蔽的一个坑。工具刚上线时跑在内网一台机器上明明网络正常但所有 HTTPS 探测全部超时。排查了半天最后发现机器的环境变量里配置了http_proxy和https_proxy指向一个早已废弃的内部代理地址。第三方 HTTP 库默认会读取这些环境变量导致每个请求都试图先连代理代理连不上自然全部失败。解决办法是显式禁用自动代理在请求会话里明确指定trust_envFalse。如果你也在写类似的工具建议在初始化 session 后立刻打印一份“当前是否使用代理”的状态避免被环境变量悄悄影响。5.2 探测目标本身的反爬和限流前文提到过有个老系统被高频探测打到限流。这提醒了我一个原则探测工具必须站在目标服务的角度想问题。有些资源 URL 是公开页面带频率限制有些内部接口有白名单机制源 IP 不在列表中会直接拒绝还有些 CDN 厂商对异常 User-Agent 做拦截。我的工具在配置里增加了自定义请求头设置默认带上了一个显眼的 User-Agent比如health-checker/1.0。这样做一方面能让目标方清楚地识别这是监控流量另一方面也能避免被误判成“扫描器”。同时每个目标都支持独立的频率配置敏感服务单独调低不再一刀切。5.3 时间戳、日志轮转与证书过期预警三个不起眼但很重要的运维细节。第一探测结果的时间戳必须用统一的时区最好直接用 UTC 毫秒时间戳落盘展示时再转本地时间。我就吃过亏监测脚本和告警平台用了不同时区配置排障时两条记录对不上白白浪费了不少时间。第二日志文件要轮转。JSON 行格式虽然好用但不轮转会无限增长。我设置了按天切割、保留 30 天的策略超过自动清理。第三工具要把 TLS 证书剩余天数作为独立字段记录当剩余天数小于 30 天就单独发一条提示告警。证书过期这种事与其等人告诉你说“突然访问不了了”不如提前两周就看到提醒。最后再分享一个小技巧探测工具的配置文件一定要用 YAML把每个目标地址、协议、期望状态码、频率、重试次数、告警阈值全部显式列出来然后写一段自检逻辑启动时检查配置是否合法。这样新同事接手时只需要改配置不用翻代码整个工具的维护成本会低很多。