第 06 站展开探险地图
给并发设上限
该用普通函数时就直接计算,需要并发时也要给任务数量画一条线。
突发一万条 URL 检查请求时,怎样限制运行数与等待数,并清楚返回 timeout、失败或 busy?
无限 Task 吃光连接,mailbox 仍继续增长
系统为每个 URL 立刻启动 Task。连接与内存很快耗尽,请求却仍在无形排队,调用者只能等到 timeout。
- 活跃 Task 数与连接数的峰值
- 队列或 mailbox 长度,以及 P95 延迟
- timeout、执行失败与满载拒绝的分类计数
它要解决什么
进程适合保管状态、表达生命周期和隔离错误。普通计算不必都经过 GenServer,无限 Task 也会耗尽连接与内存。工具要按状态和容量来选。
你会做到
能根据有没有长期状态、谁需要读写,选择普通函数、进程状态或 ETS
能给同时运行的任务设上限,并处理 timeout 和等待队列
能从 mailbox 长度、等待时间和拒绝率看出系统是否忙不过来
- 知道 GenServer 怎样保管状态,也知道监督树怎样照看进程
- 用 Task 跑过至少一个可以独立完成的小任务
先看做什么,再看怎么写
Elixir 直接设并发上限;Erlang 轮廓展示 monitor、运行集合与等待队列。
# 同时检查 URL,但并发数不会超过设定上限
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)为什么这样写
为什么既限制运行数,也限制等待数?
并发上限保护正在使用的资源;队列上限阻止等待工作悄悄吃光内存。满载时系统必须承认 busy。
async_stream 能限制同时运行数,却不是完整的持久有界队列。入口容量仍要另行设计。
从 Java、Python、JavaScript 看过来
Java
- 熟悉的起点
- bounded executor、Semaphore 与 BlockingQueue。
- BEAM 在意什么
- BEAM 仍需同时限制 worker、mailbox 或等待队列。
- 别带错直觉
- 虚拟线程便宜,也不会让数据库连接变无限。
Python
- 熟悉的起点
- asyncio.Semaphore、Queue 与 worker pool。
- BEAM 在意什么
- 异步消息很便宜,但 mailbox 默认没有容量上限。
- 别带错直觉
- 协程数量和外部资源容量不是同一个数字。
JavaScript
- 熟悉的起点
- Promise 批次、p-limit 或 stream backpressure。
- BEAM 在意什么
- spawn/cast 让调用者先走,却可能把等待转移到进程信箱。
- 别带错直觉
- 异步不等于有界。
一加一要不要 GenServer
同一批独立计算,一版 call 同一个 GenServer,一版直接调用函数。比较时间和 mailbox。
- 01
实现一个只做 CPU 小计算、没有长期状态的 Calculator GenServer
- 02
从多个 Task 同时 call 这一个 server,记录总时间和 mailbox
- 03
把计算改成普通函数,用同一批输入再测一次
# 运行 workload,并返回耗时微秒数和函数结果
:timer.tc(fn -> workload.() end)- 单个 server 会让原本互不依赖的计算排队执行
- 普通函数可以留在各个调用者中运行,更容易让多个调度器一起工作
把 max_concurrency 调到输入数量,并让每个任务占一个连接。记录资源峰值。
没有共享状态的独立计算,通常不需要先挤进同一个进程排队。
这不能说明所有 GenServer 都慢。它们主要负责状态、生命周期和消息约定。
这段代码里的关键词
背压
下游容量不足时,把信号传回上游。上游可以等待、减速、被拒绝或进入有界队列。
ETS
BEAM 中供多个进程快速读写的表。它仍有所有者;所有者退出时,表默认被删除。
有界并发
同时运行的任务数有上限,使 CPU、连接和内存用量更可控。
这里用了哪些设计模式
Bulkhead
隔离并限制并发 worker 与外部连接的占用。
Bounded Buffer
给等待队列设上限,容量用尽时不再暗中堆积。
Backpressure
通过等待、降速或拒绝,把容量信号传回上游。
下面哪件事最没有必要交给 GenServer?
写一个有界网址检查器
检查一组 URL,限制并发、timeout 和等待数量。输出成功率、P95 耗时和拒绝数。
提示 1先迈一步
先写下最多允许多少任务和等待项,再选择 API。
提示 2再缩小一点
把“等太久”和 HTTP 返回错误分开计数,它们不是同一件事。
提示 3离答案很近了
ordered: false 可以让先完成的任务先交出结果,不必等最慢的那个。
提示 4从终点往回想
先挑一条“过关信号”,为它写一个最小测试。如果电脑看不出结果,就把这句话改成一个真正能观察到的现象。
最大并发数和最多等待数量都能调整
输入突然变多时,内存不会因为无限排队持续增长
结果能分清普通失败、timeout 和因为繁忙被拒绝
记住三句话
- 1
进程适合表达谁保管状态、要活多久,以及出错影响到哪里。
- 2
异步只表示不用原地等待,不代表任务数量已经有上限。
- 3
背压让“忙不过来”被看见,并通过等待、限速、拒绝或降级把它控制住。