文档章节

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

优雅先生
 优雅先生
发布于 2014/09/11 19:52
字数 3093
阅读 1.9W
收藏 139

阿里云携手百名商业领袖、技术大咖,带您一探行进中的数字新基建!>>>

   先描述下基本场景:

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

需要做两种层面的控制:

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

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

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

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

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

频控系统吞吐量(系统每秒能够处理的请求数) 
   = 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小时。

    如果采用的数据结构是:

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调用都不可用。所以频控系统应该增加一个服务开关,以实现服务降级。


2014年11月04日22:59更新:另外API接口调用频率控制比较适合用Storm这种实时事件处理框架来处理。

2015年03月10日22:14更新:

数据结构可以用位运算优化,用一个长整数同时包含count、startTime、endTime信息。

此外如果用Redis等带缓存自动失效的系统可以省略“现在距离1970年1月1日的小时数”字段,每次调用都只需要GET-COMPARE-SET。

© 著作权归作者所有

优雅先生
粉丝 368
博文 36
码字总数 46290
作品 0
浦东
技术主管
私信 提问
加载中

评论(17)

AAABBBQA
AAABBBQA
使用
K:MD5(app_id, ip),V:count、startTime、endTime的优化数据结构
K:MD5(app_id, interface_id, uid),V:count、startTime、endTime的优化数据结构
这种数据结构如何控制60分钟内的访问限制?
JerryLin
JerryLin
NAT过来的请求怎么控制,可能后面有多个正常请求
omeweb
omeweb
用redis是OK的,但是本机的计数也是有必要的
n
newnoder
LuckyWiky
LuckyWiky

引用来自“风行”的评论

没看明白。 在你的设计中,平台 =》 应用 =》 用户 之间什么关系啊?

用户 使用 应用 来间接地使用 平台提供的API,这里会涉及到用户的IP地址吗? 应用 本身是代理 用户来访问平台的API,接口频率应该是控制应用访问API吧,跟用户有什么关系呢?

我觉得楼主应该先说明一下领域模型。
有些应用直接是client side , 就要IP控制了。
优雅先生
优雅先生 博主

引用来自“陈海天”的评论

想问问,为啥设置缓存时间1小时59秒节省内存??谢谢
提早一秒过期Redis就可以提早释放掉一些内存啊。虽然实际不一定能全部立即释放,但应该会有一些内存节省的。
陈海天
陈海天
想问问,为啥设置缓存时间1小时59秒节省内存??谢谢
陈海天
陈海天
稻草鸟人
稻草鸟人
来点实例最好
优雅先生
优雅先生 博主

引用来自“风行”的评论

没看明白。 在你的设计中,平台 =》 应用 =》 用户 之间什么关系啊?

用户 使用 应用 来间接地使用 平台提供的API,这里会涉及到用户的IP地址吗? 应用 本身是代理 用户来访问平台的API,接口频率应该是控制应用访问API吧,跟用户有什么关系呢?

我觉得楼主应该先说明一下领域模型。
某些API是用于获取用户数据的,比如评论、用户个人信息等,所以会有uid。另外所有API都是通过HTTP API开放给外部调用的,可以获取到IP地址,然后做基于IP地址的简单频率控制。
浅谈如何设计一个可用的企业级API网关

在上一篇《浅谈微服务架构下的API网关》文章中, 我们介绍了API网关的概念、优势、应用场景和选型要素, 本文我们将从API网关的架构设计与功能要素两个方面介绍如何设计一个企业级API网关。 ...

garyond
2018/08/05
0
0
WebApi 基于token的多平台身份认证架构设计

1 概述 在存在账号体系的信息系统中,对身份的鉴定是非常重要的事情。 随着移动互联网时代到来,客户端的类型越来越多, 逐渐出现了 一个服务器,N个客户端的格局 。 不同的客户端产生了不同...

osc_svcyn2cd
2018/04/12
4
0
浅谈国内取得突破的AGV几项应用技术

  1、移动机器人车体控制技术   在移动机器人的实际应用中,由于用户所需要运载的货物不同,经常需要更改传感器、驱动器的类型及数量。为了提高机器人研发设计的工作效率,增强产品的核心...

中国机器人
2018/01/02
0
0
Hybrid容器设计之第三方网站

平台化容器API释放 接上文:(阅读本文前,建议阅读前三篇文章先) 浅谈Hybrid技术的设计与实现 浅谈Hybrid技术的设计与实现第二弹 浅谈Hybrid技术的设计与实现第三弹——落地篇 之前设计Hyb...

叶小钗
2017/01/25
0
0
浅谈 PHP 与手机 APP 开发

来源:http://www.thinkphp.cn/topic/5023.html 一、先简单回答两个问题: 1、PHP 可以开发客户端? 答:不可以,因为PHP是脚本语言,是负责完成 B/S架构 或 C/S架构 的S部分,即:服务端的开...

osc_9ipdey7e
2018/05/22
3
0

没有更多内容

加载失败,请刷新页面

加载更多

什么是 PL/SQL? 怎么用?

PL/SQL 1.什么是PL/SQL? PL/SQL(Procedure Language/SQL)是Oracle对sql语言的过程化扩展,指在SQL命令语言中增加了过程处理语句(如分支/条件、循环、变量、类型等),使SQL语言具有过程处...

煌sir
52分钟前
109
0
dayjs时间处理库基本使用

Day.js 是一个轻量的 JavaScript 时间日期处理库,与 Moment.js 的 API 设计保持一致。 本文只介绍了一些常用操作,关于国际化、插件、自定义等高级内容详见官方文档。 其主要特性如下: 与 ...

whoru
55分钟前
11
0
Delphi xe使用TJSONObject解析JSON数据

在Delphi 10 Seattle中重写 “ 使用TJSONObject分析JSON数据 ”。 由于不推荐使用某些方法,因此已对其进行了更改。 要使用TJSONObject,请添加“ System.JSON”。 uses System.JSON; 使用T...

simpower
今天
11
0
树莓派使用 OLED 屏显示图片及文字

树莓派默认是不带显示屏的,如果想要查看系统的一些信息,需要使用电脑登录到树莓派,或者通过 HDMI 连接外接显示器查看。这样做总是有点麻烦,我们可以通过外接一个 OLED 屏来显示一些关键参...

良许Linux
今天
13
0
BIO学习

1. BIO介绍 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程...

steven-黄笑笑
今天
13
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部