用 ThreadPoolExecutor 给 socket.getaddrinfo 加超时与并发上限
【摘要】 介绍一个可复用的 Python 定时 DNS 解析封装:用 ThreadPoolExecutor 执行 socket.getaddrinfo,以 Future 超时控制调用方等待,并用信号量限制在途任务,避免慢解析无界占用线程。给出最小可运行示例与适用边界。
问题场景
服务端代码直接调用 socket.getaddrinfo 时,解析耗时由系统解析器和网络环境决定。如果每次请求都新建线程或线程池,慢解析会让线程数量无界增长;如果共用一个不限队列的线程池,future.result(timeout=...) 只能让调用方提前返回,后台执行 getaddrinfo 的线程仍可能被占住。
本文给出一个同步封装:解析调用有超时,线程池大小固定,并用信号量限制已提交但未结束的任务数,池满时快速失败而不是无限排队。
设计要点
ThreadPoolExecutor(max_workers=N)限制同时执行解析的线程数。Future.result(timeout=T)控制调用方等待时间。BoundedSemaphore(N)限制在途任务总数;获取不到槽位时抛出 busy 异常。- 超时后
cancel()只对尚未开始的任务有效;运行中的getaddrinfo不能被 Python 强行终止,槽位要等任务真正结束后由回调释放。
最小可运行示例
import socket
import threading
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
class DnsBusyError(RuntimeError):
pass
class DnsTimeoutError(TimeoutError):
pass
class TimedDnsResolver:
def __init__(self, max_workers: int = 4, queue_wait: float = 0.2):
if max_workers < 1:
raise ValueError('max_workers must be >= 1')
self._pool = ThreadPoolExecutor(
max_workers=max_workers,
thread_name_prefix='dns-resolve',
)
self._slots = threading.BoundedSemaphore(max_workers)
self._queue_wait = queue_wait
self._closed = False
@staticmethod
def _getaddrinfo(host, port, family, socktype, proto):
return socket.getaddrinfo(
host,
port,
family=family,
type=socktype,
proto=proto,
)
def resolve(
self,
host: str,
port: int = 0,
*,
family: int = socket.AF_UNSPEC,
socktype: int = socket.SOCK_STREAM,
proto: int = 0,
timeout: float = 2.0,
):
if self._closed:
raise RuntimeError('resolver is closed')
if not self._slots.acquire(timeout=self._queue_wait):
raise DnsBusyError('dns resolver is busy')
fut = None
try:
fut = self._pool.submit(
self._getaddrinfo, host, port, family, socktype, proto
)
fut.add_done_callback(lambda _f: self._slots.release())
return fut.result(timeout=timeout)
except FutureTimeoutError:
if fut is not None:
fut.cancel()
raise DnsTimeoutError(f'dns resolve timeout: {host!r}') from None
except BaseException:
if fut is None:
self._slots.release()
raise
def close(self):
self._closed = True
self._pool.shutdown(wait=False)
if __name__ == '__main__':
resolver = TimedDnsResolver(max_workers=4, queue_wait=0.2)
try:
for host in ('localhost', 'example.com'):
try:
infos = resolver.resolve(host, 443, timeout=1.5)
print(host, '->', infos[0][4])
except (DnsTimeoutError, DnsBusyError, OSError) as exc:
print(host, 'failed:', type(exc).__name__, exc)
finally:
resolver.close()
运行方法
将代码保存为 timed_dns.py,执行:
python timed_dns.py
程序会打印解析到的地址,或打印超时、池忙、解析失败等原因。不同操作系统的地址顺序和错误文本可能不同。示例中的 max_workers=4、queue_wait=0.2、timeout=1.5 均为占位值,应按实际部署调整。
边界与限制
- 超时不是硬取消。调用方收到
DnsTimeoutError后,运行中的getaddrinfo仍可能继续占用一个工作线程,直到系统调用返回。固定线程池能限制被占用的线程上限,但不能让已卡住的线程凭空恢复。若业务必须强杀解析,需要进程隔离或使用带硬超时能力的解析组件。 - 池满时快速失败。
queue_wait是等待空闲槽位的时间,timeout是任务提交后的等待时间,调用方最坏等待接近两者之和。 - 超时后不要立即释放槽位。任务仍在运行时释放槽位会低估在途并发,可能让线程池继续接收新任务。示例通过 done callback 在任务真正结束后释放。
- 不要为每个请求创建解析器。应全局复用一个实例,并在进程退出时调用
close()。 - 线程池实例不适合在 fork 后跨进程复用。多进程服务应在子进程内各自创建。
- 在异步框架中直接调用同步
resolve会阻塞事件循环;应把整个调用放到框架提供的线程执行器中,并注意不要造成线程池叠加。 getaddrinfo的返回顺序受系统配置影响;需要固定 IPv4 或 IPv6 时,可通过family参数限制。
该方案适合“同步调用、可接受软超时、需要限制并发”的场景;对强实时或必须可中断的解析需求,应评估进程级隔离或托管解析服务。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)