模块 08查看课程目录
08
容量中高级ElixirErlangBEAM
状态、吞吐与背压
不是每段状态都要进 GenServer,也不是每个并发都要无限展开。
3检查点
5 小时预计完成
为什么先学这个
先修正心智模型,再增加 API
进程是架构工具,不是代码组织工具。把纯计算塞进单个 GenServer 会制造瓶颈;无限 `Task.async` 会制造资源风暴;把所有读写放 ETS 又可能失去不变量。
学完你能做到
可验证的学习目标
- 能选择纯函数、进程状态或 ETS
- 能使用有界并发处理工作集
- 能用 mailbox、延迟与拒绝率判断过载
前置知识
- 理解 GenServer 与监督树
- 会写基本并发任务
最小心智模型
先抓住三个词
背压
让上游感知下游容量不足的机制。等待、拒绝、限速、有界队列都可能是实现手段。
ETS
VM 内的并发表存储,适合高读写共享数据;表所有者退出时默认会删除表。
有界并发
同时运行的任务数有上限,让 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
约 15–25 分钟可运行实验
把 Calculator GenServer 拆掉
对比纯函数并行调用与单 GenServer 串行计算。计算没有长期状态时,进程只会增加排队。
- 01
实现一个做 CPU 小计算的 Calculator GenServer
- 02
从多个 Task 并发 call 同一个 server
- 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进程用来表达状态所有权、生命周期与故障边界。
- 2异步不等于有界;容量限制必须显式。
- 3背压的目标是让过载可见、可控、可恢复。
继续核对