从 curl 到工程封装:网站测速诊断 API 的进阶实践
适用场景与接口能力边界
当我们需要对目标网站进行全面的网络质量诊断时,传统的做法是依次使用dig、traceroute、curl -w等工具手动拼凑各阶段耗时,过程繁琐且难以标准化。网站测速诊断 API 将这一过程封装为一次 HTTP 请求,返回 DNS 解析、TCP 连接、SSL 握手、TTFB、总耗时以及重定向链、SSL 证书、命中 IP/端口、页面体积等 6 大维度数据。
典型使用场景:
- CDN 加速后的节点质量评估
- 跨地域对比同一 URL 的访问延迟
- 监控服务商提供的第三方测速节点是否正常工作
- CI/CD 流水线中自动检查部署后的 TTFB 是否达标
接口单次请求即可获取全链路时间线,无需分步测量。但需注意:该 API 提供的是端到端延迟快照,不能代表用户真实网络的持续变化;QPS 限制为 2/s,不适合高频率轮询。
接口鉴权与请求参数
鉴权方式
根据官方文档,请求需要在 Header 中携带 API Key。有两种常见方式:
- X-API-Key(curl 示例中使用)
- Authorization(Bearer Token 形式,部分接口同时支持)
实际调用时优先使用X-API-Key头部,Key 可向平台申请获取。
Query 参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | 目标站点 URL,协议可省略(自动补https://) |
未传url时接口返回 400;传入example.com会被自动补全为https://example.com。
从 curl 开始:单次调试与验证
以下命令可直接在终端运行,请将YOUR_API_KEY替换为实际 Key:
curl -sS \ -X GET \ -H "X-API-Key: YOUR_API_KEY" \ "https://v1.apizero.cn/api/site-check?url=baidu.com"-sS含义:-s静默模式隐藏进度条,-S同时显示错误信息。若 Key 正确且网络畅通,响应体为 JSON 数组(单次请求返回一个元素):
[ { "code": 0, "msg": "成功", "data": { "url": "https://baidu.com", "final_url": "https://www.baidu.com/", "http_code": 200, "redirect_count": 1, "timing": { "dns_ms": 15, "connect_ms": 32.5, "ssl_ms": 78.4, "ttfb_ms": 145.2, "total_ms": 156.7 } } } ]返回值逐字段解读
响应顶层为数组,每个元素包含:
code: 0 表示成功;非 0 表示业务错误(如 URL 非法、域名不存在)。msg: 对应 code 的文本描述。data: 测速结果主体。
data内部字段:
| 字段 | 说明 |
|---|---|
url | 请求的原始 URL(可能被补全https://) |
final_url | 最终重定向到的 URL |
http_code | 最终响应的 HTTP 状态码 |
redirect_count | 发生重定向的次数 |
timing | 各阶段耗时对象,均以毫秒为单位。各字段含义见下 |
timing子字段:
dns_ms: DNS 解析耗时connect_ms: TCP 连接耗时(三次握手)ssl_ms: SSL/TLS 握手耗时ttfb_ms: TTFB(首字节时间),从请求发出到收到第一个字节的总时间(通常包含 DNS+连接+SSL+服务端处理)total_ms: 总耗时,从开始到请求完全结束(包含下载响应体)
注意:
total_ms通常大于ttfb_ms,但也可能出现total_ms < ttfb_ms的情况(若服务端压缩或分块传输导致计时边界不同),这种异常一般出现在 CHUNKED 编码中,可在工程中做阈值过滤。
工程封装:Python 版本
直接使用 curl 调试足够,但在自动化任务中需要程序化调用并进行防御性处理。下面是一个 Python 封装示例,包含:
- 环境变量管理 API Key
- 请求超时与重试
- 响应校验与错误码映射
- 数据结构化(命名元组)
import os import time import requests from collections import namedtuple from typing import Optional, Dict, Any SiteCheckResult = namedtuple('SiteCheckResult', [ 'url', 'final_url', 'http_code', 'redirect_count', 'dns_ms', 'connect_ms', 'ssl_ms', 'ttfb_ms', 'total_ms', 'raw_json' ]) class SiteCheckError(Exception): pass class SiteChecker: BASE_URL = "https://v1.apizero.cn/api/site-check" def __init__(self, api_key: str, timeout: float = 10.0, max_retries: int = 2): self._headers = {"X-API-Key": api_key} self._timeout = timeout self._retries = max_retries def check(self, url: str) -> SiteCheckResult: params = {"url": url} last_exc = None for attempt in range(1 + self._retries): try: resp = requests.get( self.BASE_URL, headers=self._headers, params=params, timeout=self._timeout ) except (requests.ConnectionError, requests.Timeout) as e: last_exc = e if attempt < self._retries: time.sleep(1) # 简单退避 continue if resp.status_code != 200: raise SiteCheckError(f"HTTP {resp.status_code}: {resp.text}") try: body = resp.json() except ValueError: raise SiteCheckError("Invalid JSON response") if not isinstance(body, list) or len(body) == 0: raise SiteCheckError("Response should be a non-empty array") item = body[0] if item.get("code") != 0: raise SiteCheckError(f"API error: {item.get('msg', 'unknown')}") data = item.get("data", {}) timing = data.get("timing", {}) return SiteCheckResult( url=data.get("url"), final_url=data.get("final_url"), http_code=data.get("http_code"), redirect_count=data.get("redirect_count"), dns_ms=timing.get("dns_ms"), connect_ms=timing.get("connect_ms"), ssl_ms=timing.get("ssl_ms"), ttfb_ms=timing.get("ttfb_ms"), total_ms=timing.get("total_ms"), raw_json=body ) raise SiteCheckError(f"Max retries exceeded: {last_exc}") ## 使用示例 if __name__ == "__main__": api_key = os.environ.get("APIZERO_API_KEY", "") if not api_key: print("请设置环境变量 APIZERO_API_KEY") exit(1) checker = SiteChecker(api_key) result = checker.check("github.com") print(f"最终URL: {result.final_url}") print(f"DNS: {result.dns_ms}ms, TCP: {result.connect_ms}ms, SSL: {result.ssl_ms}ms") print(f"TTFB: {result.ttfb_ms}ms, 总耗时: {result.total_ms}ms")封装要点说明
- 超时控制:
timeout=10.0防止网络问题导致请求挂起。 - 重试机制:网络抖动时自动重试 2 次,间隔 1s。对于业务错误(code ≠ 0)不重试,因为多半是 URL 参数问题。
- 结构化结果:使用
namedtuple避免手写解析,便于在测试中直接取值。 - 错误链:自定义异常类
SiteCheckError统一上层捕获。
常见错误与排查
| HTTP 状态码 | 可能原因 | 排查方法 |
|---|---|---|
| 400 | 缺少必填参数url | 检查请求参数是否正确 |
| 401/403 | API Key 无效或未携带 | 确认 Header 中X-API-Key的值 |
| 429 | 超过 QPS 限制 (2/s) | 降低调用频率,增加请求间隔 |
| 500 | 服务端测速节点内部错误 | 重试几次,若持续出现则查看平台状态 |
| 非 JSON 响应 | 网络代理或防火墙修改了响应体 | 使用-w "%{http_code}"先检查状态码 |
另外,传入的 URL 若无法解析(如https://notexist.example),API 会返回code为非 0 的错误信息,常见 msg 值:DNS解析失败、连接超时、SSL握手失败。
工程化注意事项
1. 异步适配
若需要同时测速多个站点(不超过 QPS 限制),建议使用asyncio+aiohttp实现并发,而不是串行循环。示例略,核心方法是将check改为异步并增加信号量控制并发数 ≤2。
2. 结果落库与超时过滤
将每次测速结果写入时序数据库(如 InfluxDB),方便观察趋势。注意:total_ms若远小于ttfb_ms(差值 > 50ms)可能是异常,应在入库前标记或丢弃。
3. 与监控系统集成
将ttfb_ms和http_code作为指标上报至 Prometheus,配合 Grafana 做面板。若 90% 分位 TTFB 超过某个阈值(如 3000ms),触发告警。
4. API Key 安全管理
禁止硬编码在代码仓库中。使用环境变量(如APIZERO_API_KEY)或密钥管理服务(Vault/KMS)。
5. 日志与调用追踪
建议在封装的 http 请求处打印请求参数和耗时(非接口返回的 total,而是客户端发起请求到收到完整响应的实际耗时),便于排查是客户端网络问题还是 API 慢。
参考文档
- API 原始文档
- 接口详情页