服务架构:从单机到分布式#
ONNX:模型的中立语言 给了我们跨平台的模型文件,模型优化:量化与剪枝 让它又轻又快。现在的问题是:怎么让外部世界调用这个模型? 文件本身不会接 HTTP 请求,不会管理并发,不会做鉴权——它只是一个"大脑",需要一套"神经系统"来连接外部世界。
这一节讲解模型服务的核心架构知识。后半部分以 Ferrinx 为例展示这些概念的具体落地。
推理模式:同步 vs 异步#
模型服务的第一个架构决策就是推理模式——调用者是否需要原地等待结果?
同步推理#
同步推理:客户端发送请求后,HTTP 连接保持打开,服务器处理完推理后立即返回结果。就像打电话——你问一句,对方当场回答。
sequenceDiagram
participant C as 客户端
participant S as 服务端
C->>S: POST /inference(图片)
Note over S: 预处理 → 推理 → 后处理
S-->>C: 200 OK(结果)
同步推理的优势是简单——一次请求,一次响应,客户端代码最直观。适合轻量模型(推理延迟 < 100ms)和实时交互场景(如手机拍照识物)。缺点是并发能力受限于单机的推理吞吐:如果一次推理耗时 50ms,单进程理论 QPS 只有 20。100 个用户同时请求,第 100 个要等 5 秒。
异步推理#
异步推理:客户端提交任务后立即获得一个任务 ID,稍后通过这个 ID 查询结果。就像发邮件——你发出去了,对方处理完再通知你。
sequenceDiagram
participant C as 客户端
participant S as 服务端
participant Q as 任务队列
participant W as 推理Worker
C->>S: POST /inference(图片)
S->>Q: 写入任务
S-->>C: 202 Accepted(task_id)
Q->>W: 分发任务
Note over W: 预处理 → 推理 → 后处理
W->>Q: 写入结果
C->>S: GET /inference/{task_id}
S->>Q: 查询结果
S-->>C: 200 OK(结果)
异步推理的核心是任务队列(Task Queue)作为缓冲——请求来了就入队,Worker 按自己的节奏消费。适合大模型(推理时间以秒计)或负载波动大的场景:任务队列削峰填谷,高峰期的请求排队等 Worker 空闲时处理,不会被打回。
特性 |
同步推理 |
异步推理 |
|---|---|---|
调用方式 |
一次请求-响应 |
提交 → 轮询结果 |
连接保持 |
推理期间一直保持 |
提交后立即断开 |
延迟 |
< 100ms(模型必须快) |
可变(秒级可接受) |
并发模型 |
受限于单机推理吞吐 |
队列缓冲 + Worker 横向扩展 |
适合模型 |
轻量模型(ResNet、LeNet) |
大模型(LLM、Stable Diffusion) |
实现复杂度 |
低 |
需要任务队列和状态管理 |
部署拓扑:单机 vs 分布式#
第二个架构决策是部署拓扑——系统由多少个进程组成?
单机模式#
所有功能在一个进程内:HTTP 服务 + 推理引擎 + 模型存储。一个二进制文件启动,零外部依赖。适合开发测试、个人项目、以及流量可控的小规模生产环境。
单机模式的扩展上限就是一台机器的算力。当推理请求超过单机处理能力时,唯一的解法是升级硬件(垂直扩展),但这很快会触及天花板。
分布式模式#
将 HTTP 服务(API Server)和推理执行(Worker)拆分为独立进程。任务队列(Redis、Kafka 或 RabbitMQ)解耦两者。API Server 只负责接收请求和写入任务,Worker 负责实际推理。
分布式模式的核心优势是横向扩展(Horizontal Scaling):推理不够用了?加 Worker 就行。从 1 个 Worker 到 10 个 Worker,不需要改一行代码,不需要重启 API Server。
graph TB
subgraph "单机模式"
A1[客户端] --> B1[API Server + 推理引擎]
B1 --> D1[(数据库)]
end
subgraph "分布式模式"
A2[客户端] --> B2[API Server]
B2 --> F2[任务队列<br/>Redis / Kafka]
F2 --> G2[Worker 1]
F2 --> G3[Worker 2]
F2 --> G4[Worker N]
B2 --> D2[(数据库)]
G2 --> E2[模型存储]
G3 --> E2
G4 --> E2
end
先单机,再分布式
对于个人项目或小团队:
从单机模式开始,最快获得可用服务
当单机吞吐不够时,将推理拆到独立 Worker
当单个 Worker 不够时,横向增加 Worker 数量
不要一开始就搭建完整的分布式系统——你很可能不需要它。过早分布式只会增加运维负担,不会提升任何实际体验。
API 设计:推理服务的接口规范#
无论同步还是异步、单机还是分布式,模型服务都需要一个API 契约来约定客户端和服务端如何通信。
RESTful 端点设计#
一个典型的推理服务 API 包含以下端点:
端点 |
方法 |
用途 |
|---|---|---|
|
GET |
健康检查(负载均衡器探活) |
|
GET |
列出可用模型 |
|
POST |
同步推理 |
|
POST |
异步推理(提交任务) |
|
GET |
查询异步推理结果 |
输入输出格式#
推理 API 的输入输出需要统一的数据格式。常见做法是将张量编码为 JSON 结构,包含形状(shape)、数据类型(dtype)和二进制数据(base64 编码):
{
"dtype": "float32",
"shape": [1, 1, 28, 28],
"data": "<base64-encoded-bytes>"
}
这种设计的优势是自描述性——API 调用者不需要额外查阅文档就能知道数据格式;同时 base64 编码比嵌套 JSON 数组紧凑得多(传输体积通常减少 50% 以上)。
认证与安全#
API 一旦暴露在公网上,就需要安全机制。模型服务的安全通常分三层。
第一层是认证(Authentication)——谁在调用?常见方案是 API Key。客户端在每个请求的 HTTP 头中携带 Authorization: Bearer <key>,服务端验证 Key 的有效性。API Key 不应明文存储——数据库中只存哈希值(如 SHA-256),即使数据库泄露也无法逆向出原始 Key。
第二层是授权(Authorization)——调用者能做什么?基于角色的权限控制(RBAC)为每个 API Key 绑定一组权限。一个只用于推理的 Key 只能调用推理端点,不能管理模型或创建新 Key——这样即使泄露,破坏范围也被限制。
第三层是限流(Rate Limiting)——调用者能调用多频繁?常见算法包括滑动窗口(统计最近 N 秒内的请求数)和令牌桶(以固定速率补充令牌,请求消耗令牌)。限流应基于 API Key 和 IP 的组合,防止单用户通过多 IP 绕过。
安全三问
设计每个 API 端点时,问三个问题:
谁可以调用?(认证)
他们可以做什么?(授权)
他们可以调用多快?(限流)
三个问题都回答了,这个端点的安全设计才算完整。
模型路由#
在分布式模式中,多个 Worker 可能加载不同的模型。当一个推理请求到达时,系统需要决定把它发给谁——这就是模型路由。
路由的核心逻辑:维护一个"模型 → Worker"的映射表。每个 Worker 启动时上报自己加载了哪些模型,并定期发送心跳刷新状态。路由引擎收到请求后:
从请求中获取目标模型名
查询映射表,找到拥有该模型的 Worker 列表
优先选择模型已缓存(热加载)的 Worker,其次选模型文件存在的 Worker
如果多个候选,轮询或负载最低优先
如果没有任何 Worker 拥有该模型,返回错误
Worker 的心跳是路由可靠性的关键。如果 Worker 宕机,心跳停止,映射表中的条目自然过期,新任务会自动分配给其他健康 Worker——无需人工介入。
架构设计原则#
从上述讨论中可以提炼出几条通用的服务架构原则。
简单性优先。单机模式虽然扩展性有限,但它消除了所有分布式复杂度——没有网络分区、没有消息丢失、没有数据一致性问题。在流量增长到真正需要分布式之前,保持简单。
渐进式复杂。从单机到分布式的过渡应该是增量式的——加一个任务队列、拆一个 Worker、再加更多 Worker。每一步都是对前一步瓶颈的精准回应,而不是从一开始就设计一个"万能"的微服务架构。
优雅降级。每个外部依赖(数据库、缓存、任务队列)都应该有回退策略。如果 Redis 不可用,同步推理仍应正常工作;如果数据库不可用,已加载的模型不应受影响。系统的核心功能要在部分组件故障时仍能对外提供服务。
案例:Ferrinx 的架构实现#
Ferrinx(UCS 深度学习社维护的开源推理服务)是上述原则的一个具体实现。它的架构可以直接映射到前文讨论的各个概念:
通用概念 |
Ferrinx 中的实现 |
|---|---|
推理模式 |
同步推理( |
部署拓扑 |
简单模式(SQLite + 单进程)+ 分布式模式(PostgreSQL + Redis + 多 Worker) |
API 认证 |
API Key + SHA-256 哈希存储 + 基于角色的权限 |
限流 |
滑动窗口 + 令牌桶两种算法 |
模型路由 |
Redis 有序集合( |
推理引擎 |
ONNX Runtime Session + LRU 缓存 + 信号量(Semaphore)并发控制 |
优雅降级 |
Redis 不可用时:同步推理正常、异步不可用、认证回退数据库查询 |
Ferrinx 的具体部署操作(编译、启动、模型注册、推理调用)在下一节 部署实践:用Ferrinx服务模型 中详细展开。
下一步#
架构概念已经讲完,接下来用 Ferrinx 实际操作一遍——从编译启动到注册模型、执行推理,把这些概念变成可以跑的命令。下一节 部署实践:用Ferrinx服务模型。