Skip to content

Swift Networking Actor Pipeline 架构与实战

6 min read

很多 iOS 项目的网络层在早期都能工作,但一旦业务复杂度上来,问题会集中爆发:同一请求被重复发送、token 刷新相互踩踏、缓存读写顺序错乱、重试放大故障。代码层面常见“if-else 大串联”,功能都在,秩序全无。

用 Actor 重构网络层不是为了追新,而是为了建立单向、可推理的状态流。真正可靠的 Networking Pipeline 必须把“状态所有权、执行顺序、失败补偿、性能预算”同时纳入设计。

一、网络层最小目标:可预期而不是“偶尔快”

高质量网络层的目标不是单次 benchmark,而是长期稳定:

  1. 同类请求行为一致。
  2. 错误路径可观测、可重试、可回滚。
  3. 峰值流量下不会雪崩。

这三个目标决定了你必须采用管线化架构,而不是散落在业务代码里的 ad-hoc 请求逻辑。

二、推荐拓扑:分层 Actor Pipeline

flowchart LR
    A[Request Builder] --> B[Auth Actor]
    B --> C[RateLimit Actor]
    C --> D[Cache Actor]
    D --> E[Transport Actor URLSession]
    E --> F[Retry Policy Actor]
    F --> G[Decoder Stage]
    G --> H[Response Mapper]
    H --> I[Domain Result]

关键点:

  1. 每一层职责单一,避免“万能网络客户端”。
  2. 有状态阶段(token、配额、缓存元信息)由独立 Actor 托管。
  3. 请求与响应使用 Sendable 模型,杜绝跨任务共享可变引用。

三、核心阶段设计

1)Auth 阶段:解决并发刷新竞态

最典型事故是多个请求同时发现 token 过期,然后并发刷新,最后互相覆盖。

建议模式:

  1. AuthActor 内部维护刷新状态机。
  2. 刷新中时后续请求等待同一刷新结果,而非重复发起。
  3. 刷新失败统一降级并上报,不让调用方各自处理。

2)RateLimit 阶段:避免客户端自我 DOS

移动端在弱网重试场景容易形成请求风暴。用 RateLimitActor 统一控制并发和节流,策略可按 endpoint 细分。

3)Cache 阶段:先定义一致性语义

缓存不是“能命中就行”。必须定义:

  1. 过期策略(TTL、版本、Etag)。
  2. 写入时机(解码前/后)。
  3. 失败回退(网络失败是否可用陈旧缓存)。

语义不先定,缓存越做越像随机行为。

4)Transport 阶段:URLSession 只做传输

URLSession 层不要塞业务判断。它只负责请求发送、超时、TLS、取消透传。业务重试、熔断、映射留在上层策略 Actor。

5)Retry 阶段:重试是策略,不是循环

重试需要输入上下文:错误类型、幂等性、网络状态、预算剩余时间。一个裸 for 循环重试三次,通常等于扩大故障。

四、请求生命周期:顺序必须固定

建议固定请求生命周期:

  1. 构造请求。
  2. 注入鉴权。
  3. 限流准入。
  4. 缓存判定。
  5. 发送传输。
  6. 策略重试。
  7. 解析映射。
  8. 回写缓存。

这个顺序不能随 endpoint 随意变。顺序一旦漂移,排障成本指数上升。

五、工程风险与防线

风险 1:Actor 里混入重 CPU 解码

防线:Actor 只做状态保护。大对象解码放在隔离外,结果再提交。

风险 2:链路中存在不可取消阶段

防线:每阶段都响应取消;网络请求、重试等待、批处理都可中断。

风险 3:重试与幂等冲突

防线:对非幂等请求默认不自动重试,必须显式声明策略。

风险 4:缓存污染

防线:缓存写入前校验 schema 版本、业务状态、请求上下文,避免把错误响应写入热缓存。

六、性能调优:先测 hop 成本,再做批处理

网络管线常见性能损耗不是网络本身,而是内部调度和复制。

优化顺序建议:

  1. 用 Instruments 看 actor hop 与任务切换开销。
  2. 合并细碎阶段,减少无效边界切换。
  3. 对批量请求做聚合发送,减少握手与头部开销。
  4. 解码阶段避免重复中间对象。

可执行预算:

  • 单请求内部 hop 次数 P95 <= 10。
  • 网络层框架开销(不含网络 RTT)P95 <= 15ms。
  • 缓存命中路径额外开销 <= 5ms。

七、测试策略:网络层必须“可演练”

1)并发一致性测试

并发发起相同 endpoint 请求,验证:

  1. token 只刷新一次。
  2. 缓存写入顺序符合预期。
  3. 结果不会互相污染。

2)弱网与超时测试

模拟高延迟、丢包、连接抖动,验证限流和重试不会形成雪崩。

3)取消测试

页面离开或业务终止时,管线各阶段应快速停止并释放资源。

4)回归测试

固定数据集和脚本,对比每次改动前后的成功率、尾延迟和重试占比。

八、可观测设计:每个阶段都要留证据

最低要求是阶段化指标和关联 ID:

  1. request_idtrace_id 贯穿全链路。
  2. 各阶段耗时独立上报。
  3. 重试次数与最终错误类型可查询。
  4. 缓存命中/失效原因可聚合。

没有阶段指标时,问题都会表现成“请求慢了”,但你无法知道慢在 auth、rate limit 还是 decode。

九、排障流程:从阶段耗时入手

flowchart TD
    A[告警: 成功率下降或延迟升高] --> B[按 trace_id 拆阶段耗时]
    B --> C{瓶颈阶段}
    C -- Auth --> D[检查刷新并发与锁等待]
    C -- RateLimit --> E[检查配额策略是否过紧]
    C -- Transport --> F[检查网络错误分布与超时]
    C -- Retry --> G[检查重试风暴与幂等策略]
    C -- Decode --> H[检查数据量与对象分配]
    D --> I[修复后回归压测]
    E --> I
    F --> I
    G --> I
    H --> I

这个流程把“排障经验”变成固定动作,减少对个人记忆依赖。

十、实现参考骨架

actor APIClient {
    private let auth: AuthActor
    private let limiter: RateLimitActor
    private let cache: CacheActor
    private let transport: TransportActor
    private let retry: RetryActor

    func request<T: Decodable & Sendable>(_ req: APIRequest<T>) async throws -> T {
        try Task.checkCancellation()
        let authed = try await auth.inject(req)
        try await limiter.acquire(authed)

        if let hit: T = await cache.read(authed) {
            return hit
        }

        let data = try await retry.execute {
            try await transport.send(authed)
        }

        let model = try req.decode(data)
        await cache.write(authed, value: model)
        return model
    }
}

这只是骨架,真正关键是策略和监控,而不是类名。

十一、治理清单

  1. 新 endpoint 必须声明幂等属性与重试策略。
  2. 新缓存策略必须声明一致性语义。
  3. 所有网络异常都要归类上报,禁止吞错。
  4. 关键阶段必须有可观测指标。
  5. 每次版本发布前跑弱网回归与并发一致性测试。

十二、结语

Networking Actor Pipeline 的价值不是“架构更漂亮”,而是把请求行为固定成可验证流程。只要顺序稳定、状态归属明确、风险有门禁,网络层才能在业务增长时继续可维护。

如果你发现网络问题永远在重复出现,通常不是某个 bug 没修,而是管线模型还没建立。先把模型做对,修复才会积累成长期收益。