本周在做运营平台的限流方案设计,整理如下。
概要
目前运营平台并无限流功能。带来的结果是:
- 有爆款活动上线时,需要提现扩容,防止服务被高并发流量给打挂掉;
- 不同业务活动会相互干扰,特别是当有些活动被恶意攻击的情况下;
我们希望在极端情况下,运营平台的业务功能仍能得到部分保障。
这里有一个问题是为何限流不放在网关层做,毕竟想nginx这样成熟的服务是自带有限流功能的。1是nginx的限流方案相对简单,主要是对uri地址的限流,而我们的需求是需要通过解析header或者body来获取到caller id和对应的act id;2是因为我们的服务存在多个网关,一个是对端上请求的,一个是对小程序请求的,还有一个是对内部服务的; 3是nginx动态配置调整也不方便。
业务现状
- 目前运营活动的请求通常来之三端,分别是NA(ios、android),独立小程序和主小程序;
- 运营平台同时提供了部分基础功能(如赶车闹钟设置),供公司内部其它服务使用;
- 不同的活动通过activity id进行区分;
- 同一个activity id通常会调用不同的方法(method name);
限流配置
{
node_num = 10, -- 服务器节点个数
act_limit = { -- 运营活动限流
{ -- 不同的活动id
actid = "bus_hblb_20210510",
switch = true, -- 开关
act_quota = 100, -- 总的配额,单位为秒
terminal_detail = {
na = { -- NA端
terminal_quota = 30,
method_detail = {
method_name_list = {"method1", "method2"},
method_quota = 20, -- method_name_list共用该配额
},
},
main_applet = { -- 主小程序
terminal_quota = 30,
method_detail = {
method_name_list = {"method1", "method2"},
method_quota = 20,
},
},
independent_applet = { -- 独立小程序
terminal_quota = 40,
method_detail = {
method_name_list = {"method1", "method2"},
method_quota = 20,
},
},
},
},
},
service_limit = { -- 内部服务接入限流
{
caller_id = "taxi", -- 调用方
switch = true,
quota = 100,
method_detail = {
method_name_list = {"method1", "method2"},
method_quota = 20,
},
},
},
}
技术选型
考虑使用golang标准库中就自带了限流算法的实现,即golang.org/x/time/rate。 该限流器是基于Token Bucket(令牌桶)实现的。
简单来说,令牌桶就是想象有一个固定大小的桶,系统会以恒定速率向桶中放Token,桶满则暂时不放。 而用户则从桶中取Token,如果有剩余Token就可以一直取。如果没有剩余Token,则需要等到系统中被放置了Token才行。
配置变更
支持限流配置的动态变更,考虑到限流实现和历史访问相关,需要注意代码实现丢掉历史信息。
统计信息
需要将线上配置的限流参数暴露出来,供业务侧根据活动的优先级调整不同活动的限流配额。
上线部署
对于已经在线上的服务,可以通过历史经验填写限流参数,如果不填写则不做限流;
对于新上线的服务,原则上都需要添加限流信息(作为上线前checklist中的一项)。考虑到新上线活动流量的不确定性,刚上线时可以把限流开关关掉,等活动运行一段时间积累一定数据后再重新打开。
补充说明
1、为何不选用集群限流方案?
答:目前的方案基于单机限流,在每台服务器配置相同且负载均衡做好的情况下,基本能做到和集群限流的参数接近。目前这两个前提基本能满足。如果采用集群整体限流方案,需要添加中心节点,系统复杂度大幅上升。
2、在复杂环境下如何设计限流方案?
答:我们当前使用docker环境,且docker分配的资源配额一致,所以限流方案比较简单。如果存在多个服务部署在同一台物理机的场景,我们就需要考虑到不同服务之间的干扰,毕竟限流的目的就是保护自己不被流量打垮。在这种场景下限流的阈值也会随着服务器负载的变化而变动,限流阈值自适应服务运行场景。