Gunicorn多线程Python应用调用GCP云函数的SSL连接异常问题
我在App Engine上部署了一个Python应用,使用Gunicorn的gthread worker启动,启动命令为:
gunicorn -w 4 --threads 16 src.main:app
应用中通过requests.post(url, json=payload)调用GCP云函数,但通过Bugsnag报错持续出现以下SSL错误:
HTTPSConnectionPool(host='GCP CLOUD FUNCTION', port=443): Max retries exceeded with url: /path (Caused by SSLError(SSLZeroReturnError(6, 'TLS/SSL connection has been closed (EOF) (_ssl.c:997)')))
当前环境:Python 3.11、requests 2.28.2、urllib3 1.26.15
已尝试的方案:
- 起初认为是证书问题,设置
verify=False后错误消失,但该问题也出现在Bugsnag、Google Cloud Storage等无法禁用该参数的库中 - 降低应用每分钟调用频率后问题仍存在
- 更新Python及依赖也未解决
- 移除Gunicorn启动命令中的
--threads参数,错误不再出现
疑问:
- Python requests是否支持多线程环境?此前查询得知只要不跨线程复用requests.Session即可,但我并未这么做,难道requests底层使用了全局Session?
- 如何在保留多线程的前提下解决该SSL异常问题?
1. Requests对多线程的支持
Requests本身支持多线程环境,核心规则确实是不要在多个线程间复用同一个requests.Session实例。但你遇到的问题,根源在于直接调用requests.post这类无Session的请求时,底层会使用一个全局默认Session——这个Session是线程不安全的,它会在所有线程间共享连接池,当多线程并发请求时,很容易出现连接被意外关闭、SSL握手失败等冲突,这就是你看到SSLZeroReturnError的原因。
2. 保留多线程的解决方案
(1)为每个线程/请求创建独立Session
不要再直接用requests.post,而是为每个线程或者每个Web请求创建专属的requests.Session,确保每个线程的连接池独立:
# 示例:在线程逻辑内创建专属Session def process_request(url, payload): with requests.Session() as session: response = session.post(url, json=payload) # 处理响应逻辑
如果是在Flask/Django这类Web框架中,可以在请求上下文内初始化Session,保证每个请求对应独立的连接池,避免线程间冲突。
(2)调整urllib3连接池配置
Requests依赖urllib3的连接池,你可以通过自定义HTTPAdapter来调整连接池参数,减少复用冲突:
from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry # 创建Session并配置适配器 session = requests.Session() # 限制连接池大小,搭配重试机制 adapter = HTTPAdapter( pool_connections=4, pool_maxsize=8, max_retries=Retry(total=3, backoff_factor=0.1) ) session.mount('https://', adapter)
如果并发量不高,也可以直接设置pool_connections=1和pool_maxsize=1,强制每次请求新建连接,彻底避免连接复用冲突(但会牺牲一定性能)。
(3)升级urllib3版本
你当前使用的urllib3 1.26.15在多线程连接池管理上存在一些已知问题,建议升级到urllib3 1.26系列的最新版,或者升级Requests到2.31.0+后使用urllib3 2.x版本——新版本对多线程环境下的连接池竞争做了优化,能有效减少这类SSL连接异常。
(4)适当降低Gunicorn线程数
如果上述方法效果有限,可以尝试降低--threads参数值(比如从16调整到8),减少线程并发量,降低连接池的竞争压力,也能缓解SSL连接关闭的问题。
内容的提问来源于stack exchange,提问作者Gammel

