Python 网络请求工程实践:从超时重试到证书信任的系统化解决

举报
yd_235270612 发表于 2026/10/22 20:59:12 2026/10/22
【摘要】 在日常自动化脚本开发中,网络请求是最容易出问题的环节。本文结合真实踩坑经历,系统性梳理超时控制、指数退避重试、幂等设计以及 SSL 证书信任链问题的排查与修复方法,并给出可直接复用的 Python 代码模板。

在写定时自动化脚本(每日签到、数据同步、健康检查、批量拉取报表)时,最容易出问题的往往不是业务逻辑,而是网络层。典型症状非常一致:脚本昨天还好好的,今天突然抛异常;或者在本机跑得好好的,换一台机器就报证书错误。这类问题看起来琐碎,却直接决定了脚本能不能长期稳定运行。

下面结合真实踩坑经历,把超时、重试、证书、幂等这四件事一次性讲清楚,并给出一个可以直接复用的模板。

一、超时:永远不要写裸的 requests.get

requests 的默认行为是永不超时。这意味着一旦服务端不返回,你的线程会一直挂住。对于定时任务,这会导致任务堆积、后续执行全部错过。

正确做法是显式给出"连接超时"与"读取超时"两个值——它们的含义完全不同:连接超时是 TCP 握手的等待时间,读取超时是两次数据包之间的最大间隔。

import requests

# 连接 3.05s,读取 10s
resp = requests.get("https://example.com/api", timeout=(3.05, 10))
resp.raise_for_status()

经验值:连接超时 3~5 秒足够(握手要么很快,要么网络已经不通),读取超时按接口的实际耗时上浮 2~3 倍来定。

二、重试:指数退避,且只对可重试的错误重试

重试不是简单地"失败就再来一次"。两个原则:一是退避,连续重试会放大对端的压力;二是区分错误类型,4xx 里的 400/401/403 重试一万次也没用,而 429/5xx 则值得重试。

from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry = Retry(
    total=3,                      # 总重试次数
    connect=2, read=2, status=2,
    backoff_factor=0.5,           # 间隔: 0.5s -> 1s -> 2s
    status_forcelist=(429, 500, 502, 503, 504),
    allowed_methods=frozenset(["GET", "HEAD", "OPTIONS"]),
    raise_on_status=False,
)

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://",  HTTPAdapter(max_retries=retry))

注意 allowed_methods 默认只对幂等方法生效。如果你的 POST 本身做了幂等设计(见下文),再把它加进去才安全。

三、证书:certifi 与系统 CA 的信任链不一致

这个坑非常隐蔽:同样是 requests.get(url),在 A 机器上正常,在 B 机器上抛 SSL: CERTIFICATE_VERIFY_FAILED。原因是 requests 默认使用 certifi 自带的证书包,而某些站点(尤其是用了企业内部 CA 或较新根证书的站点)的信任链并不在 certifi 里,却在系统的 /etc/ssl/certs/ca-certificates.crt 里。

先用 openssl 看清楚对端到底返回了什么链:

openssl s_client -showcerts -servername example.com \
    -connect example.com:443 < /dev/null 2>/dev/null | grep -E "subject|issuer"

修复方式按优先级排序:

# 方案 1(推荐,作用于单次进程):指定合并后的 CA 文件
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

# 方案 2(推荐,作用于单个 Session)
session.verify = "/path/to/merged-ca.pem"

# 方案 3:把系统 CA 追加进 certifi(会改动 site-packages,谨慎)
python - <<'EOF'
import certifi
with open(certifi.where(), "a", encoding="utf-8") as f:
    f.write("\n")
    f.write(open("/etc/ssl/certs/ca-certificates.crt", encoding="utf-8").read())
EOF

不要用 verify=False 图省事——它只是把问题藏起来,还会在日志里刷一堆 InsecureRequestWarning。

四、幂等:重复运行不应该产生副作用

定时任务一定会被重复触发:手动补跑、调度器重试、上一次还没退出下一次就启动了。如果脚本不具备幂等性,重复运行就可能重复写入、重复扣款、重复发通知。

三个低成本做法:

  • 给每次运行生成 run_id,写库时以它为唯一键,重复插入直接被约束挡掉;
  • 写文件采用"临时文件 + 原子 rename",避免半截文件被下游读到;
  • 业务请求带上客户端生成的去重键,服务端据此返回第一次的结果。

五、一个可直接复用的模板

把上面几点组合起来,就是下面这个不到 60 行的骨架,可以直接替换掉你脚本里散落的 requests 调用。

import logging, requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

log = logging.getLogger(__name__)
TIMEOUT = (3.05, 15)
CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"   # 或 certifi.where()

def build_session() -> requests.Session:
    s = requests.Session()
    s.headers.update({"User-Agent": "health-check/1.0"})
    retry = Retry(total=3, backoff_factor=0.5,
                  status_forcelist=(429, 500, 502, 503, 504),
                  allowed_methods=frozenset(["GET", "HEAD"]),
                  raise_on_status=False)
    s.mount("https://", HTTPAdapter(max_retries=retry))
    s.mount("http://",  HTTPAdapter(max_retries=retry))
    return s

def fetch_json(session, url, **kw):
    try:
        r = session.get(url, timeout=TIMEOUT, verify=CA_BUNDLE, **kw)
        r.raise_for_status()
        return r.json()
    except requests.exceptions.Timeout:
        log.warning("timeout: %s", url)
    except requests.exceptions.HTTPError as e:
        log.warning("http %s: %s", e.response.status_code, url)
    except requests.exceptions.RequestException as e:
        log.warning("failed %s: %s", url, e)
    return None

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    with build_session() as s:
        data = fetch_json(s, "https://example.com/api/status")
        print("ok" if data else "degraded")

这个模板的关键不在于代码多,而在于把"失败"当成常态:超时有上限、重试有退避、证书可配置、失败可降级。做到这四点,脚本才敢真正放进 crontab 里长期跑。

小结

  • 显式设置 timeout=(connect, read),不要让请求无限等待;
  • 用 Retry 做指数退避,并只对可恢复的错误重试;
  • 证书错误先排查信任链,用 REQUESTS_CA_BUNDLE 或 session.verify 修复,别用 verify=False;
  • 把幂等作为设计前提,而不是事后补救。

如果你也在维护类似的定时任务,欢迎在评论区交流踩过的坑。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。