先描述下基本场景: 系统API接口日均调用次数预计1亿次,提供5台服务器。 需要做两种层面的控制: > 单IP、单应用每小时调用次数不超过10000次 > 单应用、单用户、单接口每小时调用次数不超过1000次 要求每次对频控系统的调用的平均响应时间在20ms内。 此外,应用开发者和开放平台所属公司关心调用次数统计数据,如当天某应用所有接口被调用总次数、当天某应用某接口被调用次数、当天某应用用户使用数等。 根据上面,我们可以直接得到系统响应度要求和计算得到系统吞吐量要求,计算公式如下: 80%、40%是指一天中有80%的请求发生在40%的时间内,是粗略的估算值。5是服务器数量。所以得到吞吐量要求为4630tps。前期设计系统时必须参考这些性能指标,后期压测系统时必须根据这些指标设计测试计划。 总结下系统设计需要达成的目标: 请求的响应足够快 能支撑4630tps 占用的CPU、内存等硬件资源不能太夸张(隐性设计目标) A、数据结构设计初步尝试 计数是典型的key-value数据结构。 可能想到的最简单最自然的方式是下面这样的: startTime记录的是第一次调用的发生时刻,lastTime记录的是最近一次调用的发生时刻,它们用来判断是否应该重置计数值count和是否该拒绝调用。startTime和lastTime都只精确到秒级。 为了节省内存,有必要对key和value做特殊设计,最次的方案当然是直接拼接上面各个字段。但这是非常不合理的,我们来简单估算下: 假设应用有10,000个,平均每个应用的用户数为100,000,接口数为50,独立访问IP地址为1,000,000,那么数据项总共为: 那么如果每个数据项节省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、单应用已调用次数和单应用、单接口、单用户已调用次数。这样设计相当于把本来应该存成两项的数据合并到了一个缓存数据项中。 热数据的所谓“展开式”结构是指将上面两个维度的计数分开,即存成类似下面这两种结构: 这种方案有一个小缺陷,那就是小时区间的划分不太符合用户的直觉。用户的直觉是认为真正第一次调用的时刻才是计时起点。为了满足这个需求,可以回归到A节的数据结构上,变成如下: 另外,我后面仔细思考过,不冷不热数据采用Redis Hash和热数据展开存储,是否真的会像我推断的那样展开存储更快? 答案是不一定。因为Redis Hash存储的话,只需要一次Redis访问外加一些解析计算开销,而展开存储需要两次Redis访问。应该不是绝对的,需实际进行测量后才能下结论。 Redis···