服务架构:从单机到分布式#

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 空闲时处理,不会被打回。

同步推理 vs 异步推理#

特性

同步推理

异步推理

调用方式

一次请求-响应

提交 → 轮询结果

连接保持

推理期间一直保持

提交后立即断开

延迟

< 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
    

先单机,再分布式

对于个人项目或小团队:

  1. 从单机模式开始,最快获得可用服务

  2. 当单机吞吐不够时,将推理拆到独立 Worker

  3. 当单个 Worker 不够时,横向增加 Worker 数量

不要一开始就搭建完整的分布式系统——你很可能不需要它。过早分布式只会增加运维负担,不会提升任何实际体验。

API 设计:推理服务的接口规范#

无论同步还是异步、单机还是分布式,模型服务都需要一个API 契约来约定客户端和服务端如何通信。

RESTful 端点设计#

一个典型的推理服务 API 包含以下端点:

推理服务 API 端点#

端点

方法

用途

/health

GET

健康检查(负载均衡器探活)

/v1/models

GET

列出可用模型

/v1/inference/sync

POST

同步推理

/v1/inference

POST

异步推理(提交任务)

/v1/inference/{task_id}

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 端点时,问三个问题:

  1. 谁可以调用?(认证)

  2. 他们可以做什么?(授权)

  3. 他们可以调用多快?(限流)

三个问题都回答了,这个端点的安全设计才算完整。

模型路由#

在分布式模式中,多个 Worker 可能加载不同的模型。当一个推理请求到达时,系统需要决定把它发给谁——这就是模型路由。

路由的核心逻辑:维护一个"模型 → Worker"的映射表。每个 Worker 启动时上报自己加载了哪些模型,并定期发送心跳刷新状态。路由引擎收到请求后:

  1. 从请求中获取目标模型名

  2. 查询映射表,找到拥有该模型的 Worker 列表

  3. 优先选择模型已缓存(热加载)的 Worker,其次选模型文件存在的 Worker

  4. 如果多个候选,轮询或负载最低优先

  5. 如果没有任何 Worker 拥有该模型,返回错误

Worker 的心跳是路由可靠性的关键。如果 Worker 宕机,心跳停止,映射表中的条目自然过期,新任务会自动分配给其他健康 Worker——无需人工介入。

架构设计原则#

从上述讨论中可以提炼出几条通用的服务架构原则。

简单性优先。单机模式虽然扩展性有限,但它消除了所有分布式复杂度——没有网络分区、没有消息丢失、没有数据一致性问题。在流量增长到真正需要分布式之前,保持简单。

渐进式复杂。从单机到分布式的过渡应该是增量式的——加一个任务队列、拆一个 Worker、再加更多 Worker。每一步都是对前一步瓶颈的精准回应,而不是从一开始就设计一个"万能"的微服务架构。

优雅降级。每个外部依赖(数据库、缓存、任务队列)都应该有回退策略。如果 Redis 不可用,同步推理仍应正常工作;如果数据库不可用,已加载的模型不应受影响。系统的核心功能要在部分组件故障时仍能对外提供服务。

案例:Ferrinx 的架构实现#

Ferrinx(UCS 深度学习社维护的开源推理服务)是上述原则的一个具体实现。它的架构可以直接映射到前文讨论的各个概念:

通用概念 → Ferrinx 实现#

通用概念

Ferrinx 中的实现

推理模式

同步推理(/api/v1/inference/sync)+ 异步推理(Redis Streams)

部署拓扑

简单模式(SQLite + 单进程)+ 分布式模式(PostgreSQL + Redis + 多 Worker)

API 认证

API Key + SHA-256 哈希存储 + 基于角色的权限

限流

滑动窗口 + 令牌桶两种算法

模型路由

Redis 有序集合(ferrinx:models:{id}:workers)+ Worker 心跳 TTL 60s

推理引擎

ONNX Runtime Session + LRU 缓存 + 信号量(Semaphore)并发控制

优雅降级

Redis 不可用时:同步推理正常、异步不可用、认证回退数据库查询

Ferrinx 的具体部署操作(编译、启动、模型注册、推理调用)在下一节 部署实践:用Ferrinx服务模型 中详细展开。

下一步#

架构概念已经讲完,接下来用 Ferrinx 实际操作一遍——从编译启动到注册模型、执行推理,把这些概念变成可以跑的命令。下一节 部署实践:用Ferrinx服务模型。

贡献者与修订历史

查看详细修订记录
  • 2af6985 2026-06-14 - Heyan Zhu: feat(model-serving): add quantization and pruning chapter, restructure for general-first narrative
  • 0cdb1e4 2026-04-29 - Heyan Zhu: feat: add model-serving chapter and update related content