4. 数据采集基础:使用Python发送HTTP请求、记录响应时间、状态码与延迟数据提取、批量请求与并发控制

数据采集是延迟诊断的基石。在策略即代码的框架下,每一次API调用都必须被精确度量,以形成可对比的基线数据。本节将深入讲解如何使用Python构建可靠的数据采集层,涵盖从单次请求到批量并发的完整技术栈。

4.1 HTTP请求核心:requests库与响应时间记录

Python的requests库是发送HTTP请求的标准工具。在延迟诊断场景中,我们不仅关注响应内容,更关注请求的完整生命周期时间线。

关键时间指标:
  • DNS解析时间:域名到IP的转换耗时
  • TCP连接时间:三次握手建立连接耗时
  • TLS握手时间:加密协商耗时(仅HTTPS)
  • 首字节时间(TTFB):从请求发出到收到第一个字节
  • 总响应时间:从请求开始到响应完全接收

以下代码展示了如何利用requests的钩子(hooks)机制精确捕获各阶段耗时:

import requests
import time

class LatencyTracker:
    def __init__(self):
        self.timings = {}
        self.start = None

    def on_request_start(self, **kwargs):
        self.start = time.perf_counter()
        self.timings['request_start'] = self.start

    def on_response_received(self, **kwargs):
        self.timings['response_received'] = time.perf_counter()

    def on_response_complete(self, **kwargs):
        self.timings['response_complete'] = time.perf_counter()

    def get_latency_report(self):
        if not self.timings:
            return {}
        return {
            'total_latency_ms': round(
                (self.timings['response_complete'] - self.timings['request_start']) * 1000, 2
            ),
            'network_latency_ms': round(
                (self.timings['response_received'] - self.timings['request_start']) * 1000, 2
            ),
            'transfer_latency_ms': round(
                (self.timings['response_complete'] - self.timings['response_received']) * 1000, 2
            )
        }

# 使用示例
tracker = LatencyTracker()
response = requests.get(
    'https://api.example.com/health',
    hooks={
        'pre_request': tracker.on_request_start,
        'response': tracker.on_response_received
    }
)
tracker.on_response_complete()
print(tracker.get_latency_report())

4.2 状态码与延迟数据提取

每次请求完成后,必须提取结构化数据用于后续分析。状态码是判断请求成功与否的第一道关卡,而延迟数据则是性能基线的基础。

数据字段 类型 说明 提取方式
timestamp ISO 8601 请求发起时间 datetime.utcnow().isoformat()
url string 完整请求URL response.url
method string HTTP方法 response.request.method
status_code int HTTP状态码 response.status_code
total_latency_ms float 总响应时间(毫秒) 见上节LatencyTracker
ttfb_ms float 首字节时间 response.elapsed.total_seconds() * 1000
response_size_bytes int 响应体大小 len(response.content)
error_type string 异常类型(如有) type(exception).__name__
实践建议: 使用response.elapsed获取TTFB时,注意该属性返回的是timedelta对象,需转换为毫秒。对于超时或连接错误,务必捕获requests.exceptions.RequestException及其子类。

4.3 批量请求与并发控制

在发布窗口的基线偏差检查中,通常需要同时向多个端点或同一端点发送大量请求以建立统计基线。Python提供了多种并发方案,其中concurrent.futures与asyncio是最常用的两种。

4.3.1 使用ThreadPoolExecutor实现并发请求

from concurrent.futures import ThreadPoolExecutor, as_completed
import requests

def fetch_url(url: str, timeout: int = 5) -> dict:
    """发送单个请求并返回结构化数据"""
    start = time.perf_counter()
    try:
        resp = requests.get(url, timeout=timeout)
        latency_ms = round((time.perf_counter() - start) * 1000, 2)
        return {
            'url': url,
            'status': resp.status_code,
            'latency_ms': latency_ms,
            'success': True
        }
    except requests.exceptions.RequestException as e:
        return {
            'url': url,
            'status': None,
            'latency_ms': None,
            'success': False,
            'error': str(e)
        }

def batch_request(urls: list, max_workers: int = 10) -> list:
    """批量并发请求"""
    results = []
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_url = {executor.submit(fetch_url, url): url for url in urls}
        for future in as_completed(future_to_url):
            results.append(future.result())
    return results

# 示例:并发请求10个端点
urls = [f'https://api.example.com/endpoint/{i}' for i in range(10)]
batch_results = batch_request(urls, max_workers=5)

4.3.2 并发控制策略对比

