高并发 RAG 服务的负载均衡策略:轮询、最少连接与一致性哈希怎么选

高并发 RAG 服务的负载均衡策略:轮询、最少连接与一致性哈希怎么选 高并发 RAG 服务的负载均衡策略轮询、最少连接与一致性哈希怎么选一、深度引言与场景痛点RAG 服务拆成了三个实例embedding 服务、向量搜索服务、LLM API 代理。每个实例部署了 4 个 Pod。上游请求进来负载均衡器要把流量分到具体的 Pod 上。轮询最简单、最少连接最公平、一致性哈希最适合有状态服务。但你选哪个如果你的向量搜索 Pod 的响应时间差异很大——有的 Pod 因为内存索引大而慢了 200ms轮询会把请求均匀撒到快 Pod 和慢 Pod 上快的闲着慢的排队。最少连接会优先发给负载低的 Pod效果更好。但如果你的请求需要命中同一 Pod 的缓存一致性哈希保证相同查询落到同一个 Pod。每种策略都解决一个问题同时带来一个新问题。选择的本质是权衡你愿意牺牲什么来换取什么。二、底层机制与原理深度剖析四种策略里最少连接数在 RAG 场景下适用性最广。因为 embedding 和 LLM API 的响应时间差异很大同一模型、同一批 GPU 也可能因为批次大小的不同导致延迟抖动。最少连接数能自动回避慢的实例。一致性哈希的应用场景更专一你的向量搜索服务有本地缓存缓存了最近查询的 Top-K 结果。把同样的查询路由到同一个 Pod缓存命中率高。代价是扩容或缩容时哈希环需要重新平衡缓存失效率突然上升。加权轮询是个折中给性能好的 Pod 设更高权重让它们承担更多流量。权重可以人工配置也可以根据实例的 CPU/内存使用率自动调整。这对于混合部署场景部分 Pod 跑在 GPU 节点上、部分在 CPU 节点上特别有用。三、生产级代码实现from __future__ import annotations import asyncio import hashlib import random import time from dataclasses import dataclass, field from typing import Optional, Protocol class BackendProtocol(Protocol): 后端服务接口 host: str port: int async def health_check(self) - bool: ... async def request(self, *args, **kwargs): ... dataclass class Backend: host: str port: int weight: int 1 active_connections: int 0 last_health: bool True response_times: list[float] field(default_factorylist) property def avg_response_ms(self) - float: if not self.response_times: return 0 return sum(self.response_times[-20:]) / min(len(self.response_times), 20) property def health_score(self) - float: 健康分数: 越健康分数越低最少连接优先选择 if not self.last_health: return float(inf) return self.active_connections * self.avg_response_ms class LoadBalancer: 自适应负载均衡器 def __init__( self, backends: list[Backend], strategy: str least_connections, health_check_interval: float 5.0, ): self._backends backends self._strategy strategy self._check_interval health_check_interval self._rr_index 0 self._ring: dict[int, Backend] {} # for consistent hashing async def start_health_check(self) - None: while True: for backend in self._backends: try: backend.last_health await backend.health_check() except Exception: backend.last_health False await asyncio.sleep(self._check_interval) def get_backend( self, key: Optional[str] None ) - Optional[Backend]: healthy [b for b in self._backends if b.last_health] if not healthy: return None if self._strategy round_robin: return self._round_robin(healthy) elif self._strategy least_connections: return self._least_connections(healthy) elif self._strategy weighted_round_robin: return self._weighted_rr(healthy) elif self._strategy consistent_hash: if key is None: return random.choice(healthy) return self._consistent_hash(key, healthy) else: return random.choice(healthy) def _round_robin(self, backends: list[Backend]) - Backend: self._rr_index (self._rr_index 1) % len(backends) return backends[self._rr_index] def _least_connections(self, backends: list[Backend]) - Backend: return min(backends, keylambda b: b.active_connections) def _weighted_rr(self, backends: list[Backend]) - Backend: total_weight sum(b.weight for b in backends) if total_weight 0: return random.choice(backends) rand random.randint(0, total_weight - 1) cumulative 0 for b in backends: cumulative b.weight if rand cumulative: return b return backends[-1] def _consistent_hash( self, key: str, backends: list[Backend] ) - Backend: 一致性哈希: 用 MD5 取模路由 hash_val int(hashlib.md5(key.encode()).hexdigest(), 16) # 简化实现: 对虚拟节点取模 sorted_nodes sorted( [(hash(b.host str(i)) % (2**32), b) for b in backends for i in range(150)], # 每个实际节点 150 个虚拟节点 keylambda x: x[0], ) for point, backend in sorted_nodes: if hash_val point: return backend return sorted_nodes[0][1] async def execute(self, fn, key: Optional[str] None, *args, **kwargs): 在选中的后端上执行请求并跟踪连接数 backend self.get_backend(key) if backend is None: raise RuntimeError(没有健康的可用后端) backend.active_connections 1 start time.monotonic() try: async with asyncio.timeout(10.0): return await fn(backend, *args, **kwargs) finally: backend.active_connections - 1 backend.response_times.append((time.monotonic() - start) * 1000) if len(backend.response_times) 100: backend.response_times backend.response_times[-50:]Backend数据类同时跟踪了连接数、健康状态和响应时间。health_score把连接数和平均响应时间综合成一个分数用来做智能选路——不是纯看连接数而是看负担的总量。一致性哈希实现用了虚拟节点Virtual Node。每个实际节点生成 150 个虚拟节点分布在哈希环上这样扩容或缩容时缓存失效的比例接近于M/NM 是变化的节点数N 是总节点数而不是全部失效。150 个虚拟节点已经足够均匀再多会增加查找开销。execute方法是统一入口。它在执行前后自动更新连接计数和响应时间对调用方透明。超时设为 10 秒超过则触发asyncio.TimeoutError。四、边界分析与架构权衡Kubernetes 的 HPA 感知到流量后会自动扩缩新 Pod 从启动到 Ready 有 15~30 秒的延迟。在这期间负载均衡器的健康检查可能还没发现新 Pod流量仍然压在旧的 Pod 上。建议给新 Pod 设置一个预热期健康检查通过后先用 1/4 的权重接收流量30 秒后升到 1/260 秒后升到满权重。避免新 Pod 一上来就被打崩——冷启动阶段的 JIT 编译、连接池初始化和缓存未命中会让新 Pod 前几十秒的处理能力远低于稳态。另一个挑战是最少连接数策略下所有请求都倾向于选空的实例。如果一个实例短暂断连后恢复它的连接数是 0瞬间所有请求涌向它又把它打崩——这就是羊群效应。解决方案是连接数加一个最小初始值如 5让恢复的实例逐渐被选中而不是一瞬间被选中所有请求。五、总结RAG 服务的负载均衡策略按场景选无状态的嵌入和 LLM 调用用最少连接数有本地缓存的向量搜索用一致性哈希有 GPU/CPU 混部的用加权轮询。落地优先级先用最少连接数应对大多数场景需要缓存命中率的加一层一致性哈希最后考虑扩缩容时的预热和防羊群。健康检查是底座任何策略在实例不健康时都会跳过。