
写这篇东西的起因是我上周又双叒叕在技术群里看到有人问“为什么我的requests一请求就卡住不动了”。这问题太经典了几乎所有用Python写爬虫或者调API的人迟早都会撞上。最气人的是requests库本身的设计很人性化用起来也倍儿爽结果程序一跑起来就“假死”既不报错也不超时就这么一直挂着把人都能急死。我在这个坑里摸爬滚打了好几年服务过好几个业务场景今天就把我踩过的坑、排查思路和最终解决方案一次性写清楚。先说一个最常见的现象代码里明明写了requests.get(url)后面也跟着处理逻辑可程序就是卡在那一行不动了CPU占用率还不高看起来就像死锁一样。这种情况十有八九不是requests内部出问题而是底层网络连接状态、超时配置或者并发环境出了问题。搞清楚根源才能对症下药。1. 卡死问题的全貌拆解表现、本质与排查路线1.1 先给“卡死”画个像不只是没响应这么简单requests请求卡死严格来说有几种不同的表现虽然看起来都是“卡住”但处理方向完全不同完全无响应程序停在requests.get那行CtrlC都不一定能立刻中断像被钉死了一样。偶发卡顿大部分请求正常但某些特定的URL或特定时刻会卡几十秒甚至几分钟。多线程全部卡住单个请求没问题一旦开了线程池整个程序就集体“罢工”。微服务场景下的拖累接口偶尔超时导致整个服务链路阻塞后面排队的所有业务都被拖住。我遇到过最离谱的一种情况是某个数据采集任务在测试环境跑得好好的一上生产就隔几个小时卡死一次没有任何异常抛出。最后抓包才发现是目标服务器的防火墙对长连接做了静默丢弃而requests默认没有设置超时于是连接就一直挂着等一个永远不会到来的响应。1.2 本质探究requests为什么会“傻等”requests库本身不自主处理网络超时。它把底层socket的默认行为直接暴露给你了也就是说如果你不主动设置超时时间socket在建立连接和等待读取数据时会一直等下去。很多人不理解为啥会这样觉得requests默认就会超时。这是个天大的误区。Python官方文档里明确写过requests的默认timeout参数是None意思是“无限期等待”。从用户视角看这等同于“卡死”。要理解这个行为需要明白一次HTTP请求的生命周期包含三个阶段DNS解析、TCP连接建立、TLS握手HTTPS、发送请求头和数据、等待服务器返回响应体。卡死可能发生在其中任何一个阶段而每个阶段的处理手段和排查方式都不一样。1.3 排查路线从现象倒推原因我整理了自己常用的排查顺序按照这个顺序走基本能在半小时内定位问题先看卡死是“必现”还是“偶现”。必现大概率是URL写错、DNS解析不到、端口不通这类基础问题用curl带-v参数试一下立刻就知道。偶现先抓包Wireshark或tcpdump都可以看数据包是在哪一步丢的。如果是并发场景先关掉并发单线程连续跑100次试试。确认是不是代理设置的问题有时候环境变量或系统代理会导致requests走一个不可达的代理服务器。只有把现象和阶段对应起来后面的配置方案才不会瞎蒙。2. 三层级排查实战从socket层到HTTP层的卡点定位2.1 第一层socket连接建立的隐形障碍requests最终走的还是socket连接所以socket阶段的问题requests一样逃不掉。socket卡住通常有几种原因目标IP不可达路由器迟迟不返回ICMP错误报文。目标端口被防火墙拦了SYN包发出去石沉大海。目标服务器负载太高accept队列塞满新的TCP握手请求在系统层面排队。MTU问题导致TCP分片异常。这一层的排查工具推荐两个telnet和nc。举个例子telnet example.com 80如果命令一直挂着不返回说明TCP连接建立过程中出了问题。用nc再验证一次nc -vz -w 5 example.com 80nc加了-w参数之后5秒内连不上就会主动断开能快速验证端口可达性。我曾经排查过一个问题某台服务器上的爬虫程序卡死telnet目标IP的443端口完全不响应但同一台机器上的浏览器却可以正常访问网站。后来发现是浏览器走了系统代理而Python的requests没有读取系统代理配置流量直接砸向了一个根本不存在的内网IP。这种“本机环境差异”带来的坑特别容易误导排查方向。2.2 第二层DNS解析卡住的隐蔽性DNS解析超时是最容易被忽略的卡死原因。很多人觉得DNS就是查个IP嘛能有多慢但实际上在部分内网环境或弱网环境下DNS解析卡住十几秒甚至几十秒都很正常。举个例子你在代码里用了某个域名这个域名配置了多个DNS服务器其中第一台DNS服务器宕机了ndots配置又导致查询走了错误路径解析就会无限重试。Python层面可以用socket.getaddrinfo()来单独测试DNS解析是否正常import socket print(socket.getaddrinfo(example.com, 443))如果这一步卡住说明系统DNS配置有问题跟requests没关系。这种情况下就不要再折腾requests了先去修DNS配置。还有一种情况是DNS正常但解析出的IP已经变了。服务器原先的IP下线了新的IP还没完全同步到所有DNS节点于是部分用户解析到旧IP连接自然就建立不起来。这种情况重启程序也没用只能等DNS缓存过期或者手动绑定hosts。2.3 第三层HTTP连接池的泄漏与复用这里要提到requests的底层依赖urllib3维护的连接池。连接池复用连接是个好设计但在某些场景下也会变成坑。比如你用一个Session对象发了很多请求连接池里保存了到某台服务器的空闲连接。这台服务器的防火墙配置了空闲连接超时比如60秒没有流量就把连接掐了。然后你的代码隔了几分钟又用同一个Session发起请求由于无法感知到服务端已经关闭了连接数据包发过去之后就石沉大海。这种问题最坑的地方在于它不是必现的。有时候速度快就赶上了有时候慢了几秒就触发了。解决思路是给连接池配置合理的“健康检查”或者在新请求前主动重试。3. 配置层实操给requests装上安全气囊3.1 超时配置的正确姿势前面说了不设超时等于裸奔。这里给出我验证过的推荐配置。requests的超时必须分两层设置连接超时和读取超时。连接超时针对的是TCP握手和TLS握手阶段读取超时针对的是从服务器接收数据的时间间隔。import requests resp requests.get( url, timeout(3.05, 10) )上面代码里的timeout元组第一个值是连接超时时间3.05秒第二个值是读取超时时间10秒。注意我特意写了3.05这个带小数的数字这是urllib3官方文档里建议的值理由是socket层实际操作时会对超时时间做一个取整3.05可以保证实际生效值不会低于3秒。如果不小心只传了一个int值requests.get(url, timeout5)这表示连接和读取两个阶段的超时时间都是5秒但这个“5秒”是读超时不是总耗时上限。很多人以为5秒是“整个请求最多跑5秒”实际情况是连接可以跑5秒然后读响应还能再跑5秒总共最多10秒。这个认知误区如果没搞清楚设计出来的超时策略就全错了。3.2 全局超时配置不用每个请求都写一遍单次请求好配置但项目里如果需要访问几十个不同的接口每个都写一遍timeout参数代码就太啰嗦了。更优雅的方案是在Session层面统一设置import requests session requests.Session() session.request lambda *args, **kwargs: requests.Session.request( session, *args, timeout(3.05, 15), **kwargs )上面的做法比较hack。更推荐的做法是写一个自定义适配器from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter class TimeoutHTTPAdapter(HTTPAdapter): def __init__(self, *args, **kwargs): self.timeout kwargs.pop(timeout, (3.05, 15)) super().__init__(*args, **kwargs) def send(self, request, **kwargs): kwargs.setdefault(timeout, self.timeout) return super().send(request, **kwargs) adapter TimeoutHTTPAdapter(timeout(3.05, 15)) session requests.Session() session.mount(https://, adapter) session.mount(http://, adapter)这种方式把超时配置下沉到了适配器层Session发起的任何请求都会自动带上超时参数代码整洁逻辑也清晰。3.3 重试机制的参数化配置有了超时请求会在超时后抛异常但程序未必就能正常跑下去。很多时候超时只是临时性的网络抖动重试一次就成功了。所以还需要配置重试机制。requests的重试依赖urllib3的Retry类。一个健壮的重试配置要考虑这三个参数total总重试次数。connect连接阶段重试次数。read读取阶段重试次数。status_forcelist特定HTTP状态码触发重试比如500、502、503、504。backoff_factor退避因子控制重试间隔。from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter retry_strategy Retry( total3, connect3, read2, status_forcelist[500, 502, 503, 504], backoff_factor1, allowed_methods[GET, POST], ) adapter HTTPAdapter(max_retriesretry_strategy) session requests.Session() session.mount(https://, adapter) session.mount(http://, adapter)重试间隔的计算规则是{backoff_factor} * (2 ** (retry_number - 1))也就是第1次重试等1秒第2次等2秒第3次等4秒。这个指数退避策略能有效防止重试雪崩就是所有客户端同时重试导致服务器压力更大的情况。需要注意的是重试只能解决临时故障解决不了持续性的故障。如果一个接口连续重试3次都失败那就别硬刚了及时放弃并记录日志让监控系统报警才是正解。重试的正确用法是“多给几次机会”而不是“无限死磕”。4. 实战场景并发程序中的卡死与线程池治理4.1 线程池中requests卡死的典型表现并发场景下的卡死很多时候并不是requests自身的问题而是线程池的设计有缺陷。比如用ThreadPoolExecutor开10个线程去跑20个请求任务每个任务里又埋了个requests.get且没设超时。这种情况下只要有两个请求卡住这10个线程里就可能有一半线程被任务占住后面的任务全堵在队列里。更严重的是如果这些卡住的请求不释放线程线程池的核心线程会被慢慢耗尽程序就会表现为“整体卡死”。我用一个例子来说明最典型的错误写法from concurrent.futures import ThreadPoolExecutor import requests urls [...] # 假设有100个URL def fetch(u): # 没设置超时隐患极大 resp requests.get(u) return resp.text with ThreadPoolExecutor(max_workers10) as executor: results list(executor.map(fetch, urls))这段代码只要出现一个卡住的请求整个executor.map就会一直阻塞等待结果返回表现上就是“卡死”无误了。正确做法必须组合使用超时、连接池配置和future的timeout控制。4.2 线程池配合连接池的极致配置在并发场景里requests自带的连接池上限默认是10个连接即每host最多10个连接。如果你的线程数超过10部分线程就得排队等空闲连接表现上就会变慢、像卡住了一样。因此线程池大小和连接池大小必须匹配。推荐的配置是让连接池上限略大于线程数from requests.adapters import HTTPAdapter adapter HTTPAdapter( pool_connections20, pool_maxsize20, max_retriesretry_strategy ) session requests.Session() session.mount(https://, adapter)如果线程池是10个线程pool_maxsize可以配成10或者15。另外当路由到同一个host时连接池复用会生效可以明显降低TCP握手次数。4.3 使用future的timeout做双保险即使在requests层面配置了超时高并发场景下依然建议在executor层面设置第二层超时。原因很简单thread pool里的任务可能因为排队而迟迟得不到执行也可能因为requests抛了异常被某个库把线程搞阻塞了。future层面的timeout可以确保主线程不会无限等待。from concurrent.futures import ThreadPoolExecutor, as_completed with ThreadPoolExecutor(max_workers10) as executor: future_map {executor.submit(fetch, url): url for url in urls} for future in as_completed(future_map, timeout30): url future_map[future] try: data future.result() except Exception as exc: print(fURL: {url} 出错: {exc}) continueas_completed加timeout参数表示整个等待结果的过程最多持续30秒。到了时间还没完成的任务会被放弃这样即使在极端情况下requests的配置没生效程序也能及时退出不至于彻底卡死。4.4 并发数应该选多少没用过线程池的读者可能对max_workers选多少没有概念。我的经验是目标服务器没特殊限制时开CPU核心数x4到x5比较稳妥。如果是公网爬虫单机并发控制在10到20以内别太高。如果目标服务器是你的微服务集群可以用压测工具测一下接口的极限QPS然后取三分之一左右作为并发上限。并发太高不仅你自己卡对方的防火墙也会主动断开你的连接到时候反而更慢。5. 深水区TCP连接半开、NAT超时与长连接保活5.1 半开连接与连接池中毒前面提过连接池复用问题这里展开讲一下其中的核心概念TCP半开连接。所谓半开连接就是连接的一端客户端或服务器已经不知道这条TCP连接的存在了而另一端还在傻傻地维护着这个连接状态。最常见的原因就是NAT设备或防火墙的会话超时——设备在空闲时静默地把这个连接记录删了后续的数据包到达时设备直接丢弃客户端这边还以为连接正常。这种问题在移动网络环境下尤其常见手机切了WiFi、电梯里断了网TCP连接就处在半开状态。requests连接池里保存的就是这种已经死掉的连接。5.2 连接池清空与验证策略对于这种情况urllib3提供了连接回收机制但默认配置比较保守。我们需要启用连接验证在从连接池取连接时检查连接是否仍然有效。requests的HTTPAdapter里urllib3的HTTPConnectionPool是支持连接池健康检查的。最直接的方法是为每个主机配置一个连接池然后在请求前主动关闭空闲连接或者对连接池中的连接做验证import requests from requests.adapters import HTTPAdapter class HealthyHTTPAdapter(HTTPAdapter): def get_connection(self, url, proxiesNone): conn super().get_connection(url, proxiesproxies) # 这里打印一下连接池信息方便排查 print(f连接池状态: {conn.pool}) return conn session requests.Session() session.mount(https://, HealthyHTTPAdapter())但是这种检查并不会主动发探测包。真正有效的保活手段有两种一是每次使用Session前先主动关闭空闲连接session.close() session requests.Session()但如果频繁重建Session连接池复用的意义就没了。二是用urllib3的底层层面对连接做验证from urllib3.poolmanager import PoolManager from urllib3.util.retry import Retry class CustomPoolManager(PoolManager): def _new_pool(self, scheme, host, port, request_contextNone): pool super()._new_pool(scheme, host, port, request_context) # 设置连接池中的连接在取用时自动探测有效性 pool.conn_pool_maxsize 10 return pool5.3 服务端视角不主动断连接但检测到死连接怎么办如果你的角色是后端服务提供方想减少客户端卡死现象可以在服务端配置TCPKeepAlive让系统自动定时发送探测包检测连接是否真的活着。Linux下可以配置net.ipv4.tcp_keepalive_time参数一般设置在600秒以上比较合理。但注意TCP KeepAlive默认间隔太长对部分场景不够敏感。更实用的做法是设计一个健康检查端点比如GET /healthz让客户端定时探测。这个优势是可以结合业务层状态判断服务端挂了没挂一目了然。5.4 设置socket层的TCP保活参数requests本身没有直接暴露socket的保活参数但可以通过requests的Session和底层socket的继承做一个“土办法”在Session初始化时创建一个自定义socket工厂把TCP_KEEPALIVE打开。import socket import requests from urllib3.connection import HTTPConnection class KeepAliveHTTPConnection(HTTPConnection): def _new_conn(self): conn super()._new_conn() # 开启TCP KeepAlive并设置参数 conn.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # 以下两个选项在Linux下有效 try: conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) except OSError: pass return conn将自定义的连接类适配到requests中需要继承HTTPAdapter并覆盖init_poolmanagerfrom requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager class KeepAliveAdapter(HTTPAdapter): def init_poolmanager(self, connections, maxsize, blockFalse, **pool_kwargs): pool_kwargs[connection_class] KeepAliveHTTPConnection self.poolmanager PoolManager( num_poolsconnections, maxsizemaxsize, blockblock, **pool_kwargs )这个方式适合长连接存活时间敏感的场景比如需要维持WebSocket长连接或高频接口轮询的场景。但要注意并不是所有系统平台都支持设置TCP_KEEPIDLE等选项所以代码里要加try except兜底。6. 场景驱动方案库不同领域的具体解法6.1 爬虫采集场景弱网环境的生存策略爬虫场景最典型的特点是请求量大、目标多变、网络环境不可控。这种情况下卡死几乎必然发生只是时间问题。我的策略是“三管齐下”每个请求都设置超时连接3秒读取10秒。配置重试机制最多3次退避系数0.5。引入失败队列重试还失败的任务写进失败消息队列或本地文件后续单独补跑。我写过一套针对爬虫的辅助函数核心逻辑是这样的import requests import time from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def create_session_with_retries(): session requests.Session() retry Retry( total3, connect3, read2, status_forcelist[500, 502, 503, 504], backoff_factor0.5, allowed_methods[GET, POST, PUT, DELETE], ) adapter HTTPAdapter(max_retriesretry, pool_connections20, pool_maxsize20) session.mount(http://, adapter) session.mount(https://, adapter) return session def robust_request(method, url, **kwargs): session create_session_with_retries() kwargs.setdefault(timeout, (3.05, 10)) try: resp session.request(method, url, **kwargs) resp.raise_for_status() return resp except requests.exceptions.RequestException as e: # 记录日志放入失败队列 log_failure(url, e) raise这个辅助函数里有几个细节值得讲backoff_factor0.5意味着第1次重试等0.5秒第2次等1秒第3次等2秒既不会太频繁也不会等太久。status_forcelist里加了500和503这类状态码是因为这些错误通常表示服务器端负荷过重等一会儿重试成功率更高。每次新建Session代价不高但如果并发很大建议把Session对象作为一个线程的局部变量复用不要频繁创建。6.2 微服务调用场景超时与熔断必须落到全局在微服务架构里requests卡死的影响会被放大无数倍。某个依赖服务挂了如果调用方没有配置超时那么所有调用这个服务的线程都会卡住最终导致整条调用链被拖垮。我参与过的某公司内部服务早期在调用一个外部支付接口时没设超时结果支付系统升级维护导致几十个线程全部卡死业务投诉电话都被打爆了。后来整改之后所有外部HTTP调用强制要求超时配置并且引入了信号量隔离和熔断机制信号量隔离限制同时等待外部服务的最大请求数超出后直接快速失败。熔断机制连续失败超过阈值后直接短路不再发起真实请求。超时回调超时后返回默认值或缓存结果不让异常穿透到上层业务。requests配合熔断框架可以用一个装饰器实现import requests from functools import wraps CIRCUIT_OPEN False FAILURE_COUNT 0 THRESHOLD 5 def circuit_breaker(func): wraps(func) def wrapper(*args, **kwargs): global CIRCUIT_OPEN, FAILURE_COUNT if CIRCUIT_OPEN: raise RuntimeError(熔断器已打开拒绝请求) try: result func(*args, **kwargs) FAILURE_COUNT 0 return result except Exception: FAILURE_COUNT 1 if FAILURE_COUNT THRESHOLD: CIRCUIT_OPEN True raise return wrapper circuit_breaker def call_external_api(): resp requests.get(https://external-api.example.com/data, timeout(3, 8)) return resp.json()这个例子只是演示思路生产级的熔断逻辑会更加复杂比如要加时间窗口、半开状态检测等。但核心思想是一致的不要让外部请求无限期占用自己的线程资源。6.3 桌面工具/脚本场景用户体验优先如果你写的是带界面的工具比如某办公自动化小工具用requests请求后端服务。这时候卡死造成的影响不只是程序挂起而是整个界面卡住用户只能强杀进程。这种场景要注意两点请求一定要放在子线程不能在UI主线程里同步执行。即使设了超时程序也要能优雅地把超时信息显示给用户。如果用的是PyQt或Tkinter建议用QThread或线程信号/事件机制。请求过程的伪代码大体是import threading import requests class ApiWorker(threading.Thread): def __init__(self, url, callback, error_callback): super().__init__() self.url url self.callback callback self.error_callback error_callback self.daemon True def run(self): try: resp requests.get(self.url, timeout(3, 10)) self.callback(resp.json()) except Exception as e: self.error_callback(str(e))UI层启动这个线程后立即显示“加载中”的提示等回调函数刷新界面。这样即使请求被卡住用户也只是看到加载中状态不会误以为软件崩溃。7. 常见问题速查与解决方案对照表排查问题的时候按表格对照一轮效率会高出不少。这里把我实际工作中遇到频率最高的几种情况整理一下7.1 症状与解决对照表症状特征可能原因解决方案所有请求都卡死不报错未配置超时且目标不可达添加timeout参数连接3秒读取10秒部分URL卡死其他正常目标服务器对特定地域/IP段限流换代理、换出口IP、降低并发频率偶发卡死持续几十秒后恢复目标服务器防火墙静默丢弃空闲连接连接池保活、开启TCP KeepAlive多线程下集体卡死线程池任务无超时控制或连接池过小future加timeout调大pool_maxsize切换网络后卡死NAT设备删除TCP连接状态连接池中毒定期重建Session开启TCP保活参数开机第一次请求特别慢系统DNS缓存未预热或代理检测慢预热DNS关闭系统代理自动检测服务器返回500后卡住重试不完等待退避时间过长设置重试总数上限打开熔断HTTPS请求卡在TLS握手证书无法验证、TLS版本不兼容检查证书链、调整TLS版本7.2 需要记住的几个底层参数建议值参数建议值备注connect timeout3.05秒避免socket层取整导致低于3秒read timeout10-30秒取决于业务接口的耗时普通接口10秒够用retry total3次过多重试会放大请求量backoff_factor0.5或1指数退避的基础系数pool_connections10-20支持的不同host数量pool_maxsize10-20单个host的最大并发连接数max_workersCPU核心数x4或x5线程池的线程数量7.3 排查工具清单附使用场景curl -v最基础的HTTP请求调试能看到每个阶段的耗时。curl -w可以自定义输出TCP连接时间、TLS握手时间、总时间等。telnet / nc验证端口连通性。nslookup / dig检查DNS解析。Wireshark抓包看数据包交互定位是TCP层还是HTTP层的问题。py-spyPython程序的采样分析工具可以看线程卡在哪个函数里。py-spy是个好东西强烈建议收藏。当你的Python程序卡死但不知道怎么排查时跑一句py-spy dump --pid PID就能看到当前所有线程的调用栈瞬间定位是哪一行卡住了。这个工具在线上环境救过我太多次。8. 兜底方案设计与监控策略8.1 让程序在“卡死”时自动自救无论配置得多完善分布式系统和公网环境里总有一些情况是你预料不到的。所以必须设计兜底方案。最实用的兜底方案是“看门狗”机制用一个单独的线程定期检查任务线程的状态如果发现某个线程长时间没有响应就强制终结或者放弃等待。import threading import time def watchdog(target_func, timeout60): result {} def target(): try: result[data] target_func() except Exception as e: result[error] e t threading.Thread(targettarget) t.daemon True t.start() t.join(timeout) if t.is_alive(): # 超时仍然没有结束 raise TimeoutError(看门狗超时线程仍然活着) return result这个方法适用于那种“线程死不死不知道但确实现象上像卡死”的场景。但注意Python的线程无法真正被kill掉daemon线程在主进程退出时会自动结束但如果在主进程里join(timeout)后线程还在跑基本只能放弃等待结果让主流程先行处理。更保底的做法是引入消息队列和异步任务框架。比如把任务提交到Celery这类分布式任务队列中由队列实现重试、超时和结果回收。即使某个worker卡死了另一个worker可以重新领取任务不会影响主流程。8.2 结构化日志记录所有异常操作requests时日志一定要结构化不能只print异常字符串。每条日志至少要包含时间戳目标URL异常类型异常消息触发阶段连接超时/读超时/SSLError/ConnectionError重试次数请求方向出站/入站一个典型的日志记录函数长这样import logging import json logger logging.getLogger(requests_monitor) logger.setLevel(logging.INFO) fh logging.FileHandler(requests.log) formatter logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) fh.setFormatter(formatter) logger.addHandler(fh) def log_request_failure(url, exc, stage, retries): log_data { url: url, exception: type(exc).__name__, message: str(exc), stage: stage, retries: retries, } logger.error(json.dumps(log_data))有了这些日志后续做告警和数据分析就有据可依了。8.3 监控指标与告警阈值我给自己的服务定了一套指标简单有效出站HTTP请求成功率低于99.5%触发告警。平均响应时间超过2秒触发告警。P99响应时间超过5秒触发告警。请求超时率超过0.5%触发告警。卡死线程数任何线程阻塞超过5分钟即告警。这些指标可以通过Prometheus或自研监控系统收集。指标的意义不是追求好看的数据而是能在卡死开始影响业务之前就被发现。9. 写给新手的最终建议盘了这么多方案和参数最后说点实际的。无论你使用的是requests还是httpx被“卡死”问题缠身的核心原因往往就那么几个没设超时、没配重试、连接池配置不合理、并发模型设计粗糙。把这几件事做对卡死问题基本就能解决九成以上。如果程序还卡多半是底层网络环境的问题那就必须借助抓包工具去看了。不要觉得抓包是运维的活作为一个开发者对TCP连接状态、DNS解析过程、TLS握手流程有基本的感知能力是排查这类问题的前提。我个人最后的小习惯是写任何requests调用的代码时第一步就写超时配置然后写重试配置等这两样都有了才会开始写业务逻辑。这就像开车前先系安全带——多一道保险关键时候能救命。