一、为什么需要限流?
在分布式系统中,流量就像洪水一样,有时候会突然暴涨。比如,电商大促时,秒杀活动可能瞬间涌入大量请求;或者某个API被恶意刷接口,导致服务器资源被耗尽。这时候,如果没有合理的限流措施,系统可能会直接崩溃,影响正常用户的体验。
限流的核心目标就是控制请求的速率,确保系统在可承受的范围内稳定运行。Erlang作为一种高并发的函数式语言,天生适合构建高可用系统,它的轻量级进程和消息传递机制让限流实现更加优雅。
二、Erlang限流的常见策略
Erlang的限流策略可以大致分为以下几种:
- 计数器算法:简单粗暴,统计单位时间内的请求数,超过阈值就拒绝。
- 漏桶算法:请求像水一样流入桶中,系统以固定速率处理,多余的请求会被丢弃或排队。
- 令牌桶算法:系统按固定速率生成令牌,请求必须拿到令牌才能被处理。
- 基于进程的限流:利用Erlang的进程邮箱机制,控制消息处理速度。
下面我们重点看看令牌桶算法在Erlang中的实现,因为它既能平滑突发流量,又能限制长期速率。
三、令牌桶算法的Erlang实现
我们使用Erlang/OTP来实现一个简单的令牌桶限流模块。
-module(token_bucket).
-behaviour(gen_server).
-export([start_link/1, get_token/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
%% 启动限流器,参数:{容量, 填充速率(令牌/秒)}
start_link({Capacity, Rate}) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, {Capacity, Rate}, []).
%% 尝试获取一个令牌,返回 {ok, remaining} 或 {error, no_token}
get_token(Pid) ->
gen_server:call(Pid, get_token).
%% ====== 内部实现 ======
init({Capacity, Rate}) ->
%% 初始化状态:当前令牌数、上次填充时间、容量、填充速率
Now = erlang:system_time(millisecond),
State = #{tokens => Capacity, last_fill => Now, capacity => Capacity, rate => Rate},
{ok, State}.
handle_call(get_token, _From, State) ->
#{tokens := Tokens, last_fill := LastFill, capacity := Cap, rate := Rate} = State,
Now = erlang:system_time(millisecond),
%% 计算应该补充的令牌数
TimePassed = Now - LastFill,
NewTokens = min(Cap, Tokens + (TimePassed * Rate) div 1000),
UpdatedState = State#{tokens := NewTokens, last_fill := Now},
case NewTokens > 0 of
true ->
%% 有令牌,允许通过
{reply, {ok, NewTokens - 1}, UpdatedState#{tokens := NewTokens - 1}};
false ->
%% 无令牌,拒绝
{reply, {error, no_token}, UpdatedState}
end.
代码解析:
start_link/1初始化令牌桶,设置容量和填充速率。get_token/1是客户端调用的接口,尝试获取令牌。- 每次请求时,会根据时间差计算应该补充的令牌数,避免频繁填充。
- 如果令牌足够,返回
{ok, remaining},否则返回{error, no_token}。
四、结合ETS优化性能
上面的实现使用了 gen_server,适用于单节点限流。但在分布式环境下,可能需要共享限流状态。这时可以用ETS(Erlang Term Storage)来存储令牌桶数据。
-module(dist_token_bucket).
-export([init/0, get_token/1]).
init() ->
%% 创建ETS表,存储限流状态
ets:new(?MODULE, [named_table, public, {write_concurrency, true}]),
ets:insert(?MODULE, {tokens, 10}), % 初始令牌数
ets:insert(?MODULE, {last_fill, erlang:system_time(millisecond)}),
ets:insert(?MODULE, {rate, 1}), % 1 token/sec
ets:insert(?MODULE, {capacity, 10}).
get_token(Requester) ->
[{tokens, Tokens}, {last_fill, LastFill}, {rate, Rate}, {capacity, Cap}] = ets:lookup(?MODULE, tokens),
Now = erlang:system_time(millisecond),
TimePassed = Now - LastFill,
NewTokens = min(Cap, Tokens + (TimePassed * Rate) div 1000),
case NewTokens > 0 of
true ->
ets:insert(?MODULE, [{tokens, NewTokens - 1}, {last_fill, Now}]),
io:format("~p got token, remaining: ~p~n", [Requester, NewTokens - 1]),
ok;
false ->
io:format("~p denied, no tokens left~n", [Requester]),
denied
end.
优化点:
- ETS支持并发读写,适合多进程共享数据。
- 使用
write_concurrency提高写入性能。
五、应用场景与注意事项
适用场景
- API限流:防止恶意刷接口。
- 消息队列消费控制:避免消费者过载。
- 数据库访问限速:防止慢查询拖垮DB。
优缺点
- 优点:
- 平滑突发流量,避免瞬间过载。
- Erlang的轻量级进程模型让限流实现更高效。
- 缺点:
- 单机限流难以应对分布式环境(需结合Redis等外部存储)。
- 精确限流可能增加延迟。
注意事项
- 阈值设置:需根据压测结果调整,避免误杀正常请求。
- 监控与动态调整:限流策略应支持运行时调整参数。
- 失败处理:被限流的请求可以返回友好提示或进入队列重试。
六、总结
Erlang的并发模型让限流实现变得简单高效,无论是单机还是分布式场景,都能找到合适的方案。令牌桶算法是一个平衡了突发流量和长期速率的好选择,结合ETS或外部存储(如Redis)可以进一步优化性能。
在实际项目中,限流只是系统稳定性的一环,还需要结合熔断、降级等策略,才能构建真正高可用的服务。
评论