当 GPU、TPU 容量散落在不同地区时,问题不只是“如何把请求发过去”,而是如何避免某个集群的显存已经塞满、另一个集群的加速器却仍在空转。一次横跨美国和欧洲、覆盖 17,000 个计算节点的 GKE 部署表明:把全局流量分发和集群内的 LLM 感知调度分层处理,可以在三集群间获得接近线性的吞吐增长,同时把网关引入的吞吐开销控制在 1% 以内。
两层路由,各自解决不同尺度的问题
这套架构没有让一个全局负载均衡器包办所有决策,而是拆成两层:
- 多集群 GKE Inference Gateway 负责全局入口、跨区域分流和高可用。
- LLM-d router 负责集群内部更细粒度的、理解模型内存压力的调度。
测试环境包含三个 GKE 集群:us-east5、us-west8 和 europe-west4。其中配置集群保存路由配置,但不进入实际请求数据路径。客户端只看到一个全局虚拟 IP,不需要知道模型最终运行在哪个区域。
每个目标集群运行 Endpoint Picker Proxy(EPP)。EPP 读取推理引擎暴露的实时指标,并将其提供给负载均衡层。在这次部署中,核心信号是 KV-cache token 使用率:当主区域越过 40% 的 KV-cache 利用率阈值后,网关开始把溢出流量发送到下一个健康区域。
这与传统轮询有本质区别。轮询只能计算“每个后端收到了多少请求”,却看不到这些请求的实际代价:
- 一个短提示词和一个 800k token 上下文不能算作同等负载;
- 长时间生成可能持续占用内存带宽;
- KV cache 会逐步吞噬 HBM,导致引擎无法接纳新序列;
- 排队长度相同的两个集群,显存余量可能完全不同。
因此,LLM 路由的关键不是平均分配连接,而是把请求导向仍有调度空间的加速器。
基准结果说明了什么,又没有说明什么
此次测试中的扩容结果如下:
| 集群规模 | 请求吞吐 | Token 吞吐 | 成功率 |
|---|---|---|---|
| 1 个集群(us-east5) | 0.72 req/s | 2,898 tok/s | 99.87% |
| 2 个集群(增加 us-west8) | 1.40 req/s | 6,380 tok/s | 99.95% |
| 3 个集群(增加 europe-west4) | 2.10 req/s | 8,457 tok/s | 99.90% |
三集群的请求吞吐约为单集群的 2.92 倍,接近按容量线性增长。通过多集群网关访问时,系统保留了本地直连吞吐的 99.5%,也就是此次测试中的额外吞吐损失不到 1%。
但不能把这组数字解读成“跨洲调用没有成本”。吞吐、首 token 延迟和端到端延迟是三个不同指标。请求从美国东部溢出到欧洲后,网络往返时间、跨区域流量费用以及流式连接稳定性仍可能发生变化。这里证明的是:在给定的大规模生产负载和部署条件下,路由层本身没有成为明显的吞吐瓶颈。
还要注意,测试运行的是使用 SGLang 提供服务的 MoE 基础模型。实际收益会受到模型结构、批处理策略、输入输出长度分布、缓存命中率以及加速器类型影响,不能直接照搬为其他工作负载的容量承诺。
分布式模型只应把流量送给 leader
分布式推理与普通无状态 Web Deployment 还有一个重要差异:采用张量并行等多节点模式时,通常只有 rank-0 或 leader Pod 对外提供 API,worker Pod 不能被负载均衡器直接选中。
GKE 可以通过 Kubernetes Service 选择器和 LeaderWorkerSet(LWS)维护这种拓扑。下面是一个可以改造的最小 Service 示例。它假设 leader Pod 已经带有 app=llm-server 和 inference-role=leader 标签,并在 30000 端口监听;实际标签应替换成你的 LWS 或推理运行时生成的标签。
apiVersion: v1
kind: Service
metadata:
name: llm-leader
namespace: inference
spec:
selector:
app: llm-server
inference-role: leader
ports:
- name: http
port: 80
targetPort: 30000
type: ClusterIP
保存为 llm-leader-service.yaml 后执行:
kubectl apply -f llm-leader-service.yaml
# 确认 Service 只选中了 leader,而不是 worker
kubectl -n inference get pods \
-l app=llm-server,inference-role=leader \
-o wide
kubectl -n inference get endpointslices \
-l kubernetes.io/service-name=llm-leader \
-o yaml
检查 EndpointSlice 尤其重要:全局网关即使选择了正确区域,如果区域 Service 又把请求转发给不能接收 API 流量的 worker,仍会产生连接失败或超时。
用相同负载测量直连与全局入口
在自己的环境中,不要只看负载均衡器的请求计数。应该用同一批提示词分别压测区域直连地址和全局入口,至少记录成功率、请求吞吐、token 吞吐和延迟分位数。
下面的脚本只使用 Python 标准库。它假设推理服务提供 OpenAI 风格的 /v1/chat/completions JSON 接口;如果你的运行时使用其他协议,请修改 payload 和 token 统计字段。运行前准备好 endpoint、模型名和可选的 API key。
#!/usr/bin/env python3
import argparse
import concurrent.futures
import json
import os
import statistics
import time
import urllib.request
def percentile(values, p):
values = sorted(values)
if not values:
return 0.0
index = min(len(values) - 1, int((len(values) - 1) * p))
return values[index]
def send(url, model, prompt, timeout):
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
"max_tokens": 128,
"stream": False,
}
headers = {"Content-Type": "application/json"}
api_key = os.getenv("API_KEY")
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
request = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers=headers,
method="POST",
)
started = time.perf_counter()
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
result = json.loads(response.read())
latency = time.perf_counter() - started
usage = result.get("usage", {})
tokens = usage.get("total_tokens", 0)
return True, latency, tokens, ""
except Exception as exc:
return False, time.perf_counter() - started, 0, str(exc)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--url", required=True)
parser.add_argument("--model", required=True)
parser.add_argument("--requests", type=int, default=100)
parser.add_argument("--concurrency", type=int, default=10)
parser.add_argument("--timeout", type=int, default=600)
parser.add_argument("--prompt-file")
args = parser.parse_args()
prompt = "Explain why KV-cache pressure matters to LLM routing."
if args.prompt_file:
with open(args.prompt_file, "r", encoding="utf-8") as file:
prompt = file.read()
started = time.perf_counter()
with concurrent.futures.ThreadPoolExecutor(
max_workers=args.concurrency
) as executor:
futures = [
executor.submit(
send, args.url, args.model, prompt, args.timeout
)
for _ in range(args.requests)
]
results = [future.result() for future in futures]
elapsed = time.perf_counter() - started
successful = [item for item in results if item[0]]
latencies = [item[1] for item in successful]
total_tokens = sum(item[2] for item in successful)
errors = [item[3] for item in results if not item[0]]
report = {
"requests": args.requests,
"successful": len(successful),
"success_rate": len(successful) / args.requests,
"elapsed_seconds": round(elapsed, 3),
"request_throughput_rps": round(len(successful) / elapsed, 3),
"token_throughput_tps": round(total_tokens / elapsed, 3),
"latency_mean_seconds": round(statistics.mean(latencies), 3)
if latencies else None,
"latency_p50_seconds": round(percentile(latencies, 0.50), 3),
"latency_p95_seconds": round(percentile(latencies, 0.95), 3),
"sample_errors": errors[:3],
}
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()
例如,分别测试区域入口和全局入口:
export API_KEY='replace-me'
python3 global_probe.py \
--url 'https://regional.example.com/v1/chat/completions' \
--model 'your-model' \
--requests 300 \
--concurrency 30 \
--timeout 600
python3 global_probe.py \
--url 'https://global.example.com/v1/chat/completions' \
--model 'your-model' \
--requests 300 \
--concurrency 30 \
--timeout 600
为了让比较有效,两轮测试应使用相同提示词、并发度和输出上限,并在模型预热后重复多轮。若接口不返回 usage.total_tokens,脚本仍能计算请求吞吐,但 token 吞吐会是零,需要改为读取推理引擎的服务端指标。
上线前应补齐的边界条件
全局容量池很诱人,但生产设计至少还要回答以下问题:
- 信号是否可靠: KV-cache 指标的采样周期、传输延迟和异常值会不会造成错误分流?
- 是否设置回滞: 单一 40% 阈值可能在临界点引发区域间抖动,实践中可为溢出和恢复设置不同阈值。
- 缓存局部性如何处理: 把后续对话切到其他区域可能失去前缀缓存收益。会话粘性与负载均衡需要共同评估。
- 长连接是否被基础设施截断: LLM 请求可能持续数分钟,代理、负载均衡器和客户端的超时必须一致。
- 故障时能否真正接管: 不仅要模拟 Pod 故障,还应模拟整个区域不可用、指标停止更新和跨区域链路退化。
- 数据是否允许跨境: 提示词、生成内容和日志可能受到数据驻留与合规约束。
- 成本是否透明: 更高的 GPU 利用率可能伴随跨区域网络费用和更高尾延迟,应按每百万 token 的完整成本评估。
这套架构最值得借鉴的地方,并不是某一个固定阈值,而是分层决策:全球层选择仍有容量且健康的区域,本地层理解模型、KV cache 和 leader-worker 拓扑。对于被迫从多个区域拼接算力的团队,这种设计能让新增加速器更接近“加多少容量,就得到多少吞吐”,而不是继续制造昂贵的算力孤岛。