从503错误到无忧请求:自动重试与代理切换的完美实现

引言

之前的文章中,提到了通过Requests 中的 Retry 类来实现自动重试的方法,感兴趣的读者可以去看一下。

文章中有一个读者提到了能否在重试的时候自动更换代理,研究了一下,可以实现。本篇文章分享一下实现方式。

image-20241118221818607

思路

Requests 底层依赖 urllib3,urllib3 提供了连接池,可以自动管理连接,连接会在下一次请求被复用。

因为我们的连接使用了代理,连接池中的连接在初次连接的时候会先连接远程代理服务器,然后通过代理服务器发送请求,获得响应。

为了在每次重试的时候能够切换代理,无论连接池中可以存放多少连接,连接被使用完后,就不能被放回连接池了,因为如果放回去的话下次复用还是用的已经连接了上一个代理的连接,这样的话就无法切换代理了。

所以我的方案是,在每次连接使用完之后就关闭连接,让连接池重新创建连接使用。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import logging

import requests
import urllib3
from requests.adapters import HTTPAdapter
from urllib3 import HTTPConnectionPool
from urllib3 import Retry
from urllib3.connectionpool import log, HTTPSConnectionPool
from urllib3.util import parse_url

# 开启 urllib3 的日志,以便于查看重试过程
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s-%(filename)s-%(funcName)s-[%(lineno)d]-%(levelname)s-%(message)s')
urllib3_logger = logging.getLogger('urllib3')
urllib3_logger.setLevel(logging.DEBUG)
urllib3.disable_warnings()


class MyHTTPConnectionPool(HTTPConnectionPool):
def _put_conn(self, conn) -> None:
if conn:
log.debug('MyHTTPConnectionPool close connection')
conn.close()


class MyHTTPSConnectionPool(HTTPSConnectionPool):

def _put_conn(self, conn) -> None:
if conn:
log.debug('MyHTTPSConnectionPool close connection')
conn.close()


urllib3.poolmanager.pool_classes_by_scheme = {"http": MyHTTPConnectionPool,
"https": MyHTTPSConnectionPool}


class MyRetry(Retry):
def increment(self, method=None, url=None, response=None, error=None, _pool=None, _stacktrace=None):
_pool.proxy = parse_url('http://127.0.0.1:8082')
_pool.conn_kw["proxy"] = _pool.proxy
_pool.host = _pool.proxy.host
_pool.port = _pool.proxy.port
log.debug(f'!!!increment!!!')
return super().increment(method, url, response, error, _pool, _stacktrace)

def is_retry(
self, method: str, status_code: int, has_retry_after: bool = False
) -> bool:
return True


session = requests.session()
session.mount('https://', HTTPAdapter(max_retries=MyRetry.from_int(2), pool_maxsize=0, pool_connections=0))
session.mount('http://', HTTPAdapter(max_retries=MyRetry.from_int(2), pool_maxsize=0, pool_connections=0))
print(session.adapters.get('https://'))
try:
print(session.get('http://httpbin.org/status/503', proxies={'http': 'http://127.0.0.1:8081'},
timeout=10, verify=False).text[:100])
except Exception as e:
print(e)

代码思路:

  1. 首先启用日志,方便后面分析代码执行流程。
  2. 继承两个连接池的类,覆盖释放连接方法,将将要释放到连接池的连接关闭。
  3. 将我们自己的连接池类覆盖官方的 urllib3.poolmanager.pool_classes_by_scheme 字典,在初始化连接池类的时候就会用我们自己的连接池类了。
  4. 覆盖 Retry 类的 increment 方法,更改连接池的代理参数,方便下一次建立连接的时候使用新的代理地址和端口,这里我本地起了两个代理服务器,端口分别是 8081 和 8082。
  5. 覆盖 is_retry 方法,这里是为了方便调试,永远返回 True,可以保证每次都重试,因为重试的条件不太好构建,所以这里返回 True 让它可以一直重试,便于验证逻辑。如果在正式使用的时候记得要去掉。