方案 适用场景 最大并发数 资源消耗 复杂度
ThreadPoolExecutor I/O密集型,简单并发 通常10~50 中等(线程切换) 低
asyncio + aiohttp 高并发,大量请求 100~1000+ 低(协程) 中
multiprocessing CPU密集型(极少用于API采集) 取决于CPU核心 高(进程隔离) 高
requests + 循环 调试、少量请求 1(串行) 最低 最低
注意: 并发数并非越大越好。过高的并发可能导致目标服务触发限流(429 Too Many Requests),或导致本地端口耗尽。建议从max_workers=5开始,逐步增加并观察错误率。

4.4 数据采集架构总览

下图展示了从单次请求到批量并发采集的完整数据流,以及各组件之间的协作关系:

数据采集架构流程图 数据采集架构:从请求到基线数据 请求配置 URL / 方法 / 超时 / 头 并发控制器 ThreadPool / asyncio 请求执行器 requests / aiohttp 延迟追踪器 hooks / elapsed 状态码提取 2xx / 4xx / 5xx 数据聚合 JSON / CSV / 时序 基线存储 InfluxDB / Prometheus 说明 所有时间单位:毫秒 异常数据单独标记

4.5 实战:构建可复用的数据采集器

将上述技术整合为一个可复用的采集器类,便于在发布窗口的基线检查中直接调用:

import time
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from typing import List, Dict, Optional
import requests

logger = logging.getLogger(__name__)

@dataclass
class RequestResult:
    url: str
    status_code: Optional[int]
    total_latency_ms: float
    ttfb_ms: float
    response_size: int
    timestamp: str
    error: Optional[str] = None

class APILatencyCollector:
    """API延迟数据采集器"""
    
    def __init__(self, base_url: str, timeout: int = 10, max_workers: int = 10):
        self.base_url = base_url.rstrip('/')
        self.timeout = timeout
        self.max_workers = max_workers
        self.session = requests.Session()
    
    def single_request(self, endpoint: str) -> RequestResult:
        """发送单次请求并记录延迟"""
        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        start = time.perf_counter()
        try:
            resp = self.session.get(url, timeout=self.timeout)
            total_latency = round((time.perf_counter() - start) * 1000, 2)
            ttfb = round(resp.elapsed.total_seconds() * 1000, 2)
            return RequestResult(
                url=url,
                status_code=resp.status_code,
                total_latency_ms=total_latency,
                ttfb_ms=ttfb,
                response_size=len(resp.content),
                timestamp=time.strftime('%Y-%m-%dT%H:%M:%S.%fZ', time.gmtime())
            )
        except requests.exceptions.RequestException as e:
            total_latency = round((time.perf_counter() - start) * 1000, 2)
            logger.warning(f"请求失败: {url} - {str(e)}")
            return RequestResult(
                url=url,
                status_code=None,
                total_latency_ms=total_latency,
                ttfb_ms=0.0,
                response_size=0,
                timestamp=time.strftime('%Y-%m-%dT%H:%M:%S.%fZ', time.gmtime()),
                error=str(e)
            )
    
    def batch_request(self, endpoints: List[str]) -> List[RequestResult]:
        """批量并发请求"""
        results = []
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            future_map = {
                executor.submit(self.single_request, ep): ep 
                for ep in endpoints
            }
            for future in as_completed(future_map):
                try:
                    results.append(future.result())
                except Exception as e:
                    logger.error(f"批量请求异常: {str(e)}")
        return results

# 使用示例
collector = APILatencyCollector(
    base_url='https://api.example.com',
    timeout=5,
    max_workers=8
)
results = collector.batch_request(['health', 'metrics', 'status'])
for r in results:
    print(f"{r.url} -> {r.status_code} | {r.total_latency_ms}ms")
生产环境建议:
  • 使用requests.Session()复用连接,减少TCP握手开销
  • 设置合理的timeout,避免请求长时间挂起
  • 对失败请求实施指数退避重试策略(后续章节详述)
  • 所有时间戳使用UTC格式,便于跨时区分析

4.6 数据采集质量检查清单

在将采集器投入基线偏差检查前,请逐项确认以下要点:

  1. 时间精度:使用time.perf_counter()而非time.time(),避免系统时钟调整影响
  2. 异常覆盖:捕获ConnectionError、Timeout、HTTPError等所有常见异常
  3. 数据完整性:每条记录必须包含时间戳、URL、状态码、延迟四个核心字段
  4. 并发稳定性:在目标服务可承受范围内测试最大并发数,记录错误率
  5. 日志记录:对每次请求的开始和结束打印结构化日志,便于事后审计
  6. 资源清理:确保Session在使用完毕后正确关闭

完成上述数据采集基础建设后,下一阶段将进入延迟数据的统计分析与基线建模,为发布窗口中的偏差检测提供数学依据。