多 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 启动流程
# 一键启动多 Worker + Routermineru-router --workers 4 --port-base 8888等价于:
# 手动启动 4 个 WorkerCUDA_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 worker3.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] -= 14. 健康检查
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 完全一致的接口:
# 客户端代码无需修改# 原来连接单个 APIclient = 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 吞吐量对比
| 部署方式 | 单任务延迟 | 并发吞吐量 |
|---|---|---|
| 单 GPU | 30s/文档 | 3 文档/分钟 |
| 2 GPU | 30s/文档 | 6 文档/分钟 |
| 4 GPU | 30s/文档 | 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 多卡部署