用 ThreadPoolExecutor 给 socket.getaddrinfo 加超时与并发上限

举报
yd_240403232 发表于 2026/10/28 01:42:04 2026/10/28
【摘要】 介绍一个可复用的 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 均为占位值,应按实际部署调整。

边界与限制

  1. 超时不是硬取消。调用方收到 DnsTimeoutError 后,运行中的 getaddrinfo 仍可能继续占用一个工作线程,直到系统调用返回。固定线程池能限制被占用的线程上限,但不能让已卡住的线程凭空恢复。若业务必须强杀解析,需要进程隔离或使用带硬超时能力的解析组件。
  2. 池满时快速失败。queue_wait 是等待空闲槽位的时间,timeout 是任务提交后的等待时间,调用方最坏等待接近两者之和。
  3. 超时后不要立即释放槽位。任务仍在运行时释放槽位会低估在途并发,可能让线程池继续接收新任务。示例通过 done callback 在任务真正结束后释放。
  4. 不要为每个请求创建解析器。应全局复用一个实例,并在进程退出时调用 close()。
  5. 线程池实例不适合在 fork 后跨进程复用。多进程服务应在子进程内各自创建。
  6. 在异步框架中直接调用同步 resolve 会阻塞事件循环;应把整个调用放到框架提供的线程执行器中,并注意不要造成线程池叠加。
  7. getaddrinfo 的返回顺序受系统配置影响;需要固定 IPv4 或 IPv6 时,可通过 family 参数限制。

该方案适合“同步调用、可接受软超时、需要限制并发”的场景;对强实时或必须可中断的解析需求,应评估进程级隔离或托管解析服务。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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