跳转到内容

多 GPU 路由与负载均衡:mineru-router 的设计与实践

多 GPU 路由与负载均衡:mineru-router 的设计与实践

学习目标

读完本章后,你将能够:

  • 理解 mineru-router 的架构设计
  • 掌握多 GPU 部署的拓扑结构
  • 配置负载均衡策略
  • 评估单实例 vs 多实例部署的选择

前置知识


1. 部署拓扑演进

1.1 单 GPU 部署

1.2 多 GPU 部署(mineru-router)

核心优势

  • 单一入口:客户端只需连接 router
  • 自动负载均衡:任务自动分配到空闲 GPU
  • 接口兼容:router 与 api 接口完全一致

2. mineru-router 架构

2.1 整体架构

2.2 启动流程

Terminal window
# 一键启动多 Worker + Router
mineru-router --workers 4 --port-base 8888

等价于:

Terminal window
# 手动启动 4 个 Worker
CUDA_VISIBLE_DEVICES=0 mineru-api --port 8888 &
CUDA_VISIBLE_DEVICES=1 mineru-api --port 8889 &
CUDA_VISIBLE_DEVICES=2 mineru-api --port 8890 &
CUDA_VISIBLE_DEVICES=3 mineru-api --port 8891 &
# 启动 Router(负载均衡代理)
mineru-router --workers 127.0.0.1:8888,127.0.0.1:8889,...

3. 负载均衡策略

3.1 轮询策略

class RoundRobinLoadBalancer:
def __init__(self, workers: list[str]):
self.workers = workers
self._index = 0
self._lock = threading.Lock()
def get_next_worker(self) -> str:
with self._lock:
worker = self.workers[self._index]
self._index = (self._index + 1) % len(self.workers)
return worker

3.2 最少连接策略

class LeastConnectionsLoadBalancer:
def __init__(self, workers: list[str]):
self.workers = workers
self._connections = {w: 0 for w in workers}
self._lock = threading.Lock()
def get_next_worker(self) -> str:
with self._lock:
# 选择当前连接数最少的 Worker
return min(self._connections, key=self._connections.get)
def notify_start(self, worker: str):
with self._lock:
self._connections[worker] += 1
def notify_end(self, worker: str):
with self._lock:
self._connections[worker] -= 1

4. 健康检查

Router 定期检查 Worker 的健康状态:

async def health_check(worker_url: str) -> bool:
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(f"{worker_url}/health")
return resp.status_code == 200
except Exception:
return False
async def monitor_workers(workers: list[str]):
while True:
for worker in workers:
healthy = await health_check(worker)
if not healthy:
mark_worker_unhealthy(worker)
await asyncio.sleep(30) # 每 30 秒检查一次

不健康 Worker 处理

  • 从负载均衡池中移除
  • 继续健康检查,恢复后自动加入

5. 客户端透明

Router 对外暴露与 mineru-api 完全一致的接口:

# 客户端代码无需修改
# 原来连接单个 API
client = httpx.AsyncClient(base_url="http://localhost:8888")
# 现在连接 Router,代码不变
client = httpx.AsyncClient(base_url="http://localhost:8000")
# 提交任务的代码完全一致
response = await client.post("/tasks", files={"file": file})

6. 性能评估

6.1 吞吐量对比

部署方式单任务延迟并发吞吐量
单 GPU30s/文档3 文档/分钟
2 GPU30s/文档6 文档/分钟
4 GPU30s/文档12 文档/分钟

线性加速:理论上 N 个 GPU 提供 N 倍吞吐量(实际约 0.8-0.9N)

6.2 资源利用率

7. 陷阱与对策

陷阱对策
Worker 端口冲突使用 --port-base 自动递增分配
GPU 显存不均衡确保每个 Worker 绑定独立 GPU
Router 成为瓶颈Router 仅做代理转发,计算开销极小
Worker 崩溃未恢复健康检查自动检测,手动重启 Worker
跨机器部署Router 支持远程 Worker URL

8. 设计权衡

Router 代理 vs 直连 Worker

方案优势劣势
Router 代理统一入口,自动负载均衡多一层网络跳转(延迟增加 < 1ms)
客户端直连零额外延迟客户端需实现负载均衡逻辑

取舍:Router 方案将负载均衡逻辑集中在服务端,客户端无需关心部署细节,符合关注点分离原则。


参考来源

  • MinerU 源码:mineru/cli/router.py
  • MinerU CHANGELOG v3.0:mineru-router 多卡部署