Other ·

[转]开放平台API接口调用频率控制系统设计浅谈

先描述下基本场景:

系统API接口日均调用次数预计1亿次,提供5台服务器。

需要做两种层面的控制:

> 单IP、单应用每小时调用次数不超过10000次

> 单应用、单用户、单接口每小时调用次数不超过1000次

要求每次对频控系统的调用的平均响应时间在20ms内。

此外,应用开发者和开放平台所属公司关心调用次数统计数据,如当天某应用所有接口被调用总次数、当天某应用某接口被调用次数、当天某应用用户使用数等。

根据上面,我们可以直接得到系统响应度要求和计算得到系统吞吐量要求,计算公式如下:

    频控系统吞吐量(系统每秒能够处理的请求数) 
    = 80% * 1亿 / (24小时 * 60分钟 * 60秒 * 40% * 5) = 4630tps

80%、40%是指一天中有80%的请求发生在40%的时间内,是粗略的估算值。5是服务器数量。所以得到吞吐量要求为4630tps。前期设计系统时必须参考这些性能指标,后期压测系统时必须根据这些指标设计测试计划。

总结下系统设计需要达成的目标:

请求的响应足够快

能支撑4630tps

占用的CPU、内存等硬件资源不能太夸张(隐性设计目标)

A、数据结构设计初步尝试

计数是典型的key-value数据结构。

可能想到的最简单最自然的方式是下面这样的:

K(app_id, ip) => V(count, startTime, lastTime)
K(app_id, uid, interface_id) => V(count, startTime, lastTime)

startTime记录的是第一次调用的发生时刻,lastTime记录的是最近一次调用的发生时刻,它们用来判断是否应该重置计数值count和是否该拒绝调用。startTime和lastTime都只精确到秒级。

为了节省内存,有必要对key和value做特殊设计,最次的方案当然是直接拼接上面各个字段。但这是非常不合理的,我们来简单估算下:

假设应用有10,000个,平均每个应用的用户数为100,000,接口数为50,独立访问IP地址为1,000,000,那么数据项总共为:

10,000 * 1,000,000 + 10,000 * 100,000 * 50 = 600亿

那么如果每个数据项节省1个字节,能够节省的总数据存储是600G,这是非常可观的。

对于Key,一种更优方案是先拼接Key的字符串,然后MD5得到16位定长字符串作为Key,Key定长的话或许对性能提升也会有一定帮助。

对于Value,count、startTime、lastTime信息不能丢失,那么或许可以考虑下面两种优化方案:

无损压缩Value字符串,比如使用Snappy字符串压缩算法,但压缩和解压缩会带来额外的CPU计算消耗,需要权衡

计数不需要太精确,所以可以牺牲一定精确度换取空间节省。或许我们可以利用CountingBloomFilter?Key需要重新设计为:MD5(app_id, interface_id, 现在距离1970年1月1号的小时数),Value就是CountingBloomFilter数据结构了,每个调用先根据“app_id、interface_id和现在距离1970年1月1号的小时数”计算16位MD5值,然后得到所属的CountingBloomFilter(如果没有就创建),然后每次先检查是否已达到最大插入次数,如果是则直接返回,如果不是才插入。但是我们别忘了一点:CountingBloomFilter支持最大重复插入次数为15,远小于这里的1000次和10000次。所以很遗憾,CountingBloomFilter不适合这种方案。但这是一个很好的起点,Value的数据结构虽然不能用CountingBloomFilter,但或许可以用其他的优化数据结构,可以参考:

http://blog.csdn.net/hguisu/article/details/7856239

还有一篇文章标题大概是用1k内存来排序亿级数组,找不到了。另外频率控制数据结构模型还可以参考“令牌桶算法”,这里不再深入,详情看:http://en.wikipedia.org/wiki/Token_bucket

B、进一步的数据存储和数据结构设计

考虑到性能要求,肯定需要用到Cache,这里打算选用Redis做Cache。再根据上面的估算,数据项总共有600亿,所以不可能把所有数据项全部放到Redis Cache中(假设每个Cache项占100个字节,估算下需要多少内存,嘿嘿)。

所以我这里采用冷热数据分离方案。有这么三类数据:

冷数据存放在MySQL数据库,按照app_id、uid进行水平Shard

不冷不热数据采用Redis Hash结构存储(这种内存结构更加紧凑)

热数据放在另外的Redis库中,并且“展开式”存储以改善访问性能

先简单说下不冷不热数据的Redis Hash结构,Key是app_id,Field是MD5(ip, interface_id, uid, 现在距离1970年1月1号的小时数),Value包括两个计数值,即单IP、单应用已调用次数和单应用、单接口、单用户已调用次数。这样设计相当于把本来应该存成两项的数据合并到了一个缓存数据项中。

热数据的所谓“展开式”结构是指将上面两个维度的计数分开,即存成类似下面这两种结构:

K:MD5(app_id, ip, 现在距离1970年1月1号的小时数),V:count
K:MD5(app_id, interface_id, uid, 现在距离1970年1月1号的小时数),V:count