代码运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2024-11-18 22:41:15,839-retry.py-from_int-[286]-DEBUG-Converted retries value: 2 -> MyRetry(total=2, connect=None, read=None, redirect=None, status=None)
2024-11-18 22:41:15,839-retry.py-from_int-[286]-DEBUG-Converted retries value: 2 -> MyRetry(total=2, connect=None, read=None, redirect=None, status=None)
2024-11-18 22:41:15,844-connectionpool.py-_new_conn-[243]-DEBUG-Starting new HTTP connection (1): 127.0.0.1:8081
<requests.adapters.HTTPAdapter object at 0x107c1c640>
2024-11-18 22:41:17,021-connectionpool.py-_make_request-[546]-DEBUG-http://127.0.0.1:8081 "GET http://httpbin.org/status/503 HTTP/11" 503 0
2024-11-18 22:41:17,021-test7.py-increment-[44]-DEBUG-!!!increment!!!
2024-11-18 22:41:17,021-retry.py-increment-[521]-DEBUG-Incremented Retry for (url='http://httpbin.org/status/503'): MyRetry(total=1, connect=None, read=None, redirect=None, status=None)
2024-11-18 22:41:17,021-test7.py-_put_conn-[22]-DEBUG-MyHTTPConnectionPool close connection
2024-11-18 22:41:17,021-connectionpool.py-urlopen-[943]-DEBUG-Retry: http://httpbin.org/status/503
2024-11-18 22:41:17,021-connectionpool.py-_new_conn-[243]-DEBUG-Starting new HTTP connection (2): 127.0.0.1:8082
2024-11-18 22:41:17,820-connectionpool.py-_make_request-[546]-DEBUG-http://127.0.0.1:8082 "GET http://httpbin.org/status/503 HTTP/11" 503 0
2024-11-18 22:41:17,820-test7.py-increment-[44]-DEBUG-!!!increment!!!
2024-11-18 22:41:17,820-retry.py-increment-[521]-DEBUG-Incremented Retry for (url='http://httpbin.org/status/503'): MyRetry(total=0, connect=None, read=None, redirect=None, status=None)
2024-11-18 22:41:17,820-test7.py-_put_conn-[22]-DEBUG-MyHTTPConnectionPool close connection
2024-11-18 22:41:17,820-connectionpool.py-urlopen-[943]-DEBUG-Retry: http://httpbin.org/status/503
2024-11-18 22:41:17,820-connectionpool.py-_new_conn-[243]-DEBUG-Starting new HTTP connection (3): 127.0.0.1:8082
2024-11-18 22:41:18,548-connectionpool.py-_make_request-[546]-DEBUG-http://127.0.0.1:8082 "GET http://httpbin.org/status/503 HTTP/11" 503 0
2024-11-18 22:41:18,549-test7.py-increment-[44]-DEBUG-!!!increment!!!
2024-11-18 22:41:18,549-test7.py-_put_conn-[22]-DEBUG-MyHTTPConnectionPool close connection
MyHTTPConnectionPool(host='127.0.0.1', port=8082): Max retries exceeded with url: http://httpbin.org/status/503 (Caused by ResponseError('too many 503 error responses'))

从日志中可以看出,首先 starting new connection,新建了一个连接,连接到第一个代理服务器127.0.0.1:8081,然后网站返回 503 状态码,在经过 is_retry 方法判断后,打印了重试逻辑,进入到下一次请求,并且关闭了上一次连接。

重试原有的连接,重新走之前的逻辑,先新建一个连接到我们设定的 8082 的代理服务器,然后继续之前原有的流程,网站返回 503 状态码,经过判断后,打印重试逻辑,进入到下一次重试。以此类推,因为我只有两个代理服务器,所以后面的重试逻辑走的都是第二个代理服务器。

代理日志

查看下面的代理服务器日志,第一次请求到了 8081 这个代理服务器,返回 503 响应,看时间,后面的两个请求都请求到了 8082 的代理服务器上面,间隔时间很短,很快就重试完了。

image-20241118225034088

image-20241118225044328

总结

以上代码仅限于通过部分代码改动实现需求,还有很多不完善的地方,完全不能在生产环境使用,本篇文章只是提供一个思路,可以用最少的代码实现自动重试,并且在每次重试的时候切换代理服务器,提高开发效率,减少重复代码,让大家能早点下班哈哈。

对了,再提一句,覆盖 Retry 类的 increment 方法可以让我们根据响应或者错误信息来判断本次请求是否需要重试,可以通过覆盖 increment 方法来自定义重试逻辑,重试起来更加的方便。

从以上代码可以看出Requests 的代码具有高度模块化和可拓展性的优点,代码比较易于维护和理解。在需要其他功能时,可以通过继承并重写指定方法来实现我们自己的逻辑,轻松添加自定义的功能,而不需要改动原有的代码,这点还是值得好评的。

本文章首发于个人博客 LLLibra146’s blog
本文作者:LLLibra146
更多文章请关注:qrcode
版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!
本文链接
https://blog.d77.xyz/archives/undefined.html