模块 08查看课程目录
00起跑线:两门语言,一台机器01先建立 BEAM 心智模型02Elixir 基础:让数据流过函数03Erlang 基础:读懂 BEAM 的母语04两种语法,一套语义05从裸进程理解并发06从手写循环推导 OTP07监督树与应用08状态、吞吐与背压09分布式与可运维性10Erlang ↔ Elixir 互操作11综合项目:可靠任务调度器
08
容量中高级ElixirErlangBEAM

状态、吞吐与背压

不是每段状态都要进 GenServer,也不是每个并发都要无限展开。

3检查点
5 小时预计完成
为什么先学这个

先修正心智模型,再增加 API

进程是架构工具,不是代码组织工具。把纯计算塞进单个 GenServer 会制造瓶颈;无限 `Task.async` 会制造资源风暴;把所有读写放 ETS 又可能失去不变量。

学完你能做到

可验证的学习目标

  • 能选择纯函数、进程状态或 ETS
  • 能使用有界并发处理工作集
  • 能用 mailbox、延迟与拒绝率判断过载
前置知识
  • 理解 GenServer 与监督树
  • 会写基本并发任务
最小心智模型

先抓住三个词

01

背压

让上游感知下游容量不足的机制。等待、拒绝、限速、有界队列都可能是实现手段。

02

ETS

VM 内的并发表存储,适合高读写共享数据;表所有者退出时默认会删除表。

03

有界并发

同时运行的任务数有上限,让 CPU、连接和内存消耗可预测。

双语代码桥

先对齐协议,再看标点

Elixir 标准工具把有界并发封装得更直接;Erlang 版本展示其本质仍是 monitor、运行集合与待处理队列。

Elixir
urls
|> Task.async_stream(
  &check_url/1,
  max_concurrency: System.schedulers_online() * 2,
  timeout: 3_000,
  on_timeout: :kill_task,
  ordered: false
)
|> Enum.reduce(%{ok: 0, error: 0}, &count_result/2)
Erlang
%% 一个固定大小 worker 池的核心思想
run_bounded(Jobs, Limit) ->
  {Running, Pending} = start_first(Jobs, Limit),
  collect(Running, Pending, Limit).

collect(Running, Pending, Limit) ->
  receive
    {'DOWN', Ref, process, _Pid, Result} ->
      collect(start_next(remove(Ref, Running), Pending, Limit))
  end.
LAB
可运行实验

把 Calculator GenServer 拆掉

约 15–25 分钟

对比纯函数并行调用与单 GenServer 串行计算。计算没有长期状态时,进程只会增加排队。

  1. 01

    实现一个做 CPU 小计算的 Calculator GenServer

  2. 02

    从多个 Task 并发 call 同一个 server

  3. 03

    改成普通函数,重复测量总时间和 mailbox

在终端 / shell 中运行
:timer.tc(fn -> workload.() end)
预期观察
  • 单 server 把原本独立的计算串行化
  • 纯函数更容易利用调用者所在调度器
故意弄坏

把 async_stream 的 `max_concurrency` 调到输入长度,并让每个任务占用连接。记录资源峰值。

这个实验能证明

无共享状态的计算不需要通过单一进程串行化。

这个实验不能证明

一次基准不能证明所有 GenServer 都慢;它们的价值通常是状态所有权与协议,而不是算术吞吐。

快速自测

下面哪种情况最不需要 GenServer?

本章挑战

有界 URL 检查器

检查一组 URL,限制并发、单任务 timeout 和总队列长度;输出成功率、P95 延迟与被拒绝数。

提示 1轻推一下

先固定容量预算,再选 API。

提示 2缩小问题

把 timeout 与 HTTP 错误分开统计。

提示 3接近实现

ordered: false 可避免慢任务阻塞结果消费。

提示 4用验收标准反推

从下面每一条验收标准倒推一个最小测试。若某条无法写成测试,先把表述改成可观察结果。

完成标准
  • 最大并发与待处理量可配置
  • 过载不会无限占用内存
  • 指标能区分失败、超时和拒绝
带走这三句话

复习卡

  1. 1进程用来表达状态所有权、生命周期与故障边界。
  2. 2异步不等于有界;容量限制必须显式。
  3. 3背压的目标是让过载可见、可控、可恢复。
继续核对

本章一手资料

Task.async_streamETS User's Guide