这种方案有一个小缺陷,那就是小时区间的划分不太符合用户的直觉。用户的直觉是认为真正第一次调用的时刻才是计时起点。为了满足这个需求,可以回归到A节的数据结构上,变成如下:

K:MD5(app_id, ip),V:count、startTime、endTime的优化数据结构
K:MD5(app_id, interface_id, uid),V:count、startTime、endTime的优化数据结构

另外,我后面仔细思考过,不冷不热数据采用Redis Hash和热数据展开存储,是否真的会像我推断的那样展开存储更快?

答案是不一定。因为Redis Hash存储的话,只需要一次Redis访问外加一些解析计算开销,而展开存储需要两次Redis访问。应该不是绝对的,需实际进行测量后才能下结论。

Redis Cache失效时间:

如果采用的数据结构是:

K:MD5(app_id, ip, 现在距离1970年1月1号的小时数),V:count
K:MD5(app_id, interface_id, uid, 现在距离1970年1月1号的小时数),V:count

那么建议设置缓存失效时间为1小时59秒,少1秒钟对用户来说几乎是不可感知的,但可以减少不小的内存估算。

如果采用的数据结构是:

K:MD5(app_id, ip),V:count、startTime、endTime的优化数据结构
K:MD5(app_id, interface_id, uid),V:count、startTime、endTime的优化数据结构

那么建议设置缓存永不失效。另外之前说的“缓存失效导致雪崩”是错误的,那种是因为大量缓存同时失效而导致大量的缓存访问穿透,不太一样。这里更正下。

冷热数据迁移过程:

数据的冷热一直在发生着改变,所以冷热数据之间需要进行迁移。包括下面四种:

热数据中符合不冷不热数据标准的数据移动到不冷不热数据缓存

将不冷不热数据中符合热数据标准的数据迁移到热数据缓存

将不冷不热数据中符合冷数据标准的数据迁移到MySQL数据库

将MySQL数据库中符合不冷不热数据标准的数据迁移到不冷不热数据缓存

判断冷热的标准可以基于下面一种或者几种:

每天计算一次的历史平均每小时调用次数:单应用、单IP和单应用、单接口、单用户

根据应用的用户量级来区分冷热,也就是区分热应用、不热不冷应用和冷应用。比如某个用户量级超过20万,则该应用下的接口调用数据存为热数据。然后用户量级在5万到20万之间的应用的接口调用数据存为不热不冷数据。至于用户量级低于5万的应用的接口调用数据就存为冷数据了。这种是我比较推荐的方案,因为简单科学,并且只需要控制到应用粒度

基于最近50次调用的平均时间间隔来判断数据冷热,也就是对于每一个数据项还要存储一个它最近50次调用的平均时间间隔(可以借鉴下文提到的滑动窗口算法)

比如我采用第二种划分标准。每天跑一次后台脚本将热应用、不热不冷应用和冷应用区分出来,然后缓存到Redis Cache中(应用数不会太多,百万级就了不起了,所以甚至可以存为Local Cache),类似这样:

app_id => 1或者2或者3 # 1表示是热应用,2表示是不热不冷应用,3表示是冷应用

然后API接口调用时,先根据app_id去这个Cache里找出应用的热度级别,然后就知道该访问前面哪一种存储了。当然可能出现访问未命中的情况,在那种情况需要依次访问热缓存数据 => 不冷不热缓存数据 => 冷数据来确定是因为数据存储中还没有相应的Key或者是还没有及时迁移数据到正确的存储,但那种情况毕竟是很少的。

C、借债机制

比如限制某应用某个接口单用户每小时调用次数不能超过1000次,那么是否严格按照1000次的标准来哪?

答案是否。因为用户调用接口的频率可能并不均匀,比如像下面这样:

第1小时 第2小时 第3小时
1200 700 950
如果严格按照1000次标准来的话,那么第1小时内就有200次调用被拒绝。而如果采用简单的借债机制,即如允许超出1000次往上300次,那么在第一小时内的1200次调用都能成功,但欠下200的债,需要在第2小时偿还,第2小时的调用额度就变成800了,以此类推

D、RPC框架选择

频率控制系统是作为一个独立的系统供其他系统调用,所以需要一个高性能的RPC框架来保证能满足并发要求。选择Thrift,并且采用TNonblockingServer非阻塞模式。

E、滑动窗口算法

我最早听到滑动窗口算法是在大学的计算机网络课程上,它是TCP协议用来保证数据帧顺序和限制流量的一个算法。Storm中貌似也用到了滑动窗口算法来限制调用频率。所以或许可以迁移应用到这个系统的设计中。

滑动窗口算法可以参考:http://blog.csdn.net/thisispan/article/details/7545785

F、服务降级

虽然调用频率控制系统是开放平台中的一个重要系统,但我们也不希望在调用频率控制系统不可用的情况下正常的数据API调用都不可用。所以频控系统应该增加一个服务开关,以实现服务降级。

本文转自: 开放平台API接口调用频率控制系统设计浅谈

参与评论