【Bug已解决】[Bug]: API Connection Error after concurrent API calls 解决方案
【Bug已解决】[Bug]: API Connection Error after concurrent API calls 解决方案一、现象长什么样用客户端高并发打 vLLM 的 OpenAI 兼容 API/v1/chat/completions或/v1/completions时并发量一上去就出现连接错误requests.exceptions.ConnectionError: (Connection aborted., BrokenPipeError(...))或aiohttp.client_exceptions.ClientOSError: [Errno 24] Too many open files或客户端侧httpx.ConnectError: [Errno 60] Operation timed out几个典型表征只在并发高时出现串行/低并发正常说明问题在连接数 / 文件描述符 / 端口复用这类资源上限而非单次请求逻辑。错误是连接层ConnectionError / BrokenPipe / Too many open files不是业务层服务端可能在高并发下用完了文件描述符fd、或没用 keep-alive 导致 TIME_WAIT 堆积、或事件循环被连接建立压垮。服务端可能伴随OSError: [Errno 24] Too many open files这是典型 fd 耗尽每个 TCP 连接占一个 fd并发数 × 每请求连接数超过系统ulimit -n。这不是模型问题而是API server 的连接/资源处理能力在高并发下被耗尽。下面给出定位与修复客户端连接池 服务端 fd/keep-alive 调优。二、背景HTTP 服务在高并发下的连接资源每个 TCP 连接占用一个文件描述符fd。Linux 默认ulimit -n可能是 1024vLLM 的 uvicorn/fastapi 进程若并发接几千连接fd 直接耗尽 →Too many open files→ 新连接被拒 / BrokenPipe。短连接无 keep-alive会留大量 TIME_WAIT每个请求建连断连端口/连接表堆积导致Cannot assign requested address或连接超时。客户端没用连接池每次请求新建requests.Session/ 新httpx.Client连接不复用fd 暴涨。vLLM 默认用 uvicorn 起服务并发连接上限受 fd 和系统 backlog 影响。修复分两端服务端调大ulimit -n、开 keep-alive、限制并发或靠队列排队而非无限制接连接客户端用连接池requests.Session/httpx.AsyncClient复用 限并发 指数退避重试。下面用可运行代码实现客户端连接池 服务端 fd 检测。三、根因拆成三条根因fd 耗尽服务端并发连接数超过进程 fd 上限默认 1024新连接accept失败 →Too many open files/ BrokenPipe。根因是服务端 fd 上限过低 短连接不回收。客户端不复用连接客户端每次请求新建连接没用 Session/Client连接数 请求数fd 在服务端和客户端同时暴涨。根因是缺少连接池 keep-alive 复用。无并发限制 / 重试风暴客户端无脑并发 失败后立刻重试连接数在重试中指数放大把服务端压垮。根因是缺少并发上限 退避重试。修复方向服务端调大 fd 开 keep-alive客户端用连接池复用 Semaphore限并发 指数退避重试。四、最小可运行复现下面复现客户端无连接池导致连接数暴涨的判定逻辑import threading class NaiveClient: 现状每次请求都建新连接连接不复用。 def __init__(self): self.open_connections 0 self._lock threading.Lock() def request(self): with self._lock: self.open_connections 1 # 每次新建 # ... 真实请求 ... with self._lock: self.open_connections - 1 def simulate_concurrent(client, n): threads [] def job(): # 串行化统计峰值朴素客户端峰值 ≈ 并发数 client.request() for _ in range(n): t threading.Thread(targetjob); t.start(); threads.append(t) for t in threads: t.join() c NaiveClient() # 若 n2000且服务端 fd 上限 1024峰值连接会撑爆 print(朴素客户端并发 2000 时服务端需承受约 2000 并发连接易超 fd 上限)真实场景里这 2000 并发连接会直接把服务端 fd 吃满。下面用连接池 限并发修复。五、解决方案第一层最小直接修复最小修复客户端用连接池复用 TCP 连接Semaphore限制并发 指数退避重试。import asyncio import httpx class PooledClient: 带连接池 并发限制 退避重试的客户端。 def __init__(self, base_url, max_concurrency64, max_connections64): self._sem asyncio.Semaphore(max_concurrency) # httpx.AsyncClient 内部维护连接池keep-alive 复用 self._client httpx.AsyncClient(base_urlbase_url, limitshttpx.Limits( max_connectionsmax_connections, max_keepalive_connectionsmax_connections), timeouthttpx.Timeout(60.0)) async def chat(self, payload, retries3): for attempt in range(retries): async with self._sem: # 限制并发 try: r await self._client.post(/v1/chat/completions, jsonpayload) r.raise_for_status() return r.json() except (httpx.ConnectError, httpx.TransportError) as e: if attempt retries - 1: raise # 指数退避避免重试风暴 await asyncio.sleep(0.5 * (2 ** attempt)) async def run_pooled(base_url, n): client PooledClient(base_url, max_concurrency64) tasks [client.chat({model: x, messages: []}) for _ in range(n)] # 并发被信号量限到 64连接池复用fd 稳定 return await asyncio.gather(*tasks, return_exceptionsTrue) # 用法 # asyncio.run(run_pooled(http://localhost:8000, 2000))这一层改动让客户端并发被限制、连接被复用服务端承受的并发连接数稳定在max_connections64而非 2000fd 不再耗尽。六、解决方案第二层结构化改进把服务端 fd / keep-alive 调优也做成结构化检测与配置两端配合客户端限并发 服务端提 fd 开 keep-alive。import resource import os def server_fd_guard(min_fd8192): 服务端启动期检测并尽量提高 fd 上限。 soft, hard resource.getrlimit(resource.RLIMIT_NOFILE) if soft min_fd: new_soft min(min_fd, hard) try: resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, hard)) except ValueError: print(f[warn] 无法提高到 {min_fd}当前软限 {soft}硬限 {hard}) cur_soft, _ resource.getrlimit(resource.RLIMIT_NOFILE) return cur_soft def report_uvicorn_tuning(): 给出 uvicorn 启动建议keep-alive 合理的 backlog。 return { keep_alive: 30, # 开 keep-alive复用连接 backlog: 2048, # 接受队列长度 # uvicorn 启动--limit-concurrency 按 GPU 数限制避免无限制接连接 limit_concurrency: 256, } # 用法服务端 main 入口最早调用 fd server_fd_guard(8192) print(服务端可用 fd 软限:, fd) print(uvicorn 建议:, report_uvicorn_tuning())server_fd_guard在服务端启动早期提高 fd 上限受系统硬限约束report_uvicorn_tuning给出 keep-alive 并发限制建议两端配合消掉高并发连接错误。七、解决方案第三层断言 / CI 守护并发连接错误最怕线上高并发才暴露。用断言守两条不变量def check_concurrency_invariants(base_url, n, max_conn): # 不变量 1客户端并发被信号量限制实际并发连接 max_conn # 用 httpx.Limits 保证连接池上限 client httpx.AsyncClient(base_urlbase_url, limitshttpx.Limits(max_connectionsmax_conn)) assert client._limits.max_connections max_conn # 不变量 2服务端 fd 软限必须 预期并发连接 soft, _ resource.getrlimit(resource.RLIMIT_NOFILE) assert soft max_conn, ffd 软限 {soft} 应 并发 {max_conn} return True def test_concurrency_safe(): import resource check_concurrency_invariants(http://x, 2000, max_conn64) print(OK: 并发连接不变量通过) if __name__ __main__: test_concurrency_safe()把test_concurrency_safe接进 CI用本地 mock server 或只做配置校验锁死客户端限并发 / 服务端 fd 足够。八、排查清单高并发打 vLLM API 报 Connection Error按序查看错误类型Too many open files→ fd 耗尽BrokenPipe→ 服务端在请求中途断了连接常因 fd 满后 accept 失败ConnectError/Timeout→ 连接建立被压垮或端口耗尽。客户端必须用连接池用requests.Session/httpx.AsyncClient内部 keep-alive千万不要每次请求新建 client。连接复用能把 fd 数降一个数量级。限制客户端并发用Semaphoreasyncio或线程池把并发控制在服务端能承受的范围如 64~256别无脑并发 2000。退避重试防风暴连接错误重试要用指数退避且限制重试次数避免失败→立刻重试→连接数翻倍的雪崩。服务端提 fd 上限ulimit -n 8192或写在启动脚本 / systemd 里server_fd_guard在启动期尽量提高软限。服务端开 keep-alive 限并发uvicorn 设--timeout-keep-alive 30复用连接必要时--limit-concurrency按 GPU 数限制避免无限制接连接把 fd 吃满。CI 接test_concurrency_safe校验客户端限并发 / 服务端 fd 足够高并发场景前先过这道闸。九、小结高并发打 vLLM API 报 Connection Error 的根因是连接资源fd / 连接数在高并发下被耗尽服务端 fd 上限过低 客户端不复用连接 无并发限制/重试风暴。三层修复第一层PooledClient用 httpx 连接池复用 TCP 连接 Semaphore限并发 指数退避重试把服务端承受的并发连接数稳定在池上限而非请求数第二层server_fd_guard服务端启动期提高 fd 软限report_uvicorn_tuning给出 keep-alive 并发限制建议两端配合第三层CI 断言守住客户端限并发 / 服务端 fd 足够高并发前先过闸。落实后vLLM API 在高并发下连接被复用、并发被限制、fd 足够不再因Too many open files/BrokenPipe拒绝连接。